@a-t-h-i/bot-lobby 0.6.54 → 0.6.56
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/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +12 -1
- package/package.json +4 -1
- package/prompts/master.md +10 -30
- package/prompts/worker.md +8 -12
- package/src/desk/ipc.ts +4 -3
- package/src/execution/agent-runner.ts +20 -129
- package/src/execution/delegation.ts +10 -32
- package/src/execution/live-run.ts +3 -14
- package/src/execution/pi-runner.ts +36 -166
- package/src/harness/acp.ts +272 -0
- package/src/harness/catalog.ts +257 -0
- package/src/harness/claude-sdk.ts +169 -0
- package/src/harness/claude.ts +188 -0
- package/src/harness/cli-args.ts +62 -0
- package/src/harness/codex.ts +155 -0
- package/src/harness/instructions.ts +45 -0
- package/src/harness/model-manifest.json +829 -0
- package/src/harness/process.ts +175 -0
- package/src/harness/runner.ts +188 -0
- package/src/harness/status.ts +115 -0
- package/src/index.ts +3 -0
- package/src/lobby/feed.ts +1 -1
- package/src/lobby/host.ts +4 -0
- package/src/lobby/planner.ts +4 -3
- package/src/lobby/pr-review.ts +4 -3
- package/src/lobby/quickfix.ts +8 -4
- package/src/lobby/recovery.ts +2 -10
- package/src/lobby/runtime.ts +1 -1
- package/src/lobby/service.ts +3 -1
- package/src/master/goal-check.ts +1 -6
- package/src/master/master.ts +12 -24
- package/src/master/research.ts +4 -6
- package/src/pi/commands.ts +5 -45
- package/src/pi/delegation.ts +4 -4
- package/src/pi/events.ts +6 -19
- package/src/pi/grants.ts +89 -0
- package/src/pi/model-settings.ts +2 -4
- package/src/pi/model-support.ts +10 -6
- package/src/pi/route.ts +1 -1
- package/src/pi/run-summary.ts +3 -7
- package/src/pi/start-flags.ts +3 -3
- package/src/pi/start-task.ts +2 -8
- package/src/pi/tools.ts +8 -11
- package/src/roles/worker.ts +0 -18
- package/src/schemas/configuration.ts +73 -45
- package/src/schemas/findings.ts +0 -12
- package/src/schemas/harness.ts +122 -0
- package/src/schemas/task.ts +0 -1
- package/src/state/metrics.ts +1 -1
- package/src/state/work-clock.ts +140 -0
- package/src/state/work-time.ts +4 -7
- package/src/webui/api/index.ts +3 -1
- package/src/webui/api/settings.ts +12 -18
- package/src/webui/dev/fake-service.ts +27 -0
- package/src/webui/protocol.ts +32 -0
- package/src/workflow/workflow.ts +23 -318
- package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-BVehQ7yB.js} +1 -1
- package/webui/dist/assets/index-BKiKxUUG.js +114 -0
- package/webui/dist/assets/index-DOLQ4FDd.css +2 -0
- package/webui/dist/build.json +20 -12
- package/webui/dist/index.html +2 -2
- package/src/state/budget.ts +0 -301
- package/src/state/step-limits.ts +0 -41
- package/webui/dist/assets/index-Cca5uEVs.js +0 -115
- package/webui/dist/assets/index-DsVdXmh5.css +0 -2
|
@@ -8,10 +8,13 @@ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { basename, join } from "node:path";
|
|
10
10
|
import { activityWord } from "../pi/activity.ts";
|
|
11
|
+
import { skillFiles } from "../pi/grants.ts";
|
|
11
12
|
import { shortDuration } from "../text.ts";
|
|
12
13
|
import { readRelayRequest } from "../ask/relay.ts";
|
|
13
14
|
import type { AskQuestion, AskResult } from "../ask/types.ts";
|
|
14
15
|
import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
|
|
16
|
+
import type { HarnessRun } from "../schemas/harness.ts";
|
|
17
|
+
import { runHarnessAgent } from "../harness/runner.ts";
|
|
15
18
|
|
|
16
19
|
/** Live control over one running subagent: queue a steering message for its next turn. */
|
|
17
20
|
export interface RunHandle {
|
|
@@ -39,7 +42,8 @@ export interface PiRunOptions {
|
|
|
39
42
|
tools?: readonly string[];
|
|
40
43
|
model?: string;
|
|
41
44
|
thinking?: string;
|
|
42
|
-
|
|
45
|
+
/** Names of the skills this agent may load; absent keeps pi's own discovery. */
|
|
46
|
+
skills?: string[];
|
|
43
47
|
signal?: AbortSignal;
|
|
44
48
|
/** Reports the current one-word tool activity while the agent works. */
|
|
45
49
|
onActivity?: (activity: string) => void;
|
|
@@ -53,34 +57,12 @@ export interface PiRunOptions {
|
|
|
53
57
|
stallTimeoutMs?: number;
|
|
54
58
|
/** Silence allowed while one tool call is in flight (tests, builds); 0 disables. */
|
|
55
59
|
toolStallTimeoutMs?: number;
|
|
56
|
-
/** Ask the agent to wrap up and report at this elapsed time; 0 disables. */
|
|
57
|
-
wrapUpAtMs?: number;
|
|
58
60
|
/** Receives the steering handle once the process is running. */
|
|
59
61
|
onStart?: (handle: RunHandle) => void;
|
|
60
|
-
/** Under a task time budget: when the agent is warned, when its time is up, and who decides on more (see ProcessRunOptions). */
|
|
61
|
-
time?: TimeLimit;
|
|
62
62
|
/** Answers the agent's relayed questions; without it they are cancelled like any other dialog. */
|
|
63
63
|
onAsk?: RelayAsk;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* A run's allotted time under a task budget. At `headsUpAtMs` the agent is
|
|
68
|
-
* told how much is left; at `upAtMs` it is steered to stop and report where it
|
|
69
|
-
* left off. Once that report is in (its turn ended), `onTimeUp` decides: more
|
|
70
|
-
* ms keeps the same process going with its context (a follow-up prompt), 0
|
|
71
|
-
* ends it. The hard deadline (`timeoutMs`) and the stall watchdog wait while
|
|
72
|
-
* the decision is pending, and move out with any time granted.
|
|
73
|
-
*/
|
|
74
|
-
export interface TimeLimit {
|
|
75
|
-
/** Reporting time is included in the allowance, including extensions. */
|
|
76
|
-
hardLimit?: boolean;
|
|
77
|
-
upAtMs: number;
|
|
78
|
-
headsUpAtMs?: number;
|
|
79
|
-
headsUpMessage?: string;
|
|
80
|
-
upMessage: string;
|
|
81
|
-
onTimeUp?: (report: string) => Promise<{ extraMs: number; message?: string }>;
|
|
82
|
-
/** Time the agent gets to write its report after `upAtMs` before it is stopped. */
|
|
83
|
-
graceMs: number;
|
|
64
|
+
/** Run on this third-party harness instead of pi (`model` is then ignored for the harness's own). */
|
|
65
|
+
harness?: HarnessRun;
|
|
84
66
|
}
|
|
85
67
|
|
|
86
68
|
export interface ProcessOutcome {
|
|
@@ -88,17 +70,10 @@ export interface ProcessOutcome {
|
|
|
88
70
|
stdout: string;
|
|
89
71
|
stderr: string;
|
|
90
72
|
killed: boolean;
|
|
91
|
-
timedOut: boolean;
|
|
92
73
|
/** Killed by the watchdog after going silent. */
|
|
93
74
|
stalled?: boolean;
|
|
94
|
-
/** The agent was asked to wrap up before its deadline. */
|
|
95
|
-
wrappedUp?: boolean;
|
|
96
75
|
/** The RPC host rejected the prompt itself. */
|
|
97
76
|
protocolError?: string;
|
|
98
|
-
/** Stopped at its allotted time and not given more: its report says where it left off. */
|
|
99
|
-
timeUp?: boolean;
|
|
100
|
-
/** Time granted past its allotment, in ms. */
|
|
101
|
-
extendedMs?: number;
|
|
102
77
|
}
|
|
103
78
|
|
|
104
79
|
/** Streaming agent events forwarded as they arrive on stdout. */
|
|
@@ -113,14 +88,9 @@ export type PiStreamEvent =
|
|
|
113
88
|
| { type: "compaction" }
|
|
114
89
|
| { type: "compaction_end"; error?: string }
|
|
115
90
|
| { type: "usage"; input: number; output: number; cost: number; model?: string }
|
|
116
|
-
| { type: "wrap_up" }
|
|
117
|
-
/** Its allotted time is up: it was asked to stop and report where it left off. */
|
|
118
|
-
| { type: "time_up" }
|
|
119
|
-
/** It was given `ms` more and carries on. */
|
|
120
|
-
| { type: "extended"; ms: number }
|
|
121
91
|
/** One finished thinking block, bounded to `MAX_THOUGHT_CHARS`. */
|
|
122
92
|
| { type: "thought"; text: string }
|
|
123
|
-
/** It asked the user (relayed);
|
|
93
|
+
/** It asked the user (relayed); the stall watchdog waits until the answers are back. */
|
|
124
94
|
| { type: "asking"; questions: number }
|
|
125
95
|
/** The answers went back after `waitedMs` of waiting on the user. */
|
|
126
96
|
| { type: "answered"; waitedMs: number }
|
|
@@ -132,7 +102,6 @@ export const MAX_THOUGHT_CHARS = 1500;
|
|
|
132
102
|
export interface ProcessRunOptions {
|
|
133
103
|
cwd: string;
|
|
134
104
|
signal?: AbortSignal;
|
|
135
|
-
timeoutMs: number;
|
|
136
105
|
/** The task message sent to the agent over RPC stdin. */
|
|
137
106
|
prompt?: string;
|
|
138
107
|
/** Optional sink for streaming agent events. */
|
|
@@ -140,12 +109,7 @@ export interface ProcessRunOptions {
|
|
|
140
109
|
env?: Record<string, string>;
|
|
141
110
|
stallTimeoutMs?: number;
|
|
142
111
|
toolStallTimeoutMs?: number;
|
|
143
|
-
wrapUpAtMs?: number;
|
|
144
|
-
wrapUpMessage?: string;
|
|
145
112
|
onStart?: (handle: RunHandle) => void;
|
|
146
|
-
/** Grace between the deadline abort and the hard kill. */
|
|
147
|
-
graceMs?: number;
|
|
148
|
-
time?: TimeLimit;
|
|
149
113
|
onAsk?: RelayAsk;
|
|
150
114
|
}
|
|
151
115
|
|
|
@@ -162,9 +126,6 @@ export interface PiRunResult {
|
|
|
162
126
|
usage: { input: number; output: number; cost: number; turns: number };
|
|
163
127
|
model?: string;
|
|
164
128
|
stalled?: boolean;
|
|
165
|
-
wrappedUp?: boolean;
|
|
166
|
-
timeUp?: boolean;
|
|
167
|
-
extendedMs?: number;
|
|
168
129
|
}
|
|
169
130
|
|
|
170
131
|
export interface ParsedStream {
|
|
@@ -179,17 +140,8 @@ const MAX_STREAM_CHARS = 5_000_000;
|
|
|
179
140
|
const HEARTBEAT_MS = 2000;
|
|
180
141
|
const SETTLE_FALLBACK_MS = 3000;
|
|
181
142
|
const EXIT_DRAIN_MS = 1500;
|
|
182
|
-
const DEADLINE_GRACE_MS = 5000;
|
|
183
143
|
const DIALOG_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
184
144
|
|
|
185
|
-
/** The steering message sent at the wrap-up point. */
|
|
186
|
-
export const WRAP_UP_MESSAGE = [
|
|
187
|
-
"bot-lobby: you are close to your time limit.",
|
|
188
|
-
"Stop exploring now. Finish or revert any half-done edit so every file is consistent,",
|
|
189
|
-
"then write your final report in the required output format.",
|
|
190
|
-
"List anything unfinished under Blockers or Notes.",
|
|
191
|
-
].join(" ");
|
|
192
|
-
|
|
193
145
|
/** RPC protocol lines the process host reacts to (never forwarded to the caller). */
|
|
194
146
|
export interface ControlEvent {
|
|
195
147
|
type: string;
|
|
@@ -368,10 +320,15 @@ export function finishedThought(line: string): string | undefined {
|
|
|
368
320
|
export const MCP_RESOURCE_TOOLS: readonly string[] = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
|
|
369
321
|
|
|
370
322
|
/** Build the `pi` argv for one isolated, headless RPC agent run; the task goes over stdin. */
|
|
371
|
-
export function buildPiArgs(options: Omit<PiRunOptions, "task"> & { systemPromptFile?: string }): string[] {
|
|
323
|
+
export function buildPiArgs(options: Omit<PiRunOptions, "task"> & { systemPromptFile?: string; skillFiles?: string[] }): string[] {
|
|
372
324
|
const args = ["--mode", "rpc", "--no-session", "--no-prompt-templates", "--no-themes", "--extension", fileURLToPath(new URL("../index.ts", import.meta.url))];
|
|
373
325
|
if (options.model && options.model !== "inherit") args.push("--model", options.model);
|
|
374
326
|
if (options.thinking) args.push("--thinking", options.thinking);
|
|
327
|
+
// A skill grant replaces pi's own discovery with exactly the granted skills.
|
|
328
|
+
if (options.skillFiles) {
|
|
329
|
+
args.push("--no-skills");
|
|
330
|
+
for (const file of options.skillFiles) args.push("--skill", file);
|
|
331
|
+
}
|
|
375
332
|
if (options.tools && options.tools.length > 0) {
|
|
376
333
|
// pi keeps every MCP tool under --tools unless an entry names one, so name the granted ones (and the resource
|
|
377
334
|
// tools their servers need), or run without MCP at all when nothing is granted.
|
|
@@ -449,10 +406,10 @@ export function isPiLauncher(script: string): boolean {
|
|
|
449
406
|
return base === "pi" || base === "pi.js" || script.includes("pi-coding-agent");
|
|
450
407
|
}
|
|
451
408
|
|
|
452
|
-
const POSIX = process.platform !== "win32";
|
|
409
|
+
export const POSIX = process.platform !== "win32";
|
|
453
410
|
|
|
454
411
|
/** Signal the child's whole process group (POSIX), so grandchildren go too. */
|
|
455
|
-
function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
|
|
412
|
+
export function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
|
|
456
413
|
if (proc.pid === undefined) return;
|
|
457
414
|
if (POSIX) {
|
|
458
415
|
try {
|
|
@@ -469,7 +426,7 @@ function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
|
|
|
469
426
|
}
|
|
470
427
|
}
|
|
471
428
|
|
|
472
|
-
function terminate(proc: ChildProcess, graceMs = 5000): void {
|
|
429
|
+
export function terminate(proc: ChildProcess, graceMs = 5000): void {
|
|
473
430
|
signalGroup(proc, "SIGTERM");
|
|
474
431
|
const escalation = setTimeout(() => signalGroup(proc, "SIGKILL"), graceMs);
|
|
475
432
|
escalation.unref();
|
|
@@ -504,23 +461,15 @@ function rpcInput(proc: ChildProcess): { send(command: object): void; close(): v
|
|
|
504
461
|
|
|
505
462
|
interface RunState {
|
|
506
463
|
killed: boolean;
|
|
507
|
-
timedOut: boolean;
|
|
508
464
|
stalled: boolean;
|
|
509
|
-
wrappedUp: boolean;
|
|
510
465
|
settled: boolean;
|
|
511
466
|
protocolError?: string;
|
|
512
|
-
/**
|
|
513
|
-
timeUp: boolean;
|
|
514
|
-
headsUp: boolean;
|
|
515
|
-
/** Waiting on the decision: the agent is idle, so the deadline and the stall watchdog wait too. */
|
|
516
|
-
deciding: boolean;
|
|
517
|
-
/** Relayed questions waiting on the user: every clock and the watchdog wait. */
|
|
467
|
+
/** Relayed questions waiting on the user: the watchdog waits. */
|
|
518
468
|
asking: number;
|
|
519
|
-
extendedMs: number;
|
|
520
469
|
}
|
|
521
470
|
|
|
522
471
|
/** Smallest positive limit, used to pick a watchdog cadence that can see it. */
|
|
523
|
-
function watchInterval(...limits: Array<number | undefined>): number {
|
|
472
|
+
export function watchInterval(...limits: Array<number | undefined>): number {
|
|
524
473
|
const positive = limits.filter((limit): limit is number => typeof limit === "number" && limit > 0);
|
|
525
474
|
const smallest = positive.length > 0 ? Math.min(...positive) : 4000;
|
|
526
475
|
return Math.max(25, Math.min(1000, Math.floor(smallest / 4)));
|
|
@@ -528,11 +477,11 @@ function watchInterval(...limits: Array<number | undefined>): number {
|
|
|
528
477
|
|
|
529
478
|
/**
|
|
530
479
|
* Default runner: spawn pi in RPC mode, send the task over stdin, and watch it.
|
|
531
|
-
* The child leads its own process group so a stall
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
480
|
+
* The child leads its own process group so a stall or cancel takes its
|
|
481
|
+
* grandchildren (dev servers, watch-mode tests) with it, and the run ends on
|
|
482
|
+
* process exit rather than waiting for a pipe a grandchild may hold open.
|
|
483
|
+
* There is no time limit: a run ends when the agent finishes, is cancelled,
|
|
484
|
+
* or goes silent for so long it is killed as stalled.
|
|
536
485
|
*/
|
|
537
486
|
export function spawnPiProcess(args: string[], options: ProcessRunOptions): Promise<ProcessOutcome> {
|
|
538
487
|
return new Promise((resolve) => {
|
|
@@ -547,25 +496,16 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
547
496
|
windowsHide: true,
|
|
548
497
|
stdio: ["pipe", "pipe", "pipe"],
|
|
549
498
|
});
|
|
550
|
-
const state: RunState = { killed: false,
|
|
499
|
+
const state: RunState = { killed: false, stalled: false, settled: false, asking: 0 };
|
|
551
500
|
const input = rpcInput(proc);
|
|
552
|
-
|
|
553
|
-
const time = options.time;
|
|
554
|
-
// The allotted time counts from here; a grant restarts it.
|
|
555
|
-
let clockStart = started;
|
|
556
|
-
let upAtMs = time?.upAtMs ?? 0;
|
|
557
|
-
let headsUpAtMs = time?.headsUpAtMs ?? 0;
|
|
558
|
-
let lastOutput = started;
|
|
501
|
+
let lastOutput = Date.now();
|
|
559
502
|
let quietGraceUntil = 0;
|
|
560
503
|
let toolsInFlight = 0;
|
|
561
504
|
let stderr = "";
|
|
562
505
|
let exitCode: number | undefined;
|
|
563
506
|
let settleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
564
507
|
let done = false;
|
|
565
|
-
// Time spent waiting on the user's answers is not the agent's: its limits move out by it.
|
|
566
|
-
let deadlineAt = started + options.timeoutMs;
|
|
567
508
|
let askStarted = 0;
|
|
568
|
-
let pausedMs = 0;
|
|
569
509
|
const asks = new AbortController();
|
|
570
510
|
|
|
571
511
|
const onEvent = (event: PiStreamEvent) => {
|
|
@@ -574,51 +514,18 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
574
514
|
if (event.type === "retry") quietGraceUntil = Date.now() + event.delayMs;
|
|
575
515
|
options.onEvent?.(event);
|
|
576
516
|
};
|
|
577
|
-
//
|
|
517
|
+
// The run ends once its turn is over.
|
|
578
518
|
let turnDone = false;
|
|
579
519
|
const turnOver = () => {
|
|
580
520
|
if (turnDone) return;
|
|
581
521
|
turnDone = true;
|
|
582
522
|
clearTimeout(settleTimer);
|
|
583
523
|
settleTimer = undefined;
|
|
584
|
-
|
|
585
|
-
else input.close();
|
|
586
|
-
};
|
|
587
|
-
const decide = (onTimeUp: NonNullable<TimeLimit["onTimeUp"]>) => {
|
|
588
|
-
state.deciding = true;
|
|
589
|
-
clearTimeout(deadline);
|
|
590
|
-
const report = parsePiStream(collector.peek()).text;
|
|
591
|
-
onTimeUp(report).then(({ extraMs, message }) => {
|
|
592
|
-
if (done || state.killed) return;
|
|
593
|
-
state.deciding = false;
|
|
594
|
-
if (extraMs <= 0) return input.close();
|
|
595
|
-
// Granted: the same agent carries on where it stopped, its context intact.
|
|
596
|
-
state.timeUp = false;
|
|
597
|
-
state.headsUp = false;
|
|
598
|
-
state.settled = false;
|
|
599
|
-
state.extendedMs += extraMs;
|
|
600
|
-
turnDone = false;
|
|
601
|
-
clockStart = Date.now();
|
|
602
|
-
lastOutput = clockStart;
|
|
603
|
-
upAtMs = time?.hardLimit ? Math.max(1, extraMs - Math.min(time.graceMs, Math.floor(extraMs / 4))) : extraMs;
|
|
604
|
-
// The user just named the time it has: no heads-up quoting the old allotment.
|
|
605
|
-
headsUpAtMs = 0;
|
|
606
|
-
const nextLimit = extraMs + (time?.hardLimit ? 0 : time?.graceMs ?? 0);
|
|
607
|
-
deadlineAt = Date.now() + nextLimit;
|
|
608
|
-
deadline = setTimeout(onDeadline, nextLimit);
|
|
609
|
-
input.send({ type: "prompt", message: message ?? "You have more time: carry on from where you left off and finish." });
|
|
610
|
-
options.onEvent?.({ type: "extended", ms: extraMs });
|
|
611
|
-
}, () => {
|
|
612
|
-
state.deciding = false;
|
|
613
|
-
input.close();
|
|
614
|
-
});
|
|
524
|
+
input.close();
|
|
615
525
|
};
|
|
616
526
|
const relay = (id: string | undefined, questions: AskQuestion[], ask: RelayAsk) => {
|
|
617
527
|
state.asking += 1;
|
|
618
|
-
if (state.asking === 1)
|
|
619
|
-
askStarted = Date.now();
|
|
620
|
-
clearTimeout(deadline);
|
|
621
|
-
}
|
|
528
|
+
if (state.asking === 1) askStarted = Date.now();
|
|
622
529
|
options.onEvent?.({ type: "asking", questions: questions.length });
|
|
623
530
|
ask(questions, asks.signal)
|
|
624
531
|
.catch((): AskResult => ({ answers: [], cancelled: true }))
|
|
@@ -628,11 +535,7 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
628
535
|
if (state.asking === 0) {
|
|
629
536
|
const now = Date.now();
|
|
630
537
|
const waited = now - askStarted;
|
|
631
|
-
pausedMs += waited;
|
|
632
|
-
deadlineAt += waited;
|
|
633
|
-
clockStart += waited;
|
|
634
538
|
lastOutput = now;
|
|
635
|
-
if (!state.deciding) deadline = setTimeout(onDeadline, Math.max(0, deadlineAt - now));
|
|
636
539
|
options.onEvent?.({ type: "answered", waitedMs: waited });
|
|
637
540
|
}
|
|
638
541
|
input.send({ type: "extension_ui_response", id, value: JSON.stringify(result) });
|
|
@@ -675,7 +578,6 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
675
578
|
const finish = (code: number) => {
|
|
676
579
|
if (done) return;
|
|
677
580
|
done = true;
|
|
678
|
-
clearTimeout(deadline);
|
|
679
581
|
clearTimeout(settleTimer);
|
|
680
582
|
clearInterval(watch);
|
|
681
583
|
// Questions still open when the run ends are put away.
|
|
@@ -684,9 +586,8 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
684
586
|
input.close();
|
|
685
587
|
// Reap anything the agent left running in its group.
|
|
686
588
|
if (POSIX) signalGroup(proc, "SIGTERM");
|
|
687
|
-
const { killed,
|
|
688
|
-
|
|
689
|
-
resolve({ exitCode: code, stdout: collector.finish(), stderr, killed, timedOut, stalled, wrappedUp, protocolError, ...(timeUp ? { timeUp } : {}), ...(state.extendedMs > 0 ? { extendedMs: state.extendedMs } : {}) });
|
|
589
|
+
const { killed, stalled, protocolError } = state;
|
|
590
|
+
resolve({ exitCode: code, stdout: collector.finish(), stderr, killed, stalled, protocolError });
|
|
690
591
|
};
|
|
691
592
|
const onAbort = () => {
|
|
692
593
|
state.killed = true;
|
|
@@ -694,31 +595,9 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
694
595
|
input.close();
|
|
695
596
|
terminate(proc, 3000);
|
|
696
597
|
};
|
|
697
|
-
const onDeadline = () => {
|
|
698
|
-
state.killed = true;
|
|
699
|
-
state.timedOut = true;
|
|
700
|
-
input.send({ type: "abort" });
|
|
701
|
-
input.close();
|
|
702
|
-
setTimeout(() => terminate(proc), options.graceMs ?? DEADLINE_GRACE_MS).unref();
|
|
703
|
-
};
|
|
704
|
-
let deadline = setTimeout(onDeadline, options.timeoutMs);
|
|
705
598
|
const watch = setInterval(() => {
|
|
706
|
-
if (state.killed || done || state.
|
|
599
|
+
if (state.killed || done || state.asking > 0) return;
|
|
707
600
|
const now = Date.now();
|
|
708
|
-
if (!state.wrappedUp && !state.settled && options.wrapUpAtMs && options.wrapUpAtMs > 0 && now - started - pausedMs >= options.wrapUpAtMs) {
|
|
709
|
-
state.wrappedUp = true;
|
|
710
|
-
input.send({ type: "steer", message: options.wrapUpMessage ?? WRAP_UP_MESSAGE });
|
|
711
|
-
options.onEvent?.({ type: "wrap_up" });
|
|
712
|
-
}
|
|
713
|
-
if (time && !state.headsUp && !state.timeUp && !turnDone && headsUpAtMs > 0 && now - clockStart >= headsUpAtMs) {
|
|
714
|
-
state.headsUp = true;
|
|
715
|
-
if (time.headsUpMessage) input.send({ type: "steer", message: time.headsUpMessage });
|
|
716
|
-
}
|
|
717
|
-
if (time && !state.timeUp && !turnDone && upAtMs > 0 && now - clockStart >= upAtMs) {
|
|
718
|
-
state.timeUp = true;
|
|
719
|
-
input.send({ type: "steer", message: time.upMessage });
|
|
720
|
-
options.onEvent?.({ type: "time_up" });
|
|
721
|
-
}
|
|
722
601
|
const limit = toolsInFlight > 0 ? options.toolStallTimeoutMs : options.stallTimeoutMs;
|
|
723
602
|
if (limit && limit > 0 && now - Math.max(lastOutput, quietGraceUntil) >= limit) {
|
|
724
603
|
state.killed = true;
|
|
@@ -726,7 +605,7 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
726
605
|
input.close();
|
|
727
606
|
terminate(proc, 3000);
|
|
728
607
|
}
|
|
729
|
-
}, watchInterval(options.stallTimeoutMs, options.toolStallTimeoutMs
|
|
608
|
+
}, watchInterval(options.stallTimeoutMs, options.toolStallTimeoutMs));
|
|
730
609
|
watch.unref();
|
|
731
610
|
|
|
732
611
|
proc.on("exit", (code, signal) => {
|
|
@@ -747,15 +626,11 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
747
626
|
}
|
|
748
627
|
|
|
749
628
|
function toResult(parsed: ParsedStream, outcome: ProcessOutcome, aborted: boolean, options: PiRunOptions): PiRunResult {
|
|
750
|
-
const base = { output: parsed.text, usage: parsed.usage, model: parsed.model
|
|
629
|
+
const base = { output: parsed.text, usage: parsed.usage, model: parsed.model };
|
|
751
630
|
if (outcome.stalled) {
|
|
752
631
|
const quiet = shortDuration(options.stallTimeoutMs ?? 0);
|
|
753
632
|
return { ...base, status: "timeout", stalled: true, error: `stalled: no output for ${quiet}` };
|
|
754
633
|
}
|
|
755
|
-
if (outcome.timedOut) {
|
|
756
|
-
const note = outcome.wrappedUp ? " after a wrap-up request" : "";
|
|
757
|
-
return { ...base, status: "timeout", error: `hit the ${shortDuration(options.timeoutMs)} time limit${note}` };
|
|
758
|
-
}
|
|
759
634
|
if (outcome.killed || aborted || parsed.stopReason === "aborted") {
|
|
760
635
|
return { ...base, status: "cancelled", error: "agent was cancelled" };
|
|
761
636
|
}
|
|
@@ -780,29 +655,24 @@ function writePromptFile(prompt: string): { file: string; cleanup: () => void }
|
|
|
780
655
|
|
|
781
656
|
/** Run one isolated subagent and normalise its outcome. */
|
|
782
657
|
export async function runPiAgent(given: PiRunOptions, run: ProcessRunner = spawnPiProcess): Promise<PiRunResult> {
|
|
658
|
+
if (given.harness) return runHarnessAgent({ ...given, harness: given.harness });
|
|
783
659
|
const options = given.autoCompact === false ? { ...given, env: { ...given.env, [AUTO_COMPACT_ENV]: "0" } } : given;
|
|
784
660
|
const host = await delegationHost(options, run);
|
|
785
661
|
let prompt: ReturnType<typeof writePromptFile> | undefined;
|
|
786
662
|
try {
|
|
787
663
|
prompt = writePromptFile([options.systemPrompt, host.note].filter(Boolean).join("\n\n"));
|
|
788
664
|
if (options.signal?.aborted) return { status: "cancelled", output: "", usage: { input: 0, output: 0, cost: 0, turns: 0 } };
|
|
789
|
-
const args = buildPiArgs({ ...options, tools: host.tools, mcpTools: host.mcpTools, systemPromptFile: prompt.file });
|
|
665
|
+
const args = buildPiArgs({ ...options, tools: host.tools, mcpTools: host.mcpTools, systemPromptFile: prompt.file, ...(options.skills ? { skillFiles: skillFiles(options.skills, options.cwd) } : {}) });
|
|
790
666
|
const outcome = await run(args, {
|
|
791
667
|
cwd: options.cwd,
|
|
792
668
|
signal: options.signal,
|
|
793
|
-
timeoutMs: options.timeoutMs,
|
|
794
669
|
prompt: `Task: ${options.task}`,
|
|
795
670
|
env: host.env,
|
|
796
671
|
stallTimeoutMs: options.stallTimeoutMs,
|
|
797
672
|
toolStallTimeoutMs: options.toolStallTimeoutMs,
|
|
798
|
-
wrapUpAtMs: options.wrapUpAtMs,
|
|
799
|
-
...(options.time ? { time: options.time } : {}),
|
|
800
673
|
...(options.onAsk ? { onAsk: options.onAsk } : {}),
|
|
801
674
|
onStart: host.onStart,
|
|
802
675
|
onEvent: (event) => {
|
|
803
|
-
if (event.type === "asking") host.pause?.();
|
|
804
|
-
if (event.type === "answered") host.extend?.(event.waitedMs);
|
|
805
|
-
if (event.type === "extended") host.extend?.(event.ms, true);
|
|
806
676
|
if (event.type === "tool_execution_start") options.onActivity?.(activityWord(event.toolName));
|
|
807
677
|
options.onEvent?.(event);
|
|
808
678
|
},
|