Skip to content

agentHost: stabilize subagent streams and snapshots - #340653

Draft
roblourens wants to merge 4 commits into
mainfrom
roblou/agents/subagent-response-reconciliation
Draft

roblourens wants to merge 4 commits into
mainfrom
roblou/agents/subagent-response-reconciliation

Conversation

@roblourens

@roblourens roblourens commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Summary

Address two independent subagent stability findings:

  • Keep Copilot subagent markdown/reasoning stream state alive independently of the parent turn, without allowing cancelled tails to poison a later child turn.
  • Compare the retained-subagent behavior snapshot's independent chat streams by their per-chat order, rather than imposing a cross-chat total order.

Observed in GitHub 37852164334, macOS: custom agent without a display name completes as a subagent returned one completed child turn with two distinct markdown parts containing the same answer. The duplication was present in the authoritative AHP subscription snapshot, not just the test's notification buffer. Linux and Windows passed that revision.

Cause and ownership

The trace has child text delta at 22:18:06.770, parent idle at .812, then the child's assembled message at .834. Both response parts came from live callbacks; no history reconstruction occurred between them.

The host kept child part identifiers on the parent's CopilotTurn. Completing or replacing that parent discarded the child's identifiers. A later assembled child message consequently looked unstreamed and was emitted again. Further child text/reasoning deltas were also dropped when no parent turn remained.

This confirmed defect is VS Code-owned. Retained/background children can legitimately outlive the parent. The SDK's inter-agent delivery timing exposes the incorrect lifetime assumption; the fix does not require delaying SDK idle or changing native event ordering.

Cross-chat snapshot contract

GitHub 37816575914, macOS passed the retained-subagent test's primary response/turn-state assertions but failed because parent tool completion interleaved differently with a child's response. This is a test projection issue, not evidence that the runtime must serialize independent agents.

Use the existing orderIndependentActionTypes opt-in only for this scenario. The generated AHP snapshot has identical client actions and identical server fields, event counts, and per-chat sequences in all three rounds; only cross-chat order changes. A deterministic regression verifies that same-chat reordering, failed tools, and missing or duplicated child replies still fail.

Changes

  • Store actual subagent response-part identifiers separately from root-turn/Fusion-phase state.
  • Reset child state at its own user-message, tool-round, and confirmed-completion boundaries, preserving legitimate repeated answers.
  • Keep root cancellation/late-event guards and root-owned Fusion phase handling intact.
  • Add seven deterministic regression/compatibility cases for parent idle/replacement, continuing markdown/reasoning, identical follow-up answers, tool-round boundaries, and cancelled-tail reuse.
  • Opt the retained-subagent scenario into the existing partial-order projection and regenerate only its AHP snapshot in tokenless strict replay. Preserve its exact parent tool sequence and child transcript/state assertions.
  • Document the lifetime invariant and the diagnostic distinction from text-based deduplication.

No model fixtures, timeouts, retries, platform gates, runtime event ordering, or primary final-response assertions are changed.

Validation

  • Before the fix: env -u COPILOT_HOME VSCODE_SKIP_PRELAUNCH=1 ./scripts/test.sh --run src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts --grep 'subagent content lifetime' --reporter dot — 1 passed, 5 failed on unchanged production code.
  • After the fix: env -u COPILOT_HOME VSCODE_SKIP_PRELAUNCH=1 ./scripts/test.sh --run src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts --grep 'subagent content lifetime|subagent message delta does not suppress|subagent reasoning delta routes|reasoning delta after tool_start' --reporter dot — all 9 passed.
  • env -u COPILOT_HOME VSCODE_SKIP_PRELAUNCH=1 ./scripts/test.sh --run src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts --run src/vs/platform/agentHost/test/node/copilotAgent.test.ts --run src/vs/platform/agentHost/test/node/ahpSnapshot.test.ts --reporter dot — final implementation: 1621 passed, 1 existing pending. The added cancelled-tail test failed before the quarantine guard and passes with it.
  • env -u COPILOT_HOME VSCODE_SKIP_PRELAUNCH=1 ./scripts/test-integration.sh --run src/vs/platform/agentHost/test/node/e2e/providers/copilotAgentHostE2E.integrationTest.ts --grep 'custom agent without a display name completes as a subagent|retained background subagent completes repeated follow-up turns|provider errors:' --reporter dot — five unchanged strict-replay executions, all 6 passed each time (30 passes).
  • The same six-case strict-replay command passed on the final cancellation-compatible implementation. After regenerating the retained-subagent AHP snapshot, env -u COPILOT_HOME VSCODE_SKIP_PRELAUNCH=1 ./scripts/test-integration.sh --run src/vs/platform/agentHost/test/node/e2e/providers/copilotAgentHostE2E.integrationTest.ts --grep 'retained background subagent completes repeated follow-up turns|custom agent without a display name completes as a subagent' --reporter dot passed five times (10 passes).
  • npm run typecheck-client — passed.
  • npm run eslint -- src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts src/vs/platform/agentHost/test/node/ahpSnapshot.test.ts src/vs/platform/agentHost/test/node/e2e/suites/subagentSuite.ts — passed.
  • node build/lib/formatter.ts --verify src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts src/vs/platform/agentHost/test/node/ahpSnapshot.test.ts src/vs/platform/agentHost/test/node/e2e/suites/subagentSuite.ts — passed.
  • git diff --check and signed commit hygiene — passed.

Client output was refreshed using the repository incremental transpiler. Shared dependencies were not modified. Main, including the landed full-snapshot diagnostics, was merged without rewriting history.

Final head 10fec39a15a5 passed GitHub 37882030913 overall, including Compile & Hygiene and all three Electron integration jobs. Its Linux Dev Container preparation UI smoke timeout, also observed on the unchanged diagnostics-only branch, passed on an isolated retry without code changes; the original UI timeout's cause is not claimed fixed.

Both independent packaged Windows x64/Linux x64/macOS ARM64 Electron integration matrices passed: 482615 and 482616. All six logs explicitly show both the originally failing custom-agent case and retained-subagent follow-up case executing and passing (12 packaged regression passes, excluding other providers' intentional skips).

Remaining overall Azure gates are Windows smoke in 482615 and Linux smoke in 482616: the same 272K model-picker DOM click timeout reproduced on unchanged main 482526. Logs are preserved; only those failed jobs are being retried, without UI/product/test changes. Publishing and release are disabled. Old-head builds 482609/482610 and GitHub runs were superseded. The PR remains draft pending final CI gates and maintainer review; auto-merge is not enabled.

(Written by Copilot)

roblourens and others added 2 commits October 8, 2026 20:40
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 9, 2026 03:43
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Base: e7188bfe Current: c1471e6f

No screenshot changes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Quarantined post-cancellation child events can seed invisible part IDs and cause later deltas to be lost.

1 open finding
What changed in this PR

Separates subagent stream state from parent-turn state so child responses can continue after parent completion or replacement.

Changes:

  • Adds child-owned markdown/reasoning part tracking and lifecycle resets.
  • Adds regression tests for parent boundaries and repeated child turns.
  • Documents subagent stream troubleshooting.
File Description
src/​vs/​platform/​agentHost/​node/​copilot/​copilotAgentSession.ts Manages subagent response-part state independently.
src/​vs/​platform/​agentHost/​test/​node/​copilotAgentSession.test.ts Adds subagent stream-lifetime coverage.
src/​vs/​platform/​agentHost/​test/​node/​e2e/​README.md Documents the lifecycle invariant.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +2452 to +2457
private _getResponsePartState(parentToolCallId: string | undefined): ICopilotResponsePartState | undefined {
// Fusion phase content belongs to the root turn; retained subagent content does not.
return parentToolCallId !== undefined && !this._fusionPhaseLabels.has(parentToolCallId)
? this._subagentResponsePartState
: this._currentTurn.value;
}
roblourens and others added 2 commits October 8, 2026 20:54
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@roblourens roblourens changed the title agentHost: preserve subagent streams across parent turns agentHost: stabilize subagent streams and snapshots Oct 9, 2026

This branch has not been deployed

No deployments
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.

2 participants