Skip to content

feat(cli): add ACP stdio server foundation - #3658

Open
Sun-GLiang wants to merge 5 commits into
apache:mainfrom
Sun-GLiang:feat/acp-stdio-foundation
Open

feat(cli): add ACP stdio server foundation#3658
Sun-GLiang wants to merge 5 commits into
apache:mainfrom
Sun-GLiang:feat/acp-stdio-foundation

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add the maka --acp entry point and serve ACP v1 over stdio with @agentclientprotocol/sdk@1.4.0.
  • Keep initialize probeable without a Runtime Host and defer Host connection until a follow-up session method actually needs it; stdout remains reserved for ACP NDJSON.
  • Implement only the PR1 foundation and exact initialize identity; unsupported session requests remain standard -32601 responses. Session, prompt, streaming, cancellation, and permission support remain follow-up work.
  • Add reusable unit and child-process coverage using the official SDK client; the harness can opt into a real in-process Runtime Host for follow-up PRs.

Refs #3132

Verification

  • npm --workspace maka-agent test — 434 passed, 0 failed
  • npm run lint
  • npm run format:check
  • npm run check:asf-headers
  • npm run check:cli-third-party-notices
  • npm ls --workspace maka-agent --omit=dev --all
  • npx --yes npm@11.19.0 run release:cli:pack
  • npx --yes npm@11.19.0 run release:cli:smoke
  • Verified the packed tarball contains @agentclientprotocol/sdk/dist/acp.js and its package metadata.

Repository-wide npm run typecheck and npm test currently stop in unchanged packages/ui / apps/desktop code because the checked-in UI call sites reference conversationKey, settledText, unlockAutoFollow, and trailingAction fields absent from their installed component types. This branch does not modify packages/ui or apps/desktop; the CLI typecheck and affected suite pass above.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex assisted with implementation, tests, conflict resolution, and review. Each affected commit includes a Generated-by: Codex trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

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 jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Independent review of exact head 9f455618bc7a059ec667e2df62dd13c413ada158.

三问

  1. 要解决什么:给 CLI 一块 ACP v1 stdio 地基,后面才能加 session/prompt。
  2. 怎么解决:maka --acp → 官方 @agentclientprotocol/sdk@1.4.0ndJsonStream + 只实现 initialize 身份;其余请求走 SDK 的 -32601。Runtime Host 先连上、ACP 结束再关一次。
  3. 奥卡姆:生产面很小(agent ~30 行、stdio ~60 行)。一千多行主要是测试/harness/notices。这块地基立得对:协议形状交给官方 SDK,没有自造 JSON-RPC;ACP 侧还没有会话/turn 状态,所以目前不是两份真值。

四个重点

  1. 协议边界:消息形状、-32601、NDJSON 分帧都来自 SDK / JSON-RPC / ACP stdio 规范,不是手写编解码。initialize 固定回 protocolVersion: 1——只宣称 v1,客户端要 2 时回 1 让对方断开,这是规范允许的作者选择。agentInfo 是 SHOULD。缺的是 capabilities 对象(见行内 [P3])。
  2. stdiondJsonStream(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 closedfinally context.close(),Host 关一次。
  3. 跟 runtime-host:ACP agent 只拿 version,不读 Host、不存 session。Host 只做 connect/close。现在不是两份会话真值。把 Host 绑在 initialize-only 的进程上是提前耦合(连不上 Host 则 ACP 也起不来),理由写了(生命周期权威、占住 stdout)。后面加 session/new 必须走这份 Host,不能在 ACP 里再开一份 Map。
  4. 信任:stdio 对端是拉起 maka --acp 的父进程(编辑器),按本机用户跑,没有额外认证。这轮没有工具执行,权限模型可以还没有。一旦 prompt/工具落地,ACP 客户端 == 本机用户,必须接上现有 Host 权限,不能另开一条默认放行。

CI

期望:test(永远)、audit(lockfile)、packagepackage-lock.json 在 Windows release path 上)。
这个 head:三条都是 fork action_requiredcheck-runs total_count=0。空不是绿。
origin/main 3bb645e99test 是 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 },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P3] ACP v1 要求 Agent 回应所选版本以及它支持的 capabilities。这里只回 protocolVersion + agentInfo,测试还把这个形状钉死了。schema 上 capabilities 多半有默认值,客户端可能当全 false,功能上跟「session 未实现」一致。

地基阶段建议显式给出空/全 false 的 agentCapabilities(以及空 authMethods),别靠省略。这是规范要求,不是自造字段。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P3] await connection.closed 之后无条件 return 0。配套测试把 not json\n 也期望成 0,于是协议坏行和干净 EOF 在进程退出码上无法区分。Host 倒是会在 finally 里关一次,这条清理是对的。

SDK 把坏行收成 closed 而不是 reject 的话,这里仍可以看 closed 原因或 stderr 再决定非 0。不挡地基,但编辑器会把垃圾输入当成 agent 正常下班。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

The review feedback has been addressed in 4e15393ec, and all CI checks have passed.

This PR is ready for another review when you have time. Thank you!

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 的 testpackageaudit 均为终态绿。

传输层这部分做得很细,我本来预期会出问题的地方恰恰是对的。stdio 协议服务器只要有别的东西往 stdout 写一个字节就会死,所以我把这条路径上所有可能的写入方都追了一遍:本地 Host spawn 用的是 stdio: ['ignore', 'ignore', 'pipe']launcher.ts:134);SSH 路径因为没有传 interactiveSsh 而落到 batch,得到 ['ignore', 'ignore', 'ignore']cli-core.ts 只有 helpversion 会写 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 属于正确的降级,所以今天没有坏——但一个"宣称"而非"协商"出来的版本,等到真有第二个版本时往往就一直那样了。现在读一下请求版本、返回双方的最小值,只需要一行。

这些都不否定方向——作为基础层,分层是干净的,而传输恰恰是那个必须做对的部分。

@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

Addressed the review in 94abb612f.

  • P2: maka --acp no longer resolves or connects a Runtime Host before serving stdio. The server now requires only the CLI version, and the real child-process test initializes and exits without a Host. The reusable harness keeps an explicit startRuntimeHost option for the session PRs that will actually need one.
  • P3: I did not use a numeric minimum. ACP says to return the requested version when supported, otherwise the latest supported version. PR1 supports only {1}, so requests for 0, 1, or 2 must all select 1; Math.min(0, 1) would incorrectly advertise unsupported v0. The agent tests now cover both unsupported lower and higher requests.

Local verification on this head: 434 CLI tests passed, static checks passed, and the packed CLI passed the offline release smoke test.

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.

3 participants