Include agent names in turn metadata (#38483)

## What changed

- Add the canonical agent path as `agent_name` in Responses API turn metadata.
- Fall back to `/root` when a session has no canonical agent path.
- Reserve `agent_name` from client metadata overrides and omit it from MCP request metadata.

## Testing

- Cover root, named subagent, legacy subagent, MCP, and client metadata merge cases.

GitOrigin-RevId: a9adcc079a51beae7d621aba24bcd759ceaa408e
This commit is contained in:
pmccrary-oai
2026-08-14 02:24:45 +00:00
committed by copyberry
parent 5620bab61c
commit 9d012ca4f5
5 changed files with 33 additions and 3 deletions

View File

@@ -26,6 +26,7 @@ use crate::client::X_OPENAI_SUBAGENT_HEADER;
pub(crate) const INSTALLATION_ID_KEY: &str = "installation_id";
pub(crate) const SESSION_ID_KEY: &str = "session_id";
pub(crate) const THREAD_ID_KEY: &str = "thread_id";
pub(crate) const AGENT_NAME_KEY: &str = "agent_name";
pub(crate) const TURN_ID_KEY: &str = "turn_id";
pub(crate) const WINDOW_ID_KEY: &str = "window_id";
pub(crate) const REQUEST_KIND_KEY: &str = "request_kind";
@@ -55,6 +56,7 @@ const RESERVED_METADATA_KEYS: &[&str] = &[
X_CODEX_INSTALLATION_ID_HEADER,
SESSION_ID_KEY,
THREAD_ID_KEY,
AGENT_NAME_KEY,
TURN_ID_KEY,
WINDOW_ID_KEY,
X_CODEX_WINDOW_ID_HEADER,
@@ -203,6 +205,7 @@ pub struct CodexResponsesMetadata {
pub(crate) installation_id: String,
pub(crate) session_id: String,
pub(crate) thread_id: String,
pub(crate) agent_name: Option<String>,
pub(crate) turn_id: Option<String>,
pub(crate) routing_hint: Option<HeaderValue>,
pub(crate) window_id: String,
@@ -236,6 +239,7 @@ impl CodexResponsesMetadata {
installation_id,
session_id,
thread_id,
agent_name: None,
turn_id: None,
routing_hint: None,
window_id,
@@ -354,6 +358,9 @@ impl CodexResponsesMetadata {
installation_id: has_request_identity.then_some(self.installation_id.as_str()),
session_id: has_turn_identity.then_some(self.session_id.as_str()),
thread_id: has_turn_identity.then_some(self.thread_id.as_str()),
agent_name: has_turn_identity
.then_some(self.agent_name.as_deref())
.flatten(),
turn_id: has_turn_identity
.then_some(self.turn_id.as_deref())
.flatten(),
@@ -474,6 +481,8 @@ struct CodexTurnMetadataPayload<'a> {
#[serde(default, skip_serializing_if = "Option::is_none")]
thread_id: Option<&'a str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
agent_name: Option<&'a str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
turn_id: Option<&'a str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
window_id: Option<&'a str>,

View File

@@ -12,6 +12,7 @@ use std::sync::atomic::Ordering;
use serde_json::Value;
use tokio::task::JoinHandle;
use crate::responses_metadata::AGENT_NAME_KEY;
use crate::responses_metadata::CodexResponsesMetadata;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::PARENT_TURN_ID_KEY;
@@ -27,6 +28,7 @@ use codex_git_utils::get_git_remote_urls_assume_git_repo;
use codex_git_utils::get_git_repo_root;
use codex_git_utils::get_has_changes_in_repo;
use codex_git_utils::get_head_commit_hash;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
@@ -100,6 +102,7 @@ pub(crate) struct TurnMetadataState {
repo_root: Option<PathBuf>,
session_id: String,
thread_id: String,
agent_name: String,
forked_from_thread_id: Option<ThreadId>,
parent_thread_id: Option<ThreadId>,
parent_turn_id: OnceLock<String>,
@@ -151,11 +154,16 @@ impl TurnMetadataState {
);
let sandbox_mode =
Some(permission_profile_policy_tag(permission_profile, cwd.as_path()).to_string());
let agent_name = session_source
.get_agent_path()
.unwrap_or_else(AgentPath::root)
.to_string();
Self {
cwd,
repo_root,
session_id,
thread_id,
agent_name,
forked_from_thread_id,
parent_thread_id,
parent_turn_id: OnceLock::new(),
@@ -190,6 +198,7 @@ impl TurnMetadataState {
let Value::Object(mut metadata) = responses_metadata.turn_metadata_value()? else {
return None;
};
metadata.remove(AGENT_NAME_KEY);
metadata.remove(PARENT_TURN_ID_KEY);
metadata.remove(ROOT_TURN_ID_KEY);
metadata.insert(
@@ -343,6 +352,7 @@ impl TurnMetadataState {
}
CodexResponsesMetadata {
turn_id: Some(self.turn_id.clone()),
agent_name: Some(self.agent_name.clone()),
forked_from_thread_id: self.forked_from_thread_id,
parent_thread_id: self.parent_thread_id,
parent_turn_id: self.parent_turn_id.get().cloned(),

View File

@@ -22,6 +22,7 @@ use codex_analytics::CompactionPhase;
use codex_analytics::CompactionReason;
use codex_analytics::CompactionTrigger;
use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::AgentPath;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::SessionSource;
@@ -260,6 +261,7 @@ fn turn_metadata_state_includes_sandbox_metadata() {
assert_eq!(auto_review_enabled, Some(true));
assert_eq!(session_id, Some("session-a"));
assert_eq!(thread_id, Some("thread-a"));
assert_eq!(json["agent_name"].as_str(), Some("/root"));
assert!(json.get("forked_from_thread_id").is_none());
assert!(json.get("parent_thread_id").is_none());
assert!(json.get("subagent_kind").is_none());
@@ -317,7 +319,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() {
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_path: Some(AgentPath::root().join("worker").expect("agent path")),
agent_nickname: None,
agent_role: None,
}),
@@ -340,6 +342,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() {
Some("22222222-2222-4222-8222-222222222222")
);
assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn"));
assert_eq!(json["agent_name"].as_str(), Some("/root/worker"));
}
#[test]
@@ -384,6 +387,9 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() {
Some("33333333-3333-4333-8333-333333333333")
);
assert_eq!(json["subagent_kind"].as_str(), Some("thread_spawn"));
// V1 subagents have no canonical agent path and are intentionally unsupported by
// agent-name-addressed history and notes; their metadata falls back to the root agent.
assert_eq!(json["agent_name"].as_str(), Some("/root"));
}
#[test]
@@ -424,6 +430,7 @@ fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_with
Some("44444444-4444-4444-8444-444444444444")
);
assert_eq!(json["subagent_kind"].as_str(), Some(subagent_kind));
assert_eq!(json["agent_name"].as_str(), Some("/root"));
}
}
@@ -483,12 +490,14 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta(
let header = test_turn_metadata_header(&state);
let header_json: Value = serde_json::from_str(&header).expect("json");
assert_eq!(header_json["agent_name"].as_str(), Some("/root"));
assert!(header_json.get("model").is_none());
assert!(header_json.get("reasoning_effort").is_none());
let meta = state
.current_meta_value_for_mcp_request(test_mcp_turn_metadata_context())
.expect("turn metadata should be present");
assert!(meta.get("agent_name").is_none());
assert!(meta.get("request_kind").is_none());
assert_eq!(meta["model"].as_str(), Some("gpt-5.4"));
assert_eq!(meta["reasoning_effort"].as_str(), Some("high"));
@@ -694,6 +703,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
),
("session_id".to_string(), "client-supplied".to_string()),
("thread_id".to_string(), "client-supplied".to_string()),
("agent_name".to_string(), "client-supplied".to_string()),
("installation_id".to_string(), "client-supplied".to_string()),
(
"x-codex-installation-id".to_string(),
@@ -769,6 +779,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
assert_eq!(json["session_id"].as_str(), Some("session-a"));
assert_eq!(json["thread_id"].as_str(), Some("thread-a"));
assert!(json.get(LEGACY_CODE_MODE_TOOL_NAMES_KEY).is_none());
assert_eq!(json["agent_name"].as_str(), Some("/root"));
assert_eq!(
json[TOOL_NAMESPACES_INFO_KEY],
serde_json::json!({

View File

@@ -12,7 +12,7 @@ Scenario: After five varied API-key-auth turns, remote manual compaction omits s
- "thread_id": "<UUID>",
- "turn_id": "<UUID>",
- "x-codex-installation-id": "<UUID>",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"agent_name\":\"/root\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-window-id": "<UUID>:0"
- },
- "include": [

View File

@@ -12,7 +12,7 @@ Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses
- "thread_id": "<UUID>",
- "turn_id": "<UUID>",
- "x-codex-installation-id": "<UUID>",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"agent_name\":\"/root\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-window-id": "<UUID>:0"
- },
- "include": [