Skip to content

ACP drops subagent source identity and flattens parallel output into the parent stream #4106

Description

@wangmingliang-ms

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:

{
  "method": "session/update",
  "params": {
    "sessionId": "<parent-session>",
    "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:

    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:

    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:

    {
      "sessionUpdate": "tool_call",
      "toolCallId": "<subagent-A-tool-call>",
      "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:

    {
      "type": "assistant.message",
      "agentId": "<subagent-A-tool-call>",
      "data": {
        "parentToolCallId": "<subagent-A-tool-call>",
        "interactionId": "<subagent-A-interaction>",
        "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:

{
  "_meta": {
    "copilot": {
      "agentId": "<subagent-A-tool-call>",
      "parentToolCallId": "<subagent-A-tool-call>"
    }
  }
}

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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:agentsSub-agents, fleet, autopilot, plan mode, background agents, and custom agentsarea:non-interactiveNon-interactive mode (-p), CI/CD, ACP protocol, and headless automation

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions