mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-26 09:05:46 +00:00
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
134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
AgentSideConnection,
|
|
ClientSideConnection,
|
|
ndJsonStream,
|
|
type Client,
|
|
type NewSessionRequest,
|
|
type ReadTextFileRequest,
|
|
type ReadTextFileResponse,
|
|
type RequestPermissionRequest,
|
|
type RequestPermissionResponse,
|
|
type SessionNotification,
|
|
type WriteTextFileRequest,
|
|
type WriteTextFileResponse,
|
|
} from '@agentclientprotocol/sdk';
|
|
import type { KimiHarness } from '@moonshot-ai/kimi-code-sdk';
|
|
|
|
import { AcpServer } from '../src/server';
|
|
import { AUTHED_STATUS, UNAUTHED_STATUS } from './_helpers/harness-stubs';
|
|
|
|
class StubClient implements Client {
|
|
async requestPermission(_p: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
|
throw new Error('StubClient.requestPermission should not be called in auth-gate test');
|
|
}
|
|
async sessionUpdate(_n: SessionNotification): Promise<void> {
|
|
throw new Error('StubClient.sessionUpdate should not be called in auth-gate test');
|
|
}
|
|
async writeTextFile(_p: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
|
throw new Error('StubClient.writeTextFile should not be called in auth-gate test');
|
|
}
|
|
async readTextFile(_p: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
|
throw new Error('StubClient.readTextFile should not be called in auth-gate test');
|
|
}
|
|
}
|
|
|
|
function makeInMemoryStreamPair(): {
|
|
agentStream: ReturnType<typeof ndJsonStream>;
|
|
clientStream: ReturnType<typeof ndJsonStream>;
|
|
} {
|
|
const clientToAgent = new TransformStream<Uint8Array, Uint8Array>();
|
|
const agentToClient = new TransformStream<Uint8Array, Uint8Array>();
|
|
const agentStream = ndJsonStream(agentToClient.writable, clientToAgent.readable);
|
|
const clientStream = ndJsonStream(clientToAgent.writable, agentToClient.readable);
|
|
return { agentStream, clientStream };
|
|
}
|
|
|
|
function makeHarnessWithToken(hasToken: boolean): KimiHarness {
|
|
return {
|
|
auth: {
|
|
status: async () => (hasToken ? AUTHED_STATUS : UNAUTHED_STATUS),
|
|
},
|
|
} as unknown as KimiHarness;
|
|
}
|
|
|
|
describe('AcpServer auth gate', () => {
|
|
it('rejects session/new with auth_required (-32000) when no token', async () => {
|
|
const harness = makeHarnessWithToken(false);
|
|
const { agentStream, clientStream } = makeInMemoryStreamPair();
|
|
|
|
new AgentSideConnection((c) => new AcpServer(harness, c), agentStream);
|
|
const client = new ClientSideConnection((_a) => new StubClient(), clientStream);
|
|
|
|
const request: NewSessionRequest = {
|
|
cwd: '/tmp/x',
|
|
mcpServers: [],
|
|
};
|
|
|
|
await expect(client.newSession(request)).rejects.toMatchObject({
|
|
code: -32000,
|
|
});
|
|
});
|
|
|
|
it('does not call createSession when the auth gate fails', async () => {
|
|
let createCalled = false;
|
|
const harness = {
|
|
auth: {
|
|
status: async () => UNAUTHED_STATUS,
|
|
},
|
|
createSession: async (_opts: unknown) => {
|
|
createCalled = true;
|
|
return { id: 'should-not-be-reached' };
|
|
},
|
|
} as unknown as KimiHarness;
|
|
|
|
const { agentStream, clientStream } = makeInMemoryStreamPair();
|
|
new AgentSideConnection((c) => new AcpServer(harness, c), agentStream);
|
|
const client = new ClientSideConnection((_a) => new StubClient(), clientStream);
|
|
|
|
await expect(
|
|
client.newSession({ cwd: '/tmp/x', mcpServers: [] }),
|
|
).rejects.toMatchObject({ code: -32000 });
|
|
expect(createCalled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('AcpServer.authenticate', () => {
|
|
it('rejects unknown methodId with invalidParams (-32602)', async () => {
|
|
const harness = makeHarnessWithToken(true);
|
|
const { agentStream, clientStream } = makeInMemoryStreamPair();
|
|
|
|
new AgentSideConnection((c) => new AcpServer(harness, c), agentStream);
|
|
const client = new ClientSideConnection((_a) => new StubClient(), clientStream);
|
|
|
|
await expect(client.authenticate({ methodId: 'unknown' })).rejects.toMatchObject({
|
|
code: -32602,
|
|
});
|
|
});
|
|
|
|
it('returns void on valid token', async () => {
|
|
const harness = makeHarnessWithToken(true);
|
|
const { agentStream, clientStream } = makeInMemoryStreamPair();
|
|
|
|
new AgentSideConnection((c) => new AcpServer(harness, c), agentStream);
|
|
const client = new ClientSideConnection((_a) => new StubClient(), clientStream);
|
|
|
|
const result = await client.authenticate({ methodId: 'login' });
|
|
// ACP allows `AuthenticateResponse | void`; either `null`/`undefined`
|
|
// or an empty body `{}` is considered a successful ack.
|
|
expect(result ?? {}).toEqual({});
|
|
});
|
|
|
|
it('throws authRequired (-32000) when harness has no token', async () => {
|
|
const harness = makeHarnessWithToken(false);
|
|
const { agentStream, clientStream } = makeInMemoryStreamPair();
|
|
|
|
new AgentSideConnection((c) => new AcpServer(harness, c), agentStream);
|
|
const client = new ClientSideConnection((_a) => new StubClient(), clientStream);
|
|
|
|
await expect(client.authenticate({ methodId: 'login' })).rejects.toMatchObject({
|
|
code: -32000,
|
|
});
|
|
});
|
|
});
|