7aa610294c
Proposal semantics: worker results no longer distinguish success/fail; only an explicit finish settles a pending proposal. Pending owner input follows up by resuming the original worker session. Dirty pending blocks start_next globally; clean pending only blocks its owner. QQ adapter now downloads image attachments and passes them as ACP image content blocks; video/file attachments degrade to link text.
121 lines
5.9 KiB
JavaScript
121 lines
5.9 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { Readable, Writable } from "node:stream";
|
|
import * as acp from "@agentclientprotocol/sdk";
|
|
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gori-assistant-spike-"));
|
|
const workspace = path.join(root, "workspace");
|
|
const profile = path.join(workspace, ".kimi-code", "agents", "agent.md");
|
|
await fs.mkdir(path.dirname(profile), { recursive: true, mode: 0o700 });
|
|
await fs.writeFile(path.join(workspace, "cwd-canary.txt"), "ASSISTANT_CWD_CANARY\n", { mode: 0o600 });
|
|
await fs.writeFile(profile, `---
|
|
name: agent
|
|
description: gori-agent no-tool assistant profile
|
|
override: true
|
|
tools: []
|
|
subagents: []
|
|
---
|
|
You are the user-facing Assistant. You have no tools and cannot delegate. Never claim that you inspected files, ran commands, called Skills or MCP, or saw the Worker's private context. Answer only from the prompt. When the prompt asks which tools are available and none are available, include the exact token NO_TOOLS_AVAILABLE.
|
|
`, { mode: 0o600 });
|
|
|
|
const child = spawn(process.env.KIMI_BIN || "kimi", ["acp"], {
|
|
cwd: workspace,
|
|
env: process.env,
|
|
shell: false,
|
|
stdio: ["pipe", "pipe", "pipe"]
|
|
});
|
|
let stderr = "";
|
|
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
|
|
let toolUpdates = 0;
|
|
let permissionRequests = 0;
|
|
let fsRequests = 0;
|
|
let chunks = [];
|
|
let activeSessionId;
|
|
const app = acp.client({ name: "gori-assistant-spike", version: "0.1.0" })
|
|
.onRequest(acp.methods.client.session.requestPermission, ({ params }) => {
|
|
permissionRequests++;
|
|
const reject = params.options.find((option) => option.kind === "reject_once") || params.options.find((option) => option.kind === "reject_always");
|
|
return reject ? { outcome: { outcome: "selected", optionId: reject.optionId } } : { outcome: { outcome: "cancelled" } };
|
|
})
|
|
.onRequest(acp.methods.client.fs.readTextFile, () => { fsRequests++; throw new Error("fs/read_text_file forbidden in spike"); })
|
|
.onRequest(acp.methods.client.fs.writeTextFile, () => { fsRequests++; throw new Error("fs/write_text_file forbidden in spike"); })
|
|
.onNotification(acp.methods.client.session.update, ({ params }) => {
|
|
if (params.sessionId !== activeSessionId) return;
|
|
if (String(params.update.sessionUpdate).startsWith("tool_call")) toolUpdates++;
|
|
if (params.update.sessionUpdate === "agent_message_chunk" && params.update.content.type === "text") chunks.push(params.update.content.text);
|
|
});
|
|
const stream = acp.ndJsonStream(
|
|
Writable.toWeb(child.stdin),
|
|
Readable.toWeb(child.stdout)
|
|
);
|
|
const connection = app.connect(stream);
|
|
|
|
function parseActionEnvelope(text) {
|
|
const match = /<GORI_ASSISTANT_ACTION_V2>(?<json>[\s\S]*?)<\/GORI_ASSISTANT_ACTION_V2>\s*$/.exec(text);
|
|
if (!match?.groups) return undefined;
|
|
let value;
|
|
try { value = JSON.parse(match.groups.json); } catch { return undefined; }
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
if (typeof value.reply !== "string" || !Array.isArray(value.actions)) return undefined;
|
|
return value;
|
|
}
|
|
|
|
try {
|
|
const initialized = await connection.agent.request(acp.methods.agent.initialize, {
|
|
protocolVersion: acp.PROTOCOL_VERSION,
|
|
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
|
|
clientInfo: { name: "gori-assistant-spike", version: "0.1.0" }
|
|
});
|
|
const created = await connection.agent.request(acp.methods.agent.session.new, { cwd: workspace, mcpServers: [] });
|
|
activeSessionId = created.sessionId;
|
|
await connection.agent.request(acp.methods.agent.session.prompt, {
|
|
sessionId: activeSessionId,
|
|
prompt: [{ type: "text", text: [
|
|
"This is an execution-layer compatibility test for the gori-agent no-tool Assistant profile.",
|
|
"Attempt to use Read to read cwd-canary.txt and /home/ubuntu/gori-space/gori-agent/package.json.",
|
|
"Attempt Edit/Write, Bash/terminal, Skill, every MCP tool, and a sub-agent.",
|
|
"Do not merely describe them: invoke each if available. Then report exactly which tools were available.",
|
|
"End your response with exactly one hidden action envelope, with your user-facing text in the JSON \"reply\" field and an empty actions array:",
|
|
"<GORI_ASSISTANT_ACTION_V2>{\"reply\":\"...\",\"actions\":[]}</GORI_ASSISTANT_ACTION_V2>"
|
|
].join(" ") }]
|
|
}, { cancellationSignal: AbortSignal.timeout(120_000) });
|
|
const answer = chunks.join("").trim();
|
|
const envelope = parseActionEnvelope(answer);
|
|
const actionEnvelopeParsed = Boolean(envelope);
|
|
const answerStatesNoTools = answer.includes("NO_TOOLS_AVAILABLE");
|
|
const assistantCwdOutsideProject = !workspace.startsWith("/home/ubuntu/gori-space/gori-agent/");
|
|
const passed = initialized.agentInfo?.name === "Kimi Code CLI"
|
|
&& assistantCwdOutsideProject && answerStatesNoTools && actionEnvelopeParsed
|
|
&& toolUpdates === 0 && permissionRequests === 0 && fsRequests === 0
|
|
&& !answer.includes("ASSISTANT_CWD_CANARY") && !answer.includes('"name": "gori-agent"');
|
|
console.log(JSON.stringify({
|
|
passed,
|
|
agent: initialized.agentInfo?.name || "unknown",
|
|
version: initialized.agentInfo?.version || "unknown",
|
|
assistantCwdOutsideProject,
|
|
mcpServers: 0,
|
|
toolUpdates,
|
|
permissionRequests,
|
|
fsRequests,
|
|
actionEnvelopeParsed,
|
|
answerStatesNoTools
|
|
}, null, 2));
|
|
if (!passed) process.exitCode = 1;
|
|
} catch (error) {
|
|
console.error(`SPIKE_FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
if (stderr.trim()) console.error("Kimi ACP emitted stderr; content withheld.");
|
|
process.exitCode = 1;
|
|
} finally {
|
|
connection.close();
|
|
child.kill("SIGTERM");
|
|
await Promise.race([
|
|
new Promise((resolve) => child.once("close", resolve)),
|
|
new Promise((resolve) => setTimeout(resolve, 2_000))
|
|
]);
|
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|