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 宿主进程)中落地一个名为ClaudeAgent的IAgent提供方骨架:它如何与已有的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.ts | ClaudeAgent类本体(当前仓库中该文件已长成 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.ts | Node 子进程兜底路径同样转发 |
| 修改 | src/vs/platform/agentHost/node/agentHostMain.ts | 依据 env var 条件注册ClaudeAgent |
| 修改 | src/vs/platform/agentHost/node/agentHostServerMain.ts | 补注册ICopilotApiService、IClaudeProxyService、ClaudeAgent(当时三者皆缺) |
| 修改 | 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 |
同时计划给出了一条重要纪律:不要整段照抄扩展侧ClaudeCodeSession(claudeCodeAgent.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.ts,CLAUDE_AGENT_PROVIDER_ID = 'claude' as const)。 - 三个构造依赖缺一不可:日志、Copilot CAPI 服务、Claude 代理服务。
_models用observableValue承载,作为可观测模型列表对外暴露。
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_enabled、capabilities.supports.tool_calls、capabilities.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 | 备注 |
|---|---|---|
createSession | 5 | |
disposeSession | 5 | |
getSessionMessages | 5 | 完整转录重建属 Phase 13 |
listSessions | 5 | |
resolveSessionConfig | 5 | |
sessionConfigCompletions | 5 | |
shutdown | 5 | |
sendMessage | 6 | |
respondToPermissionRequest | 7 | |
respondToUserInputRequest | 7 | |
abortSession | 9 | |
changeModel | 9 | |
setClientTools | 10 | |
onClientToolCallComplete | 10 | |
setClientCustomizations | 11 | |
setCustomizationEnabled | 11 |
可省略的可选方法(接口允许不实现):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(计划中的常量为
AgentHostClaudeAgentEnabledSettingId与AgentHostEnableClaudeEnvVar),保证两端引用同一个字符串。 - 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 已具备前置条件)
ICopilotApiService与IClaudeProxyService已在此注册,因此在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,否则服务器启动即崩溃。CopilotApiService的undefined第二参(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):
getDescriptor()返回预期 descriptor。getProtectedResources()匹配 GitHub 资源形状。authenticate之前 models observable 为空。- 对 GitHub 资源
authenticate返回true,调用start(token),models 只含 Claude 族条目且形状正确。 - 对无关资源
authenticate返回false;随后对 GitHub 资源再次authenticate仍可用(代理start总共只调一次)——用于捕获“早期 return false 时已污染状态”的实现错误。 - 同一 token 二次
authenticate不再次start()。 - 不同 token:新
start(tokenB)被调用,且旧 handle 被 dispose。 - 过滤器排除矩阵:非 Anthropic vendor(含合成
auto模型)、缺/v1/messages端点、model_picker_enabled: false、tool_calls: false、无法解析的 Claude id。 AgentSession.uri('claude', 'abc')往返一致:schemeclaude、idabc、providerclaude。dispose()释放代理 handle;二次dispose()幂等。- 抽样 3–4 个 stub 方法抛出消息含
TODO: Phase及正确编号的Error。 - 注册 smoke:实例化
AgentService+ 注册ClaudeAgent,经公共服务面断言其出现在根状态。 - 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.ts缺ICopilotApiService/IClaudeProxyServiceDI,服务器模式会崩 | 按 §4.5 补齐两处注册 |
CCAModel.supported_endpoints是string[] \| undefined,直接.includes()会抛 | 一律用?.includes() |
_refreshModels在await后写回,token 可能已轮换 | 开头快照 token、写回前比对(镜像 CopilotAgent) |
不实现真实dispose()会泄漏代理引用计数 | 本阶段就实现真dispose()并注释 Phase 6 顺序不变式 |
IClaudeProxyService构造函数依赖ICopilotApiService | agentHostServerMain.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 IAgent;id = 'claude';构造函数恰好三个依赖;16 个 stub 方法全部TODO: Phase N;不出现可选方法;dispose()真实实现并释放_proxyHandle;Phase 6 子进程所有权不变式写入注释;每个新文件带微软版权头。
注册与门控:常量从 agentService.ts 导出;设置以default: false、tags: ['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/base、vs/platform、vs/typings,误引vs/workbench/vs/sessions会失败);npm run hygiene通过。
测试:全部 13 个用例存在;ensureNoDisposablesAreLeakedInTestSuite()在套件顶部;scripts/test.sh --grep ClaudeAgent全绿;无as any/as unknown as Foo;无真实网络/子进程依赖。
行为退出标准:默认关闭时只见copilotcli;开启后claude与copilotcli并列;认证后模型选择器出现 Anthropic 模型;对 Claude session 调sendMessage抛TODO: 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,核心动作:
- Launch:用
launch-smoke.sh <port>拉起应用(处理unset ELECTRON_RUN_AS_NODE、--user-data-dir、CDP 端口等);脚本内导出 env var,绕开用户设置。 - Verify agent host wiring(无需 UI):跑
verify-claude-logs.sh [--phase=N],断言五条日志不变式:copilotcli与claude两个 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。
- Verify picker UI:playwright 连 CDP,快照中应同时出现
option "Copilot CLI"与option "Claude"。文档记录了一个陷阱——下拉浮层有context-view-pointerBlock拦截点击,须用ArrowDown+Enter键盘导航而非直接 click。 - 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.log(Registering 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),仅供参考