1use std::{collections::BTreeMap, str::FromStr as _};
11
12use crate::{ModelId, WireFormat};
13
14const CODEX_SESSION_ID_PATH: &str = "x-codex-turn-metadata.session_id";
16const CODEX_THREAD_ID_PATH: &str = "x-codex-turn-metadata.thread_id";
17const CODEX_PARENT_THREAD_ID_PATH: &str = "x-codex-turn-metadata.parent_thread_id";
18const CODEX_TURN_ID_PATH: &str = "x-codex-turn-metadata.turn_id";
19const CODEX_THREAD_SOURCE_PATH: &str = "x-codex-turn-metadata.thread_source";
20const CODEX_SUBAGENT_KIND_PATH: &str = "x-codex-turn-metadata.subagent_kind";
21const CODEX_AGENT_ROLE_PATH: &str = "x-codex-turn-metadata.agent_role";
22const CODEX_TASK_ID_PATH: &str = "x-codex-turn-metadata.task_id";
23const CODEX_TASK_KIND_PATH: &str = "x-codex-turn-metadata.task_kind";
24
25const SWITCHYARD_SESSION_ID_HEADER: &str = "x-switchyard-session-id";
27const SWITCHYARD_AGENT_ID_HEADER: &str = "x-switchyard-agent-id";
28const SWITCHYARD_PARENT_AGENT_ID_HEADER: &str = "x-switchyard-parent-agent-id";
29const SWITCHYARD_IS_SUBAGENT_HEADER: &str = "x-switchyard-is-subagent";
30const SWITCHYARD_AGENT_KIND_HEADER: &str = "x-switchyard-agent-kind";
31const SWITCHYARD_AGENT_ROLE_HEADER: &str = "x-switchyard-agent-role";
32const SWITCHYARD_TASK_ID_HEADER: &str = "x-switchyard-task-id";
33const SWITCHYARD_TASK_KIND_HEADER: &str = "x-switchyard-task-kind";
34const SWITCHYARD_TURN_ID_HEADER: &str = "x-switchyard-turn-id";
35const SWITCHYARD_REQUEST_ID_HEADER: &str = "x-switchyard-request-id";
36const SWITCHYARD_SESSION_FINAL_HEADER: &str = "x-switchyard-session-final";
37
38const RELAY_SESSION_ID_HEADER: &str = "x-nemo-relay-session-id";
40const RELAY_SUBAGENT_ID_HEADER: &str = "x-nemo-relay-subagent-id";
41
42const DYNAMO_SESSION_ID_HEADER: &str = "x-dynamo-session-id";
44const DYNAMO_PARENT_SESSION_ID_HEADER: &str = "x-dynamo-parent-session-id";
45const DYNAMO_SESSION_FINAL_HEADER: &str = "x-dynamo-session-final";
46
47const CODEX_PARENT_THREAD_ID_HEADER: &str = "x-codex-parent-thread-id";
49
50const OPENAI_SUBAGENT_HEADER: &str = "x-openai-subagent";
52
53const CLAUDE_SESSION_ID_HEADER: &str = "x-claude-code-session-id";
55const CLAUDE_AGENT_ID_HEADER: &str = "x-claude-code-agent-id";
56const CLAUDE_PARENT_AGENT_ID_HEADER: &str = "x-claude-code-parent-agent-id";
57
58const OPENCODE_SESSION_ID_HEADER: &str = "x-session-id";
60
61const SESSION_ID_HEADER: &str = "session-id";
63const THREAD_ID_HEADER: &str = "thread-id";
64const TASK_ID_HEADER: &str = "x-task-id";
65const REQUEST_ID_HEADER: &str = "x-request-id";
66const CLIENT_REQUEST_ID_HEADER: &str = "x-client-request-id";
67
68const USER_AGENT_HEADER: &str = "user-agent";
70
71const CLAUDE_CHILD_IDENTITY_VERSION: [u64; 3] = [2, 1, 139];
74
75const SUBAGENT_WORK_KINDS: &[&str] = &["collab_spawn", "thread_spawn", "review"];
79
80type HeaderConfig = [(&'static str, &'static [&'static str])];
83
84const HEADER_CONFIG: &HeaderConfig = &[
86 (
87 SWITCHYARD_SESSION_ID_HEADER,
88 &[
89 SWITCHYARD_SESSION_ID_HEADER,
90 CLAUDE_SESSION_ID_HEADER,
91 RELAY_SESSION_ID_HEADER,
92 OPENCODE_SESSION_ID_HEADER,
93 CODEX_SESSION_ID_PATH,
94 SESSION_ID_HEADER,
95 ],
96 ),
97 (
98 SWITCHYARD_AGENT_ID_HEADER,
99 &[
100 SWITCHYARD_AGENT_ID_HEADER,
101 CLAUDE_AGENT_ID_HEADER,
102 RELAY_SUBAGENT_ID_HEADER,
103 DYNAMO_SESSION_ID_HEADER,
104 CODEX_THREAD_ID_PATH,
105 THREAD_ID_HEADER,
106 ],
107 ),
108 (
109 SWITCHYARD_PARENT_AGENT_ID_HEADER,
110 &[
111 SWITCHYARD_PARENT_AGENT_ID_HEADER,
112 DYNAMO_PARENT_SESSION_ID_HEADER,
113 CODEX_PARENT_THREAD_ID_PATH,
114 CODEX_PARENT_THREAD_ID_HEADER,
115 ],
116 ),
117 (
118 SWITCHYARD_AGENT_KIND_HEADER,
119 &[
120 SWITCHYARD_AGENT_KIND_HEADER,
121 CODEX_SUBAGENT_KIND_PATH,
122 OPENAI_SUBAGENT_HEADER,
123 ],
124 ),
125 (
126 SWITCHYARD_AGENT_ROLE_HEADER,
127 &[SWITCHYARD_AGENT_ROLE_HEADER, CODEX_AGENT_ROLE_PATH],
128 ),
129 (
130 SWITCHYARD_TASK_ID_HEADER,
131 &[
132 SWITCHYARD_TASK_ID_HEADER,
133 CODEX_TASK_ID_PATH,
134 TASK_ID_HEADER,
135 ],
136 ),
137 (
138 SWITCHYARD_TASK_KIND_HEADER,
139 &[SWITCHYARD_TASK_KIND_HEADER, CODEX_TASK_KIND_PATH],
140 ),
141 (
142 SWITCHYARD_TURN_ID_HEADER,
143 &[SWITCHYARD_TURN_ID_HEADER, CODEX_TURN_ID_PATH],
144 ),
145 (
146 SWITCHYARD_REQUEST_ID_HEADER,
147 &[
148 SWITCHYARD_REQUEST_ID_HEADER,
149 REQUEST_ID_HEADER,
150 CLIENT_REQUEST_ID_HEADER,
151 ],
152 ),
153 (
154 SWITCHYARD_SESSION_FINAL_HEADER,
155 &[SWITCHYARD_SESSION_FINAL_HEADER, DYNAMO_SESSION_FINAL_HEADER],
156 ),
157];
158
159#[derive(Clone, Default)]
167pub struct Metadata {
168 pub session_id: Option<String>,
170 pub agent_id: Option<String>,
172 pub parent_agent_id: Option<String>,
174 pub is_subagent: bool,
176 pub is_delegated_work: bool,
181 pub agent_kind: Option<String>,
183 pub agent_role: Option<String>,
185 pub task_id: Option<String>,
187 pub task_kind: Option<String>,
189 pub turn_id: Option<String>,
191 pub session_final: Option<bool>,
194 pub correlation_id: Option<String>,
196 pub subagent_identity_unsupported: bool,
200 pub served_model: Option<ModelId>,
202 pub serving_observations: BTreeMap<ModelId, ServingObservation>,
204 pub extra_metadata: Option<BTreeMap<String, String>>,
206 pub http_headers: Option<http::HeaderMap>,
208 pub wire_format: Option<WireFormat>,
210}
211
212#[derive(Clone, Debug)]
214pub struct ServingObservation {
215 pub received_at: std::time::Instant,
217 pub effective_prefill_tokens: usize,
219 pub active_prefill_tokens: Option<usize>,
221}
222
223impl Metadata {
224 pub fn from_headers(headers: &http::HeaderMap) -> Self {
226 let (parent_agent_id, is_subagent, is_delegated_work) = parse_sub_agent(headers);
227
228 Metadata {
229 session_id: sy_header(headers, SWITCHYARD_SESSION_ID_HEADER),
230 agent_id: sy_header(headers, SWITCHYARD_AGENT_ID_HEADER),
231 parent_agent_id,
232 is_subagent,
233 is_delegated_work,
234 agent_kind: sy_header(headers, SWITCHYARD_AGENT_KIND_HEADER),
235 agent_role: sy_header(headers, SWITCHYARD_AGENT_ROLE_HEADER),
236 task_id: sy_header(headers, SWITCHYARD_TASK_ID_HEADER),
237 task_kind: sy_header(headers, SWITCHYARD_TASK_KIND_HEADER),
238 turn_id: sy_header(headers, SWITCHYARD_TURN_ID_HEADER),
239 session_final: sy_header(headers, SWITCHYARD_SESSION_FINAL_HEADER)
240 .as_deref()
241 .and_then(parse_bool),
242 correlation_id: sy_header(headers, SWITCHYARD_REQUEST_ID_HEADER),
243 subagent_identity_unsupported: claude_lacks_child_identity(headers),
244 ..Metadata::default()
245 }
246 }
247
248 pub fn is_subagent_work(&self) -> bool {
254 self.is_delegated_work
255 }
256}
257
258fn parse_sub_agent(headers: &http::HeaderMap) -> (Option<String>, bool, bool) {
269 let explicit = header(headers, SWITCHYARD_IS_SUBAGENT_HEADER).and_then(parse_bool);
270
271 let (claude_parent, claude_subagent) = claude_lineage(headers);
272
273 let harness_kind = resolve_path(headers, CODEX_SUBAGENT_KIND_PATH)
276 .or_else(|| header(headers, OPENAI_SUBAGENT_HEADER).map(str::to_string));
277
278 let parent = sy_header(headers, SWITCHYARD_PARENT_AGENT_ID_HEADER)
281 .or_else(|| claude_parent.map(str::to_string));
282
283 let codex_child = parent.is_some()
287 && resolve_path(headers, CODEX_THREAD_SOURCE_PATH).as_deref() == Some("subagent");
288
289 let is_subagent = explicit.unwrap_or(claude_subagent || codex_child || harness_kind.is_some());
290
291 let is_delegated_work = match (explicit, harness_kind.as_deref()) {
295 (Some(false), _) => false,
296 (_, Some(kind)) => SUBAGENT_WORK_KINDS.contains(&kind),
297 (Some(true), None) => true,
298 (None, None) => claude_subagent || codex_child,
299 };
300
301 (parent, is_subagent, is_delegated_work)
302}
303
304fn claude_lineage(headers: &http::HeaderMap) -> (Option<&str>, bool) {
311 let session = header(headers, CLAUDE_SESSION_ID_HEADER);
312 let agent = header(headers, CLAUDE_AGENT_ID_HEADER);
313 let is_subagent = agent.is_some();
314 let parent = is_subagent
315 .then(|| header(headers, CLAUDE_PARENT_AGENT_ID_HEADER).or(session))
316 .flatten();
317 (parent, is_subagent)
318}
319
320fn claude_lacks_child_identity(headers: &http::HeaderMap) -> bool {
326 fn parse(user_agent: &str) -> Option<([u64; 3], bool)> {
327 let tagged = user_agent
328 .strip_prefix("claude-cli/")?
329 .split([' ', '+'])
330 .next()?;
331 let (version, prerelease) = match tagged.split_once('-') {
332 Some((version, _)) => (version, true),
333 None => (tagged, false),
334 };
335 let mut parts = version.split('.').map(|part| part.parse::<u64>().ok());
336 Some(([parts.next()??, parts.next()??, parts.next()??], prerelease))
337 }
338 header(headers, USER_AGENT_HEADER)
339 .and_then(parse)
340 .is_some_and(|(version, prerelease)| {
341 version < CLAUDE_CHILD_IDENTITY_VERSION
342 || (prerelease && version == CLAUDE_CHILD_IDENTITY_VERSION)
343 })
344}
345
346fn parse_bool(value: &str) -> Option<bool> {
348 match value.trim().to_ascii_lowercase().as_str() {
349 "1" | "true" | "yes" | "on" => Some(true),
350 "0" | "false" | "no" | "off" => Some(false),
351 _ => None,
352 }
353}
354
355fn sy_header(headers: &http::HeaderMap, key: &str) -> Option<String> {
361 let (_, paths) = HEADER_CONFIG
362 .iter()
363 .find(|(field, _)| field.eq_ignore_ascii_case(key))?;
364 paths.iter().find_map(|path| resolve_path(headers, path))
365}
366
367fn resolve_path(headers: &http::HeaderMap, path: &str) -> Option<String> {
370 let (header_name, nested) = match path.split_once('.') {
371 Some((name, rest)) => (name, Some(rest)),
372 None => (path, None),
373 };
374 let raw = headers.get(header_name)?.to_str().ok().map(|s| s.trim())?;
375 if raw.is_empty() {
376 return None;
377 }
378
379 let Some(nested) = nested else {
381 return Some(raw.to_string());
382 };
383
384 let mut current: serde_json::Value = serde_json::from_str(raw).ok()?;
386 for segment in nested.split('.') {
387 current = current.as_object()?.get(segment)?.clone();
388 }
389
390 match current {
391 serde_json::Value::String(s) => {
392 let value = s.trim();
393 (!value.is_empty()).then(|| value.to_string())
394 }
395 serde_json::Value::Null => None,
396 leaf => Some(leaf.to_string()),
397 }
398}
399
400fn header<'a>(headers: &'a http::HeaderMap, key: &str) -> Option<&'a str> {
401 headers
402 .get(key)
403 .and_then(|s| s.to_str().ok())
404 .map(str::trim)
405 .filter(|s| !s.is_empty())
406}
407
408pub fn slice_to_header_map(sl: &[(&str, &str)]) -> http::HeaderMap {
410 let mut m = http::HeaderMap::with_capacity(sl.len());
411 for (k, v) in sl {
412 m.insert(
413 http::HeaderName::from_str(k).unwrap(),
414 (*v).try_into().unwrap(),
415 );
416 }
417 m
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 const CODEX_TURN_METADATA_HEADER: &str = "x-codex-turn-metadata";
426
427 fn metadata(headers: &[(&str, &str)]) -> Metadata {
428 Metadata::from_headers(&slice_to_header_map(headers))
429 }
430
431 #[test]
432 fn normalizes_codex_metadata_and_lineage() {
433 let child_body = serde_json::json!({
434 "session_id": "root-session",
435 "thread_id": "child-agent",
436 "parent_thread_id": "root-agent",
437 "turn_id": "turn-7",
438 "subagent_kind": "collab_spawn",
439 })
440 .to_string();
441 let child = metadata(&[(CODEX_TURN_METADATA_HEADER, child_body.as_str())]);
442 assert_eq!(child.session_id.as_deref(), Some("root-session"));
443 assert_eq!(child.agent_id.as_deref(), Some("child-agent"));
444 assert_eq!(child.parent_agent_id.as_deref(), Some("root-agent"));
445 assert!(child.is_subagent);
446
447 let root_body = serde_json::json!({
448 "session_id": "root-session",
449 "thread_id": "root-agent",
450 "turn_id": "turn-1",
451 })
452 .to_string();
453 let root = metadata(&[(CODEX_TURN_METADATA_HEADER, root_body.as_str())]);
454 assert!(!root.is_subagent);
455
456 let correlated_body = serde_json::json!({
460 "session_id": "root-session",
461 "thread_id": "child-thread",
462 "parent_thread_id": "root-thread",
463 "turn_id": "turn-3",
464 })
465 .to_string();
466 let correlated = metadata(&[(CODEX_TURN_METADATA_HEADER, correlated_body.as_str())]);
467 assert_eq!(correlated.parent_agent_id.as_deref(), Some("root-thread"));
468 assert!(!correlated.is_subagent);
469 assert!(!correlated.is_subagent_work());
470
471 let child_body = serde_json::json!({
474 "session_id": "root-session",
475 "thread_id": "child-thread",
476 "parent_thread_id": "root-thread",
477 "thread_source": "subagent",
478 "turn_id": "turn-4",
479 })
480 .to_string();
481 let child = metadata(&[(CODEX_TURN_METADATA_HEADER, child_body.as_str())]);
482 assert_eq!(child.parent_agent_id.as_deref(), Some("root-thread"));
483 assert!(child.is_subagent);
484 assert!(child.is_subagent_work());
485 }
486
487 #[test]
488 fn normalizes_claude_code_metadata_and_lineage() {
489 let session = metadata(&[(
492 "x-claude-code-session-id",
493 "fb46caae-eac6-4f5f-83fd-8fc8f5743abb",
494 )]);
495 assert_eq!(
496 session.session_id.as_deref(),
497 Some("fb46caae-eac6-4f5f-83fd-8fc8f5743abb")
498 );
499
500 let child = metadata(&[
503 ("x-claude-code-session-id", "claude-session"),
504 ("x-claude-code-agent-id", "claude-agent"),
505 ]);
506 assert_eq!(child.session_id.as_deref(), Some("claude-session"));
507 assert_eq!(child.agent_id.as_deref(), Some("claude-agent"));
508 assert_eq!(child.parent_agent_id.as_deref(), Some("claude-session"));
509 assert!(child.is_subagent);
510
511 let child_without_session = metadata(&[("x-claude-code-agent-id", "claude-agent")]);
512 assert_eq!(
513 child_without_session.agent_id.as_deref(),
514 Some("claude-agent")
515 );
516 assert_eq!(child_without_session.parent_agent_id, None);
517 assert!(child_without_session.is_subagent);
518
519 let explicit_parent = metadata(&[
520 ("x-claude-code-session-id", "claude-session"),
521 ("x-claude-code-agent-id", "claude-agent"),
522 ("x-claude-code-parent-agent-id", "claude-parent-agent"),
523 ]);
524 assert_eq!(
525 explicit_parent.parent_agent_id.as_deref(),
526 Some("claude-parent-agent")
527 );
528
529 let root = metadata(&[
532 ("x-claude-code-session-id", "claude-session"),
533 ("x-claude-code-parent-agent-id", "claude-parent-agent"),
534 ]);
535 assert_eq!(root.session_id.as_deref(), Some("claude-session"));
536 assert_eq!(root.agent_id, None);
537 assert_eq!(root.parent_agent_id, None);
538 assert!(!root.is_subagent);
539 }
540
541 #[test]
542 fn flags_claude_code_builds_without_child_identity() {
543 let unsupported = |user_agent: &str| {
544 metadata(&[("user-agent", user_agent)]).subagent_identity_unsupported
545 };
546 assert!(unsupported(" claude-cli/2.1.121 (external, cli) "));
548 assert!(unsupported("claude-cli/2.1.139-beta.1"));
549 assert!(!unsupported("claude-cli/2.1.139 (external, cli)"));
551 assert!(!unsupported("claude-cli/2.1.211+build (external, cli)"));
552 assert!(!unsupported("codex_cli_rs/0.120.0"));
554 assert!(!unsupported("claude-cli/nightly"));
555 assert!(!metadata(&[]).subagent_identity_unsupported);
556 }
557
558 #[test]
559 fn normalizes_correlation_and_session_headers_without_routing() {
560 let relay = metadata(&[
562 ("x-nemo-relay-session-id", "relay-session"),
563 ("x-nemo-relay-subagent-id", "relay-child"),
564 ("x-dynamo-parent-session-id", "relay-parent"),
565 ]);
566 assert_eq!(relay.session_id.as_deref(), Some("relay-session"));
567 assert_eq!(relay.agent_id.as_deref(), Some("relay-child"));
568 assert_eq!(relay.parent_agent_id.as_deref(), Some("relay-parent"));
569 assert!(!relay.is_subagent);
570 assert!(!relay.is_subagent_work());
571
572 let opencode = metadata(&[
573 ("x-session-id", "opencode-run"),
574 ("x-parent-session-id", "opencode-parent"),
575 ]);
576 assert_eq!(opencode.session_id.as_deref(), Some("opencode-run"));
577 assert_eq!(opencode.parent_agent_id, None);
578 assert!(!opencode.is_subagent);
579
580 let codex_session = metadata(&[
581 ("session-id", "codex-run"),
582 ("x-parent-session-id", "stray-parent"),
583 ]);
584 assert_eq!(codex_session.session_id.as_deref(), Some("codex-run"));
585 assert_eq!(codex_session.parent_agent_id, None);
586 assert!(!codex_session.is_subagent);
587
588 let final_session = metadata(&[
589 ("x-dynamo-session-id", "generic-run"),
590 ("x-dynamo-parent-session-id", "generic-parent"),
591 ("x-dynamo-session-final", "true"),
592 ]);
593 assert_eq!(final_session.agent_id.as_deref(), Some("generic-run"));
594 assert_eq!(
595 final_session.parent_agent_id.as_deref(),
596 Some("generic-parent")
597 );
598 assert_eq!(final_session.session_final, Some(true));
599
600 let active_session = metadata(&[
601 ("x-dynamo-session-id", "generic-run"),
602 ("x-dynamo-session-final", "false"),
603 ]);
604 assert_eq!(active_session.session_final, Some(false));
605 }
606
607 #[test]
608 fn sy_header_resolves_paths_in_order_and_descends_into_json() {
609 let body = serde_json::json!({ "session_id": "codex-session" }).to_string();
611 let headers = slice_to_header_map(&[(CODEX_TURN_METADATA_HEADER, body.as_str())]);
612 assert_eq!(
613 sy_header(&headers, SWITCHYARD_SESSION_ID_HEADER).as_deref(),
614 Some("codex-session")
615 );
616
617 let headers = slice_to_header_map(&[
619 (SWITCHYARD_SESSION_ID_HEADER, "explicit"),
620 (CODEX_TURN_METADATA_HEADER, body.as_str()),
621 ]);
622 assert_eq!(
623 sy_header(&headers, SWITCHYARD_SESSION_ID_HEADER).as_deref(),
624 Some("explicit")
625 );
626
627 assert_eq!(
629 sy_header(&http::HeaderMap::new(), SWITCHYARD_SESSION_ID_HEADER),
630 None
631 );
632 assert_eq!(sy_header(&headers, "x-not-a-field"), None);
633 }
634
635 #[test]
637 fn nested_metadata_strings_match_flat_header_normalization() {
638 let body = serde_json::json!({ "session_id": " codex-session " }).to_string();
639 let headers = slice_to_header_map(&[(CODEX_TURN_METADATA_HEADER, body.as_str())]);
640 assert_eq!(
641 sy_header(&headers, SWITCHYARD_SESSION_ID_HEADER).as_deref(),
642 Some("codex-session")
643 );
644
645 let blank_body = serde_json::json!({ "session_id": " " }).to_string();
646 let headers = slice_to_header_map(&[
647 (CODEX_TURN_METADATA_HEADER, blank_body.as_str()),
648 (SESSION_ID_HEADER, "fallback-session"),
649 ]);
650 assert_eq!(
651 sy_header(&headers, SWITCHYARD_SESSION_ID_HEADER).as_deref(),
652 Some("fallback-session")
653 );
654 }
655
656 #[test]
657 fn subagent_routing_honors_explicit_signals_and_delegated_work_kinds() {
658 let explicitly_root = metadata(&[
661 ("x-switchyard-is-subagent", "false"),
662 ("x-openai-subagent", "review"),
663 ]);
664 assert!(!explicitly_root.is_subagent);
665
666 let explicitly_child = metadata(&[("x-switchyard-is-subagent", "true")]);
667 assert!(explicitly_child.is_subagent);
668
669 let child_with_parent = metadata(&[
670 ("x-switchyard-is-subagent", "false"),
671 ("x-switchyard-parent-agent-id", "parent"),
672 ]);
673 assert!(!child_with_parent.is_subagent);
674
675 let with_openai = metadata(&[
677 ("x-openai-subagent", "review"),
678 ("x-switchyard-agent-kind", "researcher"),
679 ]);
680 assert!(with_openai.is_subagent);
681 assert!(with_openai.is_subagent_work());
682
683 let with_explicit = metadata(&[
684 ("x-switchyard-is-subagent", "true"),
685 ("x-switchyard-agent-kind", "researcher"),
686 ]);
687 assert!(with_explicit.is_subagent);
688 assert!(with_explicit.is_subagent_work());
689
690 let claude_child = metadata(&[
692 ("x-claude-code-session-id", "root"),
693 ("x-claude-code-agent-id", "worker"),
694 ]);
695 assert!(claude_child.is_subagent_work());
696
697 let review = metadata(&[("x-openai-subagent", "review")]);
699 assert!(review.is_subagent_work());
700
701 for kind in ["compact", "memory_consolidation", "brand_new_kind"] {
704 let request = metadata(&[("x-openai-subagent", kind)]);
705 assert!(request.is_subagent, "{kind} keeps the lineage fact");
706 assert!(!request.is_subagent_work(), "{kind} is not routed as work");
707 }
708
709 assert!(!Metadata::default().is_subagent_work());
711 }
712}