openai / openai/codex

Expose an explicit spawn-to-subagent correlation key

Open
#44,095 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

CLI enhancement hooks subagent
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:

  • PreToolUse and PostToolUse expose the spawn call's tool_use_id.
  • SubagentStart and SubagentStop expose 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 parent spawn_agent call 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:

  1. Add required spawn_tool_use_id: string to SubagentStart and
    SubagentStop for thread-spawned subagents. Its value is the parent
    spawn_agent call's tool_use_id.
  2. Add spawned_agent_id: string to a successful PostToolUse event for
    spawn_agent. Its value is the created child's existing agent_id.
  3. Expose the same two values on equivalent native OTel records. For an OTel
    codex.tool_result produced by a successful spawn_agent, add
    spawned_agent_id; for subagent lifecycle records, expose both agent_id
    and spawn_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_id and receive the
    created ThreadId from spawn_agent_with_metadata or
    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 stored spawn_tool_use_id there provides the value to both
    SubagentStart and SubagentStop, including after reload.
  • thread_spawn_subagent_hook_context already supplies agent_id and
    agent_type to child tool hooks. Extending that context with
    spawn_tool_use_id keeps child ownership and lineage on the same explicit
    path.
  • The generic PostToolUsePayload currently contains the tool name, tool-use
    ID, input, and response. A spawn-specific optional spawned_agent_id, or a
    small typed extension map with the same schema result, can be populated by
    the spawn handler after it receives new_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=session or unlinked; they must not guess a nested tree.
  • Deserialization of persisted ThreadSpawn sources should default a missing
    spawn_tool_use_id to None, preserving old rollouts. A resumed old child
    remains unlinked rather than receiving an inferred edge.
  • Existing agent_id, tool_use_id, turn_id, and session_id meanings 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

  1. Released schemas: generated hook schemas expose
    spawn_tool_use_id on SubagentStart and SubagentStop, and
    spawned_agent_id on a successful PostToolUse(spawn_agent) payload.
  2. Single spawn: the parent tool result and child start/stop contain the
    same (session_id, spawn_tool_use_id) and agree on the child agent_id.
  3. 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.
  4. Nested spawn: root -> A -> A1 alongside root -> B reconstructs with
    exactly one parent per non-root agent using only explicit IDs.
  5. Lifecycle ordering: the tests pass when child start arrives before the
    parent PostToolUse, and when stop or exporter delivery is delayed.
  6. Old rollout: resuming a rollout without spawn_tool_use_id succeeds and
    emits no exact parent edge.
  7. Metadata hiding: the exact edge remains available when model-facing
    spawn metadata is hidden.
  8. 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.
  9. 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

  1. Should spawn_tool_use_id become required immediately for newly generated
    thread-spawn lifecycle schemas, or remain optional for one compatibility
    cycle while runtime tests require it for new spawns?
  2. Should spawned_agent_id be 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.
  3. Which existing OTel lifecycle record is the preferred home for
    agent_id plus spawn_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

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.