diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index 6188b04c0a1..c0e3b4be687 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -249,6 +249,7 @@ Current acpx built-in harness aliases: - `codex` - `opencode` - `gemini` +- `kimi` When OpenClaw uses the acpx backend, prefer these values for `agentId` unless your acpx config defines custom agent aliases. @@ -266,7 +267,7 @@ Core ACP baseline: dispatch: { enabled: true }, backend: "acpx", defaultAgent: "codex", - allowedAgents: ["pi", "claude", "codex", "opencode", "gemini"], + allowedAgents: ["pi", "claude", "codex", "opencode", "gemini", "kimi"], maxConcurrentSessions: 8, stream: { coalesceIdleMs: 300, diff --git a/extensions/acpx/skills/acp-router/SKILL.md b/extensions/acpx/skills/acp-router/SKILL.md index a299c9e0229..1b7944820b1 100644 --- a/extensions/acpx/skills/acp-router/SKILL.md +++ b/extensions/acpx/skills/acp-router/SKILL.md @@ -6,7 +6,7 @@ user-invocable: false # ACP Harness Router -When user intent is "run this in Pi/Claude Code/Codex/OpenCode/Gemini (ACP harness)", do not use subagent runtime or PTY scraping. Route through ACP-aware flows. +When user intent is "run this in Pi/Claude Code/Codex/OpenCode/Gemini/Kimi (ACP harness)", do not use subagent runtime or PTY scraping. Route through ACP-aware flows. ## Intent detection @@ -39,7 +39,7 @@ Do not use: - `subagents` runtime for harness control - `/acp` command delegation as a requirement for the user -- PTY scraping of pi/claude/codex/opencode/gemini CLIs when `acpx` is available +- PTY scraping of pi/claude/codex/opencode/gemini/kimi CLIs when `acpx` is available ## AgentId mapping @@ -50,6 +50,7 @@ Use these defaults when user names a harness directly: - "codex" -> `agentId: "codex"` - "opencode" -> `agentId: "opencode"` - "gemini" or "gemini cli" -> `agentId: "gemini"` +- "kimi" or "kimi cli" -> `agentId: "kimi"` These defaults match current acpx built-in aliases. @@ -87,7 +88,7 @@ Call: ## Thread spawn recovery policy -When the user asks to start a coding harness in a thread (for example "start a codex/claude/pi thread"), treat that as an ACP runtime request and try to satisfy it end-to-end. +When the user asks to start a coding harness in a thread (for example "start a codex/claude/pi/kimi thread"), treat that as an ACP runtime request and try to satisfy it end-to-end. Required behavior when ACP backend is unavailable: @@ -183,6 +184,7 @@ ${ACPX_CMD} codex sessions close oc-codex- - `codex` - `opencode` - `gemini` +- `kimi` ### Built-in adapter commands in acpx @@ -193,6 +195,7 @@ Defaults are: - `codex -> npx @zed-industries/codex-acp` - `opencode -> npx -y opencode-ai acp` - `gemini -> gemini` +- `kimi -> kimi acp` If `~/.acpx/config.json` overrides `agents`, those overrides replace defaults. diff --git a/src/acp/policy.test.ts b/src/acp/policy.test.ts index b88334b1376..38da8d992c8 100644 --- a/src/acp/policy.test.ts +++ b/src/acp/policy.test.ts @@ -47,11 +47,12 @@ describe("acp policy", () => { it("applies allowlist filtering for ACP agents", () => { const cfg = { acp: { - allowedAgents: ["Codex", "claude-code"], + allowedAgents: ["Codex", "claude-code", "kimi"], }, } satisfies OpenClawConfig; expect(isAcpAgentAllowedByPolicy(cfg, "codex")).toBe(true); expect(isAcpAgentAllowedByPolicy(cfg, "claude-code")).toBe(true); + expect(isAcpAgentAllowedByPolicy(cfg, "KIMI")).toBe(true); expect(isAcpAgentAllowedByPolicy(cfg, "gemini")).toBe(false); expect(resolveAcpAgentPolicyError(cfg, "gemini")?.code).toBe("ACP_SESSION_INIT_FAILED"); expect(resolveAcpAgentPolicyError(cfg, "codex")).toBeNull(); diff --git a/src/acp/runtime/session-identifiers.test.ts b/src/acp/runtime/session-identifiers.test.ts index fe7b0d6c2bc..eefeb139fc6 100644 --- a/src/acp/runtime/session-identifiers.test.ts +++ b/src/acp/runtime/session-identifiers.test.ts @@ -56,6 +56,33 @@ describe("session identifier helpers", () => { ); }); + it("adds a Kimi resume hint when agent identity is resolved", () => { + const lines = resolveAcpThreadSessionDetailLines({ + sessionKey: "agent:kimi:acp:resolved-1", + meta: { + backend: "acpx", + agent: "kimi", + runtimeSessionName: "runtime-1", + identity: { + state: "resolved", + source: "status", + lastUpdatedAt: Date.now(), + acpxSessionId: "acpx-kimi-123", + agentSessionId: "kimi-inner-123", + }, + mode: "persistent", + state: "idle", + lastActivityAt: Date.now(), + }, + }); + + expect(lines).toContain("agent session id: kimi-inner-123"); + expect(lines).toContain("acpx session id: acpx-kimi-123"); + expect(lines).toContain( + "resume in Kimi CLI: `kimi resume kimi-inner-123` (continues this conversation).", + ); + }); + it("shows pending identity text for status rendering", () => { const lines = resolveAcpSessionIdentifierLinesFromIdentity({ backend: "acpx", diff --git a/src/acp/runtime/session-identifiers.ts b/src/acp/runtime/session-identifiers.ts index d342d8b02eb..6b0c4da2553 100644 --- a/src/acp/runtime/session-identifiers.ts +++ b/src/acp/runtime/session-identifiers.ts @@ -22,6 +22,16 @@ const ACP_AGENT_RESUME_HINT_BY_KEY = new Map( ({ agentSessionId }) => `resume in Codex CLI: \`codex resume ${agentSessionId}\` (continues this conversation).`, ], + [ + "kimi", + ({ agentSessionId }) => + `resume in Kimi CLI: \`kimi resume ${agentSessionId}\` (continues this conversation).`, + ], + [ + "moonshot-kimi", + ({ agentSessionId }) => + `resume in Kimi CLI: \`kimi resume ${agentSessionId}\` (continues this conversation).`, + ], ]); function normalizeText(value: unknown): string | undefined { diff --git a/src/commands/agent.acp.test.ts b/src/commands/agent.acp.test.ts index e6c024f0a29..cde0ab54a94 100644 --- a/src/commands/agent.acp.test.ts +++ b/src/commands/agent.acp.test.ts @@ -31,7 +31,7 @@ function createAcpEnabledConfig(home: string, storePath: string): OpenClawConfig acp: { enabled: true, backend: "acpx", - allowedAgents: ["codex"], + allowedAgents: ["codex", "kimi"], dispatch: { enabled: true }, }, agents: { @@ -62,19 +62,20 @@ function mockConfigWithAcpOverrides( loadConfigSpy.mockReturnValue(cfg); } -function writeAcpSessionStore(storePath: string) { +function writeAcpSessionStore(storePath: string, agent = "codex") { + const sessionKey = `agent:${agent}:acp:test`; fs.mkdirSync(path.dirname(storePath), { recursive: true }); fs.writeFileSync( storePath, JSON.stringify( { - "agent:codex:acp:test": { + [sessionKey]: { sessionId: "acp-session-1", updatedAt: Date.now(), acp: { backend: "acpx", - agent: "codex", - runtimeSessionName: "agent:codex:acp:test", + agent, + runtimeSessionName: sessionKey, mode: "oneshot", state: "idle", lastActivityAt: Date.now(), @@ -278,4 +279,30 @@ describe("agentCommand ACP runtime routing", () => { expect(runEmbeddedPiAgentSpy).not.toHaveBeenCalled(); }); }); + + it("allows ACP turns for kimi when policy allowlists kimi", async () => { + await withTempHome(async (home) => { + const storePath = path.join(home, "sessions.json"); + writeAcpSessionStore(storePath, "kimi"); + mockConfigWithAcpOverrides(home, storePath, { + allowedAgents: ["kimi"], + }); + + const runTurn = vi.fn(async (_params: unknown) => {}); + mockAcpManager({ + runTurn: (params: unknown) => runTurn(params), + resolveSession: ({ sessionKey }) => resolveReadySession(sessionKey, "kimi"), + }); + + await agentCommand({ message: "ping", sessionKey: "agent:kimi:acp:test" }, runtime); + + expect(runTurn).toHaveBeenCalledWith( + expect.objectContaining({ + sessionKey: "agent:kimi:acp:test", + text: "ping", + }), + ); + expect(runEmbeddedPiAgentSpy).not.toHaveBeenCalled(); + }); + }); });