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 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 { DEPTH_ENV, getCurrentDepth, mapConcurrent, resolveModelObject, runAgent } from "./runner.js";
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
- // Determine if this agent should inherit the main session's model.
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, undefined, inheritDeps,
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, undefined, inheritDeps,
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 ?? inherited?.modelRef,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-subagent",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "In-process subagent delegation for pi with single, parallel, and background modes",
5
5
  "type": "module",
6
6
  "keywords": [
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() { cache.width = undefined; },
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
- // Expanded: render chronologically from events
398
- const expandedOut = renderExpandedChronological(width);
399
- expandedOut.push("");
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
- if (status) expandedOut.push(truncateToWidth(status, width, "..."));
402
- if (details.running && !details.backgroundJobId) {
403
- expandedOut.push(truncateToWidth(theme.fg("dim", "Ctrl+Shift+B: move to background"), width, "..."));
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
- return expandedOut;
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
- // Module-level depth counters for nested in-process subagent calls.
57
- let _currentDepth = 0;
58
- let _currentMaxDepth = DEFAULT_MAX_DEPTH;
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
- // ─── runAgent ────────────────────────────────────────────────────────────────
61
+ const TOP_LEVEL_DEPTH: DepthState = { depth: 0, maxDepth: DEFAULT_MAX_DEPTH };
62
+ const _depthContext = new AsyncLocalStorage<DepthState>();
66
63
 
67
- export interface RunAgentDeps {
68
- loaderPool?: LoaderPool;
69
- modelRegistry?: ModelRegistry;
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- function modelRef(model: any): string {
77
- return `${model.provider}/${model.id}`;
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
- * Resolve provider/modelId into a Model object.
82
- *
83
- * `ModelRegistry.find()` only returns bundled/custom registry entries. Pi itself
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 = parentDepth ?? _currentDepth;
139
- const gate = checkDepthGate(depth, _currentMaxDepth);
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: defaultRegistry } = getAuth();
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. Force this even when createAgentSession received
211
- // modelObject so restore/default fallback cannot silently win.
212
- const modelStr = modelOverride ?? (agent.model === "inherit" ? undefined : agent.model);
213
- const model = deps.modelObject ?? resolveModelObject(modelRegistry, modelStr);
214
- if (model) await session.setModel(model);
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
- // Propagate depth to nested calls. `maxDepth` is per-agent and defaults to 0,
365
- // so subagents cannot spawn subagents unless their frontmatter opts in.
366
- const prevEnvDepth = process.env[DEPTH_ENV];
367
- const prevEnvMaxDepth = process.env[MAX_DEPTH_ENV];
368
- const prevDepth = _currentDepth;
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 };