Skip to content

feat(acp-server): 实现 ACP Agent#104

Merged
N123999 merged 25 commits into
mainfrom
feat/acp-server
Jun 10, 2026
Merged

feat(acp-server): 实现 ACP Agent#104
N123999 merged 25 commits into
mainfrom
feat/acp-server

Conversation

@N123999

@N123999 N123999 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • 新建 packages/acp-server,作为 Spirit Agent 的第三个 Host 入口(与 CLI/Desktop 同级),实现 ACP (Agent Client Protocol) Agent 端
  • Zed、JetBrains Junie 等 ACP 兼容编辑器可直接接入 Spirit Agent 作为 AI 编码引擎
  • 核心架构:ACP ndJSON over stdio → noop JsonRpcPeer + NodeHostToolService 本地工具执行 → AgentRuntime
  • 完整实现 ACP Agent 接口:initializesession/newsession/promptsession/cancelsession/closesession/set_mode
  • 流式输出:使用 startUserTurnStreaming + waitForCompletedTurnResult,思考内容通过 agent_thought_chunk 外露(含增量 delta 追踪修复)
  • 权限桥接:approval-requested → ACP request_permission(支持 allow_once / allow_always / reject_once
  • Slash Commands:发现 workspace/user 级 Skills 并通过 available_commands_update 注册为 ACP 命令,输入 /skill-name 时自动读取 SKILL.md 注入系统提示词
  • agent-core 新增 ./host-bridge 子路径导出(HostToolExecutorProxyLocalHostToolServiceJsonRpcPeer
  • agent-core waitForCompletedTurnResult 从 private 改为 public,供 ACP server 调用
  • 40 个单元测试全部通过

Test plan

  • npm run build:acp-server 编译通过
  • npm test 40 个测试全部通过
  • Zed settings.json 配置 agent_servers 后启动,initialize 返回正确 capabilities
  • session/new 后收到 available_commands_update(含 llm-debug、git-commit 等 skill)
  • 输入框 / 弹出 skill 补全列表
  • 流式输出正常(文字逐字出现,不等待全量)
  • 思考内容外露正常(无重复累积)
  • 工具调用卡片正确显示(kind、title、locations)
  • 权限请求弹出 allow/reject 选项
  • session/cancel 中止当前 turn 并返回 cancelled
  • session/set_mode 切换 agent/plan/ask/debug 模式
  • /skill-name 激活 skill 并注入系统提示词

N123999 added 11 commits June 10, 2026 20:33
- 新建 packages/acp-server 包结构(package.json、tsconfig.json、types.ts)

- 添加 agent-core host-bridge 子路径导出(HostToolExecutorProxy、LocalHostToolService、JsonRpcPeer)

- 在根 package.json 中追加 build:acp-server 构建脚本
- 创建 noop-peer.ts:空操作 JsonRpcPeer 占位符,避免与 ACP ndJSON stdio 冲突

- 创建 config.ts:从环境变量读取配置(SPIRIT_ACP_API_KEY 等)

- 创建 runtime-factory.ts:直接加载 host-internal,组装 AgentRuntime 完整实例
- 创建 session-manager.ts:多会话隔离管理(创建/获取/关闭/模式切换)

- 创建 acp-agent.ts:实现 initialize/newSession/authenticate/setSessionMode/prompt/cancel/closeSession

- 创建 event-mapper.ts:RuntimeEvent → ACP session/update 映射

- 创建 permission-bridge.ts:权限请求桥接(allow_once/allow_always/reject_once)

- 创建 tool-call-mapper.ts:工具名 → ACP ToolCall.kind 映射
- event-mapper:使用 tool-call-mapper 映射工具 kind、提取 title/locations

- tool-call-mapper:添加 buildToolCallTitle 和 extractToolCallLocations

- permission-bridge:增强审批请求(包含 tool call 详情和 prompt 文本)

- permission-bridge:添加 handleQuestionsRequest 降级为权限提示

- acp-agent:集成 handleQuestionsRequest 处理 questions-requested 事件
- 创建 stdio-entry.ts:ndJSON over stdin/stdout 入口

- console.log 重定向到 stderr,防止污染 ndJSON 流

- uncaughtException/unhandledRejection 安全处理
- acp-agent.ts:改用 startUserTurnStreaming + waitForCompletedTurnResult 替代 submitUserTurn,使 onEvent 实时触发流式 chunk

- event-mapper.ts:将 update-pending-assistant-thinking 映射为 agent_thought_chunk(而非 agent_message_chunk)

- agent-core/runtime.ts:将 waitForCompletedTurnResult 从 private 改为 public,供 ACP server 调用
- buildAvailableCommands:将 skill 目录转换为 ACP AvailableCommand

- parseSlashCommand:解析 /skill-name 前缀

- buildActiveSkillPayload:读取 SKILL.md 正文 + 资源文件,构建激活负载

- upsertActiveSkill:按 id 去重插入或替换
- 将 activeSkills 从 [] 字面量改为可变数组引用

- 闭包捕获数组引用,后续 mutation 自动生效于系统提示词

- AcpRuntimeResult 新增 activeSkills 字段
- newSession:创建 session 后发送 available_commands_update

- prompt:检测 /skill-name 前缀,激活 skill 并注入系统提示词

- AcpSessionState 新增 activeSkills 和 enabledSkillCatalog 字段

- SessionManager.createSession 返回 enabledSkillCatalog
available_commands_update 在 newSession 内 fire-and-forget 时会被 SDK 消息队列排在 session/new 响应之前,导致 Zed 收到通知时 session ID 未知而丢弃。改用 setTimeout(0) 延迟到响应发送后。
@N123999

N123999 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread packages/acp-server/src/session-manager.ts
Comment thread packages/acp-server/src/acp-agent.ts Outdated
N123999 added 4 commits June 10, 2026 22:49
setSessionMode 仅调用 setAgentModeToolExposure 更新工具集,但 createToolAgentState

闭包中捕获的 planMetadata 仍为旧值,导致 buildAgentModeSystemMessage 持续输出旧模式。

将 planMetadata 从 const 改为 let 绑定,新增 setAgentMode() 重新加载 instruction

metadata 并赋值,闭包在后续轮次自动读取新值。session-manager 的 setSessionMode

改为 async 并调用此函数。
initialize 声明 promptCapabilities.image: true 但 prompt 处理仅提取文本,

ImageContent 被静默丢弃。新增 prompt-images.ts 从 ACP ContentBlock 中提取

图片:file:// URI 直接传路径,base64 data 写入临时文件后传路径,

通过 explicitImages 参数传入 startUserTurnStreaming。
@N123999 N123999 changed the title feat(acp-server): 实现 ACP Agent 端,支持 Zed 等编辑器接入 feat(acp-server): 实现 ACP Agent Jun 10, 2026
@N123999

N123999 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread packages/acp-server/src/acp-agent.ts Outdated
Comment thread packages/acp-server/src/acp-agent.ts
Comment thread packages/acp-server/src/acp-agent.ts Outdated
Comment thread packages/acp-server/src/prompt-images.ts
N123999 added 4 commits June 10, 2026 23:07
之前仅调用 AbortController.abort(),未调用 runtime.abort()。

新请求在 startUserTurnStreaming 因 isBusy() 抛错,旧 turn 继续运行

并向客户端推送流式更新。现在检测到 isBusy() 时先 abort runtime。
之前使用 kind: 'answered' / kind: 'cancelled' 和 as any 强转,

与 agent-core 的 AskQuestionsResult 联合类型不匹配。

改为 status: 'answered' + answers: [] 和 status: 'skipped'。
new URL(uri).pathname 在 Windows 上产生 /C:/... 等非本机路径,

fs 无法读取。改用 node:url 的 fileURLToPath 正确处理各平台路径。
之前读取不存在的 questions.prompt 字段,权限弹窗总是显示通用文案。

RuntimePendingQuestions 的实际结构是 .questions (AskQuestionsRequest)

内含 .title,现在正确读取并作为权限请求的描述。
@N123999

N123999 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread packages/acp-server/src/acp-agent.ts
Comment thread packages/acp-server/src/prompt-images.ts
Comment thread packages/acp-server/src/event-mapper.ts Outdated
N123999 added 3 commits June 10, 2026 23:18
重复调用 prompt 时旧 waitForCompletedTurnResult 可能抢到新 turn 的结果,

或在 abort 后误判 AbortController 状态向 ACP 抛出未处理错误。

新增 promptGenerations Map,每次 prompt 递增,完成时校验代次匹配,

不匹配则直接返回 cancelled。
extractPromptImages 返回 ExtractedImages(paths + cleanup),

acp-agent 在 startUserTurnStreaming 后的 finally 块中调用 cleanup,

防止反复发送内嵌图片导致临时目录持续累积磁盘占用。
LlmTokenUsage 不含上下文窗口上限,之前 size 和 used 均设为 totalTokens

导致客户端进度条显示已满。改为不发送 usage_update(返回 undefined),

待后续引入模型上下文窗口查询后再启用。同步更新测试用例。
@N123999

N123999 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread packages/acp-server/src/acp-agent.ts Outdated
Comment thread packages/acp-server/src/acp-agent.ts
Comment thread packages/acp-server/src/event-mapper.ts Outdated
N123999 added 3 commits June 10, 2026 23:28
cleanupImages 在 finally 中紧跟 startUserTurnStreaming,但流式轮次

异步运行,LLM 在 waitForCompletedTurnResult 期间仍可能读取图片文件。

将 finally 块改为包裹 start + waitFor 全流程。
该事件语义为替换当前待定助手全文,但 ACP agent_message_chunk

是追加式的。之前映射会导致已发过的流式片段与替换文本重复错乱。

改为返回 undefined 跳过。
…atch

session/cancel 会 abort 并清空 pendingApproval,但已在途的

handleApprovalRequest 仍可能调用 continuePendingApproval,

抛出「当前没有待确认的工具调用」成为未处理 rejection。添加 .catch(() => {})。
@N123999

N123999 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

3 issues from previous reviews remain unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 67179ef. Configure here.

@N123999 N123999 merged commit d9f0a5b into main Jun 10, 2026
1 check passed
N123999 added a commit that referenced this pull request Jul 1, 2026
feat(acp-server): 实现 ACP Agent
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.

1 participant