Skip to content

feat(acp): add agent-core-v2 ACP server#1586

Open
sailist wants to merge 5 commits into
MoonshotAI:mainfrom
sailist:v2/acp
Open

feat(acp): add agent-core-v2 ACP server#1586
sailist wants to merge 5 commits into
MoonshotAI:mainfrom
sailist:v2/acp

Conversation

@sailist

@sailist sailist commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problem is explained below.

Problem

The existing kimi acp command serves ACP clients through the legacy @moonshot-ai/acp-adapter on top of the SDK harness and the legacy agent-core. The new DI × Scope agent engine (agent-core-v2) had no ACP entry point, so ACP-compatible clients (editors, IDEs) could not drive a session on the new engine.

What changed

This PR adds @moonshot-ai/acp-server, an ACP host backed directly by agent-core-v2, plus an experimental kimi acp-v2 CLI command to launch it over stdio.

1. ACP server on agent-core-v2 (packages/acp-server)

Problem: No ACP host existed for the v2 engine.

What was done:

  • Session lifecycle management (session.ts, server.ts, lifecycle) mapping ACP session/prompt/close semantics onto v2 sessions, including replay support for session resumption.
  • Event bridging (events-map.ts, interaction-bridge.ts) translating v2 engine events into ACP session updates (streaming, tool calls, questions).
  • Permission / approval flow (approval.ts) and question handling (question.ts) surfaced to ACP clients.
  • Config options, session modes, model catalog, slash commands, and auth methods (config-options.ts, modes.ts, model-catalog.ts, slash.ts, auth-methods.ts, builtin-commands.ts).
  • ACP filesystem service (acp-fs/) so clients can read/write through the session workspace.

2. kimi acp-v2 CLI command

Problem: The v2 ACP server needed an entry point that mirrors kimi acp's host-independent wire-up without leaking the experimental engine into the default CLI module graph.

What was done:

  • Added kimi acp-v2 (apps/kimi-code/src/cli/sub/acp-v2.ts) starting the v2 ACP server over stdio.
  • --login pivots into the shared device-code login flow via the first-class AuthMethodTerminal path; KIMI_CODE_HOME is forwarded into authMethods[0].env so the login subprocess writes its token under the same data root.
  • @moonshot-ai/acp-server is loaded via lazy dynamic import, mirroring the kimi server run v2 routing.

3. Tests and workspace wiring

Problem: The new package needed integration coverage and build configuration.

What was done:

  • Added integration tests covering initialize, lifecycle, approval, questions, replay, config, skills, close, and an end-to-end scripted-provider turn (packages/acp-server/test/), plus CLI tests for acp-v2 (apps/kimi-code/test/cli/acp-v2.test.ts).
  • Registered the package in the workspace build config (flake.nix, pnpm-lock.yaml).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

- add ACP session lifecycle, configuration, permissions, and event bridging
- expose the experimental kimi acp-v2 command with terminal authentication
- add integration coverage and workspace build configuration
@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e574605

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@e574605
npx https://pkg.pr.new/@moonshot-ai/kimi-code@e574605

commit: e574605

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2dfa11e5da

ℹ️ 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 +63 to +68
async readText(path: string, _options?: ReadTextOptions): Promise<string> {
// ACP `fs.readTextFile` returns already-decoded UTF-8 text, so the
// `encoding`/`errors` decode options are a no-op here.
const { content } = await this.connection
.get()
.readTextFile({ sessionId: this.ctx.sessionId, path });

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 Gate reverse FS calls on client capabilities

When an ACP client initializes without clientCapabilities.fs.readTextFile / writeTextFile (or explicitly sets them false), this session-scoped filesystem still replaces local text IO and unconditionally sends fs/read_text_file and fs/write_text_file reverse-RPCs. That makes normal Read/Edit/Write operations fail with method-not-found for clients that do not advertise the FS extension; use the stored client capabilities to install this backend only when supported, or fall back to the inner local filesystem per operation.

Useful? React with 👍 / 👎.

Comment thread packages/acp-server/src/server.ts Outdated
Comment on lines +140 to +141
return {
protocolVersion: params.protocolVersion,

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 Negotiate the ACP protocol version

If a client sends a newer or otherwise unsupported protocolVersion (for example 99), this echoes that value back even though this server only implements the current ACP 0.23/negotiation version 1 surface. The client can then assume semantics this server does not support; mirror the existing ACP adapter behavior by returning the highest supported version instead of the raw request value.

Useful? React with 👍 / 👎.

Comment thread packages/acp-server/src/server.ts Outdated
Comment on lines +129 to +132
mcpCapabilities: {
http: true,
sse: true,
},

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 advertise MCP before forwarding servers

Advertising mcpCapabilities tells ACP clients they can pass configured MCP servers in session/new, session/load, or session/resume, but this server never reads params.mcpServers and only creates/resumes the v2 session with its cwd/id. In clients that rely on ACP-provided MCP config, the UI will think MCP is supported while none of those tools are connected; either remove these capabilities for now or convert and forward the requested servers into the v2 session creation/resume path.

Useful? React with 👍 / 👎.

Comment on lines +170 to +171
const acpSession = await this.wireSession(handle, params.sessionId);
this.sessions.set(params.sessionId, acpSession);

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 Dispose the old ACP session before replacing it

When session/load is called for a session already present in this.sessions (and session/resume has the same pattern below), resumeHandle returns the live scope and wireSession creates a new AcpInteractionBridge, but the previous wrapper is overwritten without dispose(). Subsequent approval/question interactions are observed by both bridges, causing duplicate permission prompts and leaving the stale subscription alive until the underlying session closes.

Useful? React with 👍 / 👎.

sailist added 2 commits July 13, 2026 13:07
- replace placeholder hostnames (evil.com, foo.com, internal.corp,
  real.corp) with example.test / example.com in agent-core-v2 and
  kap-server tests
- replace fixture emails (x@y.com, a@x.com) with example addresses in
  minidb tests and README
…review

- add missing appendText to AcpHostFileSystem (IHostFileSystem drift)
- replace IAgentPromptService.prompt with inject
- use Turn.cancel() instead of abortController
- gate FS reverse-RPCs on client capabilities, fallback to local FS
- return PROTOCOL_VERSION constant instead of echoing client version
- remove misleading mcpCapabilities from initialize response
- dispose old session wrapper before replacing on load/resume
- fix object stringification lint error in convert.ts
- add acp-v2 to expected CLI sub-command list in test
@sailist

sailist commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6bcb4e00a8

ℹ️ 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 thread packages/acp-server/src/session.ts Outdated
Comment on lines +226 to +228
return this.driveTurn(() =>
this.mainAgent.accessor.get(IAgentPromptService).inject(message),
);

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 Submit ACP prompts through the prompt queue

For a normal session/prompt, using IAgentPromptService.inject() takes the steering path instead of the public prompt submission path used by the other v2 surfaces. In configurations with UserPromptSubmit/external prompt hooks, those hooks are registered on onBeforeSubmitPrompt and run from enqueue(), so ACP v2 prompts can bypass prompt-blocking policy and can also steer into an active turn instead of launching/queuing a discrete user turn.

Useful? React with 👍 / 👎.

Comment on lines +122 to +124
registerScopedService(
LifecycleScope.Session,
IHostFileSystem,

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 Edit through the ACP filesystem

This Session-scoped IHostFileSystem override does not cover the Edit tool: EditTool delegates the actual read/write to the App-scoped IFileEditService, whose IHostFileSystem dependency is resolved at App scope and therefore remains the local HostFileSystem. With an ACP client that advertises fs.readTextFile/fs.writeTextFile, Edit will still read and mutate local disk instead of the editor/remote buffer, so unsaved client contents are ignored and edits may land in the wrong place.

Useful? React with 👍 / 👎.

Comment on lines +84 to +86
appendText(path: string, data: string): Promise<void> {
return this.inner.appendText(path, data);
}

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 Keep append writes on the ACP filesystem

When the client supports ACP writes, overwrite mode goes through fs.writeTextFile, but append mode from the v2 Write tool calls appendText() and this always delegates to the local filesystem. In ACP sessions backed by editor buffers or remote workspaces, Write with mode: 'append' will not update the client-visible file and may instead mutate or fail against local disk; append needs to be emulated through ACP read/write or rejected when it cannot be represented.

Useful? React with 👍 / 👎.

Comment thread packages/acp-server/src/session.ts Outdated
name: skill.name,
description: skill.description,
}));
return [...ACP_BUILTIN_SLASH_COMMANDS, ...skills];

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 Implement or stop advertising builtin slash commands

This advertises /help, /status, /compact, and the other ACP builtins to clients, but prompt() only handles skill slash commands and explicitly lets builtin commands fall through as a normal model prompt. Any client command-palette selection for those advertised commands will be sent to the LLM instead of producing the local command response, so either handle the builtin intents before launching a turn or omit them from available_commands_update until they are implemented.

Useful? React with 👍 / 👎.

…mented builtins

- replace IAgentPromptService.inject with enqueue so onBeforeSubmitPrompt
  hooks (prompt-blocking policy) are not bypassed
- stop advertising builtin slash commands (/help, /status, etc.) until
  builtin command execution is implemented
- add comment explaining appendText stays local (ACP has no append RPC)
- update skills test to match new availableCommands behavior
@sailist

sailist commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5f40ae790

ℹ️ 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 thread packages/acp-server/src/session.ts Outdated
Comment on lines +312 to +315
case 'turn.ended':
settle(() =>
resolve({ stopReason: turnEndReasonToStopReason(event.reason, event.error) }),
);

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 Ignore unrelated turn events for queued prompts

When a second session/prompt arrives while another turn is already running, IAgentPromptService.enqueue() returns a handle whose launched promise waits in the queue, but this subscription is already active and settles on the currently-running turn's turn.ended. That makes the queued prompt return early (and duplicates the active turn's updates), then its actual turn later runs with no ACP subscriber. Track the turnId returned by launch() and ignore events from any other turn until the queued prompt has actually launched.

Useful? React with 👍 / 👎.

Comment on lines +65 to +67
case 'failed':
if (error?.code === 'provider.filtered') return 'refusal';
return 'end_turn';

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 Surface auth failures through the JSON-RPC error

For provider/OAuth failures after a session is created (for example a revoked token causing agent-core-v2 to emit turn.ended with reason: 'failed' and error.code such as provider.auth_error or auth.login_required), this path resolves the prompt with stopReason: 'end_turn'. ACP clients only trigger their re-auth flow when the request rejects with auth_required, so these expired-token prompts look like successful empty turns and the user is never prompted to log in again.

Useful? React with 👍 / 👎.

…_required

- track turnId in driveTurn and ignore events from unrelated turns,
  preventing queued prompts from settling on the running turn
- reject prompt requests with auth_required when turn fails with an
  auth-related error code, enabling ACP client re-auth flow
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