pi-fast-subagent 0.7.0 → 0.9.0
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 +41 -0
- package/agents/general.md +3 -0
- package/agents/scout.md +3 -0
- package/agents.ts +14 -1
- package/format.ts +119 -0
- package/index.ts +58 -885
- package/loader-pool.ts +175 -0
- package/package.json +11 -1
- package/render.ts +348 -0
- package/runner.ts +371 -0
- package/schemas.ts +59 -0
- package/types.ts +55 -0
package/index.ts
CHANGED
|
@@ -4,117 +4,39 @@
|
|
|
4
4
|
* Uses createAgentSession() to run subagents in the same process as pi —
|
|
5
5
|
* no subprocess spawn, no cold-start overhead.
|
|
6
6
|
*
|
|
7
|
-
* Supports: single, parallel.
|
|
7
|
+
* Supports: single, parallel, background.
|
|
8
8
|
* Agent .md files are compatible with pi-subagents frontmatter format.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
ToolRenderResultOptions,
|
|
19
|
-
} from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolRenderResultOptions } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import { Theme } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { Key } from "@mariozechner/pi-tui";
|
|
16
|
+
|
|
17
|
+
import { type AgentConfig, discoverAgents } from "./agents.js";
|
|
20
18
|
import { BackgroundJobManager } from "./background-job-manager.js";
|
|
21
19
|
import type { BackgroundHandleLike, BackgroundJobResult, BackgroundSubagentJob } from "./background-types.js";
|
|
22
|
-
import { Theme } from "@mariozechner/pi-coding-agent";
|
|
23
|
-
import { Key, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
24
|
-
import { truncateToVisualLines, keyHint } from "@mariozechner/pi-coding-agent";
|
|
25
20
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (tools === "builtins") return "builtins (default)";
|
|
41
|
-
if (tools === "none") return "none";
|
|
42
|
-
return tools.join(", ");
|
|
43
|
-
}
|
|
21
|
+
formatBgJobDetails,
|
|
22
|
+
formatBgJobSummary,
|
|
23
|
+
formatDuration,
|
|
24
|
+
formatTools,
|
|
25
|
+
getFinalText,
|
|
26
|
+
summarizeTask,
|
|
27
|
+
} from "./format.js";
|
|
28
|
+
import { defaultLoaderPool } from "./loader-pool.js";
|
|
29
|
+
import { renderSubagentResult } from "./render.js";
|
|
30
|
+
import { getCurrentDepth, mapConcurrent, runAgent } from "./runner.js";
|
|
31
|
+
import { SubagentParams } from "./schemas.js";
|
|
32
|
+
import type { AgentRowStatus, OnUpdate, RunResult, SubagentDetails, ToolCallEntry } from "./types.js";
|
|
33
|
+
|
|
34
|
+
// ─── Module-level state ─────────────────────────────────────────────────────
|
|
44
35
|
|
|
45
|
-
// ─── Tool arg summarizer (compact one-liner per tool call) ─────────────────────
|
|
46
|
-
|
|
47
|
-
function shortPath(p: unknown): string {
|
|
48
|
-
if (typeof p !== "string") return "";
|
|
49
|
-
const cwd = process.cwd();
|
|
50
|
-
if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
|
|
51
|
-
return p.replace(/^\/Users\/[^/]+\/[^/]+\//, "");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function summarizeToolArgs(toolName: unknown, toolInput: unknown): string {
|
|
55
|
-
const name = String(toolName ?? "");
|
|
56
|
-
const input =
|
|
57
|
-
toolInput && typeof toolInput === "object" ? (toolInput as Record<string, unknown>) : {};
|
|
58
|
-
const filePath = (): string => shortPath(input.path ?? input.file_path) || "";
|
|
59
|
-
switch (name) {
|
|
60
|
-
case "Read":
|
|
61
|
-
case "read":
|
|
62
|
-
case "Write":
|
|
63
|
-
case "write":
|
|
64
|
-
case "Edit":
|
|
65
|
-
case "edit":
|
|
66
|
-
return filePath();
|
|
67
|
-
case "Bash":
|
|
68
|
-
case "bash": {
|
|
69
|
-
const cmd = String(input.command ?? "");
|
|
70
|
-
return cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
|
|
71
|
-
}
|
|
72
|
-
case "Glob":
|
|
73
|
-
case "glob":
|
|
74
|
-
return String(input.pattern ?? "");
|
|
75
|
-
case "find": {
|
|
76
|
-
const pat = String(input.pattern ?? "");
|
|
77
|
-
const p = shortPath(input.path);
|
|
78
|
-
return p ? `${pat} in ${p}` : pat;
|
|
79
|
-
}
|
|
80
|
-
case "Grep":
|
|
81
|
-
case "grep": {
|
|
82
|
-
const pat = String(input.pattern ?? "");
|
|
83
|
-
const g = input.glob ? ` ${input.glob}` : "";
|
|
84
|
-
return `${pat}${g}`;
|
|
85
|
-
}
|
|
86
|
-
case "ls":
|
|
87
|
-
return shortPath(input.path) || "";
|
|
88
|
-
case "subagent": {
|
|
89
|
-
const agent = String(input.agent ?? "");
|
|
90
|
-
const t = String(input.task ?? "");
|
|
91
|
-
const summary = t.length > 50 ? t.slice(0, 47) + "..." : t;
|
|
92
|
-
return agent ? `${agent}: ${summary}` : summary;
|
|
93
|
-
}
|
|
94
|
-
default: {
|
|
95
|
-
for (const v of Object.values(input)) {
|
|
96
|
-
if (typeof v === "string" && v.length > 0)
|
|
97
|
-
return v.length > 60 ? v.slice(0, 57) + "..." : v;
|
|
98
|
-
}
|
|
99
|
-
return "";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ─── Shared auth (created once, reused across calls) ─────────────────────────
|
|
105
|
-
|
|
106
|
-
let _authStorage: ReturnType<typeof AuthStorage.create> | null = null;
|
|
107
|
-
let _modelRegistry: ReturnType<typeof ModelRegistry.create> | null = null;
|
|
108
36
|
let _bgManager: BackgroundJobManager | null = null;
|
|
109
37
|
let _onBgJobComplete: ((job: BackgroundSubagentJob) => void) | null = null;
|
|
110
38
|
let _setBgStatus: ((text: string | undefined) => void) | null = null;
|
|
111
39
|
|
|
112
|
-
function getAuth() {
|
|
113
|
-
if (!_authStorage) _authStorage = AuthStorage.create();
|
|
114
|
-
if (!_modelRegistry) _modelRegistry = ModelRegistry.create(_authStorage);
|
|
115
|
-
return { authStorage: _authStorage, modelRegistry: _modelRegistry };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
40
|
function getBgManager(): BackgroundJobManager {
|
|
119
41
|
if (!_bgManager) _bgManager = new BackgroundJobManager({
|
|
120
42
|
onJobComplete: (job) => _onBgJobComplete?.(job),
|
|
@@ -127,562 +49,21 @@ function refreshBgStatus(): void {
|
|
|
127
49
|
_setBgStatus?.(running.length > 0 ? `⧗ ${running.length} bg agent${running.length > 1 ? "s" : ""}` : undefined);
|
|
128
50
|
}
|
|
129
51
|
|
|
130
|
-
// ───
|
|
131
|
-
|
|
132
|
-
interface LoaderPoolEntry {
|
|
133
|
-
idle: DefaultResourceLoader[];
|
|
134
|
-
active: Set<DefaultResourceLoader>;
|
|
135
|
-
warming: Set<Promise<void>>;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
interface LoaderLease {
|
|
139
|
-
loader: ResourceLoader;
|
|
140
|
-
release: () => void;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const _loaderPool = new Map<string, LoaderPoolEntry>();
|
|
144
|
-
|
|
145
|
-
function loaderPoolKey(cwd: string, agentDir: string, noExtensions: boolean): string {
|
|
146
|
-
return `${cwd}\0${agentDir}\0${noExtensions ? "noext" : "ext"}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getLoaderPoolEntry(cwd: string, agentDir: string, noExtensions: boolean): LoaderPoolEntry {
|
|
150
|
-
const key = loaderPoolKey(cwd, agentDir, noExtensions);
|
|
151
|
-
let entry = _loaderPool.get(key);
|
|
152
|
-
if (!entry) {
|
|
153
|
-
entry = { idle: [], active: new Set(), warming: new Set() };
|
|
154
|
-
_loaderPool.set(key, entry);
|
|
155
|
-
}
|
|
156
|
-
return entry;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function makeLoaderOptions(cwd: string, agentDir: string, noExtensions: boolean): DefaultResourceLoaderOptions {
|
|
160
|
-
return {
|
|
161
|
-
cwd,
|
|
162
|
-
agentDir,
|
|
163
|
-
noExtensions,
|
|
164
|
-
noContextFiles: true,
|
|
165
|
-
noSkills: true,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
class AgentPromptResourceLoader implements ResourceLoader {
|
|
170
|
-
constructor(
|
|
171
|
-
private readonly base: ResourceLoader,
|
|
172
|
-
private readonly systemPromptOverride: string | undefined,
|
|
173
|
-
) {}
|
|
174
|
-
|
|
175
|
-
getExtensions() { return this.base.getExtensions(); }
|
|
176
|
-
getSkills() { return this.base.getSkills(); }
|
|
177
|
-
getPrompts() { return this.base.getPrompts(); }
|
|
178
|
-
getThemes() { return this.base.getThemes(); }
|
|
179
|
-
getAgentsFiles() { return this.base.getAgentsFiles(); }
|
|
180
|
-
getSystemPrompt() { return this.systemPromptOverride ?? this.base.getSystemPrompt(); }
|
|
181
|
-
getAppendSystemPrompt() { return this.base.getAppendSystemPrompt(); }
|
|
182
|
-
extendResources(paths: Parameters<ResourceLoader["extendResources"]>[0]): void { this.base.extendResources(paths); }
|
|
183
|
-
reload(): Promise<void> { return this.base.reload(); }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function isLoaderWarm(cwd: string, agentDir: string, noExtensions: boolean): boolean {
|
|
187
|
-
const entry = _loaderPool.get(loaderPoolKey(cwd, agentDir, noExtensions));
|
|
188
|
-
return !!entry && entry.idle.length > 0;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function allowUiPaint(coldLoader: boolean): Promise<void> {
|
|
192
|
-
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
193
|
-
if (!coldLoader) return;
|
|
194
|
-
// Give pi's TUI render timer a real timers-phase turn before CPU-heavy extension loading.
|
|
195
|
-
await new Promise<void>((resolve) => setTimeout(resolve, 50));
|
|
196
|
-
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function acquireResourceLoader(
|
|
200
|
-
cwd: string,
|
|
201
|
-
agentDir: string,
|
|
202
|
-
noExtensions: boolean,
|
|
203
|
-
systemPromptOverride: string | undefined,
|
|
204
|
-
): Promise<LoaderLease> {
|
|
205
|
-
const entry = getLoaderPoolEntry(cwd, agentDir, noExtensions);
|
|
206
|
-
|
|
207
|
-
while (true) {
|
|
208
|
-
const cached = entry.idle.pop();
|
|
209
|
-
if (cached) {
|
|
210
|
-
entry.active.add(cached);
|
|
211
|
-
let released = false;
|
|
212
|
-
return {
|
|
213
|
-
loader: new AgentPromptResourceLoader(cached, systemPromptOverride),
|
|
214
|
-
release: () => {
|
|
215
|
-
if (released) return;
|
|
216
|
-
released = true;
|
|
217
|
-
entry.active.delete(cached);
|
|
218
|
-
entry.idle.push(cached);
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const warming = entry.warming.values().next().value as Promise<void> | undefined;
|
|
224
|
-
if (warming) {
|
|
225
|
-
await warming;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const loader = new DefaultResourceLoader(makeLoaderOptions(cwd, agentDir, noExtensions));
|
|
230
|
-
const warmPromise = loader.reload()
|
|
231
|
-
.then(() => { entry.idle.push(loader); })
|
|
232
|
-
.finally(() => { entry.warming.delete(warmPromise); });
|
|
233
|
-
entry.warming.add(warmPromise);
|
|
234
|
-
await warmPromise;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function warmResourceLoader(cwd: string, agentDir: string, noExtensions: boolean): void {
|
|
239
|
-
const entry = getLoaderPoolEntry(cwd, agentDir, noExtensions);
|
|
240
|
-
if (entry.idle.length > 0 || entry.active.size > 0 || entry.warming.size > 0) return;
|
|
241
|
-
const loader = new DefaultResourceLoader(makeLoaderOptions(cwd, agentDir, noExtensions));
|
|
242
|
-
const warmPromise = loader.reload()
|
|
243
|
-
.then(() => { entry.idle.push(loader); })
|
|
244
|
-
.catch(() => { /* ignore warm failures; foreground call reports real error */ })
|
|
245
|
-
.finally(() => { entry.warming.delete(warmPromise); });
|
|
246
|
-
entry.warming.add(warmPromise);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ─── Foreground detach registry ───────────────────────────────────────────────
|
|
52
|
+
// ─── Foreground detach registry ─────────────────────────────────────────────
|
|
250
53
|
|
|
251
54
|
interface ForegroundDetachEntry {
|
|
252
55
|
agentName: string;
|
|
253
56
|
task: string;
|
|
254
|
-
detach: () => string;
|
|
57
|
+
detach: () => string;
|
|
255
58
|
}
|
|
256
59
|
const _fgJobs = new Map<string, ForegroundDetachEntry>();
|
|
257
60
|
|
|
258
|
-
// ───
|
|
259
|
-
|
|
260
|
-
const MAX_DEPTH = 2;
|
|
261
|
-
const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
|
|
262
|
-
|
|
263
|
-
interface ToolCallEntry {
|
|
264
|
-
id: string;
|
|
265
|
-
name: string;
|
|
266
|
-
argSummary: string;
|
|
267
|
-
result?: string;
|
|
268
|
-
isError?: boolean;
|
|
269
|
-
durMs?: number;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
interface RunResult {
|
|
273
|
-
output: string;
|
|
274
|
-
exitCode: number;
|
|
275
|
-
error?: string;
|
|
276
|
-
model?: string;
|
|
277
|
-
toolCalls: ToolCallEntry[];
|
|
278
|
-
usage: { input: number; output: number; cost: number; turns: number };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
interface AgentRowStatus {
|
|
282
|
-
name: string;
|
|
283
|
-
taskSummary: string;
|
|
284
|
-
status: "pending" | "running" | "done" | "error";
|
|
285
|
-
durMs?: number;
|
|
286
|
-
toolCalls?: ToolCallEntry[];
|
|
287
|
-
responseText?: string;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
interface SubagentDetails {
|
|
291
|
-
mode?: "single" | "parallel";
|
|
292
|
-
agentName?: string;
|
|
293
|
-
task?: string;
|
|
294
|
-
// parallel
|
|
295
|
-
parallelAgents?: AgentRowStatus[];
|
|
296
|
-
usage: RunResult["usage"];
|
|
297
|
-
running: boolean;
|
|
298
|
-
elapsedMs?: number;
|
|
299
|
-
model?: string;
|
|
300
|
-
backgroundJobId?: string;
|
|
301
|
-
toolCalls: ToolCallEntry[];
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
type OnUpdate = (partial: { content: [{ type: "text"; text: string }]; details: unknown }) => void;
|
|
305
|
-
|
|
306
|
-
function formatDuration(ms: number): string {
|
|
307
|
-
const s = Math.max(0, Math.floor(ms / 1000));
|
|
308
|
-
const m = Math.floor(s / 60);
|
|
309
|
-
const rem = s % 60;
|
|
310
|
-
return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function summarizeTask(task: string, max = 60): string {
|
|
314
|
-
return task.length > max ? task.slice(0, max - 3) + "..." : task;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function formatBgJobSummary(job: BackgroundSubagentJob, now = Date.now()): string {
|
|
318
|
-
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
|
|
319
|
-
return `${job.id} [${job.status}] ${job.agentName} · ${dur} · ${summarizeTask(job.task)}`;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): string {
|
|
323
|
-
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
|
|
324
|
-
const lines = [`${job.id} [${job.status}] ${job.agentName} · ${dur}`, `Task: ${job.task}`];
|
|
325
|
-
if (job.model) lines.push(`Model: ${job.model}`);
|
|
326
|
-
if (job.status === "completed") lines.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
|
|
327
|
-
if (job.status === "failed") lines.push(`\nError: ${job.error ?? "(unknown)"}`);
|
|
328
|
-
if (job.status === "cancelled") lines.push("\nCancelled.");
|
|
329
|
-
if (job.status === "running") lines.push("\nStill running.");
|
|
330
|
-
return lines.join("\n");
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Module-level depth counter — avoids process.env race conditions in parallel mode
|
|
334
|
-
let _currentDepth = 0;
|
|
335
|
-
|
|
336
|
-
async function runAgent(
|
|
337
|
-
agent: AgentConfig,
|
|
338
|
-
task: string,
|
|
339
|
-
cwd: string,
|
|
340
|
-
modelOverride: string | undefined,
|
|
341
|
-
signal: AbortSignal | undefined,
|
|
342
|
-
onUpdate: OnUpdate | undefined,
|
|
343
|
-
parentDepth?: number,
|
|
344
|
-
): Promise<RunResult> {
|
|
345
|
-
const depth = parentDepth ?? _currentDepth;
|
|
346
|
-
if (depth >= MAX_DEPTH) {
|
|
347
|
-
return {
|
|
348
|
-
output: "",
|
|
349
|
-
exitCode: 1,
|
|
350
|
-
error: `Max subagent depth (${MAX_DEPTH}) exceeded. Increase PI_FAST_SUBAGENT_DEPTH env to allow deeper nesting.`,
|
|
351
|
-
toolCalls: [],
|
|
352
|
-
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const bootStartedAt = Date.now();
|
|
357
|
-
const { authStorage, modelRegistry } = getAuth();
|
|
358
|
-
const agentDir = getAgentDir();
|
|
359
|
-
const noExtensions = !agentNeedsExtensions(agent.tools);
|
|
360
|
-
const coldLoader = !isLoaderWarm(cwd, agentDir, noExtensions);
|
|
361
|
-
|
|
362
|
-
// Fire an immediate "running" emit so the UI draws the agent header + prompt
|
|
363
|
-
// before the (potentially slow) extension/session load. Without this, pi looks
|
|
364
|
-
// frozen while `loader.reload()` and `createAgentSession()` are in flight.
|
|
365
|
-
onUpdate?.({
|
|
366
|
-
content: [{ type: "text", text: "" }],
|
|
367
|
-
details: {
|
|
368
|
-
agentName: agent.name,
|
|
369
|
-
task,
|
|
370
|
-
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
371
|
-
running: true,
|
|
372
|
-
elapsedMs: 0,
|
|
373
|
-
model: modelOverride ?? agent.model,
|
|
374
|
-
toolCalls: [],
|
|
375
|
-
} satisfies SubagentDetails,
|
|
376
|
-
});
|
|
377
|
-
// Yield through timers when loader is cold so pi's render loop paints before
|
|
378
|
-
// CPU-heavy extension loading runs.
|
|
379
|
-
await allowUiPaint(coldLoader);
|
|
380
|
-
|
|
381
|
-
const loaderLease = await acquireResourceLoader(
|
|
382
|
-
cwd,
|
|
383
|
-
agentDir,
|
|
384
|
-
noExtensions,
|
|
385
|
-
agent.systemPrompt || undefined,
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
let session: Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
389
|
-
try {
|
|
390
|
-
const created = await createAgentSession({
|
|
391
|
-
cwd,
|
|
392
|
-
agentDir,
|
|
393
|
-
sessionManager: SessionManager.inMemory(cwd),
|
|
394
|
-
authStorage,
|
|
395
|
-
modelRegistry,
|
|
396
|
-
resourceLoader: loaderLease.loader,
|
|
397
|
-
});
|
|
398
|
-
session = created.session;
|
|
399
|
-
} catch (e) {
|
|
400
|
-
loaderLease.release();
|
|
401
|
-
return {
|
|
402
|
-
output: "",
|
|
403
|
-
exitCode: 1,
|
|
404
|
-
error: e instanceof Error ? e.message : String(e),
|
|
405
|
-
toolCalls: [],
|
|
406
|
-
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Resolve and apply model
|
|
411
|
-
const modelStr = modelOverride ?? agent.model;
|
|
412
|
-
if (modelStr) {
|
|
413
|
-
const [provider, ...rest] = modelStr.split("/");
|
|
414
|
-
const modelId = rest.join("/");
|
|
415
|
-
if (provider && modelId) {
|
|
416
|
-
const model = modelRegistry.find(provider, modelId);
|
|
417
|
-
if (model) await session.setModel(model);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Apply tools allowlist.
|
|
422
|
-
// "all" → no restriction (everything registered stays active)
|
|
423
|
-
// "none" → disable every tool
|
|
424
|
-
// string[] → explicit allowlist
|
|
425
|
-
if (agent.tools === "none") {
|
|
426
|
-
session.setActiveToolsByName([]);
|
|
427
|
-
} else if (Array.isArray(agent.tools) && agent.tools.length > 0) {
|
|
428
|
-
session.setActiveToolsByName(agent.tools);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Track output and usage
|
|
432
|
-
const usage = { input: 0, output: 0, cost: 0, turns: 0 };
|
|
433
|
-
let lastOutput = "";
|
|
434
|
-
let currentDelta = "";
|
|
435
|
-
let detectedModel: string | undefined;
|
|
436
|
-
const startedAt = bootStartedAt;
|
|
437
|
-
const configuredModel = modelOverride ?? agent.model;
|
|
438
|
-
const toolCalls: ToolCallEntry[] = [];
|
|
439
|
-
const toolStartTimes = new Map<string, number>();
|
|
440
|
-
|
|
441
|
-
let done = false;
|
|
442
|
-
|
|
443
|
-
function emitUpdate(): void {
|
|
444
|
-
if (done) return;
|
|
445
|
-
onUpdate?.({
|
|
446
|
-
content: [{ type: "text", text: currentDelta || lastOutput || "" }],
|
|
447
|
-
details: {
|
|
448
|
-
agentName: agent.name,
|
|
449
|
-
task,
|
|
450
|
-
usage,
|
|
451
|
-
running: true,
|
|
452
|
-
elapsedMs: Date.now() - startedAt,
|
|
453
|
-
model: detectedModel ?? configuredModel,
|
|
454
|
-
toolCalls: [...toolCalls],
|
|
455
|
-
} satisfies SubagentDetails,
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
emitUpdate();
|
|
460
|
-
|
|
461
|
-
const heartbeat = setInterval(emitUpdate, 1000);
|
|
462
|
-
|
|
463
|
-
const unsubscribe = session.subscribe((event: any) => {
|
|
464
|
-
// Stream tool execution events
|
|
465
|
-
if (event.type === "tool_execution_start") {
|
|
466
|
-
toolStartTimes.set(event.toolCallId, Date.now());
|
|
467
|
-
toolCalls.push({
|
|
468
|
-
id: event.toolCallId,
|
|
469
|
-
name: event.toolName,
|
|
470
|
-
argSummary: summarizeToolArgs(event.toolName, event.args),
|
|
471
|
-
});
|
|
472
|
-
emitUpdate();
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (event.type === "tool_execution_end") {
|
|
477
|
-
const startedAtTool = toolStartTimes.get(event.toolCallId);
|
|
478
|
-
toolStartTimes.delete(event.toolCallId);
|
|
479
|
-
const resultText: string = (event.result?.content ?? [])
|
|
480
|
-
.filter((p: any) => p.type === "text")
|
|
481
|
-
.map((p: any) => p.text as string)
|
|
482
|
-
.join("\n");
|
|
483
|
-
let entry: ToolCallEntry | undefined;
|
|
484
|
-
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
485
|
-
if (toolCalls[i]!.id === event.toolCallId) { entry = toolCalls[i]; break; }
|
|
486
|
-
}
|
|
487
|
-
if (!entry) {
|
|
488
|
-
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
489
|
-
if (toolCalls[i]!.name === event.toolName && toolCalls[i]!.result === undefined) { entry = toolCalls[i]; break; }
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (entry) {
|
|
493
|
-
entry.result = resultText;
|
|
494
|
-
entry.isError = event.isError;
|
|
495
|
-
entry.durMs = startedAtTool != null ? Date.now() - startedAtTool : undefined;
|
|
496
|
-
}
|
|
497
|
-
emitUpdate();
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Stream text deltas live to the UI
|
|
502
|
-
if (event.type === "message_update") {
|
|
503
|
-
const e = event.assistantMessageEvent;
|
|
504
|
-
if (e?.type === "text_delta" && e.delta) {
|
|
505
|
-
currentDelta += e.delta;
|
|
506
|
-
emitUpdate();
|
|
507
|
-
}
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (event.type !== "message_end" || !event.message) return;
|
|
512
|
-
const msg = event.message;
|
|
513
|
-
if (msg.role !== "assistant") return; // usage/model only tracked for assistant turns
|
|
514
|
-
|
|
515
|
-
usage.turns++;
|
|
516
|
-
const u = msg.usage;
|
|
517
|
-
if (u) {
|
|
518
|
-
usage.input += u.input ?? 0;
|
|
519
|
-
usage.output += u.output ?? 0;
|
|
520
|
-
usage.cost += u.cost?.total ?? 0;
|
|
521
|
-
}
|
|
522
|
-
if (msg.model) detectedModel = msg.model;
|
|
523
|
-
|
|
524
|
-
// Extract last text content
|
|
525
|
-
for (const part of msg.content ?? []) {
|
|
526
|
-
if (part.type === "text") {
|
|
527
|
-
lastOutput = part.text;
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
// Reset delta accumulator for next turn
|
|
532
|
-
currentDelta = "";
|
|
533
|
-
|
|
534
|
-
onUpdate?.({
|
|
535
|
-
content: [{ type: "text", text: lastOutput || "(running...)" }],
|
|
536
|
-
details: {
|
|
537
|
-
agent: agent.name,
|
|
538
|
-
usage,
|
|
539
|
-
running: true,
|
|
540
|
-
elapsedMs: Date.now() - startedAt,
|
|
541
|
-
model: detectedModel ?? configuredModel,
|
|
542
|
-
},
|
|
543
|
-
});
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
// Propagate depth to nested calls — use module counter (safe for parallel) + env for subprocess compat
|
|
547
|
-
const prevEnvDepth = process.env[DEPTH_ENV];
|
|
548
|
-
process.env[DEPTH_ENV] = String(depth + 1);
|
|
549
|
-
_currentDepth = depth + 1;
|
|
550
|
-
|
|
551
|
-
let exitCode = 0;
|
|
552
|
-
let error: string | undefined;
|
|
553
|
-
|
|
554
|
-
try {
|
|
555
|
-
if (signal?.aborted) throw new Error("Aborted");
|
|
556
|
-
|
|
557
|
-
const onAbort = () => void session.abort();
|
|
558
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
559
|
-
try {
|
|
560
|
-
await session.prompt(task);
|
|
561
|
-
} finally {
|
|
562
|
-
signal?.removeEventListener("abort", onAbort);
|
|
563
|
-
}
|
|
564
|
-
} catch (e) {
|
|
565
|
-
exitCode = 1;
|
|
566
|
-
error = signal?.aborted ? "Aborted" : e instanceof Error ? e.message : String(e);
|
|
567
|
-
} finally {
|
|
568
|
-
done = true;
|
|
569
|
-
clearInterval(heartbeat);
|
|
570
|
-
unsubscribe();
|
|
571
|
-
session.dispose();
|
|
572
|
-
loaderLease.release();
|
|
573
|
-
if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
|
|
574
|
-
else process.env[DEPTH_ENV] = prevEnvDepth;
|
|
575
|
-
_currentDepth = depth;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
582
|
-
|
|
583
|
-
async function mapConcurrent<TIn, TOut>(
|
|
584
|
-
items: TIn[],
|
|
585
|
-
concurrency: number,
|
|
586
|
-
fn: (item: TIn, i: number) => Promise<TOut>,
|
|
587
|
-
): Promise<TOut[]> {
|
|
588
|
-
if (!items.length) return [];
|
|
589
|
-
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
590
|
-
const results: TOut[] = new Array(items.length);
|
|
591
|
-
let next = 0;
|
|
592
|
-
await Promise.all(
|
|
593
|
-
Array.from({ length: limit }, async () => {
|
|
594
|
-
while (true) {
|
|
595
|
-
const i = next++;
|
|
596
|
-
if (i >= items.length) return;
|
|
597
|
-
results[i] = await fn(items[i], i);
|
|
598
|
-
}
|
|
599
|
-
}),
|
|
600
|
-
);
|
|
601
|
-
return results;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function formatTokens(n: number): string {
|
|
605
|
-
if (n < 1000) return String(n);
|
|
606
|
-
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
607
|
-
return `${Math.round(n / 1000)}k`;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function formatUsage(usage: RunResult["usage"], model?: string): string {
|
|
611
|
-
const parts: string[] = [];
|
|
612
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
613
|
-
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
614
|
-
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
615
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
616
|
-
if (model) parts.push(model);
|
|
617
|
-
return parts.join(" ");
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function getFinalText(r: RunResult): string {
|
|
621
|
-
if (r.exitCode !== 0) return `Error: ${r.error ?? r.output ?? "(no output)"}`;
|
|
622
|
-
return r.output || "(no output)";
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// ─── Tool schemas ─────────────────────────────────────────────────────────────
|
|
626
|
-
|
|
627
|
-
const TaskItem = Type.Object({
|
|
628
|
-
agent: Type.String({ description: "Agent name" }),
|
|
629
|
-
task: Type.String({ description: "Task to delegate" }),
|
|
630
|
-
model: Type.Optional(Type.String({ description: "Model override (provider/model)" })),
|
|
631
|
-
cwd: Type.Optional(Type.String({ description: "Working directory" })),
|
|
632
|
-
count: Type.Optional(Type.Number({ description: "Repeat this task N times" })),
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
const SubagentParams = Type.Object({
|
|
636
|
-
// Single mode
|
|
637
|
-
agent: Type.Optional(Type.String({ description: "Agent name (single mode)" })),
|
|
638
|
-
task: Type.Optional(Type.String({ description: "Task (single mode)" })),
|
|
639
|
-
model: Type.Optional(Type.String({ description: "Model override (single mode)" })),
|
|
640
|
-
cwd: Type.Optional(Type.String({ description: "Working directory" })),
|
|
641
|
-
|
|
642
|
-
// Parallel mode
|
|
643
|
-
tasks: Type.Optional(
|
|
644
|
-
Type.Array(TaskItem, {
|
|
645
|
-
description: "Array of {agent, task} for parallel execution. Use count to repeat one task.",
|
|
646
|
-
}),
|
|
647
|
-
),
|
|
648
|
-
concurrency: Type.Optional(
|
|
649
|
-
Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
|
|
650
|
-
),
|
|
651
|
-
|
|
652
|
-
// Background
|
|
653
|
-
background: Type.Optional(Type.Boolean({ description: "Run in background, returns job ID immediately" })),
|
|
654
|
-
jobId: Type.Optional(Type.String({ description: "Job ID for poll/cancel" })),
|
|
655
|
-
|
|
656
|
-
// Management
|
|
657
|
-
action: Type.Optional(
|
|
658
|
-
Type.Union(
|
|
659
|
-
[
|
|
660
|
-
Type.Literal("list"),
|
|
661
|
-
Type.Literal("get"),
|
|
662
|
-
Type.Literal("status"),
|
|
663
|
-
Type.Literal("poll"),
|
|
664
|
-
Type.Literal("cancel"),
|
|
665
|
-
Type.Literal("detach"),
|
|
666
|
-
],
|
|
667
|
-
{ description: "'list'/'get' for agents, 'status' for bg jobs, 'poll'/'cancel' for a specific job, 'detach' to move a foreground job to background" },
|
|
668
|
-
),
|
|
669
|
-
),
|
|
670
|
-
agentScope: Type.Optional(
|
|
671
|
-
Type.Union(
|
|
672
|
-
[Type.Literal("user"), Type.Literal("project"), Type.Literal("both")],
|
|
673
|
-
{ description: "Agent scope filter", default: "both" },
|
|
674
|
-
),
|
|
675
|
-
),
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// ─── Extension entry point ────────────────────────────────────────────────────
|
|
61
|
+
// ─── Extension entry point ──────────────────────────────────────────────────
|
|
679
62
|
|
|
680
63
|
export default function (pi: ExtensionAPI) {
|
|
681
|
-
// ─── Status keys ────────────────────────────────────────────────────────────────────
|
|
682
64
|
const BG_STATUS_KEY = "fast-subagent-bg";
|
|
683
65
|
const FG_STATUS_KEY = "fast-subagent-fg";
|
|
684
66
|
|
|
685
|
-
// ─── Background job lifecycle ─────────────────────────────────────────────────────
|
|
686
67
|
_onBgJobComplete = (job) => {
|
|
687
68
|
refreshBgStatus();
|
|
688
69
|
const elapsed = job.completedAt ? ((job.completedAt - job.startedAt) / 1000).toFixed(1) : "?";
|
|
@@ -706,12 +87,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
706
87
|
pi.on("session_start", async (_event, ctx) => {
|
|
707
88
|
_setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
|
|
708
89
|
|
|
709
|
-
// Warm one extension-capable loader after startup
|
|
710
|
-
// call
|
|
90
|
+
// Warm one extension-capable loader after startup so first `tools: all`
|
|
91
|
+
// subagent call reuses loaded extensions instead of blocking.
|
|
711
92
|
if (process.env.PI_FAST_SUBAGENT_WARM !== "0") {
|
|
712
93
|
const warmCwd = ctx.cwd;
|
|
713
94
|
const warmAgentDir = getAgentDir();
|
|
714
|
-
setTimeout(() =>
|
|
95
|
+
setTimeout(() => defaultLoaderPool.warm(warmCwd, warmAgentDir, false), 1000);
|
|
715
96
|
}
|
|
716
97
|
});
|
|
717
98
|
|
|
@@ -719,10 +100,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
719
100
|
getBgManager().shutdown();
|
|
720
101
|
_bgManager = null;
|
|
721
102
|
_setBgStatus = null;
|
|
722
|
-
|
|
103
|
+
defaultLoaderPool.clear();
|
|
723
104
|
});
|
|
724
105
|
|
|
725
|
-
// ─── Ctrl+Shift+B —
|
|
106
|
+
// ─── Ctrl+Shift+B — detach foreground subagent ────────────────────────────
|
|
726
107
|
pi.registerShortcut(Key.ctrlShift("b"), {
|
|
727
108
|
description: "Move foreground subagent to background",
|
|
728
109
|
handler: async (ctx) => {
|
|
@@ -743,7 +124,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
743
124
|
},
|
|
744
125
|
});
|
|
745
126
|
|
|
746
|
-
// ─── /agent
|
|
127
|
+
// ─── /fast-subagent:agent ─────────────────────────────────────────────────
|
|
747
128
|
pi.registerCommand("fast-subagent:agent", {
|
|
748
129
|
description: "List available subagents. Usage: /fast-subagent:agent [name] — show details for a specific agent.",
|
|
749
130
|
getArgumentCompletions(prefix: string) {
|
|
@@ -769,6 +150,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
769
150
|
`Description: ${agent.description}`,
|
|
770
151
|
agent.model ? `Model: ${agent.model}` : "",
|
|
771
152
|
`Tools: ${formatTools(agent.tools)}`,
|
|
153
|
+
`Max subagent depth: ${agent.maxDepth}`,
|
|
772
154
|
agent.systemPrompt ? `\nSystem prompt:\n${agent.systemPrompt}` : "",
|
|
773
155
|
].filter(Boolean).join("\n");
|
|
774
156
|
ctx.ui.notify(lines, "info");
|
|
@@ -781,8 +163,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
781
163
|
"Add .md files to:\n" +
|
|
782
164
|
" ~/.pi/agent/agents/ (user-level)\n" +
|
|
783
165
|
" .pi/agents/ (project-level)\n" +
|
|
784
|
-
"\nFrontmatter required: name, description. Optional: model, tools.",
|
|
785
|
-
"info"
|
|
166
|
+
"\nFrontmatter required: name, description. Optional: model, tools, maxDepth.",
|
|
167
|
+
"info",
|
|
786
168
|
);
|
|
787
169
|
return;
|
|
788
170
|
}
|
|
@@ -793,15 +175,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
793
175
|
const lines: string[] = [`Agents (${agents.length}):`];
|
|
794
176
|
if (projectAgents.length) {
|
|
795
177
|
lines.push("\nProject (.pi/agents/):");
|
|
796
|
-
for (const a of projectAgents) {
|
|
797
|
-
lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
|
|
798
|
-
}
|
|
178
|
+
for (const a of projectAgents) lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
|
|
799
179
|
}
|
|
800
180
|
if (userAgents.length) {
|
|
801
181
|
lines.push("\nUser (~/.pi/agent/agents/):");
|
|
802
|
-
for (const a of userAgents) {
|
|
803
|
-
lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
|
|
804
|
-
}
|
|
182
|
+
for (const a of userAgents) lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
|
|
805
183
|
}
|
|
806
184
|
lines.push("");
|
|
807
185
|
lines.push("Tip: /fast-subagent:agent <name> for details · Add .md files to .pi/agents/ to create new agents");
|
|
@@ -809,7 +187,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
809
187
|
},
|
|
810
188
|
});
|
|
811
189
|
|
|
812
|
-
// ─── /bg
|
|
190
|
+
// ─── /fast-subagent:bg ────────────────────────────────────────────────────
|
|
813
191
|
pi.registerCommand("fast-subagent:bg", {
|
|
814
192
|
description: "Move a running foreground subagent to background. Shortcut: Ctrl+Shift+B. Usage: /fast-subagent:bg [fg-job-id] — omit ID to list active foreground jobs.",
|
|
815
193
|
getArgumentCompletions(_prefix: string) {
|
|
@@ -835,14 +213,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
835
213
|
return;
|
|
836
214
|
}
|
|
837
215
|
const bgJobId = entry.detach();
|
|
838
|
-
ctx.ui.notify(
|
|
839
|
-
`Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`,
|
|
840
|
-
"info",
|
|
841
|
-
);
|
|
216
|
+
ctx.ui.notify(`Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`, "info");
|
|
842
217
|
},
|
|
843
218
|
});
|
|
844
219
|
|
|
845
|
-
// ─── /bg-status
|
|
220
|
+
// ─── /fast-subagent:bg-status ─────────────────────────────────────────────
|
|
846
221
|
pi.registerCommand("fast-subagent:bg-status", {
|
|
847
222
|
description: "Show active background subagents. Usage: /fast-subagent:bg-status [sa-job-id] — omit ID to open selector.",
|
|
848
223
|
getArgumentCompletions(prefix: string) {
|
|
@@ -882,7 +257,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
882
257
|
},
|
|
883
258
|
});
|
|
884
259
|
|
|
885
|
-
// ─── /bg-cancel
|
|
260
|
+
// ─── /fast-subagent:bg-cancel ─────────────────────────────────────────────
|
|
886
261
|
pi.registerCommand("fast-subagent:bg-cancel", {
|
|
887
262
|
description: "Cancel running background subagent. Usage: /fast-subagent:bg-cancel [sa-job-id] — omit ID to choose with arrow keys.",
|
|
888
263
|
getArgumentCompletions(prefix: string) {
|
|
@@ -930,6 +305,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
930
305
|
},
|
|
931
306
|
});
|
|
932
307
|
|
|
308
|
+
// ─── `subagent` tool ──────────────────────────────────────────────────────
|
|
933
309
|
pi.registerTool({
|
|
934
310
|
name: "subagent",
|
|
935
311
|
label: "Subagent",
|
|
@@ -941,209 +317,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
941
317
|
].join(" "),
|
|
942
318
|
parameters: SubagentParams,
|
|
943
319
|
|
|
944
|
-
renderResult(result: AgentToolResult<unknown>,
|
|
945
|
-
|
|
946
|
-
const details = (result.details ?? {}) as SubagentDetails;
|
|
947
|
-
const toolCalls = details.toolCalls ?? [];
|
|
948
|
-
|
|
949
|
-
// ── Parallel / Chain mode renders ────────────────────────────────
|
|
950
|
-
if (details.mode === "parallel" && details.parallelAgents) {
|
|
951
|
-
const agents = details.parallelAgents;
|
|
952
|
-
const doneCount = agents.filter((a) => a.status === "done" || a.status === "error").length;
|
|
953
|
-
|
|
954
|
-
function agentToolRow(t: ToolCallEntry): string {
|
|
955
|
-
const arg = t.argSummary || "";
|
|
956
|
-
const call = `${t.name}(${arg})`;
|
|
957
|
-
if (t.result === undefined) return theme.fg("dim", call);
|
|
958
|
-
const dur = t.durMs != null ? (t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`) : "";
|
|
959
|
-
return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function wrapL(text: string, w: number): string[] {
|
|
963
|
-
try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const cache: { width?: number } = {};
|
|
967
|
-
return {
|
|
968
|
-
invalidate() { cache.width = undefined; },
|
|
969
|
-
render(width: number): string[] {
|
|
970
|
-
const out: string[] = [];
|
|
971
|
-
const header = details.running
|
|
972
|
-
? `Parallel (${doneCount}/${agents.length} done)`
|
|
973
|
-
: `Parallel: ${agents.filter((a) => a.status === "done").length}/${agents.length} succeeded`;
|
|
974
|
-
out.push(truncateToWidth(header, width, "..."));
|
|
975
|
-
|
|
976
|
-
for (const a of agents) {
|
|
977
|
-
const dur = a.durMs != null ? (a.durMs < 1000 ? ` ${a.durMs}ms` : ` ${(a.durMs / 1000).toFixed(1)}s`) : "";
|
|
978
|
-
const mark = a.status === "pending" ? theme.fg("dim", "⋅") : a.status === "running" ? theme.fg("dim", "→") : a.status === "done" ? `✓${dur}` : `✗${dur}`;
|
|
979
|
-
|
|
980
|
-
if (expanded) {
|
|
981
|
-
// Full solo-style block per agent
|
|
982
|
-
out.push("");
|
|
983
|
-
out.push(truncateToWidth(`[${a.name}] ${mark}`, width, "..."));
|
|
984
|
-
out.push(truncateToWidth(`Prompt:`, width, "..."));
|
|
985
|
-
out.push(truncateToWidth(` ${a.taskSummary}`, width, "..."));
|
|
986
|
-
for (const t of a.toolCalls ?? []) {
|
|
987
|
-
out.push(truncateToWidth(agentToolRow(t), width, "..."));
|
|
988
|
-
}
|
|
989
|
-
if (a.responseText) {
|
|
990
|
-
out.push("Response:");
|
|
991
|
-
const preview = truncateToVisualLines(a.responseText, 6, width - 2);
|
|
992
|
-
for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
|
|
993
|
-
if (preview.skippedCount > 0) out.push(truncateToWidth(theme.fg("dim", ` … ${preview.skippedCount} more lines`), width, "..."));
|
|
994
|
-
} else if (a.status === "running") {
|
|
995
|
-
out.push(theme.fg("dim", " running..."));
|
|
996
|
-
}
|
|
997
|
-
} else {
|
|
998
|
-
// Collapsed: compact one-liner
|
|
999
|
-
const row = ` [${a.name}] ${mark} ${a.taskSummary}`;
|
|
1000
|
-
out.push(truncateToWidth(row, width, "..."));
|
|
1001
|
-
// Show tool call rows compactly
|
|
1002
|
-
for (const t of a.toolCalls ?? []) {
|
|
1003
|
-
out.push(truncateToWidth(` ${agentToolRow(t)}`, width, "..."));
|
|
1004
|
-
}
|
|
1005
|
-
if (a.responseText && (a.status === "done" || a.status === "error")) {
|
|
1006
|
-
const preview = truncateToVisualLines(a.responseText, 2, width - 4);
|
|
1007
|
-
for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
out.push("");
|
|
1013
|
-
const status = details.running
|
|
1014
|
-
? ["running", details.usage?.turns ? `${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}` : ""].filter(Boolean).join(" · ")
|
|
1015
|
-
: formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
|
|
1016
|
-
const expandHint = !expanded ? keyHint("app.tools.expand", "expand for full output") : "";
|
|
1017
|
-
out.push(truncateToWidth([status, expandHint].filter(Boolean).join(" "), width, "..."));
|
|
1018
|
-
return out;
|
|
1019
|
-
},
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function statusLine(): string {
|
|
1024
|
-
if (details.backgroundJobId) return `moved to background · ${details.backgroundJobId}`;
|
|
1025
|
-
const prefix = details.agentName ? `${theme.fg("toolTitle", details.agentName)} · ` : "";
|
|
1026
|
-
if (details.running) {
|
|
1027
|
-
const parts: string[] = ["running"];
|
|
1028
|
-
if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
|
|
1029
|
-
if (details.elapsedMs != null) parts.push(formatDuration(details.elapsedMs));
|
|
1030
|
-
if (details.model) parts.push(details.model);
|
|
1031
|
-
return prefix + parts.join(" · ");
|
|
1032
|
-
}
|
|
1033
|
-
return prefix + formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Name(arg) ✓ 0.3s or Name(arg) (dim, still running)
|
|
1037
|
-
function toolRow(t: ToolCallEntry): string {
|
|
1038
|
-
const arg = t.argSummary ? t.argSummary : "";
|
|
1039
|
-
const call = `${t.name}(${arg})`;
|
|
1040
|
-
if (t.result === undefined) return theme.fg("dim", call);
|
|
1041
|
-
const dur = t.durMs != null
|
|
1042
|
-
? t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`
|
|
1043
|
-
: "";
|
|
1044
|
-
return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function wrapLine(text: string, w: number): string[] {
|
|
1048
|
-
try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
const cache: {
|
|
1052
|
-
width?: number;
|
|
1053
|
-
promptLines?: string[];
|
|
1054
|
-
promptSkipped?: number;
|
|
1055
|
-
responseLines?: string[];
|
|
1056
|
-
skipped?: number;
|
|
1057
|
-
} = {};
|
|
1058
|
-
|
|
1059
|
-
return {
|
|
1060
|
-
invalidate() { cache.width = undefined; },
|
|
1061
|
-
render(width: number): string[] {
|
|
1062
|
-
const out: string[] = [];
|
|
1063
|
-
const indent = " ";
|
|
1064
|
-
const ellipsisLine = (count: number) =>
|
|
1065
|
-
theme.fg("muted", `${indent}… (${count} more line${count === 1 ? "" : "s"})`);
|
|
1066
|
-
|
|
1067
|
-
// ── Prompt ────────────────────────────────────────────────────
|
|
1068
|
-
if (details.task) {
|
|
1069
|
-
out.push("Prompt:");
|
|
1070
|
-
if (expanded) {
|
|
1071
|
-
for (const line of details.task.split("\n")) {
|
|
1072
|
-
for (const w of wrapLine(indent + line, width)) out.push(w);
|
|
1073
|
-
}
|
|
1074
|
-
} else {
|
|
1075
|
-
// Up to 8 visual lines from the HEAD of the prompt (keep opening, not tail).
|
|
1076
|
-
const PROMPT_PREVIEW_LINES = 8;
|
|
1077
|
-
if (cache.width !== width || cache.promptLines === undefined) {
|
|
1078
|
-
const innerWidth = Math.max(1, width - indent.length);
|
|
1079
|
-
const allVisual: string[] = [];
|
|
1080
|
-
for (const raw of details.task.split("\n")) {
|
|
1081
|
-
for (const w of wrapLine(raw, innerWidth)) allVisual.push(w);
|
|
1082
|
-
}
|
|
1083
|
-
const head = allVisual.slice(0, PROMPT_PREVIEW_LINES);
|
|
1084
|
-
cache.promptLines = head.map((l) => truncateToWidth(indent + l, width, "..."));
|
|
1085
|
-
cache.promptSkipped = Math.max(0, allVisual.length - head.length);
|
|
1086
|
-
}
|
|
1087
|
-
out.push(...cache.promptLines);
|
|
1088
|
-
if ((cache.promptSkipped ?? 0) > 0) {
|
|
1089
|
-
out.push(truncateToWidth(ellipsisLine(cache.promptSkipped!), width, "..."));
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// ── Tool calls ─────────────────────────────────────────────
|
|
1095
|
-
for (const t of toolCalls) {
|
|
1096
|
-
out.push(truncateToWidth(toolRow(t), width, "..."));
|
|
1097
|
-
if (expanded && t.result !== undefined) {
|
|
1098
|
-
for (const line of t.result.split("\n")) {
|
|
1099
|
-
for (const w of wrapLine(theme.fg("dim", indent + line), width)) out.push(w);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// ── Response ────────────────────────────────────────────
|
|
1105
|
-
const responseText = agentText || (isPartial ? "" : "");
|
|
1106
|
-
if (responseText || isPartial) {
|
|
1107
|
-
out.push("Response:");
|
|
1108
|
-
if (expanded) {
|
|
1109
|
-
for (const line of responseText.split("\n")) {
|
|
1110
|
-
for (const w of wrapLine(indent + line, width)) out.push(w);
|
|
1111
|
-
}
|
|
1112
|
-
} else {
|
|
1113
|
-
const PREVIEW_LINES = 6;
|
|
1114
|
-
if (cache.width !== width) {
|
|
1115
|
-
const preview = truncateToVisualLines(responseText, PREVIEW_LINES, width - indent.length);
|
|
1116
|
-
cache.responseLines = preview.visualLines.map((l) => truncateToWidth(indent + l, width, "..."));
|
|
1117
|
-
cache.skipped = preview.skippedCount;
|
|
1118
|
-
cache.width = width;
|
|
1119
|
-
}
|
|
1120
|
-
// truncateToVisualLines keeps the tail — show ellipsis BEFORE the visible lines.
|
|
1121
|
-
if ((cache.skipped ?? 0) > 0) {
|
|
1122
|
-
out.push(truncateToWidth(ellipsisLine(cache.skipped!), width, "..."));
|
|
1123
|
-
}
|
|
1124
|
-
out.push(...(cache.responseLines ?? []));
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
// ── Status ───────────────────────────────────────────────
|
|
1129
|
-
const status = statusLine();
|
|
1130
|
-
const totalSkipped = (cache.skipped ?? 0) + (cache.promptSkipped ?? 0);
|
|
1131
|
-
const expandHint = !expanded && totalSkipped > 0
|
|
1132
|
-
? keyHint("app.tools.expand", `expand · ${totalSkipped} lines hidden`)
|
|
1133
|
-
: !expanded && toolCalls.some((t) => t.result !== undefined)
|
|
1134
|
-
? keyHint("app.tools.expand", "expand for tool outputs")
|
|
1135
|
-
: "";
|
|
1136
|
-
const statusWithHint = [status, expandHint].filter(Boolean).join(" ");
|
|
1137
|
-
if (statusWithHint) out.push(truncateToWidth(statusWithHint, width, "..."));
|
|
1138
|
-
if (details.running && !details.backgroundJobId)
|
|
1139
|
-
out.push(truncateToWidth(theme.fg("dim", "Ctrl+Shift+B: move to background"), width, "..."));
|
|
1140
|
-
|
|
1141
|
-
return out;
|
|
1142
|
-
},
|
|
1143
|
-
};
|
|
320
|
+
renderResult(result: AgentToolResult<unknown>, opts: ToolRenderResultOptions, theme: Theme) {
|
|
321
|
+
return renderSubagentResult(result, opts, theme);
|
|
1144
322
|
},
|
|
1145
323
|
|
|
1146
|
-
async execute(_id: string, params: Record<string, any>, signal: AbortSignal | undefined, onUpdate
|
|
324
|
+
async execute(_id: string, params: Record<string, any>, signal: AbortSignal | undefined, onUpdate, ctx: ExtensionContext): Promise<any> {
|
|
1147
325
|
const cwd = params.cwd ?? ctx.cwd;
|
|
1148
326
|
const agents = discoverAgents(cwd);
|
|
1149
327
|
|
|
@@ -1156,7 +334,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1156
334
|
return { agent: found };
|
|
1157
335
|
};
|
|
1158
336
|
|
|
1159
|
-
// ── Management: list
|
|
337
|
+
// ── Management: list ────────────────────────────────────────────────
|
|
1160
338
|
if (params.action === "list" || (!params.action && !params.agent && !params.tasks)) {
|
|
1161
339
|
if (agents.length === 0) {
|
|
1162
340
|
return {
|
|
@@ -1172,7 +350,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1172
350
|
return { content: [{ type: "text", text: `Agents (${agents.length}):\n${lines.join("\n")}` }] };
|
|
1173
351
|
}
|
|
1174
352
|
|
|
1175
|
-
// ── Management: get
|
|
353
|
+
// ── Management: get ─────────────────────────────────────────────────
|
|
1176
354
|
if (params.action === "get" && params.agent) {
|
|
1177
355
|
const { agent, error } = findAgent(params.agent);
|
|
1178
356
|
if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
|
|
@@ -1181,12 +359,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1181
359
|
`**Description:** ${agent.description}`,
|
|
1182
360
|
agent.model ? `**Model:** ${agent.model}` : null,
|
|
1183
361
|
`**Tools:** ${formatTools(agent.tools)}`,
|
|
362
|
+
`**Max subagent depth:** ${agent.maxDepth}`,
|
|
1184
363
|
agent.systemPrompt ? `\n**System prompt:**\n${agent.systemPrompt}` : null,
|
|
1185
364
|
].filter(Boolean).join("\n");
|
|
1186
365
|
return { content: [{ type: "text", text: info }] };
|
|
1187
366
|
}
|
|
1188
367
|
|
|
1189
|
-
// ── Background status
|
|
368
|
+
// ── Background status ───────────────────────────────────────────────
|
|
1190
369
|
if (params.action === "status") {
|
|
1191
370
|
const jobs = getBgManager().getAllJobs();
|
|
1192
371
|
if (jobs.length === 0) return { content: [{ type: "text", text: "No background jobs." }] };
|
|
@@ -1197,7 +376,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1197
376
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1198
377
|
}
|
|
1199
378
|
|
|
1200
|
-
// ── Background poll
|
|
379
|
+
// ── Background poll ─────────────────────────────────────────────────
|
|
1201
380
|
if (params.action === "poll") {
|
|
1202
381
|
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to poll." }] };
|
|
1203
382
|
const job = getBgManager().getJob(params.jobId);
|
|
@@ -1210,7 +389,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1210
389
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1211
390
|
}
|
|
1212
391
|
|
|
1213
|
-
// ── Background cancel
|
|
392
|
+
// ── Background cancel ───────────────────────────────────────────────
|
|
1214
393
|
if (params.action === "cancel") {
|
|
1215
394
|
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to cancel." }] };
|
|
1216
395
|
const result = getBgManager().cancel(params.jobId);
|
|
@@ -1220,7 +399,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1220
399
|
return { content: [{ type: "text", text: msg }] };
|
|
1221
400
|
}
|
|
1222
401
|
|
|
1223
|
-
// ── Foreground → background detach
|
|
402
|
+
// ── Foreground → background detach ──────────────────────────────────
|
|
1224
403
|
if (params.action === "detach") {
|
|
1225
404
|
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId (fg_xxxxx) to detach." }] };
|
|
1226
405
|
const fgEntry = _fgJobs.get(params.jobId);
|
|
@@ -1229,23 +408,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
1229
408
|
return { content: [{ type: "text", text: `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.` }] };
|
|
1230
409
|
}
|
|
1231
410
|
|
|
1232
|
-
// ── Single mode
|
|
411
|
+
// ── Single mode ─────────────────────────────────────────────────────
|
|
1233
412
|
if (params.agent && params.task) {
|
|
1234
413
|
const { agent, error } = findAgent(params.agent);
|
|
1235
414
|
if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
|
|
1236
415
|
|
|
1237
|
-
// Background dispatch — fire and forget
|
|
1238
416
|
if (params.background) {
|
|
1239
417
|
const bgAbort = new AbortController();
|
|
1240
418
|
const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
|
|
1241
419
|
const resultPromise: Promise<BackgroundJobResult> = runAgent(
|
|
1242
|
-
agent, params.task, cwd, params.model, bgAbort.signal, undefined
|
|
420
|
+
agent, params.task, cwd, params.model, bgAbort.signal, undefined,
|
|
1243
421
|
).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
|
|
1244
422
|
const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
|
|
1245
423
|
return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
|
|
1246
424
|
}
|
|
1247
425
|
|
|
1248
|
-
// Foreground run with detach support
|
|
1249
426
|
const fgId = `fg_${randomUUID().slice(0, 8)}`;
|
|
1250
427
|
const agentAbort = new AbortController();
|
|
1251
428
|
const forwardAbort = () => agentAbort.abort();
|
|
@@ -1254,18 +431,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1254
431
|
let detachResolveFn: ((bgJobId: string) => void) | null = null;
|
|
1255
432
|
const detachPromise = new Promise<string>((resolve) => { detachResolveFn = resolve; });
|
|
1256
433
|
|
|
1257
|
-
// Wrap onUpdate so detach can stop forwarding updates to the parent
|
|
1258
|
-
// agent's listener (which becomes invalid once execute() returns).
|
|
1259
434
|
let forwardUpdates = true;
|
|
1260
435
|
const wrappedOnUpdate: OnUpdate | undefined = onUpdate
|
|
1261
|
-
? (partial) => { if (forwardUpdates) onUpdate(partial); }
|
|
436
|
+
? (partial) => { if (forwardUpdates) (onUpdate as unknown as OnUpdate)(partial); }
|
|
1262
437
|
: undefined;
|
|
1263
438
|
|
|
1264
439
|
const agentRunPromise: Promise<RunResult> = runAgent(
|
|
1265
440
|
agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
|
|
1266
441
|
);
|
|
1267
442
|
|
|
1268
|
-
// Derived promise for the bg manager (used only if we detach)
|
|
1269
443
|
const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
|
|
1270
444
|
.then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
|
|
1271
445
|
|
|
@@ -1296,7 +470,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1296
470
|
});
|
|
1297
471
|
|
|
1298
472
|
if (outcome === "detached") {
|
|
1299
|
-
const bgJobId = await detachPromise;
|
|
473
|
+
const bgJobId = await detachPromise;
|
|
1300
474
|
return {
|
|
1301
475
|
content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
|
|
1302
476
|
details: {
|
|
@@ -1321,12 +495,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1321
495
|
elapsedMs: undefined,
|
|
1322
496
|
model: result.model,
|
|
1323
497
|
toolCalls: result.toolCalls,
|
|
498
|
+
executionEvents: result.executionEvents,
|
|
1324
499
|
} satisfies SubagentDetails,
|
|
1325
500
|
isError: result.exitCode !== 0,
|
|
1326
501
|
};
|
|
1327
502
|
}
|
|
1328
503
|
|
|
1329
|
-
// ── Parallel mode
|
|
504
|
+
// ── Parallel mode ───────────────────────────────────────────────────
|
|
1330
505
|
if (params.tasks && params.tasks.length > 0) {
|
|
1331
506
|
const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
|
|
1332
507
|
for (const t of params.tasks) {
|
|
@@ -1343,14 +518,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1343
518
|
}));
|
|
1344
519
|
let runningUsage = { ...emptyUsage };
|
|
1345
520
|
|
|
1346
|
-
const emitParallel = (running: boolean) => onUpdate?.({
|
|
521
|
+
const emitParallel = (running: boolean) => (onUpdate as unknown as OnUpdate | undefined)?.({
|
|
1347
522
|
content: [{ type: "text", text: "" }],
|
|
1348
523
|
details: { mode: "parallel", parallelAgents: [...parallelAgents], usage: { ...runningUsage }, running, toolCalls: [] } satisfies SubagentDetails,
|
|
1349
524
|
});
|
|
1350
525
|
|
|
1351
526
|
emitParallel(true);
|
|
1352
527
|
|
|
1353
|
-
const parentDepth =
|
|
528
|
+
const parentDepth = getCurrentDepth();
|
|
1354
529
|
const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
|
|
1355
530
|
parallelAgents[i]!.status = "running";
|
|
1356
531
|
emitParallel(true);
|
|
@@ -1389,8 +564,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1389
564
|
};
|
|
1390
565
|
}
|
|
1391
566
|
|
|
1392
|
-
// ── Chain mode ────────────────────────────────────────────
|
|
1393
|
-
// Shouldn't reach here
|
|
1394
567
|
return { content: [{ type: "text", text: "Provide agent+task or tasks array." }] };
|
|
1395
568
|
},
|
|
1396
569
|
});
|