ACP drops subagent source identity and flattens parallel output into the parent stream
- Linguagem predominante
- Shell
- Estrelas
- 11.2k
- Forks
- 1.9k
- Merge médio
- 14h 16min
- PRs com merge (30d)
- 6
Descrição
### 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.
Guia de contribuição
Direção de pesquisa
Comece pelo ponto de entrada `copilot --acp` e pelo mapeamento que transforma os eventos nativos `assistant.message` e `assistant.message_delta` em notificações ACP `session/update`. Reproduza dois subagentes Explore em paralelo e, em seguida, compare cada `agent_message_chunk` com o `toolCallId` da tarefa externa e o `parentToolCallId` nativo. A tarefa estará concluída quando os clientes ACP puderem distinguir a saída primária da saída dos subagentes, ou quando os deltas dos subagentes forem suprimidos com segurança até a chegada do resultado da tarefa separada.
Escrita pelo modelo de indexação a partir do texto da issue.
Avaliação
- Stack de tecnologia
- shell
- Domínio
- api, cli
- Tipo de issue
- Bug
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Status de atividade
- Pouca atividade
- Clareza
- Razoavelmente clara
- Facilidade para iniciantes
- 45/100