use std::{collections::BTreeMap, str::FromStr as _};
use crate::WireFormat;
const CODEX_SESSION_ID_PATH: &str = "x-codex-turn-metadata.session_id";
const CODEX_THREAD_ID_PATH: &str = "x-codex-turn-metadata.thread_id";
const CODEX_PARENT_THREAD_ID_PATH: &str = "x-codex-turn-metadata.parent_thread_id";
const CODEX_TURN_ID_PATH: &str = "x-codex-turn-metadata.turn_id";
const CODEX_SUBAGENT_KIND_PATH: &str = "x-codex-turn-metadata.subagent_kind";
const CODEX_AGENT_ROLE_PATH: &str = "x-codex-turn-metadata.agent_role";
const CODEX_TASK_ID_PATH: &str = "x-codex-turn-metadata.task_id";
const CODEX_TASK_KIND_PATH: &str = "x-codex-turn-metadata.task_kind";
const SWITCHYARD_SESSION_ID_HEADER: &str = "x-switchyard-session-id";
const SWITCHYARD_AGENT_ID_HEADER: &str = "x-switchyard-agent-id";
const SWITCHYARD_PARENT_AGENT_ID_HEADER: &str = "x-switchyard-parent-agent-id";
const SWITCHYARD_IS_SUBAGENT_HEADER: &str = "x-switchyard-is-subagent";
const SWITCHYARD_AGENT_KIND_HEADER: &str = "x-switchyard-agent-kind";
const SWITCHYARD_AGENT_ROLE_HEADER: &str = "x-switchyard-agent-role";
const SWITCHYARD_TASK_ID_HEADER: &str = "x-switchyard-task-id";
const SWITCHYARD_TASK_KIND_HEADER: &str = "x-switchyard-task-kind";
const SWITCHYARD_TURN_ID_HEADER: &str = "x-switchyard-turn-id";
const SWITCHYARD_REQUEST_ID_HEADER: &str = "x-switchyard-request-id";
const SWITCHYARD_SESSION_FINAL_HEADER: &str = "x-switchyard-session-final";
const RELAY_SESSION_ID_HEADER: &str = "x-nemo-relay-session-id";
const RELAY_SUBAGENT_ID_HEADER: &str = "x-nemo-relay-subagent-id";
const DYNAMO_SESSION_ID_HEADER: &str = "x-dynamo-session-id";
const DYNAMO_PARENT_SESSION_ID_HEADER: &str = "x-dynamo-parent-session-id";
const DYNAMO_SESSION_FINAL_HEADER: &str = "x-dynamo-session-final";
const CODEX_PARENT_THREAD_ID_HEADER: &str = "x-codex-parent-thread-id";
const OPENAI_SUBAGENT_HEADER: &str = "x-openai-subagent";
const CLAUDE_SESSION_ID_HEADER: &str = "x-claude-code-session-id";
const CLAUDE_AGENT_ID_HEADER: &str = "x-claude-code-agent-id";
const CLAUDE_PARENT_AGENT_ID_HEADER: &str = "x-claude-code-parent-agent-id";
const OPENCODE_SESSION_ID_HEADER: &str = "x-session-id";
const SESSION_ID_HEADER: &str = "session-id";
const THREAD_ID_HEADER: &str = "thread-id";
const TASK_ID_HEADER: &str = "x-task-id";
const REQUEST_ID_HEADER: &str = "x-request-id";
const CLIENT_REQUEST_ID_HEADER: &str = "x-client-request-id";
const SUBAGENT_WORK_KINDS: &[&str] = &["collab_spawn", "review"];
type HeaderConfig = [(&'static str, &'static [&'static str])];
const HEADER_CONFIG: &HeaderConfig = &[
(
SWITCHYARD_SESSION_ID_HEADER,
&[
SWITCHYARD_SESSION_ID_HEADER,
CLAUDE_SESSION_ID_HEADER,
RELAY_SESSION_ID_HEADER,
OPENCODE_SESSION_ID_HEADER,
CODEX_SESSION_ID_PATH,
SESSION_ID_HEADER,
],
),
(
SWITCHYARD_AGENT_ID_HEADER,
&[
SWITCHYARD_AGENT_ID_HEADER,
CLAUDE_AGENT_ID_HEADER,
RELAY_SUBAGENT_ID_HEADER,
DYNAMO_SESSION_ID_HEADER,
CODEX_THREAD_ID_PATH,
THREAD_ID_HEADER,
],
),
(
SWITCHYARD_PARENT_AGENT_ID_HEADER,
&[
SWITCHYARD_PARENT_AGENT_ID_HEADER,
DYNAMO_PARENT_SESSION_ID_HEADER,
CODEX_PARENT_THREAD_ID_PATH,
CODEX_PARENT_THREAD_ID_HEADER,
],
),
(
SWITCHYARD_AGENT_KIND_HEADER,
&[
SWITCHYARD_AGENT_KIND_HEADER,
CODEX_SUBAGENT_KIND_PATH,
OPENAI_SUBAGENT_HEADER,
],
),
(
SWITCHYARD_AGENT_ROLE_HEADER,
&[SWITCHYARD_AGENT_ROLE_HEADER, CODEX_AGENT_ROLE_PATH],
),
(
SWITCHYARD_TASK_ID_HEADER,
&[
SWITCHYARD_TASK_ID_HEADER,
CODEX_TASK_ID_PATH,
TASK_ID_HEADER,
],
),
(
SWITCHYARD_TASK_KIND_HEADER,
&[SWITCHYARD_TASK_KIND_HEADER, CODEX_TASK_KIND_PATH],
),
(
SWITCHYARD_TURN_ID_HEADER,
&[SWITCHYARD_TURN_ID_HEADER, CODEX_TURN_ID_PATH],
),
(
SWITCHYARD_REQUEST_ID_HEADER,
&[
SWITCHYARD_REQUEST_ID_HEADER,
REQUEST_ID_HEADER,
CLIENT_REQUEST_ID_HEADER,
],
),
(
SWITCHYARD_SESSION_FINAL_HEADER,
&[SWITCHYARD_SESSION_FINAL_HEADER, DYNAMO_SESSION_FINAL_HEADER],
),
];
#[derive(Clone, Default)]
pub struct Metadata {
pub session_id: Option<String>,
pub agent_id: Option<String>,
pub parent_agent_id: Option<String>,
pub is_subagent: bool,
pub is_delegated_work: bool,
pub agent_kind: Option<String>,
pub agent_role: Option<String>,
pub task_id: Option<String>,
pub task_kind: Option<String>,
pub turn_id: Option<String>,
pub session_final: Option<bool>,
pub correlation_id: Option<String>,
pub extra_metadata: Option<BTreeMap<String, String>>,
pub http_headers: Option<http::HeaderMap>,
pub wire_format: Option<WireFormat>,
}
impl Metadata {
pub fn from_headers(headers: &http::HeaderMap) -> Self {
let (parent_agent_id, is_subagent, is_delegated_work) = parse_sub_agent(headers);
Metadata {
session_id: sy_header(headers, SWITCHYARD_SESSION_ID_HEADER),
agent_id: sy_header(headers, SWITCHYARD_AGENT_ID_HEADER),
parent_agent_id,
is_subagent,
is_delegated_work,
agent_kind: sy_header(headers, SWITCHYARD_AGENT_KIND_HEADER),
agent_role: sy_header(headers, SWITCHYARD_AGENT_ROLE_HEADER),
task_id: sy_header(headers, SWITCHYARD_TASK_ID_HEADER),
task_kind: sy_header(headers, SWITCHYARD_TASK_KIND_HEADER),
turn_id: sy_header(headers, SWITCHYARD_TURN_ID_HEADER),
session_final: sy_header(headers, SWITCHYARD_SESSION_FINAL_HEADER)
.as_deref()
.and_then(parse_bool),
correlation_id: sy_header(headers, SWITCHYARD_REQUEST_ID_HEADER),
..Metadata::default()
}
}
pub fn is_subagent_work(&self) -> bool {
self.is_delegated_work
}
}
fn parse_sub_agent(headers: &http::HeaderMap) -> (Option<String>, bool, bool) {
let explicit = header(headers, SWITCHYARD_IS_SUBAGENT_HEADER).and_then(parse_bool);
let (claude_parent, claude_subagent) = claude_lineage(headers);
let harness_kind = resolve_path(headers, CODEX_SUBAGENT_KIND_PATH)
.or_else(|| header(headers, OPENAI_SUBAGENT_HEADER).map(str::to_string));
let parent = sy_header(headers, SWITCHYARD_PARENT_AGENT_ID_HEADER)
.or_else(|| claude_parent.map(str::to_string));
let is_subagent = explicit.unwrap_or(claude_subagent || harness_kind.is_some());
let is_delegated_work = match explicit {
Some(false) => false,
Some(true) => harness_kind
.as_deref()
.map(|k| SUBAGENT_WORK_KINDS.contains(&k))
.unwrap_or(true),
None => {
claude_subagent
|| harness_kind
.as_deref()
.is_some_and(|k| SUBAGENT_WORK_KINDS.contains(&k))
}
};
(parent, is_subagent, is_delegated_work)
}
fn claude_lineage(headers: &http::HeaderMap) -> (Option<&str>, bool) {
let session = header(headers, CLAUDE_SESSION_ID_HEADER);
let agent = header(headers, CLAUDE_AGENT_ID_HEADER);
let is_subagent = agent.is_some();
let parent = is_subagent
.then(|| header(headers, CLAUDE_PARENT_AGENT_ID_HEADER).or(session))
.flatten();
(parent, is_subagent)
}
fn parse_bool(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
}
}
fn sy_header(headers: &http::HeaderMap, key: &str) -> Option<String> {
let (_, paths) = HEADER_CONFIG
.iter()
.find(|(field, _)| field.eq_ignore_ascii_case(key))?;
paths.iter().find_map(|path| resolve_path(headers, path))
}
fn resolve_path(headers: &http::HeaderMap, path: &str) -> Option<String> {
let (header_name, nested) = match path.split_once('.') {
Some((name, rest)) => (name, Some(rest)),
None => (path, None),
};
let raw = headers.get(header_name)?.to_str().ok().map(|s| s.trim())?;
if raw.is_empty() {
return None;
}
let Some(nested) = nested else {
return Some(raw.to_string());
};
let mut current: serde_json::Value = serde_json::from_str(raw).ok()?;
for segment in nested.split('.') {
current = current.as_object()?.get(segment)?.clone();
}
match current {
serde_json::Value::String(s) => Some(s),
serde_json::Value::Null => None,
leaf => Some(leaf.to_string()),
}
}
fn header<'a>(headers: &'a http::HeaderMap, key: &str) -> Option<&'a str> {
headers
.get(key)
.and_then(|s| s.to_str().ok())
.map(str::trim)
.filter(|s| !s.is_empty())
}
pub fn slice_to_header_map(sl: &[(&str, &str)]) -> http::HeaderMap {
let mut m = http::HeaderMap::with_capacity(sl.len());
for (k, v) in sl {
m.insert(
http::HeaderName::from_str(k).unwrap(),
(*v).try_into().unwrap(),
);
}
m
}
#[cfg(test)]
mod tests {
use super::*;
const CODEX_TURN_METADATA_HEADER: &str = "x-codex-turn-metadata";
fn metadata(headers: &[(&str, &str)]) -> Metadata {
Metadata::from_headers(&slice_to_header_map(headers))
}
#[test]
fn normalizes_codex_metadata_and_lineage() {
let child_body = serde_json::json!({
"session_id": "root-session",
"thread_id": "child-agent",
"parent_thread_id": "root-agent",
"turn_id": "turn-7",
"subagent_kind": "collab_spawn",
})
.to_string();
let child = metadata(&[(CODEX_TURN_METADATA_HEADER, child_body.as_str())]);
assert_eq!(child.session_id.as_deref(), Some("root-session"));
assert_eq!(child.agent_id.as_deref(), Some("child-agent"));
assert_eq!(child.parent_agent_id.as_deref(), Some("root-agent"));
assert!(child.is_subagent);
let root_body = serde_json::json!({
"session_id": "root-session",
"thread_id": "root-agent",
"turn_id": "turn-1",
})
.to_string();
let root = metadata(&[(CODEX_TURN_METADATA_HEADER, root_body.as_str())]);
assert!(!root.is_subagent);
let correlated_body = serde_json::json!({
"session_id": "root-session",
"thread_id": "child-thread",
"parent_thread_id": "root-thread",
"turn_id": "turn-3",
})
.to_string();
let correlated = metadata(&[(CODEX_TURN_METADATA_HEADER, correlated_body.as_str())]);
assert_eq!(correlated.parent_agent_id.as_deref(), Some("root-thread"));
assert!(!correlated.is_subagent);
assert!(!correlated.is_subagent_work());
}
#[test]
fn normalizes_claude_code_metadata_and_lineage() {
let session = metadata(&[(
"x-claude-code-session-id",
"fb46caae-eac6-4f5f-83fd-8fc8f5743abb",
)]);
assert_eq!(
session.session_id.as_deref(),
Some("fb46caae-eac6-4f5f-83fd-8fc8f5743abb")
);
let child = metadata(&[
("x-claude-code-session-id", "claude-session"),
("x-claude-code-agent-id", "claude-agent"),
]);
assert_eq!(child.session_id.as_deref(), Some("claude-session"));
assert_eq!(child.agent_id.as_deref(), Some("claude-agent"));
assert_eq!(child.parent_agent_id.as_deref(), Some("claude-session"));
assert!(child.is_subagent);
let child_without_session = metadata(&[("x-claude-code-agent-id", "claude-agent")]);
assert_eq!(
child_without_session.agent_id.as_deref(),
Some("claude-agent")
);
assert_eq!(child_without_session.parent_agent_id, None);
assert!(child_without_session.is_subagent);
let explicit_parent = metadata(&[
("x-claude-code-session-id", "claude-session"),
("x-claude-code-agent-id", "claude-agent"),
("x-claude-code-parent-agent-id", "claude-parent-agent"),
]);
assert_eq!(
explicit_parent.parent_agent_id.as_deref(),
Some("claude-parent-agent")
);
let root = metadata(&[
("x-claude-code-session-id", "claude-session"),
("x-claude-code-parent-agent-id", "claude-parent-agent"),
]);
assert_eq!(root.session_id.as_deref(), Some("claude-session"));
assert_eq!(root.agent_id, None);
assert_eq!(root.parent_agent_id, None);
assert!(!root.is_subagent);
}
#[test]
fn normalizes_correlation_and_session_headers_without_routing() {
let relay = metadata(&[
("x-nemo-relay-session-id", "relay-session"),
("x-nemo-relay-subagent-id", "relay-child"),
("x-dynamo-parent-session-id", "relay-parent"),
]);
assert_eq!(relay.session_id.as_deref(), Some("relay-session"));
assert_eq!(relay.agent_id.as_deref(), Some("relay-child"));
assert_eq!(relay.parent_agent_id.as_deref(), Some("relay-parent"));
assert!(!relay.is_subagent);
assert!(!relay.is_subagent_work());
let opencode = metadata(&[
("x-session-id", "opencode-run"),
("x-parent-session-id", "opencode-parent"),
]);
assert_eq!(opencode.session_id.as_deref(), Some("opencode-run"));
assert_eq!(opencode.parent_agent_id, None);
assert!(!opencode.is_subagent);
let codex_session = metadata(&[
("session-id", "codex-run"),
("x-parent-session-id", "stray-parent"),
]);
assert_eq!(codex_session.session_id.as_deref(), Some("codex-run"));
assert_eq!(codex_session.parent_agent_id, None);
assert!(!codex_session.is_subagent);
let final_session = metadata(&[
("x-dynamo-session-id", "generic-run"),
("x-dynamo-parent-session-id", "generic-parent"),
("x-dynamo-session-final", "true"),
]);
assert_eq!(final_session.agent_id.as_deref(), Some("generic-run"));
assert_eq!(
final_session.parent_agent_id.as_deref(),
Some("generic-parent")
);
assert_eq!(final_session.session_final, Some(true));
let active_session = metadata(&[
("x-dynamo-session-id", "generic-run"),
("x-dynamo-session-final", "false"),
]);
assert_eq!(active_session.session_final, Some(false));
}
#[test]
fn sy_header_resolves_paths_in_order_and_descends_into_json() {
let body = serde_json::json!({ "session_id": "codex-session" }).to_string();
let headers = slice_to_header_map(&[(CODEX_TURN_METADATA_HEADER, body.as_str())]);
assert_eq!(
sy_header(&headers, SWITCHYARD_SESSION_ID_HEADER).as_deref(),
Some("codex-session")
);
let headers = slice_to_header_map(&[
(SWITCHYARD_SESSION_ID_HEADER, "explicit"),
(CODEX_TURN_METADATA_HEADER, body.as_str()),
]);
assert_eq!(
sy_header(&headers, SWITCHYARD_SESSION_ID_HEADER).as_deref(),
Some("explicit")
);
assert_eq!(
sy_header(&http::HeaderMap::new(), SWITCHYARD_SESSION_ID_HEADER),
None
);
assert_eq!(sy_header(&headers, "x-not-a-field"), None);
}
#[test]
fn subagent_routing_honors_explicit_signals_and_delegated_work_kinds() {
let explicitly_root = metadata(&[
("x-switchyard-is-subagent", "false"),
("x-openai-subagent", "review"),
]);
assert!(!explicitly_root.is_subagent);
let explicitly_child = metadata(&[("x-switchyard-is-subagent", "true")]);
assert!(explicitly_child.is_subagent);
let child_with_parent = metadata(&[
("x-switchyard-is-subagent", "false"),
("x-switchyard-parent-agent-id", "parent"),
]);
assert!(!child_with_parent.is_subagent);
let with_openai = metadata(&[
("x-openai-subagent", "review"),
("x-switchyard-agent-kind", "researcher"),
]);
assert!(with_openai.is_subagent);
assert!(with_openai.is_subagent_work());
let with_explicit = metadata(&[
("x-switchyard-is-subagent", "true"),
("x-switchyard-agent-kind", "researcher"),
]);
assert!(with_explicit.is_subagent);
assert!(with_explicit.is_subagent_work());
let claude_child = metadata(&[
("x-claude-code-session-id", "root"),
("x-claude-code-agent-id", "worker"),
]);
assert!(claude_child.is_subagent_work());
let review = metadata(&[("x-openai-subagent", "review")]);
assert!(review.is_subagent_work());
for kind in ["compact", "memory_consolidation", "brand_new_kind"] {
let request = metadata(&[("x-openai-subagent", kind)]);
assert!(request.is_subagent, "{kind} keeps the lineage fact");
assert!(!request.is_subagent_work(), "{kind} is not routed as work");
}
assert!(!Metadata::default().is_subagent_work());
}
}