pi-fast-subagent 0.8.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 +18 -0
- package/agents.ts +2 -2
- package/format.ts +119 -0
- package/index.ts +55 -897
- 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,575 +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 DEFAULT_MAX_DEPTH = 0;
|
|
261
|
-
const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
|
|
262
|
-
const MAX_DEPTH_ENV = "PI_FAST_SUBAGENT_MAX_DEPTH";
|
|
263
|
-
|
|
264
|
-
interface ToolCallEntry {
|
|
265
|
-
id: string;
|
|
266
|
-
name: string;
|
|
267
|
-
argSummary: string;
|
|
268
|
-
result?: string;
|
|
269
|
-
isError?: boolean;
|
|
270
|
-
durMs?: number;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
interface RunResult {
|
|
274
|
-
output: string;
|
|
275
|
-
exitCode: number;
|
|
276
|
-
error?: string;
|
|
277
|
-
model?: string;
|
|
278
|
-
toolCalls: ToolCallEntry[];
|
|
279
|
-
usage: { input: number; output: number; cost: number; turns: number };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
interface AgentRowStatus {
|
|
283
|
-
name: string;
|
|
284
|
-
taskSummary: string;
|
|
285
|
-
status: "pending" | "running" | "done" | "error";
|
|
286
|
-
durMs?: number;
|
|
287
|
-
toolCalls?: ToolCallEntry[];
|
|
288
|
-
responseText?: string;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
interface SubagentDetails {
|
|
292
|
-
mode?: "single" | "parallel";
|
|
293
|
-
agentName?: string;
|
|
294
|
-
task?: string;
|
|
295
|
-
// parallel
|
|
296
|
-
parallelAgents?: AgentRowStatus[];
|
|
297
|
-
usage: RunResult["usage"];
|
|
298
|
-
running: boolean;
|
|
299
|
-
elapsedMs?: number;
|
|
300
|
-
model?: string;
|
|
301
|
-
backgroundJobId?: string;
|
|
302
|
-
toolCalls: ToolCallEntry[];
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
type OnUpdate = (partial: { content: [{ type: "text"; text: string }]; details: unknown }) => void;
|
|
306
|
-
|
|
307
|
-
function formatDuration(ms: number): string {
|
|
308
|
-
const s = Math.max(0, Math.floor(ms / 1000));
|
|
309
|
-
const m = Math.floor(s / 60);
|
|
310
|
-
const rem = s % 60;
|
|
311
|
-
return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function summarizeTask(task: string, max = 60): string {
|
|
315
|
-
return task.length > max ? task.slice(0, max - 3) + "..." : task;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function formatBgJobSummary(job: BackgroundSubagentJob, now = Date.now()): string {
|
|
319
|
-
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
|
|
320
|
-
return `${job.id} [${job.status}] ${job.agentName} · ${dur} · ${summarizeTask(job.task)}`;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): string {
|
|
324
|
-
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
|
|
325
|
-
const lines = [`${job.id} [${job.status}] ${job.agentName} · ${dur}`, `Task: ${job.task}`];
|
|
326
|
-
if (job.model) lines.push(`Model: ${job.model}`);
|
|
327
|
-
if (job.status === "completed") lines.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
|
|
328
|
-
if (job.status === "failed") lines.push(`\nError: ${job.error ?? "(unknown)"}`);
|
|
329
|
-
if (job.status === "cancelled") lines.push("\nCancelled.");
|
|
330
|
-
if (job.status === "running") lines.push("\nStill running.");
|
|
331
|
-
return lines.join("\n");
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Module-level depth counters for nested in-process subagent calls.
|
|
335
|
-
let _currentDepth = 0;
|
|
336
|
-
let _currentMaxDepth = DEFAULT_MAX_DEPTH;
|
|
337
|
-
|
|
338
|
-
async function runAgent(
|
|
339
|
-
agent: AgentConfig,
|
|
340
|
-
task: string,
|
|
341
|
-
cwd: string,
|
|
342
|
-
modelOverride: string | undefined,
|
|
343
|
-
signal: AbortSignal | undefined,
|
|
344
|
-
onUpdate: OnUpdate | undefined,
|
|
345
|
-
parentDepth?: number,
|
|
346
|
-
): Promise<RunResult> {
|
|
347
|
-
const depth = parentDepth ?? _currentDepth;
|
|
348
|
-
const isNestedCall = depth > 0;
|
|
349
|
-
if (isNestedCall && depth > _currentMaxDepth) {
|
|
350
|
-
return {
|
|
351
|
-
output: "",
|
|
352
|
-
exitCode: 1,
|
|
353
|
-
error: `Nested subagents are disabled by default. Set maxDepth: ${depth} (or higher) in the parent agent frontmatter to allow this call.`,
|
|
354
|
-
toolCalls: [],
|
|
355
|
-
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const bootStartedAt = Date.now();
|
|
360
|
-
const { authStorage, modelRegistry } = getAuth();
|
|
361
|
-
const agentDir = getAgentDir();
|
|
362
|
-
const noExtensions = !agentNeedsExtensions(agent.tools);
|
|
363
|
-
const coldLoader = !isLoaderWarm(cwd, agentDir, noExtensions);
|
|
364
|
-
|
|
365
|
-
// Fire an immediate "running" emit so the UI draws the agent header + prompt
|
|
366
|
-
// before the (potentially slow) extension/session load. Without this, pi looks
|
|
367
|
-
// frozen while `loader.reload()` and `createAgentSession()` are in flight.
|
|
368
|
-
onUpdate?.({
|
|
369
|
-
content: [{ type: "text", text: "" }],
|
|
370
|
-
details: {
|
|
371
|
-
agentName: agent.name,
|
|
372
|
-
task,
|
|
373
|
-
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
374
|
-
running: true,
|
|
375
|
-
elapsedMs: 0,
|
|
376
|
-
model: modelOverride ?? agent.model,
|
|
377
|
-
toolCalls: [],
|
|
378
|
-
} satisfies SubagentDetails,
|
|
379
|
-
});
|
|
380
|
-
// Yield through timers when loader is cold so pi's render loop paints before
|
|
381
|
-
// CPU-heavy extension loading runs.
|
|
382
|
-
await allowUiPaint(coldLoader);
|
|
383
|
-
|
|
384
|
-
const loaderLease = await acquireResourceLoader(
|
|
385
|
-
cwd,
|
|
386
|
-
agentDir,
|
|
387
|
-
noExtensions,
|
|
388
|
-
agent.systemPrompt || undefined,
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
let session: Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
392
|
-
try {
|
|
393
|
-
const created = await createAgentSession({
|
|
394
|
-
cwd,
|
|
395
|
-
agentDir,
|
|
396
|
-
sessionManager: SessionManager.inMemory(cwd),
|
|
397
|
-
authStorage,
|
|
398
|
-
modelRegistry,
|
|
399
|
-
resourceLoader: loaderLease.loader,
|
|
400
|
-
});
|
|
401
|
-
session = created.session;
|
|
402
|
-
} catch (e) {
|
|
403
|
-
loaderLease.release();
|
|
404
|
-
return {
|
|
405
|
-
output: "",
|
|
406
|
-
exitCode: 1,
|
|
407
|
-
error: e instanceof Error ? e.message : String(e),
|
|
408
|
-
toolCalls: [],
|
|
409
|
-
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Resolve and apply model
|
|
414
|
-
const modelStr = modelOverride ?? agent.model;
|
|
415
|
-
if (modelStr) {
|
|
416
|
-
const [provider, ...rest] = modelStr.split("/");
|
|
417
|
-
const modelId = rest.join("/");
|
|
418
|
-
if (provider && modelId) {
|
|
419
|
-
const model = modelRegistry.find(provider, modelId);
|
|
420
|
-
if (model) await session.setModel(model);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Apply tools allowlist.
|
|
425
|
-
// "all" → no restriction (everything registered stays active)
|
|
426
|
-
// "none" → disable every tool
|
|
427
|
-
// string[] → explicit allowlist
|
|
428
|
-
if (agent.tools === "none") {
|
|
429
|
-
session.setActiveToolsByName([]);
|
|
430
|
-
} else if (Array.isArray(agent.tools) && agent.tools.length > 0) {
|
|
431
|
-
session.setActiveToolsByName(agent.tools);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Track output and usage
|
|
435
|
-
const usage = { input: 0, output: 0, cost: 0, turns: 0 };
|
|
436
|
-
let lastOutput = "";
|
|
437
|
-
let currentDelta = "";
|
|
438
|
-
let detectedModel: string | undefined;
|
|
439
|
-
const startedAt = bootStartedAt;
|
|
440
|
-
const configuredModel = modelOverride ?? agent.model;
|
|
441
|
-
const toolCalls: ToolCallEntry[] = [];
|
|
442
|
-
const toolStartTimes = new Map<string, number>();
|
|
443
|
-
|
|
444
|
-
let done = false;
|
|
445
|
-
|
|
446
|
-
function emitUpdate(): void {
|
|
447
|
-
if (done) return;
|
|
448
|
-
onUpdate?.({
|
|
449
|
-
content: [{ type: "text", text: currentDelta || lastOutput || "" }],
|
|
450
|
-
details: {
|
|
451
|
-
agentName: agent.name,
|
|
452
|
-
task,
|
|
453
|
-
usage,
|
|
454
|
-
running: true,
|
|
455
|
-
elapsedMs: Date.now() - startedAt,
|
|
456
|
-
model: detectedModel ?? configuredModel,
|
|
457
|
-
toolCalls: [...toolCalls],
|
|
458
|
-
} satisfies SubagentDetails,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
emitUpdate();
|
|
463
|
-
|
|
464
|
-
const heartbeat = setInterval(emitUpdate, 1000);
|
|
465
|
-
|
|
466
|
-
const unsubscribe = session.subscribe((event: any) => {
|
|
467
|
-
// Stream tool execution events
|
|
468
|
-
if (event.type === "tool_execution_start") {
|
|
469
|
-
toolStartTimes.set(event.toolCallId, Date.now());
|
|
470
|
-
toolCalls.push({
|
|
471
|
-
id: event.toolCallId,
|
|
472
|
-
name: event.toolName,
|
|
473
|
-
argSummary: summarizeToolArgs(event.toolName, event.args),
|
|
474
|
-
});
|
|
475
|
-
emitUpdate();
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (event.type === "tool_execution_end") {
|
|
480
|
-
const startedAtTool = toolStartTimes.get(event.toolCallId);
|
|
481
|
-
toolStartTimes.delete(event.toolCallId);
|
|
482
|
-
const resultText: string = (event.result?.content ?? [])
|
|
483
|
-
.filter((p: any) => p.type === "text")
|
|
484
|
-
.map((p: any) => p.text as string)
|
|
485
|
-
.join("\n");
|
|
486
|
-
let entry: ToolCallEntry | undefined;
|
|
487
|
-
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
488
|
-
if (toolCalls[i]!.id === event.toolCallId) { entry = toolCalls[i]; break; }
|
|
489
|
-
}
|
|
490
|
-
if (!entry) {
|
|
491
|
-
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
492
|
-
if (toolCalls[i]!.name === event.toolName && toolCalls[i]!.result === undefined) { entry = toolCalls[i]; break; }
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
if (entry) {
|
|
496
|
-
entry.result = resultText;
|
|
497
|
-
entry.isError = event.isError;
|
|
498
|
-
entry.durMs = startedAtTool != null ? Date.now() - startedAtTool : undefined;
|
|
499
|
-
}
|
|
500
|
-
emitUpdate();
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Stream text deltas live to the UI
|
|
505
|
-
if (event.type === "message_update") {
|
|
506
|
-
const e = event.assistantMessageEvent;
|
|
507
|
-
if (e?.type === "text_delta" && e.delta) {
|
|
508
|
-
currentDelta += e.delta;
|
|
509
|
-
emitUpdate();
|
|
510
|
-
}
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (event.type !== "message_end" || !event.message) return;
|
|
515
|
-
const msg = event.message;
|
|
516
|
-
if (msg.role !== "assistant") return; // usage/model only tracked for assistant turns
|
|
517
|
-
|
|
518
|
-
usage.turns++;
|
|
519
|
-
const u = msg.usage;
|
|
520
|
-
if (u) {
|
|
521
|
-
usage.input += u.input ?? 0;
|
|
522
|
-
usage.output += u.output ?? 0;
|
|
523
|
-
usage.cost += u.cost?.total ?? 0;
|
|
524
|
-
}
|
|
525
|
-
if (msg.model) detectedModel = msg.model;
|
|
526
|
-
|
|
527
|
-
// Extract last text content
|
|
528
|
-
for (const part of msg.content ?? []) {
|
|
529
|
-
if (part.type === "text") {
|
|
530
|
-
lastOutput = part.text;
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
// Reset delta accumulator for next turn
|
|
535
|
-
currentDelta = "";
|
|
536
|
-
|
|
537
|
-
onUpdate?.({
|
|
538
|
-
content: [{ type: "text", text: lastOutput || "(running...)" }],
|
|
539
|
-
details: {
|
|
540
|
-
agent: agent.name,
|
|
541
|
-
usage,
|
|
542
|
-
running: true,
|
|
543
|
-
elapsedMs: Date.now() - startedAt,
|
|
544
|
-
model: detectedModel ?? configuredModel,
|
|
545
|
-
},
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
// Propagate depth to nested calls. `maxDepth` is per-agent and defaults to 0,
|
|
550
|
-
// so subagents cannot spawn subagents unless their frontmatter opts in.
|
|
551
|
-
const prevEnvDepth = process.env[DEPTH_ENV];
|
|
552
|
-
const prevEnvMaxDepth = process.env[MAX_DEPTH_ENV];
|
|
553
|
-
const prevDepth = _currentDepth;
|
|
554
|
-
const prevMaxDepth = _currentMaxDepth;
|
|
555
|
-
const maxDepth = Math.max(DEFAULT_MAX_DEPTH, agent.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
556
|
-
_currentDepth = depth + 1;
|
|
557
|
-
_currentMaxDepth = depth + maxDepth;
|
|
558
|
-
process.env[DEPTH_ENV] = String(_currentDepth);
|
|
559
|
-
process.env[MAX_DEPTH_ENV] = String(_currentMaxDepth);
|
|
560
|
-
|
|
561
|
-
let exitCode = 0;
|
|
562
|
-
let error: string | undefined;
|
|
563
|
-
|
|
564
|
-
try {
|
|
565
|
-
if (signal?.aborted) throw new Error("Aborted");
|
|
566
|
-
|
|
567
|
-
const onAbort = () => void session.abort();
|
|
568
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
569
|
-
try {
|
|
570
|
-
await session.prompt(task);
|
|
571
|
-
} finally {
|
|
572
|
-
signal?.removeEventListener("abort", onAbort);
|
|
573
|
-
}
|
|
574
|
-
} catch (e) {
|
|
575
|
-
exitCode = 1;
|
|
576
|
-
error = signal?.aborted ? "Aborted" : e instanceof Error ? e.message : String(e);
|
|
577
|
-
} finally {
|
|
578
|
-
done = true;
|
|
579
|
-
clearInterval(heartbeat);
|
|
580
|
-
unsubscribe();
|
|
581
|
-
session.dispose();
|
|
582
|
-
loaderLease.release();
|
|
583
|
-
if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
|
|
584
|
-
else process.env[DEPTH_ENV] = prevEnvDepth;
|
|
585
|
-
if (prevEnvMaxDepth === undefined) delete process.env[MAX_DEPTH_ENV];
|
|
586
|
-
else process.env[MAX_DEPTH_ENV] = prevEnvMaxDepth;
|
|
587
|
-
_currentDepth = prevDepth;
|
|
588
|
-
_currentMaxDepth = prevMaxDepth;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
595
|
-
|
|
596
|
-
async function mapConcurrent<TIn, TOut>(
|
|
597
|
-
items: TIn[],
|
|
598
|
-
concurrency: number,
|
|
599
|
-
fn: (item: TIn, i: number) => Promise<TOut>,
|
|
600
|
-
): Promise<TOut[]> {
|
|
601
|
-
if (!items.length) return [];
|
|
602
|
-
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
603
|
-
const results: TOut[] = new Array(items.length);
|
|
604
|
-
let next = 0;
|
|
605
|
-
await Promise.all(
|
|
606
|
-
Array.from({ length: limit }, async () => {
|
|
607
|
-
while (true) {
|
|
608
|
-
const i = next++;
|
|
609
|
-
if (i >= items.length) return;
|
|
610
|
-
results[i] = await fn(items[i], i);
|
|
611
|
-
}
|
|
612
|
-
}),
|
|
613
|
-
);
|
|
614
|
-
return results;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function formatTokens(n: number): string {
|
|
618
|
-
if (n < 1000) return String(n);
|
|
619
|
-
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
620
|
-
return `${Math.round(n / 1000)}k`;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function formatUsage(usage: RunResult["usage"], model?: string): string {
|
|
624
|
-
const parts: string[] = [];
|
|
625
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
626
|
-
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
627
|
-
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
628
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
629
|
-
if (model) parts.push(model);
|
|
630
|
-
return parts.join(" ");
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function getFinalText(r: RunResult): string {
|
|
634
|
-
if (r.exitCode !== 0) return `Error: ${r.error ?? r.output ?? "(no output)"}`;
|
|
635
|
-
return r.output || "(no output)";
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// ─── Tool schemas ─────────────────────────────────────────────────────────────
|
|
639
|
-
|
|
640
|
-
const TaskItem = Type.Object({
|
|
641
|
-
agent: Type.String({ description: "Agent name" }),
|
|
642
|
-
task: Type.String({ description: "Task to delegate" }),
|
|
643
|
-
model: Type.Optional(Type.String({ description: "Model override (provider/model)" })),
|
|
644
|
-
cwd: Type.Optional(Type.String({ description: "Working directory" })),
|
|
645
|
-
count: Type.Optional(Type.Number({ description: "Repeat this task N times" })),
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
const SubagentParams = Type.Object({
|
|
649
|
-
// Single mode
|
|
650
|
-
agent: Type.Optional(Type.String({ description: "Agent name (single mode)" })),
|
|
651
|
-
task: Type.Optional(Type.String({ description: "Task (single mode)" })),
|
|
652
|
-
model: Type.Optional(Type.String({ description: "Model override (single mode)" })),
|
|
653
|
-
cwd: Type.Optional(Type.String({ description: "Working directory" })),
|
|
654
|
-
|
|
655
|
-
// Parallel mode
|
|
656
|
-
tasks: Type.Optional(
|
|
657
|
-
Type.Array(TaskItem, {
|
|
658
|
-
description: "Array of {agent, task} for parallel execution. Use count to repeat one task.",
|
|
659
|
-
}),
|
|
660
|
-
),
|
|
661
|
-
concurrency: Type.Optional(
|
|
662
|
-
Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
|
|
663
|
-
),
|
|
664
|
-
|
|
665
|
-
// Background
|
|
666
|
-
background: Type.Optional(Type.Boolean({ description: "Run in background, returns job ID immediately" })),
|
|
667
|
-
jobId: Type.Optional(Type.String({ description: "Job ID for poll/cancel" })),
|
|
668
|
-
|
|
669
|
-
// Management
|
|
670
|
-
action: Type.Optional(
|
|
671
|
-
Type.Union(
|
|
672
|
-
[
|
|
673
|
-
Type.Literal("list"),
|
|
674
|
-
Type.Literal("get"),
|
|
675
|
-
Type.Literal("status"),
|
|
676
|
-
Type.Literal("poll"),
|
|
677
|
-
Type.Literal("cancel"),
|
|
678
|
-
Type.Literal("detach"),
|
|
679
|
-
],
|
|
680
|
-
{ description: "'list'/'get' for agents, 'status' for bg jobs, 'poll'/'cancel' for a specific job, 'detach' to move a foreground job to background" },
|
|
681
|
-
),
|
|
682
|
-
),
|
|
683
|
-
agentScope: Type.Optional(
|
|
684
|
-
Type.Union(
|
|
685
|
-
[Type.Literal("user"), Type.Literal("project"), Type.Literal("both")],
|
|
686
|
-
{ description: "Agent scope filter", default: "both" },
|
|
687
|
-
),
|
|
688
|
-
),
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
// ─── Extension entry point ────────────────────────────────────────────────────
|
|
61
|
+
// ─── Extension entry point ──────────────────────────────────────────────────
|
|
692
62
|
|
|
693
63
|
export default function (pi: ExtensionAPI) {
|
|
694
|
-
// ─── Status keys ────────────────────────────────────────────────────────────────────
|
|
695
64
|
const BG_STATUS_KEY = "fast-subagent-bg";
|
|
696
65
|
const FG_STATUS_KEY = "fast-subagent-fg";
|
|
697
66
|
|
|
698
|
-
// ─── Background job lifecycle ─────────────────────────────────────────────────────
|
|
699
67
|
_onBgJobComplete = (job) => {
|
|
700
68
|
refreshBgStatus();
|
|
701
69
|
const elapsed = job.completedAt ? ((job.completedAt - job.startedAt) / 1000).toFixed(1) : "?";
|
|
@@ -719,12 +87,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
719
87
|
pi.on("session_start", async (_event, ctx) => {
|
|
720
88
|
_setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
|
|
721
89
|
|
|
722
|
-
// Warm one extension-capable loader after startup
|
|
723
|
-
// call
|
|
90
|
+
// Warm one extension-capable loader after startup so first `tools: all`
|
|
91
|
+
// subagent call reuses loaded extensions instead of blocking.
|
|
724
92
|
if (process.env.PI_FAST_SUBAGENT_WARM !== "0") {
|
|
725
93
|
const warmCwd = ctx.cwd;
|
|
726
94
|
const warmAgentDir = getAgentDir();
|
|
727
|
-
setTimeout(() =>
|
|
95
|
+
setTimeout(() => defaultLoaderPool.warm(warmCwd, warmAgentDir, false), 1000);
|
|
728
96
|
}
|
|
729
97
|
});
|
|
730
98
|
|
|
@@ -732,10 +100,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
732
100
|
getBgManager().shutdown();
|
|
733
101
|
_bgManager = null;
|
|
734
102
|
_setBgStatus = null;
|
|
735
|
-
|
|
103
|
+
defaultLoaderPool.clear();
|
|
736
104
|
});
|
|
737
105
|
|
|
738
|
-
// ─── Ctrl+Shift+B —
|
|
106
|
+
// ─── Ctrl+Shift+B — detach foreground subagent ────────────────────────────
|
|
739
107
|
pi.registerShortcut(Key.ctrlShift("b"), {
|
|
740
108
|
description: "Move foreground subagent to background",
|
|
741
109
|
handler: async (ctx) => {
|
|
@@ -756,7 +124,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
756
124
|
},
|
|
757
125
|
});
|
|
758
126
|
|
|
759
|
-
// ─── /agent
|
|
127
|
+
// ─── /fast-subagent:agent ─────────────────────────────────────────────────
|
|
760
128
|
pi.registerCommand("fast-subagent:agent", {
|
|
761
129
|
description: "List available subagents. Usage: /fast-subagent:agent [name] — show details for a specific agent.",
|
|
762
130
|
getArgumentCompletions(prefix: string) {
|
|
@@ -796,7 +164,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
796
164
|
" ~/.pi/agent/agents/ (user-level)\n" +
|
|
797
165
|
" .pi/agents/ (project-level)\n" +
|
|
798
166
|
"\nFrontmatter required: name, description. Optional: model, tools, maxDepth.",
|
|
799
|
-
"info"
|
|
167
|
+
"info",
|
|
800
168
|
);
|
|
801
169
|
return;
|
|
802
170
|
}
|
|
@@ -807,15 +175,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
807
175
|
const lines: string[] = [`Agents (${agents.length}):`];
|
|
808
176
|
if (projectAgents.length) {
|
|
809
177
|
lines.push("\nProject (.pi/agents/):");
|
|
810
|
-
for (const a of projectAgents) {
|
|
811
|
-
lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
|
|
812
|
-
}
|
|
178
|
+
for (const a of projectAgents) lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
|
|
813
179
|
}
|
|
814
180
|
if (userAgents.length) {
|
|
815
181
|
lines.push("\nUser (~/.pi/agent/agents/):");
|
|
816
|
-
for (const a of userAgents) {
|
|
817
|
-
lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
|
|
818
|
-
}
|
|
182
|
+
for (const a of userAgents) lines.push(` ${a.name}${a.model ? ` [${a.model}]` : ""} — ${a.description}`);
|
|
819
183
|
}
|
|
820
184
|
lines.push("");
|
|
821
185
|
lines.push("Tip: /fast-subagent:agent <name> for details · Add .md files to .pi/agents/ to create new agents");
|
|
@@ -823,7 +187,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
823
187
|
},
|
|
824
188
|
});
|
|
825
189
|
|
|
826
|
-
// ─── /bg
|
|
190
|
+
// ─── /fast-subagent:bg ────────────────────────────────────────────────────
|
|
827
191
|
pi.registerCommand("fast-subagent:bg", {
|
|
828
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.",
|
|
829
193
|
getArgumentCompletions(_prefix: string) {
|
|
@@ -849,14 +213,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
849
213
|
return;
|
|
850
214
|
}
|
|
851
215
|
const bgJobId = entry.detach();
|
|
852
|
-
ctx.ui.notify(
|
|
853
|
-
`Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`,
|
|
854
|
-
"info",
|
|
855
|
-
);
|
|
216
|
+
ctx.ui.notify(`Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`, "info");
|
|
856
217
|
},
|
|
857
218
|
});
|
|
858
219
|
|
|
859
|
-
// ─── /bg-status
|
|
220
|
+
// ─── /fast-subagent:bg-status ─────────────────────────────────────────────
|
|
860
221
|
pi.registerCommand("fast-subagent:bg-status", {
|
|
861
222
|
description: "Show active background subagents. Usage: /fast-subagent:bg-status [sa-job-id] — omit ID to open selector.",
|
|
862
223
|
getArgumentCompletions(prefix: string) {
|
|
@@ -896,7 +257,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
896
257
|
},
|
|
897
258
|
});
|
|
898
259
|
|
|
899
|
-
// ─── /bg-cancel
|
|
260
|
+
// ─── /fast-subagent:bg-cancel ─────────────────────────────────────────────
|
|
900
261
|
pi.registerCommand("fast-subagent:bg-cancel", {
|
|
901
262
|
description: "Cancel running background subagent. Usage: /fast-subagent:bg-cancel [sa-job-id] — omit ID to choose with arrow keys.",
|
|
902
263
|
getArgumentCompletions(prefix: string) {
|
|
@@ -944,6 +305,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
944
305
|
},
|
|
945
306
|
});
|
|
946
307
|
|
|
308
|
+
// ─── `subagent` tool ──────────────────────────────────────────────────────
|
|
947
309
|
pi.registerTool({
|
|
948
310
|
name: "subagent",
|
|
949
311
|
label: "Subagent",
|
|
@@ -955,209 +317,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
955
317
|
].join(" "),
|
|
956
318
|
parameters: SubagentParams,
|
|
957
319
|
|
|
958
|
-
renderResult(result: AgentToolResult<unknown>,
|
|
959
|
-
|
|
960
|
-
const details = (result.details ?? {}) as SubagentDetails;
|
|
961
|
-
const toolCalls = details.toolCalls ?? [];
|
|
962
|
-
|
|
963
|
-
// ── Parallel / Chain mode renders ────────────────────────────────
|
|
964
|
-
if (details.mode === "parallel" && details.parallelAgents) {
|
|
965
|
-
const agents = details.parallelAgents;
|
|
966
|
-
const doneCount = agents.filter((a) => a.status === "done" || a.status === "error").length;
|
|
967
|
-
|
|
968
|
-
function agentToolRow(t: ToolCallEntry): string {
|
|
969
|
-
const arg = t.argSummary || "";
|
|
970
|
-
const call = `${t.name}(${arg})`;
|
|
971
|
-
if (t.result === undefined) return theme.fg("dim", call);
|
|
972
|
-
const dur = t.durMs != null ? (t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`) : "";
|
|
973
|
-
return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
function wrapL(text: string, w: number): string[] {
|
|
977
|
-
try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
const cache: { width?: number } = {};
|
|
981
|
-
return {
|
|
982
|
-
invalidate() { cache.width = undefined; },
|
|
983
|
-
render(width: number): string[] {
|
|
984
|
-
const out: string[] = [];
|
|
985
|
-
const header = details.running
|
|
986
|
-
? `Parallel (${doneCount}/${agents.length} done)`
|
|
987
|
-
: `Parallel: ${agents.filter((a) => a.status === "done").length}/${agents.length} succeeded`;
|
|
988
|
-
out.push(truncateToWidth(header, width, "..."));
|
|
989
|
-
|
|
990
|
-
for (const a of agents) {
|
|
991
|
-
const dur = a.durMs != null ? (a.durMs < 1000 ? ` ${a.durMs}ms` : ` ${(a.durMs / 1000).toFixed(1)}s`) : "";
|
|
992
|
-
const mark = a.status === "pending" ? theme.fg("dim", "⋅") : a.status === "running" ? theme.fg("dim", "→") : a.status === "done" ? `✓${dur}` : `✗${dur}`;
|
|
993
|
-
|
|
994
|
-
if (expanded) {
|
|
995
|
-
// Full solo-style block per agent
|
|
996
|
-
out.push("");
|
|
997
|
-
out.push(truncateToWidth(`[${a.name}] ${mark}`, width, "..."));
|
|
998
|
-
out.push(truncateToWidth(`Prompt:`, width, "..."));
|
|
999
|
-
out.push(truncateToWidth(` ${a.taskSummary}`, width, "..."));
|
|
1000
|
-
for (const t of a.toolCalls ?? []) {
|
|
1001
|
-
out.push(truncateToWidth(agentToolRow(t), width, "..."));
|
|
1002
|
-
}
|
|
1003
|
-
if (a.responseText) {
|
|
1004
|
-
out.push("Response:");
|
|
1005
|
-
const preview = truncateToVisualLines(a.responseText, 6, width - 2);
|
|
1006
|
-
for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
|
|
1007
|
-
if (preview.skippedCount > 0) out.push(truncateToWidth(theme.fg("dim", ` … ${preview.skippedCount} more lines`), width, "..."));
|
|
1008
|
-
} else if (a.status === "running") {
|
|
1009
|
-
out.push(theme.fg("dim", " running..."));
|
|
1010
|
-
}
|
|
1011
|
-
} else {
|
|
1012
|
-
// Collapsed: compact one-liner
|
|
1013
|
-
const row = ` [${a.name}] ${mark} ${a.taskSummary}`;
|
|
1014
|
-
out.push(truncateToWidth(row, width, "..."));
|
|
1015
|
-
// Show tool call rows compactly
|
|
1016
|
-
for (const t of a.toolCalls ?? []) {
|
|
1017
|
-
out.push(truncateToWidth(` ${agentToolRow(t)}`, width, "..."));
|
|
1018
|
-
}
|
|
1019
|
-
if (a.responseText && (a.status === "done" || a.status === "error")) {
|
|
1020
|
-
const preview = truncateToVisualLines(a.responseText, 2, width - 4);
|
|
1021
|
-
for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
out.push("");
|
|
1027
|
-
const status = details.running
|
|
1028
|
-
? ["running", details.usage?.turns ? `${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}` : ""].filter(Boolean).join(" · ")
|
|
1029
|
-
: formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
|
|
1030
|
-
const expandHint = !expanded ? keyHint("app.tools.expand", "expand for full output") : "";
|
|
1031
|
-
out.push(truncateToWidth([status, expandHint].filter(Boolean).join(" "), width, "..."));
|
|
1032
|
-
return out;
|
|
1033
|
-
},
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
function statusLine(): string {
|
|
1038
|
-
if (details.backgroundJobId) return `moved to background · ${details.backgroundJobId}`;
|
|
1039
|
-
const prefix = details.agentName ? `${theme.fg("toolTitle", details.agentName)} · ` : "";
|
|
1040
|
-
if (details.running) {
|
|
1041
|
-
const parts: string[] = ["running"];
|
|
1042
|
-
if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
|
|
1043
|
-
if (details.elapsedMs != null) parts.push(formatDuration(details.elapsedMs));
|
|
1044
|
-
if (details.model) parts.push(details.model);
|
|
1045
|
-
return prefix + parts.join(" · ");
|
|
1046
|
-
}
|
|
1047
|
-
return prefix + formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Name(arg) ✓ 0.3s or Name(arg) (dim, still running)
|
|
1051
|
-
function toolRow(t: ToolCallEntry): string {
|
|
1052
|
-
const arg = t.argSummary ? t.argSummary : "";
|
|
1053
|
-
const call = `${t.name}(${arg})`;
|
|
1054
|
-
if (t.result === undefined) return theme.fg("dim", call);
|
|
1055
|
-
const dur = t.durMs != null
|
|
1056
|
-
? t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`
|
|
1057
|
-
: "";
|
|
1058
|
-
return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
function wrapLine(text: string, w: number): string[] {
|
|
1062
|
-
try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
const cache: {
|
|
1066
|
-
width?: number;
|
|
1067
|
-
promptLines?: string[];
|
|
1068
|
-
promptSkipped?: number;
|
|
1069
|
-
responseLines?: string[];
|
|
1070
|
-
skipped?: number;
|
|
1071
|
-
} = {};
|
|
1072
|
-
|
|
1073
|
-
return {
|
|
1074
|
-
invalidate() { cache.width = undefined; },
|
|
1075
|
-
render(width: number): string[] {
|
|
1076
|
-
const out: string[] = [];
|
|
1077
|
-
const indent = " ";
|
|
1078
|
-
const ellipsisLine = (count: number) =>
|
|
1079
|
-
theme.fg("muted", `${indent}… (${count} more line${count === 1 ? "" : "s"})`);
|
|
1080
|
-
|
|
1081
|
-
// ── Prompt ────────────────────────────────────────────────────
|
|
1082
|
-
if (details.task) {
|
|
1083
|
-
out.push("Prompt:");
|
|
1084
|
-
if (expanded) {
|
|
1085
|
-
for (const line of details.task.split("\n")) {
|
|
1086
|
-
for (const w of wrapLine(indent + line, width)) out.push(w);
|
|
1087
|
-
}
|
|
1088
|
-
} else {
|
|
1089
|
-
// Up to 8 visual lines from the HEAD of the prompt (keep opening, not tail).
|
|
1090
|
-
const PROMPT_PREVIEW_LINES = 8;
|
|
1091
|
-
if (cache.width !== width || cache.promptLines === undefined) {
|
|
1092
|
-
const innerWidth = Math.max(1, width - indent.length);
|
|
1093
|
-
const allVisual: string[] = [];
|
|
1094
|
-
for (const raw of details.task.split("\n")) {
|
|
1095
|
-
for (const w of wrapLine(raw, innerWidth)) allVisual.push(w);
|
|
1096
|
-
}
|
|
1097
|
-
const head = allVisual.slice(0, PROMPT_PREVIEW_LINES);
|
|
1098
|
-
cache.promptLines = head.map((l) => truncateToWidth(indent + l, width, "..."));
|
|
1099
|
-
cache.promptSkipped = Math.max(0, allVisual.length - head.length);
|
|
1100
|
-
}
|
|
1101
|
-
out.push(...cache.promptLines);
|
|
1102
|
-
if ((cache.promptSkipped ?? 0) > 0) {
|
|
1103
|
-
out.push(truncateToWidth(ellipsisLine(cache.promptSkipped!), width, "..."));
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// ── Tool calls ─────────────────────────────────────────────
|
|
1109
|
-
for (const t of toolCalls) {
|
|
1110
|
-
out.push(truncateToWidth(toolRow(t), width, "..."));
|
|
1111
|
-
if (expanded && t.result !== undefined) {
|
|
1112
|
-
for (const line of t.result.split("\n")) {
|
|
1113
|
-
for (const w of wrapLine(theme.fg("dim", indent + line), width)) out.push(w);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// ── Response ────────────────────────────────────────────
|
|
1119
|
-
const responseText = agentText || (isPartial ? "" : "");
|
|
1120
|
-
if (responseText || isPartial) {
|
|
1121
|
-
out.push("Response:");
|
|
1122
|
-
if (expanded) {
|
|
1123
|
-
for (const line of responseText.split("\n")) {
|
|
1124
|
-
for (const w of wrapLine(indent + line, width)) out.push(w);
|
|
1125
|
-
}
|
|
1126
|
-
} else {
|
|
1127
|
-
const PREVIEW_LINES = 6;
|
|
1128
|
-
if (cache.width !== width) {
|
|
1129
|
-
const preview = truncateToVisualLines(responseText, PREVIEW_LINES, width - indent.length);
|
|
1130
|
-
cache.responseLines = preview.visualLines.map((l) => truncateToWidth(indent + l, width, "..."));
|
|
1131
|
-
cache.skipped = preview.skippedCount;
|
|
1132
|
-
cache.width = width;
|
|
1133
|
-
}
|
|
1134
|
-
// truncateToVisualLines keeps the tail — show ellipsis BEFORE the visible lines.
|
|
1135
|
-
if ((cache.skipped ?? 0) > 0) {
|
|
1136
|
-
out.push(truncateToWidth(ellipsisLine(cache.skipped!), width, "..."));
|
|
1137
|
-
}
|
|
1138
|
-
out.push(...(cache.responseLines ?? []));
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// ── Status ───────────────────────────────────────────────
|
|
1143
|
-
const status = statusLine();
|
|
1144
|
-
const totalSkipped = (cache.skipped ?? 0) + (cache.promptSkipped ?? 0);
|
|
1145
|
-
const expandHint = !expanded && totalSkipped > 0
|
|
1146
|
-
? keyHint("app.tools.expand", `expand · ${totalSkipped} lines hidden`)
|
|
1147
|
-
: !expanded && toolCalls.some((t) => t.result !== undefined)
|
|
1148
|
-
? keyHint("app.tools.expand", "expand for tool outputs")
|
|
1149
|
-
: "";
|
|
1150
|
-
const statusWithHint = [status, expandHint].filter(Boolean).join(" ");
|
|
1151
|
-
if (statusWithHint) out.push(truncateToWidth(statusWithHint, width, "..."));
|
|
1152
|
-
if (details.running && !details.backgroundJobId)
|
|
1153
|
-
out.push(truncateToWidth(theme.fg("dim", "Ctrl+Shift+B: move to background"), width, "..."));
|
|
1154
|
-
|
|
1155
|
-
return out;
|
|
1156
|
-
},
|
|
1157
|
-
};
|
|
320
|
+
renderResult(result: AgentToolResult<unknown>, opts: ToolRenderResultOptions, theme: Theme) {
|
|
321
|
+
return renderSubagentResult(result, opts, theme);
|
|
1158
322
|
},
|
|
1159
323
|
|
|
1160
|
-
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> {
|
|
1161
325
|
const cwd = params.cwd ?? ctx.cwd;
|
|
1162
326
|
const agents = discoverAgents(cwd);
|
|
1163
327
|
|
|
@@ -1170,7 +334,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1170
334
|
return { agent: found };
|
|
1171
335
|
};
|
|
1172
336
|
|
|
1173
|
-
// ── Management: list
|
|
337
|
+
// ── Management: list ────────────────────────────────────────────────
|
|
1174
338
|
if (params.action === "list" || (!params.action && !params.agent && !params.tasks)) {
|
|
1175
339
|
if (agents.length === 0) {
|
|
1176
340
|
return {
|
|
@@ -1186,7 +350,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1186
350
|
return { content: [{ type: "text", text: `Agents (${agents.length}):\n${lines.join("\n")}` }] };
|
|
1187
351
|
}
|
|
1188
352
|
|
|
1189
|
-
// ── Management: get
|
|
353
|
+
// ── Management: get ─────────────────────────────────────────────────
|
|
1190
354
|
if (params.action === "get" && params.agent) {
|
|
1191
355
|
const { agent, error } = findAgent(params.agent);
|
|
1192
356
|
if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
|
|
@@ -1201,7 +365,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1201
365
|
return { content: [{ type: "text", text: info }] };
|
|
1202
366
|
}
|
|
1203
367
|
|
|
1204
|
-
// ── Background status
|
|
368
|
+
// ── Background status ───────────────────────────────────────────────
|
|
1205
369
|
if (params.action === "status") {
|
|
1206
370
|
const jobs = getBgManager().getAllJobs();
|
|
1207
371
|
if (jobs.length === 0) return { content: [{ type: "text", text: "No background jobs." }] };
|
|
@@ -1212,7 +376,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1212
376
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1213
377
|
}
|
|
1214
378
|
|
|
1215
|
-
// ── Background poll
|
|
379
|
+
// ── Background poll ─────────────────────────────────────────────────
|
|
1216
380
|
if (params.action === "poll") {
|
|
1217
381
|
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to poll." }] };
|
|
1218
382
|
const job = getBgManager().getJob(params.jobId);
|
|
@@ -1225,7 +389,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1225
389
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1226
390
|
}
|
|
1227
391
|
|
|
1228
|
-
// ── Background cancel
|
|
392
|
+
// ── Background cancel ───────────────────────────────────────────────
|
|
1229
393
|
if (params.action === "cancel") {
|
|
1230
394
|
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to cancel." }] };
|
|
1231
395
|
const result = getBgManager().cancel(params.jobId);
|
|
@@ -1235,7 +399,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1235
399
|
return { content: [{ type: "text", text: msg }] };
|
|
1236
400
|
}
|
|
1237
401
|
|
|
1238
|
-
// ── Foreground → background detach
|
|
402
|
+
// ── Foreground → background detach ──────────────────────────────────
|
|
1239
403
|
if (params.action === "detach") {
|
|
1240
404
|
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId (fg_xxxxx) to detach." }] };
|
|
1241
405
|
const fgEntry = _fgJobs.get(params.jobId);
|
|
@@ -1244,23 +408,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
1244
408
|
return { content: [{ type: "text", text: `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.` }] };
|
|
1245
409
|
}
|
|
1246
410
|
|
|
1247
|
-
// ── Single mode
|
|
411
|
+
// ── Single mode ─────────────────────────────────────────────────────
|
|
1248
412
|
if (params.agent && params.task) {
|
|
1249
413
|
const { agent, error } = findAgent(params.agent);
|
|
1250
414
|
if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
|
|
1251
415
|
|
|
1252
|
-
// Background dispatch — fire and forget
|
|
1253
416
|
if (params.background) {
|
|
1254
417
|
const bgAbort = new AbortController();
|
|
1255
418
|
const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
|
|
1256
419
|
const resultPromise: Promise<BackgroundJobResult> = runAgent(
|
|
1257
|
-
agent, params.task, cwd, params.model, bgAbort.signal, undefined
|
|
420
|
+
agent, params.task, cwd, params.model, bgAbort.signal, undefined,
|
|
1258
421
|
).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
|
|
1259
422
|
const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
|
|
1260
423
|
return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
|
|
1261
424
|
}
|
|
1262
425
|
|
|
1263
|
-
// Foreground run with detach support
|
|
1264
426
|
const fgId = `fg_${randomUUID().slice(0, 8)}`;
|
|
1265
427
|
const agentAbort = new AbortController();
|
|
1266
428
|
const forwardAbort = () => agentAbort.abort();
|
|
@@ -1269,18 +431,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1269
431
|
let detachResolveFn: ((bgJobId: string) => void) | null = null;
|
|
1270
432
|
const detachPromise = new Promise<string>((resolve) => { detachResolveFn = resolve; });
|
|
1271
433
|
|
|
1272
|
-
// Wrap onUpdate so detach can stop forwarding updates to the parent
|
|
1273
|
-
// agent's listener (which becomes invalid once execute() returns).
|
|
1274
434
|
let forwardUpdates = true;
|
|
1275
435
|
const wrappedOnUpdate: OnUpdate | undefined = onUpdate
|
|
1276
|
-
? (partial) => { if (forwardUpdates) onUpdate(partial); }
|
|
436
|
+
? (partial) => { if (forwardUpdates) (onUpdate as unknown as OnUpdate)(partial); }
|
|
1277
437
|
: undefined;
|
|
1278
438
|
|
|
1279
439
|
const agentRunPromise: Promise<RunResult> = runAgent(
|
|
1280
440
|
agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
|
|
1281
441
|
);
|
|
1282
442
|
|
|
1283
|
-
// Derived promise for the bg manager (used only if we detach)
|
|
1284
443
|
const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
|
|
1285
444
|
.then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
|
|
1286
445
|
|
|
@@ -1311,7 +470,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1311
470
|
});
|
|
1312
471
|
|
|
1313
472
|
if (outcome === "detached") {
|
|
1314
|
-
const bgJobId = await detachPromise;
|
|
473
|
+
const bgJobId = await detachPromise;
|
|
1315
474
|
return {
|
|
1316
475
|
content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
|
|
1317
476
|
details: {
|
|
@@ -1336,12 +495,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1336
495
|
elapsedMs: undefined,
|
|
1337
496
|
model: result.model,
|
|
1338
497
|
toolCalls: result.toolCalls,
|
|
498
|
+
executionEvents: result.executionEvents,
|
|
1339
499
|
} satisfies SubagentDetails,
|
|
1340
500
|
isError: result.exitCode !== 0,
|
|
1341
501
|
};
|
|
1342
502
|
}
|
|
1343
503
|
|
|
1344
|
-
// ── Parallel mode
|
|
504
|
+
// ── Parallel mode ───────────────────────────────────────────────────
|
|
1345
505
|
if (params.tasks && params.tasks.length > 0) {
|
|
1346
506
|
const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
|
|
1347
507
|
for (const t of params.tasks) {
|
|
@@ -1358,14 +518,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1358
518
|
}));
|
|
1359
519
|
let runningUsage = { ...emptyUsage };
|
|
1360
520
|
|
|
1361
|
-
const emitParallel = (running: boolean) => onUpdate?.({
|
|
521
|
+
const emitParallel = (running: boolean) => (onUpdate as unknown as OnUpdate | undefined)?.({
|
|
1362
522
|
content: [{ type: "text", text: "" }],
|
|
1363
523
|
details: { mode: "parallel", parallelAgents: [...parallelAgents], usage: { ...runningUsage }, running, toolCalls: [] } satisfies SubagentDetails,
|
|
1364
524
|
});
|
|
1365
525
|
|
|
1366
526
|
emitParallel(true);
|
|
1367
527
|
|
|
1368
|
-
const parentDepth =
|
|
528
|
+
const parentDepth = getCurrentDepth();
|
|
1369
529
|
const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
|
|
1370
530
|
parallelAgents[i]!.status = "running";
|
|
1371
531
|
emitParallel(true);
|
|
@@ -1404,8 +564,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1404
564
|
};
|
|
1405
565
|
}
|
|
1406
566
|
|
|
1407
|
-
// ── Chain mode ────────────────────────────────────────────
|
|
1408
|
-
// Shouldn't reach here
|
|
1409
567
|
return { content: [{ type: "text", text: "Provide agent+task or tasks array." }] };
|
|
1410
568
|
},
|
|
1411
569
|
});
|