Skip to content

feat(acp): surface Claude Code subagent parent on ACPToolCallEvent - #5453

Draft
JoaVirtudes19 wants to merge 1 commit into
OpenHands:mainfrom
JoaVirtudes19:feat/acp-subagent-parent-tool-call-id
Draft

JoaVirtudes19 wants to merge 1 commit into
OpenHands:mainfrom
JoaVirtudes19:feat/acp-subagent-parent-tool-call-id

Conversation

@JoaVirtudes19

Copy link
Copy Markdown

HUMAN:


AGENT:

Why

When an ACPAgent runs Claude Code, Claude Code can hand work to subagents through its Task/Agent tool. Their tool calls arrive on the same ACP session as the main agent's, so every ACPToolCallEvent looks the same. A consumer cannot tell the main agent's calls from a subagent's, or which subagent made which call, and with subagents running in parallel the event stream is hard to follow.

claude-agent-acp already sends the link: it stamps _meta.claudeCode.parentToolUseId, the tool_call_id of the spawning Task call, on every update a subagent produces (agentclientprotocol/claude-agent-acp#341). ACPAgent drops _meta when it builds the event.

Summary

  • ACPToolCallEvent gets an optional parent_tool_call_id: str | None = None. It holds the tool_call_id of the tool call that spawned the subagent making the call. It is None when the ACP server reports no parent, as for the main agent's own calls.
  • The bridge reads it from field_meta on both ToolCallStart and ToolCallProgress, and carries it to the terminal event and to the supersede event emitted by _cancel_inflight_tool_calls.
  • Tests: three new tests cover the start, progress-only and cancel paths. The existing spec'd ToolCallStart/ToolCallProgress mocks now set field_meta = None, because reading an unset field on MagicMock(spec=...) raises. feat(acp): add Gemini CLI as supported ACP server #2619 did the same for content.

Issue Number

To be filed before this leaves draft. Related: #3907, which asks for the same parent link for the SDK's own TaskToolSet subagents.

How to Test

Unit tests:

CI=true uv run pytest -n auto tests/sdk/agent/test_acp_agent.py tests/sdk/agent/test_acp_tracing.py \
  tests/sdk/agent/test_acp_dedup_and_truncation.py tests/sdk/agent/test_acp_conformance.py \
  tests/sdk/event tests/sdk/conversation

1529 passed. pre-commit passes on the changed files, and make test-server-schema passes.

Live, against Claude Code through a LiteLLM proxy, using claude-agent-acp 0.63.0 (the SDK's pinned version):

ANTHROPIC_BASE_URL=<proxy> ANTHROPIC_API_KEY=<key> ACP_MODEL=anthropic/claude-sonnet-5 \
  uv run python .pr/acp_subagent_parent_live_check.py

The main agent runs echo main-agent, then spawns two subagents in parallel that run echo subagent-a and echo subagent-b. Every ACPToolCallEvent on conversation.state.events:

tool_call_id                       status      parent_tool_call_id                title
toolu_014ftaMP6NN57uyNALTDLgp3     pending     None                               Terminal
toolu_018WYSnHjdCzgBLJtEFbbCJ4     pending     None                               Task
toolu_016a5ov5XYw9irtgmn1pTSHt     pending     None                               Task
toolu_014ftaMP6NN57uyNALTDLgp3     completed   None                               echo main-agent
toolu_016AaAJZjZmKnwd6kpjDCEBJ     pending     toolu_018WYSnHjdCzgBLJtEFbbCJ4     echo subagent-a
toolu_016AaAJZjZmKnwd6kpjDCEBJ     completed   toolu_018WYSnHjdCzgBLJtEFbbCJ4     echo subagent-a
toolu_01C3WofVMiRsoSNFgbLvetxM     pending     toolu_016a5ov5XYw9irtgmn1pTSHt     echo subagent-b
toolu_01C3WofVMiRsoSNFgbLvetxM     completed   toolu_016a5ov5XYw9irtgmn1pTSHt     echo subagent-b
toolu_018WYSnHjdCzgBLJtEFbbCJ4     completed   None                               Subagent A echo test
toolu_016a5ov5XYw9irtgmn1pTSHt     completed   None                               Subagent B echo test

subagent calls: 2, distinct parents: 2

On the wire (exclude_none=True):
{"tool_call_id":"toolu_014ftaMP6NN57uyNALTDLgp3","title":"echo main-agent","status":"completed","kind":"ACPToolCallEvent"}
{"tool_call_id":"toolu_016AaAJZjZmKnwd6kpjDCEBJ","title":"echo subagent-a","status":"completed","parent_tool_call_id":"toolu_018WYSnHjdCzgBLJtEFbbCJ4","kind":"ACPToolCallEvent"}

Each subagent's calls point at the Task call that spawned it. The main agent's own command and its two Task calls carry no parent, and the main agent's event serializes exactly as before.

Video/Screenshots

N/A: there is no UI change. The live output above is the evidence.

Design Doc

Not added: the change is one optional field, and the before/after shape is in the output above.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

Compatibility.

Relation to ACP subagent sessions.

  • The ACP subagent RFD (feat(unstable): Intial RFD and schema for subagents agentclientprotocol/agent-client-protocol#1992, unstable) models subagents as child sessions.
  • ACPAgent declares no client capabilities, and agent-client-protocol 0.12.1 has no subagent types. Servers therefore keep subagent activity on the root session, and claude-agent-acp documents parentToolUseId on every update for such clients.
  • This PR surfaces that existing signal; it does not model subagents. If ACPAgent later opts into native sessions, child activity becomes per-session and this field simply stays None for those events. Nothing needs to migrate or be removed.
  • The name follows the SDK's tool_call_id vocabulary. It is distinct from the conversation-tree Event.parent_id.

Why a parsed field, not a _meta passthrough.

  • ACPAgent already reads harness-specific _meta, for example Gemini's _meta.quota in _extract_token_usage. The parse here is unconditional and type-checked; it does not branch on which harness is running.
  • A raw passthrough would carry Claude's full toolResponse on every update. That would duplicate raw_output and bypass _mask_tool_call_entry.
  • ToolCallProgress is merged too because claude-agent-acp attaches the parent best-effort. If a call is first announced by the permission flow, the start notification can lack the parent and a later tool_call_update restores it. In that case the started event has None and the terminal event carries the parent, which fits the "last event per tool_call_id wins" handling in RemoteConversation.
  • claude-agent-acp does not forward subagent text or thinking unless the client opts in, so tool calls are the only subagent output ACPAgent receives.

Docs. No OpenHands/docs page documents ACPToolCallEvent fields today. I'm happy to add a short "Subagent tool calls" note to sdk/guides/agent-acp.mdx in a companion PR.

🤖 Generated with Claude Code

claude-agent-acp stamps _meta.claudeCode.parentToolUseId on every update a
subagent produces. ACPToolCallEvent now carries it as parent_tool_call_id,
read on ToolCallStart and ToolCallProgress and kept on the terminal and
supersede events, so consumers can tell a subagent's tool calls from the
main agent's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. Because this is a fork PR, the workflow will open or update a cleanup PR against main after merge.

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