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.
This commit is contained in:
zenord
2026-08-19 13:36:23 +08:00
parent 33854721bf
commit 7aa610294c
24 changed files with 1285 additions and 391 deletions
+35
View File
@@ -0,0 +1,35 @@
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-image-cap-spike-"));
const workspace = path.join(root, "workspace");
await fs.mkdir(workspace, { recursive: true, mode: 0o700 });
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"); });
const app = acp.client({ name: "gori-image-cap-spike", version: "0.1.0" });
const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
const connection = app.connect(stream);
try {
const initialized = await connection.agent.request(acp.methods.agent.initialize, {
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
clientInfo: { name: "gori-image-cap-spike", version: "0.1.0" }
});
console.log(JSON.stringify(initialized, null, 2));
} finally {
child.kill("SIGKILL");
await fs.rm(root, { recursive: true, force: true });
}
+59
View File
@@ -0,0 +1,59 @@
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-image-e2e-spike-"));
const workspace = path.join(root, "workspace");
await fs.mkdir(workspace, { recursive: true, mode: 0o700 });
const imageB64 = (await fs.readFile("/tmp/test7.b64", "utf8")).trim();
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 sessionId;
const chunks = [];
const app = acp.client({ name: "gori-image-e2e-spike", version: "0.1.0" })
.onNotification(acp.methods.client.session.update, ({ params }) => {
if (params.sessionId !== sessionId) return;
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);
try {
await connection.agent.request(acp.methods.agent.initialize, {
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
clientInfo: { name: "gori-image-e2e-spike", version: "0.1.0" }
});
const created = await connection.agent.request(acp.methods.agent.session.new, { cwd: workspace, mcpServers: [] });
sessionId = created.sessionId;
const result = await connection.agent.request(acp.methods.agent.session.prompt, {
sessionId: created.sessionId,
prompt: [
{ type: "image", data: imageB64, mimeType: "image/png" },
{ type: "text", text: "What number is drawn in this image? Answer with just the number." }
]
});
console.log("stopReason:", result.stopReason);
console.log("reply:", JSON.stringify(chunks.join("")));
} catch (error) {
console.error("PROMPT ERROR:", error?.message || error);
if (stderr) console.error("STDERR tail:", stderr.slice(-800));
process.exitCode = 1;
} finally {
child.kill("SIGKILL");
await fs.rm(root, { recursive: true, force: true });
}
+2 -2
View File
@@ -54,7 +54,7 @@ const stream = acp.ndJsonStream(
const connection = app.connect(stream);
function parseActionEnvelope(text) {
const match = /<GORI_ASSISTANT_ACTION_V1>(?<json>[\s\S]*?)<\/GORI_ASSISTANT_ACTION_V1>\s*$/.exec(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; }
@@ -79,7 +79,7 @@ try {
"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_V1>{\"reply\":\"...\",\"actions\":[]}</GORI_ASSISTANT_ACTION_V1>"
"<GORI_ASSISTANT_ACTION_V2>{\"reply\":\"...\",\"actions\":[]}</GORI_ASSISTANT_ACTION_V2>"
].join(" ") }]
}, { cancellationSignal: AbortSignal.timeout(120_000) });
const answer = chunks.join("").trim();