VS Code 源码级解读:Agent Host 中的 `ClaudeAgent` 骨架如何落地(Phase 4 实现计划)
2026/9/8 21:31:29 网站建设 项目流程

VS Code 源码级解读:Agent Host 中的ClaudeAgent骨架如何落地(Phase 4 实现计划)

【免费下载链接】vscodeVisual Studio Code项目地址: https://gitcode.com/GitHub_Trending/vscode6/vscode

导读

本文基于仓库内src/vs/platform/agentHost/node/claude/phase4-plan.md这一份“交接式实现计划”,完整拆解 Visual Studio Code(VS Code)如何在 agent host(Agent 宿主进程)中落地一个名为ClaudeAgentIAgent提供方骨架:它如何与已有的CopilotAgent并列注册、如何被一处chat.agentHost.claudeAgent.enabled开关默认遮蔽、如何复用 GitHub Copilot 的认证与模型目录,以及其余方法如何以TODO: Phase N的形式为后续阶段预留位置。读完本文,你将掌握该实现的类骨架设计、注册门控链路、模型过滤谓词、stub 方法分阶段映射表、单元测试范式与 live-system smoke 流程,并能用仓库源码逐一印证。

本文所依据的计划文档是一份真实被执行的“Phase 4 契约”,其相邻的 roadmap.md 已标记 Phase 4✅ DONE(PR landed,commit7211c0f3746,live-system smoke 完成于 2026-05-01)。因此文中既有“计划如何设计”,也有“当前仓库实现到何种程度”的对照,读者可顺藤摸瓜阅读 phase5-plan.md、smoke.md 等后续文档。


1. 背景:为什么需要一个ClaudeAgent骨架

在 agent host 的架构里,IAgent是统一抽象:CopilotAgent是第一个也是当时唯一的树内参考实现。计划的目标是在 agent host 中新增第二个 providerClaudeAgent,让它:

  • IAgentService注册自己,与CopilotAgent并列出现在根状态(root state)中;
  • 通过ICopilotApiService.models()拉取并筛选出 Anthropic 家族的模型对外广告;
  • 复用与CopilotAgent完全相同的 GitHub 资源认证https://api.github.com/https://github.com/login/oauth),用户只登录一次 GitHub Copilot 即可选用 Claude 模型,全程不直连 Anthropic。

而 Phase 4 明确不含SDK、子进程或sendMessage——这些属于 Phase 6+。绝大多数IAgent方法在此阶段以throw new Error('TODO: Phase N')占位。

1.1 为什么默认关闭

因为骨架是“绝大多数用户可见方法都会抛TODO”的桩件,如果把注册默认打开,任何选择 Claude 模型的用户都会在第一个提示词上撞到TODO: Phase 5错误。因此计划在原始 brief 之外新增了注册门控:默认关闭、需要显式开启,直到用户可见方法不再抛错(自然的里程碑是 Phase 6sendMessage落地)才打开默认值。这一设计演进值得注意:当前仓库中该设置项的默认值已经翻转为true(见下文 5.2 节),说明代码已越过 Phase 6,进入了默认可见的状态。

1.2 退出标准(exit criteria)

  • 设置chat.agentHost.claudeAgent.enabled: true后,连接 agent host 的 workbench 客户端能在根状态看到claudeprovider,能挑选 Claude 模型;
  • 调用sendMessage()抛出TODO: Phase 6
  • 设置关闭(默认)时,根状态中只有copilotcli

2. 文件改动地图

计划针对以下文件创建/修改,目标实现规模约 200–300 行:

动作文件用途
新建src/vs/platform/agentHost/node/claude/claudeAgent.tsClaudeAgent类本体(当前仓库中该文件已长成 2672 行的完整实现)
新建src/vs/platform/agentHost/test/node/claudeAgent.test.ts单元测试(当前仓库已存在 claudeAgent.test.ts,另有claudeAgent.integrationTest.ts
修改src/vs/platform/agentHost/common/agentService.ts导出设置项 ID 与环境变量常量
修改src/vs/workbench/contrib/chat/browser/chat.contribution.ts注册chat.agentHost.claudeAgent.enabled
修改src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts读取设置、以 env var 转发进 utility process
修改src/vs/platform/agentHost/node/nodeAgentHostStarter.tsNode 子进程兜底路径同样转发
修改src/vs/platform/agentHost/node/agentHostMain.ts依据 env var 条件注册ClaudeAgent
修改src/vs/platform/agentHost/node/agentHostServerMain.ts补注册ICopilotApiServiceIClaudeProxyServiceClaudeAgent(当时三者皆缺)
修改scripts/launch-smoke.sh相关辅助脚本导出 env var,避免 smoke 触碰用户设置

路径演化提示:计划书写于早期,部分落地位置在后来的实现中有调整。例如设置 schema 当前实际注册于 agentHostStarter.config.contribution.ts,而非计划当时指定的chat.contribution.ts;阅读时以源码为准。


3.ClaudeAgent类骨架规范

3.1 参考实现地图

计划明确给出了“照谁抄、抄哪段”:

Phase 4 关注点参考来源
类外壳、Disposable、Emitter、observable树内IAgent参考实现 copilotAgent.ts
getDescriptor/getProtectedResources/authenticate只参考CopilotAgent(扩展侧没有IAgent对应物)
模型过滤extensions/copilot/src/extension/chatSessions/claude/node/claudeCodeModels.ts:165–179

同时计划给出了一条重要纪律:不要整段照抄扩展侧ClaudeCodeSessionclaudeCodeAgent.ts:122+)——那个类已累积约 20 层关注点(MCP gateway、plugins、edit tracker、settings tracker、OTel、hooks、debug logger、ripgrep PATH、runtime data、folder MRU)。每个关注点应等到真正需要它的那个 phase 再进入树内实现。

3.2 类外壳与 provider id

export class ClaudeAgent extends Disposable implements IAgent { readonly id = 'claude' as const; private readonly _onDidSessionProgress = this._register(new Emitter<AgentSignal>()); readonly onDidSessionProgress = this._onDidSessionProgress.event; private readonly _models = observableValue<readonly IAgentModelInfo[]>(this, []); readonly models: IObservable<readonly IAgentModelInfo[]> = this._models; private _githubToken: string | undefined; private _proxyHandle: IClaudeProxyHandle | undefined; constructor( @ILogService private readonly _logService: ILogService, @ICopilotApiService private readonly _copilotApiService: ICopilotApiService, @IClaudeProxyService private readonly _claudeProxyService: IClaudeProxyService, ) { super(); } // ... }

关键决策点:

  • Provider id 是'claude',不是'claude-code'。因为该 id 会经由AgentSession.uri()成为 session URI 的 scheme(claude:/<uuid>),品牌化信息放在displayName里。AgentProvider在 agentService.ts 中实际是一个string类型别名而非枚举(对应实现位于common/agent.tsCLAUDE_AGENT_PROVIDER_ID = 'claude' as const)。
  • 三个构造依赖缺一不可:日志、Copilot CAPI 服务、Claude 代理服务。
  • _modelsobservableValue承载,作为可观测模型列表对外暴露。

3.3getDescriptor()getProtectedResources()

镜像CopilotAgent对应实现,用户可见字符串一律走localize()

getDescriptor(): IAgentDescriptor { return { provider: 'claude', displayName: localize('claudeAgent.displayName', "Claude"), description: localize('claudeAgent.description', "Claude agent backed by the Anthropic Claude Agent SDK"), }; } getProtectedResources(): ProtectedResourceMetadata[] { return [{ resource: 'https://api.github.com', resource_name: 'GitHub Copilot', authorization_servers: ['https://github.com/login/oauth'], scopes_supported: ['read:user', 'user:email'], required: true, }]; }

这保证了 Claude 与 Copilot 共享同一 GitHub 保护资源——认证机制天然复用,客户端无需新增登录流程。

3.4authenticate():本阶段唯一的真实逻辑

计划强调不要在构造函数里启动代理IClaudeProxyService.start()要求非空 GitHub token(claudeProxyService.ts 中有此约束),因此急切构造不可行。代理启动必须推迟到 token 到达的authenticate()

async authenticate(resource: string, token: string): Promise<boolean> { if (resource !== 'https://api.github.com') { return false; } const tokenChanged = this._githubToken !== token; this._githubToken = token; this._logService.info(`[Claude] Auth token ${tokenChanged ? 'updated' : 'unchanged'}`); if (tokenChanged) { // Restart proxy with new token. Old handle's dispose() decrements // refcount; ClaudeProxyService applies most-recent-token-wins. const oldHandle = this._proxyHandle; this._proxyHandle = await this._claudeProxyService.start(token); oldHandle?.dispose(); void this._refreshModels(); } return true; }

顺序是关键:先拿到新 handle、再释放旧 handle,使两次调用之间代理服务不会整体拆除(引用计数始终 ≥ 1)。

3.5_refreshModels():token 变化驱动 + 过期写保护

private async _refreshModels(): Promise<void> { const tokenAtStart = this._githubToken; if (!tokenAtStart) { this._models.set([], undefined); return; } try { const all = await this._copilotApiService.models(tokenAtStart); const filtered = all.filter(m => isClaudeModel(m)).map(m => toAgentModelInfo(m, this.id)); if (this._githubToken === tokenAtStart) { this._models.set(filtered, undefined); } } catch (err) { this._logService.error(err, '[Claude] Failed to refresh models'); if (this._githubToken === tokenAtStart) { this._models.set([], undefined); } } }

关键点:await之后写入前重新比对 token。若慢速的models(tokA)在 token 轮换后才返回,其迟到结果必须被丢弃——这正是文档 §5 测试用例 13“stale-write guard”要固化下来的不变式。

3.6 模型过滤谓词与IAgentModelInfo投影

模块级辅助函数,两个检查合一:CCAModel 的表面过滤 + Claude id 解析器(后者能优雅排除auto这类合成 id):

function isClaudeModel(m: CCAModel): boolean { return ( m.vendor === 'Anthropic' && !!m.supported_endpoints?.includes('/v1/messages') && !!m.model_picker_enabled && !!m.capabilities?.supports?.tool_calls && tryParseClaudeModelId(m.id) !== undefined // from claudeModelId.ts ); } function toAgentModelInfo(m: CCAModel, provider: AgentProvider): IAgentModelInfo { return { provider, id: m.id, name: m.name, maxContextWindow: m.capabilities?.limits?.max_context_window_tokens, supportsVision: !!m.capabilities?.supports?.vision, }; }

对应的字段类型引用自 copilot-api.d.ts:vendor、可选的supported_endpoints(必须用?.includes,因为它是string[] | undefined)、model_picker_enabledcapabilities.supports.tool_callscapabilities.limits.max_context_window_tokens。编写时必须对照IAgentModelInfo真实接口形状(agentService.ts 中的定义)删减不存在的字段。

现状对照:这一谓词已完整落在当前 claudeAgent.ts 中(isClaudeModel位于文件头部附近),且后续还扩展了 reasoning effort 等字段——证明计划的过滤器设计经受住了后续阶段的演进。

3.7 stub 映射表

未实现的所有必需IAgent方法统一throw new Error('TODO: Phase N'),phase 编号必须与 roadmap 一致:

方法Phase N备注
createSession5
disposeSession5
getSessionMessages5完整转录重建属 Phase 13
listSessions5
resolveSessionConfig5
sessionConfigCompletions5
shutdown5
sendMessage6
respondToPermissionRequest7
respondToUserInputRequest7
abortSession9
changeModel9
setClientTools10
onClientToolCallComplete10
setClientCustomizations11
setCustomizationEnabled11

可省略的可选方法(接口允许不实现):truncateSession?setPendingMessages?getCustomizations?getSessionCustomizations?onArchivedChanged?onDidCustomizationsChange?

由于 roadmap 的执行顺序并不按编号线性推进(实际为 1 → 1.5 → 2 → 3 → 4 → 5 → 6 → 9 → 13 → 7 → …),提交每个Phase N前必须交叉核对 roadmap,避免编号漂移。

3.8dispose():必须真实实现,不能是 TODO

AgentService.dispose()会无条件调用provider.dispose()_proxyHandle是引用计数的;不释放就会泄漏代理服务生命周期:

override dispose(): void { // Phase 6+ INVARIANT: SDK subprocess(es) MUST be killed before disposing // the proxy handle. In Phase 4 there are no subprocesses, so this is safe. this._proxyHandle?.dispose(); this._proxyHandle = undefined; this._githubToken = undefined; this._models.set([], undefined); super.dispose(); }

注释是强制的——Phase 6 引入 SDK 子进程后,释放顺序必须满足IClaudeProxyHandle的约定。


4. 注册与门控链路

4.1 契约总览

门控的目的:默认安装的用户永远看不到一个“桩 provider”。其契约是一条完整链路:

[workbench] [agent host process] chat.agentHost.claudeAgent.enabled ---> env var=1 ---> registerProvider(ClaudeAgent)
  • 设置键名与 env var 名都集中在 agentService.ts(计划中的常量为AgentHostClaudeAgentEnabledSettingIdAgentHostEnableClaudeEnvVar),保证两端引用同一个字符串。
  • env var 兼作开发者覆盖开关:在父进程(如 smoke 脚本)里设置它即可绕过 workbench 设置。
  • 变更需要重启 agent host——env var 在进程 spawn 时捕获,运行中翻转设置不会生效,设置描述必须写清楚这一点。

4.2 设置 schema

[AgentHostClaudeAgentEnabledSettingId]: { type: 'boolean', description: nls.localize('chat.agentHost.claudeAgent.enabled', "When enabled, the Claude agent provider is registered inside the agent host. The agent host process must be restarted for changes to this setting to take effect."), default: false, tags: ['experimental', 'advanced'], included: product.quality !== 'stable', },

included: product.quality !== 'stable'意味着 stable 构建中该设置项根本不出现,只有 Insiders 等非稳定构建可见——这正服务于“不让 Insiders 用户以外的任何人在生产里撞到桩件错误”。

现状对照:当前 agentHostStarter.config.contribution.ts 中该设置已注册并带有企业策略绑定(policy nameClaude3PIntegration),且default已变为true——这正是计划中“等用户可见方法不再抛错后翻转默认值”的落实结果。

4.3 两个 starter:设置 → env var

Electron utility-process 路径(electronAgentHostStarter.ts),在start()构造 env 前计算:

const claudeEnabled = this._configurationService.getValue<boolean>(AgentHostClaudeAgentEnabledSettingId) || process.env[AgentHostEnableClaudeEnvVar] === '1'; this.utilityProcess.start({ // ... env: { ...deepClone(process.env), ...shellEnv, VSCODE_ESM_ENTRYPOINT: 'vs/platform/agentHost/node/agentHostMain', VSCODE_PIPE_LOGGING: 'true', VSCODE_VERBOSE_LOGGING: 'true', ...(claudeEnabled ? { [AgentHostEnableClaudeEnvVar]: '1' } : {}), } });

Node 子进程兜底(nodeAgentHostStarter.ts)优先级逻辑相同(设置 OR 继承的 env var),写入env后再构造IIPCOptions。当前仓库中两处 starter 均确实读取该设置并向下游传递。

4.4agentHostMain.ts(DI 已具备前置条件)

ICopilotApiServiceIClaudeProxyService已在此注册,因此在CopilotAgent之后追加一个受 env var 门控的注册:

agentService.registerProvider(instantiationService.createInstance(CopilotAgent)); if (process.env[AgentHostEnableClaudeEnvVar] === '1') { agentService.registerProvider(instantiationService.createInstance(ClaudeAgent)); }

导入注意:树内 import 一律带.js后缀(即便源文件是.ts),例如ClaudeAgent./claude/claudeAgent.js导入。

4.5agentHostServerMain.ts(DI 裸奔,需补三样再门控)

当时该文件既不注册ICopilotApiService也不注册IClaudeProxyService,独立服务器模式下直接注册ClaudeAgent会崩溃。计划要求在if (!options.quiet)块内、createInstance(CopilotAgent)之前补齐:

const copilotApiService = instantiationService.createInstance(CopilotApiService, undefined); diServices.set(ICopilotApiService, copilotApiService); const claudeProxyService = disposables.add(instantiationService.createInstance(ClaudeProxyService)); diServices.set(IClaudeProxyService, claudeProxyService);

随后条件注册 Claude,并新增 CLI 旗标--enable-claude-agent(镜像已有的--enable-mock-agent),与 env var 取或:

// parseServerOptions(): const enableClaudeAgent = argv.includes('--enable-claude-agent') || process.env[AgentHostEnableClaudeEnvVar] === '1'; // !options.quiet 块内,CopilotAgent 注册之后: if (options.enableClaudeAgent) { const claudeAgent = disposables.add(instantiationService.createInstance(ClaudeAgent)); agentService.registerProvider(claudeAgent); log('ClaudeAgent registered'); }

注册顺序敏感IClaudeProxyService的构造函数依赖ICopilotApiService,因此必须先注册ICopilotApiService,否则服务器启动即崩溃。CopilotApiServiceundefined第二参(fetchFn槽位)是有意为之,与agentHostMain.ts行为一致。


5. 测试文件规范

新文件src/vs/platform/agentHost/test/node/claudeAgent.test.ts(当前已存在),镜像 copilotAgent.test.ts 的createTestAgentContext装配风格。Mock 策略是“最小替身”而非繁重模拟

  • IClaudeProxyServicemock:start(token)返回{ baseUrl: 'http://127.0.0.1:0', nonce: 'test-nonce', dispose: () => disposeCount++ },追踪调用次数与最后一次 token。
  • ICopilotApiServicemock:models(token)返回混合了 Anthropic / 非 Anthropic / 无 tool_calls / 未进 picker 的预置CCAModel[]

13 个测试用例(快照断言遵循assert.deepStrictEqual):

  1. getDescriptor()返回预期 descriptor。
  2. getProtectedResources()匹配 GitHub 资源形状。
  3. authenticate之前 models observable 为空。
  4. 对 GitHub 资源authenticate返回true,调用start(token),models 只含 Claude 族条目且形状正确。
  5. 对无关资源authenticate返回false;随后对 GitHub 资源再次authenticate仍可用(代理start总共只调一次)——用于捕获“早期 return false 时已污染状态”的实现错误。
  6. 同一 token 二次authenticate不再次start()
  7. 不同 token:新start(tokenB)被调用,且旧 handle 被 dispose。
  8. 过滤器排除矩阵:非 Anthropic vendor(含合成auto模型)、缺/v1/messages端点、model_picker_enabled: falsetool_calls: false、无法解析的 Claude id。
  9. AgentSession.uri('claude', 'abc')往返一致:schemeclaude、idabc、providerclaude
  10. dispose()释放代理 handle;二次dispose()幂等。
  11. 抽样 3–4 个 stub 方法抛出消息含TODO: Phase及正确编号的Error
  12. 注册 smoke:实例化AgentService+ 注册ClaudeAgent,经公共服务面断言其出现在根状态。
  13. stale-write guard:慢models(tokA)authenticate(tokB)已发布[B]之后才返回时,迟到的[A]结果必须被丢弃。

测试用例 13 是文档着力强调的一个不变式——它把CopilotAgent已依赖但从未测试的“过期写保护”固化为测试。控制解析顺序用 async.ts 的DeferredPromise

const tokAModels = new DeferredPromise<CCAModel[]>(); mockApi.models = (token: string) => token === 'tokA' ? tokAModels.p : Promise.resolve([CLAUDE_MODEL_B]); void agent.authenticate('https://api.github.com', 'tokA'); // refresh-A starts, hangs on tokAModels.p await agent.authenticate('https://api.github.com', 'tokB'); // refresh-B runs to completion, models == [B] tokAModels.complete([CLAUDE_MODEL_A]); // refresh-A unblocks; guard must drop the write await new Promise(r => setImmediate(r)); assert.deepStrictEqual(agent.models.get().map(m => m.id), [CLAUDE_MODEL_B.id]);

测试套件顶部还要调用ensureNoDisposablesAreLeakedInTestSuite()做泄漏检测,且 mock 的IClaudeProxyHandle不应注册进测试 store(用普通对象字面量而非Disposable子类)。


6. 风险与应对(gotchas)

风险缓解
旧 roadmap 说“构造函数里启动代理”——错误,start()需要 token以本计划 +claudeProxyService.ts为准:推迟到authenticate()
agentHostServerMain.tsICopilotApiService/IClaudeProxyServiceDI,服务器模式会崩按 §4.5 补齐两处注册
CCAModel.supported_endpointsstring[] \| undefined,直接.includes()会抛一律用?.includes()
_refreshModelsawait后写回,token 可能已轮换开头快照 token、写回前比对(镜像 CopilotAgent)
不实现真实dispose()会泄漏代理引用计数本阶段就实现真dispose()并注释 Phase 6 顺序不变式
IClaudeProxyService构造函数依赖ICopilotApiServiceagentHostServerMain.ts中先注册前者
异步断言时机敏感(模型拉取是 promise)await new Promise(r => setImmediate(r))排空微任务;需要控制时序时用DeferredPromise,避免计时器等待
忘记门控——默认开启会把TODO: Phase N暴露给每个 Insiders 用户两个 main 文件都必须以 env var 门控;未设置 env var 时 smoke 应只见copilotcli
运行中翻转设置不生效env var 在 spawn 时捕获;重载窗口或杀宿主进程后生效,设置描述写清
smoke 报告“ClaudeAgent 没注册”但实为 env var 丢失launch-smoke.sh导出 env var;Electron starter 通过展开process.env后再合并显式 env var 转发,Node starter 显式写入 spawn env

7. 验收清单(摘要)

计划把 PR “完成”定义为一组按序可勾选的验收项:

代码结构claudeAgent.ts存在且export class ClaudeAgent extends Disposable implements IAgentid = 'claude';构造函数恰好三个依赖;16 个 stub 方法全部TODO: Phase N;不出现可选方法;dispose()真实实现并释放_proxyHandle;Phase 6 子进程所有权不变式写入注释;每个新文件带微软版权头。

注册与门控:常量从 agentService.ts 导出;设置以default: falsetags: ['experimental', 'advanced']、非 stable 才可见注册;描述注明需重启;两个 starter 都转发 env var;两个 main 文件都在env var === '1'时才注册。

编译与静态检查VS Code - Build(或npm run typecheck-client)零 TypeScript 错误;npm run eslint -- <两个新文件>通过;npm run valid-layers-check通过(仅允许导入vs/basevs/platformvs/typings,误引vs/workbench/vs/sessions会失败);npm run hygiene通过。

测试:全部 13 个用例存在;ensureNoDisposablesAreLeakedInTestSuite()在套件顶部;scripts/test.sh --grep ClaudeAgent全绿;无as any/as unknown as Foo;无真实网络/子进程依赖。

行为退出标准:默认关闭时只见copilotcli;开启后claudecopilotcli并列;认证后模型选择器出现 Anthropic 模型;对 Claude session 调sendMessageTODO: Phase 6

失败排查表(节选):IAgentModelInfo编译错 → 加了不存在的字段(如family),重读 agentService.ts 中接口形状;valid-layers-check失败 → 误从高层导入;Test 13 flake → 用DeferredPromise+setImmediate代替计时器;dispose()测试泄漏 → mock handle 用普通对象字面量;agentHostServerMain.ts启动崩溃 → DI 注册顺序;Cannot find module './claude/claudeAgent.js'→ import 缺.js后缀。


8. live-system smoke:真实“能开机、能注册、能出现在 UI”

单元测试证明类在隔离下接线正确,而 live smoke 证明它真的能启动、注册并在已认证用户的 Agents 应用中浮现。完整流程见 smoke.md,核心动作:

  1. Launch:用launch-smoke.sh <port>拉起应用(处理unset ELECTRON_RUN_AS_NODE--user-data-dir、CDP 端口等);脚本内导出 env var,绕开用户设置。
  2. Verify agent host wiring(无需 UI):跑verify-claude-logs.sh [--phase=N],断言五条日志不变式:
    • copilotcliclaude两个 provider 都注册;
    • [Claude] Auth token updated出现(证明agentService.authenticate向拥有该资源的所有 provider 扇出);
    • [ClaudeProxyService] listening on http://127.0.0.1:<port>
    • 根状态 IPC 日志含"provider": "claude"块;
    • ≥ 1 个 Claude 家族模型 id(claude-opus-*claude-sonnet-*等)浮现——同时验证模型过滤谓词与tryParseClaudeModelId
  3. Verify picker UI:playwright 连 CDP,快照中应同时出现option "Copilot CLI"option "Claude"。文档记录了一个陷阱——下拉浮层有context-view-pointerBlock拦截点击,须用ArrowDown+Enter键盘导航而非直接 click。
  4. Drive a prompt:Phase 4 发消息命中createSessionstub,快照出现TODO: Phase 5(Phase 5 则是TODO: Phase 6;Phase 6+ 才是真实模型输出)。

Phase 4 的实际 smoke 已把证据落盘到/tmp/claude-phase4-smoke/<timestamp>/,包括registration.logRegistering agent provider: copilotcli+…: claude两行)、一条捕获的claude:/e32d3567-9da7-41c4-a71a-57daa0a6cf46session URI、46 个 Claude 模型条目、以及首个提示词触发TODO: Phase 5的截图,其中四份必需工件要随 PR 附上。最终状态为:除“disabled-gate 实跑”一项有意推迟(交给 Phase 5 的单元级门控测试覆盖)外,Phase 4 全部必须不变式均验证通过。


9. 已决决策(resolved decisions)

  • dispose()是否等待在途_refreshModels不等待。dispose()super.dispose()之前清空_githubToken,因此任何被挂起的_refreshModels恢复后都会发现 token 不匹配而放弃写入——在途刷新自动变成 no-op,测试用例 13 固化该不变式。fire-and-forget,与CopilotAgent一致。
  • 规范只说了“注册”,为什么还要加设置门控?Phase 4 是桩件,用户可见方法全抛TODO。默认开启意味着任何 Insiders 用户在 picker 里选中 Claude 都会在首个提示词上撞TODO: Phase 5。默认关闭 + 设置 + 开发者 env var 使 Phase 4 可测试而不广泛暴露。设置included: product.quality !== 'stable'使其在稳定构建完全隐藏。只有用户可见方法不再抛错时才翻转默认(或移除门控)——自然里程碑是 Phase 6sendMessage落地。
  • workbench 设置 vs. agent-host 根配置(IAgentConfigurationService)?选 workbench 设置。根配置服务于经 IPC 协议流动的运行时/每会话旋钮;而这是“provider 是否存在”的功能开关,应在进程 spawn 时决定。

10. 从 Phase 4 骨架到完整实现:当前仓库的落点

把计划与当前源码对照,可以直观看到这条演进线已被完整走完:

  • claudeAgent.ts 已从计划的 ~300 行骨架长成 2672 行完整实现,isClaudeModel过滤谓词、toAgentModelInfo投影函数均按计划落地并扩展;文件头还保留了ClaudeSessionMetadataStore、fork anchor、subagent 等 Phase 5–19 引入的能力引用。
  • roadmap.md 显示 Phase 4 之后的 session 生命周期(5)、sendMessage(6)、fork(6.5)、resumeSessionAt截断(6.7)、tool calls + permission(7)、transcript 重建(13)等均已 ✅ DONE,且 phase 编号作为稳定标识被TODO: Phase N抛错与注释持续引用。
  • 设置默认值已翻转、注册门控从“默认遮蔽”演化为“默认可见”,正是计划 §9 中“Phase 6 落地后打开默认”的兑现。

对后续维护者最有价值的,是 Phase 4 确立的两条贯穿性纪律:“每个关注点只在真正需要的 phase 进入实现”(拒绝照抄扩展侧 ~20 层关注点的ClaudeCodeSession),以及“先 acquire 新 handle 再 dispose 旧 handle、await 后写回前比对 token、dispose 必须真实”这三条与生命周期安全相关的编码不变式。它们共同保证了一个多 provider agent host 在并排注册、共享认证与共享代理引用计数时不会互相踩踏。


延伸阅读

  • roadmap.md:阶段总图与执行顺序、各 phase 落点与经验总结
  • phase5-plan.md:session 生命周期(create/dispose/list/shutdown)的实现契约
  • smoke.md:live-system smoke 完整可重复流程与常见失败对照表
  • agentService.ts:provider 注册、设置项 ID、env var 常量与启用解析逻辑
  • copilotAgent.ts:IAgent的树内参考实现,ClaudeAgent各方法的行为镜像来源
  • claudeAgent.test.ts:上述 13 个用例的落地形态

【免费下载链接】vscodeVisual Studio Code项目地址: https://gitcode.com/GitHub_Trending/vscode6/vscode

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询