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 +11 -2
- package/agents/scout.md +4 -3
- package/index.ts +132 -5
- package/package.json +1 -1
- package/render.ts +127 -4
- package/runner.ts +71 -12
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
|
|
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:
|
|
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
|
-
-
|
|
40
|
-
- do not
|
|
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,
|
|
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,
|
|
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
|
|
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
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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)
|