pi-fast-subagent 0.9.2 → 0.9.4
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 +0 -8
- package/index.ts +5 -119
- package/package.json +1 -1
- package/render.ts +67 -8
- package/runner.ts +36 -99
package/README.md
CHANGED
|
@@ -272,14 +272,6 @@ Cancel a specific job directly:
|
|
|
272
272
|
/fast-subagent:bg-cancel sa_ab12cd34
|
|
273
273
|
```
|
|
274
274
|
|
|
275
|
-
### `/fast-subagent:debug-model`
|
|
276
|
-
|
|
277
|
-
Show cached/inferred main-model state used for model inheritance troubleshooting.
|
|
278
|
-
|
|
279
|
-
```
|
|
280
|
-
/fast-subagent:debug-model
|
|
281
|
-
```
|
|
282
|
-
|
|
283
275
|
## Keyboard Shortcuts
|
|
284
276
|
|
|
285
277
|
| Shortcut | Action |
|
package/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "./format.js";
|
|
28
28
|
import { defaultLoaderPool } from "./loader-pool.js";
|
|
29
29
|
import { renderSubagentCall, renderSubagentResult } from "./render.js";
|
|
30
|
-
import {
|
|
30
|
+
import { mapConcurrent, runAgent } from "./runner.js";
|
|
31
31
|
import { SubagentParams } from "./schemas.js";
|
|
32
32
|
import type { AgentRowStatus, OnUpdate, RunResult, SubagentDetails, ToolCallEntry } from "./types.js";
|
|
33
33
|
|
|
@@ -36,9 +36,6 @@ import type { AgentRowStatus, OnUpdate, RunResult, SubagentDetails, ToolCallEntr
|
|
|
36
36
|
let _bgManager: BackgroundJobManager | null = null;
|
|
37
37
|
let _onBgJobComplete: ((job: BackgroundSubagentJob) => void) | null = null;
|
|
38
38
|
let _setBgStatus: ((text: string | undefined) => void) | null = null;
|
|
39
|
-
let _currentMainModel: string | undefined;
|
|
40
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
-
let _currentMainModelObject: any | undefined;
|
|
42
39
|
|
|
43
40
|
function getBgManager(): BackgroundJobManager {
|
|
44
41
|
if (!_bgManager) _bgManager = new BackgroundJobManager({
|
|
@@ -52,53 +49,6 @@ function refreshBgStatus(): void {
|
|
|
52
49
|
_setBgStatus?.(running.length > 0 ? `⧗ ${running.length} bg agent${running.length > 1 ? "s" : ""}` : undefined);
|
|
53
50
|
}
|
|
54
51
|
|
|
55
|
-
function rememberMainModel(model: ExtensionContext["model"]): void {
|
|
56
|
-
if (!model) return;
|
|
57
|
-
_currentMainModel = `${model.provider}/${model.id}`;
|
|
58
|
-
_currentMainModelObject = model;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function rememberMainModelReference(modelRef: string | undefined, ctx: ExtensionContext): void {
|
|
62
|
-
if (!modelRef) return;
|
|
63
|
-
_currentMainModel = modelRef;
|
|
64
|
-
_currentMainModelObject = resolveModelObject(ctx.modelRegistry, modelRef) ?? _currentMainModelObject;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function rememberPayloadModel(payload: unknown, ctx: ExtensionContext): void {
|
|
68
|
-
const modelId = typeof payload === "object" && payload && "model" in payload && typeof (payload as { model?: unknown }).model === "string"
|
|
69
|
-
? (payload as { model: string }).model
|
|
70
|
-
: undefined;
|
|
71
|
-
if (!modelId) return;
|
|
72
|
-
|
|
73
|
-
const lower = modelId.toLowerCase();
|
|
74
|
-
const matches = ctx.modelRegistry.getAll().filter((m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower);
|
|
75
|
-
if (matches.length === 1) rememberMainModel(matches[0]);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isMainSessionEvent(): boolean {
|
|
79
|
-
return !process.env[DEPTH_ENV];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function rememberLatestSessionModel(ctx: ExtensionContext): void {
|
|
83
|
-
const scan = (entries: ReturnType<ExtensionContext["sessionManager"]["getEntries"]>): boolean => {
|
|
84
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
85
|
-
const e = entries[i]!;
|
|
86
|
-
if (e.type === "model_change") {
|
|
87
|
-
rememberMainModelReference(`${(e as any).provider}/${(e as any).modelId}`, ctx);
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
};
|
|
93
|
-
if (!scan(ctx.sessionManager.getEntries())) scan(ctx.sessionManager.getBranch());
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function getInheritedMainModel(ctx: ExtensionContext): { modelRef?: string; deps: { modelRegistry: ExtensionContext["modelRegistry"]; modelObject?: unknown } } {
|
|
97
|
-
rememberMainModel(ctx.model);
|
|
98
|
-
if (!_currentMainModelObject && _currentMainModel) rememberMainModelReference(_currentMainModel, ctx);
|
|
99
|
-
if (_currentMainModelObject) return { modelRef: _currentMainModel, deps: { modelRegistry: ctx.modelRegistry, modelObject: _currentMainModelObject } };
|
|
100
|
-
return { modelRef: _currentMainModel, deps: { modelRegistry: ctx.modelRegistry } };
|
|
101
|
-
}
|
|
102
52
|
|
|
103
53
|
// ─── Foreground detach registry ─────────────────────────────────────────────
|
|
104
54
|
|
|
@@ -135,28 +85,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
135
85
|
);
|
|
136
86
|
};
|
|
137
87
|
|
|
138
|
-
pi.on("model_select", async (event) => {
|
|
139
|
-
if (!isMainSessionEvent()) return;
|
|
140
|
-
rememberMainModel(event.model);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
pi.on("before_provider_request", async (event, ctx) => {
|
|
144
|
-
if (!isMainSessionEvent()) return;
|
|
145
|
-
if (ctx.model) rememberMainModel(ctx.model);
|
|
146
|
-
else rememberPayloadModel(event.payload, ctx);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
pi.on("turn_start", async (_event, ctx) => {
|
|
150
|
-
if (!isMainSessionEvent()) return;
|
|
151
|
-
rememberMainModel(ctx.model);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
88
|
pi.on("session_start", async (_event, ctx) => {
|
|
155
89
|
_setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
|
|
156
|
-
if (!isMainSessionEvent()) return;
|
|
157
|
-
// Seed _currentMainModel from ctx.model or session entries.
|
|
158
|
-
if (ctx.model) rememberMainModel(ctx.model);
|
|
159
|
-
else rememberLatestSessionModel(ctx);
|
|
160
90
|
|
|
161
91
|
// Warm one extension-capable loader after startup so first `tools: all`
|
|
162
92
|
// subagent call reuses loaded extensions instead of blocking.
|
|
@@ -171,8 +101,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
171
101
|
getBgManager().shutdown();
|
|
172
102
|
_bgManager = null;
|
|
173
103
|
_setBgStatus = null;
|
|
174
|
-
_currentMainModel = undefined;
|
|
175
|
-
_currentMainModelObject = undefined;
|
|
176
104
|
defaultLoaderPool.clear();
|
|
177
105
|
});
|
|
178
106
|
|
|
@@ -197,30 +125,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
197
125
|
},
|
|
198
126
|
});
|
|
199
127
|
|
|
200
|
-
// ─── /fast-subagent:debug-model ──────────────────────────────────────────
|
|
201
|
-
pi.registerCommand("fast-subagent:debug-model", {
|
|
202
|
-
description: "Debug: show cached main model and session entries.",
|
|
203
|
-
async handler(_args, ctx) {
|
|
204
|
-
const entries = ctx.sessionManager.getEntries();
|
|
205
|
-
const modelEntries = entries.filter((e) => e.type === "model_change");
|
|
206
|
-
const branch = ctx.sessionManager.getBranch();
|
|
207
|
-
const branchModelEntries = branch.filter((e) => e.type === "model_change");
|
|
208
|
-
if (!_currentMainModelObject && _currentMainModel) rememberMainModelReference(_currentMainModel, ctx);
|
|
209
|
-
const registryMatch = resolveModelObject(ctx.modelRegistry, _currentMainModel);
|
|
210
|
-
const lines = [
|
|
211
|
-
`_currentMainModel: ${_currentMainModel ?? "(undefined)"}`,
|
|
212
|
-
`_currentMainModelObject: ${_currentMainModelObject ? `${_currentMainModelObject.provider}/${_currentMainModelObject.id}` : "(undefined)"}`,
|
|
213
|
-
`ctx.model: ${ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "(undefined)"}`,
|
|
214
|
-
`resolveModelObject(_currentMainModel): ${registryMatch ? `${registryMatch.provider}/${registryMatch.id}` : "(undefined)"}`,
|
|
215
|
-
`getEntries() total: ${entries.length}`,
|
|
216
|
-
`getEntries() model_change: ${modelEntries.map((e) => `${(e as any).provider}/${(e as any).modelId}`).join(", ") || "none"}`,
|
|
217
|
-
`getBranch() total: ${branch.length}`,
|
|
218
|
-
`getBranch() model_change: ${branchModelEntries.map((e) => `${(e as any).provider}/${(e as any).modelId}`).join(", ") || "none"}`,
|
|
219
|
-
];
|
|
220
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
221
|
-
},
|
|
222
|
-
});
|
|
223
|
-
|
|
224
128
|
// ─── /fast-subagent:agent ─────────────────────────────────────────────────
|
|
225
129
|
pi.registerCommand("fast-subagent:agent", {
|
|
226
130
|
description: "List available subagents. Usage: /fast-subagent:agent [name] — show details for a specific agent.",
|
|
@@ -514,26 +418,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
514
418
|
const { agent, error } = findAgent(params.agent);
|
|
515
419
|
if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
|
|
516
420
|
|
|
517
|
-
|
|
518
|
-
const shouldInherit = !params.model && (!agent.model || agent.model === "inherit");
|
|
519
|
-
let inheritedModel: string | undefined;
|
|
520
|
-
let inheritDeps: { modelRegistry: ExtensionContext["modelRegistry"]; modelObject?: unknown } = { modelRegistry: ctx.modelRegistry };
|
|
521
|
-
if (shouldInherit) {
|
|
522
|
-
let inherited = getInheritedMainModel(ctx);
|
|
523
|
-
if (!inherited.modelRef) {
|
|
524
|
-
rememberLatestSessionModel(ctx);
|
|
525
|
-
inherited = getInheritedMainModel(ctx);
|
|
526
|
-
}
|
|
527
|
-
inheritedModel = inherited.modelRef;
|
|
528
|
-
inheritDeps = inherited.deps;
|
|
529
|
-
}
|
|
530
|
-
const effectiveModel = params.model ?? (shouldInherit ? inheritedModel : (agent.model === "inherit" ? undefined : agent.model));
|
|
421
|
+
const effectiveModel = params.model;
|
|
531
422
|
|
|
532
423
|
if (params.background) {
|
|
533
424
|
const bgAbort = new AbortController();
|
|
534
425
|
const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
|
|
535
426
|
const resultPromise: Promise<BackgroundJobResult> = runAgent(
|
|
536
|
-
agent, params.task, cwd, effectiveModel, bgAbort.signal, undefined,
|
|
427
|
+
agent, params.task, cwd, effectiveModel, bgAbort.signal, undefined,
|
|
537
428
|
).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
|
|
538
429
|
const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
|
|
539
430
|
return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
|
|
@@ -553,7 +444,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
553
444
|
: undefined;
|
|
554
445
|
|
|
555
446
|
const agentRunPromise: Promise<RunResult> = runAgent(
|
|
556
|
-
agent, params.task, cwd, effectiveModel, agentAbort.signal, wrappedOnUpdate,
|
|
447
|
+
agent, params.task, cwd, effectiveModel, agentAbort.signal, wrappedOnUpdate,
|
|
557
448
|
);
|
|
558
449
|
|
|
559
450
|
const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
|
|
@@ -641,7 +532,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
641
532
|
|
|
642
533
|
emitParallel(true);
|
|
643
534
|
|
|
644
|
-
const parentDepth = getCurrentDepth();
|
|
645
535
|
const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
|
|
646
536
|
parallelAgents[i]!.status = "running";
|
|
647
537
|
emitParallel(true);
|
|
@@ -658,17 +548,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
658
548
|
parallelAgents[i]!.responseText = (partial.content?.[0] as any)?.text || parallelAgents[i]!.responseText;
|
|
659
549
|
emitParallel(true);
|
|
660
550
|
};
|
|
661
|
-
const shouldInherit = !t.model && (!agent.model || agent.model === "inherit");
|
|
662
|
-
const inherited = shouldInherit ? getInheritedMainModel(ctx) : undefined;
|
|
663
551
|
const result = await runAgent(
|
|
664
552
|
agent,
|
|
665
553
|
t.task,
|
|
666
554
|
t.cwd ?? cwd,
|
|
667
|
-
t.model
|
|
555
|
+
t.model,
|
|
668
556
|
signal,
|
|
669
557
|
agentOnUpdate,
|
|
670
|
-
parentDepth,
|
|
671
|
-
inherited?.deps ?? { modelRegistry: ctx.modelRegistry },
|
|
672
558
|
);
|
|
673
559
|
parallelAgents[i]!.status = result.exitCode === 0 ? "done" : "error";
|
|
674
560
|
parallelAgents[i]!.durMs = Date.now() - agentStart;
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -302,6 +302,16 @@ export function renderSubagentResult(
|
|
|
302
302
|
promptSkipped?: number;
|
|
303
303
|
responseLines?: string[];
|
|
304
304
|
skipped?: number;
|
|
305
|
+
expandedWidth?: number;
|
|
306
|
+
expandedEventsLen?: number;
|
|
307
|
+
expandedLastEventTs?: number;
|
|
308
|
+
expandedTask?: string;
|
|
309
|
+
expandedAgentName?: string;
|
|
310
|
+
expandedToolCallsLen?: number;
|
|
311
|
+
expandedAgentTextLen?: number;
|
|
312
|
+
expandedBodyLines?: string[];
|
|
313
|
+
expandedFooterKey?: string;
|
|
314
|
+
expandedOutputLines?: string[];
|
|
305
315
|
} = {};
|
|
306
316
|
|
|
307
317
|
function renderExpandedChronological(width: number): string[] {
|
|
@@ -386,7 +396,19 @@ export function renderSubagentResult(
|
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
return {
|
|
389
|
-
invalidate() {
|
|
399
|
+
invalidate() {
|
|
400
|
+
cache.width = undefined;
|
|
401
|
+
cache.expandedWidth = undefined;
|
|
402
|
+
cache.expandedEventsLen = undefined;
|
|
403
|
+
cache.expandedLastEventTs = undefined;
|
|
404
|
+
cache.expandedTask = undefined;
|
|
405
|
+
cache.expandedAgentName = undefined;
|
|
406
|
+
cache.expandedToolCallsLen = undefined;
|
|
407
|
+
cache.expandedAgentTextLen = undefined;
|
|
408
|
+
cache.expandedBodyLines = undefined;
|
|
409
|
+
cache.expandedFooterKey = undefined;
|
|
410
|
+
cache.expandedOutputLines = undefined;
|
|
411
|
+
},
|
|
390
412
|
render(width: number): string[] {
|
|
391
413
|
const out: string[] = [];
|
|
392
414
|
const indent = " ";
|
|
@@ -394,15 +416,52 @@ export function renderSubagentResult(
|
|
|
394
416
|
theme.fg("muted", `${indent}… (${count} more line${count === 1 ? "" : "s"})`);
|
|
395
417
|
|
|
396
418
|
if (expanded) {
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
419
|
+
const events = details.executionEvents || [];
|
|
420
|
+
const lastEventTs = events.length ? events[events.length - 1]!.timestamp : undefined;
|
|
421
|
+
const taskKey = details.task ?? "";
|
|
422
|
+
const agentKey = details.agentName ?? "";
|
|
423
|
+
const bodyCacheHit =
|
|
424
|
+
cache.expandedWidth === width
|
|
425
|
+
&& cache.expandedEventsLen === events.length
|
|
426
|
+
&& cache.expandedLastEventTs === lastEventTs
|
|
427
|
+
&& cache.expandedTask === taskKey
|
|
428
|
+
&& cache.expandedAgentName === agentKey
|
|
429
|
+
&& cache.expandedToolCallsLen === toolCalls.length
|
|
430
|
+
&& cache.expandedAgentTextLen === agentText.length
|
|
431
|
+
&& Array.isArray(cache.expandedBodyLines);
|
|
432
|
+
|
|
433
|
+
let bodyLines: string[];
|
|
434
|
+
if (bodyCacheHit) {
|
|
435
|
+
bodyLines = cache.expandedBodyLines!;
|
|
436
|
+
} else {
|
|
437
|
+
bodyLines = renderExpandedChronological(width);
|
|
438
|
+
cache.expandedWidth = width;
|
|
439
|
+
cache.expandedEventsLen = events.length;
|
|
440
|
+
cache.expandedLastEventTs = lastEventTs;
|
|
441
|
+
cache.expandedTask = taskKey;
|
|
442
|
+
cache.expandedAgentName = agentKey;
|
|
443
|
+
cache.expandedToolCallsLen = toolCalls.length;
|
|
444
|
+
cache.expandedAgentTextLen = agentText.length;
|
|
445
|
+
cache.expandedBodyLines = bodyLines;
|
|
446
|
+
cache.expandedFooterKey = undefined;
|
|
447
|
+
cache.expandedOutputLines = undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
400
450
|
const status = statusLine();
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
451
|
+
const bgHint = details.running && !details.backgroundJobId
|
|
452
|
+
? truncateToWidth(theme.fg("dim", "Ctrl+Shift+B: move to background"), width, "...")
|
|
453
|
+
: "";
|
|
454
|
+
const footerKey = `${status}__${bgHint}`;
|
|
455
|
+
|
|
456
|
+
if (cache.expandedFooterKey !== footerKey || !Array.isArray(cache.expandedOutputLines)) {
|
|
457
|
+
const expandedOut = [...bodyLines, ""];
|
|
458
|
+
if (status) expandedOut.push(truncateToWidth(status, width, "..."));
|
|
459
|
+
if (bgHint) expandedOut.push(bgHint);
|
|
460
|
+
cache.expandedFooterKey = footerKey;
|
|
461
|
+
cache.expandedOutputLines = expandedOut;
|
|
404
462
|
}
|
|
405
|
-
|
|
463
|
+
|
|
464
|
+
return cache.expandedOutputLines!;
|
|
406
465
|
}
|
|
407
466
|
|
|
408
467
|
// Collapsed view
|
package/runner.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* via `onUpdate`, and enforces the per-agent `maxDepth` gate on nested calls.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
|
+
|
|
8
10
|
import {
|
|
9
11
|
AuthStorage,
|
|
10
12
|
createAgentSession,
|
|
@@ -32,8 +34,6 @@ export function getAuth() {
|
|
|
32
34
|
// ─── Depth gating ────────────────────────────────────────────────────────────
|
|
33
35
|
|
|
34
36
|
export const DEFAULT_MAX_DEPTH = 0;
|
|
35
|
-
export const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
|
|
36
|
-
export const MAX_DEPTH_ENV = "PI_FAST_SUBAGENT_MAX_DEPTH";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Pure helper: given the current nesting depth and the allowed max depth,
|
|
@@ -53,75 +53,28 @@ export function checkDepthGate(depth: number, maxDepth: number): { allowed: bool
|
|
|
53
53
|
return { allowed: true };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/** Read-only accessors for the current in-flight depth (used by parallel mode). */
|
|
61
|
-
export function getCurrentDepth(): number {
|
|
62
|
-
return _currentDepth;
|
|
56
|
+
export interface DepthState {
|
|
57
|
+
depth: number;
|
|
58
|
+
maxDepth: number;
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
const TOP_LEVEL_DEPTH: DepthState = { depth: 0, maxDepth: DEFAULT_MAX_DEPTH };
|
|
62
|
+
const _depthContext = new AsyncLocalStorage<DepthState>();
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/** Pass a Model object directly to bypass registry lookup (used for model: inherit). */
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
-
modelObject?: any;
|
|
64
|
+
/** Read the depth/maxDepth in the current async context, or top-level defaults if none. */
|
|
65
|
+
export function getDepthState(): DepthState {
|
|
66
|
+
return _depthContext.getStore() ?? TOP_LEVEL_DEPTH;
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
|
|
76
|
-
function
|
|
77
|
-
return
|
|
69
|
+
/** Run `fn` with `state` set as the current async-scoped depth context. */
|
|
70
|
+
export function runWithDepth<T>(state: DepthState, fn: () => Promise<T>): Promise<T> {
|
|
71
|
+
return _depthContext.run(state, fn);
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* can run ad-hoc provider model IDs (for example via `--model
|
|
85
|
-
* anthropic/claude-sonnet-4-6`) by cloning provider config from a known model.
|
|
86
|
-
* Do same here so `model: inherit` can use main session's exact model ID even
|
|
87
|
-
* before pi's bundled registry knows it.
|
|
88
|
-
*/
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
-
export function resolveModelObject(modelRegistry: ModelRegistry, modelReference: string | undefined): any | undefined {
|
|
91
|
-
const ref = modelReference?.trim();
|
|
92
|
-
if (!ref) return undefined;
|
|
93
|
-
|
|
94
|
-
const allModels = modelRegistry.getAll();
|
|
95
|
-
const lowerRef = ref.toLowerCase();
|
|
96
|
-
const exact = allModels.find((m) => modelRef(m).toLowerCase() === lowerRef || m.id.toLowerCase() === lowerRef);
|
|
97
|
-
if (exact) return exact;
|
|
98
|
-
|
|
99
|
-
const slash = ref.indexOf("/");
|
|
100
|
-
if (slash === -1) return undefined;
|
|
101
|
-
|
|
102
|
-
const providerRef = ref.slice(0, slash).trim();
|
|
103
|
-
const modelId = ref.slice(slash + 1).trim();
|
|
104
|
-
if (!providerRef || !modelId) return undefined;
|
|
105
|
-
|
|
106
|
-
const providerModels = allModels.filter((m) => m.provider.toLowerCase() === providerRef.toLowerCase());
|
|
107
|
-
if (providerModels.length === 0) return undefined;
|
|
108
|
-
const provider = providerModels[0]!.provider;
|
|
109
|
-
|
|
110
|
-
const found = modelRegistry.find(provider, modelId);
|
|
111
|
-
if (found) return found;
|
|
112
|
-
|
|
113
|
-
// Unknown model ID for known provider: clone closest provider model so auth,
|
|
114
|
-
// API, baseUrl, headers, compat, cost shape, and token defaults survive.
|
|
115
|
-
const idLower = modelId.toLowerCase();
|
|
116
|
-
const family = ["opus", "sonnet", "haiku", "gpt", "gemini", "kimi", "glm", "grok"].find((token) => idLower.includes(token));
|
|
117
|
-
const familyModels = family ? providerModels.filter((m) => m.id.toLowerCase().includes(family)) : [];
|
|
118
|
-
const base = (familyModels[0] ?? providerModels[0])!;
|
|
119
|
-
return {
|
|
120
|
-
...base,
|
|
121
|
-
provider,
|
|
122
|
-
id: modelId,
|
|
123
|
-
name: modelId,
|
|
124
|
-
};
|
|
74
|
+
// ─── runAgent ────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export interface RunAgentDeps {
|
|
77
|
+
loaderPool?: LoaderPool;
|
|
125
78
|
}
|
|
126
79
|
|
|
127
80
|
export async function runAgent(
|
|
@@ -131,12 +84,11 @@ export async function runAgent(
|
|
|
131
84
|
modelOverride: string | undefined,
|
|
132
85
|
signal: AbortSignal | undefined,
|
|
133
86
|
onUpdate: OnUpdate | undefined,
|
|
134
|
-
parentDepth?: number,
|
|
135
87
|
deps: RunAgentDeps = {},
|
|
136
88
|
): Promise<RunResult> {
|
|
137
89
|
const pool = deps.loaderPool ?? defaultLoaderPool;
|
|
138
|
-
const depth
|
|
139
|
-
const gate = checkDepthGate(depth,
|
|
90
|
+
const { depth, maxDepth } = getDepthState();
|
|
91
|
+
const gate = checkDepthGate(depth, maxDepth);
|
|
140
92
|
if (!gate.allowed) {
|
|
141
93
|
return {
|
|
142
94
|
output: "",
|
|
@@ -148,8 +100,7 @@ export async function runAgent(
|
|
|
148
100
|
}
|
|
149
101
|
|
|
150
102
|
const bootStartedAt = Date.now();
|
|
151
|
-
const { authStorage, modelRegistry
|
|
152
|
-
const modelRegistry = deps.modelRegistry ?? defaultRegistry;
|
|
103
|
+
const { authStorage, modelRegistry } = getAuth();
|
|
153
104
|
const agentDir = getAgentDir();
|
|
154
105
|
const noExtensions = !agentNeedsExtensions(agent.tools);
|
|
155
106
|
const coldLoader = !pool.isWarm(cwd, agentDir, noExtensions);
|
|
@@ -171,8 +122,6 @@ export async function runAgent(
|
|
|
171
122
|
});
|
|
172
123
|
await allowUiPaint(coldLoader);
|
|
173
124
|
|
|
174
|
-
const createPrevEnvDepth = process.env[DEPTH_ENV];
|
|
175
|
-
process.env[DEPTH_ENV] = String(depth + 1);
|
|
176
125
|
const loaderLease = await pool.acquire(
|
|
177
126
|
cwd,
|
|
178
127
|
agentDir,
|
|
@@ -189,13 +138,10 @@ export async function runAgent(
|
|
|
189
138
|
authStorage,
|
|
190
139
|
modelRegistry,
|
|
191
140
|
resourceLoader: loaderLease.loader,
|
|
192
|
-
...(deps.modelObject ? { model: deps.modelObject } : {}),
|
|
193
141
|
});
|
|
194
142
|
session = created.session;
|
|
195
143
|
} catch (e) {
|
|
196
144
|
loaderLease.release();
|
|
197
|
-
if (createPrevEnvDepth === undefined) delete process.env[DEPTH_ENV];
|
|
198
|
-
else process.env[DEPTH_ENV] = createPrevEnvDepth;
|
|
199
145
|
return {
|
|
200
146
|
output: "",
|
|
201
147
|
exitCode: 1,
|
|
@@ -204,14 +150,17 @@ export async function runAgent(
|
|
|
204
150
|
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
205
151
|
};
|
|
206
152
|
}
|
|
207
|
-
if (createPrevEnvDepth === undefined) delete process.env[DEPTH_ENV];
|
|
208
|
-
else process.env[DEPTH_ENV] = createPrevEnvDepth;
|
|
209
153
|
|
|
210
|
-
// Resolve and apply model
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
154
|
+
// Resolve and apply model
|
|
155
|
+
const modelStr = modelOverride ?? agent.model;
|
|
156
|
+
if (modelStr) {
|
|
157
|
+
const [provider, ...rest] = modelStr.split("/");
|
|
158
|
+
const modelId = rest.join("/");
|
|
159
|
+
if (provider && modelId) {
|
|
160
|
+
const model = modelRegistry.find(provider, modelId);
|
|
161
|
+
if (model) await session.setModel(model);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
215
164
|
|
|
216
165
|
// Apply tools allowlist.
|
|
217
166
|
// "all" → no restriction (everything registered stays active)
|
|
@@ -361,17 +310,11 @@ export async function runAgent(
|
|
|
361
310
|
});
|
|
362
311
|
});
|
|
363
312
|
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const prevMaxDepth = _currentMaxDepth;
|
|
370
|
-
const maxDepth = Math.max(DEFAULT_MAX_DEPTH, agent.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
371
|
-
_currentDepth = depth + 1;
|
|
372
|
-
_currentMaxDepth = depth + maxDepth;
|
|
373
|
-
process.env[DEPTH_ENV] = String(_currentDepth);
|
|
374
|
-
process.env[MAX_DEPTH_ENV] = String(_currentMaxDepth);
|
|
313
|
+
// Per-agent maxDepth; defaults to 0 so subagents can't spawn subagents unless
|
|
314
|
+
// their frontmatter opts in. AsyncLocalStorage scopes this to the nested
|
|
315
|
+
// call, so overlapping parallel agents don't trample each other's state.
|
|
316
|
+
const agentMaxDepth = Math.max(DEFAULT_MAX_DEPTH, agent.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
317
|
+
const childState: DepthState = { depth: depth + 1, maxDepth: depth + agentMaxDepth };
|
|
375
318
|
|
|
376
319
|
let exitCode = 0;
|
|
377
320
|
let error: string | undefined;
|
|
@@ -382,7 +325,7 @@ export async function runAgent(
|
|
|
382
325
|
const onAbort = () => void session.abort();
|
|
383
326
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
384
327
|
try {
|
|
385
|
-
await session.prompt(task);
|
|
328
|
+
await runWithDepth(childState, () => session.prompt(task));
|
|
386
329
|
} finally {
|
|
387
330
|
signal?.removeEventListener("abort", onAbort);
|
|
388
331
|
}
|
|
@@ -395,12 +338,6 @@ export async function runAgent(
|
|
|
395
338
|
unsubscribe();
|
|
396
339
|
session.dispose();
|
|
397
340
|
loaderLease.release();
|
|
398
|
-
if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
|
|
399
|
-
else process.env[DEPTH_ENV] = prevEnvDepth;
|
|
400
|
-
if (prevEnvMaxDepth === undefined) delete process.env[MAX_DEPTH_ENV];
|
|
401
|
-
else process.env[MAX_DEPTH_ENV] = prevEnvMaxDepth;
|
|
402
|
-
_currentDepth = prevDepth;
|
|
403
|
-
_currentMaxDepth = prevMaxDepth;
|
|
404
341
|
}
|
|
405
342
|
|
|
406
343
|
return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, executionEvents, usage };
|