Skip to content

agentHost/claude: Phase 6 — sendMessage, single-turn, no tools#314216

Open
TylerLeonhardt wants to merge 7 commits intomainfrom
tyler/claude-phase6
Open

agentHost/claude: Phase 6 — sendMessage, single-turn, no tools#314216
TylerLeonhardt wants to merge 7 commits intomainfrom
tyler/claude-phase6

Conversation

@TylerLeonhardt
Copy link
Copy Markdown
Member

Summary

Implements the Phase 6 plan for the Claude IAgent provider: provisional sessions materialize on first sendMessage, route a single-turn prompt through the Anthropic Claude Agent SDK's WarmQuery, and stream SDKMessages back as protocol AgentSignals via a pure mapSDKMessageToAgentSignals reducer.

Tools remain denied (canUseTool: 'deny') for this phase. Fork moves to Phase 6.5 (needs message-UUID lookup); plan-mode UI to Phase 7.

What's in this stack

This PR stacks 4 commits on main:

  1. agentHost/claude: post-Phase-4 cleanup — pre-merged separately
  2. agentHost/claude: lock Phase 5 implementation plan — Phase 5 plan
  3. agentHost/claude: Phase 5 — IAgent provider skeleton — Phase 5 implementation
  4. agentHost/claude: Phase 6 — sendMessage, single-turn, no toolsthis work

Highlights of the Phase 6 commit

  • Materialization race safety. ClaudeAgent.sendMessage routes through _sessionSequencer so concurrent first sends collapse into one materialize + N ordered sends — no double-fork of the SDK subprocess.
  • Two abort gates in _materializeProvisional. Post-sdk.startup() and post-_writeCustomizationDirectory() checks ensure that a disposeSession landing mid-materialize disposes the produced WarmQuery instead of leaking a subprocess into _sessions. (Council-review C1.)
  • Session ↔ mapper split. ClaudeAgentSession owns the prompt iterator + per-turn deferreds; mapSDKMessageToAgentSignals is a pure reducer with state owned by the session — independently testable, no shared mutable state between sessions.
  • IClaudeAgentSdkService.startup() added alongside listSessions() so the agent can be tested with a fake SDK without spawning a real subprocess.
  • Reducer ordering invariant locked. SessionResponsePart is allocated on content_block_start BEFORE any delta can arrive — deltas are SDK-ordered after the start — so the protocol's "part-must-precede-delta" invariant (actions.ts:233, 540) holds by construction.

Tests

  • 43 unit tests in claudeAgent.test.ts covering: materialize lifecycle (provisional/abort/double-materialize), prompt iterator pumping, dispose ↔ shutdown ↔ send race matrix, the C1 dispose-during-customization-write regression, mapper output for text + thinking + result, and the SDK loader's cache-and-log-once contract.
  • 2 proxy-backed integration tests in claudeAgent.integrationTest.ts (council-review C2): real ClaudeProxyService (loopback HTTP server), stubbed Copilot CAPI streaming canned MessageStreamEvents — exercises the full agent → proxy → CAPI → SSE → agent pipeline including the ANTHROPIC_AUTH_TOKEN nonce contract.
  • Council reviewed by 3 reviewers (Gemini / GPT / Opus). C1 / C2 / S1 findings all addressed in this PR.

Validation

  • tsgo --noEmit -p src/tsconfig.json: clean
  • eslint on all modified files: clean
  • npm run gulp -- hygiene: clean for affected files
  • 43/43 unit pass, 2/2 integration pass

Out of scope (deferred per plan)

  • Tools beyond canUseTool: 'deny' (Phase 6.5+)
  • Fork via config.fork (Phase 6.5)
  • Mode dropdown / branch UI (Phase 7+)
  • Transcript reconstruction in getSessionMessages (Phase 13)
  • Subagent + permission-request signals (Phase 7)

- roadmap.md: mark Phase 4 as DONE, link merged PR #313780.
- phase4-plan.md: record live-system smoke completion in §7.8;
  disabled-gate run skipped (covered by unit tests + env-var guard).
- claudeAgent.test.ts: drop gratuitous 'as unknown as' cast in the
  CCAModel fixture (literal already matches CCAModelBilling exactly;
  plan §7.4 forbids unsafe casts in tests).
Handoff plan for Phase 5 (replace 7 throwing stubs in claudeAgent.ts).
Locked against post-PR-#313841 reality (provisional sessions,
onDidMaterializeSession, 30s empty-session GC) and the IAgent contract
on origin/main.

Decisions captured:
- Non-fork createSession is synchronous and in-memory; fork deferred
  to Phase 6 (throws TODO).
- IClaudeAgentSdkService surface mirrors IAgent (no dir parameter on
  listSessions); SDK loader caches resolved module, retries on
  failure, logs once.
- listSessions joins SDK enumeration with workbench session DB
  metadata via ISessionDataService; per-entry try/catch resilience.
- shutdown() routes per-session teardown through the same
  SequencerByKey<string> used by disposeSession() so concurrent
  shutdown/disposeSession cannot double-dispose a wrapper in Phase 6.
- 14 unit tests defined (12 lifecycle + 2 resolved-config), including
  log-once contract and shutdown/disposeSession race guard.
Lands the ClaudeAgent IAgent provider behind the
'chat.agentHost.claudeAgent.enabled' setting (env gate
VSCODE_AGENT_HOST_ENABLE_CLAUDE=1). Pins
@anthropic-ai/claude-agent-sdk@0.2.112 in workspace + remote/.

Implemented in this phase:
* createSession - non-fork, in-memory wrapper only. Honors
  config.session for restore. The fork path and SDK session
  creation are deferred to Phase 6.
* listSessions - SDK is source of truth; per-session DB read
  is a best-effort overlay (failure never excludes an entry).
* disposeSession / shutdown - routed through a per-session
  SequencerByKey to serialize teardown.
* getDescriptor, getProtectedResources, models,
  onDidSessionProgress, setClientCustomizations,
  setClientTools, onClientToolCallComplete,
  setCustomizationEnabled, authenticate, respondTo*Request -
  minimal Phase-5 wiring.

Stubbed for Phase 6 (throw async 'TODO: Phase 6'):
sendMessage, abortSession, changeModel, getSessionMessages,
plus the createSession fork path.

Tests: 29 unit tests in claudeAgent.test.ts cover the
createSession restore-id path, listSessions overlay resilience,
dispose serialization, and stub surfaces.

Note: provisional / onDidMaterializeSession is intentionally
omitted in Phase 5 (see plan section 3.3.1) - the workbench needs
an immediate sessionAdded until the agent has real materialization
work, which arrives in Phase 6 alongside SDK query() startup.
Implements the Phase 6 plan: provisional sessions materialize on first sendMessage, route a single-turn prompt through the Anthropic Claude Agent SDK's WarmQuery, and stream SDKMessages back as protocol AgentSignals via a pure mapSDKMessageToAgentSignals reducer.

Tools remain denied (canUseTool: 'deny'); fork moves to Phase 6.5; Plan Mode UI moves to Phase 7.

Highlights:

- ClaudeAgent.sendMessage routes through _sessionSequencer to collapse concurrent first sends into one materialize + N ordered sends.

- _materializeProvisional has two abort gates (post-startup + post-customizationDirectory write) so disposeSession landing mid-materialize cannot leak a WarmQuery subprocess.

- ClaudeAgentSession owns the prompt iterator + per-turn deferreds; mapSDKMessageToAgentSignals is a pure reducer with state owned by the session.

- IClaudeAgentSdkService gains startup() alongside listSessions().

Tests: 43 unit + 2 proxy-backed integration. Council-review fixes (C1 dispose race, C2 missing integration test, S1 cwd-less ratification) included.
Copilot AI review requested due to automatic review settings May 4, 2026 20:01
# Conflicts:
#	package-lock.json
#	package.json
#	remote/package-lock.json
#	remote/package.json
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements Phase 6 of the agent-host Claude provider by introducing a provisional→materialized session lifecycle and a single-turn sendMessage path that materializes the Claude SDK on first prompt, then streams SDK messages through a pure reducer into protocol AgentSignals (with tools still denied for this phase).

Changes:

  • Adds provisional session materialization on first sendMessage, plus a startup() surface on IClaudeAgentSdkService to pre-warm the SDK subprocess.
  • Introduces ClaudeAgentSession (query owner + message processing loop), and pure helpers for prompt/attachment resolution and SDK-message→signal mapping.
  • Adds a proxy-backed integration test and updates smoke/roadmap/plan docs for Phase 6 validation.
Show a summary per file
File Description
src/vs/platform/agentHost/test/node/claudeAgent.integrationTest.ts New proxy-backed integration tests exercising the end-to-end pipeline with a stubbed CAPI and recording SDK service.
src/vs/platform/agentHost/node/claude/smoke.md Updates smoke procedure and validation checks for Phase 6+ streaming/materialization behavior.
src/vs/platform/agentHost/node/claude/roadmap.md Marks Phase 4 as done and links to the landed PR.
src/vs/platform/agentHost/node/claude/phase6-plan.md Adds detailed Phase 6 implementation plan and acceptance criteria (reference documentation).
src/vs/platform/agentHost/node/claude/phase5-plan.md Adds Phase 5 plan documentation (reference documentation).
src/vs/platform/agentHost/node/claude/phase4-plan.md Updates Phase 4 smoke checklist status/results.
src/vs/platform/agentHost/node/claude/claudePromptResolver.ts New pure helper to convert prompt + attachments into Anthropic content blocks.
src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts New pure mapper from SDK messages to agent-host protocol actions/signals.
src/vs/platform/agentHost/node/claude/claudeAgentSession.ts New session runtime that owns the SDK query, prompt iterable, abort, and message loop.
src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts New lazy SDK wrapper service with listSessions() + Phase 6 startup().
src/vs/platform/agentHost/node/claude/claudeAgent.ts Implements provisional sessions, materialization, real sendMessage, env wiring, and session config schema.
src/vs/platform/agentHost/node/agentHostServerMain.ts Registers IClaudeAgentSdkService for server-mode agent host.
src/vs/platform/agentHost/node/agentHostMain.ts Registers IClaudeAgentSdkService for local/utility-process agent host.
src/vs/platform/agentHost/common/claudeSessionConfigKeys.ts New well-known Claude session config keys/types (permissionMode).
remote/package.json Adds @anthropic-ai/claude-agent-sdk dependency to remote bundle.
package.json Adds @anthropic-ai/claude-agent-sdk dependency.
package-lock.json Lockfile updates for the new dependency and its transitive deps.
eslint.config.js Allows importing @anthropic-ai/claude-agent-sdk in the agentHost area.

Copilot's findings

Files not reviewed (1)
  • remote/package-lock.json: Language not supported
  • Files reviewed: 18/20 changed files
  • Comments generated: 2

Comment thread src/vs/platform/agentHost/node/claude/claudeAgent.ts Outdated
Comment thread src/vs/platform/agentHost/node/claude/claudeAgent.ts
… abort)

Two Copilot-reviewer comments on #314216:

1. listSessions: wrap _sdkService.listSessions() in try/catch. AgentService.listSessions fans out across providers via Promise.all; an SDK dynamic-import failure would otherwise nuke every other provider's session list. Now logs and returns [].

2. dispose: abort _provisionalSessions AbortControllers before super.dispose(). Previously a racing first sendMessage parked inside _writeCustomizationDirectory could pass the materialize abort gates and call _sessions.set on a disposed DisposableMap, orphaning the WarmQuery. Aborting first triggers the existing post-customization-write abort gate, which asyncDisposes the WarmQuery.

Tests: 2 new regressions (listSessions empty on SDK throw; agent.dispose() during racing materialize disposes the WarmQuery). 45/45 unit + 2/2 integration pass.
aeschli
aeschli previously approved these changes May 4, 2026
@TylerLeonhardt TylerLeonhardt enabled auto-merge (squash) May 4, 2026 20:40
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.

4 participants