@a-t-h-i/bot-lobby 0.6.54 → 0.6.55
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/package.json +1 -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 +16 -129
- package/src/execution/delegation.ts +10 -32
- package/src/execution/live-run.ts +3 -14
- package/src/execution/pi-runner.ts +27 -162
- package/src/index.ts +3 -0
- package/src/lobby/feed.ts +1 -1
- package/src/lobby/host.ts +2 -0
- package/src/lobby/planner.ts +2 -2
- package/src/lobby/pr-review.ts +3 -3
- package/src/lobby/quickfix.ts +4 -3
- 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 +10 -22
- package/src/master/research.ts +3 -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 +6 -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 +47 -39
- package/src/schemas/findings.ts +0 -12
- 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 +2 -1
- package/src/webui/api/settings.ts +6 -18
- package/src/webui/dev/fake-service.ts +14 -0
- package/src/webui/protocol.ts +23 -0
- package/src/workflow/workflow.ts +23 -318
- package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-CIfjkb5H.js} +1 -1
- package/webui/dist/assets/index-BkXhtB6A.css +2 -0
- package/webui/dist/assets/index-DIM2-FUQ.js +114 -0
- package/webui/dist/build.json +10 -6
- 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,6 +8,7 @@ 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";
|
|
@@ -39,7 +40,8 @@ export interface PiRunOptions {
|
|
|
39
40
|
tools?: readonly string[];
|
|
40
41
|
model?: string;
|
|
41
42
|
thinking?: string;
|
|
42
|
-
|
|
43
|
+
/** Names of the skills this agent may load; absent keeps pi's own discovery. */
|
|
44
|
+
skills?: string[];
|
|
43
45
|
signal?: AbortSignal;
|
|
44
46
|
/** Reports the current one-word tool activity while the agent works. */
|
|
45
47
|
onActivity?: (activity: string) => void;
|
|
@@ -53,52 +55,21 @@ export interface PiRunOptions {
|
|
|
53
55
|
stallTimeoutMs?: number;
|
|
54
56
|
/** Silence allowed while one tool call is in flight (tests, builds); 0 disables. */
|
|
55
57
|
toolStallTimeoutMs?: number;
|
|
56
|
-
/** Ask the agent to wrap up and report at this elapsed time; 0 disables. */
|
|
57
|
-
wrapUpAtMs?: number;
|
|
58
58
|
/** Receives the steering handle once the process is running. */
|
|
59
59
|
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
60
|
/** Answers the agent's relayed questions; without it they are cancelled like any other dialog. */
|
|
63
61
|
onAsk?: RelayAsk;
|
|
64
62
|
}
|
|
65
63
|
|
|
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;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
64
|
export interface ProcessOutcome {
|
|
87
65
|
exitCode: number;
|
|
88
66
|
stdout: string;
|
|
89
67
|
stderr: string;
|
|
90
68
|
killed: boolean;
|
|
91
|
-
timedOut: boolean;
|
|
92
69
|
/** Killed by the watchdog after going silent. */
|
|
93
70
|
stalled?: boolean;
|
|
94
|
-
/** The agent was asked to wrap up before its deadline. */
|
|
95
|
-
wrappedUp?: boolean;
|
|
96
71
|
/** The RPC host rejected the prompt itself. */
|
|
97
72
|
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
73
|
}
|
|
103
74
|
|
|
104
75
|
/** Streaming agent events forwarded as they arrive on stdout. */
|
|
@@ -113,14 +84,9 @@ export type PiStreamEvent =
|
|
|
113
84
|
| { type: "compaction" }
|
|
114
85
|
| { type: "compaction_end"; error?: string }
|
|
115
86
|
| { 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
87
|
/** One finished thinking block, bounded to `MAX_THOUGHT_CHARS`. */
|
|
122
88
|
| { type: "thought"; text: string }
|
|
123
|
-
/** It asked the user (relayed);
|
|
89
|
+
/** It asked the user (relayed); the stall watchdog waits until the answers are back. */
|
|
124
90
|
| { type: "asking"; questions: number }
|
|
125
91
|
/** The answers went back after `waitedMs` of waiting on the user. */
|
|
126
92
|
| { type: "answered"; waitedMs: number }
|
|
@@ -132,7 +98,6 @@ export const MAX_THOUGHT_CHARS = 1500;
|
|
|
132
98
|
export interface ProcessRunOptions {
|
|
133
99
|
cwd: string;
|
|
134
100
|
signal?: AbortSignal;
|
|
135
|
-
timeoutMs: number;
|
|
136
101
|
/** The task message sent to the agent over RPC stdin. */
|
|
137
102
|
prompt?: string;
|
|
138
103
|
/** Optional sink for streaming agent events. */
|
|
@@ -140,12 +105,7 @@ export interface ProcessRunOptions {
|
|
|
140
105
|
env?: Record<string, string>;
|
|
141
106
|
stallTimeoutMs?: number;
|
|
142
107
|
toolStallTimeoutMs?: number;
|
|
143
|
-
wrapUpAtMs?: number;
|
|
144
|
-
wrapUpMessage?: string;
|
|
145
108
|
onStart?: (handle: RunHandle) => void;
|
|
146
|
-
/** Grace between the deadline abort and the hard kill. */
|
|
147
|
-
graceMs?: number;
|
|
148
|
-
time?: TimeLimit;
|
|
149
109
|
onAsk?: RelayAsk;
|
|
150
110
|
}
|
|
151
111
|
|
|
@@ -162,9 +122,6 @@ export interface PiRunResult {
|
|
|
162
122
|
usage: { input: number; output: number; cost: number; turns: number };
|
|
163
123
|
model?: string;
|
|
164
124
|
stalled?: boolean;
|
|
165
|
-
wrappedUp?: boolean;
|
|
166
|
-
timeUp?: boolean;
|
|
167
|
-
extendedMs?: number;
|
|
168
125
|
}
|
|
169
126
|
|
|
170
127
|
export interface ParsedStream {
|
|
@@ -179,17 +136,8 @@ const MAX_STREAM_CHARS = 5_000_000;
|
|
|
179
136
|
const HEARTBEAT_MS = 2000;
|
|
180
137
|
const SETTLE_FALLBACK_MS = 3000;
|
|
181
138
|
const EXIT_DRAIN_MS = 1500;
|
|
182
|
-
const DEADLINE_GRACE_MS = 5000;
|
|
183
139
|
const DIALOG_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
184
140
|
|
|
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
141
|
/** RPC protocol lines the process host reacts to (never forwarded to the caller). */
|
|
194
142
|
export interface ControlEvent {
|
|
195
143
|
type: string;
|
|
@@ -368,10 +316,15 @@ export function finishedThought(line: string): string | undefined {
|
|
|
368
316
|
export const MCP_RESOURCE_TOOLS: readonly string[] = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
|
|
369
317
|
|
|
370
318
|
/** 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[] {
|
|
319
|
+
export function buildPiArgs(options: Omit<PiRunOptions, "task"> & { systemPromptFile?: string; skillFiles?: string[] }): string[] {
|
|
372
320
|
const args = ["--mode", "rpc", "--no-session", "--no-prompt-templates", "--no-themes", "--extension", fileURLToPath(new URL("../index.ts", import.meta.url))];
|
|
373
321
|
if (options.model && options.model !== "inherit") args.push("--model", options.model);
|
|
374
322
|
if (options.thinking) args.push("--thinking", options.thinking);
|
|
323
|
+
// A skill grant replaces pi's own discovery with exactly the granted skills.
|
|
324
|
+
if (options.skillFiles) {
|
|
325
|
+
args.push("--no-skills");
|
|
326
|
+
for (const file of options.skillFiles) args.push("--skill", file);
|
|
327
|
+
}
|
|
375
328
|
if (options.tools && options.tools.length > 0) {
|
|
376
329
|
// pi keeps every MCP tool under --tools unless an entry names one, so name the granted ones (and the resource
|
|
377
330
|
// tools their servers need), or run without MCP at all when nothing is granted.
|
|
@@ -504,19 +457,11 @@ function rpcInput(proc: ChildProcess): { send(command: object): void; close(): v
|
|
|
504
457
|
|
|
505
458
|
interface RunState {
|
|
506
459
|
killed: boolean;
|
|
507
|
-
timedOut: boolean;
|
|
508
460
|
stalled: boolean;
|
|
509
|
-
wrappedUp: boolean;
|
|
510
461
|
settled: boolean;
|
|
511
462
|
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. */
|
|
463
|
+
/** Relayed questions waiting on the user: the watchdog waits. */
|
|
518
464
|
asking: number;
|
|
519
|
-
extendedMs: number;
|
|
520
465
|
}
|
|
521
466
|
|
|
522
467
|
/** Smallest positive limit, used to pick a watchdog cadence that can see it. */
|
|
@@ -528,11 +473,11 @@ function watchInterval(...limits: Array<number | undefined>): number {
|
|
|
528
473
|
|
|
529
474
|
/**
|
|
530
475
|
* 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
|
-
*
|
|
476
|
+
* The child leads its own process group so a stall or cancel takes its
|
|
477
|
+
* grandchildren (dev servers, watch-mode tests) with it, and the run ends on
|
|
478
|
+
* process exit rather than waiting for a pipe a grandchild may hold open.
|
|
479
|
+
* There is no time limit: a run ends when the agent finishes, is cancelled,
|
|
480
|
+
* or goes silent for so long it is killed as stalled.
|
|
536
481
|
*/
|
|
537
482
|
export function spawnPiProcess(args: string[], options: ProcessRunOptions): Promise<ProcessOutcome> {
|
|
538
483
|
return new Promise((resolve) => {
|
|
@@ -547,25 +492,16 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
547
492
|
windowsHide: true,
|
|
548
493
|
stdio: ["pipe", "pipe", "pipe"],
|
|
549
494
|
});
|
|
550
|
-
const state: RunState = { killed: false,
|
|
495
|
+
const state: RunState = { killed: false, stalled: false, settled: false, asking: 0 };
|
|
551
496
|
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;
|
|
497
|
+
let lastOutput = Date.now();
|
|
559
498
|
let quietGraceUntil = 0;
|
|
560
499
|
let toolsInFlight = 0;
|
|
561
500
|
let stderr = "";
|
|
562
501
|
let exitCode: number | undefined;
|
|
563
502
|
let settleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
564
503
|
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
504
|
let askStarted = 0;
|
|
568
|
-
let pausedMs = 0;
|
|
569
505
|
const asks = new AbortController();
|
|
570
506
|
|
|
571
507
|
const onEvent = (event: PiStreamEvent) => {
|
|
@@ -574,51 +510,18 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
574
510
|
if (event.type === "retry") quietGraceUntil = Date.now() + event.delayMs;
|
|
575
511
|
options.onEvent?.(event);
|
|
576
512
|
};
|
|
577
|
-
//
|
|
513
|
+
// The run ends once its turn is over.
|
|
578
514
|
let turnDone = false;
|
|
579
515
|
const turnOver = () => {
|
|
580
516
|
if (turnDone) return;
|
|
581
517
|
turnDone = true;
|
|
582
518
|
clearTimeout(settleTimer);
|
|
583
519
|
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
|
-
});
|
|
520
|
+
input.close();
|
|
615
521
|
};
|
|
616
522
|
const relay = (id: string | undefined, questions: AskQuestion[], ask: RelayAsk) => {
|
|
617
523
|
state.asking += 1;
|
|
618
|
-
if (state.asking === 1)
|
|
619
|
-
askStarted = Date.now();
|
|
620
|
-
clearTimeout(deadline);
|
|
621
|
-
}
|
|
524
|
+
if (state.asking === 1) askStarted = Date.now();
|
|
622
525
|
options.onEvent?.({ type: "asking", questions: questions.length });
|
|
623
526
|
ask(questions, asks.signal)
|
|
624
527
|
.catch((): AskResult => ({ answers: [], cancelled: true }))
|
|
@@ -628,11 +531,7 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
628
531
|
if (state.asking === 0) {
|
|
629
532
|
const now = Date.now();
|
|
630
533
|
const waited = now - askStarted;
|
|
631
|
-
pausedMs += waited;
|
|
632
|
-
deadlineAt += waited;
|
|
633
|
-
clockStart += waited;
|
|
634
534
|
lastOutput = now;
|
|
635
|
-
if (!state.deciding) deadline = setTimeout(onDeadline, Math.max(0, deadlineAt - now));
|
|
636
535
|
options.onEvent?.({ type: "answered", waitedMs: waited });
|
|
637
536
|
}
|
|
638
537
|
input.send({ type: "extension_ui_response", id, value: JSON.stringify(result) });
|
|
@@ -675,7 +574,6 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
675
574
|
const finish = (code: number) => {
|
|
676
575
|
if (done) return;
|
|
677
576
|
done = true;
|
|
678
|
-
clearTimeout(deadline);
|
|
679
577
|
clearTimeout(settleTimer);
|
|
680
578
|
clearInterval(watch);
|
|
681
579
|
// Questions still open when the run ends are put away.
|
|
@@ -684,9 +582,8 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
684
582
|
input.close();
|
|
685
583
|
// Reap anything the agent left running in its group.
|
|
686
584
|
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 } : {}) });
|
|
585
|
+
const { killed, stalled, protocolError } = state;
|
|
586
|
+
resolve({ exitCode: code, stdout: collector.finish(), stderr, killed, stalled, protocolError });
|
|
690
587
|
};
|
|
691
588
|
const onAbort = () => {
|
|
692
589
|
state.killed = true;
|
|
@@ -694,31 +591,9 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
694
591
|
input.close();
|
|
695
592
|
terminate(proc, 3000);
|
|
696
593
|
};
|
|
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
594
|
const watch = setInterval(() => {
|
|
706
|
-
if (state.killed || done || state.
|
|
595
|
+
if (state.killed || done || state.asking > 0) return;
|
|
707
596
|
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
597
|
const limit = toolsInFlight > 0 ? options.toolStallTimeoutMs : options.stallTimeoutMs;
|
|
723
598
|
if (limit && limit > 0 && now - Math.max(lastOutput, quietGraceUntil) >= limit) {
|
|
724
599
|
state.killed = true;
|
|
@@ -726,7 +601,7 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
726
601
|
input.close();
|
|
727
602
|
terminate(proc, 3000);
|
|
728
603
|
}
|
|
729
|
-
}, watchInterval(options.stallTimeoutMs, options.toolStallTimeoutMs
|
|
604
|
+
}, watchInterval(options.stallTimeoutMs, options.toolStallTimeoutMs));
|
|
730
605
|
watch.unref();
|
|
731
606
|
|
|
732
607
|
proc.on("exit", (code, signal) => {
|
|
@@ -747,15 +622,11 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
747
622
|
}
|
|
748
623
|
|
|
749
624
|
function toResult(parsed: ParsedStream, outcome: ProcessOutcome, aborted: boolean, options: PiRunOptions): PiRunResult {
|
|
750
|
-
const base = { output: parsed.text, usage: parsed.usage, model: parsed.model
|
|
625
|
+
const base = { output: parsed.text, usage: parsed.usage, model: parsed.model };
|
|
751
626
|
if (outcome.stalled) {
|
|
752
627
|
const quiet = shortDuration(options.stallTimeoutMs ?? 0);
|
|
753
628
|
return { ...base, status: "timeout", stalled: true, error: `stalled: no output for ${quiet}` };
|
|
754
629
|
}
|
|
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
630
|
if (outcome.killed || aborted || parsed.stopReason === "aborted") {
|
|
760
631
|
return { ...base, status: "cancelled", error: "agent was cancelled" };
|
|
761
632
|
}
|
|
@@ -786,23 +657,17 @@ export async function runPiAgent(given: PiRunOptions, run: ProcessRunner = spawn
|
|
|
786
657
|
try {
|
|
787
658
|
prompt = writePromptFile([options.systemPrompt, host.note].filter(Boolean).join("\n\n"));
|
|
788
659
|
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 });
|
|
660
|
+
const args = buildPiArgs({ ...options, tools: host.tools, mcpTools: host.mcpTools, systemPromptFile: prompt.file, ...(options.skills ? { skillFiles: skillFiles(options.skills, options.cwd) } : {}) });
|
|
790
661
|
const outcome = await run(args, {
|
|
791
662
|
cwd: options.cwd,
|
|
792
663
|
signal: options.signal,
|
|
793
|
-
timeoutMs: options.timeoutMs,
|
|
794
664
|
prompt: `Task: ${options.task}`,
|
|
795
665
|
env: host.env,
|
|
796
666
|
stallTimeoutMs: options.stallTimeoutMs,
|
|
797
667
|
toolStallTimeoutMs: options.toolStallTimeoutMs,
|
|
798
|
-
wrapUpAtMs: options.wrapUpAtMs,
|
|
799
|
-
...(options.time ? { time: options.time } : {}),
|
|
800
668
|
...(options.onAsk ? { onAsk: options.onAsk } : {}),
|
|
801
669
|
onStart: host.onStart,
|
|
802
670
|
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
671
|
if (event.type === "tool_execution_start") options.onActivity?.(activityWord(event.toolName));
|
|
807
672
|
options.onEvent?.(event);
|
|
808
673
|
},
|
package/src/index.ts
CHANGED
|
@@ -21,11 +21,14 @@ import { registerFreshContext } from "./pi/fresh-context.ts";
|
|
|
21
21
|
import { registerAskTool } from "./ask/tool.ts";
|
|
22
22
|
import { registerWebTools } from "./web/tools.ts";
|
|
23
23
|
import { registerToolBudget } from "./pi/tool-budget.ts";
|
|
24
|
+
import { bindGrants } from "./pi/grants.ts";
|
|
24
25
|
|
|
25
26
|
export default function (pi: ExtensionAPI): void {
|
|
26
27
|
registerAgentPermissions(pi);
|
|
27
28
|
registerDelegation(pi);
|
|
28
29
|
registerAutoCompaction(pi);
|
|
30
|
+
// Settings lists this session's MCP tools and skills to grant, and agents find their granted skills through it.
|
|
31
|
+
if (!isSubagentProcess()) bindGrants(pi);
|
|
29
32
|
let root = detectProjectRoot(process.cwd(), CONFIG_DIR_NAME);
|
|
30
33
|
pi.on("session_start", (_event, ctx) => { root = detectProjectRoot(ctx.cwd, CONFIG_DIR_NAME); });
|
|
31
34
|
// First, so every session_start handler below finds the classifier bound to this session's keys.
|
package/src/lobby/feed.ts
CHANGED
|
@@ -266,7 +266,7 @@ export class LobbyFeed {
|
|
|
266
266
|
if (run.status !== "running") this.settleThought(key);
|
|
267
267
|
if (run.status !== "running" && before !== run.status) {
|
|
268
268
|
this.settle(undefined, key);
|
|
269
|
-
const kind: ActivityKind = run.status === "success" ?
|
|
269
|
+
const kind: ActivityKind = run.status === "success" ? "success" : run.status === "cancelled" ? "warning" : "error";
|
|
270
270
|
this.log(source, `${run.role} ${run.status === "success" ? "finished" : run.status}${run.error ? ` — ${run.error.split("\n")[0]}` : ""}`, kind, at);
|
|
271
271
|
}
|
|
272
272
|
}
|
package/src/lobby/host.ts
CHANGED
|
@@ -148,6 +148,8 @@ export interface LobbyService {
|
|
|
148
148
|
configChanged?(): void;
|
|
149
149
|
/** The models Pi offers, for the settings page's picker. */
|
|
150
150
|
models?(): ModelChoice[];
|
|
151
|
+
/** The MCP tools and skills pi offers to grant agents, for the settings page. */
|
|
152
|
+
grants?(): { mcpTools: Array<{ name: string; server: string; description: string; readOnly: boolean }>; skills: Array<{ name: string; description: string; path: string }> };
|
|
151
153
|
/** The classifier models pi can call with its logins, for the classifier's picker. */
|
|
152
154
|
classifierModels?(): Array<{ id: string; label: string }>;
|
|
153
155
|
/** The model this window's session runs on (`provider/id`), when it has one. */
|
package/src/lobby/planner.ts
CHANGED
|
@@ -139,7 +139,7 @@ export interface PlannerDeps {
|
|
|
139
139
|
configDir: string;
|
|
140
140
|
/** The oracle chairing the panel (the Planner settings entry). */
|
|
141
141
|
profile: () => QuickFixProfile;
|
|
142
|
-
/** A seat's model, thinking and
|
|
142
|
+
/** A seat's model, thinking and grants; the oracle's profile when absent. */
|
|
143
143
|
memberProfile?: (member: PanelMember) => QuickFixProfile;
|
|
144
144
|
/** Seats on the panel when the session starts; every seat when absent. */
|
|
145
145
|
panel?: readonly PanelMember[];
|
|
@@ -1015,7 +1015,7 @@ export class PlanningSession {
|
|
|
1015
1015
|
if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
|
|
1016
1016
|
this.deps.onChange?.();
|
|
1017
1017
|
},
|
|
1018
|
-
|
|
1018
|
+
...(profile.skills ? { skills: profile.skills } : {}),
|
|
1019
1019
|
signal,
|
|
1020
1020
|
stallTimeoutMs: this.deps.stallTimeoutMs,
|
|
1021
1021
|
toolStallTimeoutMs: this.deps.toolStallTimeoutMs,
|
package/src/lobby/pr-review.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
/**
|
|
3
3
|
* Reviewing pull requests from the Git tab. A read-only agent (on QA's model,
|
|
4
|
-
* thinking and
|
|
4
|
+
* thinking and grants) gets the pull request's description, changed files
|
|
5
5
|
* and diff from `gh`, may read the repository for context, and writes a
|
|
6
6
|
* review: verdict, summary, findings, tests, questions. Jev can read a pull
|
|
7
7
|
* request first, in a moment, and say whether a full review is worth its
|
|
@@ -74,7 +74,7 @@ export interface PullReviewDeps {
|
|
|
74
74
|
root: string;
|
|
75
75
|
configDir: string;
|
|
76
76
|
exec: Exec;
|
|
77
|
-
/** QA's model, thinking and
|
|
77
|
+
/** QA's model, thinking and grants, read as a review starts. */
|
|
78
78
|
profile: () => QuickFixProfile;
|
|
79
79
|
stallTimeoutMs?: number;
|
|
80
80
|
toolStallTimeoutMs?: number;
|
|
@@ -285,7 +285,7 @@ export class PullReviews {
|
|
|
285
285
|
if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
|
|
286
286
|
this.deps.onChange?.();
|
|
287
287
|
},
|
|
288
|
-
|
|
288
|
+
...(profile.skills ? { skills: profile.skills } : {}),
|
|
289
289
|
signal,
|
|
290
290
|
stallTimeoutMs: this.deps.stallTimeoutMs,
|
|
291
291
|
toolStallTimeoutMs: this.deps.toolStallTimeoutMs,
|
package/src/lobby/quickfix.ts
CHANGED
|
@@ -89,9 +89,10 @@ export interface SubmitOptions {
|
|
|
89
89
|
export interface QuickFixProfile {
|
|
90
90
|
model?: string;
|
|
91
91
|
thinking: string;
|
|
92
|
-
timeoutMs: number;
|
|
93
92
|
instructions?: string;
|
|
94
93
|
mcpTools?: string[];
|
|
94
|
+
/** Skill names it may load; absent keeps every skill pi finds. */
|
|
95
|
+
skills?: string[];
|
|
95
96
|
/** False when its settings turn the agent's auto-compaction off. */
|
|
96
97
|
autoCompact?: boolean;
|
|
97
98
|
/** Where the job goes when its model runs out of usage or is unavailable. */
|
|
@@ -101,7 +102,7 @@ export interface QuickFixDeps {
|
|
|
101
102
|
cwd: string;
|
|
102
103
|
root: string;
|
|
103
104
|
configDir: string;
|
|
104
|
-
/** Model, thinking and
|
|
105
|
+
/** Model, thinking and grants for the next job, read from settings at start time. */
|
|
105
106
|
profile: () => QuickFixProfile;
|
|
106
107
|
stallTimeoutMs?: number;
|
|
107
108
|
toolStallTimeoutMs?: number;
|
|
@@ -355,7 +356,7 @@ export class QuickFixQueue {
|
|
|
355
356
|
if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
|
|
356
357
|
this.changed();
|
|
357
358
|
},
|
|
358
|
-
|
|
359
|
+
...(profile.skills ? { skills: profile.skills } : {}),
|
|
359
360
|
signal: controller.signal,
|
|
360
361
|
stallTimeoutMs: this.deps.stallTimeoutMs,
|
|
361
362
|
toolStallTimeoutMs: this.deps.toolStallTimeoutMs,
|
package/src/lobby/recovery.ts
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
17
17
|
import { loadTask, ownedTask, saveTask } from "../state/persistence.ts";
|
|
18
18
|
import { resolvePhaseTiming } from "../state/phase-timing.ts";
|
|
19
|
-
import { updateBudget } from "../state/budget.ts";
|
|
20
19
|
import { clearStaleRuns } from "../state/work-time.ts";
|
|
21
20
|
import { livePresence } from "../state/presence.ts";
|
|
22
21
|
import {
|
|
@@ -59,8 +58,8 @@ const INTERRUPTED_DECISION = "pi stopped unexpectedly; the task carried on in th
|
|
|
59
58
|
|
|
60
59
|
/**
|
|
61
60
|
* Clear what the stopped process left marked as under way on a task: the
|
|
62
|
-
* questions it was waiting on (they no longer wait)
|
|
63
|
-
*
|
|
61
|
+
* questions it was waiting on (they no longer wait) and the runs it noted as
|
|
62
|
+
* active. A decision records
|
|
64
63
|
* the stop, so the task's history says what happened.
|
|
65
64
|
*/
|
|
66
65
|
export function settleInterrupted(root: string, configDir: string, taskId: string, now = new Date(), note = INTERRUPTED_DECISION): void {
|
|
@@ -71,13 +70,6 @@ export function settleInterrupted(root: string, configDir: string, taskId: strin
|
|
|
71
70
|
task.decisions.push({ domain: "master", text: note, createdAt: at });
|
|
72
71
|
task.updatedAt = at;
|
|
73
72
|
saveTask(root, configDir, task);
|
|
74
|
-
updateBudget(root, configDir, taskId, (budget) => {
|
|
75
|
-
for (const allotment of budget.allotments) {
|
|
76
|
-
if (allotment.endedAt) continue;
|
|
77
|
-
allotment.endedAt = at;
|
|
78
|
-
allotment.outcome = "stopped";
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
73
|
clearStaleRuns(root, configDir, taskId);
|
|
82
74
|
}
|
|
83
75
|
|
package/src/lobby/runtime.ts
CHANGED
|
@@ -126,7 +126,7 @@ function handToQuickFix(state: Runtime, request: string, builder: Domain | undef
|
|
|
126
126
|
const job = state.quickfix.submit(request, Date.now(), {
|
|
127
127
|
force: true,
|
|
128
128
|
...(profile ? { profile } : {}),
|
|
129
|
-
note: `The oracle sent your request here: ${reason.replace(/[.\s]+$/, "")}.${builder ? ` A quick feature: it runs on ${builder === "designer" ? "DESIGN" : "DEV"}'s model, thinking and
|
|
129
|
+
note: `The oracle sent your request here: ${reason.replace(/[.\s]+$/, "")}.${builder ? ` A quick feature: it runs on ${builder === "designer" ? "DESIGN" : "DEV"}'s model, thinking and grants.` : ""}`,
|
|
130
130
|
});
|
|
131
131
|
lobbyTopics.bump("quickfix");
|
|
132
132
|
pushNotice("The oracle sent your request to the quick-fix agent.", "info");
|
package/src/lobby/service.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { settleInterrupted, type Relaunch } from "./recovery.ts";
|
|
|
47
47
|
import { carryOnMessage, resumeRoute, RESUMED_DECISION, type Drivers } from "./resume.ts";
|
|
48
48
|
import { archivePlan as archivePlanOnDisk, deletePlan as deletePlanOnDisk, isPlanId, previousPlans, readPlan, type PlanRecord, type PlanSummary } from "../state/plan-history.ts";
|
|
49
49
|
import { applyMasterModel } from "../pi/model-settings.ts";
|
|
50
|
+
import { grantableSkills, grantableTools } from "../pi/grants.ts";
|
|
50
51
|
import { lobbyTopics } from "./topics.ts";
|
|
51
52
|
import type { LobbyAgentKind, PanelMember } from "../schemas/configuration.ts";
|
|
52
53
|
import type { LiveSession, LobbyService, SwitchTarget } from "./host.ts";
|
|
@@ -719,7 +720,7 @@ function deleteTask(state: Runtime, taskId: string, where: "list" | "archive"):
|
|
|
719
720
|
return `deleted ${taskId} for good`;
|
|
720
721
|
}
|
|
721
722
|
|
|
722
|
-
/** What a pull request review runs on: QA's model, thinking and
|
|
723
|
+
/** What a pull request review runs on: QA's model, thinking and grants. */
|
|
723
724
|
export function reviewProfile(state: Runtime) {
|
|
724
725
|
return resolveReviewProfile(loadConfig(), {
|
|
725
726
|
lookup: modelLookup(state.ctx),
|
|
@@ -988,6 +989,7 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
988
989
|
}
|
|
989
990
|
},
|
|
990
991
|
classifierModels: () => availableClassifiers().map((model) => ({ id: model.id, label: model.label })),
|
|
992
|
+
grants: () => ({ mcpTools: grantableTools(state.pi), skills: grantableSkills(state.root, state.pi) }),
|
|
991
993
|
issuesEnabled: () => loadConfig().lobby.issues,
|
|
992
994
|
recovered: () => state.recovered,
|
|
993
995
|
seatLabel: (member) => {
|
package/src/master/goal-check.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
9
9
|
import type { BotLobbyConfig, ProfileResolver } from "../schemas/configuration.ts";
|
|
10
10
|
import { profileFor } from "../schemas/configuration.ts";
|
|
11
|
-
import { runAgent, watchdogOptions
|
|
11
|
+
import { runAgent, watchdogOptions } from "../execution/agent-runner.ts";
|
|
12
12
|
import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
13
13
|
import { loadPrompt } from "../prompts/loader.ts";
|
|
14
14
|
import { truncate } from "../text.ts";
|
|
@@ -33,11 +33,8 @@ export interface GoalCheckRequest {
|
|
|
33
33
|
profile?: ProfileResolver;
|
|
34
34
|
signal?: AbortSignal;
|
|
35
35
|
onUpdate?: (run: AgentRun) => void;
|
|
36
|
-
time?: AgentTime;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
/** At most this long: it reads, it does not test. */
|
|
40
|
-
export const GOAL_CHECK_MS = 6 * 60 * 1000;
|
|
41
38
|
/** Most of the change it reads; a longer diff is cut, and the check reads the files it needs. */
|
|
42
39
|
export const GOAL_DIFF_CHARS = 60_000;
|
|
43
40
|
|
|
@@ -106,8 +103,6 @@ export async function runGoalCheck(request: GoalCheckRequest, run: ProcessRunner
|
|
|
106
103
|
systemPrompt: goalCheckPrompt(profile.instructions),
|
|
107
104
|
...(profile.model ? { model: profile.model } : {}),
|
|
108
105
|
thinking: profile.thinking,
|
|
109
|
-
timeoutMs: Math.min(GOAL_CHECK_MS, profile.timeoutMs),
|
|
110
|
-
...(request.time ? { time: request.time } : {}),
|
|
111
106
|
cwd: request.cwd,
|
|
112
107
|
...(request.signal ? { signal: request.signal } : {}),
|
|
113
108
|
...(request.onUpdate ? { onUpdate: (update: AgentRun) => request.onUpdate!({ ...update, note: update.note ?? "goal check" }) } : {}),
|