ACP drops subagent source identity and flattens parallel output into the parent stream
- Dominant language
- Shell
- Stars
- 11.2k
- Forks
- 1.9k
- Avg merge
- 14h 16m
- Merged PRs (30d)
- 6
Description
### Describe the bug
When `copilot --acp` runs multiple synchronous subagents in parallel, subagent text deltas are forwarded into the parent ACP session as ordinary `agent_message_chunk` updates, but their source identity is removed.
Copilot's internal/SDK events already distinguish these messages with `agentId` and `data.parentToolCallId`. The outer `task` tool calls also have stable, distinct `toolCallId` values. However, the ACP notifications for the corresponding text contain only:
```json
{
"method": "session/update",
"params": {
"sessionId": "",
"update": {
"sessionUpdate": "agent_message_chunk",
"content": {
"type": "text",
"text": "..."
}
}
}
}
```
There is no `_meta`, `parentToolCallId`, `agentId`, message ID, or child session ID on these updates. Consequently, an ACP client cannot tell whether a chunk came from the primary agent, subagent A, or subagent B. When parallel subagents stream concurrently, their text can be interleaved and rendered as a corrupted primary-agent response.
This appears to be an ACP adapter regression/gap rather than a core event-model limitation. In particular, #2265 was closed after newer SDK versions added `parentToolCallId` to `assistant.message` and `assistant.message_delta`; that field is available before the ACP mapping but is not preserved in the ACP output.
### Affected version
GitHub Copilot CLI `1.0.71-0`
### Steps to reproduce the behavior
1. Start Copilot in ACP stdio mode:
```bash
copilot --acp
```
2. From an ACP client, call `initialize`, then `session/new`.
3. Send a prompt that requires exactly two synchronous Explore subagents to run in parallel, with distinguishable output. For example:
```text
Launch exactly two Explore subagents concurrently.
Subagent A must return a long report beginning with ALPHA_SUBAGENT_START
and ending with ALPHA_SUBAGENT_END.
Subagent B must return a long report beginning with BETA_SUBAGENT_START
and ending with BETA_SUBAGENT_END.
Wait for both, then reply PRIMARY_FINAL.
```
4. Record every `session/update` notification without transforming it.
5. Observe that ACP first emits two distinct outer `tool_call` updates. Each has a different `toolCallId` and a `rawInput` object identifying the subagent invocation, for example:
```json
{
"sessionUpdate": "tool_call",
"toolCallId": "",
"status": "pending",
"rawInput": {
"name": "alpha-probe",
"agent_type": "explore",
"description": "...",
"prompt": "..."
}
}
```
6. While those tasks run, observe many `agent_message_chunk` notifications. All of them have the exact same field shape and none carries either outer task's `toolCallId` or another source identifier.
7. Compare with the session's native events. The corresponding subagent events are distinguishable:
```json
{
"type": "assistant.message",
"agentId": "",
"data": {
"parentToolCallId": "",
"interactionId": "",
"content": "ALPHA_SUBAGENT_START..."
}
}
```
In one controlled run with two parallel Explore subagents, ACP emitted 2,333 `agent_message_chunk` notifications. Every chunk had only `sessionUpdate`, `content.type`, and `content.text`. No source metadata was present.
The final outer `tool_call_update` for each task does contain that subagent's complete, correctly separated response in `rawOutput.content`, and it exactly matches the corresponding native `assistant.message`. However, this arrives only at completion, after the unscoped deltas have already been emitted into the parent message stream.
### Expected behavior
ACP clients should be able to distinguish primary-agent output from each subagent's output.
A minimal backward-compatible fix would be to preserve the identity already present on the native event, for example in `_meta` on the ACP notification or update:
```json
{
"_meta": {
"copilot": {
"agentId": "",
"parentToolCallId": ""
}
}
}
```
Subagent lifecycle events should ideally also be exposed so clients can create and finish the correct UI activity item.
Longer term, this could align with ACP's child-session/subagent direction. Until source-aware ACP updates are available, a safer fallback would be to suppress subagent message/reasoning deltas from the parent ACP message stream rather than emit them as indistinguishable primary-agent chunks. The completed task result is already available through the corresponding outer `tool_call_update.rawOutput`.
### Additional context
- Reproduced on Linux x86_64 using ACP over stdio.
- This is most visible with two or more parallel synchronous subagents because their streamed text may interleave.
- Primary-agent output is also represented as the same unscoped `agent_message_chunk` shape, so an ACP client cannot safely filter or route individual chunks by source.
- The two outer `task` tool calls remain distinguishable by `toolCallId`; the loss occurs specifically when native assistant/reasoning events are mapped to ACP message/thought chunks.
- Related but not duplicate:
- #2265 concerns SDK/Fleet per-subagent output and was closed after `parentToolCallId` was added to SDK events. This report is about that identity being dropped by the ACP adapter.
- #2645 concerns missing SDK delta events. Here, deltas are emitted over ACP, but without their source identity.
Contributor guide
Research direction
Start with the `copilot --acp` entry point and the mapping that turns native `assistant.message` and `assistant.message_delta` events into ACP `session/update` notifications. Reproduce two parallel Explore subagents, then compare each `agent_message_chunk` with the outer task `toolCallId` and native `parentToolCallId`. Done means ACP clients can distinguish primary and subagent output, or subagent deltas are safely suppressed until the separated task result arrives.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- shell
- Domain
- api, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100