feat: persist prompt render provenance

This commit is contained in:
2026-08-19 06:40:29 +09:00
parent 4208b6228e
commit 50b05051bb
8 changed files with 187 additions and 44 deletions
+3 -1
View File
@@ -53,7 +53,9 @@ pub use segment::{
};
pub use segment_log::{LogEntry, RestoredState, SegmentOrigin, SessionExtension, collect_state};
pub use store::{Store, StoreError};
pub use system_item::{SystemItem, SystemReminder, SystemReminderSource, render_worker_event};
pub use system_item::{
PromptRenderProvenance, SystemItem, SystemReminder, SystemReminderSource, render_worker_event,
};
pub use worker_metadata::{
CombinedStore, FsWorkerStore, WorkerActiveSegmentRef, WorkerAggregateStore, WorkerMetadata,
WorkerMetadataStore, WorkerPeer, WorkerReclaimedChild, WorkerSpawnedChild,
+60 -5
View File
@@ -82,6 +82,7 @@ impl SystemReminder {
SystemReminderSource::TaskInactivity => SystemItem::TaskReminder {
source: self.source,
body: self.rendered_body(),
prompt_provenance: None,
},
}
}
@@ -102,6 +103,16 @@ fn render_system_reminder(body: &str) -> String {
format!("{SYSTEM_REMINDER_OPEN}\n{body}\n{SYSTEM_REMINDER_CLOSE}")
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PromptRenderProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_id: Option<String>,
pub config_revision: u64,
pub source_digest: String,
pub projection_digest: String,
pub logical_name: String,
}
/// One agent-injected system item, tagged by origin.
///
/// Each variant carries the kind-specific raw data clients use for
@@ -124,13 +135,23 @@ pub enum SystemItem {
/// `Method::Notify`. `message` is the raw caller-supplied text;
/// `body` is the wrapped LLM-context form (Worker renders it via
/// `notify_wrapper` at commit time).
Notification { message: String, body: String },
Notification {
message: String,
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt_provenance: Option<PromptRenderProvenance>,
},
/// Lifecycle event reported by a child Worker via `Method::WorkerEvent`.
/// `event` is the typed payload (so the TUI can render per-child
/// banners without re-parsing); `body` is the wrapped LLM-context
/// form (same `notify_wrapper` path as `Notification`).
WorkerEvent { event: WorkerEvent, body: String },
WorkerEvent {
event: WorkerEvent,
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt_provenance: Option<PromptRenderProvenance>,
},
/// `@<path>` file reference resolution. `body` is the rendered
/// LLM-context text (`[File: <path>]\n…` for regular files,
@@ -162,12 +183,18 @@ pub enum SystemItem {
#[serde(default = "default_task_reminder_source")]
source: SystemReminderSource,
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt_provenance: Option<PromptRenderProvenance>,
},
/// Synthetic note inserted after an interrupted turn before the next
/// user input. `body` is the exact LLM-context text explaining that the
/// previous turn was cut short.
Interrupt { body: String },
Interrupt {
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt_provenance: Option<PromptRenderProvenance>,
},
}
impl SystemItem {
@@ -184,7 +211,7 @@ impl SystemItem {
format!("Ignored legacy procedure item: /{slug}")
}
SystemItem::TaskReminder { body, .. } => body.clone(),
SystemItem::Interrupt { body } => body.clone(),
SystemItem::Interrupt { body, .. } => body.clone(),
}
}
@@ -237,11 +264,36 @@ pub fn render_worker_event(event: &WorkerEvent) -> String {
mod tests {
use super::*;
#[test]
fn legacy_prompt_rendered_items_default_missing_provenance() {
let notification: SystemItem = serde_json::from_str(
r#"{"kind":"notification","message":"legacy","body":"legacy body"}"#,
)
.unwrap();
let interrupt: SystemItem =
serde_json::from_str(r#"{"kind":"interrupt","body":"legacy interrupt"}"#).unwrap();
assert!(matches!(
notification,
SystemItem::Notification {
prompt_provenance: None,
..
}
));
assert!(matches!(
interrupt,
SystemItem::Interrupt {
prompt_provenance: None,
..
}
));
}
#[test]
fn notification_history_text_returns_stored_body() {
let item = SystemItem::Notification {
message: "child done".into(),
body: "[Notification]\nchild done\n\n(non-blocking hint…)".into(),
prompt_provenance: None,
};
assert_eq!(
item.history_text(),
@@ -256,6 +308,7 @@ mod tests {
worker_name: "child".into(),
},
body: "[Notification]\npod `child` finished a turn\n\n(non-blocking hint…)".into(),
prompt_provenance: None,
};
assert!(item.history_text().starts_with("[Notification]\n"));
assert!(item.history_text().contains("`child`"));
@@ -292,7 +345,7 @@ mod tests {
fn system_reminder_source_is_retained_in_system_item() {
let item = SystemReminder::task_inactivity("remember tasks").into_system_item();
match item {
SystemItem::TaskReminder { source, body } => {
SystemItem::TaskReminder { source, body, .. } => {
assert_eq!(source, SystemReminderSource::TaskInactivity);
assert_eq!(
body,
@@ -352,6 +405,7 @@ mod tests {
worker_name: "child".into(),
},
body: "[Notification] worker `child` finished a turn".into(),
prompt_provenance: None,
};
let json = serde_json::to_string(&item).unwrap();
let parsed: SystemItem = serde_json::from_str(&json).unwrap();
@@ -359,6 +413,7 @@ mod tests {
SystemItem::WorkerEvent {
event: WorkerEvent::TurnEnded { worker_name },
body,
..
} => {
assert_eq!(worker_name, "child");
assert!(body.contains("`child`"));