ACP drops subagent source identity and flattens parallel output into the parent stream
- 主要言語
- Shell
- スター
- 11.2k
- フォーク
- 1.9k
- 平均マージ
- 14時間 16分
- マージ済み PR(30日)
- 6
説明
### 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.
コントリビューションガイド
調査の方向性
エントリポイント `copilot --acp` と、ネイティブの `assistant.message` および `assistant.message_delta` イベントを ACP の `session/update` 通知に変換するマッピングから始めます。2 つの並列な Explore サブエージェントを再現し、その後、各 `agent_message_chunk` を外側のタスクの `toolCallId` およびネイティブの `parentToolCallId` と比較します。ACP クライアントがプライマリ出力とサブエージェント出力を区別できるか、または分離されたタスクの結果が到着するまでサブエージェントのデルタが安全に抑制されれば完了です。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- shell
- 領域
- api, cli
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 静か
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 45/100