@hcflabs/pi-subagent 28.2.1 → 28.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/direct-herdr.js +57 -25
- package/docs/orchestration.md +2 -0
- package/package.json +1 -1
package/dist/direct-herdr.js
CHANGED
|
@@ -86,6 +86,9 @@ export function exactDirectTerminalTurn(jsonl, prompt) {
|
|
|
86
86
|
return reason === "stop" || reason === "error" || reason === "aborted";
|
|
87
87
|
}
|
|
88
88
|
const turnSuffix = (turn) => `\n\nTurn identity: ${turn}`;
|
|
89
|
+
/** How long to wait for Pi to retry a turn that ended with a provider error, and how many times. */
|
|
90
|
+
const PROVIDER_RETRY_GRACE_MS = 65_000;
|
|
91
|
+
const PROVIDER_RETRY_WAITS = 5;
|
|
89
92
|
/** Refuse continuation unless the saved session's exact recorded turn is terminal and no later user turn exists. */
|
|
90
93
|
export async function assertSettledDirectSession(sessionFile, turn) {
|
|
91
94
|
const info = await stat(sessionFile).catch((error) => {
|
|
@@ -481,36 +484,65 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
481
484
|
deadline = Date.now() + idleMs;
|
|
482
485
|
}
|
|
483
486
|
};
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
"--until", "done", "--until", "blocked", "--until", "unknown",
|
|
498
|
-
"--timeout", String(Math.max(1, Math.min(current === "idle" ? 1000 : OPERATION_MS - 1000, remaining)))];
|
|
487
|
+
let retryWaits = 0;
|
|
488
|
+
// A provider error, such as HTTP 429, ends a turn with stopReason "error", and Pi can then retry it.
|
|
489
|
+
// Wait a bounded time for the retry before the error counts as the final answer.
|
|
490
|
+
const retryPending = async () => {
|
|
491
|
+
if (retryWaits >= PROVIDER_RETRY_WAITS)
|
|
492
|
+
return false;
|
|
493
|
+
const info = await stat(sessionFile).catch(() => undefined);
|
|
494
|
+
if (!info || info.size > SESSION_LIMIT)
|
|
495
|
+
return false;
|
|
496
|
+
if (exactFinalTurn(await readFile(sessionFile, "utf8"), prompt)?.stopReason !== "error")
|
|
497
|
+
return false;
|
|
498
|
+
retryWaits += 1;
|
|
499
|
+
const args = ["agent", "wait", name, "--until", "working", "--timeout", String(PROVIDER_RETRY_GRACE_MS)];
|
|
499
500
|
const waited = await herdr.exec(args, options(signal));
|
|
500
501
|
if (waited.code !== 0 || waited.killed) {
|
|
501
|
-
if (!waited.killed && hasHerdrErrorCode(waited, "timeout"))
|
|
502
|
-
|
|
503
|
-
}
|
|
502
|
+
if (!waited.killed && hasHerdrErrorCode(waited, "timeout"))
|
|
503
|
+
return false;
|
|
504
504
|
throw new Error(herdrCommandFailure(args, waited));
|
|
505
505
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
506
|
+
current = inspect(JSON.parse(waited.stdout), "agent_info", name, paneId, tabId);
|
|
507
|
+
deadline = Date.now() + idleMs;
|
|
508
|
+
return current === "working";
|
|
509
|
+
};
|
|
510
|
+
for (;;) {
|
|
511
|
+
while (current === "working" || current === "idle") {
|
|
512
|
+
// A turn may finish before observation begins (or between waits).
|
|
513
|
+
if (current === "idle") {
|
|
514
|
+
const answer = await maybeAnswer();
|
|
515
|
+
if (answer !== undefined)
|
|
516
|
+
return answer;
|
|
517
|
+
}
|
|
518
|
+
await observeProgress();
|
|
519
|
+
const remaining = deadline - Date.now();
|
|
520
|
+
if (remaining <= 0)
|
|
521
|
+
throw new Error(`Herdr agent in tab ${tabId} made no progress for ${idleMs}ms; inspect ${sessionFile} before retrying.`);
|
|
522
|
+
const args = ["agent", "wait", name,
|
|
523
|
+
...(current === "idle" ? ["--until", "working"] : ["--until", "idle"]),
|
|
524
|
+
"--until", "done", "--until", "blocked", "--until", "unknown",
|
|
525
|
+
"--timeout", String(Math.max(1, Math.min(current === "idle" ? 1000 : OPERATION_MS - 1000, remaining)))];
|
|
526
|
+
const waited = await herdr.exec(args, options(signal));
|
|
527
|
+
if (waited.code !== 0 || waited.killed) {
|
|
528
|
+
if (!waited.killed && hasHerdrErrorCode(waited, "timeout")) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
throw new Error(herdrCommandFailure(args, waited));
|
|
532
|
+
}
|
|
533
|
+
const next = inspect(JSON.parse(waited.stdout), "agent_info", name, paneId, tabId);
|
|
534
|
+
if (next !== current)
|
|
535
|
+
deadline = Date.now() + idleMs;
|
|
536
|
+
current = next;
|
|
537
|
+
}
|
|
538
|
+
if (current !== "done")
|
|
539
|
+
throw new Error(`Herdr agent in tab ${tabId} became ${current}; inspect it before retrying.`);
|
|
540
|
+
const answer = await maybeAnswer();
|
|
541
|
+
if (answer !== undefined)
|
|
542
|
+
return answer;
|
|
543
|
+
if (!await retryPending())
|
|
544
|
+
return readAnswer();
|
|
510
545
|
}
|
|
511
|
-
if (current !== "done")
|
|
512
|
-
throw new Error(`Herdr agent in tab ${tabId} became ${current}; inspect it before retrying.`);
|
|
513
|
-
return readAnswer();
|
|
514
546
|
},
|
|
515
547
|
async focus() {
|
|
516
548
|
// A reused pane ID must not receive focus: the agent identity must match the launch first.
|
package/docs/orchestration.md
CHANGED
|
@@ -143,6 +143,8 @@ If the extension cannot prove termination of the stopped worker, the workflow ke
|
|
|
143
143
|
|
|
144
144
|
The extension retrieves each final answer from its exact Pi session file, not its possibly truncated screen. Idle, blocked, unknown, or malformed answers fail with recoverable tab and session identities. No whole-run deadline applies.
|
|
145
145
|
|
|
146
|
+
A provider error, such as HTTP 429, can end a worker turn before Pi retries it automatically. When the final turn has `stopReason: "error"`, the extension waits up to 65 seconds for the worker to work again, at most 5 times for one task. The task fails only when Pi does not retry in that time.
|
|
147
|
+
|
|
146
148
|
The tasks in a request share a 40 KiB inline answer budget. Each worker prompt states its share. A longer exact answer is still a success. The result shows a UTF-8-safe prefix and a truncation marker. The marker gives the path of a private `answer-<turn>.md` file beside the session file, which holds the full answer. For a write task, the budget applies to the `answer` field of the completion object. A chain step receives the full answer of the previous step in `{previous}`; the budget applies only to the result for Main. The result details keep `answerFile` for each full-answer file. Recovery text includes these paths outside the output limit, even when checkout notes remove the inline link.
|
|
147
149
|
|
|
148
150
|
Herdr accepts prompt text as one command argument. To stay below the operating system's argument limit, a task packet above 64 KiB uses a private, exclusively created `task-<turn>.md` file beside the session file. This packet includes the full chain context. The worker prompt requires the Role's `read` tool to read all parts with line offsets before work starts. Pi's native `read` can return a line of exactly 50 KiB (51,200 bytes), but not a longer line. If any packet line is longer, or `read` is absent, the worker uses the Role's `bash` tool. If a line is too long and `bash` is absent, the task fails before tab creation with the read limit and a request for `bash` or shorter lines. A Role with neither tool also fails before tab creation; the extension adds no tool. The task file stays available for recovery.
|