Skip to content

Surface spawn_agent subagent activity over ACP (subagents are invisible to ACP clients) #238

Description

@gadzan

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:

  1. 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)
  2. 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
  3. (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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions