pi-fast-subagent 0.9.1 → 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
 
@@ -272,6 +272,14 @@ 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
+
275
283
  ## Keyboard Shortcuts
276
284
 
277
285
  | Shortcut | Action |
@@ -289,7 +297,7 @@ Goal: keep this extension **small and focused** — aligned with pi's philosophy
289
297
 
290
298
  - Async/background isolation not supported in-process
291
299
  - Git worktree isolation not supported
292
- - Nested subagent depth limited to 2 by default
300
+ - Nested subagent spawning disabled by default (`maxDepth: 0`); opt in per agent via frontmatter
293
301
 
294
302
  ## Tool Reference
295
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
@@ -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 { getCurrentDepth, mapConcurrent, runAgent } from "./runner.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.",
@@ -417,11 +514,26 @@ export default function (pi: ExtensionAPI) {
417
514
  const { agent, error } = findAgent(params.agent);
418
515
  if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
419
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
+
420
532
  if (params.background) {
421
533
  const bgAbort = new AbortController();
422
534
  const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
423
535
  const resultPromise: Promise<BackgroundJobResult> = runAgent(
424
- agent, params.task, cwd, params.model, bgAbort.signal, undefined,
536
+ agent, params.task, cwd, effectiveModel, bgAbort.signal, undefined, undefined, inheritDeps,
425
537
  ).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
426
538
  const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
427
539
  return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
@@ -441,7 +553,7 @@ export default function (pi: ExtensionAPI) {
441
553
  : undefined;
442
554
 
443
555
  const agentRunPromise: Promise<RunResult> = runAgent(
444
- agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
556
+ agent, params.task, cwd, effectiveModel, agentAbort.signal, wrappedOnUpdate, undefined, inheritDeps,
445
557
  );
446
558
 
447
559
  const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
@@ -546,7 +658,18 @@ export default function (pi: ExtensionAPI) {
546
658
  parallelAgents[i]!.responseText = (partial.content?.[0] as any)?.text || parallelAgents[i]!.responseText;
547
659
  emitParallel(true);
548
660
  };
549
- 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
+ );
550
673
  parallelAgents[i]!.status = result.exitCode === 0 ? "done" : "error";
551
674
  parallelAgents[i]!.durMs = Date.now() - agentStart;
552
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.1",
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
@@ -334,11 +334,22 @@ export function renderSubagentResult(
334
334
  }
335
335
  }
336
336
  } else {
337
- // Render events chronologically
337
+ // Render events chronologically. Coalesce text deltas to avoid one-line-per-token output.
338
338
  let lastWasText = false;
339
+ let textBuffer = "";
339
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
+
340
350
  for (const evt of events) {
341
351
  if (evt.type === "tool_start") {
352
+ if (lastWasText) flushTextBuffer();
342
353
  lastWasText = false;
343
354
  const call = `${evt.toolName}(${evt.argSummary})`;
344
355
  toolLineMap.set(evt.toolCallId, out.length);
@@ -348,10 +359,9 @@ export function renderSubagentResult(
348
359
  out.push(truncateToWidth(theme.fg("toolTitle", agentLabel), width, "..."));
349
360
  lastWasText = true;
350
361
  }
351
- for (const line of evt.text.split("\n")) {
352
- for (const w of wrapLine(indent + line, width)) out.push(w);
353
- }
362
+ textBuffer += evt.text;
354
363
  } else if (evt.type === "tool_end") {
364
+ if (lastWasText) flushTextBuffer();
355
365
  lastWasText = false;
356
366
  const toolLineIdx = toolLineMap.get(evt.toolCallId);
357
367
  const dur = evt.durMs != null
@@ -368,6 +378,8 @@ export function renderSubagentResult(
368
378
  }
369
379
  }
370
380
  }
381
+
382
+ if (lastWasText) flushTextBuffer();
371
383
  }
372
384
 
373
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)