feat(cli): add ACP stdio server foundation - #3658
Conversation
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
|
Hi maintainers, the PR1 implementation for the ACP stdio foundation is complete and ready for review. When you have time, please review #3658. Thank you! |
jackwener
left a comment
There was a problem hiding this comment.
Independent review of exact head 9f455618bc7a059ec667e2df62dd13c413ada158.
三问
- 要解决什么:给 CLI 一块 ACP v1 stdio 地基,后面才能加 session/prompt。
- 怎么解决:
maka --acp→ 官方@agentclientprotocol/sdk@1.4.0的ndJsonStream+ 只实现initialize身份;其余请求走 SDK 的-32601。Runtime Host 先连上、ACP 结束再关一次。 - 奥卡姆:生产面很小(agent ~30 行、stdio ~60 行)。一千多行主要是测试/harness/notices。这块地基立得对:协议形状交给官方 SDK,没有自造 JSON-RPC;ACP 侧还没有会话/turn 状态,所以目前不是两份真值。
四个重点
- 协议边界:消息形状、
-32601、NDJSON 分帧都来自 SDK / JSON-RPC / ACP stdio 规范,不是手写编解码。initialize固定回protocolVersion: 1——只宣称 v1,客户端要 2 时回 1 让对方断开,这是规范允许的作者选择。agentInfo是 SHOULD。缺的是 capabilities 对象(见行内 [P3])。 - stdio:
ndJsonStream(stdout, stdin)参数顺序与 SDK 一致;半包/粘包由 SDK 按\n缓冲。Host 子进程stdio: ['ignore','ignore','pipe'],不会把 Host 日志灌进 ACP stdout。子进程测试:stdin EOF 后 exit 0,stdout 每行都是合法 JSON-RPC。未测:对端不读时的写背压、超大行(SDK 缓冲无上限)。协议坏行与干净 EOF 都是 exit 0(见行内 [P3])。退出路径await closed再finally context.close(),Host 关一次。 - 跟 runtime-host:ACP agent 只拿
version,不读 Host、不存 session。Host 只做 connect/close。现在不是两份会话真值。把 Host 绑在 initialize-only 的进程上是提前耦合(连不上 Host 则 ACP 也起不来),理由写了(生命周期权威、占住 stdout)。后面加session/new必须走这份 Host,不能在 ACP 里再开一份 Map。 - 信任:stdio 对端是拉起
maka --acp的父进程(编辑器),按本机用户跑,没有额外认证。这轮没有工具执行,权限模型可以还没有。一旦 prompt/工具落地,ACP 客户端 == 本机用户,必须接上现有 Host 权限,不能另开一条默认放行。
CI
期望:test(永远)、audit(lockfile)、package(package-lock.json 在 Windows release path 上)。
这个 head:三条都是 fork action_required,check-runs total_count=0。空不是绿。
origin/main 3bb645e99 的 test 是 completed/success,不是 main 红误伤。
mergeable=MERGEABLE,不是 conflict。未 approve,未合。
代码 GO(仅 [P3])。门禁 NO-GO,等 workflow 放行且 test completed+success。
| export function createMakaAcpAgent(options: MakaAcpAgentOptions): AgentApp { | ||
| return agent({ name: 'maka' }).onRequest(methods.agent.initialize, () => ({ | ||
| protocolVersion: 1, | ||
| agentInfo: { name: 'maka', title: 'Maka', version: options.version }, |
There was a problem hiding this comment.
[P3] ACP v1 要求 Agent 回应所选版本以及它支持的 capabilities。这里只回 protocolVersion + agentInfo,测试还把这个形状钉死了。schema 上 capabilities 多半有默认值,客户端可能当全 false,功能上跟「session 未实现」一致。
地基阶段建议显式给出空/全 false 的 agentCapabilities(以及空 authMethods),别靠省略。这是规范要求,不是自造字段。
There was a problem hiding this comment.
Fixed in 4e15393ec. initialize now explicitly returns agentCapabilities: {} and authMethods: [], while advertising none of the session, prompt, MCP, or authentication features that remain out of scope. Both the in-process and real child-process tests assert the exact wire contract.
| ); | ||
| const connection = createMakaAcpAgent({ version: input.version }).connect(stream); | ||
| await connection.closed; | ||
| return 0; |
There was a problem hiding this comment.
[P3] await connection.closed 之后无条件 return 0。配套测试把 not json\n 也期望成 0,于是协议坏行和干净 EOF 在进程退出码上无法区分。Host 倒是会在 finally 里关一次,这条清理是对的。
SDK 把坏行收成 closed 而不是 reject 的话,这里仍可以看 closed 原因或 stderr 再决定非 0。不挡地基,但编辑器会把垃圾输入当成 agent 正常下班。
There was a problem hiding this comment.
Verified against SDK 1.4.0: ndJsonStream handles malformed JSON by writing the standard -32700 Parse error response and continuing to read; it does not close the connection with a parse-error reason. The subsequent EOF is therefore a normal close, and converting it to a non-zero exit would remove JSON-RPC recovery semantics. In 4e15393ec, the test now asserts the -32700 response explicitly. A real stdin transport error is separately propagated through the CLI fatal path and covered as a rejection, while the Runtime Host still closes exactly once.
Generated-by: Codex
|
The review feedback has been addressed in This PR is ready for another review when you have time. Thank you! |
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed at 4e15393ecbc670d1585da2f49d6ddff8fafcb0ca, with test, package and audit terminal green on that head.
The transport work is careful, and the part I expected to be wrong is right. A stdio protocol server dies the moment anything else writes to stdout, so I traced every writer on this path: the local Host spawn uses stdio: ['ignore', 'ignore', 'pipe'] (launcher.ts:134), the SSH path resolves to batch here because interactiveSsh is unset, so it gets ['ignore', 'ignore', 'ignore'], and cli-core.ts only writes to stdout for help and version, neither of which can co-occur with --acp. Errors go to stderr. Nothing corrupts the ndjson frame. The tests also assert the Host context closes exactly once on EOF, on a parse error, and on a transport error, which is the property most easily lost when a finally is added later.
[P2] The Runtime Host connection gates the handshake, and nothing uses it yet
stdio-server.ts awaits connectRuntimeHostCli(...) before the stream exists, and the context it returns is never passed to createMakaAcpAgent, which takes only { version }. Right now the agent implements exactly one method — initialize, returning static capabilities.
So a client that connects only to negotiate — the first thing any ACP client does — causes a Runtime Host to be connected or spawned, held for the life of the connection, and then closed. And when the Host cannot be reached or spawned, --acp exits non-zero before writing a single protocol frame. The client does not see an ACP error it can reason about; it sees a transport that died during startup. That is a poor failure mode for a handshake that needs nothing from the Host.
Please defer the connection until a method actually needs it, or at minimum let initialize answer first and surface a Host failure as a protocol-level error. As a foundation this is the cheaper shape too: it keeps --acp probeable in environments where no Host can start.
[P3] initialize ignores the client's requested protocol version
The handler is () => ({ protocolVersion: 1, ... }) — the request parameters are not read. Returning 1 to a client that asked for something higher is the correct downgrade, so nothing is broken today, but a version that is asserted rather than negotiated tends to stay that way once a second version exists. Reading the requested version and returning the agreed minimum now costs one line.
Nothing here blocks the direction — as a foundation the layering is clean, and the transport is the part that had to be right.
简体中文
已在 4e15393ecbc670d1585da2f49d6ddff8fafcb0ca 上审查,该 head 的 test、package、audit 均为终态绿。
传输层这部分做得很细,我本来预期会出问题的地方恰恰是对的。stdio 协议服务器只要有别的东西往 stdout 写一个字节就会死,所以我把这条路径上所有可能的写入方都追了一遍:本地 Host spawn 用的是 stdio: ['ignore', 'ignore', 'pipe'](launcher.ts:134);SSH 路径因为没有传 interactiveSsh 而落到 batch,得到 ['ignore', 'ignore', 'ignore'];cli-core.ts 只有 help 和 version 会写 stdout,而它们不可能与 --acp 同时发生;错误一律走 stderr。没有任何东西会污染 ndjson 帧。测试还断言了 Host context 在 EOF、解析错误、传输错误三种情况下都恰好关闭一次——这正是后来有人加 finally 时最容易丢掉的性质。
[P2] Runtime Host 连接卡在握手之前,而目前根本没人用它
stdio-server.ts 在流建立之前就 await 了 connectRuntimeHostCli(...),而它返回的 context 从未传给 createMakaAcpAgent——后者只接受 { version }。目前 agent 只实现了一个方法:initialize,返回静态能力集。
于是,一个仅仅为了协商而连上来的客户端(任何 ACP 客户端做的第一件事)会导致一个 Runtime Host 被连接或拉起、在整个连接期间被持有、然后关闭。而当 Host 连不上或拉不起来时,--acp 会在写出任何一个协议帧之前以非零码退出。客户端看到的不是一个它能处理的 ACP 错误,而是一个启动期就死掉的传输。对一个完全不需要 Host 的握手来说,这个失败形态很差。
建议把连接推迟到真正需要它的方法,或者至少让 initialize 先应答、把 Host 失败作为协议级错误暴露出去。作为"基础"来说这个形状也更省:在起不了 Host 的环境里,--acp 仍然可被探测。
[P3] initialize 忽略了客户端请求的协议版本
处理器是 () => ({ protocolVersion: 1, ... }),请求参数没有被读取。对一个请求了更高版本的客户端返回 1 属于正确的降级,所以今天没有坏——但一个"宣称"而非"协商"出来的版本,等到真有第二个版本时往往就一直那样了。现在读一下请求版本、返回双方的最小值,只需要一行。
这些都不否定方向——作为基础层,分层是干净的,而传输恰恰是那个必须做对的部分。
Generated-by: Codex
|
Addressed the review in
Local verification on this head: 434 CLI tests passed, static checks passed, and the packed CLI passed the offline release smoke test. |
Summary
maka --acpentry point and serve ACP v1 over stdio with@agentclientprotocol/sdk@1.4.0.initializeprobeable without a Runtime Host and defer Host connection until a follow-up session method actually needs it; stdout remains reserved for ACP NDJSON.initializeidentity; unsupported session requests remain standard-32601responses. Session, prompt, streaming, cancellation, and permission support remain follow-up work.Refs #3132
Verification
npm --workspace maka-agent test— 434 passed, 0 failednpm run lintnpm run format:checknpm run check:asf-headersnpm run check:cli-third-party-noticesnpm ls --workspace maka-agent --omit=dev --allnpx --yes npm@11.19.0 run release:cli:packnpx --yes npm@11.19.0 run release:cli:smoke@agentclientprotocol/sdk/dist/acp.jsand its package metadata.Repository-wide
npm run typecheckandnpm testcurrently stop in unchangedpackages/ui/apps/desktopcode because the checked-in UI call sites referenceconversationKey,settledText,unlockAutoFollow, andtrailingActionfields absent from their installed component types. This branch does not modifypackages/uiorapps/desktop; the CLI typecheck and affected suite pass above.AI use
Select exactly one:
Tool(s) and scope: Codex assisted with implementation, tests, conflict resolution, and review. Each affected commit includes a
Generated-by: Codextrailer.Checklist
Does this PR entail a change in behavior?