refactor: require annotated session log history
This commit is contained in:
@@ -631,8 +631,8 @@ fn protocol_command_status(status: WorkdirCommandStatus) -> ProtocolCommandStatu
|
||||
///
|
||||
/// `Worker::wire_history_persistence` is called separately to wire the
|
||||
/// per-item history commit callback so every assistant / tool item
|
||||
/// landing in `worker.history` becomes a singular `LogEntry::AssistantItem`
|
||||
/// / `ToolResult` commit through the sync writer.
|
||||
/// landing in `worker.history` becomes a singular `LogEntry::AnnotatedAssistantItem`
|
||||
/// / `AnnotatedToolResult` commit through the sync writer.
|
||||
pub(crate) fn wire_event_bridges_on_engine<C, St>(
|
||||
worker: &mut Worker<C, St>,
|
||||
event_tx: &broadcast::Sender<Event>,
|
||||
@@ -1317,7 +1317,7 @@ async fn controller_loop<C, St>(
|
||||
}
|
||||
// Stage the run without a speculative user-message echo.
|
||||
// `Worker::run` validates the input, commits
|
||||
// `LogEntry::UserInput`, and the session-log sink turns that
|
||||
// `LogEntry::AnnotatedUserInput`, and the session-log sink turns that
|
||||
// committed entry into the live `Event::UserMessage`. That
|
||||
// keeps every client ordered against `SegmentStart` replay and
|
||||
// makes persisted history the single source of visible user
|
||||
@@ -1343,7 +1343,7 @@ async fn controller_loop<C, St>(
|
||||
Method::Notify { message, auto_run } => {
|
||||
// Client-side live echo is delivered as `Event::SystemItem`
|
||||
// once the interceptor commits the corresponding
|
||||
// `LogEntry::SystemItem` entry — drained out of the
|
||||
// `LogEntry::AnnotatedSystemItem` entry — drained out of the
|
||||
// notify buffer + broadcast through the sink. No
|
||||
// separate echo here.
|
||||
worker.push_notify(message, auto_run);
|
||||
|
||||
@@ -963,9 +963,12 @@ permission = "read"
|
||||
.append(
|
||||
session_id,
|
||||
segment_id,
|
||||
&LogEntry::UserInput {
|
||||
&LogEntry::AnnotatedUserInput {
|
||||
ts: 1,
|
||||
extensions: vec![],
|
||||
history: vec![crate::session_history::test_logged_history_entry(
|
||||
agen::Item::user_message("verify current Flow conditions"),
|
||||
)],
|
||||
segments: vec![Segment::Text {
|
||||
content: "verify current Flow conditions".into(),
|
||||
}],
|
||||
|
||||
@@ -161,7 +161,7 @@ impl From<HookTurnEndAction> for TurnEndAction {
|
||||
///
|
||||
/// Hook code can use this handle only when the Worker host includes it in an
|
||||
/// event-specific context. The handle queues typed requests; the host drains the
|
||||
/// queue, commits each entry through `LogEntry::SystemItem`, and only then makes
|
||||
/// queue, commits each entry through `LogEntry::AnnotatedSystemItem`, and only then makes
|
||||
/// the matching system message visible to the model. It deliberately exposes no
|
||||
/// raw `agen::Item`, history writer, event sender, `Worker`, `Engine`, or
|
||||
/// notification buffer.
|
||||
|
||||
@@ -539,9 +539,9 @@ mod tests {
|
||||
text: "done".into(),
|
||||
}],
|
||||
};
|
||||
let assistant_entry = LogEntry::AssistantItem {
|
||||
let assistant_entry = LogEntry::AnnotatedAssistantItem {
|
||||
ts: 1,
|
||||
item: assistant_item.clone(),
|
||||
entry: crate::session_history::test_logged_history_entry(assistant_item.clone()),
|
||||
};
|
||||
|
||||
let in_flight_guard = in_flight.snapshot_guard();
|
||||
@@ -593,9 +593,9 @@ mod tests {
|
||||
text: "done".into(),
|
||||
}],
|
||||
};
|
||||
let assistant_entry = LogEntry::AssistantItem {
|
||||
let assistant_entry = LogEntry::AnnotatedAssistantItem {
|
||||
ts: 1,
|
||||
item: assistant_item.clone(),
|
||||
entry: crate::session_history::test_logged_history_entry(assistant_item.clone()),
|
||||
};
|
||||
|
||||
in_flight.clear_for_committed_item_then(&assistant_item, || {
|
||||
@@ -608,7 +608,7 @@ mod tests {
|
||||
|
||||
assert!(matches!(
|
||||
entries_snapshot.as_slice(),
|
||||
[LogEntry::AssistantItem { item, .. }] if item == &assistant_item
|
||||
[LogEntry::AnnotatedAssistantItem { entry, .. }] if entry.item == assistant_item
|
||||
));
|
||||
assert!(in_flight_snapshot.is_empty());
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(crate) struct WorkerInterceptor {
|
||||
pending_notifies: NotifyBuffer,
|
||||
/// Submit-scoped stash of resolver-produced typed system items.
|
||||
/// Drained inside `on_prompt_submit`, committed as
|
||||
/// `LogEntry::SystemItem` entries through `log_writer`, and
|
||||
/// `LogEntry::AnnotatedSystemItem` entries through `log_writer`, and
|
||||
/// returned to the worker as `Item::system_message` via
|
||||
/// `PromptAction::ContinueWith`. Populated by `Worker::run`
|
||||
/// immediately before handing off to the worker.
|
||||
@@ -71,7 +71,7 @@ pub(crate) struct WorkerInterceptor {
|
||||
/// Workspace scope associated with Prompt projection provenance.
|
||||
prompt_workspace_id: Option<String>,
|
||||
/// Type-erased commit handle. The interceptor uses it to commit
|
||||
/// `LogEntry::SystemItem` entries directly (sync) before
|
||||
/// `LogEntry::AnnotatedSystemItem` entries directly (sync) before
|
||||
/// returning the corresponding `Item::system_message`s up to the
|
||||
/// worker. `None` in tests / `Worker::new` paths where no writer is
|
||||
/// attached.
|
||||
@@ -142,7 +142,7 @@ impl WorkerInterceptor {
|
||||
self
|
||||
}
|
||||
|
||||
/// Commit each `SystemItem` as its own `LogEntry::SystemItem`
|
||||
/// Commit each `SystemItem` as its own `LogEntry::AnnotatedSystemItem`
|
||||
/// entry through the attached writer (no-op when no writer is
|
||||
/// wired). Sync — writes complete before the matching
|
||||
/// `Item::system_message`s reach the worker via
|
||||
@@ -540,7 +540,6 @@ mod tests {
|
||||
entry: session_store::LogEntry,
|
||||
) -> Result<(), session_store::StoreError> {
|
||||
let item = match entry {
|
||||
session_store::LogEntry::SystemItem { item, .. } => Some(item),
|
||||
session_store::LogEntry::AnnotatedSystemItem { entry, .. } => Some(entry.item),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! `WorkerInterceptor::pending_history_appends`, which the Engine calls
|
||||
//! at the head of each turn loop iteration. The drain renders each
|
||||
//! pending entry into a typed `SystemItem` (with the `notify_wrapper`
|
||||
//! prompt applied), commits a `LogEntry::SystemItem` per entry through
|
||||
//! prompt applied), commits a `LogEntry::AnnotatedSystemItem` per entry through
|
||||
//! the session-log sink, and returns the corresponding
|
||||
//! `Item::system_message`s for the worker to append to its
|
||||
//! persistent history.
|
||||
|
||||
@@ -29,18 +29,12 @@ pub fn subscribe_worker_protocol_session(handle: &WorkerHandle) -> WorkerProtoco
|
||||
|
||||
pub fn live_log_entry_event(entry: LogEntry) -> Option<Event> {
|
||||
match entry {
|
||||
entry @ (LogEntry::SegmentStart { .. } | LogEntry::AnnotatedSegmentStart { .. }) => {
|
||||
entry @ LogEntry::AnnotatedSegmentStart { .. } => {
|
||||
let session =
|
||||
session_store::public_snapshot::project_current_session_snapshot(&[entry]);
|
||||
Some(Event::SegmentRotated { session })
|
||||
}
|
||||
LogEntry::UserInput { segments, .. } | LogEntry::AnnotatedUserInput { segments, .. } => {
|
||||
Some(Event::UserMessage { segments })
|
||||
}
|
||||
LogEntry::SystemItem { item, .. } => {
|
||||
let value = serde_json::to_value(&item).expect("SystemItem is Serialize");
|
||||
Some(Event::SystemItem { item: value })
|
||||
}
|
||||
LogEntry::AnnotatedUserInput { segments, .. } => Some(Event::UserMessage { segments }),
|
||||
LogEntry::AnnotatedSystemItem { entry, .. } => {
|
||||
let value = serde_json::to_value(&entry.item).expect("SystemItem is Serialize");
|
||||
Some(Event::SystemItem { item: value })
|
||||
@@ -89,9 +83,12 @@ mod tests {
|
||||
#[test]
|
||||
fn user_input_log_entry_maps_to_user_message_event() {
|
||||
let segments = vec![protocol::Segment::text("hello from log")];
|
||||
let event = live_log_entry_event(LogEntry::UserInput {
|
||||
let event = live_log_entry_event(LogEntry::AnnotatedUserInput {
|
||||
ts: session_store::segment_log::now_millis(),
|
||||
extensions: vec![],
|
||||
history: vec![crate::session_history::test_logged_history_entry(
|
||||
agen::Item::user_message("hello from log"),
|
||||
)],
|
||||
segments: segments.clone(),
|
||||
})
|
||||
.expect("UserInput must be live-relevant");
|
||||
|
||||
@@ -77,7 +77,7 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: WorkerHandle)
|
||||
let mut writer = JsonLineWriter::new(writer);
|
||||
|
||||
// Hold the in-flight stream lock while taking the session-log mirror
|
||||
// snapshot. `LogEntry::AssistantItem` is mirror-only for live clients,
|
||||
// snapshot. `LogEntry::AnnotatedAssistantItem` is mirror-only for live clients,
|
||||
// so a finalized assistant block must be observed either as an already
|
||||
// committed entry or as the still-present in-flight block. This lock
|
||||
// order matches `append_entry` (in-flight clear before sink publish) and
|
||||
|
||||
@@ -50,7 +50,7 @@ struct SinkInner {
|
||||
/// Broadcast channel for live entry updates. The same `Sender`
|
||||
/// survives session swaps so existing subscribers keep their
|
||||
/// receiver — they observe the swap as a freshly broadcast
|
||||
/// `LogEntry::SegmentStart` and reset their view accordingly.
|
||||
/// `LogEntry::AnnotatedSegmentStart` and reset their view accordingly.
|
||||
broadcast_tx: broadcast::Sender<LogEntry>,
|
||||
}
|
||||
|
||||
@@ -89,9 +89,9 @@ impl SegmentLogSink {
|
||||
///
|
||||
/// Live broadcast fires for committed session-log entries that
|
||||
/// socket clients must see in log order:
|
||||
/// - `LogEntry::SegmentStart` → `Event::SegmentRotated` on the wire.
|
||||
/// - `LogEntry::UserInput` → `Event::UserMessage`.
|
||||
/// - `LogEntry::SystemItem` → `Event::SystemItem`.
|
||||
/// - `LogEntry::AnnotatedSegmentStart` → `Event::SegmentRotated` on the wire.
|
||||
/// - `LogEntry::AnnotatedUserInput` → `Event::UserMessage`.
|
||||
/// - `LogEntry::AnnotatedSystemItem` → `Event::SystemItem`.
|
||||
/// - `LogEntry::Invoke` → `Event::InvokeStart`.
|
||||
/// Everything else (AssistantItem, ToolResult, TurnEnd,
|
||||
/// RunCompleted, RunErrored, PausedTurnAbandoned, LlmUsage, Extension,
|
||||
@@ -120,11 +120,8 @@ impl SegmentLogSink {
|
||||
fn is_live_relevant(entry: &LogEntry) -> bool {
|
||||
matches!(
|
||||
entry,
|
||||
LogEntry::SegmentStart { .. }
|
||||
| LogEntry::AnnotatedSegmentStart { .. }
|
||||
| LogEntry::UserInput { .. }
|
||||
LogEntry::AnnotatedSegmentStart { .. }
|
||||
| LogEntry::AnnotatedUserInput { .. }
|
||||
| LogEntry::SystemItem { .. }
|
||||
| LogEntry::AnnotatedSystemItem { .. }
|
||||
| LogEntry::Invoke { .. }
|
||||
)
|
||||
@@ -132,7 +129,7 @@ impl SegmentLogSink {
|
||||
|
||||
/// Atomically swap the mirror to `[initial]` and broadcast the new
|
||||
/// session-start entry. Used during compaction / fork: the new
|
||||
/// `LogEntry::SegmentStart` is the first entry of the replacement
|
||||
/// `LogEntry::AnnotatedSegmentStart` is the first entry of the replacement
|
||||
/// session, and existing subscribers transition by replaying it
|
||||
/// like any other live entry.
|
||||
///
|
||||
@@ -234,7 +231,7 @@ mod tests {
|
||||
use session_store::segment_log::now_millis;
|
||||
|
||||
fn session_start() -> LogEntry {
|
||||
LogEntry::SegmentStart {
|
||||
LogEntry::AnnotatedSegmentStart {
|
||||
ts: now_millis(),
|
||||
session_id: uuid::Uuid::nil(),
|
||||
system_prompt: None,
|
||||
@@ -253,9 +250,12 @@ mod tests {
|
||||
}
|
||||
|
||||
fn user_input(text: &str) -> LogEntry {
|
||||
LogEntry::UserInput {
|
||||
LogEntry::AnnotatedUserInput {
|
||||
ts: now_millis(),
|
||||
extensions: vec![],
|
||||
history: vec![crate::session_history::test_logged_history_entry(
|
||||
agen::Item::user_message(text),
|
||||
)],
|
||||
segments: vec![protocol::Segment::Text {
|
||||
content: text.to_owned(),
|
||||
}],
|
||||
@@ -270,7 +270,10 @@ mod tests {
|
||||
|
||||
let (snapshot, mut rx) = sink.subscribe_with_snapshot();
|
||||
assert_eq!(snapshot.len(), 2);
|
||||
assert!(matches!(snapshot[0], LogEntry::SegmentStart { .. }));
|
||||
assert!(matches!(
|
||||
snapshot[0],
|
||||
LogEntry::AnnotatedSegmentStart { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
snapshot[1],
|
||||
LogEntry::TurnEnd { turn_count: 1, .. }
|
||||
@@ -279,13 +282,15 @@ mod tests {
|
||||
}
|
||||
|
||||
fn notification_entry(text: &str) -> LogEntry {
|
||||
LogEntry::SystemItem {
|
||||
LogEntry::AnnotatedSystemItem {
|
||||
ts: now_millis(),
|
||||
item: session_store::SystemItem::Notification {
|
||||
message: text.to_owned(),
|
||||
body: format!("[Notification] {text}"),
|
||||
prompt_provenance: None,
|
||||
},
|
||||
entry: crate::session_history::test_logged_system_entry(
|
||||
session_store::SystemItem::Notification {
|
||||
message: text.to_owned(),
|
||||
body: format!("[Notification] {text}"),
|
||||
prompt_provenance: None,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +310,7 @@ mod tests {
|
||||
// for Event::UserMessage.
|
||||
sink.publish(user_input("hi from log"));
|
||||
match rx.try_recv() {
|
||||
Ok(LogEntry::UserInput { segments, .. }) => {
|
||||
Ok(LogEntry::AnnotatedUserInput { segments, .. }) => {
|
||||
assert_eq!(segments.len(), 1);
|
||||
}
|
||||
other => panic!("expected UserInput, got {other:?}"),
|
||||
@@ -314,7 +319,7 @@ mod tests {
|
||||
// SystemItem is live-relevant.
|
||||
sink.publish(notification_entry("hi"));
|
||||
match rx.try_recv() {
|
||||
Ok(LogEntry::SystemItem { .. }) => {}
|
||||
Ok(LogEntry::AnnotatedSystemItem { .. }) => {}
|
||||
other => panic!("expected SystemItem, got {other:?}"),
|
||||
}
|
||||
|
||||
@@ -332,7 +337,7 @@ mod tests {
|
||||
|
||||
assert_eq!(snapshot.len(), 1);
|
||||
match rx.try_recv() {
|
||||
Ok(LogEntry::SystemItem { .. }) => {}
|
||||
Ok(LogEntry::AnnotatedSystemItem { .. }) => {}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
assert!(rx.try_recv().is_err());
|
||||
@@ -348,13 +353,16 @@ mod tests {
|
||||
sink.reset_with_initial(session_start());
|
||||
|
||||
match rx.try_recv() {
|
||||
Ok(LogEntry::SegmentStart { .. }) => {}
|
||||
Ok(LogEntry::AnnotatedSegmentStart { .. }) => {}
|
||||
other => panic!("expected SegmentStart broadcast, got {other:?}"),
|
||||
}
|
||||
|
||||
let (post_snapshot, _) = sink.subscribe_with_snapshot();
|
||||
assert_eq!(post_snapshot.len(), 1);
|
||||
assert!(matches!(post_snapshot[0], LogEntry::SegmentStart { .. }));
|
||||
assert!(matches!(
|
||||
post_snapshot[0],
|
||||
LogEntry::AnnotatedSegmentStart { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
//! retained only as explicit `LegacyUnknown` entries.
|
||||
|
||||
use agen::{HistoryEntry, Item};
|
||||
use protocol::Segment;
|
||||
use session_store::{
|
||||
LogEntry, LoggedHistoryDerivation, LoggedHistoryEntry, LoggedSessionHistoryEntryId,
|
||||
LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin, LoggedWorkerSubject, SegmentId,
|
||||
@@ -18,6 +17,32 @@ pub type WorkerHistoryProvenance = LoggedSessionHistoryOrigin;
|
||||
pub type SessionHistoryDerivation = LoggedHistoryDerivation;
|
||||
pub type WorkerSubjectSnapshot = LoggedWorkerSubject;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_logged_history_entry(item: impl Into<Item>) -> LoggedHistoryEntry {
|
||||
LoggedHistoryEntry {
|
||||
item: session_store::LoggedItem::from(item.into()),
|
||||
metadata: LoggedSessionHistoryMetadata {
|
||||
entry_id: LoggedSessionHistoryEntryId::new(),
|
||||
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
|
||||
derivation: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_logged_system_entry(
|
||||
item: session_store::SystemItem,
|
||||
) -> session_store::LoggedSystemHistoryEntry {
|
||||
session_store::LoggedSystemHistoryEntry {
|
||||
item,
|
||||
metadata: LoggedSessionHistoryMetadata {
|
||||
entry_id: LoggedSessionHistoryEntryId::new(),
|
||||
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
|
||||
derivation: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn worker_subject(session_id: SessionId) -> WorkerSubjectSnapshot {
|
||||
WorkerSubjectSnapshot {
|
||||
workspace_id: None,
|
||||
@@ -53,10 +78,6 @@ pub(crate) fn to_logged_history_entry(
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_entry(item: Item) -> HistoryEntry<SessionHistoryMetadata> {
|
||||
HistoryEntry::new(item, SessionHistoryMetadata::legacy_unknown())
|
||||
}
|
||||
|
||||
fn from_logged(entry: &LoggedHistoryEntry) -> HistoryEntry<SessionHistoryMetadata> {
|
||||
HistoryEntry::new(Item::from(entry.item.clone()), entry.metadata.clone())
|
||||
}
|
||||
@@ -74,32 +95,15 @@ pub(crate) fn restore_history_entries(
|
||||
LogEntry::AnnotatedSegmentStart { history: seed, .. } => {
|
||||
history = seed.iter().map(from_logged).collect();
|
||||
}
|
||||
LogEntry::SegmentStart { history: seed, .. } => {
|
||||
history = seed
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Item::from)
|
||||
.map(legacy_entry)
|
||||
.collect();
|
||||
}
|
||||
LogEntry::AnnotatedUserInput { history: input, .. } => {
|
||||
history.extend(input.iter().map(from_logged))
|
||||
}
|
||||
LogEntry::UserInput { segments, .. } => history.push(legacy_entry(Item::user_message(
|
||||
Segment::flatten_to_text(segments),
|
||||
))),
|
||||
LogEntry::AnnotatedAssistantItem { entry, .. }
|
||||
| LogEntry::AnnotatedToolResult { entry, .. } => history.push(from_logged(entry)),
|
||||
LogEntry::AssistantItem { item, .. } | LogEntry::ToolResult { item, .. } => {
|
||||
history.push(legacy_entry(Item::from(item.clone())));
|
||||
}
|
||||
LogEntry::AnnotatedSystemItem { entry, .. } => history.push(HistoryEntry::new(
|
||||
entry.item.to_history_item(),
|
||||
entry.metadata.clone(),
|
||||
)),
|
||||
LogEntry::SystemItem { item, .. } => {
|
||||
history.push(legacy_entry(item.to_history_item()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -110,23 +114,9 @@ pub(crate) fn restore_history_entries(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use agen::llm_client::RequestConfig;
|
||||
use protocol::Segment;
|
||||
use session_store::LogEntry;
|
||||
|
||||
#[test]
|
||||
fn legacy_user_role_is_not_inferred_as_human_authority() {
|
||||
let entries = vec![LogEntry::UserInput {
|
||||
ts: 1,
|
||||
segments: vec![Segment::text("legacy")],
|
||||
extensions: Vec::new(),
|
||||
}];
|
||||
let restored =
|
||||
restore_history_entries(SessionId::now_v7(), SegmentId::now_v7(), &entries).unwrap();
|
||||
assert!(matches!(
|
||||
restored[0].annotation.origin,
|
||||
WorkerHistoryProvenance::LegacyUnknown
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_flow_and_unknown_caller_input_round_trip_without_role_inference() {
|
||||
let session_id = SessionId::now_v7();
|
||||
|
||||
@@ -112,8 +112,12 @@ impl InternalSpawnedWorkerRecord {
|
||||
fn stop_summary(&self) -> SubWorkerStopSummary {
|
||||
let mut counts = BTreeMap::<String, u64>::new();
|
||||
for entry in self.session.entries() {
|
||||
if let session_store::LogEntry::AssistantItem {
|
||||
item: LoggedItem::ToolCall { name, .. },
|
||||
if let session_store::LogEntry::AnnotatedAssistantItem {
|
||||
entry:
|
||||
session_store::LoggedHistoryEntry {
|
||||
item: LoggedItem::ToolCall { name, .. },
|
||||
..
|
||||
},
|
||||
..
|
||||
} = entry
|
||||
{
|
||||
@@ -1024,11 +1028,16 @@ mod tests {
|
||||
&& worker.parent_session_id.as_deref() == Some("parent-session")
|
||||
&& matches!(*event, Event::TextDone { ref text } if text == "answer")
|
||||
));
|
||||
record.session.publish_test_entry(LogEntry::UserInput {
|
||||
ts: 1,
|
||||
segments: vec![protocol::Segment::text("question")],
|
||||
extensions: Vec::new(),
|
||||
});
|
||||
record
|
||||
.session
|
||||
.publish_test_entry(LogEntry::AnnotatedUserInput {
|
||||
ts: 1,
|
||||
segments: vec![protocol::Segment::text("question")],
|
||||
history: vec![crate::session_history::test_logged_history_entry(
|
||||
agen::Item::user_message("question"),
|
||||
)],
|
||||
extensions: Vec::new(),
|
||||
});
|
||||
let committed = tokio::time::timeout(Duration::from_secs(1), parent_rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1159,14 +1168,18 @@ mod tests {
|
||||
let (mut record, _events) = record("child", InternalWorkerVisibility::ParentClient).await;
|
||||
record.change_tracker = Some(tracker);
|
||||
for (index, name) in ["Read", "Read", "Grep"].into_iter().enumerate() {
|
||||
record.session.publish_test_entry(LogEntry::AssistantItem {
|
||||
ts: index as u64,
|
||||
item: LoggedItem::ToolCall {
|
||||
call_id: format!("call-{index}"),
|
||||
name: name.to_string(),
|
||||
arguments: "{}".to_string(),
|
||||
},
|
||||
});
|
||||
record
|
||||
.session
|
||||
.publish_test_entry(LogEntry::AnnotatedAssistantItem {
|
||||
ts: index as u64,
|
||||
entry: crate::session_history::test_logged_history_entry(
|
||||
LoggedItem::ToolCall {
|
||||
call_id: format!("call-{index}"),
|
||||
name: name.to_string(),
|
||||
arguments: "{}".to_string(),
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
registry.start_protocol_forwarding(record.clone());
|
||||
install_record(®istry, record);
|
||||
|
||||
+51
-42
@@ -900,7 +900,6 @@ where
|
||||
self.state.increment_entries();
|
||||
if let Some(in_flight) = &self.in_flight {
|
||||
let committed_item = match &entry {
|
||||
LogEntry::AssistantItem { item, .. } => Some(item.clone()),
|
||||
LogEntry::AnnotatedAssistantItem { entry, .. } => Some(entry.item.clone()),
|
||||
_ => None,
|
||||
};
|
||||
@@ -1207,11 +1206,11 @@ pub struct Worker<C: LlmClient, St: Store> {
|
||||
memory_task: Option<JoinHandle<()>>,
|
||||
/// Typed user submissions in submit order. K-th entry corresponds to
|
||||
/// the K-th `Item::user_message` in `worker.history()` (modulo seed
|
||||
/// history loaded via `SegmentStart.history`, whose original segments
|
||||
/// history loaded via `AnnotatedSegmentStart.history`, whose original segments
|
||||
/// are not preserved). Populated from log on `restore_from_manifest`,
|
||||
/// appended after `save_user_input` on each `run`. Pre-`Event::Snapshot`
|
||||
/// this fed `WorkerSharedState.user_segments`; the new wire format
|
||||
/// carries typed atoms via `LogEntry::UserInput { segments }` so
|
||||
/// carries typed atoms via `LogEntry::AnnotatedUserInput { segments }` so
|
||||
/// this remains purely an in-memory tracker for compact alignment.
|
||||
user_segments: Vec<Vec<Segment>>,
|
||||
/// Worker-side session-log mirror + broadcast sink. Populated alongside
|
||||
@@ -1221,7 +1220,8 @@ pub struct Worker<C: LlmClient, St: Store> {
|
||||
sink: SegmentLogSink,
|
||||
/// `true` once `wire_history_persistence` has installed the
|
||||
/// `Engine::on_history_append` callback that commits each appended
|
||||
/// item as a singular `LogEntry::AssistantItem` / `ToolResult`
|
||||
/// item as a singular `LogEntry::AnnotatedAssistantItem` /
|
||||
/// `AnnotatedToolResult`
|
||||
/// directly through the writer. Tests that drive `Worker::new` without
|
||||
/// going through the controller leave this `false`; `persist_turn`
|
||||
/// then walks the post-`history_before` slice inline so entries
|
||||
@@ -1345,15 +1345,16 @@ impl<C: LlmClient + 'static, St: Store + Clone + 'static> Worker<C, St> {
|
||||
}
|
||||
|
||||
/// Wire `Engine::on_history_append` to commit each appended item
|
||||
/// directly as a singular `LogEntry::AssistantItem` / `ToolResult`
|
||||
/// directly as a singular `LogEntry::AnnotatedAssistantItem` /
|
||||
/// `AnnotatedToolResult`
|
||||
/// through the writer. The controller calls this once per spawned
|
||||
/// Worker after the worker is built; tests that drive `Worker::new` may
|
||||
/// opt in to the same wiring or leave it off (in which case
|
||||
/// `persist_turn`'s inline fallback writes entries at turn end).
|
||||
///
|
||||
/// `user_message` items are skipped because they are committed
|
||||
/// up-front via `commit_entry(LogEntry::UserInput { segments })`.
|
||||
/// `role:system` items are committed as typed `LogEntry::SystemItem`
|
||||
/// up-front via `commit_entry(LogEntry::AnnotatedUserInput { segments })`.
|
||||
/// `role:system` items are committed as typed `LogEntry::AnnotatedSystemItem`
|
||||
/// entries by their producers (for example `WorkerInterceptor` and
|
||||
/// interrupted-turn prep) before they reach the worker's history, so this
|
||||
/// callback would otherwise double-write them.
|
||||
@@ -1941,8 +1942,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
||||
}
|
||||
|
||||
let input = match entries.get(target.user_input_entry_index) {
|
||||
Some(LogEntry::UserInput { segments, .. })
|
||||
| Some(LogEntry::AnnotatedUserInput { segments, .. }) => segments.clone(),
|
||||
Some(LogEntry::AnnotatedUserInput { segments, .. }) => segments.clone(),
|
||||
_ => {
|
||||
return Err(RewindError::Invalid(
|
||||
"rewind target is no longer a user message".into(),
|
||||
@@ -2081,8 +2081,8 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
||||
/// Cheap clone via `Option<Clone>`.
|
||||
/// Snapshot of the typed user segments tracked alongside worker
|
||||
/// history. The K-th entry corresponds to the K-th `Item::user_message`
|
||||
/// derived from `LogEntry::UserInput` entries (post-compaction); seed
|
||||
/// history loaded via `SegmentStart.history` does not contribute,
|
||||
/// derived from `LogEntry::AnnotatedUserInput` entries (post-compaction); seed
|
||||
/// history loaded via `AnnotatedSegmentStart.history` does not contribute,
|
||||
/// which is acceptable because the original segments are unrecoverable.
|
||||
pub fn user_segments(&self) -> &[Vec<Segment>] {
|
||||
&self.user_segments
|
||||
@@ -3533,12 +3533,13 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
||||
// slice from `history_before` inline so the test's
|
||||
// `restore`-style assertions still see entries on disk.
|
||||
if !self.history_persistence_wired {
|
||||
let new_items: Vec<Item> = self.session.history().entries()[history_before..]
|
||||
let new_entries: Vec<_> = self.session.history().entries()[history_before..]
|
||||
.iter()
|
||||
.map(|entry| entry.item.clone())
|
||||
.map(to_logged_history_entry)
|
||||
.collect();
|
||||
let ts = segment_log::now_millis();
|
||||
for item in &new_items {
|
||||
for history_entry in new_entries {
|
||||
let item = Item::from(history_entry.item.clone());
|
||||
if item.is_user_message() {
|
||||
continue;
|
||||
}
|
||||
@@ -3551,7 +3552,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let entry = session_store::classify_history_item(item, ts);
|
||||
let entry = session_store::classify_logged_history_entry(history_entry, ts);
|
||||
self.commit_entry(entry)?;
|
||||
}
|
||||
}
|
||||
@@ -6257,8 +6258,7 @@ fn build_rewind_targets(segment_id: uuid::Uuid, entries: &[LogEntry]) -> Vec<Rew
|
||||
let mut targets = Vec::new();
|
||||
for (entry_index, entry) in entries.iter().enumerate() {
|
||||
let (segments, ts) = match entry {
|
||||
LogEntry::UserInput { segments, ts, .. }
|
||||
| LogEntry::AnnotatedUserInput { segments, ts, .. } => (segments, ts),
|
||||
LogEntry::AnnotatedUserInput { segments, ts, .. } => (segments, ts),
|
||||
_ => continue,
|
||||
};
|
||||
turn_index += 1;
|
||||
@@ -6300,8 +6300,7 @@ fn rewind_truncate_entries(entries: &[LogEntry], user_input_entry_index: usize)
|
||||
|
||||
fn suffix_has_tool_side_effects(entries: &[LogEntry]) -> bool {
|
||||
entries.iter().any(|entry| match entry {
|
||||
LogEntry::ToolResult { .. } | LogEntry::AnnotatedToolResult { .. } => true,
|
||||
LogEntry::AssistantItem { item, .. } => logged_item_is_tool_call(item),
|
||||
LogEntry::AnnotatedToolResult { .. } => true,
|
||||
LogEntry::AnnotatedAssistantItem { entry, .. } => logged_item_is_tool_call(&entry.item),
|
||||
_ => false,
|
||||
})
|
||||
@@ -7636,7 +7635,7 @@ mod build_summary_prompt_tests {
|
||||
);
|
||||
assert!(checkpoint.is_none());
|
||||
|
||||
let mut replacement_entries = vec![LogEntry::SegmentStart {
|
||||
let mut replacement_entries = vec![LogEntry::AnnotatedSegmentStart {
|
||||
ts: segment_log::now_millis(),
|
||||
session_id: uuid::Uuid::nil(),
|
||||
system_prompt: None,
|
||||
@@ -7964,9 +7963,12 @@ mod build_summary_prompt_tests {
|
||||
);
|
||||
append_test_entry(
|
||||
worker,
|
||||
LogEntry::UserInput {
|
||||
LogEntry::AnnotatedUserInput {
|
||||
ts: ts + 1,
|
||||
extensions: vec![],
|
||||
history: vec![crate::session_history::test_logged_history_entry(
|
||||
Item::user_message(text),
|
||||
)],
|
||||
segments: vec![text_segment(text)],
|
||||
},
|
||||
);
|
||||
@@ -7986,16 +7988,18 @@ mod build_summary_prompt_tests {
|
||||
append_user_turn(&worker, 20, "second message");
|
||||
append_test_entry(
|
||||
&worker,
|
||||
LogEntry::ToolResult {
|
||||
LogEntry::AnnotatedToolResult {
|
||||
ts: 30,
|
||||
item: session_store::LoggedItem::ToolResult {
|
||||
call_id: "call-1".into(),
|
||||
summary: "wrote a file".into(),
|
||||
content: None,
|
||||
attachments: Vec::new(),
|
||||
disposition: Default::default(),
|
||||
is_error: false,
|
||||
},
|
||||
entry: crate::session_history::test_logged_history_entry(
|
||||
session_store::LoggedItem::ToolResult {
|
||||
call_id: "call-1".into(),
|
||||
summary: "wrote a file".into(),
|
||||
content: None,
|
||||
attachments: Vec::new(),
|
||||
disposition: Default::default(),
|
||||
is_error: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8029,16 +8033,18 @@ mod build_summary_prompt_tests {
|
||||
append_user_turn(&worker, 20, "second message");
|
||||
append_test_entry(
|
||||
&worker,
|
||||
LogEntry::ToolResult {
|
||||
LogEntry::AnnotatedToolResult {
|
||||
ts: 30,
|
||||
item: session_store::LoggedItem::ToolResult {
|
||||
call_id: "call-1".into(),
|
||||
summary: "wrote a file".into(),
|
||||
content: None,
|
||||
attachments: Vec::new(),
|
||||
disposition: Default::default(),
|
||||
is_error: false,
|
||||
},
|
||||
entry: crate::session_history::test_logged_history_entry(
|
||||
session_store::LoggedItem::ToolResult {
|
||||
call_id: "call-1".into(),
|
||||
summary: "wrote a file".into(),
|
||||
content: None,
|
||||
attachments: Vec::new(),
|
||||
disposition: Default::default(),
|
||||
is_error: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
let (head_entries, targets) = worker.list_rewind_targets().unwrap();
|
||||
@@ -8456,9 +8462,9 @@ mod build_summary_prompt_tests {
|
||||
worker.wire_history_persistence();
|
||||
let dangling_call = Item::tool_call("call-1", "SideEffect", "{}");
|
||||
worker
|
||||
.commit_entry(LogEntry::AssistantItem {
|
||||
.commit_entry(LogEntry::AnnotatedAssistantItem {
|
||||
ts: segment_log::now_millis(),
|
||||
item: dangling_call.clone().into(),
|
||||
entry: crate::session_history::test_logged_history_entry(dangling_call.clone()),
|
||||
})
|
||||
.unwrap();
|
||||
worker.set_history_for_test(vec![dangling_call]);
|
||||
@@ -8863,9 +8869,12 @@ mod build_summary_prompt_tests {
|
||||
);
|
||||
worker.set_history_for_test(vec![evidence.clone()]);
|
||||
worker
|
||||
.commit_entry(LogEntry::UserInput {
|
||||
.commit_entry(LogEntry::AnnotatedUserInput {
|
||||
ts: segment_log::now_millis(),
|
||||
extensions: vec![],
|
||||
history: vec![crate::session_history::test_logged_history_entry(
|
||||
evidence.clone(),
|
||||
)],
|
||||
segments: vec![text_segment(
|
||||
"The cancellation regression must leave this evidence available for retry.",
|
||||
)],
|
||||
|
||||
@@ -25,6 +25,17 @@ use worker::{Worker, WorkerController};
|
||||
|
||||
type TestStore = CombinedStore<FsStore, FsWorkerStore>;
|
||||
|
||||
fn annotated(item: Item) -> session_store::LoggedHistoryEntry {
|
||||
session_store::LoggedHistoryEntry {
|
||||
item: session_store::LoggedItem::from(item),
|
||||
metadata: session_store::LoggedSessionHistoryMetadata {
|
||||
entry_id: session_store::LoggedSessionHistoryEntryId::new(),
|
||||
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
|
||||
derivation: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockClient {
|
||||
responses: Arc<Vec<Vec<LlmEvent>>>,
|
||||
@@ -210,7 +221,6 @@ fn system_texts_in_sink_session_start(
|
||||
.into_iter()
|
||||
.map(|entry| entry.item)
|
||||
.collect::<Vec<_>>(),
|
||||
session_store::LogEntry::SegmentStart { history, .. } => history,
|
||||
_ => continue,
|
||||
};
|
||||
return history
|
||||
@@ -310,17 +320,14 @@ permission = "write"
|
||||
// Simulate a foreign writer appending to the same segment. This bumps
|
||||
// the on-disk entry count past the Worker's own append tally without
|
||||
// updating the Worker's `entries_written`.
|
||||
store
|
||||
.append(
|
||||
session_id,
|
||||
source_segment_id,
|
||||
&LogEntry::UserInput {
|
||||
ts: 9999,
|
||||
segments: vec![protocol::Segment::text("interloper")],
|
||||
extensions: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
session_store::save_user_input(
|
||||
&store,
|
||||
session_id,
|
||||
source_segment_id,
|
||||
vec![protocol::Segment::text("interloper")],
|
||||
vec![annotated(Item::user_message("interloper"))],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Next run triggers ensure_segment_head, which sees the drift.
|
||||
worker.run_text("second").await.unwrap();
|
||||
@@ -348,11 +355,6 @@ permission = "write"
|
||||
session_id: seg_session,
|
||||
forked_from: Some(origin),
|
||||
..
|
||||
}
|
||||
| LogEntry::SegmentStart {
|
||||
session_id: seg_session,
|
||||
forked_from: Some(origin),
|
||||
..
|
||||
} => {
|
||||
assert_eq!(*seg_session, session_id);
|
||||
assert_eq!(origin.segment_id, source_segment_id);
|
||||
@@ -366,7 +368,7 @@ permission = "write"
|
||||
assert_eq!(source_after.len(), source_len_before + 1);
|
||||
assert!(matches!(
|
||||
source_after.last(),
|
||||
Some(LogEntry::UserInput { .. })
|
||||
Some(LogEntry::AnnotatedUserInput { .. })
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -35,29 +35,16 @@ fn history_from_sink(handle: &WorkerHandle) -> Vec<Item> {
|
||||
LogEntry::AnnotatedSegmentStart { history, .. } => {
|
||||
items.extend(history.into_iter().map(|entry| Item::from(entry.item)));
|
||||
}
|
||||
LogEntry::SegmentStart { history, .. } => {
|
||||
items.extend(history.into_iter().map(Item::from));
|
||||
}
|
||||
LogEntry::AnnotatedUserInput { history, .. } => {
|
||||
items.extend(history.into_iter().map(|entry| Item::from(entry.item)));
|
||||
}
|
||||
LogEntry::UserInput { segments, .. } => {
|
||||
let text = protocol::Segment::flatten_to_text(&segments);
|
||||
items.push(Item::user_message(text));
|
||||
}
|
||||
LogEntry::AnnotatedAssistantItem { entry, .. }
|
||||
| LogEntry::AnnotatedToolResult { entry, .. } => {
|
||||
items.push(Item::from(entry.item));
|
||||
}
|
||||
LogEntry::AssistantItem { item, .. } | LogEntry::ToolResult { item, .. } => {
|
||||
items.push(Item::from(item));
|
||||
}
|
||||
LogEntry::AnnotatedSystemItem { entry, .. } => {
|
||||
items.push(entry.item.to_history_item());
|
||||
}
|
||||
LogEntry::SystemItem { item, .. } => {
|
||||
items.push(item.to_history_item());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +54,6 @@ fn history_from_sink(handle: &WorkerHandle) -> Vec<Item> {
|
||||
fn system_item(entry: &LogEntry) -> Option<&session_store::SystemItem> {
|
||||
match entry {
|
||||
LogEntry::AnnotatedSystemItem { entry, .. } => Some(&entry.item),
|
||||
LogEntry::SystemItem { item, .. } => Some(item),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1095,7 +1081,7 @@ async fn run_with_paste_segment_inlines_content_and_emits_typed_user_message() {
|
||||
// Mixed input: plain text + a paste chip + trailing text. Worker must
|
||||
// flatten this into one user-message string (paste content inlined,
|
||||
// no `[Clipboard ...]` label leaking to the LLM); the committed
|
||||
// `LogEntry::UserInput` must carry the typed segments unchanged so
|
||||
// `LogEntry::AnnotatedUserInput` must carry the typed segments unchanged so
|
||||
// socket clients can derive `Event::UserMessage` and re-render the chip.
|
||||
let segments = vec![
|
||||
protocol::Segment::text("see "),
|
||||
@@ -1130,7 +1116,7 @@ async fn run_with_paste_segment_inlines_content_and_emits_typed_user_message() {
|
||||
_ => {}
|
||||
},
|
||||
entry = entry_rx.recv() => match entry {
|
||||
Ok(session_store::LogEntry::UserInput { segments, .. } | session_store::LogEntry::AnnotatedUserInput { segments, .. }) => {
|
||||
Ok(session_store::LogEntry::AnnotatedUserInput { segments, .. }) => {
|
||||
user_input_segments = Some(segments);
|
||||
if saw_turn_end {
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user