Summary
When the agent uses the native spawn_agent tool, the adapter does not emit
anything over ACP. The spawned subagent runs, produces a full transcript, and
reports back — but an ACP client sees zero trace of it: no tool_call, no
tool_call_update, no nested session, no metadata. The only hint a client gets
is whatever the parent model happens to narrate in its own assistant text
("I dispatched two subagents…").
The result is that every ACP client (terminal UI, web dashboards, etc.) appears
to "lose" the subagent's work. Users reasonably conclude the agent hallucinated
the subagents, when in fact they ran and did real work — the data just never
crosses the ACP boundary.
Environment
@agentclientprotocol/codex-acp 0.0.44
codex-cli 0.136.0
- ACP client: a third-party client that records every
session/update
notification it receives.
What happens
spawn_agent is a codex-native tool. It executes entirely inside the codex
process; the call and its result are recorded only in codex's own rollout store
(~/.codex/sessions/.../rollout-*.jsonl). The adapter never translates it into
an ACP tool_call, so it is absent from the session/update stream.
I exhaustively searched the ACP session/update stream for a session that did
spawn two subagents. Counts:
| Marker |
In codex rollout |
In ACP session/update stream |
spawn_agent call |
6 |
0 |
subagent_notification result |
5 |
0 |
| child session ids |
present |
0 |
nested sessionId |
n/a |
0 (only the parent) |
spawn_agent in available_commands_update |
n/a |
not advertised |
The only stream hits for "subagent" were (a) the user's own prompt text and
(b) the parent model's narration in agent_message_chunk — i.e. plain text,
not a structured record.
Concrete shapes (from the codex rollout)
The tool call, as codex records it internally:
{
"type": "function_call",
"name": "spawn_agent",
"call_id": "call_2GXFXjaZUZHGQC8G98UIeQCl",
"arguments": {
"agent_type": "explorer",
"fork_context": true,
"message": "You are reviewing uncommitted changes ... Return findings ordered by severity ..."
}
}
The result is injected back into the parent as a synthetic user message:
{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "<subagent_notification>\n{\"agent_path\":\"019efcc9-4e4b-...\",\"status\":{\"completed\":\"<the subagent's final report text>\"}}"
}]
}
So the full lifecycle the adapter has access to is:
spawn_agent call (with call_id, agent_type, message) → child agent runs →
notification with agent_path + status.completed (the subagent's final report).
Why it matters
ACP exists so a client can render the agent's work. Tool calls, file reads,
commands, and edits all surface as tool_call/tool_call_update today.
Subagents are arguably the most important thing to surface — they do
self-contained units of work whose output the user wants to inspect — yet they
are the one activity that is completely hidden. This makes the adapter's
behavior look broken from the outside and erodes trust in the agent.
Proposed solution
Map the spawn_agent lifecycle onto the ACP tool-call protocol the adapter
already uses for other tools:
-
On spawn_agent call — emit a tool_call session/update:
toolCallId: the call_id (e.g. call_2GXFXjaZUZHGQC8G98UIeQCl)
kind: a sensible classifier (e.g. "think" / "other", or a new
"delegate" kind if you'd rather not overload existing ones)
status: "in_progress"
title: something like spawn ${agent_type}: ${first line of message}
rawInput: the parsed arguments (agent_type, fork_context, message)
-
On the subagent_notification — emit a tool_call_update for the same
toolCallId:
status: "completed" (or "failed" if the notification carries an error)
content: the subagent's report (status.completed) as a
[{ "type": "content", "content": { "type": "text", "text": ... } }] block,
so clients show it like any other tool output
- optionally carry
agent_path (the child session id) in _meta so a client
can offer a drill-down
-
(Optional, larger) stream the child agent's own updates — either as a
nested ACP session keyed by agent_path, or as incremental
tool_call_update content on the parent's tool call. Even without this,
step 1+2 alone make subagent work visible, which solves the reported problem.
Acceptance criteria
- A session that calls
spawn_agent produces at least one tool_call and one
tool_call_update in the ACP session/update stream.
- The
tool_call_update content contains the subagent's final report text.
- Existing tool-call rendering in ACP clients shows the subagent step with no
client-side changes required.
Alternatives considered
- Client reads codex rollout files directly (
~/.codex/sessions/...):
works, but it is codex-specific, reaches into private on-disk state, and
breaks whenever the rollout format changes. ACP exists precisely so clients
don't have to do this.
Summary
When the agent uses the native
spawn_agenttool, the adapter does not emitanything over ACP. The spawned subagent runs, produces a full transcript, and
reports back — but an ACP client sees zero trace of it: no
tool_call, notool_call_update, no nested session, no metadata. The only hint a client getsis whatever the parent model happens to narrate in its own assistant text
("I dispatched two subagents…").
The result is that every ACP client (terminal UI, web dashboards, etc.) appears
to "lose" the subagent's work. Users reasonably conclude the agent hallucinated
the subagents, when in fact they ran and did real work — the data just never
crosses the ACP boundary.
Environment
@agentclientprotocol/codex-acp0.0.44codex-cli0.136.0session/updatenotification it receives.
What happens
spawn_agentis a codex-native tool. It executes entirely inside the codexprocess; the call and its result are recorded only in codex's own rollout store
(
~/.codex/sessions/.../rollout-*.jsonl). The adapter never translates it intoan ACP
tool_call, so it is absent from thesession/updatestream.I exhaustively searched the ACP
session/updatestream for a session that didspawn two subagents. Counts:
session/updatestreamspawn_agentcallsubagent_notificationresultsessionIdspawn_agentinavailable_commands_updateThe only stream hits for "subagent" were (a) the user's own prompt text and
(b) the parent model's narration in
agent_message_chunk— i.e. plain text,not a structured record.
Concrete shapes (from the codex rollout)
The tool call, as codex records it internally:
{ "type": "function_call", "name": "spawn_agent", "call_id": "call_2GXFXjaZUZHGQC8G98UIeQCl", "arguments": { "agent_type": "explorer", "fork_context": true, "message": "You are reviewing uncommitted changes ... Return findings ordered by severity ..." } }The result is injected back into the parent as a synthetic user message:
{ "type": "message", "role": "user", "content": [{ "type": "input_text", "text": "<subagent_notification>\n{\"agent_path\":\"019efcc9-4e4b-...\",\"status\":{\"completed\":\"<the subagent's final report text>\"}}" }] }So the full lifecycle the adapter has access to is:
spawn_agentcall (withcall_id,agent_type,message) → child agent runs →notification with
agent_path+status.completed(the subagent's final report).Why it matters
ACP exists so a client can render the agent's work. Tool calls, file reads,
commands, and edits all surface as
tool_call/tool_call_updatetoday.Subagents are arguably the most important thing to surface — they do
self-contained units of work whose output the user wants to inspect — yet they
are the one activity that is completely hidden. This makes the adapter's
behavior look broken from the outside and erodes trust in the agent.
Proposed solution
Map the
spawn_agentlifecycle onto the ACP tool-call protocol the adapteralready uses for other tools:
On
spawn_agentcall — emit atool_callsession/update:toolCallId: thecall_id(e.g.call_2GXFXjaZUZHGQC8G98UIeQCl)kind: a sensible classifier (e.g."think"/"other", or a new"delegate"kind if you'd rather not overload existing ones)status:"in_progress"title: something likespawn ${agent_type}: ${first line of message}rawInput: the parsedarguments(agent_type,fork_context,message)On the
subagent_notification— emit atool_call_updatefor the sametoolCallId:status:"completed"(or"failed"if the notification carries an error)content: the subagent's report (status.completed) as a[{ "type": "content", "content": { "type": "text", "text": ... } }]block,so clients show it like any other tool output
agent_path(the child session id) in_metaso a clientcan offer a drill-down
(Optional, larger) stream the child agent's own updates — either as a
nested ACP session keyed by
agent_path, or as incrementaltool_call_updatecontent on the parent's tool call. Even without this,step 1+2 alone make subagent work visible, which solves the reported problem.
Acceptance criteria
spawn_agentproduces at least onetool_calland onetool_call_updatein the ACPsession/updatestream.tool_call_updatecontent contains the subagent's final report text.client-side changes required.
Alternatives considered
~/.codex/sessions/...):works, but it is codex-specific, reaches into private on-disk state, and
breaks whenever the rollout format changes. ACP exists precisely so clients
don't have to do this.