@jwilger/pi-development-system 0.48.0 → 0.51.1
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/extensions/development-system.ts +1 -1
- package/package.json +2 -2
- package/skills/strict-lints/SKILL.md +2 -1
- package/src/core/profile.ts +3 -27
- package/src/core/spawn-overrides.ts +2 -2
- package/src/core/test-runner.ts +10 -3
- package/src/review/digest.ts +7 -1
- package/src/state/profile-detect.ts +27 -0
- package/src/subagents/VENDORED.md +1 -1
- package/src/subagents/index.ts +144 -60
- package/src/subagents/orch/inherited-tools.ts +3 -3
- package/src/subagents/orch/mailbox.ts +11 -7
- package/src/subagents/orch/manager.ts +90 -57
- package/src/subagents/orch/runtime.ts +38 -33
- package/src/subagents/orch/tools.ts +12 -10
- package/src/subagents/prefs/agent-import.ts +13 -13
- package/src/subagents/prefs/config.ts +85 -185
- package/src/subagents/prefs/import-discovery.ts +6 -6
- package/src/subagents/prefs/models.ts +6 -10
- package/src/subagents/prefs/settings.ts +38 -38
- package/src/subagents/types.ts +37 -34
- package/src/subagents/ui/agent-navigation-editor.ts +20 -13
- package/src/subagents/ui/dialog.ts +99 -84
- package/src/subagents/ui/import-picker.ts +28 -11
- package/src/subagents/ui/live-agent-view.ts +17 -5
- package/src/subagents/ui/model-picker.ts +26 -17
- package/src/subagents/ui/settings-ui.ts +16 -34
- package/src/subagents/ui/status-ui.ts +140 -51
- package/src/subagents/ui/thread-tree.ts +3 -9
- package/src/subagents/ui/ui.ts +162 -265
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { parseSpawnOverrides } from "../../core/spawn-overrides.ts";
|
|
3
|
+
import { isParseError } from "../../core/types.ts";
|
|
4
|
+
import { DEFAULT_MANAGER_SETTINGS, type ManagerSettings } from "../prefs/settings.ts";
|
|
3
5
|
import type {
|
|
4
6
|
AgentDriver,
|
|
5
7
|
AgentType,
|
|
@@ -13,21 +15,18 @@ import type {
|
|
|
13
15
|
TranscriptObservation,
|
|
14
16
|
TranscriptSnapshot,
|
|
15
17
|
} from "../types.ts";
|
|
16
|
-
import { immutableTranscriptValue } from "./transcript.ts";
|
|
17
|
-
import { parseSpawnOverrides } from "../../core/spawn-overrides.ts";
|
|
18
|
-
import { isParseError } from "../../core/types.ts";
|
|
19
18
|
import { canonicalPath, inheritContext, isDescendant, parentPath } from "./paths.ts";
|
|
20
|
-
import {
|
|
19
|
+
import { immutableTranscriptValue } from "./transcript.ts";
|
|
21
20
|
|
|
22
21
|
interface Record {
|
|
23
22
|
view: ThreadView;
|
|
24
23
|
definition: AgentType;
|
|
25
24
|
inherited: AgentMessage[];
|
|
26
|
-
contextReady?: Promise<void
|
|
25
|
+
contextReady?: Promise<void> | undefined;
|
|
27
26
|
contextPending?: boolean;
|
|
28
27
|
startup: AbortController;
|
|
29
28
|
driver?: AgentDriver;
|
|
30
|
-
initializing?: Promise<AgentDriver
|
|
29
|
+
initializing?: Promise<AgentDriver> | undefined;
|
|
31
30
|
run?: Promise<void>;
|
|
32
31
|
started?: Promise<void>;
|
|
33
32
|
sessionLeafId?: string | null;
|
|
@@ -40,18 +39,26 @@ interface Record {
|
|
|
40
39
|
stopRequested: boolean;
|
|
41
40
|
observationRevision?: number;
|
|
42
41
|
driverGeneration?: number;
|
|
43
|
-
transcriptSource?: TranscriptSnapshot;
|
|
42
|
+
transcriptSource?: TranscriptSnapshot | undefined;
|
|
44
43
|
transcriptListeners?: Set<TranscriptListener>;
|
|
45
|
-
detachTranscript?: () => void;
|
|
46
|
-
observationError?: string;
|
|
44
|
+
detachTranscript?: (() => void) | undefined;
|
|
45
|
+
observationError?: string | undefined;
|
|
47
46
|
}
|
|
48
47
|
const active = (view: Pick<ThreadView, "state">) =>
|
|
49
48
|
view.state === "starting" || view.state === "running";
|
|
49
|
+
const settledState = (record: {
|
|
50
|
+
stopRequested: boolean;
|
|
51
|
+
pauseRequested: boolean;
|
|
52
|
+
}): ThreadView["state"] => {
|
|
53
|
+
if (record.stopRequested) return "stopped";
|
|
54
|
+
return record.pauseRequested ? "paused" : "completed";
|
|
55
|
+
};
|
|
56
|
+
const swallow = (): undefined => undefined;
|
|
50
57
|
const errorText = (error: unknown) => (error instanceof Error ? error.message : String(error));
|
|
51
58
|
|
|
52
59
|
function abortable<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
53
60
|
if (signal.aborted) {
|
|
54
|
-
|
|
61
|
+
work.catch(swallow);
|
|
55
62
|
return Promise.reject(signal.reason);
|
|
56
63
|
}
|
|
57
64
|
return new Promise((resolve, reject) => {
|
|
@@ -75,7 +82,10 @@ export class ThreadManager {
|
|
|
75
82
|
private records = new Map<string, Record>();
|
|
76
83
|
private disposed = false;
|
|
77
84
|
private epoch = 0;
|
|
78
|
-
|
|
85
|
+
private options: ManagerOptions;
|
|
86
|
+
constructor(options: ManagerOptions) {
|
|
87
|
+
this.options = options;
|
|
88
|
+
}
|
|
79
89
|
|
|
80
90
|
/** New limits affect future launches/resumes, never cancel existing work. */
|
|
81
91
|
setLimits(limits: ManagerSettings): void {
|
|
@@ -158,14 +168,14 @@ export class ThreadManager {
|
|
|
158
168
|
return {
|
|
159
169
|
view,
|
|
160
170
|
definition: structuredClone(record.definition),
|
|
161
|
-
...(!view.sessionFile
|
|
171
|
+
...(!(view.sessionFile || record.contextPending)
|
|
162
172
|
? { inherited: structuredClone(record.inherited) }
|
|
163
173
|
: {}),
|
|
164
174
|
};
|
|
165
175
|
});
|
|
166
176
|
}
|
|
167
177
|
restore(saved: SavedThread[]): void {
|
|
168
|
-
if (this.records.size) throw new Error("Restore requires an empty thread registry");
|
|
178
|
+
if (this.records.size > 0) throw new Error("Restore requires an empty thread registry");
|
|
169
179
|
const restored = new Map<string, Record>();
|
|
170
180
|
for (const item of saved) {
|
|
171
181
|
const savedView = structuredClone(item.view);
|
|
@@ -187,12 +197,12 @@ export class ThreadManager {
|
|
|
187
197
|
inputTokens: storedView.inputTokens ?? 0,
|
|
188
198
|
outputTokens: storedView.outputTokens ?? 0,
|
|
189
199
|
};
|
|
190
|
-
|
|
200
|
+
view.startedAt = undefined;
|
|
191
201
|
if (view.parent !== parentPath(path)) throw new Error(`Invalid saved parent for ${path}`);
|
|
192
202
|
if (active(view)) {
|
|
193
203
|
view.state = "paused";
|
|
194
204
|
view.status = "Interrupted by reload; send input to resume";
|
|
195
|
-
|
|
205
|
+
view.output = undefined;
|
|
196
206
|
}
|
|
197
207
|
restored.set(path, {
|
|
198
208
|
view,
|
|
@@ -201,9 +211,7 @@ export class ThreadManager {
|
|
|
201
211
|
// A reservation saved before its lexical parent opened has no snapshot yet.
|
|
202
212
|
contextPending: !view.sessionFile && item.inherited === undefined && view.parent !== null,
|
|
203
213
|
startup: new AbortController(),
|
|
204
|
-
...(Object.
|
|
205
|
-
? { sessionLeafId }
|
|
206
|
-
: {}),
|
|
214
|
+
...(Object.hasOwn(savedView, "sessionLeafId") ? { sessionLeafId } : {}),
|
|
207
215
|
pauseRequested: false,
|
|
208
216
|
stopRequested: false,
|
|
209
217
|
});
|
|
@@ -257,19 +265,15 @@ export class ThreadManager {
|
|
|
257
265
|
: level > maxLevels
|
|
258
266
|
)
|
|
259
267
|
throw new Error("Agent depth limit reached");
|
|
260
|
-
this.archiveFinished(this.options.maxThreads ?? DEFAULT_MANAGER_SETTINGS.maxThreads);
|
|
261
268
|
this.assertCapacity();
|
|
262
269
|
const type = structuredClone(this.options.getType(args.type));
|
|
263
270
|
if (overrides !== undefined) type.spawnOverrides = { ...overrides };
|
|
264
271
|
const parentRecord = parent && parent !== "/root" ? this.record(parent) : undefined;
|
|
265
272
|
this.assertStartableAncestors(path);
|
|
273
|
+
// Last, so a spawn refused by any check above drops nothing.
|
|
274
|
+
this.archiveFinished(this.options.maxThreads ?? DEFAULT_MANAGER_SETTINGS.maxThreads, path);
|
|
266
275
|
// Reservation is synchronous: subtree cancellation sees children even during lazy parent reopen.
|
|
267
|
-
const inherited =
|
|
268
|
-
parent === "/root"
|
|
269
|
-
? inheritContext(this.options.rootSnapshot())
|
|
270
|
-
: parentRecord?.driver
|
|
271
|
-
? inheritContext(parentRecord.driver.snapshot())
|
|
272
|
-
: [];
|
|
276
|
+
const inherited = this.inheritedFor(parent, parentRecord);
|
|
273
277
|
const now = Date.now();
|
|
274
278
|
const record: Record = {
|
|
275
279
|
view: {
|
|
@@ -298,7 +302,9 @@ export class ThreadManager {
|
|
|
298
302
|
this.records.set(path, record);
|
|
299
303
|
if (record.contextPending) this.prepareInheritedContext(record);
|
|
300
304
|
this.start(record, args.task);
|
|
301
|
-
return args.wait === false
|
|
305
|
+
return args.wait === false
|
|
306
|
+
? this.view(record)
|
|
307
|
+
: await this.wait(caller, path, undefined, signal);
|
|
302
308
|
}
|
|
303
309
|
|
|
304
310
|
async steer(caller: string, path: string, message: string): Promise<ThreadView> {
|
|
@@ -321,7 +327,9 @@ export class ThreadManager {
|
|
|
321
327
|
if (record.stopRequested && active(record.view))
|
|
322
328
|
throw new Error("Thread is stopping; wait for stopped state before resuming");
|
|
323
329
|
if (record.view.state === "running") {
|
|
324
|
-
|
|
330
|
+
const live = record.driver;
|
|
331
|
+
if (live === undefined) throw new Error("Running thread has no driver");
|
|
332
|
+
await live.steer(message);
|
|
325
333
|
this.touch(record);
|
|
326
334
|
return this.view(record);
|
|
327
335
|
}
|
|
@@ -351,12 +359,13 @@ export class ThreadManager {
|
|
|
351
359
|
const finish = (error?: Error) => {
|
|
352
360
|
if (timer) clearTimeout(timer);
|
|
353
361
|
signal?.removeEventListener("abort", cancel);
|
|
354
|
-
|
|
362
|
+
if (error) reject(error);
|
|
363
|
+
else resolve();
|
|
355
364
|
};
|
|
356
365
|
const cancel = () => finish(new Error("Waiting cancelled; thread remains running"));
|
|
357
366
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
358
367
|
if (timeoutMs !== undefined) timer = setTimeout(() => finish(), timeoutMs);
|
|
359
|
-
record.run
|
|
368
|
+
(record.run ?? Promise.resolve()).then(
|
|
360
369
|
() => finish(),
|
|
361
370
|
(error) => finish(new Error(errorText(error))),
|
|
362
371
|
);
|
|
@@ -404,7 +413,7 @@ export class ThreadManager {
|
|
|
404
413
|
await item.run;
|
|
405
414
|
item.view.state = "stopped";
|
|
406
415
|
item.view.status = "Stopped; session retained";
|
|
407
|
-
|
|
416
|
+
item.view.output = undefined;
|
|
408
417
|
this.touch(item);
|
|
409
418
|
}),
|
|
410
419
|
);
|
|
@@ -429,14 +438,15 @@ export class ThreadManager {
|
|
|
429
438
|
// Idle/restored history may be lazily loaded, but attachment never prompts/resumes a turn.
|
|
430
439
|
if (!active(record.view)) await this.ensureDriver(record, true);
|
|
431
440
|
this.assertLive();
|
|
432
|
-
const listeners =
|
|
441
|
+
const listeners = record.transcriptListeners ?? new Set<TranscriptListener>();
|
|
442
|
+
record.transcriptListeners = listeners;
|
|
433
443
|
const registered: TranscriptListener = (snapshot) => listener(snapshot);
|
|
434
444
|
listeners.add(registered);
|
|
435
445
|
if (record.driver) this.attachTranscript(record, record.driver);
|
|
436
446
|
else {
|
|
437
447
|
// A starting child gets a placeholder immediately; existing startup supplies the live source.
|
|
438
|
-
|
|
439
|
-
if (this.
|
|
448
|
+
this.ensureDriver(record, true).catch((error) => {
|
|
449
|
+
if (this.isShutDown() || !listeners.has(registered)) return;
|
|
440
450
|
record.observationError = errorText(error);
|
|
441
451
|
this.publishTranscript(record);
|
|
442
452
|
});
|
|
@@ -445,7 +455,7 @@ export class ThreadManager {
|
|
|
445
455
|
snapshot: this.transcriptSnapshot(record),
|
|
446
456
|
unsubscribe: () => {
|
|
447
457
|
listeners.delete(registered);
|
|
448
|
-
if (
|
|
458
|
+
if (listeners.size === 0) {
|
|
449
459
|
record.detachTranscript?.();
|
|
450
460
|
record.detachTranscript = undefined;
|
|
451
461
|
record.transcriptSource = undefined;
|
|
@@ -473,7 +483,7 @@ export class ThreadManager {
|
|
|
473
483
|
await Promise.all(
|
|
474
484
|
records.map(async (record) => {
|
|
475
485
|
await record.driver?.abort();
|
|
476
|
-
await record.initializing?.catch(
|
|
486
|
+
await record.initializing?.catch(swallow);
|
|
477
487
|
await record.run;
|
|
478
488
|
record.driver?.dispose();
|
|
479
489
|
}),
|
|
@@ -488,8 +498,8 @@ export class ThreadManager {
|
|
|
488
498
|
record.view.state = "starting";
|
|
489
499
|
record.view.status = "Starting";
|
|
490
500
|
record.view.startedAt = Date.now();
|
|
491
|
-
|
|
492
|
-
|
|
501
|
+
record.view.output = undefined;
|
|
502
|
+
record.view.error = undefined;
|
|
493
503
|
const epoch = this.epoch;
|
|
494
504
|
let markStarted!: () => void;
|
|
495
505
|
record.started = new Promise((resolve) => {
|
|
@@ -501,7 +511,7 @@ export class ThreadManager {
|
|
|
501
511
|
if (contextReady) await abortable(contextReady, record.startup.signal);
|
|
502
512
|
record.startup.signal.throwIfAborted();
|
|
503
513
|
const driver = await this.ensureDriver(record);
|
|
504
|
-
if (record.stopRequested || this.
|
|
514
|
+
if (record.stopRequested || this.isShutDown()) {
|
|
505
515
|
record.view.state = "stopped";
|
|
506
516
|
return;
|
|
507
517
|
}
|
|
@@ -511,11 +521,7 @@ export class ThreadManager {
|
|
|
511
521
|
this.touch(record);
|
|
512
522
|
markStarted();
|
|
513
523
|
await task;
|
|
514
|
-
record.view.state = record
|
|
515
|
-
? "stopped"
|
|
516
|
-
: record.pauseRequested
|
|
517
|
-
? "paused"
|
|
518
|
-
: "completed";
|
|
524
|
+
record.view.state = settledState(record);
|
|
519
525
|
if (record.view.state === "completed") {
|
|
520
526
|
record.view.output = driver.output();
|
|
521
527
|
record.view.status = "Completed; session retained";
|
|
@@ -531,7 +537,7 @@ export class ThreadManager {
|
|
|
531
537
|
record.liveOutputTokens = 0;
|
|
532
538
|
markStarted(); // Failed/cancelled initialization must also release callers waiting to steer.
|
|
533
539
|
this.touch(record);
|
|
534
|
-
if (!this.
|
|
540
|
+
if (!this.isShutDown() && epoch === this.epoch)
|
|
535
541
|
this.options.onEvent?.({
|
|
536
542
|
kind: "settled",
|
|
537
543
|
thread: this.view(record),
|
|
@@ -586,7 +592,7 @@ export class ThreadManager {
|
|
|
586
592
|
return this.options.createDriver(options);
|
|
587
593
|
})
|
|
588
594
|
.then(async (driver) => {
|
|
589
|
-
if (this.
|
|
595
|
+
if (this.isShutDown() || signal.aborted) {
|
|
590
596
|
await driver.abort();
|
|
591
597
|
driver.dispose();
|
|
592
598
|
throw new Error("Driver startup cancelled");
|
|
@@ -615,7 +621,8 @@ export class ThreadManager {
|
|
|
615
621
|
private attachTranscript(record: Record, driver: AgentDriver): void {
|
|
616
622
|
if (record.detachTranscript || !record.transcriptListeners?.size) return;
|
|
617
623
|
const observation = driver.observeTranscript?.((snapshot) => {
|
|
618
|
-
if (this.
|
|
624
|
+
if (this.isShutDown() || record.driver !== driver || !record.transcriptListeners?.size)
|
|
625
|
+
return;
|
|
619
626
|
record.transcriptSource = snapshot;
|
|
620
627
|
this.publishTranscript(record);
|
|
621
628
|
});
|
|
@@ -641,7 +648,7 @@ export class ThreadManager {
|
|
|
641
648
|
});
|
|
642
649
|
}
|
|
643
650
|
private publishTranscript(record: Record): void {
|
|
644
|
-
if (this.
|
|
651
|
+
if (this.isShutDown() || !record.transcriptListeners?.size) return;
|
|
645
652
|
record.observationRevision = (record.observationRevision ?? 0) + 1;
|
|
646
653
|
const snapshot = this.transcriptSnapshot(record);
|
|
647
654
|
for (const listener of record.transcriptListeners) {
|
|
@@ -655,7 +662,7 @@ export class ThreadManager {
|
|
|
655
662
|
private touch(record: Record): void {
|
|
656
663
|
record.view.updatedAt = Date.now();
|
|
657
664
|
this.publishTranscript(record);
|
|
658
|
-
if (!this.
|
|
665
|
+
if (!this.isShutDown()) this.options.onEvent?.({ kind: "change", thread: this.view(record) });
|
|
659
666
|
}
|
|
660
667
|
/** Fold driver-cumulative usage into persisted totals. Partials only refresh the live view. */
|
|
661
668
|
private applyUsage(
|
|
@@ -669,7 +676,8 @@ export class ThreadManager {
|
|
|
669
676
|
if (input === record.liveInputTokens && output === record.liveOutputTokens) return;
|
|
670
677
|
record.liveInputTokens = input;
|
|
671
678
|
record.liveOutputTokens = output;
|
|
672
|
-
if (!this.
|
|
679
|
+
if (!this.isShutDown())
|
|
680
|
+
this.options.onEvent?.({ kind: "metrics", thread: this.view(record) });
|
|
673
681
|
return;
|
|
674
682
|
}
|
|
675
683
|
record.view.inputTokens =
|
|
@@ -685,7 +693,7 @@ export class ThreadManager {
|
|
|
685
693
|
const startedAt = record.view.startedAt;
|
|
686
694
|
if (startedAt === undefined) return;
|
|
687
695
|
record.view.elapsedMs = (record.view.elapsedMs ?? 0) + Math.max(0, Date.now() - startedAt);
|
|
688
|
-
|
|
696
|
+
record.view.startedAt = undefined;
|
|
689
697
|
}
|
|
690
698
|
private prepareInheritedContext(record: Record, readOnly = false): Promise<void> | undefined {
|
|
691
699
|
if (!record.contextPending) return record.contextReady;
|
|
@@ -709,7 +717,7 @@ export class ThreadManager {
|
|
|
709
717
|
throw error;
|
|
710
718
|
});
|
|
711
719
|
record.contextReady = ready;
|
|
712
|
-
|
|
720
|
+
ready.catch(swallow);
|
|
713
721
|
return ready;
|
|
714
722
|
}
|
|
715
723
|
private sessionLeafId(record: Record): string | null | undefined {
|
|
@@ -730,7 +738,7 @@ export class ThreadManager {
|
|
|
730
738
|
sessionFile: record.driver ? record.driver.sessionFile : record.view.sessionFile,
|
|
731
739
|
});
|
|
732
740
|
// devsys: show per-spawn pins so agent_status reports what the thread was told to run on.
|
|
733
|
-
if (record.definition
|
|
741
|
+
if (record.definition.spawnOverrides) view.pinned = { ...record.definition.spawnOverrides };
|
|
734
742
|
if (record.liveInputTokens) view.inputTokens = (view.inputTokens ?? 0) + record.liveInputTokens;
|
|
735
743
|
if (record.liveOutputTokens)
|
|
736
744
|
view.outputTokens = (view.outputTokens ?? 0) + record.liveOutputTokens;
|
|
@@ -741,8 +749,16 @@ export class ThreadManager {
|
|
|
741
749
|
if (!record) throw new Error(`Unknown thread ${path}`);
|
|
742
750
|
return record;
|
|
743
751
|
}
|
|
752
|
+
private inheritedFor(parent: string | null, parentRecord: Record | undefined): AgentMessage[] {
|
|
753
|
+
if (parent === "/root") return inheritContext(this.options.rootSnapshot());
|
|
754
|
+
return parentRecord?.driver ? inheritContext(parentRecord.driver.snapshot()) : [];
|
|
755
|
+
}
|
|
756
|
+
/** A method, not a field read: the flag flips across awaits, which static narrowing cannot see. */
|
|
757
|
+
private isShutDown(): boolean {
|
|
758
|
+
return this.disposed;
|
|
759
|
+
}
|
|
744
760
|
private assertLive(): void {
|
|
745
|
-
if (this.
|
|
761
|
+
if (this.isShutDown()) throw new Error("Thread manager has shut down");
|
|
746
762
|
}
|
|
747
763
|
private assertCallerMaySteer(caller: string): void {
|
|
748
764
|
if (caller !== "/root" && this.record(caller).stopRequested)
|
|
@@ -765,16 +781,33 @@ export class ThreadManager {
|
|
|
765
781
|
* on their own history. Make room for one more thread by dropping the oldest finished
|
|
766
782
|
* thread that has no retained descendant; live and paused threads are never dropped.
|
|
767
783
|
* Throws only when every retained thread is still live, paused or a parent of one.
|
|
784
|
+
* `spawning` is the path about to be created: its ancestors are kept (a finished parent
|
|
785
|
+
* with no other child is still the new thread's parent).
|
|
768
786
|
*/
|
|
769
|
-
private archiveFinished(limit: number): void {
|
|
787
|
+
private archiveFinished(limit: number, spawning: string): void {
|
|
770
788
|
while (this.records.size >= limit) {
|
|
771
789
|
const paths = [...this.records.keys()];
|
|
772
790
|
const victim = paths.find((path) => {
|
|
773
|
-
const
|
|
791
|
+
const held = this.records.get(path);
|
|
792
|
+
const state = held?.view.state;
|
|
774
793
|
const finished = state === "completed" || state === "failed" || state === "stopped";
|
|
775
|
-
|
|
794
|
+
// A driver still being created would land on a dropped record and never be disposed.
|
|
795
|
+
const opening =
|
|
796
|
+
held?.driver === undefined &&
|
|
797
|
+
(held?.initializing !== undefined || held?.contextReady !== undefined);
|
|
798
|
+
return (
|
|
799
|
+
finished &&
|
|
800
|
+
!opening &&
|
|
801
|
+
!isDescendant(spawning, path) &&
|
|
802
|
+
!paths.some((other) => isDescendant(other, path))
|
|
803
|
+
);
|
|
776
804
|
});
|
|
777
805
|
if (victim === undefined) throw new Error("Total thread limit reached");
|
|
806
|
+
const record = this.records.get(victim);
|
|
807
|
+
// Release what the record holds: shutdown() only walks retained records.
|
|
808
|
+
record?.detachTranscript?.();
|
|
809
|
+
record?.transcriptListeners?.clear();
|
|
810
|
+
record?.driver?.dispose();
|
|
778
811
|
this.records.delete(victim);
|
|
779
812
|
}
|
|
780
813
|
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
//
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, realpath } from "node:fs/promises";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
4
6
|
import { clampThinkingLevel } from "@earendil-works/pi-ai/compat";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
|
-
import { mkdir, readFile, realpath } from "node:fs/promises";
|
|
7
7
|
import {
|
|
8
8
|
createAgentSession,
|
|
9
9
|
createCodemodeExtension,
|
|
10
10
|
createToolSearchExtension,
|
|
11
11
|
DefaultResourceLoader,
|
|
12
|
+
type ExtensionContext,
|
|
12
13
|
getAgentDir,
|
|
13
14
|
ModelRuntime,
|
|
14
15
|
SessionManager,
|
|
15
16
|
SettingsManager,
|
|
16
|
-
type ExtensionContext,
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import {
|
|
19
|
-
BOOTSTRAP_MESSAGE,
|
|
20
|
-
buildBootstrapMessage,
|
|
21
|
-
buildQueuedUserMessage,
|
|
22
|
-
buildUpdateDetails,
|
|
23
|
-
DurableMailbox,
|
|
24
|
-
} from "./mailbox.ts";
|
|
25
|
-
import { selectTools } from "../prefs/config.ts";
|
|
26
|
-
import { TranscriptChannel } from "./transcript.ts";
|
|
27
|
-
import type { InheritedToolSource } from "./inherited-tools.ts";
|
|
28
|
-
import type { ModelSelectionMode, ToolFilteringMode } from "../prefs/settings.ts";
|
|
29
18
|
import { applySpawnOverrides, modelSource } from "../../core/spawn-overrides.ts";
|
|
19
|
+
import { selectTools } from "../prefs/config.ts";
|
|
30
20
|
import {
|
|
31
|
-
modelIdentity,
|
|
32
21
|
getModelPreferences,
|
|
33
|
-
selectPreferredModel,
|
|
34
22
|
ModelPreferenceError,
|
|
23
|
+
modelIdentity,
|
|
24
|
+
selectPreferredModel,
|
|
35
25
|
} from "../prefs/models.ts";
|
|
26
|
+
import type { ModelSelectionMode, ToolFilteringMode } from "../prefs/settings.ts";
|
|
36
27
|
import {
|
|
37
|
-
THINKING_LEVELS,
|
|
38
28
|
type AgentType,
|
|
39
|
-
type ResolvedAgentSettings,
|
|
40
29
|
type DriverFactory,
|
|
41
30
|
type DriverOptions,
|
|
31
|
+
type ResolvedAgentSettings,
|
|
32
|
+
THINKING_LEVELS,
|
|
42
33
|
type ThinkingLevel,
|
|
43
34
|
} from "../types.ts";
|
|
35
|
+
import type { InheritedToolSource } from "./inherited-tools.ts";
|
|
36
|
+
import {
|
|
37
|
+
BOOTSTRAP_MESSAGE,
|
|
38
|
+
buildBootstrapMessage,
|
|
39
|
+
buildQueuedUserMessage,
|
|
40
|
+
buildUpdateDetails,
|
|
41
|
+
DurableMailbox,
|
|
42
|
+
} from "./mailbox.ts";
|
|
43
|
+
import { TranscriptChannel } from "./transcript.ts";
|
|
44
44
|
|
|
45
45
|
const BUILTINS = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"];
|
|
46
46
|
/** Persisted before transcript messages so older-leaf restores still carry ownership. */
|
|
@@ -157,7 +157,7 @@ export function createDriverFactory(
|
|
|
157
157
|
type: AgentType,
|
|
158
158
|
parentPath?: string | null,
|
|
159
159
|
restored?: ReturnType<SessionManager["buildSessionContext"]>,
|
|
160
|
-
): { provider?: string; id?: string; thinkingLevel: ThinkingLevel };
|
|
160
|
+
): { provider?: string | undefined; id?: string | undefined; thinkingLevel: ThinkingLevel };
|
|
161
161
|
} {
|
|
162
162
|
// Keep resolved settings even after disposal: descendants inherit settings, not the caller's history.
|
|
163
163
|
const resolved = new Map<string, { provider: string; id: string; thinking: ThinkingLevel }>();
|
|
@@ -226,9 +226,9 @@ export function createDriverFactory(
|
|
|
226
226
|
const candidates = filtering
|
|
227
227
|
? availableScopedModels(normalizeScopedModels(ctx.scopedModels))
|
|
228
228
|
: availableModelCandidates();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
));
|
|
229
|
+
const preferred = selectTypeModelPreference(type, candidates, filtering);
|
|
230
|
+
if (preferred === undefined) throw new Error("No preferred model is available");
|
|
231
|
+
({ provider, id } = parseModelIdentity(preferred));
|
|
232
232
|
} else if (source === "restored" && restored?.model) {
|
|
233
233
|
provider = restored.model.provider;
|
|
234
234
|
id = restored.model.modelId;
|
|
@@ -252,7 +252,7 @@ export function createDriverFactory(
|
|
|
252
252
|
const { provider, id, thinkingLevel } = resolveInitialSettings(type, parentPath);
|
|
253
253
|
const model =
|
|
254
254
|
normalizeScopedModels(ctx.scopedModels).find(
|
|
255
|
-
(
|
|
255
|
+
(scoped) => scoped.model.provider === provider && scoped.model.id === id,
|
|
256
256
|
)?.model ?? (provider && id ? ctx.modelRegistry.find(provider, id) : undefined);
|
|
257
257
|
if (!model)
|
|
258
258
|
throw new Error(
|
|
@@ -381,14 +381,15 @@ export function createDriverFactory(
|
|
|
381
381
|
modelId: string | undefined,
|
|
382
382
|
scopedModels: readonly ScopedModel[],
|
|
383
383
|
) => {
|
|
384
|
-
if (!providerId
|
|
384
|
+
if (!(providerId && modelId))
|
|
385
385
|
throw new Error(
|
|
386
386
|
`Subagent model ${providerId ?? "(unset)"}/${modelId ?? "(unset)"} is unavailable; select a physical model in the root or agent type`,
|
|
387
387
|
);
|
|
388
388
|
await mirrorRuntimeAuth(providerId);
|
|
389
389
|
const sourceModel =
|
|
390
|
-
scopedModels.find(
|
|
391
|
-
|
|
390
|
+
scopedModels.find(
|
|
391
|
+
(scoped) => scoped.model.provider === providerId && scoped.model.id === modelId,
|
|
392
|
+
)?.model ?? ctx.modelRegistry.find(providerId, modelId);
|
|
392
393
|
if (sourceModel?.api === "pi-virtual") {
|
|
393
394
|
throw new Error(
|
|
394
395
|
`Virtual model ${providerId}/${modelId} cannot be reproduced through the public registry API. Set this agent type's model to a physical provider/model-id.`,
|
|
@@ -414,13 +415,16 @@ export function createDriverFactory(
|
|
|
414
415
|
const rootActive = new Set(inherited.activeNames);
|
|
415
416
|
// Preserve deferred/codemode exposure rather than flooding model declarations with MCP tools.
|
|
416
417
|
const activeToolNames = toolNames.filter(
|
|
417
|
-
(name) =>
|
|
418
|
+
(name) =>
|
|
419
|
+
localNames.has(name) ||
|
|
420
|
+
rootActive.has(name) ||
|
|
421
|
+
(options.type.tools?.allow?.includes(name) && toolFiltering === "allowed"),
|
|
418
422
|
);
|
|
419
423
|
const customTools = [...options.tools, ...externalTools].filter(
|
|
420
424
|
(tool) => allowed.has(tool.name) && tool.name !== "codemode" && tool.name !== "tool_search",
|
|
421
425
|
);
|
|
422
426
|
const settingsManager = SettingsManager.inMemory({ cacheWarming: "off" });
|
|
423
|
-
let parkQueue = () =>
|
|
427
|
+
let parkQueue: () => void = () => undefined;
|
|
424
428
|
const pauseBoundary = () => {
|
|
425
429
|
if (!options.shouldPause()) return undefined;
|
|
426
430
|
parkQueue();
|
|
@@ -558,7 +562,7 @@ export function createDriverFactory(
|
|
|
558
562
|
throw error;
|
|
559
563
|
}
|
|
560
564
|
const streamFunction = session.agent.streamFunction;
|
|
561
|
-
session.agent.streamFunction =
|
|
565
|
+
session.agent.streamFunction = (requestModel, context, streamOptions) => {
|
|
562
566
|
assertScopedRequestAuthorized(
|
|
563
567
|
modelIdentity(requestModel),
|
|
564
568
|
normalizeScopedModels(getRootContext().scopedModels),
|
|
@@ -579,7 +583,7 @@ export function createDriverFactory(
|
|
|
579
583
|
sessionManager.appendMessage(buildBootstrapMessage());
|
|
580
584
|
emit("activity", BOOTSTRAP_MESSAGE);
|
|
581
585
|
};
|
|
582
|
-
const queueAccepted =
|
|
586
|
+
const queueAccepted = (kind: "steer" | "followUp", content: string): Promise<void> => {
|
|
583
587
|
assertCurrentModelAuthorized();
|
|
584
588
|
ensurePersistedSession();
|
|
585
589
|
const accepted = mailbox.accept(kind, content);
|
|
@@ -587,6 +591,7 @@ export function createDriverFactory(
|
|
|
587
591
|
if (kind === "followUp") session.agent.followUp(buildQueuedUserMessage(accepted));
|
|
588
592
|
else session.agent.steer(buildQueuedUserMessage(accepted));
|
|
589
593
|
mailbox.markEnqueued(accepted.id);
|
|
594
|
+
return Promise.resolve();
|
|
590
595
|
};
|
|
591
596
|
const resumePending = async () => {
|
|
592
597
|
for (const accepted of mailbox.replayablePending()) {
|
|
@@ -641,7 +646,7 @@ export function createDriverFactory(
|
|
|
641
646
|
const usage = message.usage;
|
|
642
647
|
const input = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
643
648
|
const output = usage.output;
|
|
644
|
-
if (!Number.isFinite(input)
|
|
649
|
+
if (!(Number.isFinite(input) && Number.isFinite(output))) return;
|
|
645
650
|
return { input, output, reported: input + output + usage.totalTokens > 0 };
|
|
646
651
|
};
|
|
647
652
|
|
|
@@ -650,7 +655,7 @@ export function createDriverFactory(
|
|
|
650
655
|
.find((entry) => entry.type === "custom" && entry.customType === THREAD_OWNERSHIP_TYPE);
|
|
651
656
|
const inheritedMetadata =
|
|
652
657
|
ownership?.type === "custom"
|
|
653
|
-
? (ownership.data as { inheritedCount?: unknown })
|
|
658
|
+
? (ownership.data as { inheritedCount?: unknown }).inheritedCount
|
|
654
659
|
: undefined;
|
|
655
660
|
const inheritedCount =
|
|
656
661
|
typeof inheritedMetadata === "number" &&
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
|
-
import { Type } from "typebox";
|
|
3
1
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import
|
|
2
|
+
import { Type } from "typebox";
|
|
5
3
|
import { ModelPreferenceError } from "../prefs/models.ts";
|
|
6
4
|
import type { AgentType, ResolvedAgentSettings, ThreadView } from "../types.ts";
|
|
5
|
+
import type { ThreadManager } from "./manager.ts";
|
|
7
6
|
|
|
8
7
|
const path = Type.String({ description: "Absolute agent path, or name relative to the caller" });
|
|
9
8
|
const text = Type.String({ minLength: 1 });
|
|
@@ -30,8 +29,10 @@ export function agentTools(
|
|
|
30
29
|
const make = (
|
|
31
30
|
name: string,
|
|
32
31
|
description: string,
|
|
32
|
+
// biome-ignore-start lint/suspicious/noExplicitAny: the host validates params against the TypeBox schema before execute runs, and each handler reads only fields that schema declares
|
|
33
33
|
parameters: any,
|
|
34
34
|
execute: (params: any, signal?: AbortSignal) => Promise<any> | any,
|
|
35
|
+
// biome-ignore-end lint/suspicious/noExplicitAny: end of the schema-validated boundary
|
|
35
36
|
): ToolDefinition => ({
|
|
36
37
|
name,
|
|
37
38
|
label: name,
|
|
@@ -53,12 +54,7 @@ export function agentTools(
|
|
|
53
54
|
return {
|
|
54
55
|
name,
|
|
55
56
|
description,
|
|
56
|
-
error:
|
|
57
|
-
error instanceof ModelPreferenceError
|
|
58
|
-
? error.summary
|
|
59
|
-
: error instanceof Error
|
|
60
|
-
? error.message
|
|
61
|
-
: String(error),
|
|
57
|
+
error: describeError(error),
|
|
62
58
|
};
|
|
63
59
|
}
|
|
64
60
|
});
|
|
@@ -99,7 +95,8 @@ export function agentTools(
|
|
|
99
95
|
),
|
|
100
96
|
thinkingLevel: Type.Optional(
|
|
101
97
|
Type.String({
|
|
102
|
-
description:
|
|
98
|
+
description:
|
|
99
|
+
"Pin this child's thinking level: off, minimal, low, medium, high, xhigh or max.",
|
|
103
100
|
}),
|
|
104
101
|
),
|
|
105
102
|
}),
|
|
@@ -175,3 +172,8 @@ export function agentTools(
|
|
|
175
172
|
),
|
|
176
173
|
];
|
|
177
174
|
}
|
|
175
|
+
|
|
176
|
+
function describeError(error: unknown): string {
|
|
177
|
+
if (error instanceof ModelPreferenceError) return error.summary;
|
|
178
|
+
return error instanceof Error ? error.message : String(error);
|
|
179
|
+
}
|