add session source to telemetry

This commit is contained in:
Roy Han
2026-03-19 17:56:55 -07:00
parent 659d21f146
commit 678f52a33b
33 changed files with 1264 additions and 83 deletions
@@ -1557,6 +1557,16 @@
"null"
]
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"summary": {
"items": {
"$ref": "#/definitions/ReasoningItemReasoningSummary"
@@ -1697,6 +1707,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -1724,6 +1744,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"output": {
"$ref": "#/definitions/FunctionCallOutputBody"
},
@@ -1799,6 +1829,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"name": {
"type": [
"string",
@@ -1835,6 +1875,16 @@
"execution": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": "string"
},
@@ -1878,6 +1928,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -1903,6 +1963,16 @@
"id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"result": {
"type": "string"
},
@@ -1937,6 +2007,16 @@
"ghost_commit": {
"$ref": "#/definitions/GhostCommit"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"ghost_snapshot"
@@ -1957,6 +2037,16 @@
"encrypted_content": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"compaction"
@@ -2018,6 +2108,16 @@
}
]
},
"session_source": {
"anyOf": [
{
"$ref": "#/definitions/SessionSourceMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -2418,6 +2518,21 @@
],
"type": "string"
},
"SessionSourceMetadata": {
"enum": [
"cli",
"vscode",
"exec",
"mcp",
"subagent_review",
"subagent_compact",
"subagent_memory_consolidation",
"subagent_thread_spawn",
"subagent_other",
"unknown"
],
"type": "string"
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
@@ -10224,6 +10224,16 @@
"null"
]
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"summary": {
"items": {
"$ref": "#/definitions/v2/ReasoningItemReasoningSummary"
@@ -10364,6 +10374,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -10391,6 +10411,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"output": {
"$ref": "#/definitions/v2/FunctionCallOutputBody"
},
@@ -10466,6 +10496,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"name": {
"type": [
"string",
@@ -10502,6 +10542,16 @@
"execution": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": "string"
},
@@ -10545,6 +10595,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -10570,6 +10630,16 @@
"id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"result": {
"type": "string"
},
@@ -10604,6 +10674,16 @@
"ghost_commit": {
"$ref": "#/definitions/v2/GhostCommit"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"ghost_snapshot"
@@ -10624,6 +10704,16 @@
"encrypted_content": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/v2/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"compaction"
@@ -10685,6 +10775,16 @@
}
]
},
"session_source": {
"anyOf": [
{
"$ref": "#/definitions/v2/SessionSourceMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -11186,6 +11286,21 @@
}
]
},
"SessionSourceMetadata": {
"enum": [
"cli",
"vscode",
"exec",
"mcp",
"subagent_review",
"subagent_compact",
"subagent_memory_consolidation",
"subagent_thread_spawn",
"subagent_other",
"unknown"
],
"type": "string"
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
@@ -7012,6 +7012,16 @@
"null"
]
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"summary": {
"items": {
"$ref": "#/definitions/ReasoningItemReasoningSummary"
@@ -7152,6 +7162,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -7179,6 +7199,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"output": {
"$ref": "#/definitions/FunctionCallOutputBody"
},
@@ -7254,6 +7284,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"name": {
"type": [
"string",
@@ -7290,6 +7330,16 @@
"execution": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": "string"
},
@@ -7333,6 +7383,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -7358,6 +7418,16 @@
"id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"result": {
"type": "string"
},
@@ -7392,6 +7462,16 @@
"ghost_commit": {
"$ref": "#/definitions/GhostCommit"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"ghost_snapshot"
@@ -7412,6 +7492,16 @@
"encrypted_content": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"compaction"
@@ -7473,6 +7563,16 @@
}
]
},
"session_source": {
"anyOf": [
{
"$ref": "#/definitions/SessionSourceMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -8946,6 +9046,21 @@
}
]
},
"SessionSourceMetadata": {
"enum": [
"cli",
"vscode",
"exec",
"mcp",
"subagent_review",
"subagent_compact",
"subagent_memory_consolidation",
"subagent_thread_spawn",
"subagent_other",
"unknown"
],
"type": "string"
},
"Settings": {
"description": "Settings for a collaboration mode.",
"properties": {
@@ -405,6 +405,16 @@
"null"
]
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"summary": {
"items": {
"$ref": "#/definitions/ReasoningItemReasoningSummary"
@@ -545,6 +555,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -572,6 +592,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"output": {
"$ref": "#/definitions/FunctionCallOutputBody"
},
@@ -647,6 +677,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"name": {
"type": [
"string",
@@ -683,6 +723,16 @@
"execution": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": "string"
},
@@ -726,6 +776,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -751,6 +811,16 @@
"id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"result": {
"type": "string"
},
@@ -785,6 +855,16 @@
"ghost_commit": {
"$ref": "#/definitions/GhostCommit"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"ghost_snapshot"
@@ -805,6 +885,16 @@
"encrypted_content": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"compaction"
@@ -866,6 +956,16 @@
}
]
},
"session_source": {
"anyOf": [
{
"$ref": "#/definitions/SessionSourceMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -1007,6 +1107,21 @@
],
"type": "string"
},
"SessionSourceMetadata": {
"enum": [
"cli",
"vscode",
"exec",
"mcp",
"subagent_review",
"subagent_compact",
"subagent_memory_consolidation",
"subagent_thread_spawn",
"subagent_other",
"unknown"
],
"type": "string"
},
"UserMessageType": {
"enum": [
"prompt",
@@ -471,6 +471,16 @@
"null"
]
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"summary": {
"items": {
"$ref": "#/definitions/ReasoningItemReasoningSummary"
@@ -611,6 +621,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -638,6 +658,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"output": {
"$ref": "#/definitions/FunctionCallOutputBody"
},
@@ -713,6 +743,16 @@
"call_id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"name": {
"type": [
"string",
@@ -749,6 +789,16 @@
"execution": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": "string"
},
@@ -792,6 +842,16 @@
],
"writeOnly": true
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"status": {
"type": [
"string",
@@ -817,6 +877,16 @@
"id": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"result": {
"type": "string"
},
@@ -851,6 +921,16 @@
"ghost_commit": {
"$ref": "#/definitions/GhostCommit"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"ghost_snapshot"
@@ -871,6 +951,16 @@
"encrypted_content": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"$ref": "#/definitions/ResponseItemMetadata"
},
{
"type": "null"
}
]
},
"type": {
"enum": [
"compaction"
@@ -932,6 +1022,16 @@
}
]
},
"session_source": {
"anyOf": [
{
"$ref": "#/definitions/SessionSourceMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -1088,6 +1188,21 @@
],
"type": "string"
},
"SessionSourceMetadata": {
"enum": [
"cli",
"vscode",
"exec",
"mcp",
"subagent_review",
"subagent_compact",
"subagent_memory_consolidation",
"subagent_thread_spawn",
"subagent_other",
"unknown"
],
"type": "string"
},
"UserMessageType": {
"enum": [
"prompt",
@@ -12,8 +12,8 @@ import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSumm
import type { ResponseItemMetadata } from "./ResponseItemMetadata";
import type { WebSearchAction } from "./WebSearchAction";
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, metadata?: ResponseItemMetadata, 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?: ResponseItemMetadata, end_turn?: boolean, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, metadata?: ResponseItemMetadata, } | { "type": "local_shell_call",
/**
* Set when using the Responses API.
*/
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, } | { "type": "ghost_snapshot", ghost_commit: GhostCommit, } | { "type": "compaction", encrypted_content: string, } | { "type": "other" };
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, metadata?: ResponseItemMetadata, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], metadata?: ResponseItemMetadata, } | { "type": "web_search_call", status?: string, action?: WebSearchAction, metadata?: ResponseItemMetadata, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, metadata?: ResponseItemMetadata, } | { "type": "ghost_snapshot", ghost_commit: GhostCommit, metadata?: ResponseItemMetadata, } | { "type": "compaction", encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "other" };
@@ -3,10 +3,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ReviewDecisionMetadata } from "./ReviewDecisionMetadata";
import type { SandboxPolicyMetadata } from "./SandboxPolicyMetadata";
import type { SessionSourceMetadata } from "./SessionSourceMetadata";
import type { UserMessageType } from "./UserMessageType";
export type ResponseItemMetadata = { user_message_type?: UserMessageType,
/**
* Client-visible UUID generated by Codex for this item.
*/
uuid?: string, sandbox_policy?: SandboxPolicyMetadata, is_tool_call_escalated?: boolean, review_decision?: ReviewDecisionMetadata, };
uuid?: string, sandbox_policy?: SandboxPolicyMetadata, is_tool_call_escalated?: boolean, review_decision?: ReviewDecisionMetadata, session_source?: SessionSourceMetadata, };
@@ -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 SessionSourceMetadata = "cli" | "vscode" | "exec" | "mcp" | "subagent_review" | "subagent_compact" | "subagent_memory_consolidation" | "subagent_thread_spawn" | "subagent_other" | "unknown";
@@ -67,6 +67,7 @@ export type { ServerNotification } from "./ServerNotification";
export type { ServerRequest } from "./ServerRequest";
export type { ServiceTier } from "./ServiceTier";
export type { SessionSource } from "./SessionSource";
export type { SessionSourceMetadata } from "./SessionSourceMetadata";
export type { Settings } from "./Settings";
export type { SubAgentSource } from "./SubAgentSource";
export type { ThreadId } from "./ThreadId";
+1
View File
@@ -187,6 +187,7 @@ impl AgentControl {
ResponseItem::FunctionCallOutput {
call_id: call_id.clone(),
output,
metadata: None,
},
));
let initial_history = InitialHistory::Forked(forked_rollout_items);
+3 -5
View File
@@ -551,11 +551,9 @@ async fn spawn_agent_fork_injects_output_for_parent_spawn_call() {
.expect("child thread should be registered");
let history = child_thread.codex.session.clone_history().await;
let injected_output = history.raw_items().iter().find_map(|item| match item {
ResponseItem::FunctionCallOutput { call_id, output }
if call_id == &parent_spawn_call_id =>
{
Some(output)
}
ResponseItem::FunctionCallOutput {
call_id, output, ..
} if call_id == &parent_spawn_call_id => Some(output),
_ => None,
});
let injected_output =
+2
View File
@@ -145,6 +145,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
summary: Vec::new(),
content: None,
encrypted_content: Some("encrypted-old".to_string()),
metadata: None,
}],
&turn_context,
)
@@ -174,6 +175,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
summary: Vec::new(),
content: None,
encrypted_content: Some("encrypted-latest".to_string()),
metadata: None,
}],
&turn_context,
)
+4
View File
@@ -155,6 +155,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
output: FunctionCallOutputPayload::from_text(raw_output.to_string()),
metadata: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -168,6 +169,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
call_id: "call-2".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text(raw_output.to_string()),
metadata: None,
},
];
@@ -187,6 +189,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
output: FunctionCallOutputPayload::from_text(expected_output.to_string()),
metadata: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -200,6 +203,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
call_id: "call-2".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text(expected_output.to_string()),
metadata: None,
},
]
);
+125 -60
View File
@@ -334,6 +334,7 @@ use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::models::SandboxPolicyMetadata;
use codex_protocol::models::SessionSourceMetadata;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::InitialHistory;
@@ -1043,39 +1044,29 @@ fn user_message_metadata_patch(
}
}
fn stamp_message_metadata_on_response_item(
item: ResponseItem,
patch: Option<ResponseItemMetadata>,
) -> ResponseItem {
let Some(patch) = patch else {
return item;
};
if patch.is_empty() {
return item;
}
match item {
ResponseItem::Message {
id,
role,
content,
metadata,
end_turn,
phase,
} if role == "user" => {
let mut metadata_value = metadata.unwrap_or_default();
metadata_value.merge_from(patch);
let metadata = (!metadata_value.is_empty()).then_some(metadata_value);
ResponseItem::Message {
id,
role,
content,
metadata,
end_turn,
phase,
}
fn session_source_to_metadata(session_source: &SessionSource) -> SessionSourceMetadata {
match session_source {
SessionSource::Cli => SessionSourceMetadata::Cli,
SessionSource::VSCode => SessionSourceMetadata::Vscode,
SessionSource::Exec => SessionSourceMetadata::Exec,
SessionSource::Mcp => SessionSourceMetadata::Mcp,
SessionSource::SubAgent(SubAgentSource::Review) => SessionSourceMetadata::SubagentReview,
SessionSource::SubAgent(SubAgentSource::Compact) => SessionSourceMetadata::SubagentCompact,
SessionSource::SubAgent(SubAgentSource::MemoryConsolidation) => {
SessionSourceMetadata::SubagentMemoryConsolidation
}
other => other,
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) => {
SessionSourceMetadata::SubagentThreadSpawn
}
SessionSource::SubAgent(SubAgentSource::Other(_)) => SessionSourceMetadata::SubagentOther,
SessionSource::Unknown => SessionSourceMetadata::Unknown,
}
}
fn session_source_metadata_patch(session_source: &SessionSource) -> ResponseItemMetadata {
ResponseItemMetadata {
session_source: Some(session_source_to_metadata(session_source)),
..ResponseItemMetadata::default()
}
}
@@ -1091,25 +1082,35 @@ fn response_item_tool_call_id(item: &ResponseItem) -> Option<&str> {
}
}
fn tool_call_metadata_slot_mut(
fn response_item_metadata_slot_mut(
item: &mut ResponseItem,
) -> Option<&mut Option<ResponseItemMetadata>> {
match item {
ResponseItem::LocalShellCall { metadata, .. }
ResponseItem::Message { metadata, .. }
| ResponseItem::Reasoning { metadata, .. }
| ResponseItem::LocalShellCall { metadata, .. }
| ResponseItem::FunctionCall { metadata, .. }
| ResponseItem::CustomToolCall { metadata, .. } => Some(metadata),
| ResponseItem::ToolSearchCall { metadata, .. }
| ResponseItem::FunctionCallOutput { metadata, .. }
| ResponseItem::CustomToolCall { metadata, .. }
| ResponseItem::CustomToolCallOutput { metadata, .. }
| ResponseItem::ToolSearchOutput { metadata, .. }
| ResponseItem::WebSearchCall { metadata, .. }
| ResponseItem::ImageGenerationCall { metadata, .. }
| ResponseItem::GhostSnapshot { metadata, .. }
| ResponseItem::Compaction { metadata, .. } => Some(metadata),
_ => None,
}
}
fn stamp_tool_metadata_on_response_item(
fn stamp_response_item_metadata_on_response_item(
mut item: ResponseItem,
patch: ResponseItemMetadata,
) -> ResponseItem {
if patch.is_empty() {
return item;
}
let Some(metadata_slot) = tool_call_metadata_slot_mut(&mut item) else {
let Some(metadata_slot) = response_item_metadata_slot_mut(&mut item) else {
return item;
};
let mut metadata = metadata_slot.take().unwrap_or_default();
@@ -1118,17 +1119,47 @@ fn stamp_tool_metadata_on_response_item(
item
}
fn tool_call_metadata_or_default(item: &ResponseItem) -> Option<ResponseItemMetadata> {
fn response_item_metadata_or_default(item: &ResponseItem) -> Option<ResponseItemMetadata> {
match item {
ResponseItem::LocalShellCall { metadata, .. }
ResponseItem::Message { metadata, .. }
| ResponseItem::Reasoning { metadata, .. }
| ResponseItem::LocalShellCall { metadata, .. }
| ResponseItem::FunctionCall { metadata, .. }
| ResponseItem::CustomToolCall { metadata, .. } => {
Some(metadata.clone().unwrap_or_default())
}
| ResponseItem::ToolSearchCall { metadata, .. }
| ResponseItem::FunctionCallOutput { metadata, .. }
| ResponseItem::CustomToolCall { metadata, .. }
| ResponseItem::CustomToolCallOutput { metadata, .. }
| ResponseItem::ToolSearchOutput { metadata, .. }
| ResponseItem::WebSearchCall { metadata, .. }
| ResponseItem::ImageGenerationCall { metadata, .. }
| ResponseItem::GhostSnapshot { metadata, .. }
| ResponseItem::Compaction { metadata, .. } => Some(metadata.clone().unwrap_or_default()),
_ => None,
}
}
fn review_decision_to_metadata(decision: &ReviewDecision) -> ReviewDecisionMetadata {
match decision {
ReviewDecision::Approved => ReviewDecisionMetadata::Approved,
ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
ReviewDecisionMetadata::ApprovedWithAmendment
}
ReviewDecision::ApprovedForSession => ReviewDecisionMetadata::ApprovedForSession,
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
codex_protocol::protocol::NetworkPolicyRuleAction::Allow => {
ReviewDecisionMetadata::ApprovedWithNetworkPolicyAllow
}
codex_protocol::protocol::NetworkPolicyRuleAction::Deny => {
ReviewDecisionMetadata::DeniedWithNetworkPolicyDeny
}
},
ReviewDecision::Denied => ReviewDecisionMetadata::Denied,
ReviewDecision::Abort => ReviewDecisionMetadata::Abort,
}
}
#[derive(Clone, Debug, Default)]
struct ToolApprovalMetadataSnapshot {
approval_outcomes_by_call_id: HashMap<String, ApprovalOutcomeMetadata>,
@@ -1150,7 +1181,7 @@ fn stamp_tool_approval_metadata_with_snapshot(
let outcome = snapshot.approval_outcomes_by_call_id.get(call_id).cloned();
let has_pending_approval = snapshot.pending_approval_call_ids.contains(call_id);
let mut metadata = match tool_call_metadata_or_default(&response_item) {
let mut metadata = match response_item_metadata_or_default(&response_item) {
Some(metadata) => metadata,
None => return response_item,
};
@@ -1168,11 +1199,11 @@ fn stamp_tool_approval_metadata_with_snapshot(
metadata.review_decision = None;
}
None => {
return stamp_tool_metadata_on_response_item(response_item, metadata);
return stamp_response_item_metadata_on_response_item(response_item, metadata);
}
}
stamp_tool_metadata_on_response_item(response_item, metadata)
stamp_response_item_metadata_on_response_item(response_item, metadata)
}
#[derive(Clone)]
@@ -4033,6 +4064,14 @@ impl Session {
turn_context: &TurnContext,
response_item: ResponseItem,
) {
let response_item = if self.enabled(Feature::ItemMetadata) {
stamp_response_item_metadata_on_response_item(
response_item,
session_source_metadata_patch(&turn_context.session_source),
)
} else {
response_item
};
let response_item = self
.stamp_tool_approval_metadata(turn_context, response_item)
.await;
@@ -4060,11 +4099,15 @@ impl Session {
patch.sandbox_policy = Some(sandbox_policy_to_metadata(
turn_context.sandbox_policy.get(),
));
patch.merge_from(session_source_metadata_patch(&turn_context.session_source));
Some(patch)
} else {
None
};
let response_item = stamp_message_metadata_on_response_item(response_item, metadata_patch);
let response_item = match metadata_patch {
Some(patch) => stamp_response_item_metadata_on_response_item(response_item, patch),
None => response_item,
};
// Persist the user message to history, but emit the turn item from `UserInput` so
// UI-only `text_elements` are preserved. `ResponseItem::Message` does not carry
@@ -4165,11 +4208,17 @@ impl Session {
}
let mut input_item: ResponseInputItem = input.into();
let metadata = Some(user_message_metadata_patch(
let mut metadata = user_message_metadata_patch(
UserMessageType::PromptSteering,
self.enabled(Feature::ItemMetadata)
.then(|| sandbox_policy_to_metadata(active_task.turn_context.sandbox_policy.get())),
));
);
if self.enabled(Feature::ItemMetadata) {
metadata.merge_from(session_source_metadata_patch(
&active_task.turn_context.session_source,
));
}
let metadata = Some(metadata);
if self.enabled(Feature::ItemMetadata)
&& let Some(metadata_patch) = metadata.as_ref()
{
@@ -4189,20 +4238,31 @@ impl Session {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let sandbox_policy = at.tasks.first().map(|(_, turn_context)| {
sandbox_policy_to_metadata(turn_context.turn_context.sandbox_policy.get())
let first_turn_context = at.tasks.first().map(|(_, task)| &task.turn_context);
let sandbox_policy = first_turn_context.map(|turn_context| {
sandbox_policy_to_metadata(turn_context.sandbox_policy.get())
});
let session_source =
first_turn_context.map(|turn_context| turn_context.session_source.clone());
let mut ts = at.turn_state.lock().await;
for mut item in input {
let metadata = match &item {
ResponseInputItem::Message { .. } => Some(user_message_metadata_patch(
UserMessageType::PromptQueued,
if self.enabled(Feature::ItemMetadata) {
sandbox_policy.clone()
} else {
None
},
)),
ResponseInputItem::Message { .. } => {
let mut metadata = user_message_metadata_patch(
UserMessageType::PromptQueued,
if self.enabled(Feature::ItemMetadata) {
sandbox_policy.clone()
} else {
None
},
);
if self.enabled(Feature::ItemMetadata)
&& let Some(session_source) = session_source.as_ref()
{
metadata.merge_from(session_source_metadata_patch(session_source));
}
Some(metadata)
}
_ => None,
};
@@ -5920,11 +5980,16 @@ pub(crate) async fn run_turn(
.await;
let mut initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input.clone());
let initial_message_metadata = Some(user_message_metadata_patch(
let mut initial_message_metadata = user_message_metadata_patch(
UserMessageType::Prompt,
sess.enabled(Feature::ItemMetadata)
.then(|| sandbox_policy_to_metadata(turn_context.sandbox_policy.get())),
));
);
if sess.enabled(Feature::ItemMetadata) {
initial_message_metadata
.merge_from(session_source_metadata_patch(&turn_context.session_source));
}
let initial_message_metadata = Some(initial_message_metadata);
if sess.enabled(Feature::ItemMetadata)
&& let Some(metadata_patch) = initial_message_metadata.as_ref()
{
+260
View File
@@ -17,6 +17,9 @@ use crate::tools::format_exec_output_str;
use codex_protocol::ThreadId;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::LocalShellAction;
use codex_protocol::models::LocalShellExecAction;
use codex_protocol::models::LocalShellStatus;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -61,6 +64,7 @@ use codex_app_server_protocol::AppInfo;
use codex_execpolicy::Decision;
use codex_execpolicy::NetworkRuleProtocol;
use codex_execpolicy::Policy;
use codex_git::GhostCommit;
use codex_network_proxy::NetworkProxyConfig;
use codex_otel::TelemetryAuthMode;
use codex_protocol::models::BaseInstructions;
@@ -68,6 +72,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::WebSearchAction;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::ConversationAudioParams;
use codex_protocol::protocol::RealtimeAudioFrame;
@@ -3679,6 +3684,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
status: "completed".to_string(),
revised_prompt: Some("a tiny blue square".to_string()),
result: "Zm9v".to_string(),
metadata: None,
}],
None,
)
@@ -3722,6 +3728,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
status: "completed".to_string(),
revised_prompt: Some("a tiny blue square".to_string()),
result: "Zm9v".to_string(),
metadata: None,
};
let mut ctx = HandleOutputCtx {
@@ -3762,6 +3769,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
status: "completed".to_string(),
revised_prompt: Some("broken payload".to_string()),
result: "_-8".to_string(),
metadata: None,
};
let mut ctx = HandleOutputCtx {
@@ -4355,6 +4363,8 @@ async fn task_finish_emits_prompt_queued_metadata_for_injected_user_input_when_f
..
} if metadata.user_message_type
== Some(codex_protocol::models::UserMessageType::PromptQueued)
&& metadata.session_source
== Some(codex_protocol::models::SessionSourceMetadata::Exec)
)
));
@@ -4389,6 +4399,203 @@ async fn task_finish_emits_prompt_queued_metadata_for_injected_user_input_when_f
));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn record_response_item_stamps_session_source_on_all_supported_variants() {
let (mut sess, tc, _rx) = make_session_and_context_with_rx().await;
Arc::get_mut(&mut sess)
.expect("session should be uniquely owned in this test")
.features
.enable(crate::features::Feature::ItemMetadata)
.expect("feature flag should be enabled for this test");
sess.spawn_task(
Arc::clone(&tc),
vec![UserInput::Text {
text: "start".to_string(),
text_elements: Vec::new(),
}],
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: false,
},
)
.await;
let items = vec![
ResponseItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: "hello".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
ResponseItem::Reasoning {
id: "reasoning-1".to_string(),
summary: vec![],
content: None,
encrypted_content: None,
metadata: None,
},
ResponseItem::LocalShellCall {
id: None,
call_id: Some("shell-1".to_string()),
status: LocalShellStatus::Completed,
action: LocalShellAction::Exec(LocalShellExecAction {
command: vec!["echo".to_string(), "hello".to_string()],
timeout_ms: None,
working_directory: None,
env: None,
user: None,
}),
metadata: None,
},
ResponseItem::FunctionCall {
id: None,
name: "shell".to_string(),
namespace: None,
arguments: "{}".to_string(),
call_id: "call-1".to_string(),
metadata: None,
},
ResponseItem::ToolSearchCall {
id: None,
call_id: Some("search-1".to_string()),
status: None,
execution: "client".to_string(),
arguments: serde_json::json!({"query": "calendar"}),
metadata: None,
},
ResponseItem::FunctionCallOutput {
call_id: "call-output-1".to_string(),
output: FunctionCallOutputPayload {
body: FunctionCallOutputBody::Text("ok".to_string()),
success: None,
},
metadata: None,
},
ResponseItem::CustomToolCall {
id: None,
status: Some("completed".to_string()),
call_id: "custom-1".to_string(),
name: "custom".to_string(),
input: "payload".to_string(),
metadata: None,
},
ResponseItem::CustomToolCallOutput {
call_id: "custom-output-1".to_string(),
output: FunctionCallOutputPayload {
body: FunctionCallOutputBody::Text("done".to_string()),
success: None,
},
metadata: None,
},
ResponseItem::ToolSearchOutput {
call_id: Some("search-out-1".to_string()),
status: "completed".to_string(),
execution: "client".to_string(),
tools: vec![],
metadata: None,
},
ResponseItem::WebSearchCall {
id: None,
status: Some("completed".to_string()),
action: Some(WebSearchAction::Search {
query: Some("weather".to_string()),
queries: None,
}),
metadata: None,
},
ResponseItem::ImageGenerationCall {
id: "image-1".to_string(),
status: "completed".to_string(),
revised_prompt: Some("cat".to_string()),
result: "image-result".to_string(),
metadata: None,
},
ResponseItem::GhostSnapshot {
ghost_commit: GhostCommit::new("deadbeef".to_string(), None, Vec::new(), Vec::new()),
metadata: None,
},
ResponseItem::Compaction {
encrypted_content: "blob".to_string(),
metadata: None,
},
];
for item in items {
sess.record_response_item_and_emit_turn_item(tc.as_ref(), item)
.await;
}
let expected_session_source = codex_protocol::models::SessionSourceMetadata::Exec;
let stamped_count = sess
.clone_history()
.await
.raw_items()
.iter()
.filter(|item| match item {
ResponseItem::Message {
metadata: Some(metadata),
..
}
| ResponseItem::Reasoning {
metadata: Some(metadata),
..
}
| ResponseItem::LocalShellCall {
metadata: Some(metadata),
..
}
| ResponseItem::FunctionCall {
metadata: Some(metadata),
..
}
| ResponseItem::ToolSearchCall {
metadata: Some(metadata),
..
}
| ResponseItem::FunctionCallOutput {
metadata: Some(metadata),
..
}
| ResponseItem::CustomToolCall {
metadata: Some(metadata),
..
}
| ResponseItem::CustomToolCallOutput {
metadata: Some(metadata),
..
}
| ResponseItem::ToolSearchOutput {
metadata: Some(metadata),
..
}
| ResponseItem::WebSearchCall {
metadata: Some(metadata),
..
}
| ResponseItem::ImageGenerationCall {
metadata: Some(metadata),
..
}
| ResponseItem::GhostSnapshot {
metadata: Some(metadata),
..
}
| ResponseItem::Compaction {
metadata: Some(metadata),
..
} => metadata.session_source == Some(expected_session_source.clone()),
_ => false,
})
.count();
assert_eq!(stamped_count, 13);
}
#[test]
fn review_decision_metadata_mapping_is_stable() {
assert_eq!(
@@ -4454,6 +4661,59 @@ fn sandbox_policy_metadata_mapping_is_stable() {
);
}
#[test]
fn session_source_metadata_mapping_is_stable() {
assert_eq!(
session_source_to_metadata(&SessionSource::Cli),
codex_protocol::models::SessionSourceMetadata::Cli
);
assert_eq!(
session_source_to_metadata(&SessionSource::VSCode),
codex_protocol::models::SessionSourceMetadata::Vscode
);
assert_eq!(
session_source_to_metadata(&SessionSource::Exec),
codex_protocol::models::SessionSourceMetadata::Exec
);
assert_eq!(
session_source_to_metadata(&SessionSource::Mcp),
codex_protocol::models::SessionSourceMetadata::Mcp
);
assert_eq!(
session_source_to_metadata(&SessionSource::SubAgent(SubAgentSource::Review)),
codex_protocol::models::SessionSourceMetadata::SubagentReview
);
assert_eq!(
session_source_to_metadata(&SessionSource::SubAgent(SubAgentSource::Compact)),
codex_protocol::models::SessionSourceMetadata::SubagentCompact
);
assert_eq!(
session_source_to_metadata(&SessionSource::SubAgent(
SubAgentSource::MemoryConsolidation
)),
codex_protocol::models::SessionSourceMetadata::SubagentMemoryConsolidation
);
assert_eq!(
session_source_to_metadata(&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: ThreadId::default(),
depth: 2,
agent_nickname: Some("mini".to_string()),
agent_role: Some("reviewer".to_string()),
})),
codex_protocol::models::SessionSourceMetadata::SubagentThreadSpawn
);
assert_eq!(
session_source_to_metadata(&SessionSource::SubAgent(SubAgentSource::Other(
"custom".to_string()
))),
codex_protocol::models::SessionSourceMetadata::SubagentOther
);
assert_eq!(
session_source_to_metadata(&SessionSource::Unknown),
codex_protocol::models::SessionSourceMetadata::Unknown
);
}
async fn setup_tool_call_metadata_runtime_test() -> (
Arc<Session>,
Arc<TurnContext>,
+4
View File
@@ -563,6 +563,8 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last() {
let compacted_history = vec![ResponseItem::Compaction {
encrypted_content: "encrypted".to_string(),
metadata: None,
}];
let initial_context = vec![ResponseItem::Message {
id: None,
@@ -590,6 +592,8 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
},
ResponseItem::Compaction {
encrypted_content: "encrypted".to_string(),
metadata: None,
},
];
assert_eq!(refreshed, expected);
+14 -9
View File
@@ -348,23 +348,27 @@ impl ContextManager {
fn process_item(&self, item: &ResponseItem, policy: TruncationPolicy) -> ResponseItem {
let policy_with_serialization_budget = policy * 1.2;
match item {
ResponseItem::FunctionCallOutput { call_id, output } => {
ResponseItem::FunctionCallOutput {
call_id: call_id.clone(),
output: truncate_function_output_payload(
output,
policy_with_serialization_budget,
),
}
}
ResponseItem::FunctionCallOutput {
call_id,
output,
metadata,
..
} => ResponseItem::FunctionCallOutput {
call_id: call_id.clone(),
output: truncate_function_output_payload(output, policy_with_serialization_budget),
metadata: metadata.clone(),
},
ResponseItem::CustomToolCallOutput {
call_id,
name,
output,
metadata,
..
} => ResponseItem::CustomToolCallOutput {
call_id: call_id.clone(),
name: name.clone(),
output: truncate_function_output_payload(output, policy_with_serialization_budget),
metadata: metadata.clone(),
},
ResponseItem::Message { .. }
| ResponseItem::Reasoning { .. }
@@ -463,6 +467,7 @@ pub(crate) fn estimate_response_item_model_visible_bytes(item: &ResponseItem) ->
}
| ResponseItem::Compaction {
encrypted_content: content,
..
} => i64::try_from(estimate_reasoning_length(content.len())).unwrap_or(i64::MAX),
item => {
let raw = serde_json::to_string(item)
@@ -78,6 +78,8 @@ fn custom_tool_call_output(call_id: &str, output: &str) -> ResponseItem {
call_id: call_id.to_string(),
name: None,
output: FunctionCallOutputPayload::from_text(output.to_string()),
metadata: None,
}
}
@@ -91,6 +93,8 @@ fn reasoning_msg(text: &str) -> ResponseItem {
text: text.to_string(),
}]),
encrypted_content: None,
metadata: None,
}
}
@@ -102,6 +106,8 @@ fn reasoning_with_encrypted_content(len: usize) -> ResponseItem {
}],
content: None,
encrypted_content: Some("a".repeat(len)),
metadata: None,
}
}
@@ -149,6 +155,8 @@ fn filters_non_api_messages() {
text: "thinking...".to_string(),
}]),
encrypted_content: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -295,6 +303,8 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
detail: None,
},
]),
metadata: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -316,6 +326,8 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
detail: None,
},
]),
metadata: None,
},
];
let history = create_history_with_items(items);
@@ -361,6 +373,8 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
.to_string(),
},
]),
metadata: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -382,6 +396,8 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
.to_string(),
},
]),
metadata: None,
},
];
assert_eq!(stripped, expected);
@@ -421,6 +437,8 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -442,6 +460,8 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -475,6 +495,8 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
metadata: None,
},
]);
@@ -496,6 +518,8 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: String::new(),
metadata: None,
},
]
);
@@ -505,6 +529,8 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
fn get_history_for_prompt_drops_ghost_commits() {
let items = vec![ResponseItem::GhostSnapshot {
ghost_commit: GhostCommit::new("ghost-1".to_string(), None, Vec::new(), Vec::new()),
metadata: None,
}];
let history = create_history_with_items(items);
let modalities = default_input_modalities();
@@ -548,6 +574,8 @@ fn remove_first_item_removes_matching_output_for_function_call() {
ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
},
];
let mut h = create_history_with_items(items);
@@ -561,6 +589,8 @@ fn remove_first_item_removes_matching_call_for_output() {
ResponseItem::FunctionCallOutput {
call_id: "call-2".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
},
ResponseItem::FunctionCall {
id: None,
@@ -591,6 +621,8 @@ fn remove_last_item_removes_matching_call_for_output() {
ResponseItem::FunctionCallOutput {
call_id: "call-delete-last".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
},
];
let mut h = create_history_with_items(items);
@@ -614,6 +646,8 @@ fn replace_last_turn_images_replaces_tool_output_images() {
]),
success: Some(true),
},
metadata: None,
},
];
let mut history = create_history_with_items(items);
@@ -634,6 +668,8 @@ fn replace_last_turn_images_replaces_tool_output_images() {
]),
success: Some(true),
},
metadata: None,
},
]
);
@@ -676,6 +712,8 @@ fn remove_first_item_handles_local_shell_pair() {
ResponseItem::FunctionCallOutput {
call_id: "call-3".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
},
];
let mut h = create_history_with_items(items);
@@ -834,6 +872,8 @@ fn remove_first_item_handles_custom_tool_pair() {
call_id: "tool-1".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
},
];
let mut h = create_history_with_items(items);
@@ -860,6 +900,8 @@ fn normalization_retains_local_shell_outputs() {
ResponseItem::FunctionCallOutput {
call_id: "shell-1".to_string(),
output: FunctionCallOutputPayload::from_text("Total output lines: 1\n\nok".to_string()),
metadata: None,
},
];
@@ -883,6 +925,8 @@ fn record_items_truncates_function_call_output_content() {
body: FunctionCallOutputBody::Text(long_output.clone()),
success: Some(true),
},
metadata: None,
};
history.record_items([&item], policy);
@@ -915,6 +959,8 @@ fn record_items_truncates_custom_tool_call_output_content() {
call_id: "tool-200".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text(long_output.clone()),
metadata: None,
};
history.record_items([&item], policy);
@@ -948,6 +994,8 @@ fn record_items_respects_custom_token_limit() {
body: FunctionCallOutputBody::Text(long_output),
success: Some(true),
},
metadata: None,
};
history.record_items([&item], policy);
@@ -1087,6 +1135,8 @@ fn normalize_adds_missing_output_for_function_call() {
ResponseItem::FunctionCallOutput {
call_id: "call-x".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
]
);
@@ -1122,6 +1172,8 @@ fn normalize_adds_missing_output_for_custom_tool_call() {
call_id: "tool-x".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
]
);
@@ -1166,6 +1218,8 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
ResponseItem::FunctionCallOutput {
call_id: "shell-1".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
]
);
@@ -1177,6 +1231,8 @@ fn normalize_removes_orphan_function_call_output() {
let items = vec![ResponseItem::FunctionCallOutput {
call_id: "orphan-1".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
}];
let mut h = create_history_with_items(items);
@@ -1192,6 +1248,8 @@ fn normalize_removes_orphan_custom_tool_call_output() {
call_id: "orphan-2".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
}];
let mut h = create_history_with_items(items);
@@ -1217,6 +1275,8 @@ fn normalize_mixed_inserts_and_removals() {
ResponseItem::FunctionCallOutput {
call_id: "c2".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
},
// Will get an inserted custom tool output
ResponseItem::CustomToolCall {
@@ -1260,6 +1320,8 @@ fn normalize_mixed_inserts_and_removals() {
ResponseItem::FunctionCallOutput {
call_id: "c1".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -1273,6 +1335,8 @@ fn normalize_mixed_inserts_and_removals() {
call_id: "t1".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
ResponseItem::LocalShellCall {
id: None,
@@ -1289,6 +1353,8 @@ fn normalize_mixed_inserts_and_removals() {
ResponseItem::FunctionCallOutput {
call_id: "s1".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
]
);
@@ -1320,6 +1386,8 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
ResponseItem::FunctionCallOutput {
call_id: "call-x".to_string(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
]
);
@@ -1333,6 +1401,8 @@ fn normalize_adds_missing_output_for_tool_search_call() {
status: Some("completed".to_string()),
execution: "client".to_string(),
arguments: "{}".into(),
metadata: None,
}];
let mut h = create_history_with_items(items);
@@ -1347,12 +1417,16 @@ fn normalize_adds_missing_output_for_tool_search_call() {
status: Some("completed".to_string()),
execution: "client".to_string(),
arguments: "{}".into(),
metadata: None,
},
ResponseItem::ToolSearchOutput {
call_id: Some("search-call-x".to_string()),
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
},
]
);
@@ -1402,6 +1476,8 @@ fn normalize_removes_orphan_function_call_output_panics_in_debug() {
let items = vec![ResponseItem::FunctionCallOutput {
call_id: "orphan-1".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1415,6 +1491,8 @@ fn normalize_removes_orphan_custom_tool_call_output_panics_in_debug() {
call_id: "orphan-2".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1428,6 +1506,8 @@ fn normalize_removes_orphan_client_tool_search_output() {
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
}];
let mut h = create_history_with_items(items);
@@ -1445,6 +1525,8 @@ fn normalize_removes_orphan_client_tool_search_output_panics_in_debug() {
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
}];
let mut h = create_history_with_items(items);
h.normalize_history(&default_input_modalities());
@@ -1457,6 +1539,8 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() {
status: "completed".to_string(),
execution: "server".to_string(),
tools: Vec::new(),
metadata: None,
}];
let mut h = create_history_with_items(items);
@@ -1469,6 +1553,8 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() {
status: "completed".to_string(),
execution: "server".to_string(),
tools: Vec::new(),
metadata: None,
}]
);
}
@@ -1489,6 +1575,8 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
ResponseItem::FunctionCallOutput {
call_id: "c2".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
},
ResponseItem::CustomToolCall {
id: None,
@@ -1569,6 +1657,8 @@ fn image_data_url_payload_does_not_dominate_function_call_output_estimate() {
detail: None,
},
]),
metadata: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1595,6 +1685,8 @@ fn image_data_url_payload_does_not_dominate_custom_tool_call_output_estimate() {
detail: None,
},
]),
metadata: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1625,6 +1717,8 @@ fn non_base64_image_urls_are_unchanged() {
detail: None,
},
]),
metadata: None,
};
assert_eq!(
@@ -1668,6 +1762,8 @@ fn non_image_base64_data_url_is_unchanged() {
detail: None,
},
]),
metadata: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1752,6 +1848,8 @@ fn original_detail_images_scale_with_dimensions() {
detail: Some(ImageDetail::Original),
},
]),
metadata: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1783,6 +1881,8 @@ fn original_detail_webp_images_scale_with_dimensions() {
detail: Some(ImageDetail::Original),
},
]),
metadata: None,
};
let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -34,6 +34,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
ResponseItem::FunctionCallOutput {
call_id: call_id.clone(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
));
}
@@ -59,6 +60,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
status: "completed".to_string(),
execution: "client".to_string(),
tools: Vec::new(),
metadata: None,
},
));
}
@@ -81,6 +83,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
call_id: call_id.clone(),
name: None,
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
));
}
@@ -104,6 +107,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
ResponseItem::FunctionCallOutput {
call_id: call_id.clone(),
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
metadata: None,
},
));
}
+1
View File
@@ -155,6 +155,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
status,
revised_prompt,
result,
..
} => Some(TurnItem::ImageGeneration(
codex_protocol::items::ImageGenerationItem {
id: id.clone(),
+12
View File
@@ -323,6 +323,8 @@ fn parses_reasoning_summary_and_raw_content() {
text: "raw details".to_string(),
}]),
encrypted_content: None,
metadata: None,
};
let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
@@ -355,6 +357,8 @@ fn parses_reasoning_including_raw_content() {
},
]),
encrypted_content: None,
metadata: None,
};
let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
@@ -380,6 +384,8 @@ fn parses_web_search_call() {
query: Some("weather".to_string()),
queries: None,
}),
metadata: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -408,6 +414,8 @@ fn parses_web_search_open_page_call() {
action: Some(WebSearchAction::OpenPage {
url: Some("https://example.com".to_string()),
}),
metadata: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -436,6 +444,8 @@ fn parses_web_search_find_in_page_call() {
url: Some("https://example.com".to_string()),
pattern: Some("needle".to_string()),
}),
metadata: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -462,6 +472,8 @@ fn parses_partial_web_search_call_without_action_as_other() {
id: Some("ws_partial".to_string()),
status: Some("in_progress".to_string()),
action: None,
metadata: None,
};
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
+2
View File
@@ -103,6 +103,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
output: codex_protocol::models::FunctionCallOutputPayload::from_text(
"repo visibility: public".to_string(),
),
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -219,6 +220,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
output: codex_protocol::models::FunctionCallOutputPayload::from_text(
"repo is public".to_string(),
),
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -47,6 +47,8 @@ fn truncates_rollout_from_start_before_nth_user_only() {
}],
content: None,
encrypted_content: None,
metadata: None,
},
ResponseItem::FunctionCall {
id: None,
+4
View File
@@ -386,6 +386,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
Some(ResponseItem::FunctionCallOutput {
call_id: call_id.clone(),
output: output.clone(),
metadata: None,
})
}
ResponseInputItem::CustomToolCallOutput {
@@ -396,12 +397,14 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
call_id: call_id.clone(),
name: name.clone(),
output: output.clone(),
metadata: None,
}),
ResponseInputItem::McpToolCallOutput { call_id, output } => {
let output = output.as_function_call_output_payload();
Some(ResponseItem::FunctionCallOutput {
call_id: call_id.clone(),
output,
metadata: None,
})
}
ResponseInputItem::ToolSearchOutput {
@@ -414,6 +417,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
status: status.clone(),
execution: execution.clone(),
tools: tools.clone(),
metadata: None,
}),
_ => None,
}
@@ -112,6 +112,7 @@ impl SessionTask for GhostSnapshotTask {
.session
.record_conversation_items(&ctx, &[ResponseItem::GhostSnapshot {
ghost_commit: ghost_commit.clone(),
metadata: None,
}])
.await;
info!("ghost commit captured: {}", ghost_commit.id());
+1 -1
View File
@@ -81,7 +81,7 @@ impl SessionTask for UndoTask {
.enumerate()
.rev()
.find_map(|(idx, item)| match item {
ResponseItem::GhostSnapshot { ghost_commit } => {
ResponseItem::GhostSnapshot { ghost_commit, .. } => {
Some((idx, ghost_commit.clone()))
}
_ => None,
@@ -54,6 +54,8 @@ fn drops_from_last_user_only() {
}],
content: None,
encrypted_content: None,
metadata: None,
},
ResponseItem::FunctionCall {
id: None,
+2
View File
@@ -126,6 +126,8 @@ fn response_item_records_turn_ttft_ignores_empty_non_output_items() {
&ResponseItem::FunctionCallOutput {
call_id: "call-1".to_string(),
output: FunctionCallOutputPayload::from_text("ok".to_string()),
metadata: None,
}
));
}
+7
View File
@@ -395,6 +395,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
call_id: "legacy-js-call".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("legacy js_repl stdout".to_string()),
metadata: None,
}),
},
RolloutLine {
@@ -537,6 +538,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
detail: Some(ImageDetail::Original),
},
]),
metadata: None,
}),
},
RolloutLine {
@@ -561,6 +563,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
detail: Some(ImageDetail::Original),
},
]),
metadata: None,
}),
},
];
@@ -1857,6 +1860,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
text: "content".into(),
}]),
encrypted_content: None,
metadata: None,
});
prompt.input.push(ResponseItem::Message {
id: Some("message-id".into()),
@@ -1875,6 +1879,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
query: Some("weather".into()),
queries: None,
}),
metadata: None,
});
prompt.input.push(ResponseItem::FunctionCall {
id: Some("function-id".into()),
@@ -1887,6 +1892,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
prompt.input.push(ResponseItem::FunctionCallOutput {
call_id: "function-call-id".into(),
output: FunctionCallOutputPayload::from_text("ok".into()),
metadata: None,
});
prompt.input.push(ResponseItem::LocalShellCall {
id: Some("local-shell-id".into()),
@@ -1913,6 +1919,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
call_id: "custom-tool-call-id".into(),
name: None,
output: FunctionCallOutputPayload::from_text("ok".into()),
metadata: None,
});
let mut stream = client_session
+3
View File
@@ -1594,6 +1594,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
},
codex_protocol::models::ResponseItem::Compaction {
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
];
let compact_mock =
@@ -2804,6 +2805,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
},
codex_protocol::models::ResponseItem::Compaction {
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
];
let compact_mock =
@@ -2925,6 +2927,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
},
codex_protocol::models::ResponseItem::Compaction {
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
];
let compact_mock =
+10 -1
View File
@@ -79,6 +79,7 @@ fn format_labeled_requests_snapshot(
fn compacted_summary_only_output(summary: &str) -> Vec<ResponseItem> {
vec![ResponseItem::Compaction {
encrypted_content: summary_with_prefix(summary),
metadata: None,
}]
}
@@ -221,6 +222,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
let compacted_history = vec![ResponseItem::Compaction {
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
}];
let compact_mock = responses::mount_compact_json_once(
harness.server(),
@@ -1117,6 +1119,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
let compacted_history = vec![
ResponseItem::Compaction {
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -1172,7 +1175,10 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
let has_compaction_item = replacement_history.iter().any(|item| {
matches!(
item,
ResponseItem::Compaction { encrypted_content }
ResponseItem::Compaction {
encrypted_content,
..
}
if encrypted_content == "ENCRYPTED_COMPACTION_SUMMARY"
)
});
@@ -1268,6 +1274,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
},
ResponseItem::Compaction {
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
];
let compact_mock = responses::mount_compact_json_once(
@@ -1401,6 +1408,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
},
ResponseItem::Compaction {
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
];
let compact_mock = responses::mount_compact_json_once(
@@ -2412,6 +2420,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
let compacted_history = vec![ResponseItem::Compaction {
encrypted_content: summary_with_prefix("REMOTE_SUMMARY_ONLY"),
metadata: None,
}];
let compact_mock = responses::mount_compact_json_once(
harness.server(),
+14
View File
@@ -215,6 +215,13 @@ async fn user_message_type_metadata_is_emitted_when_feature_enabled() -> anyhow:
.and_then(Value::as_str),
Some("prompt")
);
assert_eq!(
user_message
.get("metadata")
.and_then(|metadata| metadata.get("session_source"))
.and_then(Value::as_str),
Some("exec")
);
Ok(())
}
@@ -344,6 +351,13 @@ async fn user_message_type_prompt_steering_metadata_is_emitted_when_feature_enab
.and_then(Value::as_str),
Some("prompt_steering")
);
assert_eq!(
steered_message
.get("metadata")
.and_then(|metadata| metadata.get("session_source"))
.and_then(Value::as_str),
Some("exec")
);
Ok(())
}
+101 -4
View File
@@ -291,6 +291,22 @@ pub enum SandboxPolicyMetadata {
FullAccess,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum SessionSourceMetadata {
Cli,
Vscode,
Exec,
Mcp,
SubagentReview,
SubagentCompact,
SubagentMemoryConsolidation,
SubagentThreadSpawn,
SubagentOther,
Unknown,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
pub struct ResponseItemMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -309,6 +325,9 @@ pub struct ResponseItemMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub review_decision: Option<ReviewDecisionMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub session_source: Option<SessionSourceMetadata>,
}
impl ResponseItemMetadata {
@@ -318,6 +337,7 @@ impl ResponseItemMetadata {
&& self.sandbox_policy.is_none()
&& self.is_tool_call_escalated.is_none()
&& self.review_decision.is_none()
&& self.session_source.is_none()
}
pub fn merge_from(&mut self, other: Self) {
@@ -336,6 +356,9 @@ impl ResponseItemMetadata {
if other.review_decision.is_some() {
self.review_decision = other.review_decision;
}
if other.session_source.is_some() {
self.session_source = other.session_source;
}
}
}
@@ -442,6 +465,9 @@ pub enum ResponseItem {
#[ts(optional)]
content: Option<Vec<ReasoningItemContent>>,
encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
LocalShellCall {
/// Legacy id field retained for compatibility with older payloads.
@@ -484,6 +510,9 @@ pub enum ResponseItem {
execution: String,
#[ts(type = "unknown")]
arguments: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
// NOTE: The `output` field for `function_call_output` uses a dedicated payload type with
// custom serialization. On the wire it is either:
@@ -495,6 +524,9 @@ pub enum ResponseItem {
#[ts(as = "FunctionCallOutputBody")]
#[schemars(with = "FunctionCallOutputBody")]
output: FunctionCallOutputPayload,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
CustomToolCall {
#[serde(default, skip_serializing)]
@@ -522,6 +554,9 @@ pub enum ResponseItem {
#[ts(as = "FunctionCallOutputBody")]
#[schemars(with = "FunctionCallOutputBody")]
output: FunctionCallOutputPayload,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
ToolSearchOutput {
call_id: Option<String>,
@@ -529,6 +564,9 @@ pub enum ResponseItem {
execution: String,
#[ts(type = "unknown[]")]
tools: Vec<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
// Emitted by the Responses API when the agent triggers a web search.
// Example payload (from SSE `response.output_item.done`):
@@ -548,6 +586,9 @@ pub enum ResponseItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
action: Option<WebSearchAction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
// Emitted by the Responses API when the agent triggers image generation.
// Example payload:
@@ -565,14 +606,23 @@ pub enum ResponseItem {
#[ts(optional)]
revised_prompt: Option<String>,
result: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
// Generated by the harness but considered exactly as a model response.
GhostSnapshot {
ghost_commit: GhostCommit,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
#[serde(alias = "compaction_summary")]
Compaction {
encrypted_content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
},
#[serde(other)]
Other,
@@ -1136,12 +1186,18 @@ impl From<ResponseInputItem> for ResponseItem {
end_turn: None,
phase: None,
},
ResponseInputItem::FunctionCallOutput { call_id, output } => {
Self::FunctionCallOutput { call_id, output }
}
ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput {
call_id,
output,
metadata: None,
},
ResponseInputItem::McpToolCallOutput { call_id, output } => {
let output = output.into_function_call_output_payload();
Self::FunctionCallOutput { call_id, output }
Self::FunctionCallOutput {
call_id,
output,
metadata: None,
}
}
ResponseInputItem::CustomToolCallOutput {
call_id,
@@ -1151,6 +1207,7 @@ impl From<ResponseInputItem> for ResponseItem {
call_id,
name,
output,
metadata: None,
},
ResponseInputItem::ToolSearchOutput {
call_id,
@@ -1162,6 +1219,7 @@ impl From<ResponseInputItem> for ResponseItem {
status,
execution,
tools,
metadata: None,
},
}
}
@@ -1729,6 +1787,7 @@ mod tests {
status: "completed".to_string(),
revised_prompt: Some("A small blue square".to_string()),
result: "Zm9v".to_string(),
metadata: None,
}
);
}
@@ -1750,6 +1809,7 @@ mod tests {
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
metadata: None,
}
);
}
@@ -2623,6 +2683,7 @@ mod tests {
item,
ResponseItem::Compaction {
encrypted_content: "abc".into(),
metadata: None,
}
);
Ok(())
@@ -2703,6 +2764,7 @@ mod tests {
id: expected_id.clone(),
status: expected_status.clone(),
action: expected_action.clone(),
metadata: None,
};
assert_eq!(parsed, expected);
@@ -2793,6 +2855,7 @@ mod tests {
"query": "calendar create",
"limit": 1,
}),
metadata: None,
}
);
@@ -2853,6 +2916,7 @@ mod tests {
"additionalProperties": false,
}
})],
metadata: None,
}
);
@@ -2906,6 +2970,7 @@ mod tests {
arguments: serde_json::json!({
"paths": ["crm"],
}),
metadata: None,
}
);
@@ -2925,6 +2990,7 @@ mod tests {
status: "completed".to_string(),
execution: "server".to_string(),
tools: vec![],
metadata: None,
}
);
@@ -3135,4 +3201,35 @@ mod tests {
Ok(())
}
#[test]
fn response_item_metadata_helpers_include_session_source() {
let mut metadata = ResponseItemMetadata::default();
assert!(metadata.is_empty());
metadata.merge_from(ResponseItemMetadata {
session_source: Some(SessionSourceMetadata::Exec),
..Default::default()
});
assert_eq!(metadata.session_source, Some(SessionSourceMetadata::Exec));
assert!(!metadata.is_empty());
}
#[test]
fn session_source_metadata_serializes_snake_case() -> Result<()> {
assert_eq!(
serde_json::to_value(SessionSourceMetadata::Cli)?,
serde_json::Value::String("cli".to_string())
);
assert_eq!(
serde_json::to_value(SessionSourceMetadata::Vscode)?,
serde_json::Value::String("vscode".to_string())
);
assert_eq!(
serde_json::to_value(SessionSourceMetadata::SubagentThreadSpawn)?,
serde_json::Value::String("subagent_thread_spawn".to_string())
);
Ok(())
}
}