Compare commits

...

39 Commits

Author SHA1 Message Date
Roy Han
f354fa7124 merge cleanup 2026-03-23 13:01:03 -07:00
rhan-oai
810773ab86 Merge branch 'main' into rhan/surface-updates 2026-03-23 12:26:09 -07:00
Roy Han
30cd30a6e4 fmt 2026-03-20 18:04:47 -07:00
Roy Han
632ae61296 lint 2026-03-20 17:37:14 -07:00
rhan-oai
56ca2b539a fix lib.rs comment 2026-03-20 16:04:31 -07:00
Roy Han
13c91bde2e mandate metadata_id and use constructor 2026-03-20 15:59:38 -07:00
Roy Han
1db74b7287 rename uuid to metadata_id and introduce metadata variants 2026-03-20 15:38:36 -07:00
Roy Han
ddab859227 features 2026-03-19 23:30:20 -07:00
Roy Han
6f6fb8c017 Merge branch 'main' into rhan/surface-updates 2026-03-19 23:08:27 -07:00
Roy Han
5590153fac feature flag schema 2026-03-19 15:38:25 -07:00
Roy Han
73559b5898 move uuid stamping callsite to earliest boundary 2026-03-19 15:20:34 -07:00
Roy Han
3bce713f76 lint 2026-03-19 14:39:44 -07:00
rhan-oai
70972adbb9 Merge branch 'main' into rhan/surface-updates 2026-03-19 14:15:22 -07:00
rhan-oai
bc6d8e294a Merge branch 'main' into rhan/surface-updates 2026-03-19 13:42:37 -07:00
Roy Han
1bfbb86ce1 lint 2026-03-19 12:12:16 -07:00
Roy Han
892a00b3ed lint 2026-03-19 12:02:39 -07:00
rhan-oai
09f6d84504 Merge branch 'main' into rhan/surface-updates 2026-03-19 11:23:44 -07:00
Roy Han
6bebd3f3fb rerun ci on latest head 2026-03-19 11:16:54 -07:00
Roy Han
7bf187b172 trim test fat 2026-03-19 09:51:24 -07:00
Roy Han
2fc35f8b6b add uuid to metadata 2026-03-19 09:47:54 -07:00
Roy Han
1879c53d6e lint 2026-03-16 18:48:53 -07:00
Roy Han
56b0fe29fd Merge origin/main into rhan/surface-updates and resolve guardian tests conflict 2026-03-16 17:42:39 -07:00
Roy Han
445a6433a3 more compatibility 2026-03-13 21:08:50 -07:00
Roy Han
b2ded6f1e9 lint 2026-03-13 18:51:56 -07:00
Roy Han
66c0851437 Merge origin/main into rhan/surface-updates (resolve #14374 conflicts) 2026-03-13 18:36:03 -07:00
Roy Han
e9bf09ba09 rework to isolate core 2026-03-13 18:11:59 -07:00
Roy Han
f9570b714c Revert "initial surface projection for sandbox"
This reverts commit 79d8f99a7f.
2026-03-13 17:29:32 -07:00
Roy Han
8c019789b3 Revert "Replace item sandbox metadata with user message type metadata"
This reverts commit 2fa740749e.
2026-03-13 17:29:07 -07:00
Roy Han
13fee8b4dc fix 2026-03-12 11:10:20 -07:00
Roy Han
d8b457c841 Merge origin/main into rhan/surface-updates to resolve PR #14374 conflicts 2026-03-12 10:31:10 -07:00
Roy Han
1c435e54eb reduce schema 2026-03-11 17:58:42 -07:00
Roy Han
d2a4a8bbd7 schema update 2026-03-11 16:39:33 -07:00
Roy Han
0238699f48 nullable semantics 2026-03-11 16:27:17 -07:00
Roy Han
95027497d4 app-server-schema 2026-03-11 16:00:56 -07:00
Roy Han
6204ce60d0 fmt 2026-03-11 15:26:51 -07:00
Roy Han
2fa740749e Replace item sandbox metadata with user message type metadata 2026-03-11 15:19:15 -07:00
Roy Han
9e058f1bc5 more fixes 2026-03-11 13:25:59 -07:00
Roy Han
fcc2d8be21 fix missing items 2026-03-11 13:25:59 -07:00
Roy Han
79d8f99a7f initial surface projection for sandbox 2026-03-11 13:25:59 -07:00
54 changed files with 635 additions and 7 deletions

View File

@@ -1500,6 +1500,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMessageMetadata"
},
{
"type": "null"
}
]
},
"phase": {
"anyOf": [
{
@@ -1950,6 +1960,28 @@
}
]
},
"ResponseItemMessageMetadata": {
"properties": {
"metadata_id": {
"description": "Client-visible metadata ID generated by Codex for this item.",
"type": "string"
},
"user_message_type": {
"anyOf": [
{
"$ref": "#/definitions/UserMessageType"
},
{
"type": "null"
}
]
}
},
"required": [
"metadata_id"
],
"type": "object"
},
"ResponsesApiWebSearchAction": {
"oneOf": [
{
@@ -3330,6 +3362,14 @@
}
]
},
"UserMessageType": {
"enum": [
"prompt",
"prompt_steering",
"prompt_queued"
],
"type": "string"
},
"WindowsSandboxSetupMode": {
"enum": [
"elevated",

View File

@@ -10275,6 +10275,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMessageMetadata"
},
{
"type": "null"
}
]
},
"phase": {
"anyOf": [
{
@@ -10725,6 +10735,28 @@
}
]
},
"ResponseItemMessageMetadata": {
"properties": {
"metadata_id": {
"description": "Client-visible metadata ID generated by Codex for this item.",
"type": "string"
},
"user_message_type": {
"anyOf": [
{
"$ref": "#/definitions/v2/UserMessageType"
},
{
"type": "null"
}
]
}
},
"required": [
"metadata_id"
],
"type": "object"
},
"ResponsesApiWebSearchAction": {
"oneOf": [
{
@@ -14370,6 +14402,14 @@
}
]
},
"UserMessageType": {
"enum": [
"prompt",
"prompt_steering",
"prompt_queued"
],
"type": "string"
},
"Verbosity": {
"description": "Controls output length/detail on GPT-5 models via the Responses API. Serialized with lowercase values to match the OpenAI API.",
"enum": [

View File

@@ -7023,6 +7023,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMessageMetadata"
},
{
"type": "null"
}
]
},
"phase": {
"anyOf": [
{
@@ -7473,6 +7483,28 @@
}
]
},
"ResponseItemMessageMetadata": {
"properties": {
"metadata_id": {
"description": "Client-visible metadata ID generated by Codex for this item.",
"type": "string"
},
"user_message_type": {
"anyOf": [
{
"$ref": "#/definitions/UserMessageType"
},
{
"type": "null"
}
]
}
},
"required": [
"metadata_id"
],
"type": "object"
},
"ResponsesApiWebSearchAction": {
"oneOf": [
{
@@ -12130,6 +12162,14 @@
}
]
},
"UserMessageType": {
"enum": [
"prompt",
"prompt_steering",
"prompt_queued"
],
"type": "string"
},
"Verbosity": {
"description": "Controls output length/detail on GPT-5 models via the Responses API. Serialized with lowercase values to match the OpenAI API.",
"enum": [

View File

@@ -348,6 +348,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMessageMetadata"
},
{
"type": "null"
}
]
},
"phase": {
"anyOf": [
{
@@ -798,6 +808,28 @@
}
]
},
"ResponseItemMessageMetadata": {
"properties": {
"metadata_id": {
"description": "Client-visible metadata ID generated by Codex for this item.",
"type": "string"
},
"user_message_type": {
"anyOf": [
{
"$ref": "#/definitions/UserMessageType"
},
{
"type": "null"
}
]
}
},
"required": [
"metadata_id"
],
"type": "object"
},
"ResponsesApiWebSearchAction": {
"oneOf": [
{
@@ -898,6 +930,14 @@
"type": "object"
}
]
},
"UserMessageType": {
"enum": [
"prompt",
"prompt_steering",
"prompt_queued"
],
"type": "string"
}
},
"properties": {

View File

@@ -414,6 +414,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMessageMetadata"
},
{
"type": "null"
}
]
},
"phase": {
"anyOf": [
{
@@ -864,6 +874,28 @@
}
]
},
"ResponseItemMessageMetadata": {
"properties": {
"metadata_id": {
"description": "Client-visible metadata ID generated by Codex for this item.",
"type": "string"
},
"user_message_type": {
"anyOf": [
{
"$ref": "#/definitions/UserMessageType"
},
{
"type": "null"
}
]
}
},
"required": [
"metadata_id"
],
"type": "object"
},
"ResponsesApiWebSearchAction": {
"oneOf": [
{
@@ -979,6 +1011,14 @@
"flex"
],
"type": "string"
},
"UserMessageType": {
"enum": [
"prompt",
"prompt_steering",
"prompt_queued"
],
"type": "string"
}
},
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",

View File

@@ -9,9 +9,10 @@ import type { LocalShellStatus } from "./LocalShellStatus";
import type { MessagePhase } from "./MessagePhase";
import type { ReasoningItemContent } from "./ReasoningItemContent";
import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
import type { ResponseItemMessageMetadata } from "./ResponseItemMessageMetadata";
import type { WebSearchAction } from "./WebSearchAction";
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, end_turn?: boolean, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, metadata?: ResponseItemMessageMetadata, end_turn?: boolean, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
/**
* Set when using the Responses API.
*/

View File

@@ -0,0 +1,10 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserMessageType } from "./UserMessageType";
export type ResponseItemMessageMetadata = { user_message_type?: UserMessageType,
/**
* Client-visible metadata ID generated by Codex for this item.
*/
metadata_id: string, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UserMessageType = "prompt" | "prompt_steering" | "prompt_queued";

View File

@@ -60,6 +60,7 @@ export type { RequestId } from "./RequestId";
export type { Resource } from "./Resource";
export type { ResourceTemplate } from "./ResourceTemplate";
export type { ResponseItem } from "./ResponseItem";
export type { ResponseItemMessageMetadata } from "./ResponseItemMessageMetadata";
export type { ReviewDecision } from "./ReviewDecision";
export type { ServerNotification } from "./ServerNotification";
export type { ServerRequest } from "./ServerRequest";
@@ -69,6 +70,7 @@ export type { Settings } from "./Settings";
export type { SubAgentSource } from "./SubAgentSource";
export type { ThreadId } from "./ThreadId";
export type { Tool } from "./Tool";
export type { UserMessageType } from "./UserMessageType";
export type { Verbosity } from "./Verbosity";
export type { WebSearchAction } from "./WebSearchAction";
export type { WebSearchContextSize } from "./WebSearchContextSize";

View File

@@ -2795,6 +2795,7 @@ mod tests {
content: vec![codex_protocol::models::ContentItem::InputText {
text: "plain text".into(),
}],
metadata: None,
end_turn: None,
phase: None,
}),

View File

@@ -129,6 +129,7 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
content: vec![ContentItem::OutputText {
text: "REMOTE_COMPACT_SUMMARY".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},

View File

@@ -768,6 +768,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
content: vec![ContentItem::InputText {
text: "history override".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}]),
@@ -1596,6 +1597,7 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> {
content: vec![ContentItem::InputText {
text: history_text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}];

View File

@@ -297,6 +297,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
id: Some("msg_1".into()),
role: "user".into(),
content: vec![ContentItem::InputText { text: "hi".into() }],
metadata: None,
end_turn: None,
phase: None,
}],

View File

@@ -407,6 +407,9 @@
"include_apply_patch_tool": {
"type": "boolean"
},
"item_metadata": {
"type": "boolean"
},
"js_repl": {
"type": "boolean"
},
@@ -2013,6 +2016,9 @@
"include_apply_patch_tool": {
"type": "boolean"
},
"item_metadata": {
"type": "boolean"
},
"js_repl": {
"type": "boolean"
},

View File

@@ -38,6 +38,7 @@ impl InterAgentInstruction {
content: vec![ContentItem::OutputText {
text: self.as_text(),
}],
metadata: None,
end_turn: None,
phase: None,
}

View File

@@ -63,6 +63,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
content: vec![ContentItem::InputText {
text: "first request".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}],
@@ -87,6 +88,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
content: vec![ContentItem::OutputText {
text: "commentary".to_string(),
}],
metadata: None,
end_turn: None,
phase: Some(MessagePhase::Commentary),
}],
@@ -101,6 +103,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
content: vec![ContentItem::OutputText {
text: "final response".to_string(),
}],
metadata: None,
end_turn: None,
phase: Some(MessagePhase::FinalAnswer),
}],
@@ -115,6 +118,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
content: vec![ContentItem::InputText {
text: "latest request".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}],
@@ -270,6 +274,7 @@ async fn monitor_action_posts_expected_arc_request() {
content: vec![ContentItem::InputText {
text: "please run the tool".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}],
@@ -351,6 +356,7 @@ async fn monitor_action_uses_env_url_and_token_overrides() {
content: vec![ContentItem::InputText {
text: "please run the tool".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}],
@@ -421,6 +427,7 @@ async fn monitor_action_rejects_legacy_response_fields() {
content: vec![ContentItem::InputText {
text: "please run the tool".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}],

View File

@@ -3,7 +3,10 @@ use codex_api::common::OpenAiVerbosity;
use codex_api::common::TextControls;
use codex_api::create_text_param_for_request;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItemMessageMetadata;
use codex_protocol::models::UserMessageType;
use pretty_assertions::assert_eq;
use super::*;
@@ -198,6 +201,76 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
);
}
#[test]
fn formatted_input_preserves_message_metadata_id_when_disabled() {
let prompt = Prompt {
input: vec![ResponseItem::Message {
id: Some("msg_123".to_string()),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "hello".to_string(),
}],
metadata: Some(ResponseItemMessageMetadata {
user_message_type: Some(UserMessageType::Prompt),
metadata_id: "2585a800-7d93-4f52-8648-d9cb39f413d2".to_string(),
}),
end_turn: None,
phase: None,
}],
..Default::default()
};
let formatted = prompt.get_formatted_input();
assert_eq!(formatted.len(), 1);
match &formatted[0] {
ResponseItem::Message { metadata, .. } => {
let metadata = metadata.as_ref().expect("metadata should be present");
assert_eq!(metadata.user_message_type, Some(UserMessageType::Prompt));
assert_eq!(
metadata.metadata_id,
"2585a800-7d93-4f52-8648-d9cb39f413d2".to_string()
);
}
other => panic!("expected message item, got {other:?}"),
}
}
#[test]
fn formatted_input_preserves_existing_message_metadata_id() {
let prompt = Prompt {
input: vec![ResponseItem::Message {
id: Some("msg_123".to_string()),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "hello".to_string(),
}],
metadata: Some(ResponseItemMessageMetadata {
user_message_type: Some(UserMessageType::Prompt),
metadata_id: "2585a800-7d93-4f52-8648-d9cb39f413d2".to_string(),
}),
end_turn: None,
phase: None,
}],
..Default::default()
};
let formatted = prompt.get_formatted_input();
assert_eq!(formatted.len(), 1);
match &formatted[0] {
ResponseItem::Message { metadata, .. } => {
let metadata = metadata.as_ref().expect("metadata should be present");
assert_eq!(metadata.user_message_type, Some(UserMessageType::Prompt));
assert_eq!(
Some(metadata.metadata_id.as_str()),
Some("2585a800-7d93-4f52-8648-d9cb39f413d2")
);
}
other => panic!("expected message item, got {other:?}"),
}
}
#[test]
fn tool_search_output_namespace_serializes_with_deferred_child_tools() {
let namespace = tools::ToolSearchOutputTool::Namespace(tools::ResponsesApiNamespace {

View File

@@ -362,6 +362,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ResponseItemMessageMetadata;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::InitialHistory;
@@ -2760,6 +2761,7 @@ impl Session {
.inject_response_items(vec![ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText { text }],
metadata: None,
}])
.await
.is_err()
@@ -2857,6 +2859,7 @@ impl Session {
.inject_response_items(vec![ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText { text }],
metadata: None,
}])
.await
.is_err()
@@ -3309,9 +3312,11 @@ impl Session {
turn_context: &TurnContext,
items: &[ResponseItem],
) {
self.record_into_history(items, turn_context).await;
self.persist_rollout_response_items(items).await;
self.send_raw_response_items(turn_context, items).await;
let items = self.prepare_history_items(items);
self.record_into_history_prepared(&items, turn_context)
.await;
self.persist_rollout_response_items(&items).await;
self.send_raw_response_items(turn_context, &items).await;
}
/// Append ResponseItems to the in-memory conversation history only.
@@ -3319,6 +3324,47 @@ impl Session {
&self,
items: &[ResponseItem],
turn_context: &TurnContext,
) {
let items = self.prepare_history_items(items);
self.record_into_history_prepared(&items, turn_context)
.await;
}
fn prepare_history_items(&self, items: &[ResponseItem]) -> Vec<ResponseItem> {
if self.enabled(Feature::ItemMetadata) {
items
.iter()
.cloned()
.map(|item| match item {
ResponseItem::Message {
id,
role,
content,
metadata,
end_turn,
phase,
} => ResponseItem::Message {
id,
role,
content,
metadata: Some(metadata.unwrap_or_else(|| {
ResponseItemMessageMetadata::new(/*user_message_type*/ None)
})),
end_turn,
phase,
},
other => other,
})
.collect()
} else {
items.to_vec()
}
}
async fn record_into_history_prepared(
&self,
items: &[ResponseItem],
turn_context: &TurnContext,
) {
let mut state = self.state.lock().await;
state.record_items(items.iter(), turn_context.truncation_policy);
@@ -3334,6 +3380,7 @@ impl Session {
content: vec![ContentItem::InputText {
text: format!("Warning: {}", message.into()),
}],
metadata: None,
end_turn: None,
phase: None,
};

View File

@@ -16,6 +16,7 @@ fn user_message(text: &str) -> ResponseItem {
content: vec![ContentItem::InputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -28,6 +29,7 @@ fn assistant_message(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}

View File

@@ -15,6 +15,7 @@ use crate::models_manager::model_info;
use crate::shell::default_user_shell;
use crate::tools::format_exec_output_str;
use codex_features::Feature;
use codex_features::Features;
use codex_protocol::ThreadId;
use codex_protocol::models::FunctionCallOutputBody;
@@ -128,6 +129,7 @@ fn user_message(text: &str) -> ResponseItem {
content: vec![ContentItem::InputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -140,6 +142,7 @@ fn assistant_message(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -152,6 +155,7 @@ fn skill_message(text: &str) -> ResponseItem {
content: vec![ContentItem::InputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -883,6 +887,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -894,6 +899,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
content: vec![ContentItem::InputText {
text: "stale developer instructions".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -4007,6 +4013,7 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
content: vec![ContentItem::InputText {
text: format!("{}\nsummary", crate::compact::SUMMARY_PREFIX),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -4370,6 +4377,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
content: vec![ContentItem::InputText {
text: "late pending input".to_string(),
}],
metadata: None,
}])
.await
.expect("inject pending input into active turn");
@@ -4383,6 +4391,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
content: vec![ContentItem::InputText {
text: "late pending input".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -4577,6 +4586,37 @@ async fn steer_input_returns_active_turn_id() {
assert!(sess.has_pending_input().await);
}
#[tokio::test]
async fn record_into_history_generates_message_metadata_id_when_item_metadata_enabled() {
let (mut sess, tc) = make_session_and_context().await;
let _ = sess.features.enable(Feature::ItemMetadata);
let item = ResponseItem::Message {
id: Some("msg_123".to_string()),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "hello".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
sess.record_into_history(std::slice::from_ref(&item), &tc)
.await;
let history = sess.state.lock().await.clone_history();
let [ResponseItem::Message { metadata, .. }] = history.raw_items() else {
panic!("expected a single message item in history");
};
let metadata_id = metadata
.as_ref()
.map(|metadata| metadata.metadata_id.as_str())
.expect("metadata_id should be generated when item metadata is enabled");
uuid::Uuid::parse_str(metadata_id).expect("metadata_id should be valid");
}
#[tokio::test]
async fn prepend_pending_input_keeps_older_tail_ahead_of_newer_input() {
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
@@ -4599,18 +4639,21 @@ async fn prepend_pending_input_keeps_older_tail_ahead_of_newer_input() {
content: vec![ContentItem::InputText {
text: "blocked queued prompt".to_string(),
}],
metadata: None,
};
let later = ResponseInputItem::Message {
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "later queued prompt".to_string(),
}],
metadata: None,
};
let newer = ResponseInputItem::Message {
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "newer queued prompt".to_string(),
}],
metadata: None,
};
sess.inject_response_items(vec![blocked.clone(), later.clone()])
@@ -4845,6 +4888,7 @@ async fn sample_rollout(
content: vec![ContentItem::InputText {
text: "first user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -4860,6 +4904,7 @@ async fn sample_rollout(
content: vec![ContentItem::OutputText {
text: "assistant reply one".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -4887,6 +4932,7 @@ async fn sample_rollout(
content: vec![ContentItem::InputText {
text: "second user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -4902,6 +4948,7 @@ async fn sample_rollout(
content: vec![ContentItem::OutputText {
text: "assistant reply two".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -4929,6 +4976,7 @@ async fn sample_rollout(
content: vec![ContentItem::InputText {
text: "third user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -4944,6 +4992,7 @@ async fn sample_rollout(
content: vec![ContentItem::OutputText {
text: "assistant reply three".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};

View File

@@ -260,6 +260,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
content: vec![ContentItem::InputText {
text: "stale developer message".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -269,6 +270,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},

View File

@@ -128,6 +128,7 @@ impl CodexThread {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text: message }],
metadata: None,
end_turn: None,
phase: None,
};
@@ -264,9 +265,15 @@ impl CodexThread {
fn pending_message_input_item(message: &ResponseItem) -> CodexResult<ResponseInputItem> {
match message {
ResponseItem::Message { role, content, .. } => Ok(ResponseInputItem::Message {
ResponseItem::Message {
role,
content,
metadata,
..
} => Ok(ResponseInputItem::Message {
role: role.clone(),
content: content.clone(),
metadata: metadata.clone(),
}),
_ => Err(CodexErr::InvalidRequest(
"append_message only supports ResponseItem::Message".to_string(),

View File

@@ -367,6 +367,7 @@ fn build_compacted_history_with_limit(
content: vec![ContentItem::InputText {
text: message.clone(),
}],
metadata: None,
end_turn: None,
phase: None,
});
@@ -382,6 +383,7 @@ fn build_compacted_history_with_limit(
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text: summary_text }],
metadata: None,
end_turn: None,
phase: None,
});

View File

@@ -59,6 +59,7 @@ fn collect_user_messages_extracts_user_text_only() {
content: vec![ContentItem::OutputText {
text: "ignored".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -68,6 +69,7 @@ fn collect_user_messages_extracts_user_text_only() {
content: vec![ContentItem::InputText {
text: "first".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -93,6 +95,7 @@ do things
</INSTRUCTIONS>"#
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -102,6 +105,7 @@ do things
content: vec![ContentItem::InputText {
text: "<ENVIRONMENT_CONTEXT>cwd=/tmp</ENVIRONMENT_CONTEXT>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -111,6 +115,7 @@ do things
content: vec![ContentItem::InputText {
text: "real user message".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -194,6 +199,7 @@ async fn process_compacted_history_replaces_developer_messages() {
content: vec![ContentItem::InputText {
text: "stale permissions".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -203,6 +209,7 @@ async fn process_compacted_history_replaces_developer_messages() {
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -212,6 +219,7 @@ async fn process_compacted_history_replaces_developer_messages() {
content: vec![ContentItem::InputText {
text: "stale personality".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -224,6 +232,7 @@ async fn process_compacted_history_replaces_developer_messages() {
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
});
@@ -238,6 +247,7 @@ async fn process_compacted_history_reinjects_full_initial_context() {
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}];
@@ -249,6 +259,7 @@ async fn process_compacted_history_reinjects_full_initial_context() {
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
});
@@ -269,6 +280,7 @@ keep me updated
</INSTRUCTIONS>"#
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -282,6 +294,7 @@ keep me updated
</environment_context>"#
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -295,6 +308,7 @@ keep me updated
</turn_aborted>"#
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -304,6 +318,7 @@ keep me updated
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -313,6 +328,7 @@ keep me updated
content: vec![ContentItem::InputText {
text: "stale developer instructions".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -325,6 +341,7 @@ keep me updated
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
});
@@ -340,6 +357,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
content: vec![ContentItem::InputText {
text: "older user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -349,6 +367,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
content: vec![ContentItem::InputText {
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -358,6 +377,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
content: vec![ContentItem::InputText {
text: "latest user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -372,6 +392,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
content: vec![ContentItem::InputText {
text: "older user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -381,6 +402,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
content: vec![ContentItem::InputText {
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -392,6 +414,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
content: vec![ContentItem::InputText {
text: "latest user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
});
@@ -406,6 +429,7 @@ async fn process_compacted_history_reinjects_model_switch_message() {
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}];
@@ -436,6 +460,7 @@ async fn process_compacted_history_reinjects_model_switch_message() {
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
});
@@ -451,6 +476,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
content: vec![ContentItem::InputText {
text: "older user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -460,6 +486,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
content: vec![ContentItem::InputText {
text: "latest user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -469,6 +496,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
content: vec![ContentItem::InputText {
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -479,6 +507,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
content: vec![ContentItem::InputText {
text: "fresh permissions".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}];
@@ -492,6 +521,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
content: vec![ContentItem::InputText {
text: "older user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -501,6 +531,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
content: vec![ContentItem::InputText {
text: "fresh permissions".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -510,6 +541,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
content: vec![ContentItem::InputText {
text: "latest user".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -519,6 +551,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
content: vec![ContentItem::InputText {
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -537,6 +570,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
content: vec![ContentItem::InputText {
text: "fresh permissions".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}];
@@ -550,6 +584,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
content: vec![ContentItem::InputText {
text: "fresh permissions".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},

View File

@@ -33,6 +33,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -65,6 +66,7 @@ fn user_msg(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -77,6 +79,7 @@ fn user_input_text_msg(text: &str) -> ResponseItem {
content: vec![ContentItem::InputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -133,6 +136,7 @@ fn filters_non_api_messages() {
content: vec![ContentItem::OutputText {
text: "ignored".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -164,6 +168,7 @@ fn filters_non_api_messages() {
content: vec![ContentItem::OutputText {
text: "hi".to_string()
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -173,6 +178,7 @@ fn filters_non_api_messages() {
content: vec![ContentItem::OutputText {
text: "hello".to_string()
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -307,6 +313,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
text: "caption".to_string(),
},
],
metadata: None,
end_turn: None,
phase: None,
},
@@ -370,6 +377,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
text: "caption".to_string(),
},
],
metadata: None,
end_turn: None,
phase: None,
},
@@ -428,6 +436,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
image_url: "https://example.com/img.png".to_string(),
},
],
metadata: None,
end_turn: None,
phase: None,
}]);
@@ -456,6 +465,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
content: vec![ContentItem::InputText {
text: "hi".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -476,6 +486,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
content: vec![ContentItem::InputText {
text: "hi".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -492,6 +503,7 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
content: vec![ContentItem::InputText {
text: "generate a lobster".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -512,6 +524,7 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
content: vec![ContentItem::InputText {
text: "generate a lobster".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -668,6 +681,7 @@ fn replace_last_turn_images_does_not_touch_user_images() {
content: vec![ContentItem::InputImage {
image_url: "data:image/png;base64,AAA".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}];
@@ -1528,6 +1542,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() {
},
ContentItem::InputImage { image_url },
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -1537,6 +1552,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() {
content: vec![ContentItem::InputText {
text: "Here is the screenshot".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -1610,6 +1626,7 @@ fn non_base64_image_urls_are_unchanged() {
content: vec![ContentItem::InputImage {
image_url: "https://example.com/foo.png".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -1641,6 +1658,7 @@ fn data_url_without_base64_marker_is_unchanged() {
content: vec![ContentItem::InputImage {
image_url: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -1679,6 +1697,7 @@ fn mixed_case_data_url_markers_are_adjusted() {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputImage { image_url }],
metadata: None,
end_turn: None,
phase: None,
};
@@ -1710,6 +1729,7 @@ fn multiple_inline_images_apply_multiple_fixed_costs() {
image_url: image_url_two,
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -1793,6 +1813,7 @@ fn text_only_items_unchanged() {
content: vec![ContentItem::OutputText {
text: "Hello world, this is a response.".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};

View File

@@ -180,6 +180,7 @@ fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<Response
id: None,
role: role.to_string(),
content,
metadata: None,
end_turn: None,
phase: None,
})

View File

@@ -59,6 +59,7 @@ impl ContextualUserFragmentDefinition {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text }],
metadata: None,
end_turn: None,
phase: None,
}

View File

@@ -31,6 +31,7 @@ fn parses_user_message_with_text_and_two_images() {
image_url: img2.clone(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -74,6 +75,7 @@ fn skips_local_image_label_text() {
text: user_text.clone(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -153,6 +155,7 @@ fn skips_unnamed_image_label_text() {
text: user_text.clone(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -183,6 +186,7 @@ fn skips_user_instructions_and_env() {
content: vec![ContentItem::InputText {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -192,6 +196,7 @@ fn skips_user_instructions_and_env() {
content: vec![ContentItem::InputText {
text: "<environment_context>test_text</environment_context>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -201,6 +206,7 @@ fn skips_user_instructions_and_env() {
content: vec![ContentItem::InputText {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -211,6 +217,7 @@ fn skips_user_instructions_and_env() {
text: "<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -220,6 +227,7 @@ fn skips_user_instructions_and_env() {
content: vec![ContentItem::InputText {
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -236,6 +244,7 @@ fn skips_user_instructions_and_env() {
.to_string(),
},
],
metadata: None,
end_turn: None,
phase: None,
},
@@ -287,6 +296,7 @@ fn parses_hook_prompt_and_hides_other_contextual_fragments() {
.to_string(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -316,6 +326,7 @@ fn parses_agent_message() {
content: vec![ContentItem::OutputText {
text: "Hello from Codex".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};

View File

@@ -86,6 +86,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
text: "Please check the repo visibility and push the docs fix if needed."
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -109,6 +110,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
text: "The repo is public; I now need approval to push the docs fix."
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -162,6 +164,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
content: vec![ContentItem::InputText {
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -171,6 +174,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
content: vec![ContentItem::OutputText {
text: "hello".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -197,6 +201,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
content: vec![ContentItem::InputText {
text: "check the repo".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -219,6 +224,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
content: vec![ContentItem::OutputText {
text: "I need to push a fix".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},

View File

@@ -331,6 +331,7 @@ mod job {
&rollout_contents,
)?,
}],
metadata: None,
end_turn: None,
phase: None,
}],
@@ -487,8 +488,10 @@ mod job {
id,
role,
content,
metadata,
end_turn,
phase,
..
} = item
else {
return should_persist_response_item_for_memories(item).then(|| item.clone());
@@ -515,6 +518,7 @@ mod job {
id: id.clone(),
role: role.clone(),
content,
metadata: metadata.clone(),
end_turn: *end_turn,
phase: phase.clone(),
})

View File

@@ -22,6 +22,7 @@ fn serializes_memory_rollout_with_agents_removed_but_environment_kept() {
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".to_string(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -32,6 +33,7 @@ fn serializes_memory_rollout_with_agents_removed_but_environment_kept() {
text: "<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -42,6 +44,7 @@ fn serializes_memory_rollout_with_agents_removed_but_environment_kept() {
text: "<subagent_notification>{\"agent_id\":\"a\",\"status\":\"completed\"}</subagent_notification>"
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -64,6 +67,7 @@ fn serializes_memory_rollout_with_agents_removed_but_environment_kept() {
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>"
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},

View File

@@ -1135,6 +1135,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
content: vec![ContentItem::OutputText {
text: format!("reply-{idx}"),
}],
metadata: None,
end_turn: None,
phase: None,
}),

View File

@@ -13,6 +13,7 @@ fn user_msg(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -25,6 +26,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}

View File

@@ -16,6 +16,7 @@ fn assistant_output_text(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: Some(true),
phase: None,
}

View File

@@ -464,6 +464,7 @@ impl Session {
"{TURN_ABORTED_OPEN_TAG}\n{TURN_ABORTED_INTERRUPTED_GUIDANCE}\n</turn_aborted>"
),
}],
metadata: None,
end_turn: None,
phase: None,
};

View File

@@ -239,6 +239,7 @@ pub(crate) async fn exit_review_mode(
id: Some(REVIEW_USER_MESSAGE_ID.to_string()),
role: "user".to_string(),
content: vec![ContentItem::InputText { text: user_message }],
metadata: None,
end_turn: None,
phase: None,
}],
@@ -260,6 +261,7 @@ pub(crate) async fn exit_review_mode(
content: vec![ContentItem::OutputText {
text: assistant_message,
}],
metadata: None,
end_turn: None,
phase: None,
},

View File

@@ -340,7 +340,16 @@ async fn persist_user_shell_output(
}
let response_input_item = match output_item {
ResponseItem::Message { role, content, .. } => ResponseInputItem::Message { role, content },
ResponseItem::Message {
role,
content,
metadata,
..
} => ResponseInputItem::Message {
role,
content,
metadata,
},
_ => unreachable!("user shell command output record should always be a message"),
};

View File

@@ -21,6 +21,7 @@ fn user_msg(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -32,6 +33,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}

View File

@@ -1183,6 +1183,7 @@ async fn resume_agent_restores_closed_agent_and_accepts_send_input() {
content: vec![ContentItem::InputText {
text: "materialized".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
})]),

View File

@@ -102,6 +102,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() {
content: vec![ContentItem::OutputText {
text: "hello".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}));
@@ -115,6 +116,7 @@ fn response_item_records_turn_ttft_ignores_empty_non_output_items() {
content: vec![ContentItem::OutputText {
text: String::new(),
}],
metadata: None,
end_turn: None,
phase: None,
}));

View File

@@ -637,6 +637,7 @@ pub fn user_message_item(text: &str) -> ResponseItem {
content: vec![ContentItem::InputText {
text: text.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}

View File

@@ -105,6 +105,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
content: vec![ContentItem::InputText {
text: "hello".into(),
}],
metadata: None,
end_turn: None,
phase: None,
}];
@@ -218,6 +219,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
content: vec![ContentItem::InputText {
text: "hello".into(),
}],
metadata: None,
end_turn: None,
phase: None,
}];
@@ -330,6 +332,7 @@ async fn responses_respects_model_info_overrides_from_config() {
content: vec![ContentItem::InputText {
text: "hello".into(),
}],
metadata: None,
end_turn: None,
phase: None,
}];

View File

@@ -177,6 +177,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
content: vec![codex_protocol::models::ContentItem::InputText {
text: "resumed user message".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -199,6 +200,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
content: vec![codex_protocol::models::ContentItem::OutputText {
text: "resumed system instruction".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -221,6 +223,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
content: vec![codex_protocol::models::ContentItem::OutputText {
text: "resumed assistant message".to_string(),
}],
metadata: None,
end_turn: None,
phase: Some(MessagePhase::Commentary),
};
@@ -401,6 +404,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
content: vec![ContentItem::InputImage {
image_url: legacy_image_url.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
}),
@@ -1857,6 +1861,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
content: vec![ContentItem::OutputText {
text: "message".into(),
}],
metadata: None,
end_turn: None,
phase: None,
});

View File

@@ -1629,6 +1629,7 @@ fn message_item(text: &str) -> ResponseItem {
id: None,
role: "user".into(),
content: vec![ContentItem::InputText { text: text.into() }],
metadata: None,
end_turn: None,
phase: None,
}
@@ -1639,6 +1640,7 @@ fn assistant_message_item(id: &str, text: &str) -> ResponseItem {
id: Some(id.to_string()),
role: "assistant".into(),
content: vec![ContentItem::OutputText { text: text.into() }],
metadata: None,
end_turn: None,
phase: None,
}

View File

@@ -1589,6 +1589,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
content: vec![codex_protocol::models::ContentItem::OutputText {
text: remote_summary.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -2798,6 +2799,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
content: vec![codex_protocol::models::ContentItem::OutputText {
text: "REMOTE_COMPACT_SUMMARY".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -2918,6 +2920,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
content: vec![codex_protocol::models::ContentItem::OutputText {
text: "REMOTE_COMPACT_SUMMARY".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},

View File

@@ -1124,6 +1124,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
content: vec![ContentItem::OutputText {
text: "COMPACTED_ASSISTANT_NOTE".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -1261,6 +1262,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
content: vec![ContentItem::InputText {
text: stale_developer_message.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -1393,6 +1395,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
content: vec![ContentItem::InputText {
text: stale_developer_message.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},

View File

@@ -157,6 +157,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
text: "pasted image".to_string(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -240,6 +241,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
text: "dropped image".to_string(),
},
],
metadata: None,
end_turn: None,
phase: None,
};

View File

@@ -530,6 +530,7 @@ async fn review_input_isolated_from_parent_history() {
content: vec![codex_protocol::models::ContentItem::InputText {
text: "parent: earlier user message".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};
@@ -550,6 +551,7 @@ async fn review_input_isolated_from_parent_history() {
content: vec![codex_protocol::models::ContentItem::OutputText {
text: "parent: assistant reply".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
};

View File

@@ -166,6 +166,8 @@ pub enum Feature {
ToolCallMcpElicitation,
/// Enable personality selection in the TUI.
Personality,
/// Emit item-level metadata such as user message type and generated metadata IDs.
ItemMetadata,
/// Enable native artifact tools.
Artifact,
/// Enable Fast mode selection in the TUI and request layer.
@@ -791,6 +793,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::ItemMetadata,
key: "item_metadata",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::Artifact,
key: "artifact",

View File

@@ -147,6 +147,12 @@ fn image_detail_original_feature_is_under_development() {
assert_eq!(Feature::ImageDetailOriginal.default_enabled(), false);
}
#[test]
fn item_metadata_is_under_development_and_disabled_by_default() {
assert_eq!(Feature::ItemMetadata.stage(), Stage::UnderDevelopment);
assert_eq!(Feature::ItemMetadata.default_enabled(), false);
}
#[test]
fn collab_is_legacy_alias_for_multi_agent() {
assert_eq!(feature_for_key("multi_agent"), Some(Feature::Collab));

View File

@@ -265,6 +265,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
id: Some(uuid::Uuid::new_v4().to_string()),
role: "user".to_string(),
content,
metadata: None,
end_turn: None,
phase: None,
})

View File

@@ -229,6 +229,9 @@ pub enum ResponseInputItem {
Message {
role: String,
content: Vec<ContentItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMessageMetadata>,
},
FunctionCallOutput {
call_id: String,
@@ -258,6 +261,33 @@ pub enum ResponseInputItem {
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum UserMessageType {
Prompt,
PromptSteering,
PromptQueued,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
pub struct ResponseItemMessageMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub user_message_type: Option<UserMessageType>,
/// Client-visible metadata ID generated by Codex for this item.
pub metadata_id: String,
}
impl ResponseItemMessageMetadata {
pub fn new(user_message_type: Option<UserMessageType>) -> Self {
Self {
user_message_type,
metadata_id: uuid::Uuid::new_v4().to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentItem {
@@ -300,6 +330,9 @@ pub enum ResponseItem {
id: Option<String>,
role: String,
content: Vec<ContentItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMessageMetadata>,
// Do not use directly, no available consistently across all providers.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
@@ -865,6 +898,7 @@ impl From<DeveloperInstructions> for ResponseItem {
content: vec![ContentItem::InputText {
text: di.into_text(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -1013,9 +1047,14 @@ pub fn local_image_content_items_with_label_number(
impl From<ResponseInputItem> for ResponseItem {
fn from(item: ResponseInputItem) -> Self {
match item {
ResponseInputItem::Message { role, content } => Self::Message {
ResponseInputItem::Message {
role,
content,
metadata,
} => Self::Message {
role,
content,
metadata,
id: None,
end_turn: None,
phase: None,
@@ -1153,6 +1192,7 @@ impl From<Vec<UserInput>> for ResponseInputItem {
UserInput::Skill { .. } | UserInput::Mention { .. } => Vec::new(), // Tool bodies are injected later in core
})
.collect::<Vec<ContentItem>>(),
metadata: None,
}
}
}
@@ -3026,4 +3066,31 @@ mod tests {
Ok(())
}
#[test]
fn response_item_message_metadata_serializes_required_metadata_id() -> Result<()> {
let metadata = ResponseItemMessageMetadata::new(Some(UserMessageType::Prompt));
let serialized = serde_json::to_value(&metadata)?;
let metadata = serialized.as_object().expect("metadata should be present");
let metadata_id = metadata
.get("metadata_id")
.and_then(serde_json::Value::as_str)
.expect("metadata_id should be present");
uuid::Uuid::parse_str(metadata_id).expect("metadata_id should be valid");
assert_eq!(
metadata.get("user_message_type"),
Some(&serde_json::json!("prompt"))
);
Ok(())
}
#[test]
fn response_item_message_metadata_new_generates_metadata_id() {
let metadata = ResponseItemMessageMetadata::new(Some(UserMessageType::Prompt));
assert_eq!(metadata.user_message_type, Some(UserMessageType::Prompt));
uuid::Uuid::parse_str(&metadata.metadata_id).expect("metadata_id should be valid");
}
}

View File

@@ -2506,6 +2506,7 @@ impl From<CompactedItem> for ResponseItem {
content: vec![ContentItem::OutputText {
text: value.message,
}],
metadata: None,
end_turn: None,
phase: None,
}

View File

@@ -169,6 +169,7 @@ mod tests {
content: vec![ContentItem::InputText {
text: "hello from response item".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
});