Add ACP-backed role sessions

This commit is contained in:
zenord
2026-08-16 10:08:03 +08:00
parent d59c4a654f
commit a80fe68d57
34 changed files with 1683 additions and 887 deletions
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env node
import fs from "node:fs";
import { Readable, Writable } from "node:stream";
import * as acp from "@agentclientprotocol/sdk";
const pending = new Map();
const logFile = process.env.FAKE_ACP_LOG;
const log = (entry) => { if (logFile) fs.appendFileSync(logFile, `${JSON.stringify(entry)}\n`); };
const app = acp.agent({ name: "fake-acp-agent" })
.onRequest(acp.methods.agent.initialize, ({ params }) => {
log({ method: "initialize" });
return {
protocolVersion: params.protocolVersion,
agentCapabilities: { loadSession: true, sessionCapabilities: { resume: {}, close: {}, list: {} } },
agentInfo: { name: "fake-acp-agent", version: "1" }
};
})
.onRequest(acp.methods.agent.session.new, ({ params }) => {
const sessionId = `fake-${Date.now()}-${Math.random().toString(16).slice(2)}`;
log({ method: "session/new", sessionId, cwd: params.cwd });
return { sessionId };
})
.onRequest(acp.methods.agent.session.load, ({ params }) => { log({ method: "session/load", sessionId: params.sessionId }); return {}; })
.onRequest(acp.methods.agent.session.resume, ({ params }) => { log({ method: "session/resume", sessionId: params.sessionId }); return {}; })
.onRequest(acp.methods.agent.session.close, ({ params }) => { log({ method: "session/close", sessionId: params.sessionId }); return {}; })
.onRequest(acp.methods.agent.session.list, () => ({ sessions: [] }))
.onRequest(acp.methods.agent.session.prompt, async ({ params, client, signal }) => {
const text = params.prompt.filter((item) => item.type === "text").map((item) => item.text).join("");
log({ method: "session/prompt", sessionId: params.sessionId, text });
if (text === "crash") process.exit(9);
if (text === "permission") {
const response = await client.request(acp.methods.client.session.requestPermission, {
sessionId: params.sessionId,
toolCall: { toolCallId: "bash-1", title: "bash git status", kind: "execute", name: "bash", rawInput: "git status" },
options: [
{ optionId: "allow", name: "Allow", kind: "allow_once" },
{ optionId: "reject", name: "Reject", kind: "reject_once" }
]
});
await update(client, params.sessionId, response.outcome.outcome === "selected" ? response.outcome.optionId : "cancelled");
return { stopReason: "end_turn" };
}
if (text === "hang") {
await new Promise((resolve) => {
const done = () => resolve(undefined);
pending.set(params.sessionId, done);
signal.addEventListener("abort", done, { once: true });
});
pending.delete(params.sessionId);
return { stopReason: "cancelled" };
}
await update(client, params.sessionId, text.includes("Initialize this ACP session") ? "READY" : `reply:${params.sessionId}:${text}`);
return { stopReason: "end_turn" };
})
.onNotification(acp.methods.agent.session.cancel, ({ params }) => {
log({ method: "session/cancel", sessionId: params.sessionId });
pending.get(params.sessionId)?.();
});
function update(client, sessionId, text) {
return client.notify(acp.methods.client.session.update, {
sessionId,
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } }
});
}
const stream = acp.ndJsonStream(
Writable.toWeb(process.stdout),
Readable.toWeb(process.stdin)
);
const connection = app.connect(stream);
await connection.closed;