initial commit
This commit is contained in:
214
src/wrapper/cli.mjs
Normal file
214
src/wrapper/cli.mjs
Normal file
@@ -0,0 +1,214 @@
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user