pi-fast-subagent 0.9.2 → 0.9.3

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 { getCurrentDepth, 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
@@ -658,17 +549,14 @@ export default function (pi: ExtensionAPI) {
658
549
  parallelAgents[i]!.responseText = (partial.content?.[0] as any)?.text || parallelAgents[i]!.responseText;
659
550
  emitParallel(true);
660
551
  };
661
- const shouldInherit = !t.model && (!agent.model || agent.model === "inherit");
662
- const inherited = shouldInherit ? getInheritedMainModel(ctx) : undefined;
663
552
  const result = await runAgent(
664
553
  agent,
665
554
  t.task,
666
555
  t.cwd ?? cwd,
667
- t.model ?? inherited?.modelRef,
556
+ t.model,
668
557
  signal,
669
558
  agentOnUpdate,
670
559
  parentDepth,
671
- inherited?.deps ?? { modelRegistry: ctx.modelRegistry },
672
560
  );
673
561
  parallelAgents[i]!.status = result.exitCode === 0 ? "done" : "error";
674
562
  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.3",
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
@@ -66,62 +66,6 @@ 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
- };
125
69
  }
126
70
 
127
71
  export async function runAgent(
@@ -148,8 +92,7 @@ export async function runAgent(
148
92
  }
149
93
 
150
94
  const bootStartedAt = Date.now();
151
- const { authStorage, modelRegistry: defaultRegistry } = getAuth();
152
- const modelRegistry = deps.modelRegistry ?? defaultRegistry;
95
+ const { authStorage, modelRegistry } = getAuth();
153
96
  const agentDir = getAgentDir();
154
97
  const noExtensions = !agentNeedsExtensions(agent.tools);
155
98
  const coldLoader = !pool.isWarm(cwd, agentDir, noExtensions);
@@ -189,7 +132,6 @@ export async function runAgent(
189
132
  authStorage,
190
133
  modelRegistry,
191
134
  resourceLoader: loaderLease.loader,
192
- ...(deps.modelObject ? { model: deps.modelObject } : {}),
193
135
  });
194
136
  session = created.session;
195
137
  } catch (e) {
@@ -207,11 +149,16 @@ export async function runAgent(
207
149
  if (createPrevEnvDepth === undefined) delete process.env[DEPTH_ENV];
208
150
  else process.env[DEPTH_ENV] = createPrevEnvDepth;
209
151
 
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);
152
+ // Resolve and apply model
153
+ const modelStr = modelOverride ?? agent.model;
154
+ if (modelStr) {
155
+ const [provider, ...rest] = modelStr.split("/");
156
+ const modelId = rest.join("/");
157
+ if (provider && modelId) {
158
+ const model = modelRegistry.find(provider, modelId);
159
+ if (model) await session.setModel(model);
160
+ }
161
+ }
215
162
 
216
163
  // Apply tools allowlist.
217
164
  // "all" → no restriction (everything registered stays active)