mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
The shipped subagent example could say a child was done while it was still working, lose the useful part of process failures, and return a normal-looking tool result when the delegated work had actually failed. Single and chain output could also exceed the normal tool limit. This also adds a dedicated regression suite covering progress state, cancellation, process failures, output limits, cwd-based agent selection, and persisted tool-result error state. After this change: - queued, running, completed, failed, and aborted work stay distinct throughout the run; - interrupting the parent run preserves partial child results, stops queued work from starting, and escalates when a child ignores SIGTERM; - signal exits, spawn failures, provider errors, stderr, and assistant output remain visible instead of collapsing to `(no output)`; - failed subagents become failed tool results, so the parent model can react instead of treating them as successful calls; - single and chain results stay within the normal 50 KB / 2,000-line limit; - project agents are selected and confirmed from each task's actual working directory rather than the dispatcher's directory. ## Further potential improvements These are useful follow-ups, but they are separate product and storage decisions rather than part of the failure-handling fixes in this PR. <details> <summary>Potential follow-ups</summary> ### Keep persisted transcripts bounded Subagent tool results persist the full child message history in `details`, which can make parent session files large and slow to reload. A follow-up could compact that history or spill it to disk. ### Define an aggregate parallel-output budget Parallel mode still gives every task its own 50 KB budget. A follow-up could keep every task represented while enforcing one overall tool-result limit. ### Report child usage to the calling session Per-child token and cost usage is visible in result cards but is not added to the calling session's totals. ### Isolate malformed agent definitions One malformed YAML agent can still break discovery for every valid agent in the same directory. ### Limit subagent recursion A nesting limit would prevent accidental process and paid-request multiplication when subagents can invoke the subagent tool again. * https://github.com/earendil-works/pi/issues/8195 ### Add a configurable timeout Unattended runs would benefit from a generous opt-in timeout for children that never complete. </details> ## Related issues ### Spawn failures lose their diagnostics This directly addresses the reported case where a nonexistent child working directory produces `(no output)` instead of the actual spawn error. The regression suite includes that same reproduction. * https://github.com/earendil-works/pi/issues/8129 ### Persisted child history can bloat sessions This PR intentionally keeps full child history in tool-result details, so session compaction or spill-to-disk remains follow-up work rather than something fixed here. * https://github.com/earendil-works/pi/issues/7452
80 lines
2.9 KiB
JavaScript
80 lines
2.9 KiB
JavaScript
import fs from "node:fs";
|
|
|
|
const task = process.argv.find((arg) => arg.startsWith("Task: "))?.slice(6) ?? "success";
|
|
const requestedModelIndex = process.argv.indexOf("--model");
|
|
const requestedModel = requestedModelIndex >= 0 ? process.argv[requestedModelIndex + 1] : "fixture-model";
|
|
|
|
const zeroUsage = () => ({
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
});
|
|
|
|
const assistant = (text, options = {}) => ({
|
|
role: "assistant",
|
|
content: options.toolCall
|
|
? [
|
|
{ type: "text", text },
|
|
{ type: "toolCall", id: "call-1", name: "read", arguments: { path: "README.md" } },
|
|
]
|
|
: [{ type: "text", text }],
|
|
api: "anthropic-messages",
|
|
provider: "fixture",
|
|
model: options.model ?? requestedModel,
|
|
usage: options.usage ?? zeroUsage(),
|
|
stopReason: options.stopReason ?? "stop",
|
|
...(options.errorMessage ? { errorMessage: options.errorMessage } : {}),
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
const emit = (event) => process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
const emitMessage = (message) => emit({ type: "message_end", message });
|
|
|
|
if (task.startsWith("record-argv:")) {
|
|
fs.writeFileSync(task.slice("record-argv:".length), JSON.stringify(process.argv));
|
|
emitMessage(assistant("done"));
|
|
} else if (task.startsWith("record-invocation:")) {
|
|
fs.writeFileSync(
|
|
task.slice("record-invocation:".length),
|
|
JSON.stringify({ argv: process.argv, cwd: process.cwd() }),
|
|
);
|
|
emitMessage(assistant("done"));
|
|
} else if (task === "tool-then-final") {
|
|
emitMessage(assistant("working", { stopReason: "toolUse", toolCall: true }));
|
|
setTimeout(() => emitMessage(assistant("finished")), 150);
|
|
} else if (task.startsWith("tool-wait:")) {
|
|
const releaseFile = task.slice("tool-wait:".length);
|
|
emitMessage(assistant("working", { stopReason: "toolUse", toolCall: true }));
|
|
const timer = setInterval(() => {
|
|
if (!fs.existsSync(releaseFile)) return;
|
|
clearInterval(timer);
|
|
emitMessage(assistant("finished"));
|
|
}, 10);
|
|
} else if (task === "signal") {
|
|
process.kill(process.pid, "SIGTERM");
|
|
} else if (task === "error" || task === "model-error") {
|
|
process.stderr.write("stderr evidence\n");
|
|
emitMessage(
|
|
assistant("assistant evidence", {
|
|
stopReason: "error",
|
|
errorMessage: "provider evidence",
|
|
}),
|
|
);
|
|
if (task === "error") process.exitCode = 1;
|
|
} else if (task.startsWith("large")) {
|
|
emitMessage(assistant(`${"x".repeat(60 * 1024)}\n${"line\n".repeat(2100)}`));
|
|
} else if (task.startsWith("ignore-sigterm:")) {
|
|
fs.writeFileSync(task.slice("ignore-sigterm:".length), String(process.pid));
|
|
process.on("SIGTERM", () => {});
|
|
emitMessage(assistant("partial", { stopReason: "toolUse" }));
|
|
setInterval(() => {}, 1000);
|
|
} else if (task.startsWith("hold:")) {
|
|
fs.appendFileSync(task.slice("hold:".length), `${process.pid}\n`);
|
|
setInterval(() => {}, 1000);
|
|
} else {
|
|
process.stderr.write(`Unknown fixture task: ${task}\n`);
|
|
process.exitCode = 2;
|
|
}
|