mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Track client-authored developer messages in rollout history (#38243)
## What changed - Mark developer messages received from clients with `client_authored` harness metadata when `retain_client_developer_messages` is enabled. - Carry that provenance through pending input and persist it alongside rollout response items without adding it to provider requests or the serialized `TurnInput` payload. - Preserve existing behavior for unannotated response items and when the feature is disabled. ## Testing - Cover client item injection before and during turns, additional application context, rollout replay, image resize notices, and serialization boundaries. GitOrigin-RevId: f385c177732becde732272e4b8440eb91f5c1039
This commit is contained in:
1 parent
3d7bb2dd2e
commit
0e0ef5d818
16 files changed
+423
-60
No files matched your search
BIN
Binary file not shown.
@@ -2,6 +2,8 @@ use anyhow::Context;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use app_test_support::MockResponsesConfig;
|
use app_test_support::MockResponsesConfig;
|
||||||
use app_test_support::TestAppServer;
|
use app_test_support::TestAppServer;
|
||||||
|
use codex_app_server_protocol::AdditionalContextEntry;
|
||||||
|
use codex_app_server_protocol::AdditionalContextKind;
|
||||||
use codex_app_server_protocol::ThreadInjectItemsParams;
|
use codex_app_server_protocol::ThreadInjectItemsParams;
|
||||||
use codex_app_server_protocol::ThreadInjectItemsResponse;
|
use codex_app_server_protocol::ThreadInjectItemsResponse;
|
||||||
use codex_app_server_protocol::ThreadStartParams;
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
@@ -10,6 +12,7 @@ use codex_app_server_protocol::TurnStartParams;
|
|||||||
use codex_app_server_protocol::TurnStartResponse;
|
use codex_app_server_protocol::TurnStartResponse;
|
||||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||||
use codex_core::RolloutRecorder;
|
use codex_core::RolloutRecorder;
|
||||||
|
use codex_features::Feature;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_rollout::InitialHistory;
|
use codex_rollout::InitialHistory;
|
||||||
@@ -18,6 +21,7 @@ use core_test_support::responses;
|
|||||||
use core_test_support::responses::strip_response_item_id;
|
use core_test_support::responses::strip_response_item_id;
|
||||||
use core_test_support::responses::strip_response_item_ids_from_json;
|
use core_test_support::responses::strip_response_item_ids_from_json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
@@ -34,7 +38,9 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
|||||||
let response_mock = responses::mount_sse_once(&server, body).await;
|
let response_mock = responses::mount_sse_once(&server, body).await;
|
||||||
|
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
|
MockResponsesConfig::new(&server.uri())
|
||||||
|
.enable_feature(Feature::RetainClientDeveloperMessages)
|
||||||
|
.write(codex_home.path())?;
|
||||||
|
|
||||||
let mut mcp = TestAppServer::builder()
|
let mut mcp = TestAppServer::builder()
|
||||||
.with_codex_home(codex_home.path())
|
.with_codex_home(codex_home.path())
|
||||||
@@ -60,11 +66,27 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
|||||||
phase: None,
|
phase: None,
|
||||||
internal_chat_message_metadata_passthrough: None,
|
internal_chat_message_metadata_passthrough: None,
|
||||||
};
|
};
|
||||||
|
let developer_item = |text: &str| ResponseItem::Message {
|
||||||
|
id: None,
|
||||||
|
role: "developer".to_string(),
|
||||||
|
content: vec![ContentItem::InputText {
|
||||||
|
text: text.to_string(),
|
||||||
|
}],
|
||||||
|
phase: None,
|
||||||
|
internal_chat_message_metadata_passthrough: None,
|
||||||
|
};
|
||||||
|
let injected_developer_item = developer_item("Injected developer context");
|
||||||
|
let marker_shaped_developer_item =
|
||||||
|
developer_item("<image_resize_notice>\nclient message\n</image_resize_notice>");
|
||||||
|
|
||||||
let inject_req = mcp
|
let inject_req = mcp
|
||||||
.send_thread_inject_items_request(ThreadInjectItemsParams {
|
.send_thread_inject_items_request(ThreadInjectItemsParams {
|
||||||
thread_id: thread.id.clone(),
|
thread_id: thread.id.clone(),
|
||||||
items: vec![serde_json::to_value(&injected_item)?],
|
items: vec![
|
||||||
|
serde_json::to_value(&injected_item)?,
|
||||||
|
serde_json::to_value(&injected_developer_item)?,
|
||||||
|
serde_json::to_value(&marker_shaped_developer_item)?,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let _response: ThreadInjectItemsResponse =
|
let _response: ThreadInjectItemsResponse =
|
||||||
@@ -75,14 +97,36 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
|||||||
let InitialHistory::Resumed(resumed_history) = history else {
|
let InitialHistory::Resumed(resumed_history) = history else {
|
||||||
panic!("expected resumed rollout history");
|
panic!("expected resumed rollout history");
|
||||||
};
|
};
|
||||||
assert!(
|
let persisted_injected_items = resumed_history
|
||||||
resumed_history
|
.history
|
||||||
.history
|
.iter()
|
||||||
.iter()
|
.filter_map(|item| match item {
|
||||||
.any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if strip_response_item_id(responses::strip_metadata(response_item.item.clone())) == injected_item)),
|
RolloutItem::ResponseItem(envelope) => Some((
|
||||||
"injected item should be persisted in rollout history"
|
strip_response_item_id(responses::strip_metadata(envelope.item.clone())),
|
||||||
|
envelope
|
||||||
|
.metadata
|
||||||
|
.as_ref()
|
||||||
|
.map(|metadata| metadata.client_authored),
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.filter(|(item, _)| {
|
||||||
|
item == &injected_item
|
||||||
|
|| item == &injected_developer_item
|
||||||
|
|| item == &marker_shaped_developer_item
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
persisted_injected_items,
|
||||||
|
vec![
|
||||||
|
(injected_item.clone(), None),
|
||||||
|
(injected_developer_item.clone(), Some(true)),
|
||||||
|
(marker_shaped_developer_item.clone(), Some(true)),
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let application_context_text = "Application developer context";
|
||||||
|
let untrusted_context_text = "Untrusted client context";
|
||||||
let turn_req = mcp
|
let turn_req = mcp
|
||||||
.send_turn_start_request(TurnStartParams {
|
.send_turn_start_request(TurnStartParams {
|
||||||
thread_id: thread.id.clone(),
|
thread_id: thread.id.clone(),
|
||||||
@@ -91,6 +135,22 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
|||||||
text: "Hello".to_string(),
|
text: "Hello".to_string(),
|
||||||
text_elements: Vec::new(),
|
text_elements: Vec::new(),
|
||||||
}],
|
}],
|
||||||
|
additional_context: Some(HashMap::from([
|
||||||
|
(
|
||||||
|
"application_context".to_string(),
|
||||||
|
AdditionalContextEntry {
|
||||||
|
value: application_context_text.to_string(),
|
||||||
|
kind: AdditionalContextKind::Application,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"untrusted_context".to_string(),
|
||||||
|
AdditionalContextEntry {
|
||||||
|
value: untrusted_context_text.to_string(),
|
||||||
|
kind: AdditionalContextKind::Untrusted,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
])),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -101,6 +161,44 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
let InitialHistory::Resumed(resumed_history) =
|
||||||
|
RolloutRecorder::get_rollout_history(rollout_path).await?
|
||||||
|
else {
|
||||||
|
panic!("expected resumed rollout history");
|
||||||
|
};
|
||||||
|
let persisted_additional_context = resumed_history
|
||||||
|
.history
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| match item {
|
||||||
|
RolloutItem::ResponseItem(envelope) => {
|
||||||
|
let ResponseItem::Message { role, content, .. } = &envelope.item else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
content.iter().find_map(|item| {
|
||||||
|
let ContentItem::InputText { text } = item else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
(text.contains(application_context_text)
|
||||||
|
|| text.contains(untrusted_context_text))
|
||||||
|
.then(|| {
|
||||||
|
(
|
||||||
|
role.as_str(),
|
||||||
|
envelope
|
||||||
|
.metadata
|
||||||
|
.as_ref()
|
||||||
|
.map(|metadata| metadata.client_authored),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
persisted_additional_context,
|
||||||
|
vec![("developer", Some(true)), ("user", None)]
|
||||||
|
);
|
||||||
|
|
||||||
let injected_value = serde_json::to_value(&injected_item)?;
|
let injected_value = serde_json::to_value(&injected_item)?;
|
||||||
let model_input: Vec<Value> = response_mock
|
let model_input: Vec<Value> = response_mock
|
||||||
.single_request()
|
.single_request()
|
||||||
@@ -108,6 +206,16 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(strip_response_item_ids_from_json)
|
.map(strip_response_item_ids_from_json)
|
||||||
.collect();
|
.collect();
|
||||||
|
assert!(
|
||||||
|
model_input
|
||||||
|
.iter()
|
||||||
|
.all(|item| item.get("metadata").is_none() && item.get("client_authored").is_none()),
|
||||||
|
"private harness metadata must never enter the provider request"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
response_item_text_position(&model_input, application_context_text).is_some(),
|
||||||
|
"application-provided developer context should reach the model"
|
||||||
|
);
|
||||||
let environment_context_index =
|
let environment_context_index =
|
||||||
response_item_text_position(&model_input, "<environment_context>")
|
response_item_text_position(&model_input, "<environment_context>")
|
||||||
.expect("environment context should be injected before the first user turn");
|
.expect("environment context should be injected before the first user turn");
|
||||||
@@ -182,8 +290,8 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<()
|
|||||||
|
|
||||||
let injected_item = ResponseItem::Message {
|
let injected_item = ResponseItem::Message {
|
||||||
id: None,
|
id: None,
|
||||||
role: "assistant".to_string(),
|
role: "developer".to_string(),
|
||||||
content: vec![ContentItem::OutputText {
|
content: vec![ContentItem::InputText {
|
||||||
text: "Injected after first turn".to_string(),
|
text: "Injected after first turn".to_string(),
|
||||||
}],
|
}],
|
||||||
phase: None,
|
phase: None,
|
||||||
@@ -200,6 +308,27 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<()
|
|||||||
let _response: ThreadInjectItemsResponse =
|
let _response: ThreadInjectItemsResponse =
|
||||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(inject_req)).await??;
|
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(inject_req)).await??;
|
||||||
|
|
||||||
|
let rollout_path = thread.path.as_ref().context("thread path missing")?;
|
||||||
|
let InitialHistory::Resumed(resumed_history) =
|
||||||
|
RolloutRecorder::get_rollout_history(rollout_path).await?
|
||||||
|
else {
|
||||||
|
panic!("expected resumed rollout history");
|
||||||
|
};
|
||||||
|
let persisted_developer_item = resumed_history
|
||||||
|
.history
|
||||||
|
.iter()
|
||||||
|
.find_map(|item| match item {
|
||||||
|
RolloutItem::ResponseItem(envelope)
|
||||||
|
if strip_response_item_id(responses::strip_metadata(envelope.item.clone()))
|
||||||
|
== injected_item =>
|
||||||
|
{
|
||||||
|
Some(envelope)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.context("injected developer item should be persisted")?;
|
||||||
|
assert_eq!(persisted_developer_item.metadata, None);
|
||||||
|
|
||||||
let second_turn_req = mcp
|
let second_turn_req = mcp
|
||||||
.send_turn_start_request(TurnStartParams {
|
.send_turn_start_request(TurnStartParams {
|
||||||
thread_id: thread.id.clone(),
|
thread_id: thread.id.clone(),
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ impl CodexThread {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
self.session
|
self.session
|
||||||
.inject_no_new_turn(items, Some(turn_context.as_ref()))
|
.inject_client_response_items(items, turn_context.as_ref())
|
||||||
.await;
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ pub(crate) async fn record_pending_input(
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
TurnInput::ResponseItem(item) => {
|
TurnInput::ResponseItem(item) => {
|
||||||
sess.record_conversation_items(turn_context, std::slice::from_ref(&item))
|
sess.record_annotated_conversation_items(turn_context, vec![item])
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
TurnInput::InterAgentCommunication(communication) => {
|
TurnInput::InterAgentCommunication(communication) => {
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ pub(super) async fn user_input_or_turn_inner(
|
|||||||
let mut task_input = additional_context_input
|
let mut task_input = additional_context_input
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ResponseItem::from)
|
.map(ResponseItem::from)
|
||||||
|
.map(|item| sess.annotate_client_response_item(item))
|
||||||
.map(TurnInput::ResponseItem)
|
.map(TurnInput::ResponseItem)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if !items.is_empty() {
|
if !items.is_empty() {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ use crate::state::ActiveTurn;
|
|||||||
use crate::state::TurnState;
|
use crate::state::TurnState;
|
||||||
use crate::tasks::MailboxParentProvenance;
|
use crate::tasks::MailboxParentProvenance;
|
||||||
use crate::tasks::RegularTask;
|
use crate::tasks::RegularTask;
|
||||||
|
use codex_features::Feature;
|
||||||
|
use codex_history::CodexHarnessMetadata;
|
||||||
|
use codex_history::ResponseItemEnvelope;
|
||||||
use codex_protocol::config_types::ModeKind;
|
use codex_protocol::config_types::ModeKind;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -27,7 +30,11 @@ impl Session {
|
|||||||
self.input_queue
|
self.input_queue
|
||||||
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||||
active_turn.turn_state.as_ref(),
|
active_turn.turn_state.as_ref(),
|
||||||
input.into_iter().map(TurnInput::ResponseItem).collect(),
|
input
|
||||||
|
.into_iter()
|
||||||
|
.map(ResponseItemEnvelope::new)
|
||||||
|
.map(TurnInput::ResponseItem)
|
||||||
|
.collect(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -36,6 +43,82 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Preserves trusted client provenance while items wait for an active turn.
|
||||||
|
#[expect(
|
||||||
|
clippy::await_holding_invalid_type,
|
||||||
|
reason = "active turn checks and turn state updates must remain atomic"
|
||||||
|
)]
|
||||||
|
pub(crate) async fn inject_client_response_items(
|
||||||
|
&self,
|
||||||
|
items: Vec<ResponseItem>,
|
||||||
|
turn_context: &TurnContext,
|
||||||
|
) {
|
||||||
|
let items = items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| self.annotate_client_response_item(item))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut active = self.active_turn.lock().await;
|
||||||
|
if let Some(active_turn) = active.as_mut() {
|
||||||
|
self.input_queue
|
||||||
|
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||||
|
active_turn.turn_state.as_ref(),
|
||||||
|
items.into_iter().map(TurnInput::ResponseItem).collect(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(active);
|
||||||
|
self.record_annotated_conversation_items(turn_context, items)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn annotate_client_response_item(&self, item: ResponseItem) -> ResponseItemEnvelope {
|
||||||
|
let metadata = (self.enabled(Feature::RetainClientDeveloperMessages)
|
||||||
|
&& matches!(&item, ResponseItem::Message { role, .. } if role == "developer"))
|
||||||
|
.then_some(CodexHarnessMetadata {
|
||||||
|
client_authored: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
ResponseItemEnvelope { item, metadata }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn record_annotated_conversation_items(
|
||||||
|
&self,
|
||||||
|
turn_context: &TurnContext,
|
||||||
|
items: Vec<ResponseItemEnvelope>,
|
||||||
|
) {
|
||||||
|
if !self.enabled(Feature::RetainClientDeveloperMessages)
|
||||||
|
|| items.iter().all(|item| item.metadata.is_none())
|
||||||
|
{
|
||||||
|
let items = items
|
||||||
|
.into_iter()
|
||||||
|
.map(ResponseItemEnvelope::into_item)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
self.record_conversation_items(turn_context, &items).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut annotated_items = Vec::with_capacity(items.len());
|
||||||
|
let mut image_preparations = Vec::new();
|
||||||
|
for envelope in items {
|
||||||
|
let (prepared_items, prepared_images) = self.prepare_conversation_items_for_history(
|
||||||
|
turn_context,
|
||||||
|
std::slice::from_ref(&envelope.item),
|
||||||
|
);
|
||||||
|
image_preparations.extend(prepared_images);
|
||||||
|
|
||||||
|
let mut metadata = envelope.metadata;
|
||||||
|
annotated_items.extend(prepared_items.into_owned().into_iter().map(|item| {
|
||||||
|
ResponseItemEnvelope {
|
||||||
|
item,
|
||||||
|
metadata: metadata.take(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
self.record_prepared_conversation_items(turn_context, annotated_items, image_preparations)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Starts a regular turn with the provided input only if automatic idle work
|
/// Starts a regular turn with the provided input only if automatic idle work
|
||||||
/// is allowed for the current session state.
|
/// is allowed for the current session state.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::state::MailboxDeliveryPhase;
|
|||||||
use crate::state::TurnState;
|
use crate::state::TurnState;
|
||||||
use codex_diagnostics::Gauge;
|
use codex_diagnostics::Gauge;
|
||||||
use codex_diagnostics::GaugeGuard;
|
use codex_diagnostics::GaugeGuard;
|
||||||
|
use codex_history::ResponseItemEnvelope;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::protocol::InterAgentCommunication;
|
use codex_protocol::protocol::InterAgentCommunication;
|
||||||
use codex_protocol::user_input::UserInput;
|
use codex_protocol::user_input::UserInput;
|
||||||
@@ -22,10 +23,44 @@ pub enum TurnInput {
|
|||||||
content: Vec<UserInput>,
|
content: Vec<UserInput>,
|
||||||
client_id: Option<String>,
|
client_id: Option<String>,
|
||||||
},
|
},
|
||||||
ResponseItem(ResponseItem),
|
// Preserve the existing serialized format while carrying injection API metadata
|
||||||
|
// through the in-memory queue.
|
||||||
|
ResponseItem(#[serde(with = "turn_input_response_item")] ResponseItemEnvelope),
|
||||||
InterAgentCommunication(InterAgentCommunication),
|
InterAgentCommunication(InterAgentCommunication),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod turn_input_response_item {
|
||||||
|
use super::ResponseItem;
|
||||||
|
use super::ResponseItemEnvelope;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Deserializer;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::Serializer;
|
||||||
|
use serde::ser::Error as _;
|
||||||
|
|
||||||
|
pub(super) fn serialize<S>(
|
||||||
|
item: &ResponseItemEnvelope,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
if item.metadata.is_some() {
|
||||||
|
return Err(S::Error::custom(
|
||||||
|
"annotated response items cannot cross the turn-input serialization boundary",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
item.item.serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<ResponseItemEnvelope, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
ResponseItem::deserialize(deserializer).map(ResponseItemEnvelope::new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub(crate) enum InputQueueActivity {
|
pub(crate) enum InputQueueActivity {
|
||||||
Mailbox,
|
Mailbox,
|
||||||
@@ -339,9 +374,41 @@ impl TurnInputQueue {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use codex_history::CodexHarnessMetadata;
|
||||||
use codex_protocol::AgentPath;
|
use codex_protocol::AgentPath;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_item_serde_preserves_legacy_shape_and_rejects_metadata() {
|
||||||
|
let item = ResponseItem::Other;
|
||||||
|
let input = TurnInput::ResponseItem(item.clone().into());
|
||||||
|
let value = serde_json::json!({"ResponseItem": item});
|
||||||
|
|
||||||
|
assert_eq!(serde_json::to_value(&input).unwrap(), value);
|
||||||
|
assert_eq!(serde_json::from_value::<TurnInput>(value).unwrap(), input);
|
||||||
|
|
||||||
|
let annotated = TurnInput::ResponseItem(ResponseItemEnvelope {
|
||||||
|
item: ResponseItem::Other,
|
||||||
|
metadata: Some(CodexHarnessMetadata {
|
||||||
|
client_authored: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert!(serde_json::to_value(annotated).is_err());
|
||||||
|
|
||||||
|
let forged = serde_json::json!({
|
||||||
|
"ResponseItem": {
|
||||||
|
"type": "message",
|
||||||
|
"role": "developer",
|
||||||
|
"content": [],
|
||||||
|
"metadata": {"client_authored": true}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let TurnInput::ResponseItem(envelope) = serde_json::from_value(forged).unwrap() else {
|
||||||
|
panic!("expected response item");
|
||||||
|
};
|
||||||
|
assert!(envelope.metadata.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
fn make_mail(
|
fn make_mail(
|
||||||
author: AgentPath,
|
author: AgentPath,
|
||||||
recipient: AgentPath,
|
recipient: AgentPath,
|
||||||
|
|||||||
@@ -3037,14 +3037,33 @@ impl Session {
|
|||||||
) {
|
) {
|
||||||
let (items, image_preparations) =
|
let (items, image_preparations) =
|
||||||
self.prepare_conversation_items_for_history(turn_context, items);
|
self.prepare_conversation_items_for_history(turn_context, items);
|
||||||
let items = items.as_ref();
|
let items = items
|
||||||
|
.into_owned()
|
||||||
|
.into_iter()
|
||||||
|
.map(ResponseItemEnvelope::new)
|
||||||
|
.collect();
|
||||||
|
self.record_prepared_conversation_items(turn_context, items, image_preparations)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_prepared_conversation_items(
|
||||||
|
&self,
|
||||||
|
turn_context: &TurnContext,
|
||||||
|
items: Vec<ResponseItemEnvelope>,
|
||||||
|
image_preparations: Vec<ImagePreparationMetadata>,
|
||||||
|
) {
|
||||||
|
let response_items = items
|
||||||
|
.iter()
|
||||||
|
.map(|envelope| envelope.item.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
state.current_time_reminder.note_recorded_items(items);
|
state
|
||||||
state.record_items(
|
.current_time_reminder
|
||||||
items.iter(),
|
.note_recorded_items(&response_items);
|
||||||
turn_context.model_info.truncation_policy.into(),
|
state
|
||||||
);
|
.history
|
||||||
|
.record_annotated_items(&items, turn_context.model_info.truncation_policy.into());
|
||||||
}
|
}
|
||||||
for image in image_preparations {
|
for image in image_preparations {
|
||||||
self.services
|
self.services
|
||||||
@@ -3054,8 +3073,11 @@ impl Session {
|
|||||||
metadata: image,
|
metadata: image,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.persist_rollout_response_items(items).await;
|
let rollout_items: Vec<RolloutItem> =
|
||||||
self.send_raw_response_items(turn_context, items).await;
|
items.into_iter().map(RolloutItem::ResponseItem).collect();
|
||||||
|
self.persist_rollout_items(&rollout_items).await;
|
||||||
|
self.send_raw_response_items(turn_context, &response_items)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn record_step_world_state_if_changed(
|
pub(crate) async fn record_step_world_state_if_changed(
|
||||||
@@ -3349,16 +3371,6 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn persist_rollout_response_items(&self, items: &[ResponseItem]) {
|
|
||||||
let rollout_items: Vec<RolloutItem> = items
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(ResponseItemEnvelope::new)
|
|
||||||
.map(RolloutItem::ResponseItem)
|
|
||||||
.collect();
|
|
||||||
self.persist_rollout_items(&rollout_items).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled(&self, feature: Feature) -> bool {
|
pub fn enabled(&self, feature: Feature) -> bool {
|
||||||
self.features.enabled(feature)
|
self.features.enabled(feature)
|
||||||
}
|
}
|
||||||
@@ -4086,6 +4098,7 @@ impl Session {
|
|||||||
let mut pending_input = additional_context_input
|
let mut pending_input = additional_context_input
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ResponseItem::from)
|
.map(ResponseItem::from)
|
||||||
|
.map(|item| self.annotate_client_response_item(item))
|
||||||
.map(TurnInput::ResponseItem)
|
.map(TurnInput::ResponseItem)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
pending_input.push(TurnInput::UserInput {
|
pending_input.push(TurnInput::UserInput {
|
||||||
|
|||||||
@@ -10636,7 +10636,7 @@ async fn try_start_turn_if_idle_rejects_active_turn_without_injecting() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let item = TurnInput::ResponseItem(user_message("synthetic idle input"));
|
let item = TurnInput::ResponseItem(user_message("synthetic idle input").into());
|
||||||
let err = sess
|
let err = sess
|
||||||
.try_start_turn_if_idle(vec![item.clone()])
|
.try_start_turn_if_idle(vec![item.clone()])
|
||||||
.await
|
.await
|
||||||
@@ -10662,7 +10662,7 @@ async fn try_start_turn_if_idle_rejects_plan_mode_without_injecting() {
|
|||||||
state.session_configuration.collaboration_mode = collaboration_mode;
|
state.session_configuration.collaboration_mode = collaboration_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = TurnInput::ResponseItem(user_message("synthetic idle input"));
|
let item = TurnInput::ResponseItem(user_message("synthetic idle input").into());
|
||||||
let err = sess
|
let err = sess
|
||||||
.try_start_turn_if_idle(vec![item.clone()])
|
.try_start_turn_if_idle(vec![item.clone()])
|
||||||
.await
|
.await
|
||||||
@@ -10718,7 +10718,7 @@ async fn try_start_turn_if_idle_rejects_empty_user_input_in_plan_mode() {
|
|||||||
content: Vec::new(),
|
content: Vec::new(),
|
||||||
client_id: Some("empty-queued-user-message".to_string()),
|
client_id: Some("empty-queued-user-message".to_string()),
|
||||||
},
|
},
|
||||||
TurnInput::ResponseItem(user_message("automatic idle input")),
|
TurnInput::ResponseItem(user_message("automatic idle input").into()),
|
||||||
];
|
];
|
||||||
let error = sess
|
let error = sess
|
||||||
.try_start_turn_if_idle(input.clone())
|
.try_start_turn_if_idle(input.clone())
|
||||||
@@ -10747,7 +10747,7 @@ async fn try_start_turn_if_idle_rejects_pending_trigger_turn_without_injecting()
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let item = TurnInput::ResponseItem(user_message("synthetic idle input"));
|
let item = TurnInput::ResponseItem(user_message("synthetic idle input").into());
|
||||||
let err = sess
|
let err = sess
|
||||||
.try_start_turn_if_idle(vec![item.clone()])
|
.try_start_turn_if_idle(vec![item.clone()])
|
||||||
.await
|
.await
|
||||||
@@ -10775,7 +10775,7 @@ async fn try_start_turn_if_idle_rejects_active_review_turn_without_injecting() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let item = TurnInput::ResponseItem(user_message("synthetic idle input"));
|
let item = TurnInput::ResponseItem(user_message("synthetic idle input").into());
|
||||||
let err = sess
|
let err = sess
|
||||||
.try_start_turn_if_idle(vec![item.clone()])
|
.try_start_turn_if_idle(vec![item.clone()])
|
||||||
.await
|
.await
|
||||||
@@ -10987,7 +10987,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
|
|||||||
sess.input_queue
|
sess.input_queue
|
||||||
.extend_pending_input_for_turn_state(
|
.extend_pending_input_for_turn_state(
|
||||||
turn_state.as_ref(),
|
turn_state.as_ref(),
|
||||||
vec![TurnInput::ResponseItem(pending_item.clone())],
|
vec![TurnInput::ResponseItem(pending_item.clone().into())],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -10998,7 +10998,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
|
|||||||
sess.input_queue
|
sess.input_queue
|
||||||
.take_pending_input_for_turn_state(turn_state.as_ref())
|
.take_pending_input_for_turn_state(turn_state.as_ref())
|
||||||
.await,
|
.await,
|
||||||
vec![TurnInput::ResponseItem(pending_item)]
|
vec![TurnInput::ResponseItem(pending_item.into())]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use codex_protocol::models::ContentItem;
|
|||||||
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
|
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
|
||||||
use codex_protocol::models::ImageDetail;
|
use codex_protocol::models::ImageDetail;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::models::ResponseInputItem;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
@@ -355,6 +356,9 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R
|
|||||||
|
|
||||||
let mut resume_builder = test_codex().with_config(|config| {
|
let mut resume_builder = test_codex().with_config(|config| {
|
||||||
let _ = config.features.enable(Feature::ImageResizeNotice);
|
let _ = config.features.enable(Feature::ImageResizeNotice);
|
||||||
|
let _ = config
|
||||||
|
.features
|
||||||
|
.enable(Feature::RetainClientDeveloperMessages);
|
||||||
});
|
});
|
||||||
let resumed = resume_builder
|
let resumed = resume_builder
|
||||||
.resume(&server, initial.home.clone(), rollout_path.clone())
|
.resume(&server, initial.home.clone(), rollout_path.clone())
|
||||||
@@ -372,7 +376,7 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R
|
|||||||
.codex
|
.codex
|
||||||
.submit(Op::UserInput {
|
.submit(Op::UserInput {
|
||||||
items: vec![UserInput::Image {
|
items: vec![UserInput::Image {
|
||||||
image_url: original_image_url,
|
image_url: original_image_url.clone(),
|
||||||
detail: Some(ImageDetail::High),
|
detail: Some(ImageDetail::High),
|
||||||
}],
|
}],
|
||||||
final_output_json_schema: None,
|
final_output_json_schema: None,
|
||||||
@@ -446,7 +450,7 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R
|
|||||||
|
|
||||||
resumed.codex.shutdown_and_wait().await?;
|
resumed.codex.shutdown_and_wait().await?;
|
||||||
let replayed = resume_builder
|
let replayed = resume_builder
|
||||||
.resume(&server, resumed.home.clone(), rollout_path)
|
.resume(&server, resumed.home.clone(), rollout_path.clone())
|
||||||
.await?;
|
.await?;
|
||||||
let replayed_mock = responses::mount_sse_once(
|
let replayed_mock = responses::mount_sse_once(
|
||||||
&server,
|
&server,
|
||||||
@@ -466,5 +470,53 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(replayed_notices, vec![expected_notice.to_string()]);
|
assert_eq!(replayed_notices, vec![expected_notice.to_string()]);
|
||||||
|
|
||||||
|
let replayed = test_codex()
|
||||||
|
.with_config(|config| {
|
||||||
|
let _ = config.features.enable(Feature::ImageResizeNotice);
|
||||||
|
let _ = config
|
||||||
|
.features
|
||||||
|
.enable(Feature::RetainClientDeveloperMessages);
|
||||||
|
})
|
||||||
|
.restart(&server, &replayed)
|
||||||
|
.await?;
|
||||||
|
let existing_rollout_lines = fs::read_to_string(&rollout_path)?.lines().count();
|
||||||
|
replayed
|
||||||
|
.codex
|
||||||
|
.inject_response_items(vec![
|
||||||
|
ResponseInputItem::Message {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: vec![ContentItem::InputImage {
|
||||||
|
image_url: original_image_url,
|
||||||
|
detail: Some(ImageDetail::High),
|
||||||
|
}],
|
||||||
|
phase: None,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
ResponseInputItem::Message {
|
||||||
|
role: "developer".to_string(),
|
||||||
|
content: vec![ContentItem::InputText {
|
||||||
|
text: "<image_resize_notice>client message</image_resize_notice>".to_string(),
|
||||||
|
}],
|
||||||
|
phase: None,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.await?;
|
||||||
|
let persisted_developer_metadata = fs::read_to_string(&rollout_path)?
|
||||||
|
.lines()
|
||||||
|
.skip(existing_rollout_lines)
|
||||||
|
.filter_map(|line| {
|
||||||
|
let RolloutItem::ResponseItem(envelope) = serde_json::from_str::<RolloutLine>(line)
|
||||||
|
.expect("new rollout line should deserialize")
|
||||||
|
.item
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
matches!(envelope.item, ResponseItem::Message { role, .. } if role == "developer")
|
||||||
|
.then(|| envelope.metadata.map(|metadata| metadata.client_authored))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(persisted_developer_metadata, vec![None, Some(true)]);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -71,9 +71,9 @@ async fn idle_response_items_include_pending_mailbox_in_first_request() -> anyho
|
|||||||
|
|
||||||
submit_queue_only_agent_mail(test.codex.as_ref(), "pending mailbox input").await;
|
submit_queue_only_agent_mail(test.codex.as_ref(), "pending mailbox input").await;
|
||||||
test.codex
|
test.codex
|
||||||
.try_start_turn_if_idle(vec![TurnInput::ResponseItem(responses::user_message_item(
|
.try_start_turn_if_idle(vec![TurnInput::ResponseItem(
|
||||||
"automatic response item",
|
responses::user_message_item("automatic response item").into(),
|
||||||
))])
|
)])
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
anyhow::anyhow!("idle response items were rejected: {:?}", error.reason())
|
anyhow::anyhow!("idle response items were rejected: {:?}", error.reason())
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ impl GoalRuntimeHandle {
|
|||||||
let item = continuation_steering_item(&protocol_goal_from_state(goal));
|
let item = continuation_steering_item(&protocol_goal_from_state(goal));
|
||||||
|
|
||||||
if let Err(err) = thread
|
if let Err(err) = thread
|
||||||
.try_start_turn_if_idle(vec![TurnInput::ResponseItem(item)])
|
.try_start_turn_if_idle(vec![TurnInput::ResponseItem(item.into())])
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let reason = err.reason();
|
let reason = err.reason();
|
||||||
|
|||||||
@@ -616,7 +616,7 @@ async fn non_user_input_cannot_enter_the_user_message_queue() -> anyhow::Result<
|
|||||||
let error = service
|
let error = service
|
||||||
.enqueue(
|
.enqueue(
|
||||||
ThreadId::new(),
|
ThreadId::new(),
|
||||||
TurnInput::ResponseItem(ResponseItem::Other),
|
TurnInput::ResponseItem(ResponseItem::Other.into()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect_err("response item should not enter the user queue");
|
.expect_err("response item should not enter the user queue");
|
||||||
|
|||||||
@@ -40,10 +40,12 @@ pub struct ResponseItemEnvelope {
|
|||||||
|
|
||||||
/// Metadata owned by the Codex harness and persisted with a response item.
|
/// Metadata owned by the Codex harness and persisted with a response item.
|
||||||
///
|
///
|
||||||
/// This intentionally has no fields yet. Keeping it closed prevents rollout
|
|
||||||
/// metadata from becoming an untyped extension point.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||||
pub struct CodexHarnessMetadata {}
|
pub struct CodexHarnessMetadata {
|
||||||
|
/// Whether a developer message was supplied by an app-server client.
|
||||||
|
#[serde(default)]
|
||||||
|
pub client_authored: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl ResponseItemEnvelope {
|
impl ResponseItemEnvelope {
|
||||||
/// Wraps a raw Responses API item for persisted history.
|
/// Wraps a raw Responses API item for persisted history.
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()>
|
|||||||
ordinal: Some(7),
|
ordinal: Some(7),
|
||||||
item: RolloutItem::ResponseItem(ResponseItemEnvelope {
|
item: RolloutItem::ResponseItem(ResponseItemEnvelope {
|
||||||
item: response_item.clone(),
|
item: response_item.clone(),
|
||||||
metadata: Some(CodexHarnessMetadata {}),
|
metadata: Some(CodexHarnessMetadata {
|
||||||
|
client_authored: true,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let serialized = serde_json::to_value(&line)?;
|
let serialized = serde_json::to_value(&line)?;
|
||||||
@@ -82,7 +84,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()>
|
|||||||
"ordinal": 7,
|
"ordinal": 7,
|
||||||
"type": "response_item",
|
"type": "response_item",
|
||||||
"payload": response_item,
|
"payload": response_item,
|
||||||
"metadata": {},
|
"metadata": { "client_authored": true },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(serialized["payload"].get("metadata"), None);
|
assert_eq!(serialized["payload"].get("metadata"), None);
|
||||||
@@ -91,7 +93,12 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()>
|
|||||||
let RolloutItem::ResponseItem(envelope) = restored.item else {
|
let RolloutItem::ResponseItem(envelope) = restored.item else {
|
||||||
panic!("expected response item");
|
panic!("expected response item");
|
||||||
};
|
};
|
||||||
assert_eq!(envelope.metadata, Some(CodexHarnessMetadata {}));
|
assert_eq!(
|
||||||
|
envelope.metadata,
|
||||||
|
Some(CodexHarnessMetadata {
|
||||||
|
client_authored: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +125,7 @@ fn response_item_envelope_ignores_unknown_harness_metadata_fields() -> Result<()
|
|||||||
let RolloutItem::ResponseItem(envelope) = line.item else {
|
let RolloutItem::ResponseItem(envelope) = line.item else {
|
||||||
panic!("expected response item");
|
panic!("expected response item");
|
||||||
};
|
};
|
||||||
assert_eq!(envelope.metadata, Some(CodexHarnessMetadata {}));
|
assert_eq!(envelope.metadata, Some(CodexHarnessMetadata::default()));
|
||||||
|
|
||||||
let compacted = serde_json::from_value::<CompactedItem>(json!({
|
let compacted = serde_json::from_value::<CompactedItem>(json!({
|
||||||
"message": "summary",
|
"message": "summary",
|
||||||
@@ -127,7 +134,7 @@ fn response_item_envelope_ignores_unknown_harness_metadata_fields() -> Result<()
|
|||||||
}))?;
|
}))?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
compacted.replacement_history.expect("replacement history")[0].metadata,
|
compacted.replacement_history.expect("replacement history")[0].metadata,
|
||||||
Some(CodexHarnessMetadata {})
|
Some(CodexHarnessMetadata::default())
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -175,7 +182,9 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu
|
|||||||
replacement_history: Some(vec![
|
replacement_history: Some(vec![
|
||||||
ResponseItemEnvelope {
|
ResponseItemEnvelope {
|
||||||
item: developer_message.clone(),
|
item: developer_message.clone(),
|
||||||
metadata: Some(CodexHarnessMetadata {}),
|
metadata: Some(CodexHarnessMetadata {
|
||||||
|
client_authored: true,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
ResponseItemEnvelope::new(compaction_item.clone()),
|
ResponseItemEnvelope::new(compaction_item.clone()),
|
||||||
]),
|
]),
|
||||||
@@ -191,7 +200,10 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu
|
|||||||
json!({
|
json!({
|
||||||
"message": "summary",
|
"message": "summary",
|
||||||
"replacement_history": [developer_message, compaction_item],
|
"replacement_history": [developer_message, compaction_item],
|
||||||
"replacement_history_metadata": [{}, {}],
|
"replacement_history_metadata": [
|
||||||
|
{ "client_authored": true },
|
||||||
|
{ "client_authored": false },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -201,11 +213,13 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu
|
|||||||
Some(vec![
|
Some(vec![
|
||||||
ResponseItemEnvelope {
|
ResponseItemEnvelope {
|
||||||
item: developer_message,
|
item: developer_message,
|
||||||
metadata: Some(CodexHarnessMetadata {}),
|
metadata: Some(CodexHarnessMetadata {
|
||||||
|
client_authored: true,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
ResponseItemEnvelope {
|
ResponseItemEnvelope {
|
||||||
item: compaction_item,
|
item: compaction_item,
|
||||||
metadata: Some(CodexHarnessMetadata {}),
|
metadata: Some(CodexHarnessMetadata::default()),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@@ -260,7 +274,9 @@ fn compacted_metadata_remains_compatible_with_legacy_response_item_readers() ->
|
|||||||
let response_item = response_message("developer");
|
let response_item = response_message("developer");
|
||||||
let envelope = ResponseItemEnvelope {
|
let envelope = ResponseItemEnvelope {
|
||||||
item: response_item.clone(),
|
item: response_item.clone(),
|
||||||
metadata: Some(CodexHarnessMetadata {}),
|
metadata: Some(CodexHarnessMetadata {
|
||||||
|
client_authored: true,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
let response_line = serde_json::to_value(RolloutItem::ResponseItem(envelope.clone()))?;
|
let response_line = serde_json::to_value(RolloutItem::ResponseItem(envelope.clone()))?;
|
||||||
let LegacyRolloutItem::ResponseItem(legacy_response) =
|
let LegacyRolloutItem::ResponseItem(legacy_response) =
|
||||||
|
|||||||
@@ -1331,7 +1331,7 @@ async fn read_head_for_summary_omits_harness_metadata() {
|
|||||||
ordinal: None,
|
ordinal: None,
|
||||||
item: RolloutItem::ResponseItem(ResponseItemEnvelope {
|
item: RolloutItem::ResponseItem(ResponseItemEnvelope {
|
||||||
item: response_item.clone(),
|
item: response_item.clone(),
|
||||||
metadata: Some(CodexHarnessMetadata {}),
|
metadata: Some(CodexHarnessMetadata::default()),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
fs::write(
|
fs::write(
|
||||||
|
|||||||
Reference in new issue
Block a user