215 lines
6.1 KiB
JavaScript
215 lines
6.1 KiB
JavaScript
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
import { spawn } from "node:child_process";
|
|
import { normalizePiEvent } from "./normalize.mjs";
|
|
import { renderHeader, renderEventLine } from "./render.mjs";
|
|
|
|
async function appendJsonLine(path, value) {
|
|
await appendBestEffort(path, `${JSON.stringify(value)}\n`);
|
|
}
|
|
|
|
async function appendBestEffort(path, text) {
|
|
try {
|
|
await appendFile(path, text, "utf8");
|
|
} catch {
|
|
// Best-effort artifact logging should never prevent result.json from being written.
|
|
}
|
|
}
|
|
|
|
function makeResult(meta, startedAt, input = {}) {
|
|
const errorText = typeof input.errorMessage === "string" ? input.errorMessage.trim() : "";
|
|
const exitCode = typeof input.exitCode === "number" ? input.exitCode : 1;
|
|
return {
|
|
runId: meta.runId,
|
|
mode: meta.mode,
|
|
taskIndex: meta.taskIndex,
|
|
step: meta.step,
|
|
agent: meta.agent,
|
|
agentSource: meta.agentSource,
|
|
task: meta.task,
|
|
cwd: meta.cwd,
|
|
requestedModel: meta.requestedModel,
|
|
resolvedModel: input.resolvedModel ?? meta.resolvedModel,
|
|
sessionPath: meta.sessionPath,
|
|
startedAt,
|
|
finishedAt: new Date().toISOString(),
|
|
exitCode,
|
|
stopReason: input.stopReason ?? (exitCode === 0 ? undefined : "error"),
|
|
finalText: input.finalText ?? "",
|
|
usage: input.usage,
|
|
stdoutPath: meta.stdoutPath,
|
|
stderrPath: meta.stderrPath,
|
|
resultPath: meta.resultPath,
|
|
eventsPath: meta.eventsPath,
|
|
transcriptPath: meta.transcriptPath,
|
|
errorMessage: errorText || undefined,
|
|
};
|
|
}
|
|
|
|
async function runWrapper(meta, startedAt) {
|
|
const header = renderHeader(meta);
|
|
await appendBestEffort(meta.transcriptPath, `${header}\n`);
|
|
console.log(header);
|
|
|
|
const effectiveModel =
|
|
typeof meta.resolvedModel === "string" && meta.resolvedModel.length > 0
|
|
? meta.resolvedModel
|
|
: meta.requestedModel;
|
|
|
|
const args = ["--mode", "json", "--session", meta.sessionPath];
|
|
if (effectiveModel) args.push("--model", effectiveModel);
|
|
if (Array.isArray(meta.tools) && meta.tools.length > 0) args.push("--tools", meta.tools.join(","));
|
|
if (meta.systemPromptPath) args.push("--append-system-prompt", meta.systemPromptPath);
|
|
args.push(meta.task);
|
|
|
|
let finalText = "";
|
|
let resolvedModel = meta.resolvedModel;
|
|
let stopReason;
|
|
let usage = undefined;
|
|
let stdoutBuffer = "";
|
|
let stderrText = "";
|
|
let spawnError;
|
|
let queue = Promise.resolve();
|
|
|
|
const enqueue = (work) => {
|
|
queue = queue.then(work, work);
|
|
return queue;
|
|
};
|
|
|
|
const handleStdoutLine = async (line) => {
|
|
if (!line.trim()) return;
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(line);
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const normalized = normalizePiEvent(parsed);
|
|
if (!normalized) return;
|
|
|
|
await appendJsonLine(meta.eventsPath, normalized);
|
|
const rendered = renderEventLine(normalized);
|
|
await appendBestEffort(meta.transcriptPath, `${rendered}\n`);
|
|
console.log(rendered);
|
|
|
|
if (normalized.type === "assistant_text") {
|
|
finalText = normalized.text;
|
|
resolvedModel = normalized.model ?? resolvedModel;
|
|
stopReason = normalized.stopReason ?? stopReason;
|
|
usage = normalized.usage ?? usage;
|
|
}
|
|
};
|
|
|
|
const childEnv = { ...process.env };
|
|
// Ensure the copilot initiator flag is not accidentally inherited from the parent
|
|
// environment; set it only for github-copilot models.
|
|
delete childEnv.PI_TMUX_SUBAGENT_GITHUB_COPILOT_INITIATOR;
|
|
// Mark every child run as a nested tmux subagent so it cannot spawn further subagents.
|
|
childEnv.PI_TMUX_SUBAGENT_CHILD = "1";
|
|
|
|
if (typeof effectiveModel === "string" && effectiveModel.startsWith("github-copilot/")) {
|
|
childEnv.PI_TMUX_SUBAGENT_GITHUB_COPILOT_INITIATOR = "agent";
|
|
}
|
|
|
|
const child = spawn("pi", args, {
|
|
cwd: meta.cwd,
|
|
env: childEnv,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
const text = chunk.toString();
|
|
enqueue(async () => {
|
|
stdoutBuffer += text;
|
|
await appendBestEffort(meta.stdoutPath, text);
|
|
|
|
const lines = stdoutBuffer.split("\n");
|
|
stdoutBuffer = lines.pop() ?? "";
|
|
|
|
for (const line of lines) {
|
|
await handleStdoutLine(line);
|
|
}
|
|
});
|
|
});
|
|
|
|
child.stderr.on("data", (chunk) => {
|
|
const text = chunk.toString();
|
|
enqueue(async () => {
|
|
stderrText += text;
|
|
await appendBestEffort(meta.stderrPath, text);
|
|
});
|
|
});
|
|
|
|
const exitCode = await new Promise((resolve) => {
|
|
let done = false;
|
|
const finish = (code) => {
|
|
if (done) return;
|
|
done = true;
|
|
resolve(code);
|
|
};
|
|
|
|
child.on("error", (error) => {
|
|
spawnError = error;
|
|
finish(1);
|
|
});
|
|
|
|
child.on("close", (code) => {
|
|
finish(code ?? (spawnError ? 1 : 0));
|
|
});
|
|
});
|
|
|
|
await queue;
|
|
|
|
if (stdoutBuffer.trim()) {
|
|
await handleStdoutLine(stdoutBuffer);
|
|
stdoutBuffer = "";
|
|
}
|
|
|
|
if (spawnError) {
|
|
const message = spawnError instanceof Error ? spawnError.stack ?? spawnError.message : String(spawnError);
|
|
if (!stderrText.trim()) {
|
|
stderrText = message;
|
|
await appendBestEffort(meta.stderrPath, `${message}\n`);
|
|
}
|
|
}
|
|
|
|
return makeResult(meta, startedAt, {
|
|
exitCode,
|
|
stopReason,
|
|
finalText,
|
|
usage,
|
|
resolvedModel,
|
|
errorMessage: stderrText,
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const metaPath = process.argv[2];
|
|
if (!metaPath) throw new Error("Expected meta.json path as argv[2]");
|
|
|
|
const meta = JSON.parse(await readFile(metaPath, "utf8"));
|
|
const startedAt = meta.startedAt ?? new Date().toISOString();
|
|
|
|
let result;
|
|
try {
|
|
result = await runWrapper(meta, startedAt);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
await appendBestEffort(meta.stderrPath, `${message}\n`);
|
|
result = makeResult(meta, startedAt, {
|
|
exitCode: 1,
|
|
stopReason: "error",
|
|
errorMessage: message,
|
|
});
|
|
}
|
|
|
|
await writeFile(meta.resultPath, JSON.stringify(result, null, 2), "utf8");
|
|
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.stack : String(error));
|
|
process.exitCode = 1;
|
|
});
|