pi-fast-subagent 0.9.0 → 0.9.2

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
@@ -1,6 +1,6 @@
1
1
  # pi-fast-subagent
2
2
 
3
- In-process subagent delegation for [pi](https://github.com/badlogic/pi-mono).
3
+ In-process subagent delegation for [pi](https://github.com/badlogic/pi-mono) with max visibility.
4
4
 
5
5
  Runs subagents with `createAgentSession()` in same process instead of spawning `pi` subprocesses. This removes subprocess cold-start and reuses pi auth/model registry.
6
6
 
@@ -14,6 +14,7 @@ Runs subagents with `createAgentSession()` in same process instead of spawning `
14
14
  - User + project agent discovery
15
15
  - Project agents override user agents
16
16
  - Max nesting depth guard
17
+ - Streamed prompt preview while the parent LLM is writing the subagent task
17
18
  - Chronological expanded view (Ctrl+O): subagent tool calls and response text interleaved in execution order
18
19
  - Collapsed view shows response + trailing tool calls as an indented tree
19
20
 
@@ -271,6 +272,14 @@ Cancel a specific job directly:
271
272
  /fast-subagent:bg-cancel sa_ab12cd34
272
273
  ```
273
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
+
274
283
  ## Keyboard Shortcuts
275
284
 
276
285
  | Shortcut | Action |
@@ -288,7 +297,7 @@ Goal: keep this extension **small and focused** — aligned with pi's philosophy
288
297
 
289
298
  - Async/background isolation not supported in-process
290
299
  - Git worktree isolation not supported
291
- - Nested subagent depth limited to 2 by default
300
+ - Nested subagent spawning disabled by default (`maxDepth: 0`); opt in per agent via frontmatter
292
301
 
293
302
  ## Tool Reference
294
303
 
package/agents/scout.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: scout
3
3
  description: Explores codebases, maps structure, traces data flow, answers how things work across many files
4
- model: anthropic/claude-haiku-4-5
4
+ model: openai-codex/gpt-5.4-mini
5
5
 
6
6
  # tools: which tools this agent can use.
7
7
  # (omit) → all tools: builtins + every parent extension (default)
@@ -36,5 +36,6 @@ Output style:
36
36
  - use sections
37
37
  - include file paths
38
38
  - include short bullets
39
- - mention notable patterns, risks, and coupling
40
- - do not propose code changes unless asked
39
+ - describe what code does, not whether it is good or bad
40
+ - do not rate, review, or analyze quality
41
+ - do not propose code changes
package/index.ts CHANGED
@@ -26,8 +26,8 @@ import {
26
26
  summarizeTask,
27
27
  } from "./format.js";
28
28
  import { defaultLoaderPool } from "./loader-pool.js";
29
- import { renderSubagentResult } from "./render.js";
30
- import { getCurrentDepth, mapConcurrent, runAgent } from "./runner.js";
29
+ import { renderSubagentCall, renderSubagentResult } from "./render.js";
30
+ import { DEPTH_ENV, getCurrentDepth, mapConcurrent, resolveModelObject, 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,6 +36,9 @@ 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;
39
42
 
40
43
  function getBgManager(): BackgroundJobManager {
41
44
  if (!_bgManager) _bgManager = new BackgroundJobManager({
@@ -49,6 +52,54 @@ function refreshBgStatus(): void {
49
52
  _setBgStatus?.(running.length > 0 ? `⧗ ${running.length} bg agent${running.length > 1 ? "s" : ""}` : undefined);
50
53
  }
51
54
 
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
  // ─── Foreground detach registry ─────────────────────────────────────────────
53
104
 
54
105
  interface ForegroundDetachEntry {
@@ -84,8 +135,28 @@ export default function (pi: ExtensionAPI) {
84
135
  );
85
136
  };
86
137
 
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
+
87
154
  pi.on("session_start", async (_event, ctx) => {
88
155
  _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);
89
160
 
90
161
  // Warm one extension-capable loader after startup so first `tools: all`
91
162
  // subagent call reuses loaded extensions instead of blocking.
@@ -100,6 +171,8 @@ export default function (pi: ExtensionAPI) {
100
171
  getBgManager().shutdown();
101
172
  _bgManager = null;
102
173
  _setBgStatus = null;
174
+ _currentMainModel = undefined;
175
+ _currentMainModelObject = undefined;
103
176
  defaultLoaderPool.clear();
104
177
  });
105
178
 
@@ -124,6 +197,30 @@ export default function (pi: ExtensionAPI) {
124
197
  },
125
198
  });
126
199
 
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
+
127
224
  // ─── /fast-subagent:agent ─────────────────────────────────────────────────
128
225
  pi.registerCommand("fast-subagent:agent", {
129
226
  description: "List available subagents. Usage: /fast-subagent:agent [name] — show details for a specific agent.",
@@ -317,6 +414,10 @@ export default function (pi: ExtensionAPI) {
317
414
  ].join(" "),
318
415
  parameters: SubagentParams,
319
416
 
417
+ renderCall(args, theme, context) {
418
+ return renderSubagentCall(args, theme, context);
419
+ },
420
+
320
421
  renderResult(result: AgentToolResult<unknown>, opts: ToolRenderResultOptions, theme: Theme) {
321
422
  return renderSubagentResult(result, opts, theme);
322
423
  },
@@ -413,11 +514,26 @@ export default function (pi: ExtensionAPI) {
413
514
  const { agent, error } = findAgent(params.agent);
414
515
  if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
415
516
 
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));
531
+
416
532
  if (params.background) {
417
533
  const bgAbort = new AbortController();
418
534
  const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
419
535
  const resultPromise: Promise<BackgroundJobResult> = runAgent(
420
- agent, params.task, cwd, params.model, bgAbort.signal, undefined,
536
+ agent, params.task, cwd, effectiveModel, bgAbort.signal, undefined, undefined, inheritDeps,
421
537
  ).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
422
538
  const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
423
539
  return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
@@ -437,7 +553,7 @@ export default function (pi: ExtensionAPI) {
437
553
  : undefined;
438
554
 
439
555
  const agentRunPromise: Promise<RunResult> = runAgent(
440
- agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
556
+ agent, params.task, cwd, effectiveModel, agentAbort.signal, wrappedOnUpdate, undefined, inheritDeps,
441
557
  );
442
558
 
443
559
  const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
@@ -542,7 +658,18 @@ export default function (pi: ExtensionAPI) {
542
658
  parallelAgents[i]!.responseText = (partial.content?.[0] as any)?.text || parallelAgents[i]!.responseText;
543
659
  emitParallel(true);
544
660
  };
545
- const result = await runAgent(agent, t.task, t.cwd ?? cwd, t.model, signal, agentOnUpdate, parentDepth);
661
+ const shouldInherit = !t.model && (!agent.model || agent.model === "inherit");
662
+ const inherited = shouldInherit ? getInheritedMainModel(ctx) : undefined;
663
+ const result = await runAgent(
664
+ agent,
665
+ t.task,
666
+ t.cwd ?? cwd,
667
+ t.model ?? inherited?.modelRef,
668
+ signal,
669
+ agentOnUpdate,
670
+ parentDepth,
671
+ inherited?.deps ?? { modelRegistry: ctx.modelRegistry },
672
+ );
546
673
  parallelAgents[i]!.status = result.exitCode === 0 ? "done" : "error";
547
674
  parallelAgents[i]!.durMs = Date.now() - agentStart;
548
675
  parallelAgents[i]!.toolCalls = result.toolCalls;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-subagent",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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
@@ -8,6 +8,7 @@ import { join } from "node:path";
8
8
  import { getAgentDir, Theme, truncateToVisualLines, keyHint } from "@mariozechner/pi-coding-agent";
9
9
  import type { AgentToolResult, ToolRenderResultOptions } from "@mariozechner/pi-coding-agent";
10
10
  import { truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
11
+ import type { Component } from "@mariozechner/pi-tui";
11
12
 
12
13
  import { formatDuration, formatUsage } from "./format.js";
13
14
  import type { SubagentDetails, ToolCallEntry } from "./types.js";
@@ -73,6 +74,116 @@ function readPreviewSettings(): { previewLines: number; promptPreviewLines: numb
73
74
  return _settingsCache;
74
75
  }
75
76
 
77
+ interface SubagentCallArgs {
78
+ agent?: unknown;
79
+ task?: unknown;
80
+ tasks?: unknown;
81
+ action?: unknown;
82
+ background?: unknown;
83
+ }
84
+
85
+ interface SubagentRenderCallContext {
86
+ state: Record<string, unknown>;
87
+ executionStarted: boolean;
88
+ argsComplete: boolean;
89
+ }
90
+
91
+ function asString(v: unknown): string | undefined {
92
+ return typeof v === "string" ? v : undefined;
93
+ }
94
+
95
+ function taskPreviewLines(task: string, width: number, maxLines: number): { lines: string[]; skipped: number } {
96
+ const innerWidth = Math.max(1, width - 2);
97
+ const visual: string[] = [];
98
+ for (const raw of task.split("\n")) {
99
+ try {
100
+ for (const w of wrapTextWithAnsi(raw, innerWidth)) visual.push(w);
101
+ } catch {
102
+ visual.push(truncateToWidth(raw, innerWidth, "..."));
103
+ }
104
+ }
105
+ const lines = visual.slice(0, maxLines).map((line) => truncateToWidth(` ${line}`, width, "..."));
106
+ return { lines, skipped: Math.max(0, visual.length - lines.length) };
107
+ }
108
+
109
+ /**
110
+ * Render tool-call args while provider is still streaming them. This makes long
111
+ * subagent prompt generation visible before execute() can start.
112
+ */
113
+ export function renderSubagentCall(
114
+ args: SubagentCallArgs,
115
+ theme: Theme,
116
+ context: SubagentRenderCallContext,
117
+ ): Component {
118
+ const cache = context.state as {
119
+ callWidth?: number;
120
+ callLines?: string[];
121
+ callKey?: string;
122
+ };
123
+
124
+ return {
125
+ invalidate() {
126
+ cache.callWidth = undefined;
127
+ cache.callLines = undefined;
128
+ cache.callKey = undefined;
129
+ },
130
+ render(width: number): string[] {
131
+ const key = JSON.stringify({ args, executionStarted: context.executionStarted, argsComplete: context.argsComplete, width });
132
+ if (cache.callWidth === width && cache.callKey === key && cache.callLines) return cache.callLines;
133
+
134
+ const out: string[] = [];
135
+ const agent = asString(args.agent);
136
+ const action = asString(args.action);
137
+ const task = asString(args.task);
138
+ const tasks = Array.isArray(args.tasks) ? args.tasks as Array<Record<string, unknown>> : undefined;
139
+ const isParallel = !!tasks?.length;
140
+ const status = context.executionStarted
141
+ ? "running"
142
+ : context.argsComplete
143
+ ? "starting"
144
+ : task || isParallel
145
+ ? "writing prompt"
146
+ : "waiting for prompt";
147
+ const mode = isParallel ? `Parallel (${tasks!.length})` : "Subagent";
148
+ const bg = args.background === true ? " · background" : "";
149
+ const target = agent ? ` ${agent}` : action ? ` ${action}` : "";
150
+ out.push(truncateToWidth(`${theme.fg("toolTitle", mode)}${target}${bg} · ${theme.fg("dim", status)}`, width, "..."));
151
+
152
+ // Once execution starts, result renderer owns prompt display. Keep call row compact.
153
+ if (context.executionStarted) {
154
+ cache.callWidth = width;
155
+ cache.callKey = key;
156
+ cache.callLines = out;
157
+ return out;
158
+ }
159
+
160
+ const maxLines = readPreviewSettings().promptPreviewLines;
161
+ if (task) {
162
+ out.push(truncateToWidth("Prompt:", width, "..."));
163
+ const preview = taskPreviewLines(task, width, maxLines);
164
+ out.push(...preview.lines);
165
+ if (preview.skipped > 0) out.push(truncateToWidth(theme.fg("muted", ` … (${preview.skipped} more lines)`), width, "..."));
166
+ } else if (tasks?.length) {
167
+ const maxRows = Math.max(1, Math.min(maxLines, tasks.length));
168
+ for (let i = 0; i < maxRows; i++) {
169
+ const t = tasks[i]!;
170
+ const rowAgent = asString(t.agent) ?? "?";
171
+ const rowTask = asString(t.task) ?? "";
172
+ out.push(truncateToWidth(` [${rowAgent}] ${rowTask || theme.fg("dim", "writing prompt...")}`, width, "..."));
173
+ }
174
+ if (tasks.length > maxRows) out.push(truncateToWidth(theme.fg("muted", ` … (${tasks.length - maxRows} more task${tasks.length - maxRows === 1 ? "" : "s"})`), width, "..."));
175
+ } else {
176
+ out.push(truncateToWidth(theme.fg("dim", " waiting for streamed tool arguments..."), width, "..."));
177
+ }
178
+
179
+ cache.callWidth = width;
180
+ cache.callKey = key;
181
+ cache.callLines = out;
182
+ return out;
183
+ },
184
+ };
185
+ }
186
+
76
187
  export function renderSubagentResult(
77
188
  result: AgentToolResult<unknown>,
78
189
  { isPartial, expanded }: ToolRenderResultOptions,
@@ -223,11 +334,22 @@ export function renderSubagentResult(
223
334
  }
224
335
  }
225
336
  } else {
226
- // Render events chronologically
337
+ // Render events chronologically. Coalesce text deltas to avoid one-line-per-token output.
227
338
  let lastWasText = false;
339
+ let textBuffer = "";
228
340
  const agentLabel = `${details.agentName ?? "Agent"}:`;
341
+
342
+ const flushTextBuffer = () => {
343
+ if (!textBuffer) return;
344
+ for (const line of textBuffer.split("\n")) {
345
+ for (const w of wrapLine(indent + line, width)) out.push(w);
346
+ }
347
+ textBuffer = "";
348
+ };
349
+
229
350
  for (const evt of events) {
230
351
  if (evt.type === "tool_start") {
352
+ if (lastWasText) flushTextBuffer();
231
353
  lastWasText = false;
232
354
  const call = `${evt.toolName}(${evt.argSummary})`;
233
355
  toolLineMap.set(evt.toolCallId, out.length);
@@ -237,10 +359,9 @@ export function renderSubagentResult(
237
359
  out.push(truncateToWidth(theme.fg("toolTitle", agentLabel), width, "..."));
238
360
  lastWasText = true;
239
361
  }
240
- for (const line of evt.text.split("\n")) {
241
- for (const w of wrapLine(indent + line, width)) out.push(w);
242
- }
362
+ textBuffer += evt.text;
243
363
  } else if (evt.type === "tool_end") {
364
+ if (lastWasText) flushTextBuffer();
244
365
  lastWasText = false;
245
366
  const toolLineIdx = toolLineMap.get(evt.toolCallId);
246
367
  const dur = evt.durMs != null
@@ -257,6 +378,8 @@ export function renderSubagentResult(
257
378
  }
258
379
  }
259
380
  }
381
+
382
+ if (lastWasText) flushTextBuffer();
260
383
  }
261
384
 
262
385
  return out;
package/runner.ts CHANGED
@@ -66,6 +66,62 @@ export function getCurrentDepth(): number {
66
66
 
67
67
  export interface RunAgentDeps {
68
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;
73
+ }
74
+
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ function modelRef(model: any): string {
77
+ return `${model.provider}/${model.id}`;
78
+ }
79
+
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
+ };
69
125
  }
70
126
 
71
127
  export async function runAgent(
@@ -92,7 +148,8 @@ export async function runAgent(
92
148
  }
93
149
 
94
150
  const bootStartedAt = Date.now();
95
- const { authStorage, modelRegistry } = getAuth();
151
+ const { authStorage, modelRegistry: defaultRegistry } = getAuth();
152
+ const modelRegistry = deps.modelRegistry ?? defaultRegistry;
96
153
  const agentDir = getAgentDir();
97
154
  const noExtensions = !agentNeedsExtensions(agent.tools);
98
155
  const coldLoader = !pool.isWarm(cwd, agentDir, noExtensions);
@@ -114,6 +171,8 @@ export async function runAgent(
114
171
  });
115
172
  await allowUiPaint(coldLoader);
116
173
 
174
+ const createPrevEnvDepth = process.env[DEPTH_ENV];
175
+ process.env[DEPTH_ENV] = String(depth + 1);
117
176
  const loaderLease = await pool.acquire(
118
177
  cwd,
119
178
  agentDir,
@@ -130,10 +189,13 @@ export async function runAgent(
130
189
  authStorage,
131
190
  modelRegistry,
132
191
  resourceLoader: loaderLease.loader,
192
+ ...(deps.modelObject ? { model: deps.modelObject } : {}),
133
193
  });
134
194
  session = created.session;
135
195
  } catch (e) {
136
196
  loaderLease.release();
197
+ if (createPrevEnvDepth === undefined) delete process.env[DEPTH_ENV];
198
+ else process.env[DEPTH_ENV] = createPrevEnvDepth;
137
199
  return {
138
200
  output: "",
139
201
  exitCode: 1,
@@ -142,17 +204,14 @@ export async function runAgent(
142
204
  usage: { input: 0, output: 0, cost: 0, turns: 0 },
143
205
  };
144
206
  }
145
-
146
- // Resolve and apply model
147
- const modelStr = modelOverride ?? agent.model;
148
- if (modelStr) {
149
- const [provider, ...rest] = modelStr.split("/");
150
- const modelId = rest.join("/");
151
- if (provider && modelId) {
152
- const model = modelRegistry.find(provider, modelId);
153
- if (model) await session.setModel(model);
154
- }
155
- }
207
+ if (createPrevEnvDepth === undefined) delete process.env[DEPTH_ENV];
208
+ else process.env[DEPTH_ENV] = createPrevEnvDepth;
209
+
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);
156
215
 
157
216
  // Apply tools allowlist.
158
217
  // "all" → no restriction (everything registered stays active)