mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
feat(acp): add kimi harness support surfaces
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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-<conversationId>
|
||||
- `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.
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -22,6 +22,16 @@ const ACP_AGENT_RESUME_HINT_BY_KEY = new Map<string, SessionResumeHintResolver>(
|
||||
({ 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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user