Files
gori-agent/scripts/side-session-spike.mjs
zenord 7aa610294c feat: pending-finish proposals and QQ image input
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.
2026-08-19 13:36:23 +08:00

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 });
}