Files
Agent-System/tools/pi-bridge/index.mjs
T

222 lines
5.7 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* pi-coding-agent bridge (stdio JSON).
*
* Input (stdin):
* {
* "workspace": "/abs/path",
* "model": "claude-sonnet-4-5",
* "prompt": "...",
* "targets": ["docs/RD-UN.md"]
* }
*
* Output (stdout, last line JSON): 同 cursor-bridge / opencode-bridge 协议
*
* 设计:与 cursor-bridge / opencode-bridge 完全相同的协议层。
*
* 调用的 pi 命令:
* pi --mode rpc --no-session --provider <p> --model <m>
* --append-system-prompt INSTRUCTIONS.md
* --tools read,write,edit --approve
*
* 工作目录:Go 端 cmd.Dir 设(pi 没有 --cwd flag;node 桥 spawn 时再覆盖一次)
*
* 完成信号:
* - agent_settled → 成功("最稳妥" 标志,自动重试/队列全走完才算)
* - agent_error → 失败
* - turn_end → 单轮结束,不一定等于整体任务完成
*/
import { spawn } from "node:child_process";
import { readFileSync, existsSync } from "node:fs";
import { resolve, relative, isAbsolute } from "node:path";
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
function fail(reason, extra = {}, stderr = "") {
process.stdout.write(
JSON.stringify({
ok: false,
patches: [],
fallback_reason: reason,
stderr: stderr ? stderr.slice(0, 400) : undefined,
...extra,
}) + "\n",
);
process.exit(0);
}
function safeRel(workspace, target) {
const abs = isAbsolute(target) ? target : resolve(workspace, target);
const rel = relative(workspace, abs);
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
return null;
}
return rel.replace(/\\/g, "/");
}
async function main() {
let req;
try {
const raw = await readStdin();
req = JSON.parse(raw);
} catch (e) {
fail("invalid_json: " + (e && e.message ? e.message : String(e)));
return;
}
const workspace = req.workspace;
const model = req.model || process.env.PI_AGENT_MODEL || "claude-sonnet-4-5";
const provider = req.provider || process.env.PI_AGENT_PROVIDER || "anthropic";
const prompt = req.prompt || "";
const targets = Array.isArray(req.targets) ? req.targets : [];
if (!workspace || !existsSync(workspace)) {
fail("workspace_missing");
return;
}
if (!prompt.trim()) {
fail("prompt_empty");
return;
}
const allowed = new Set();
for (const t of targets) {
const rel = safeRel(workspace, t);
if (rel) allowed.add(rel);
}
if (allowed.size === 0) {
fail("no_targets");
return;
}
// 1. spawn pi --mode rpc
const piBin = process.env.PI_AGENT_BIN || "pi";
const args = [
"--mode", "rpc",
"--no-session",
"--provider", provider,
"--model", model,
];
const sysPromptFile = process.env.PI_AGENT_SYSTEM_PROMPT_FILE || "INSTRUCTIONS.md";
const absSysPrompt = resolve(workspace, sysPromptFile);
if (existsSync(absSysPrompt)) {
args.push("--append-system-prompt", sysPromptFile);
}
args.push(
"--tools", "read,write,edit",
"--approve",
);
const env = { ...process.env };
env.PI_AGENT_MODEL = model;
env.PI_AGENT_PROVIDER = provider;
let agentId = "";
let runId = "";
let lastErr = "";
let succeeded = false;
try {
const child = spawn(piBin, args, {
cwd: workspace,
env,
stdio: ["pipe", "pipe", "pipe"],
});
// 2. 写 JSONL prompt 命令到 stdin
const cmdId = "req-" + Date.now();
child.stdin.write(
JSON.stringify({
id: cmdId,
type: "prompt",
message: prompt,
}) + "\n",
);
// 3. 按行解析 ndjson 事件流
let buf = "";
child.stdout.on("data", (chunk) => {
buf += chunk.toString("utf8");
let nl;
while ((nl = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, nl).trim();
buf = buf.slice(nl + 1);
if (!line) continue;
let ev;
try { ev = JSON.parse(line); } catch { continue; }
if (!ev || typeof ev !== "object") continue;
const t = ev.type || "";
if (t === "session" && ev.id) {
agentId = ev.id;
}
if (t === "agent_settled") {
succeeded = true;
// agent_settled 表示任务真正结束;关闭 stdin 让 pi 退出
child.stdin.end();
}
if (t === "agent_error") {
lastErr = ev.error?.message || ev.message || "agent_error";
}
}
});
let stderrBuf = "";
child.stderr.on("data", (chunk) => {
stderrBuf += chunk.toString("utf8");
lastErr = (stderrBuf + " " + lastErr).slice(-400);
});
await new Promise((resolveDone) => {
child.on("close", () => resolveDone());
child.on("error", (err) => {
lastErr = err && err.message ? err.message : String(err);
resolveDone();
});
});
} catch (e) {
fail("startup_or_run: " + (e && e.message ? e.message : String(e)));
return;
}
if (!succeeded) {
fail(lastErr || "agent_not_settled", { agent_id: agentId, run_id: runId }, lastErr);
return;
}
// 4. 读 workspace 内目标文件
const patches = [];
for (const rel of allowed) {
const abs = resolve(workspace, rel);
if (!existsSync(abs)) continue;
const content = readFileSync(abs, "utf8");
patches.push({ rel_path: rel, content });
}
if (patches.length === 0) {
fail("empty_patches", { agent_id: agentId, run_id: runId });
return;
}
process.stdout.write(
JSON.stringify({
ok: true,
agent_id: agentId,
run_id: runId,
patches,
fallback_reason: "",
}) + "\n",
);
}
main().catch((e) => {
fail("unhandled: " + (e && e.message ? e.message : String(e)));
});