Skip to content

Attribute Codex subagent tool calls and record what subagents return - #300

Merged
Siddhant-K-code merged 1 commit into
mainfrom
siddhant-k-code-attribute-codex-subagent-tool-events
Oct 26, 2026
Merged

Siddhant-K-code merged 1 commit into
mainfrom
siddhant-k-code-attribute-codex-subagent-tool-events

Conversation

@Siddhant-K-code

Copy link
Copy Markdown
Owner

Follow-up to #294. Codex fires a subagent's hooks under the parent session_id. Because of that, a subagent's tool calls could not be told apart from the parent's, and the trace did not record what the subagent returned.

Recording (hooks.py)

  • For providers that keep subagents in-session (everything outside _SUBAGENT_SESSION_PROVIDERS), tool_call, tool_result and error events get agent_id and agent_type when the payload has an agent_id. Copilot events are unchanged.
  • subagent_stop records last_assistant_message as text and also records agent_transcript_path, the same way handle_stop does, and redaction applies. stop_hook_active does not suppress the event.
  • Pending-call pairing still matches on tool_use_id first. When there is no tool_use_id, it prefers a pending call from the same agent, then the oldest one.

Rendering

Attribution only appears when agent_id is present, so unattributed traces render exactly as before. I checked this byte-for-byte against main, with and without color.

Command Change
replay [subagent <type> <short id>] tag on tool_call/tool_result/error; agent=<id> on subagent_start/stop; returned: "…" line on subagent_stop
replay --expand-subagents (event tree) same tags; agent=<short id> on start/stop; returned: line
replay --tree, tree (summary) └─ subagent <type> <short id> N tools ✗ N errors line under the session; tree --json gains subagents only when present
stats Subagent Tool Calls: section, only when a subagent made tool calls
explain Subagent: <type> <short id> N tools line per phase

The short ID is the last 8 characters, because Codex agent IDs are UUIDv7 and share a timestamp prefix with the session ID.

Synthetic before/after for replay:

before:
+  1.00s ⤷ subagent_start default
+  2.00s → tool_call Bash
                $ echo synthetic
+  4.00s ⤶ subagent_stop default

after:
+  1.00s ⤷ subagent_start default agent=d0000011-0000-4000-8000-000000000011
+  2.00s → tool_call Bash [subagent default 00000011]
                $ echo synthetic
+  4.00s ⤶ subagent_stop default agent=d0000011-0000-4000-8000-000000000011
                returned: "Synthetic subagent answer."

tree:

<session>  openai-codex  $0.0012  3 tools  4.0s  ✓
└─ subagent default 00000011  2 tools  ✗ 1 errors

Final-answer audit

A subagent_stop now carries text, so I checked every consumer that reads text or a final message to make sure none of them takes this as the session's answer:

  • Filter on event type, not affected: explain, review, postmortem, why, timeline, checkpoint/compaction, OTLP and GenAI export, Langfuse, share, compare, freeze, eval scorers, cost, compliance, watch, the web dashboard and the VS Code panel. They read assistant_response or llm_response.
  • Fixed: disclosure previously undercounted. It now counts a subagent's returned text as a response and as content-rich.

A test checks that the checkpoint, OTLP, GenAI and timeline final answer stays the same when a subagent_stop with text comes after the session's answer.

Docs

  • capture_matrix.json and docs/capture-matrix.md are updated. The remaining Codex gap: no event links a subagent to the spawn_agent call that started it.
  • docs/integrations.md and docs/commands.md are also updated.

Tests

New or extended tests cover:

  • tool events with and without agent_id
  • agent_type without agent_id is ignored
  • the SubagentStop fields, stop_hook_active, and both store-level and hook-level redaction
  • pairing: interleaved calls, no tool_use_id, and oldest-call fallback
  • Copilot is unchanged
  • capture-matrix fixture assertions
  • replay, tree, summary, JSON, stats and explain output for both attributed and unattributed traces
  • disclosure counts, and that the final answer is unchanged

Full suite: 2482 tests OK (2 skipped). I also ran a live check with codex-cli 0.163.0-alpha.11: one subagent ran one shell command in a read-only sandbox, and its tool events carried agent_id/agent_type and rendered as attributed. subagent_stop had text and agent_transcript_path.

Codex fires a subagent's hooks under the parent session_id, so its tool
calls were indistinguishable from the parent's. Record agent_id and
agent_type on tool_call, tool_result and error events for providers that
keep subagents in-session, and record SubagentStop's last_assistant_message
(as text) and agent_transcript_path, with redaction. stop_hook_active does
not suppress subagent_stop. Copilot events are unchanged.

Pending-call pairing still matches by tool_use_id first; without one it
prefers a pending call from the same agent.

replay, replay --tree/--expand-subagents, tree, stats and explain show the
attribution only when agent_id is present, so unattributed traces render
as before. Disclosure counts a subagent's returned text as a response.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Siddhant-K-code
Siddhant-K-code merged commit f892a72 into main Oct 26, 2026
5 checks passed
@Siddhant-K-code
Siddhant-K-code deleted the siddhant-k-code-attribute-codex-subagent-tool-events branch October 26, 2026 18:00
Siddhant-K-code added a commit that referenced this pull request Oct 26, 2026
Bump version to 0.103.0. Includes #293, #299 and #300.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant