Preserve harness metadata across conversation history (#38058)

## What changed

- Wrap response items with optional harness-owned metadata while keeping the persisted response payload shape backward compatible.
- Store compacted-history metadata in an aligned sidecar and reject malformed sidecars.
- Carry metadata through history normalization, compaction, resume, fork, truncation, migration, and rollout recording without sending it to model providers.

## Testing

- Cover legacy rollout compatibility, metadata serialization and validation, compaction retention, and resume/fork reconstruction.

GitOrigin-RevId: 17a6127c9048ff984cb4df4d97c0751a38c29f5f
This commit is contained in:
rka-oai authored and copyberry committed 2026-08-11 19:30:12 +00:00
1 parent 44d992c14e
commit 3a6f747d77
67 files changed
+2075 -655

No files matched your search

@@ -220,7 +220,7 @@ fn source_model_items(items: &[RolloutItem]) -> Option<Vec<SourceModelItem<'_>>>
RolloutItem::SessionMeta(_) | RolloutItem::InterAgentCommunicationMetadata { .. } => {}
RolloutItem::ResponseItem(response_item) => {
model_items.push(SourceModelItem {
response_item,
response_item: &response_item.item,
append_start_index: append_start_index.take().unwrap_or(index),
});
}
@@ -253,7 +253,7 @@ fn history_model_items(items: &[RolloutItem]) -> Option<Vec<&ResponseItem>> {
for item in items {
match item {
RolloutItem::SessionMeta(_) | RolloutItem::InterAgentCommunicationMetadata { .. } => {}
RolloutItem::ResponseItem(response_item) => model_items.push(response_item),
RolloutItem::ResponseItem(response_item) => model_items.push(&response_item.item),
RolloutItem::EventMsg(
EventMsg::ContextCompacted(_) | EventMsg::ThreadRolledBack(_),
)
@@ -75,15 +75,18 @@ fn requires_a_strict_nonempty_model_prefix() {
assert!(plan_append(&source, &rewritten).is_none());
}
let mut with_tool_call = history;
with_tool_call.push(RolloutItem::ResponseItem(ResponseItem::FunctionCall {
id: None,
name: "native_tool".to_string(),
namespace: None,
arguments: "{}".to_string(),
encrypted_function_args: None,
call_id: "native-call".to_string(),
internal_chat_message_metadata_passthrough: None,
}));
with_tool_call.push(RolloutItem::ResponseItem(
ResponseItem::FunctionCall {
id: None,
name: "native_tool".to_string(),
namespace: None,
arguments: "{}".to_string(),
encrypted_function_args: None,
call_id: "native-call".to_string(),
internal_chat_message_metadata_passthrough: None,
}
.into(),
));
assert!(plan_append(&source, &with_tool_call).is_none());
}
@@ -105,7 +108,10 @@ fn model_messages(items: &[RolloutItem]) -> Vec<(MessageRole, &str)> {
items
.iter()
.filter_map(|item| match item {
RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) => {
RolloutItem::ResponseItem(response_item) => {
let ResponseItem::Message { role, content, .. } = &response_item.item else {
return None;
};
match (role.as_str(), content.as_slice()) {
("user", [ContentItem::InputText { text }]) => {
Some((MessageRole::User, text.as_str()))
@@ -120,7 +120,7 @@ pub(super) fn rollout_items_from_messages(messages: Vec<ConversationMessage>) ->
)));
response_item_bytes =
response_item_bytes.saturating_add(message_byte_count(&message));
items.push(RolloutItem::ResponseItem(response_item(message)));
items.push(RolloutItem::ResponseItem(response_item(message).into()));
current_turn = Some((turn_id, started_at));
}
MessageRole::Assistant => {
@@ -137,7 +137,7 @@ pub(super) fn rollout_items_from_messages(messages: Vec<ConversationMessage>) ->
memory_citation: None,
},
)));
items.push(RolloutItem::ResponseItem(response_item(message)));
items.push(RolloutItem::ResponseItem(response_item(message).into()));
}
}
}
@@ -325,7 +325,8 @@ mod tests {
.filter(|item| {
matches!(
item,
RolloutItem::ResponseItem(ResponseItem::Message { .. })
RolloutItem::ResponseItem(response_item)
if matches!(&response_item.item, ResponseItem::Message { .. })
)
})
.count();