Expose an explicit spawn-to-subagent correlation key
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
Proposal: expose an explicit Codex spawn-to-subagent correlation key
Codex variants: CLI hook and OTel contracts; Codex app for macOS as a consumer
Status: proposal
Validated against: Codex rust-v0.153.4 (release source commit
3d2ee51ca2d5db578f328aa75e20aa22c0197c9a) and main commit
38cbebaf3fe3e81a94bf462079e7cf9659fc9e50 inspected on 2026-09-09
Problem
Codex currently exposes the two ends of a native spawn_agent operation, but
not one stable value that joins them:
PreToolUseandPostToolUseexpose the spawn call'stool_use_id.SubagentStartandSubagentStopexpose the created child thread as
agent_id.- A child tool hook can expose the emitting child's
agent_id, but that does
not identify which parentspawn_agentcall created the child.
session_id is shared with subagent hooks, and turn_id can be shared by
multiple concurrent spawns. Neither value identifies one spawn edge. Event
timestamps, delivery order, transcript paths, and "latest spawn" matching are
also ambiguous and should not be used to reconstruct an auditable agent tree.
The released hook schemas therefore cannot distinguish this valid execution:
root turn T
spawn tool call S1 -> child A
spawn tool call S2 -> child B
from a swapped association S1 -> B, S2 -> A when lifecycle events arrive
before, after, or interleaved with the parent tool results.
Why this is useful
Codex's native multi-agent workflow is already valuable: it can delegate work
to concurrent and nested agents while retaining the surrounding Codex workflow.
The missing correlation edge limits how accurately that workflow can be
observed after it runs.
Third-party observability tools such as OpenTelemetry collectors, Langfuse,
Phoenix, Opik, and Jaeger can display an agent tree only if Codex exports the
relationship between a particular spawn_agent call and the child it created.
Today they can group activity by session, but they cannot reconstruct the exact
tree deterministically when a turn contains concurrent spawns. No backend can
recover an identifier that the source events do not contain.
Exposing this edge would let these tools provide reliable workflow timelines,
per-agent latency and token attribution, failed-delegation diagnosis, and
auditable root-cause analysis without reading unstable transcripts or applying
timing heuristics. It would also make integrations portable: the same explicit
IDs would work across backends instead of requiring a Codex-specific guess in
each one.
The same contract could improve Codex's own macOS app. The app could evolve its
task timeline and agent tooling to show which spawn created each child, how
nested delegation progressed, and where time or failures accumulated. This is
an opportunity to build on an already strong multi-agent experience: the
proposal does not change how agents work, only makes their existing
relationships reliably inspectable by Codex and external tools.
Minimal proposal
Add one correlation value to both sides of the existing relationship:
- Add required
spawn_tool_use_id: stringtoSubagentStartand
SubagentStopfor thread-spawned subagents. Its value is the parent
spawn_agentcall'stool_use_id. - Add
spawned_agent_id: stringto a successfulPostToolUseevent for
spawn_agent. Its value is the created child's existingagent_id. - Expose the same two values on equivalent native OTel records. For an OTel
codex.tool_resultproduced by a successfulspawn_agent, add
spawned_agent_id; for subagent lifecycle records, expose bothagent_id
andspawn_tool_use_id.
The stable join key is (session_id, spawn_tool_use_id). The joined edge is:
parent PostToolUse.tool_use_id
== child SubagentStart.spawn_tool_use_id
== child SubagentStop.spawn_tool_use_id
parent PostToolUse.spawned_agent_id
== child SubagentStart.agent_id
== child SubagentStop.agent_id
No new event ID, attempt ID, durable queue, trace protocol, or backend-specific
identifier is needed to solve this relationship.
Why spawned_agent_id, not agent_id, on the parent event
PostToolUse already has an optional agent_id. In a hook running inside a
child, that field identifies the agent that emitted the tool event. Reusing it
for the child created by a root or nested spawn would give one field two
meanings. spawned_agent_id keeps the existing emitter meaning intact.
The child ID should not be specified only as a nested value in
tool_response. The response is tool-specific and model-facing, and current
multi-agent variants can hide spawn metadata. Correlation needs a stable hook
and telemetry contract independent of display settings.
Source feasibility
The change can be localized to the existing spawn and hook context paths:
- Both v1 and v2 spawn handlers retain the invocation
call_idand receive the
createdThreadIdfromspawn_agent_with_metadataor
spawn_agent_with_communication. At that point the parent tool call and
child ID are known together without inference. SessionSource::SubAgent(SubAgentSource::ThreadSpawn { ... })already carries
the parent thread and child metadata into the child session. Adding an
optional storedspawn_tool_use_idthere provides the value to both
SubagentStartandSubagentStop, including after reload.thread_spawn_subagent_hook_contextalready suppliesagent_idand
agent_typeto child tool hooks. Extending that context with
spawn_tool_use_idkeeps child ownership and lineage on the same explicit
path.- The generic
PostToolUsePayloadcurrently contains the tool name, tool-use
ID, input, and response. A spawn-specific optionalspawned_agent_id, or a
small typed extension map with the same schema result, can be populated by
the spawn handler after it receivesnew_thread_id.
The implementation should pass the spawn call ID for every native spawn. The
existing fork_parent_spawn_call_id option is conditional on context forking
and therefore is not the general spawn-edge contract.
Example hook payloads
Successful root spawn tool result:
{
"session_id": "session-1",
"turn_id": "turn-root",
"hook_event_name": "PostToolUse",
"tool_name": "spawn_agent",
"tool_use_id": "spawn-S1",
"spawned_agent_id": "agent-A",
"tool_input": { "task_name": "a", "message": "..." },
"tool_response": { "task_name": "/root/a" }
}
Child lifecycle events:
{
"session_id": "session-1",
"turn_id": "turn-A",
"hook_event_name": "SubagentStart",
"agent_id": "agent-A",
"agent_type": "default",
"spawn_tool_use_id": "spawn-S1"
}
{
"session_id": "session-1",
"turn_id": "turn-A",
"hook_event_name": "SubagentStop",
"agent_id": "agent-A",
"agent_type": "default",
"spawn_tool_use_id": "spawn-S1",
"stop_hook_active": false,
"last_assistant_message": "..."
}
Existing common fields such as cwd, model, permission_mode, and
transcript_path are omitted from the examples for readability.
Concurrency and nesting example
These records may be delivered in any order:
| Event | Emitting agent | tool_use_id / spawn_tool_use_id |
spawned_agent_id / agent_id |
|---|---|---|---|
PostToolUse(spawn_agent) |
root | spawn-S1 |
agent-A |
PostToolUse(spawn_agent) |
root | spawn-S2 |
agent-B |
SubagentStart |
A | spawn-S1 |
agent-A |
SubagentStart |
B | spawn-S2 |
agent-B |
PostToolUse(spawn_agent) |
A | spawn-SA1 |
agent-A1 |
SubagentStart |
A1 | spawn-SA1 |
agent-A1 |
The explicit pairs reconstruct exactly:
root --spawn-S1--> A --spawn-SA1--> A1
--spawn-S2--> B
The nested parent is also explicit because the existing agent_id on A's
PostToolUse(spawn_agent) identifies A as the emitter, while
spawned_agent_id identifies A1 as the created child. The result does not
depend on timestamps or on which start or tool-result event arrives first.
Backward compatibility
- The new fields are additive in JSON payloads.
- During a compatibility window, generated schemas may make the fields
optional so consumers can feature-detect them. For a successful native
thread spawn produced by a version that advertises this capability, both
fields should be present. - Consumers must construct a parent-child edge only when the two explicit IDs
agree. With an older producer or a missing field, they should report
correlation_scope=sessionorunlinked; they must not guess a nested tree. - Deserialization of persisted
ThreadSpawnsources should default a missing
spawn_tool_use_idtoNone, preserving old rollouts. A resumed old child
remains unlinked rather than receiving an inferred edge. - Existing
agent_id,tool_use_id,turn_id, andsession_idmeanings do
not change.
Privacy impact
The proposal adds only opaque IDs already created locally by Codex. It does not
add prompts, tool inputs, tool outputs, transcript content, credentials, user
identifiers, or network destinations. It does not enable OTel export or change
the existing opt-in boundary. Prompt content remains subject to the existing
separate telemetry setting.
Acceptance tests
- Released schemas: generated hook schemas expose
spawn_tool_use_idonSubagentStartandSubagentStop, and
spawned_agent_idon a successfulPostToolUse(spawn_agent)payload. - Single spawn: the parent tool result and child start/stop contain the
same(session_id, spawn_tool_use_id)and agree on the childagent_id. - Concurrent siblings: two spawns launched in the same parent turn receive
different tool-use IDs and join to their correct different child IDs after
the captured events are randomly permuted. - Nested spawn:
root -> A -> A1alongsideroot -> Breconstructs with
exactly one parent per non-root agent using only explicit IDs. - Lifecycle ordering: the tests pass when child start arrives before the
parentPostToolUse, and when stop or exporter delivery is delayed. - Old rollout: resuming a rollout without
spawn_tool_use_idsucceeds and
emits no exact parent edge. - Metadata hiding: the exact edge remains available when model-facing
spawn metadata is hidden. - OTel parity: exported OTel records expose the same edge as hooks, with no
prompt or tool-response content required. OTel export remains disabled by
default. - No heuristics: a verifier rejects or leaves unlinked any lifecycle event
whose explicit key is missing or conflicting; it never falls back to time,
order, turn, transcript path, or latest-spawn matching.
Non-goals
- Redesigning all Codex telemetry or defining a complete distributed tracing
protocol. - Adding
event_id,attempt_id, a durable spool, or delivery deduplication. - Inferring lineage for old sessions.
- Changing scheduling, concurrency, wait/interrupt behavior, permissions,
sandboxing, worktrees, model traffic, or agent UI behavior. - Enabling hooks or OTel for users, changing Codex configuration, or selecting
an observability backend.
Open technical questions
- Should
spawn_tool_use_idbecome required immediately for newly generated
thread-spawn lifecycle schemas, or remain optional for one compatibility
cycle while runtime tests require it for new spawns? - Should
spawned_agent_idbe a dedicated optional member of the generic
PostToolUsePayload, or should core support typed tool-specific hook
extensions? The wire field and semantics should be identical either way. - Which existing OTel lifecycle record is the preferred home for
agent_idplusspawn_tool_use_id? If no lifecycle log currently exists,
a narrowly scoped subagent start/stop record is sufficient; new span
hierarchy is not required for this proposal.
Sources
- OpenAI Docs: Hooks
- OpenAI Docs: observability and telemetry
- Codex
rust-v0.153.4generated hook schemas SubAgentSource::ThreadSpawnatrust-v0.153.4- v1 spawn handler at
rust-v0.153.4 - v2 spawn handler at
rust-v0.153.4 - Hook runtime at
rust-v0.153.4 - Hook wire structs at
rust-v0.153.4 - OTel tool-result event at
rust-v0.153.4
The current main branch was checked separately only to determine whether the
gap had already been closed. It had not at commit
38cbebaf3fe3e81a94bf462079e7cf9659fc9e50. Release behavior and the payload
examples above remain grounded in rust-v0.153.4, the latest published release
at the time of validation.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the v1 and v2 spawn handlers through SessionSource::SubAgent, thread_spawn_subagent_hook_context, and PostToolUsePayload. Review the listed acceptance tests and existing OTel lifecycle records before resolving the open schema questions. Done means explicit parent-child IDs survive concurrent, nested, reordered, delayed, and resumed-old-session cases without heuristic matching.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design, observability
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100