pi-fast-subagent 0.1.2 → 0.2.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 +1 -13
- package/index.ts +383 -215
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -8,8 +8,7 @@ Runs subagents with `createAgentSession()` in same process instead of spawning `
|
|
|
8
8
|
|
|
9
9
|
- Single mode: `{ agent, task }`
|
|
10
10
|
- Parallel mode: `{ tasks: [...] }`
|
|
11
|
-
-
|
|
12
|
-
- Per-call or per-step model override
|
|
11
|
+
- Per-call model override
|
|
13
12
|
- User + project agent discovery
|
|
14
13
|
- Project agents override user agents
|
|
15
14
|
- Max nesting depth guard
|
|
@@ -108,17 +107,6 @@ subagent({
|
|
|
108
107
|
})
|
|
109
108
|
```
|
|
110
109
|
|
|
111
|
-
### Chain
|
|
112
|
-
|
|
113
|
-
```js
|
|
114
|
-
subagent({
|
|
115
|
-
chain: [
|
|
116
|
-
{ agent: "scout", task: "Explore app structure" },
|
|
117
|
-
{ agent: "scout", task: "Based on this: {previous}\n\nExtract only auth flow." }
|
|
118
|
-
]
|
|
119
|
-
})
|
|
120
|
-
```
|
|
121
|
-
|
|
122
110
|
## Notes
|
|
123
111
|
|
|
124
112
|
- Async/background isolation not supported in-process
|
package/index.ts
CHANGED
|
@@ -4,13 +4,20 @@
|
|
|
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
|
-
*
|
|
8
|
-
* Supports: single, parallel, chain.
|
|
7
|
+
* Supports: single, parallel.
|
|
9
8
|
* Agent .md files are compatible with pi-subagents frontmatter format.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
|
-
import type {
|
|
13
|
-
|
|
11
|
+
import type {
|
|
12
|
+
AgentToolResult,
|
|
13
|
+
AgentToolUpdateCallback,
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ExtensionContext,
|
|
16
|
+
ToolRenderResultOptions,
|
|
17
|
+
} from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import { Theme } from "@mariozechner/pi-coding-agent";
|
|
19
|
+
import { truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
20
|
+
import { truncateToVisualLines, keyHint } from "@mariozechner/pi-coding-agent";
|
|
14
21
|
import {
|
|
15
22
|
AuthStorage,
|
|
16
23
|
createAgentSession,
|
|
@@ -19,9 +26,70 @@ import {
|
|
|
19
26
|
ModelRegistry,
|
|
20
27
|
SessionManager,
|
|
21
28
|
} from "@mariozechner/pi-coding-agent";
|
|
29
|
+
|
|
30
|
+
type DefaultResourceLoaderOptions = ConstructorParameters<typeof DefaultResourceLoader>[0];
|
|
22
31
|
import { Type } from "@sinclair/typebox";
|
|
23
32
|
import { type AgentConfig, discoverAgents } from "./agents.js";
|
|
24
33
|
|
|
34
|
+
// ─── Tool arg summarizer (compact one-liner per tool call) ─────────────────────
|
|
35
|
+
|
|
36
|
+
function shortPath(p: unknown): string {
|
|
37
|
+
if (typeof p !== "string") return "";
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
|
|
40
|
+
return p.replace(/^\/Users\/[^/]+\/[^/]+\//, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function summarizeToolArgs(toolName: unknown, toolInput: unknown): string {
|
|
44
|
+
const name = String(toolName ?? "");
|
|
45
|
+
const input =
|
|
46
|
+
toolInput && typeof toolInput === "object" ? (toolInput as Record<string, unknown>) : {};
|
|
47
|
+
const filePath = (): string => shortPath(input.path ?? input.file_path) || "";
|
|
48
|
+
switch (name) {
|
|
49
|
+
case "Read":
|
|
50
|
+
case "read":
|
|
51
|
+
case "Write":
|
|
52
|
+
case "write":
|
|
53
|
+
case "Edit":
|
|
54
|
+
case "edit":
|
|
55
|
+
return filePath();
|
|
56
|
+
case "Bash":
|
|
57
|
+
case "bash": {
|
|
58
|
+
const cmd = String(input.command ?? "");
|
|
59
|
+
return cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
|
|
60
|
+
}
|
|
61
|
+
case "Glob":
|
|
62
|
+
case "glob":
|
|
63
|
+
return String(input.pattern ?? "");
|
|
64
|
+
case "find": {
|
|
65
|
+
const pat = String(input.pattern ?? "");
|
|
66
|
+
const p = shortPath(input.path);
|
|
67
|
+
return p ? `${pat} in ${p}` : pat;
|
|
68
|
+
}
|
|
69
|
+
case "Grep":
|
|
70
|
+
case "grep": {
|
|
71
|
+
const pat = String(input.pattern ?? "");
|
|
72
|
+
const g = input.glob ? ` ${input.glob}` : "";
|
|
73
|
+
return `${pat}${g}`;
|
|
74
|
+
}
|
|
75
|
+
case "ls":
|
|
76
|
+
return shortPath(input.path) || "";
|
|
77
|
+
case "subagent": {
|
|
78
|
+
const agent = String(input.agent ?? "");
|
|
79
|
+
const t = String(input.task ?? "");
|
|
80
|
+
const summary = t.length > 50 ? t.slice(0, 47) + "..." : t;
|
|
81
|
+
return agent ? `${agent}: ${summary}` : summary;
|
|
82
|
+
}
|
|
83
|
+
default: {
|
|
84
|
+
for (const v of Object.values(input)) {
|
|
85
|
+
if (typeof v === "string" && v.length > 0)
|
|
86
|
+
return v.length > 60 ? v.slice(0, 57) + "..." : v;
|
|
87
|
+
}
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
25
93
|
// ─── Shared auth (created once, reused across calls) ─────────────────────────
|
|
26
94
|
|
|
27
95
|
let _authStorage: ReturnType<typeof AuthStorage.create> | null = null;
|
|
@@ -38,14 +106,45 @@ function getAuth() {
|
|
|
38
106
|
const MAX_DEPTH = 2;
|
|
39
107
|
const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
|
|
40
108
|
|
|
109
|
+
interface ToolCallEntry {
|
|
110
|
+
id: string;
|
|
111
|
+
name: string;
|
|
112
|
+
argSummary: string;
|
|
113
|
+
result?: string;
|
|
114
|
+
isError?: boolean;
|
|
115
|
+
durMs?: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
41
118
|
interface RunResult {
|
|
42
119
|
output: string;
|
|
43
120
|
exitCode: number;
|
|
44
121
|
error?: string;
|
|
45
122
|
model?: string;
|
|
123
|
+
toolCalls: ToolCallEntry[];
|
|
46
124
|
usage: { input: number; output: number; cost: number; turns: number };
|
|
47
125
|
}
|
|
48
126
|
|
|
127
|
+
interface AgentRowStatus {
|
|
128
|
+
name: string;
|
|
129
|
+
taskSummary: string;
|
|
130
|
+
status: "pending" | "running" | "done" | "error";
|
|
131
|
+
durMs?: number;
|
|
132
|
+
toolCalls?: ToolCallEntry[];
|
|
133
|
+
responseText?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface SubagentDetails {
|
|
137
|
+
mode?: "single" | "parallel";
|
|
138
|
+
task?: string;
|
|
139
|
+
// parallel
|
|
140
|
+
parallelAgents?: AgentRowStatus[];
|
|
141
|
+
usage: RunResult["usage"];
|
|
142
|
+
running: boolean;
|
|
143
|
+
elapsedMs?: number;
|
|
144
|
+
model?: string;
|
|
145
|
+
toolCalls: ToolCallEntry[];
|
|
146
|
+
}
|
|
147
|
+
|
|
49
148
|
type OnUpdate = (partial: { content: [{ type: "text"; text: string }]; details: unknown }) => void;
|
|
50
149
|
|
|
51
150
|
function formatDuration(ms: number): string {
|
|
@@ -55,6 +154,9 @@ function formatDuration(ms: number): string {
|
|
|
55
154
|
return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
|
|
56
155
|
}
|
|
57
156
|
|
|
157
|
+
// Module-level depth counter — avoids process.env race conditions in parallel mode
|
|
158
|
+
let _currentDepth = 0;
|
|
159
|
+
|
|
58
160
|
async function runAgent(
|
|
59
161
|
agent: AgentConfig,
|
|
60
162
|
task: string,
|
|
@@ -62,13 +164,15 @@ async function runAgent(
|
|
|
62
164
|
modelOverride: string | undefined,
|
|
63
165
|
signal: AbortSignal | undefined,
|
|
64
166
|
onUpdate: OnUpdate | undefined,
|
|
167
|
+
parentDepth?: number,
|
|
65
168
|
): Promise<RunResult> {
|
|
66
|
-
const depth =
|
|
169
|
+
const depth = parentDepth ?? _currentDepth;
|
|
67
170
|
if (depth >= MAX_DEPTH) {
|
|
68
171
|
return {
|
|
69
172
|
output: "",
|
|
70
173
|
exitCode: 1,
|
|
71
174
|
error: `Max subagent depth (${MAX_DEPTH}) exceeded. Increase PI_FAST_SUBAGENT_DEPTH env to allow deeper nesting.`,
|
|
175
|
+
toolCalls: [],
|
|
72
176
|
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
73
177
|
};
|
|
74
178
|
}
|
|
@@ -77,7 +181,7 @@ async function runAgent(
|
|
|
77
181
|
const agentDir = getAgentDir();
|
|
78
182
|
|
|
79
183
|
// Build resource loader — no extensions/context files to keep subagent lean
|
|
80
|
-
const loaderOptions:
|
|
184
|
+
const loaderOptions: DefaultResourceLoaderOptions = {
|
|
81
185
|
cwd,
|
|
82
186
|
agentDir,
|
|
83
187
|
noExtensions: true,
|
|
@@ -124,54 +228,78 @@ async function runAgent(
|
|
|
124
228
|
let detectedModel: string | undefined;
|
|
125
229
|
const startedAt = Date.now();
|
|
126
230
|
const configuredModel = modelOverride ?? agent.model;
|
|
231
|
+
const toolCalls: ToolCallEntry[] = [];
|
|
232
|
+
const toolStartTimes = new Map<string, number>();
|
|
127
233
|
|
|
128
|
-
|
|
129
|
-
content: [{ type: "text", text: "Starting subagent..." }],
|
|
130
|
-
details: {
|
|
131
|
-
agent: agent.name,
|
|
132
|
-
usage,
|
|
133
|
-
running: true,
|
|
134
|
-
elapsedMs: 0,
|
|
135
|
-
model: configuredModel,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const heartbeat = setInterval(() => {
|
|
234
|
+
function emitUpdate(): void {
|
|
140
235
|
onUpdate?.({
|
|
141
|
-
content: [{ type: "text", text: currentDelta || lastOutput || "
|
|
236
|
+
content: [{ type: "text", text: currentDelta || lastOutput || "" }],
|
|
142
237
|
details: {
|
|
143
|
-
|
|
238
|
+
task,
|
|
144
239
|
usage,
|
|
145
240
|
running: true,
|
|
146
241
|
elapsedMs: Date.now() - startedAt,
|
|
147
242
|
model: detectedModel ?? configuredModel,
|
|
148
|
-
|
|
243
|
+
toolCalls: [...toolCalls],
|
|
244
|
+
} satisfies SubagentDetails,
|
|
149
245
|
});
|
|
150
|
-
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
emitUpdate();
|
|
249
|
+
|
|
250
|
+
const heartbeat = setInterval(emitUpdate, 1000);
|
|
151
251
|
|
|
152
252
|
const unsubscribe = session.subscribe((event: any) => {
|
|
253
|
+
// Stream tool execution events
|
|
254
|
+
if (event.type === "tool_execution_start") {
|
|
255
|
+
toolStartTimes.set(event.toolCallId, Date.now());
|
|
256
|
+
toolCalls.push({
|
|
257
|
+
id: event.toolCallId,
|
|
258
|
+
name: event.toolName,
|
|
259
|
+
argSummary: summarizeToolArgs(event.toolName, event.args),
|
|
260
|
+
});
|
|
261
|
+
emitUpdate();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (event.type === "tool_execution_end") {
|
|
266
|
+
const startedAtTool = toolStartTimes.get(event.toolCallId);
|
|
267
|
+
toolStartTimes.delete(event.toolCallId);
|
|
268
|
+
const resultText: string = (event.result?.content ?? [])
|
|
269
|
+
.filter((p: any) => p.type === "text")
|
|
270
|
+
.map((p: any) => p.text as string)
|
|
271
|
+
.join("\n");
|
|
272
|
+
let entry: ToolCallEntry | undefined;
|
|
273
|
+
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
274
|
+
if (toolCalls[i]!.id === event.toolCallId) { entry = toolCalls[i]; break; }
|
|
275
|
+
}
|
|
276
|
+
if (!entry) {
|
|
277
|
+
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
278
|
+
if (toolCalls[i]!.name === event.toolName && toolCalls[i]!.result === undefined) { entry = toolCalls[i]; break; }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (entry) {
|
|
282
|
+
entry.result = resultText;
|
|
283
|
+
entry.isError = event.isError;
|
|
284
|
+
entry.durMs = startedAtTool != null ? Date.now() - startedAtTool : undefined;
|
|
285
|
+
}
|
|
286
|
+
emitUpdate();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
153
290
|
// Stream text deltas live to the UI
|
|
154
291
|
if (event.type === "message_update") {
|
|
155
292
|
const e = event.assistantMessageEvent;
|
|
156
293
|
if (e?.type === "text_delta" && e.delta) {
|
|
157
294
|
currentDelta += e.delta;
|
|
158
|
-
|
|
159
|
-
content: [{ type: "text", text: currentDelta }],
|
|
160
|
-
details: {
|
|
161
|
-
agent: agent.name,
|
|
162
|
-
usage,
|
|
163
|
-
running: true,
|
|
164
|
-
elapsedMs: Date.now() - startedAt,
|
|
165
|
-
model: detectedModel ?? configuredModel,
|
|
166
|
-
},
|
|
167
|
-
});
|
|
295
|
+
emitUpdate();
|
|
168
296
|
}
|
|
169
297
|
return;
|
|
170
298
|
}
|
|
171
299
|
|
|
172
300
|
if (event.type !== "message_end" || !event.message) return;
|
|
173
301
|
const msg = event.message;
|
|
174
|
-
if (msg.role !== "assistant") return;
|
|
302
|
+
if (msg.role !== "assistant") return; // usage/model only tracked for assistant turns
|
|
175
303
|
|
|
176
304
|
usage.turns++;
|
|
177
305
|
const u = msg.usage;
|
|
@@ -204,9 +332,10 @@ async function runAgent(
|
|
|
204
332
|
});
|
|
205
333
|
});
|
|
206
334
|
|
|
207
|
-
// Propagate depth to
|
|
208
|
-
const
|
|
335
|
+
// Propagate depth to nested calls — use module counter (safe for parallel) + env for subprocess compat
|
|
336
|
+
const prevEnvDepth = process.env[DEPTH_ENV];
|
|
209
337
|
process.env[DEPTH_ENV] = String(depth + 1);
|
|
338
|
+
_currentDepth = depth + 1;
|
|
210
339
|
|
|
211
340
|
let exitCode = 0;
|
|
212
341
|
let error: string | undefined;
|
|
@@ -228,11 +357,12 @@ async function runAgent(
|
|
|
228
357
|
clearInterval(heartbeat);
|
|
229
358
|
unsubscribe();
|
|
230
359
|
session.dispose();
|
|
231
|
-
if (
|
|
232
|
-
else process.env[DEPTH_ENV] =
|
|
360
|
+
if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
|
|
361
|
+
else process.env[DEPTH_ENV] = prevEnvDepth;
|
|
362
|
+
_currentDepth = depth;
|
|
233
363
|
}
|
|
234
364
|
|
|
235
|
-
return { output: lastOutput, exitCode, error, model: detectedModel, usage };
|
|
365
|
+
return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, usage };
|
|
236
366
|
}
|
|
237
367
|
|
|
238
368
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -289,19 +419,6 @@ const TaskItem = Type.Object({
|
|
|
289
419
|
count: Type.Optional(Type.Number({ description: "Repeat this task N times" })),
|
|
290
420
|
});
|
|
291
421
|
|
|
292
|
-
const ChainItem = Type.Object({
|
|
293
|
-
agent: Type.String({ description: "Agent name" }),
|
|
294
|
-
task: Type.Optional(
|
|
295
|
-
Type.String({
|
|
296
|
-
description:
|
|
297
|
-
"Task template. Supports {previous} (output from prior step) and {task} (first step task). " +
|
|
298
|
-
"Defaults to {previous} for steps 2+.",
|
|
299
|
-
}),
|
|
300
|
-
),
|
|
301
|
-
model: Type.Optional(Type.String({ description: "Model override (provider/model)" })),
|
|
302
|
-
cwd: Type.Optional(Type.String({ description: "Working directory" })),
|
|
303
|
-
});
|
|
304
|
-
|
|
305
422
|
const SubagentParams = Type.Object({
|
|
306
423
|
// Single mode
|
|
307
424
|
agent: Type.Optional(Type.String({ description: "Agent name (single mode)" })),
|
|
@@ -319,13 +436,6 @@ const SubagentParams = Type.Object({
|
|
|
319
436
|
Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
|
|
320
437
|
),
|
|
321
438
|
|
|
322
|
-
// Chain mode
|
|
323
|
-
chain: Type.Optional(
|
|
324
|
-
Type.Array(ChainItem, {
|
|
325
|
-
description: "Sequential chain. Use {previous} in task to receive prior step output.",
|
|
326
|
-
}),
|
|
327
|
-
),
|
|
328
|
-
|
|
329
439
|
// Management
|
|
330
440
|
action: Type.Optional(
|
|
331
441
|
Type.Union(
|
|
@@ -352,54 +462,186 @@ export default function (pi: ExtensionAPI) {
|
|
|
352
462
|
label: "Subagent",
|
|
353
463
|
description: [
|
|
354
464
|
"Delegate tasks to specialized subagents. Runs IN-PROCESS — no subprocess cold-start overhead.",
|
|
355
|
-
"Modes: single ({ agent, task }), parallel ({ tasks: [...] })
|
|
356
|
-
"Chain supports {task} (first step task) and {previous} (prior step output) template vars.",
|
|
465
|
+
"Modes: single ({ agent, task }), parallel ({ tasks: [...] }).",
|
|
357
466
|
"Agents defined as .md files in ~/.pi/agent/agents/ (user) or .pi/agents/ (project).",
|
|
358
467
|
"Use { action: 'list' } to discover available agents.",
|
|
359
468
|
].join(" "),
|
|
360
469
|
parameters: SubagentParams,
|
|
361
470
|
|
|
362
|
-
renderResult(result
|
|
363
|
-
const
|
|
364
|
-
const details = (result.details ?? {}) as
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
471
|
+
renderResult(result: AgentToolResult<unknown>, { isPartial, expanded }: ToolRenderResultOptions, theme: Theme) {
|
|
472
|
+
const agentText = result.content?.[0]?.type === "text" ? (result.content[0] as any).text as string : "";
|
|
473
|
+
const details = (result.details ?? {}) as SubagentDetails;
|
|
474
|
+
const toolCalls = details.toolCalls ?? [];
|
|
475
|
+
|
|
476
|
+
// ── Parallel / Chain mode renders ────────────────────────────────
|
|
477
|
+
if (details.mode === "parallel" && details.parallelAgents) {
|
|
478
|
+
const agents = details.parallelAgents;
|
|
479
|
+
const doneCount = agents.filter((a) => a.status === "done" || a.status === "error").length;
|
|
480
|
+
|
|
481
|
+
function agentToolRow(t: ToolCallEntry): string {
|
|
482
|
+
const arg = t.argSummary || "";
|
|
483
|
+
const call = `${t.name}(${arg})`;
|
|
484
|
+
if (t.result === undefined) return theme.fg("dim", call);
|
|
485
|
+
const dur = t.durMs != null ? (t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`) : "";
|
|
486
|
+
return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function wrapL(text: string, w: number): string[] {
|
|
490
|
+
try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
|
|
491
|
+
}
|
|
370
492
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
493
|
+
const cache: { width?: number } = {};
|
|
494
|
+
return {
|
|
495
|
+
invalidate() { cache.width = undefined; },
|
|
496
|
+
render(width: number): string[] {
|
|
497
|
+
const out: string[] = [];
|
|
498
|
+
const header = details.running
|
|
499
|
+
? `Parallel (${doneCount}/${agents.length} done)`
|
|
500
|
+
: `Parallel: ${agents.filter((a) => a.status === "done").length}/${agents.length} succeeded`;
|
|
501
|
+
out.push(truncateToWidth(header, width, "..."));
|
|
502
|
+
|
|
503
|
+
for (const a of agents) {
|
|
504
|
+
const dur = a.durMs != null ? (a.durMs < 1000 ? ` ${a.durMs}ms` : ` ${(a.durMs / 1000).toFixed(1)}s`) : "";
|
|
505
|
+
const mark = a.status === "pending" ? theme.fg("dim", "⋅") : a.status === "running" ? theme.fg("dim", "→") : a.status === "done" ? `✓${dur}` : `✗${dur}`;
|
|
506
|
+
|
|
507
|
+
if (expanded) {
|
|
508
|
+
// Full solo-style block per agent
|
|
509
|
+
out.push("");
|
|
510
|
+
out.push(truncateToWidth(`[${a.name}] ${mark}`, width, "..."));
|
|
511
|
+
out.push(truncateToWidth(`Prompt:`, width, "..."));
|
|
512
|
+
out.push(truncateToWidth(` ${a.taskSummary}`, width, "..."));
|
|
513
|
+
for (const t of a.toolCalls ?? []) {
|
|
514
|
+
out.push(truncateToWidth(agentToolRow(t), width, "..."));
|
|
515
|
+
}
|
|
516
|
+
if (a.responseText) {
|
|
517
|
+
out.push("Response:");
|
|
518
|
+
const preview = truncateToVisualLines(a.responseText, 6, width - 2);
|
|
519
|
+
for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
|
|
520
|
+
if (preview.skippedCount > 0) out.push(truncateToWidth(theme.fg("dim", ` … ${preview.skippedCount} more lines`), width, "..."));
|
|
521
|
+
} else if (a.status === "running") {
|
|
522
|
+
out.push(theme.fg("dim", " running..."));
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
// Collapsed: compact one-liner
|
|
526
|
+
const row = ` [${a.name}] ${mark} ${a.taskSummary}`;
|
|
527
|
+
out.push(truncateToWidth(row, width, "..."));
|
|
528
|
+
// Show tool call rows compactly
|
|
529
|
+
for (const t of a.toolCalls ?? []) {
|
|
530
|
+
out.push(truncateToWidth(` ${agentToolRow(t)}`, width, "..."));
|
|
531
|
+
}
|
|
532
|
+
if (a.responseText && (a.status === "done" || a.status === "error")) {
|
|
533
|
+
const preview = truncateToVisualLines(a.responseText, 2, width - 4);
|
|
534
|
+
for (const l of preview.visualLines) out.push(truncateToWidth(" " + l, width, "..."));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
out.push("");
|
|
540
|
+
const status = details.running
|
|
541
|
+
? ["running", details.usage?.turns ? `${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}` : ""].filter(Boolean).join(" · ")
|
|
542
|
+
: formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
|
|
543
|
+
const expandHint = !expanded ? keyHint("app.tools.expand", "expand for full output") : "";
|
|
544
|
+
out.push(truncateToWidth([status, expandHint].filter(Boolean).join(" "), width, "..."));
|
|
545
|
+
return out;
|
|
546
|
+
},
|
|
547
|
+
};
|
|
381
548
|
}
|
|
382
549
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
550
|
+
function statusLine(): string {
|
|
551
|
+
if (details.running) {
|
|
552
|
+
const parts: string[] = ["running"];
|
|
553
|
+
if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
|
|
554
|
+
if (details.elapsedMs != null) parts.push(formatDuration(details.elapsedMs));
|
|
555
|
+
if (details.model) parts.push(details.model);
|
|
556
|
+
return parts.join(" · ");
|
|
557
|
+
}
|
|
558
|
+
return formatUsage(details.usage ?? { input: 0, output: 0, cost: 0, turns: 0 }, details.model);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Name(arg) ✓ 0.3s or Name(arg) (dim, still running)
|
|
562
|
+
function toolRow(t: ToolCallEntry): string {
|
|
563
|
+
const arg = t.argSummary ? t.argSummary : "";
|
|
564
|
+
const call = `${t.name}(${arg})`;
|
|
565
|
+
if (t.result === undefined) return theme.fg("dim", call);
|
|
566
|
+
const dur = t.durMs != null
|
|
567
|
+
? t.durMs < 1000 ? ` ${t.durMs}ms` : ` ${(t.durMs / 1000).toFixed(1)}s`
|
|
568
|
+
: "";
|
|
569
|
+
return `${call}${t.isError ? " ✗" : ` ✓${dur}`}`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function wrapLine(text: string, w: number): string[] {
|
|
573
|
+
try { return wrapTextWithAnsi(text, w); } catch { return [truncateToWidth(text, w, "...")]; }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const cache: { width?: number; responseLines?: string[]; skipped?: number } = {};
|
|
577
|
+
|
|
394
578
|
return {
|
|
395
|
-
invalidate() {},
|
|
579
|
+
invalidate() { cache.width = undefined; },
|
|
396
580
|
render(width: number): string[] {
|
|
397
|
-
|
|
581
|
+
const out: string[] = [];
|
|
582
|
+
const indent = " ";
|
|
583
|
+
|
|
584
|
+
// ── Prompt ────────────────────────────────────────────────────
|
|
585
|
+
if (details.task) {
|
|
586
|
+
out.push("Prompt:");
|
|
587
|
+
const taskLines = details.task.split("\n");
|
|
588
|
+
if (expanded) {
|
|
589
|
+
for (const line of taskLines) {
|
|
590
|
+
for (const w of wrapLine(indent + line, width)) out.push(w);
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
// Single truncated line in collapsed
|
|
594
|
+
const oneLiner = taskLines[0] ?? "";
|
|
595
|
+
out.push(truncateToWidth(indent + oneLiner, width, "..."));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── Tool calls ─────────────────────────────────────────────
|
|
600
|
+
for (const t of toolCalls) {
|
|
601
|
+
out.push(truncateToWidth(toolRow(t), width, "..."));
|
|
602
|
+
if (expanded && t.result !== undefined) {
|
|
603
|
+
for (const line of t.result.split("\n")) {
|
|
604
|
+
for (const w of wrapLine(theme.fg("dim", indent + line), width)) out.push(w);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ── Response ────────────────────────────────────────────
|
|
610
|
+
const responseText = agentText || (isPartial ? "" : "");
|
|
611
|
+
if (responseText || isPartial) {
|
|
612
|
+
out.push("Response:");
|
|
613
|
+
if (expanded) {
|
|
614
|
+
for (const line of responseText.split("\n")) {
|
|
615
|
+
for (const w of wrapLine(indent + line, width)) out.push(w);
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
const PREVIEW_LINES = 6;
|
|
619
|
+
if (cache.width !== width) {
|
|
620
|
+
const preview = truncateToVisualLines(responseText, PREVIEW_LINES, width - indent.length);
|
|
621
|
+
cache.responseLines = preview.visualLines.map((l) => truncateToWidth(indent + l, width, "..."));
|
|
622
|
+
cache.skipped = preview.skippedCount;
|
|
623
|
+
cache.width = width;
|
|
624
|
+
}
|
|
625
|
+
out.push(...(cache.responseLines ?? []));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ── Status ───────────────────────────────────────────────
|
|
630
|
+
const status = statusLine();
|
|
631
|
+
const expandHint = !expanded && (cache.skipped ?? 0) > 0
|
|
632
|
+
? keyHint("app.tools.expand", `expand · ${cache.skipped} lines hidden`)
|
|
633
|
+
: !expanded && toolCalls.some((t) => t.result !== undefined)
|
|
634
|
+
? keyHint("app.tools.expand", "expand for tool outputs")
|
|
635
|
+
: "";
|
|
636
|
+
const statusWithHint = [status, expandHint].filter(Boolean).join(" ");
|
|
637
|
+
if (statusWithHint) out.push(truncateToWidth(statusWithHint, width, "..."));
|
|
638
|
+
|
|
639
|
+
return out;
|
|
398
640
|
},
|
|
399
641
|
};
|
|
400
642
|
},
|
|
401
643
|
|
|
402
|
-
async execute(_id, params, signal, onUpdate, ctx) {
|
|
644
|
+
async execute(_id: string, params: Record<string, any>, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<unknown> | undefined, ctx: ExtensionContext): Promise<any> {
|
|
403
645
|
const cwd = params.cwd ?? ctx.cwd;
|
|
404
646
|
const agents = discoverAgents(cwd);
|
|
405
647
|
|
|
@@ -413,7 +655,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
413
655
|
};
|
|
414
656
|
|
|
415
657
|
// ── Management: list ──────────────────────────────────────────────────────
|
|
416
|
-
if (params.action === "list" || (!params.agent && !params.tasks
|
|
658
|
+
if (params.action === "list" || (!params.agent && !params.tasks)) {
|
|
417
659
|
if (agents.length === 0) {
|
|
418
660
|
return {
|
|
419
661
|
content: [{
|
|
@@ -459,18 +701,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
459
701
|
return {
|
|
460
702
|
content: [{ type: "text", text: getFinalText(result) }],
|
|
461
703
|
details: {
|
|
704
|
+
task: params.task,
|
|
462
705
|
usage: result.usage,
|
|
463
706
|
running: false,
|
|
464
707
|
elapsedMs: undefined,
|
|
465
708
|
model: result.model,
|
|
466
|
-
|
|
709
|
+
toolCalls: result.toolCalls,
|
|
710
|
+
} satisfies SubagentDetails,
|
|
467
711
|
isError: result.exitCode !== 0,
|
|
468
712
|
};
|
|
469
713
|
}
|
|
470
714
|
|
|
471
|
-
// ── Parallel mode
|
|
715
|
+
// ── Parallel mode ─────────────────────────────────────────────
|
|
472
716
|
if (params.tasks && params.tasks.length > 0) {
|
|
473
|
-
// Expand count shorthand
|
|
474
717
|
const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
|
|
475
718
|
for (const t of params.tasks) {
|
|
476
719
|
const n = t.count ?? 1;
|
|
@@ -478,138 +721,63 @@ export default function (pi: ExtensionAPI) {
|
|
|
478
721
|
}
|
|
479
722
|
|
|
480
723
|
const concurrency = params.concurrency ?? 4;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
doneCount++;
|
|
493
|
-
onUpdate?.({
|
|
494
|
-
content: [{ type: "text", text: `Parallel: ${doneCount}/${expanded.length} done...` }],
|
|
495
|
-
details: {},
|
|
496
|
-
});
|
|
497
|
-
return { ...result, agentName: t.agent };
|
|
498
|
-
},
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
const successCount = allResults.filter((r) => r.exitCode === 0).length;
|
|
502
|
-
const summaries = allResults.map((r) => {
|
|
503
|
-
const out = getFinalText(r);
|
|
504
|
-
const preview = out.length > 300 ? `${out.slice(0, 300)}...` : out;
|
|
505
|
-
return `**[${r.agentName}]** ${r.exitCode === 0 ? "✓" : "✗"}\n${preview}`;
|
|
724
|
+
const emptyUsage = { input: 0, output: 0, cost: 0, turns: 0 };
|
|
725
|
+
const parallelAgents: AgentRowStatus[] = expanded.map((t) => ({
|
|
726
|
+
name: t.agent,
|
|
727
|
+
taskSummary: t.task.length > 60 ? t.task.slice(0, 57) + "..." : t.task,
|
|
728
|
+
status: "pending" as const,
|
|
729
|
+
}));
|
|
730
|
+
let runningUsage = { ...emptyUsage };
|
|
731
|
+
|
|
732
|
+
const emitParallel = (running: boolean) => onUpdate?.({
|
|
733
|
+
content: [{ type: "text", text: "" }],
|
|
734
|
+
details: { mode: "parallel", parallelAgents: [...parallelAgents], usage: { ...runningUsage }, running, toolCalls: [] } satisfies SubagentDetails,
|
|
506
735
|
});
|
|
507
|
-
const totalUsage = allResults.reduce(
|
|
508
|
-
(acc, r) => ({
|
|
509
|
-
input: acc.input + r.usage.input,
|
|
510
|
-
output: acc.output + r.usage.output,
|
|
511
|
-
cost: acc.cost + r.usage.cost,
|
|
512
|
-
turns: acc.turns + r.usage.turns,
|
|
513
|
-
}),
|
|
514
|
-
{ input: 0, output: 0, cost: 0, turns: 0 },
|
|
515
|
-
);
|
|
516
736
|
|
|
517
|
-
|
|
518
|
-
content: [{
|
|
519
|
-
type: "text",
|
|
520
|
-
text: [
|
|
521
|
-
`Parallel: ${successCount}/${allResults.length} succeeded`,
|
|
522
|
-
"",
|
|
523
|
-
summaries.join("\n\n"),
|
|
524
|
-
"",
|
|
525
|
-
formatUsage(totalUsage),
|
|
526
|
-
].join("\n"),
|
|
527
|
-
}],
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// ── Chain mode ────────────────────────────────────────────────────────────
|
|
532
|
-
if (params.chain && params.chain.length > 0) {
|
|
533
|
-
const firstTask = params.chain[0]?.task ?? "";
|
|
534
|
-
let previousOutput = "";
|
|
535
|
-
|
|
536
|
-
const stepResults: Array<RunResult & { agentName: string; step: number }> = [];
|
|
737
|
+
emitParallel(true);
|
|
537
738
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
739
|
+
const parentDepth = _currentDepth;
|
|
740
|
+
const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
|
|
741
|
+
parallelAgents[i]!.status = "running";
|
|
742
|
+
emitParallel(true);
|
|
743
|
+
const { agent, error } = findAgent(t.agent);
|
|
541
744
|
if (error || !agent) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Resolve task template
|
|
549
|
-
let task = step.task ?? (i === 0 ? firstTask : "{previous}");
|
|
550
|
-
task = task
|
|
551
|
-
.replace(/\{previous\}/g, previousOutput)
|
|
552
|
-
.replace(/\{task\}/g, firstTask);
|
|
553
|
-
|
|
554
|
-
if (onUpdate) {
|
|
555
|
-
onUpdate({
|
|
556
|
-
content: [{
|
|
557
|
-
type: "text",
|
|
558
|
-
text: `Chain step ${i + 1}/${params.chain.length}: ${step.agent}...`,
|
|
559
|
-
}],
|
|
560
|
-
details: {},
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const result = await runAgent(
|
|
565
|
-
agent,
|
|
566
|
-
task,
|
|
567
|
-
step.cwd ?? cwd,
|
|
568
|
-
step.model,
|
|
569
|
-
signal,
|
|
570
|
-
onUpdate,
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
stepResults.push({ ...result, agentName: step.agent, step: i + 1 });
|
|
574
|
-
|
|
575
|
-
if (result.exitCode !== 0) {
|
|
576
|
-
return {
|
|
577
|
-
content: [{
|
|
578
|
-
type: "text",
|
|
579
|
-
text: `Chain failed at step ${i + 1} (${step.agent}): ${result.error ?? "(no output)"}`,
|
|
580
|
-
}],
|
|
581
|
-
isError: true,
|
|
582
|
-
};
|
|
745
|
+
parallelAgents[i]!.status = "error";
|
|
746
|
+
emitParallel(true);
|
|
747
|
+
return { agentName: t.agent, output: "", exitCode: 1, error, model: undefined, toolCalls: [] as ToolCallEntry[], usage: emptyUsage };
|
|
583
748
|
}
|
|
749
|
+
const agentStart = Date.now();
|
|
750
|
+
const agentOnUpdate: OnUpdate = (partial) => {
|
|
751
|
+
const d = partial.details as SubagentDetails | undefined;
|
|
752
|
+
parallelAgents[i]!.toolCalls = d?.toolCalls ? [...d.toolCalls] : parallelAgents[i]!.toolCalls;
|
|
753
|
+
parallelAgents[i]!.responseText = (partial.content?.[0] as any)?.text || parallelAgents[i]!.responseText;
|
|
754
|
+
emitParallel(true);
|
|
755
|
+
};
|
|
756
|
+
const result = await runAgent(agent, t.task, t.cwd ?? cwd, t.model, signal, agentOnUpdate, parentDepth);
|
|
757
|
+
parallelAgents[i]!.status = result.exitCode === 0 ? "done" : "error";
|
|
758
|
+
parallelAgents[i]!.durMs = Date.now() - agentStart;
|
|
759
|
+
parallelAgents[i]!.toolCalls = result.toolCalls;
|
|
760
|
+
parallelAgents[i]!.responseText = result.output;
|
|
761
|
+
runningUsage = { input: runningUsage.input + result.usage.input, output: runningUsage.output + result.usage.output, cost: runningUsage.cost + result.usage.cost, turns: runningUsage.turns + result.usage.turns };
|
|
762
|
+
emitParallel(true);
|
|
763
|
+
return { ...result, agentName: t.agent, toolCalls: result.toolCalls ?? [] };
|
|
764
|
+
});
|
|
584
765
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const last = stepResults[stepResults.length - 1];
|
|
589
|
-
const totalUsage = stepResults.reduce(
|
|
590
|
-
(acc, r) => ({
|
|
591
|
-
input: acc.input + r.usage.input,
|
|
592
|
-
output: acc.output + r.usage.output,
|
|
593
|
-
cost: acc.cost + r.usage.cost,
|
|
594
|
-
turns: acc.turns + r.usage.turns,
|
|
595
|
-
}),
|
|
596
|
-
{ input: 0, output: 0, cost: 0, turns: 0 },
|
|
766
|
+
const totalUsage = allResults.reduce(
|
|
767
|
+
(acc, r) => ({ input: acc.input + r.usage.input, output: acc.output + r.usage.output, cost: acc.cost + r.usage.cost, turns: acc.turns + r.usage.turns }),
|
|
768
|
+
emptyUsage,
|
|
597
769
|
);
|
|
770
|
+
const outputs = allResults.map((r) => `[${r.agentName}] ${r.exitCode === 0 ? "✓" : "✗"}\n${getFinalText(r)}`).join("\n\n");
|
|
598
771
|
|
|
599
772
|
return {
|
|
600
|
-
content: [{
|
|
601
|
-
|
|
602
|
-
text: [
|
|
603
|
-
last.output,
|
|
604
|
-
"",
|
|
605
|
-
`Chain: ${stepResults.length} steps · ${formatUsage(totalUsage)}`,
|
|
606
|
-
].join("\n"),
|
|
607
|
-
}],
|
|
773
|
+
content: [{ type: "text", text: outputs }],
|
|
774
|
+
details: { mode: "parallel", parallelAgents, usage: totalUsage, running: false, toolCalls: [] } satisfies SubagentDetails,
|
|
608
775
|
};
|
|
609
776
|
}
|
|
610
777
|
|
|
778
|
+
// ── Chain mode ────────────────────────────────────────────
|
|
611
779
|
// Shouldn't reach here
|
|
612
|
-
return { content: [{ type: "text", text: "Provide agent+task
|
|
780
|
+
return { content: [{ type: "text", text: "Provide agent+task or tasks array." }] };
|
|
613
781
|
},
|
|
614
782
|
});
|
|
615
783
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-fast-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "In-process subagent delegation for pi with single, parallel, and chain modes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -34,5 +34,10 @@
|
|
|
34
34
|
"@mariozechner/pi-tui": "*",
|
|
35
35
|
"@sinclair/typebox": "*"
|
|
36
36
|
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@mariozechner/pi-coding-agent": "^0.68.0",
|
|
39
|
+
"@mariozechner/pi-tui": "^0.68.0",
|
|
40
|
+
"@sinclair/typebox": "^0.34.41"
|
|
41
|
+
},
|
|
37
42
|
"license": "MIT"
|
|
38
43
|
}
|