Skip to content

feat(acp): implement Agent Communication Protocol adapter for IDE integration#368

Merged
sailist merged 1 commit into
MoonshotAI:mainfrom
sailist:feat/acp
Jun 3, 2026
Merged

feat(acp): implement Agent Communication Protocol adapter for IDE integration#368
sailist merged 1 commit into
MoonshotAI:mainfrom
sailist:feat/acp

Conversation

@sailist
Copy link
Copy Markdown
Collaborator

@sailist sailist commented Jun 3, 2026

Summary

This PR implements the Agent Communication Protocol (ACP) adapter, enabling IDEs like Zed and JetBrains to drive a kimi-code session over stdio. It introduces the new @moonshot-ai/acp-adapter package, wires the kimi acp CLI subcommand, and provides full two-way conversion between ACP messages and Kimi Code's internal session/harness/model abstractions — including prompt streaming, tool calls, approvals, plan review, session mode/model switching, image/embedded-resource content, and MCP server forwarding.


1. ACP adapter package and CLI subcommand

Problem: Kimi Code was CLI-only with no standardized machine interface for external editors or IDEs to consume agent capabilities.

What was done:

  • Scaffolded @moonshot-ai/acp-adapter package with version negotiation, AgentSideConnection wrapper, and stdio JSON-RPC server.
  • Registered kimi acp subcommand in the CLI with stdout-safe logging to avoid corrupting the JSON-RPC transport.
  • Implemented graceful SIGINT / SIGTERM shutdown.

2. Session lifecycle and state management

Problem: ACP clients need to create, list, load, and reconfigure sessions remotely.

What was done:

  • Implemented session/new, session/load, session/list, session/set_model, and session/set_mode handlers.
  • Added history replay on session/load so resumed sessions retain prior context.
  • Surfaced session configuration options (model + mode side by side) via configOptions in newSession / loadSession responses.
  • Added setSessionConfigOption handler with config_option_update notification, replacing the previous current_mode_update discriminator.
  • Split thinking out of the model id into a separate SessionConfigBoolean toggle (thinking), dynamically shown based on the selected model's thinkingSupported flag.

3. Prompt and content conversion

Problem: ACP sends rich prompts (text, images, embedded resources) that must be mapped into Kimi Code's internal message format.

What was done:

  • Converted ACP prompt content blocks (text only, images, embedded text resources) into Kimi Code prompts.
  • Added resource_link support for embedded resources.
  • Implemented prompt cancellation hookup so clients can stop an in-flight generation.

4. Tool execution and result streaming

Problem: Agent tool calls and their results need to stream back to the ACP client in real time.

What was done:

  • Mapped ToolCallStarted, ToolCallDelta, and ToolCallProgress events to ACP tool call messages.
  • Converted tool results (text and diff) back into ACP content blocks.
  • Added hideOutput marker for tools that own their own UI so the client doesn't double-render.
  • Bridged thinking blocks from the agent into ACP thinking annotations.

5. Approval and permission flow

Problem: Agent approvals (file edits, shell commands, etc.) must be surfaced in the IDE's native permission UX.

What was done:

  • Bridged ApprovalHandler to session/request_permission so IDE users can approve or deny agent actions.
  • Mapped approval display blocks (diff and text) into ACP permission options.
  • Surfaced plan-review options and plan markdown through requestPermission so users can approve or tweak agent plans before execution.
  • Bridged AskUserQuestion to session/request_permission for interactive multi-choice queries.

6. Plan updates, commands, and MCP bridging

Problem: ACP clients should see what the agent is planning and what commands are available, and should be able to reuse the user's MCP servers.

What was done:

  • Implemented AgentPlanUpdate and AvailableCommandsUpdate event forwarding.
  • Forwarded MCP servers from the ACP client into the Kimi Code harness (HTTP transport only).

7. Node SDK and documentation

Problem: The Node SDK lacked a way to enumerate available models, and there was no user-facing IDE setup guide.

What was done:

  • Exposed KimiHarness.listAvailableModels() in @moonshot-ai/kimi-code-sdk.
  • Added IDE integration guides for Zed and JetBrains in both English and Chinese.
  • Added a full ACP capability matrix reference page (kimi-acp.md).

8. Testing

What was done:

  • Added end-to-end happy-path tests against the ACP TS SDK client.
  • Added end-to-end tests for unsaved-buffer reads.
  • Added unit tests for approval display mapping, plan review mapping, and core adapter logic.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run gen-changesets and included the changeset (acp-initial.md).
  • I have updated user documentation (docs/en/guides/ides.md, docs/zh/guides/ides.md, docs/en/reference/kimi-acp.md, docs/zh/reference/kimi-acp.md).

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 3, 2026

🦋 Changeset detected

Latest commit: 3e97dd0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@moonshot-ai/acp-adapter Minor
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 3, 2026

pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@a0855c3
npx https://pkg.pr.new/@moonshot-ai/kimi-code@a0855c3

commit: a0855c3

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 381446fd3a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +722 to +727
const onSigint = (): void => {
void cleanup('SIGINT');
};
const onSigterm = (): void => {
void cleanup('SIGTERM');
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close the ACP stream after handling signals

When SIGINT/SIGTERM is delivered while stdin is still open (for example Ctrl-C in a terminal), these handlers only start cleanup() and then return. They do not close/destroy the ndjson stream or exit the process, so the main path remains blocked on await runAcpServerWithStream(harness, stream) and the CLI can hang instead of terminating after the harness drain.

Useful? React with 👍 / 👎.

@@ -0,0 +1,48 @@
{
"name": "@moonshot-ai/acp-adapter",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add the ACP package to Nix workspace lists

Adding this new workspace package makes pnpm include @moonshot-ai/acp-adapter, and apps/kimi-code now depends on it, but flake.nix still omits both ./packages/acp-adapter from workspacePaths and @moonshot-ai/acp-adapter from workspaceNames (checked the hardcoded lists). Under the repository's Nix build, the package files are excluded from the source fileset and pnpmConfigHook will not fetch this workspace dependency, so the CLI build cannot resolve the adapter there.

Useful? React with 👍 / 👎.

Comment thread apps/kimi-code/package.json Outdated
"dependencies": {
"@earendil-works/pi-tui": "^0.74.0",
"@mariozechner/clipboard": "^0.3.2",
"@moonshot-ai/acp-adapter": "workspace:^",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not publish a private workspace runtime dependency

Because @moonshot-ai/acp-adapter is marked private: true, adding it under the publishable CLI's runtime dependencies makes the npm package depend on an internal workspace package that will not be available to users installing @moonshot-ai/kimi-code. The CLI already bundles @moonshot-ai/* packages in apps/kimi-code/tsdown.config.ts, so this should not be a published runtime dependency; otherwise npm install -g @moonshot-ai/kimi-code can fail resolving the private adapter even though the code is bundled.

Useful? React with 👍 / 👎.

Comment thread packages/node-sdk/src/rpc.ts Outdated
Comment on lines 152 to 157
// `coreInput` includes the optional `mcpServers` extension (Phase
// 10.1) — the in-process RPC layer round-trips arbitrary JSON
// fields, so the extra payload survives the kernel boundary even
// though agent-core's `CreateSessionPayload` does not yet declare
// it. The kernel-side consumer is a separate concern.
return rpc.createSession(coreInput);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Actually consume forwarded ACP MCP servers

In sessions where an IDE supplies mcpServers, this only forwards the extra field through the SDK RPC call, but agent-core's createSession/resumeSession payloads do not declare or read it and build mcpConfig solely from on-disk/plugin config. The advertised ACP MCP forwarding therefore drops client-provided servers, so tools from those servers never become available in the session.

Useful? React with 👍 / 👎.

Comment thread packages/acp-adapter/src/server.ts Outdated
Comment on lines +116 to +117
if (!(await this.harness.auth.hasUsableToken())) {
throw RequestError.authRequired();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow API-key provider configurations through ACP

When the user configured Kimi Code via /provider or environment/API-key provider settings instead of OAuth login, this unconditional OAuth-token gate rejects session/new before the normal provider manager can use the configured API key. Since the rest of the app supports ProviderConfig.apiKey and /provider, ACP should either check whether the selected provider actually needs OAuth or let session creation/prompting surface auth failures, otherwise valid non-OAuth setups cannot start an ACP session.

Useful? React with 👍 / 👎.

Comment on lines +872 to +876
this.session.prompt(parts).catch((err) => {
if (settled) return;
settled = true;
unsub();
reject(mapPromptError(err, sessionId));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject busy prompts instead of hanging

If an ACP client sends a second session/prompt while the agent already has an active turn, core emits an error event with turn.agent_busy and returns from Session.prompt() successfully; it does not reject and no turn.ended event belongs to this request. This code only rejects on Session.prompt() rejection or resolves on turn.ended, so the JSON-RPC request remains pending forever in that scenario.

Useful? React with 👍 / 👎.

Comment on lines +91 to +92
stat(path: string, options?: { followSymlinks?: boolean }): Promise<StatResult> {
return this.inner.stat(path, options);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route ACP-backed stat checks through the client

When the client advertises fs.readTextFile, the actual Read tool still calls kaos.stat() before readBytes/readText; this implementation delegates stat to the local filesystem, so a file that exists only as an IDE unsaved buffer fails with local ENOENT before the ACP readTextFile request is ever sent. That breaks the advertised unsaved-buffer read path for new files or virtual client files even though the client can provide their contents.

Useful? React with 👍 / 👎.

@sailist sailist force-pushed the feat/acp branch 2 times, most recently from 32944f5 to 9ddcd74 Compare June 3, 2026 07:34
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ddcd74fe4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (!acpKaos) {
return this.runPromptBody(parts, sessionId, conn);
}
return runWithKaos(acpKaos, () => this.runPromptBody(parts, sessionId, conn));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bind the ACP Kaos to the session tools

When an IDE advertises fs.readTextFile, this only wraps runPromptBody in runWithKaos, but the real built-in Read/Write/Edit tools capture agent.kaos when they are initialized (packages/agent-core/src/agent/tool/index.ts:376-380) rather than calling getCurrentKaos(). In real ACP sessions those tools therefore keep using the LocalKaos created at session creation and never send readTextFile/writeTextFile to the client, so unsaved-buffer file I/O works only in the test fakes that explicitly call getCurrentKaos().

Useful? React with 👍 / 👎.

*/
private async maybeBuildAcpKaos(): Promise<AcpKaos | undefined> {
const fs = this.clientCapabilities?.fs;
if (!fs?.readTextFile && !fs?.writeTextFile) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect partial ACP fs capabilities

When a client advertises only one filesystem capability, this condition still swaps in AcpKaos for every file operation; AcpKaos.readText unconditionally calls conn.readTextFile and writes similarly call writeTextFile. For a write-only or read-only client, ordinary tools on the unsupported side will send reverse-RPC requests the client did not advertise instead of falling back to local filesystem behavior, so partial capability negotiation breaks.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13eae126c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

let start = 0;
for (let i = 0; i < text.length; i++) {
if (text.charCodeAt(i) === 0x0a /* \n */) {
yield text.slice(start, i);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve newline terminators from ACP reads

When an ACP client serves an unsaved buffer with CRLF line endings, this generator strips the \n before handing lines to the normal Read tool. ReadTool relies on Kaos.readLines() preserving newline terminators, like LocalKaos, to classify CRLF vs lone carriage returns; with this path it sees \r without \n and renders the file as mixed/lone-CR text with visible carriage returns. Preserve the terminator (or otherwise match LocalKaos.readLines) so ACP-backed reads display the same content as local reads.

Useful? React with 👍 / 👎.

transport: 'stdio',
command: stdio.command,
args: stdio.args,
env: envArrayToRecord(stdio.env),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Default missing stdio MCP env to an empty map

For stdio MCP servers, ACP only requires name, command, and args; env may be omitted. In that common case this calls envArrayToRecord(undefined) and throws before session/new/session/load can create the session, so an otherwise valid IDE stdio MCP configuration without environment variables breaks the whole ACP session setup. Treat a missing stdio.env as [] before converting it.

Useful? React with 👍 / 👎.

});
return;
}
if (event.type === 'turn.ended') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore child-agent turn endings for prompt completion

When a prompt invokes a subagent, the child agent emits its own turn.ended event on the same session with a different agentId. This listener does not filter for the main agent or for the turn started by this prompt, so the ACP session/prompt request can resolve as soon as the subagent finishes while the parent turn is still processing the result and may continue streaming afterward. Check the event's agent/turn identity before settling the prompt response.

Useful? React with 👍 / 👎.

Comment on lines +825 to +826
if (event.display) {
const planNote = planFromDisplayBlock(sessionId, event.turnId, event.display);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit plan updates from the actual TodoList source

This is the only path that emits ACP plan updates, but it depends on ToolCallStartedEvent.display carrying kind: 'todo_list'. The current TodoListTool.resolveExecution() in packages/agent-core/src/tools/builtin/state/todo-list.ts does not set any display, so normal TodoList updates only arrive later as textual tool results and this branch never sends sessionUpdate: 'plan'; IDE clients therefore do not see the advertised plan updates. Wire a structured display from the TodoList tool or derive the plan update from the result path.

Useful? React with 👍 / 👎.

Comment on lines +200 to +202
} catch {
// ENOENT-style failure → treat as empty (mirrors Python open('a')).
existing = '';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not overwrite on failed ACP append reads

When appending through an ACP-backed filesystem, any failure from the initial readTextFile is treated as if the target were empty. If the client returns a transient error, permission error, or other non-ENOENT failure for an existing unsaved buffer, this then sends writeTextFile with only the appended text and can replace the existing file contents. Only fall back to empty for a real not-found condition; otherwise propagate the read failure.

Useful? React with 👍 / 👎.

* needs human authorization to proceed with a tool call.
*/
private async handleApproval(req: ApprovalRequest): Promise<ApprovalResponse> {
const toolCall = buildPermissionToolCallUpdate(this.currentTurnId, req);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the approval request turn id for tool correlation

ApprovalRequest already carries the exact turnId for the tool awaiting permission, but this uses the last turn id observed from the event stream. If the approval arrives before that cache is set, or after another agent in the same session has emitted an event, the permission request is sent with a raw or wrong-prefixed toolCallId and clients cannot attach it to the tool card they already rendered. Build the permission update from req.turnId instead of the mutable session-wide cache.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae4c7e4669

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return;
}

let turnId = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Align replay turn IDs with live turn numbering

When loading a session that already has turns, this replay counter starts at 0 and beginAssistantTurn increments before emitting, so the first historical assistant turn is replayed as turn 1. In agent-core, live turn IDs start at 0 (TurnFlow initializes to -1 and increments on launch/restore), so after one saved turn the next live prompt also uses turn 1; ACP tool IDs like 1:<toolCallId> can then collide with the replayed tool cards and clients may update the old card instead of creating/updating the new one. Start the synthetic counter at -1 or otherwise mirror the persisted/live turn numbering.

Useful? React with 👍 / 👎.

this.harness,
currentModelId,
currentThinkingEnabled,
DEFAULT_MODE_ID,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reflect configured default modes in ACP options

When a user has defaultPlanMode: true or defaultPermissionMode: 'auto'/'yolo', KimiCore.createSession applies those defaults to the main agent, but the ACP response still hard-codes DEFAULT_MODE_ID for the mode config option. In those configurations the IDE shows “Default” while prompts actually run in plan/auto/yolo mode, so users can approve or edit under the wrong assumption until they manually change the picker. Initialize the ACP current mode from the same config defaults (or from the created session state) before building configOptions.

Useful? React with 👍 / 👎.

if (typeof this.harness.getConfig !== 'function') return false;
try {
const config = await this.harness.getConfig();
const declared = (config as { defaultThinking?: unknown }).defaultThinking;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Derive the thinking toggle from actual session defaults

For a normal config with no defaultThinking override, KimiCore.createSession calls resolveThinkingLevel(undefined, config), which defaults to a non-off effort (high) unless thinking.mode is explicitly off; this ACP resolver only reads defaultThinking and otherwise returns false. As a result, thinking-capable models are advertised to the IDE as “Thinking Off” while the session is actually using thinking by default, and configs that set only [thinking] mode/effort are also misrepresented. Use the same resolveThinkingLevel semantics or inspect config.thinking before building the toggle.

Useful? React with 👍 / 👎.

Comment on lines +521 to +522
if (part.type === 'text' && part.text) {
await conn.sessionUpdate({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replay non-text user content on session load

When a saved conversation contains a user image/video part, session/load drops it because replay only emits user_message_chunk updates for text parts. The adapter advertises image prompt support and ACP load is supposed to reconstruct the prior conversation in the client, so reopening an image-based session loses the user-provided media from the IDE transcript even though it remains in the SDK history. Convert persisted image_url/other supported user parts back to ACP content blocks instead of skipping every non-text part.

Useful? React with 👍 / 👎.

Comment thread packages/acp-adapter/src/server.ts Outdated
return {
protocolVersion: this.negotiated.protocolVersion,
agentCapabilities,
authMethods: [],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Advertise a usable auth method before requiring auth

For first-run ACP clients where harnessIsAuthed() is false, session/new and session/load return auth_required, but initialization advertises no authentication methods and authenticate() is implemented as method-not-found. In that state the client has no ACP path to recover (for example to run the existing login flow) and can only fail session creation indefinitely, so expose a real auth method or avoid returning auth_required when none is available.

Useful? React with 👍 / 👎.

@sailist sailist force-pushed the feat/acp branch 2 times, most recently from 9dce8de to 60252f5 Compare June 3, 2026 10:18
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60252f506c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

promptCapabilities: {
image: true,
audio: false,
embeddedContext: false,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Advertise embedded context when accepting resources

When an ACP client follows the negotiated prompt capabilities, this embeddedContext: false tells it not to send resource blocks; the ACP prompt docs require clients to restrict prompt content by initialization capabilities, and this commit's converter/docs explicitly accept resource and resource_link prompt content. In IDE flows that attach file/@-mention context as embedded resources, the client will suppress that context before acpBlocksToPromptParts() can convert it, so the model receives only the user's text. Either advertise embedded context support here or stop claiming/handling resource prompts.

Useful? React with 👍 / 👎.

Comment on lines +236 to +240
session = await this.harness.resumeSession({
id: params.sessionId,
// @ts-expect-error — see block comment above; mcpServers is a
// kernel-only field that the SDK forwards via spread.
mcpServers,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate cwd before loading a session

When session/load is called with a valid sessionId from a different workspace than params.cwd (for example stale IDE state or a manually selected previous session), this resumes the stored session anyway and then replays it to the current client. The underlying SDK Session has its own workDir, so subsequent tools run against the old project while the ACP client believes it loaded the current cwd; reject or validate mismatched cwd before registering/replaying the session.

Useful? React with 👍 / 👎.

// other string (including a stale `true` / `false` boolean
// sent by a pre-Phase-16 client) reads as "off" rather than
// silently flipping based on truthiness.
await acpSession.setThinking(value === 'on');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject unknown thinking config values

For set_config_option calls with configId: 'thinking', any value other than the exact string 'on' is silently treated as off. A typo, stale client value such as 'true', or future thought-level value will therefore disable thinking even though the advertised select options are only 'on' and 'off'; validate the value and return invalid_params unless it is one of the advertised options.

Useful? React with 👍 / 👎.

Comment on lines +137 to +141
if (
block.kind === 'file_io' &&
block.before !== undefined &&
block.after !== undefined
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Show write contents in approval requests

The Write tool's display payload is kind: 'file_io' with content but no before/after, so this branch returns null and buildPermissionToolCallUpdate() falls back to only the generic “Requesting approval...” text. In manual ACP sessions, users approving a file write cannot inspect the content being written, unlike edit diffs; render content as a text block or a new-file diff (oldText: null) before asking for approval.

Useful? React with 👍 / 👎.

Comment on lines +215 to +218
// Best-effort stringify for object/array outputs.
let text: string;
try {
text = JSON.stringify(out);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve media content in tool results

When a tool returns structured content parts, such as ReadMedia returning text plus an image_url/video_url part, this object/array fallback JSON-stringifies the whole payload into one text block. ACP tool-call content can carry regular content blocks, so image reads in an IDE surface as raw JSON/data URLs instead of renderable media (and the client may have to display a huge base64 string); convert known content parts to ACP content blocks before falling back to JSON.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8095d333d3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// otherwise the client would race the load completion against
// its own UI bootstrap. This is the ONE difference vs.
// `resumeSession`, which intentionally omits this step.
await acpSession.replayHistory();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replay history when loading active ACP sessions

When an ACP client calls session/load for a session that is already active in this server process (for example switching back to a session created earlier via session/new), setupSessionFromExisting() gets the active SDK Session from KimiHarness.resumeSession() rather than a freshly resumed one. Sessions created through createSession() do not carry a resume state, so this replayHistory() call logs and emits no transcript while loadSession still returns success, leaving the client with a blank or incomplete conversation. Load should either fetch/replay the persisted records for active sessions or reuse an existing replay-capable state instead of silently skipping it.

Useful? React with 👍 / 👎.

// the absolute path to this very binary (`process.argv[1]`) so the
// client can spawn it with `args:['login']` for the top-level
// `kimi login` subcommand — matches kimi-cli `acp/server.py:77-96`.
const legacyCommand = process.argv[1];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Advertise an executable legacy auth command on Windows

For Windows npm installs, process.argv[1] is the JavaScript entrypoint that Node is running, not the .cmd shim the user launched. When a legacy ACP client follows _meta['terminal-auth'] and spawns this command directly, Windows will try to execute the .mjs file instead of node/the shim, so terminal login fails for those clients even though kimi login works in a shell. Use an actually executable command for this path, or include the Node executable plus script in the advertised legacy command/args.

Useful? React with 👍 / 👎.

Comment on lines +140 to +141
const text = await this.readText(path);
const buf = Buffer.from(text, 'utf8');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve binary reads when ACP fs is enabled

When a client advertises fs.readTextFile, this implementation also backs Kaos.readBytes() with the text-only ACP read. The built-in ReadMediaFile tool uses readBytes() for image/video headers and payloads, so binary media reads will be sent through fs/read_text_file, where clients can reject the request or return normalized text that corrupts the bytes. Delegate byte reads to the inner filesystem or otherwise avoid the text RPC for media/binary reads.

Useful? React with 👍 / 👎.

Comment on lines +292 to +297
await this.session.setModel(baseKey);
if (hasSuffix && typeof this.session.setThinking === 'function') {
await this.session.setThinking(THINKING_ON_LEVEL);
this.currentThinkingEnabledInternal = true;
}
this.currentModelIdInternal = baseKey;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear thinking before switching to non-thinking models

If the current ACP session has Thinking On and the client switches the model selector to an alias that does not support thinking, setModel() preserves the existing SDK thinking level while buildSessionConfigOptions() hides the thinking option for the new model. The user then has no ACP control to turn it off, and subsequent prompts can still send provider-specific reasoning parameters to a model that does not support them. When changing to a non-thinking model, clear the SDK/adaptor thinking state or keep the toggle visible so the client can disable it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0855c34a0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (active !== undefined) return active;

const summary = await this.rpc.resumeSession({ id });
const summary = await this.rpc.resumeSession({ ...input, id });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply MCP servers when resuming active sessions

When an ACP client calls session/load/session/resume with mcpServers for a session already cached in this process, the early activeSessions return above bypasses this new { ...input, id } forwarding, so the caller-provided MCP servers are never connected; I also checked KimiCore.resumeSession, which has the same active-session fast path before its new merge. This breaks IDE reconnect/switch-back flows where the session exists already but the client supplies its current MCP config, leaving those tools unavailable until the session is fully closed and reopened.

Useful? React with 👍 / 👎.

// `null` (the schema-allowed sentinel for "no filter") the same
// as `undefined`.
const cwd = params.cwd ?? undefined;
const summaries = await this.harness.listSessions(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate session listing behind authentication

Unlike session/new, session/load, and session/resume, this handler reads and returns saved session metadata without first checking harnessIsAuthed(). In a first-run or logged-out ACP process, any connected client can call session/list and receive prior session IDs, workspace paths, titles, and timestamps even though it cannot load those sessions; apply the same auth gate before calling harness.listSessions() to avoid leaking local history metadata.

Useful? React with 👍 / 👎.

…g, and IDE integration

This commit scaffolds the @moonshot-ai/acp-adapter package and introduces
the full ACP (Agent Communication Protocol) server implementation for
Kimi Code CLI, including:

- Scaffold @moonshot-ai/acp-adapter workspace package with build skeleton
- `kimi acp` CLI subcommand and stdout-safe logging
- ACP version negotiation and AgentSideConnection wrapper
- Auth gate for session creation
- Session lifecycle: new, list, load with history replay
- Prompt content conversion (text, image, embedded resources, resource links)
- Assistant streaming with thinking support and end-turn handling
- Tool call streaming (started, delta, progress) with result conversion (text / diff)
- Approval handling with diff/text display blocks mapped to ACP options
- Kaos read/write interface (AcpKaos) for unsaved buffer access
- Session mode (yolo/auto) and model management
- Config options builder with thinking toggle
- MCP server forwarding from ACP to harness
- Agent plan updates and available commands updates
- AskUserQuestion bridged to session/request_permission
- Plan review options surfaced through requestPermission
- Error mapping, ext_method stubs, and graceful shutdown
- IDE integration guide (Zed + JetBrains)
- End-to-end tests against ACP TS SDK client
- Changeset, telemetry, and release notes
@sailist sailist merged commit 3eafa79 into MoonshotAI:main Jun 3, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 3, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e97dd05e6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +923 to +928
const onSigint = (): void => {
void cleanup('SIGINT');
};
const onSigterm = (): void => {
void cleanup('SIGTERM');
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close the ACP stream when handling signals

When kimi acp receives SIGINT/SIGTERM while stdin remains open (for example a user pressing Ctrl-C in a terminal or an editor sending SIGTERM without closing the pipe), installing these listeners suppresses Node's default process termination, but the handler only calls harness.close(). The main await runAcpServerWithStream(...) is still waiting for conn.closed, so the finally block that removes listeners never runs and the ACP process can hang instead of exiting after cleanup.

Useful? React with 👍 / 👎.

"zod": "^4.3.6"
},
"devDependencies": {
"@moonshot-ai/acp-adapter": "workspace:^",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ship the ACP SDK with the published CLI

Adding the ACP adapter as a devDependency is enough for the local build, but the non-native CLI bundle only force-bundles @moonshot-ai/* packages (apps/kimi-code/tsdown.config.ts), so the adapter's runtime import of @agentclientprotocol/sdk can remain external. Since @agentclientprotocol/sdk is not listed in this publishable package's dependencies, users installing the npm CLI can hit ERR_MODULE_NOT_FOUND as soon as kimi acp loads; either bundle that package too or declare it as a runtime dependency.

Useful? React with 👍 / 👎.

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