pi-fast-subagent 0.1.1 → 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 +386 -205
- package/package.json +13 -2
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,41 +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
|
-
|
|
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);
|
|
385
559
|
}
|
|
386
|
-
|
|
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
|
+
|
|
578
|
+
return {
|
|
579
|
+
invalidate() { cache.width = undefined; },
|
|
580
|
+
render(width: number): string[] {
|
|
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;
|
|
640
|
+
},
|
|
641
|
+
};
|
|
387
642
|
},
|
|
388
643
|
|
|
389
|
-
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> {
|
|
390
645
|
const cwd = params.cwd ?? ctx.cwd;
|
|
391
646
|
const agents = discoverAgents(cwd);
|
|
392
647
|
|
|
@@ -400,7 +655,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
400
655
|
};
|
|
401
656
|
|
|
402
657
|
// ── Management: list ──────────────────────────────────────────────────────
|
|
403
|
-
if (params.action === "list" || (!params.agent && !params.tasks
|
|
658
|
+
if (params.action === "list" || (!params.agent && !params.tasks)) {
|
|
404
659
|
if (agents.length === 0) {
|
|
405
660
|
return {
|
|
406
661
|
content: [{
|
|
@@ -446,18 +701,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
446
701
|
return {
|
|
447
702
|
content: [{ type: "text", text: getFinalText(result) }],
|
|
448
703
|
details: {
|
|
704
|
+
task: params.task,
|
|
449
705
|
usage: result.usage,
|
|
450
706
|
running: false,
|
|
451
707
|
elapsedMs: undefined,
|
|
452
708
|
model: result.model,
|
|
453
|
-
|
|
709
|
+
toolCalls: result.toolCalls,
|
|
710
|
+
} satisfies SubagentDetails,
|
|
454
711
|
isError: result.exitCode !== 0,
|
|
455
712
|
};
|
|
456
713
|
}
|
|
457
714
|
|
|
458
|
-
// ── Parallel mode
|
|
715
|
+
// ── Parallel mode ─────────────────────────────────────────────
|
|
459
716
|
if (params.tasks && params.tasks.length > 0) {
|
|
460
|
-
// Expand count shorthand
|
|
461
717
|
const expanded: Array<{ agent: string; task: string; model?: string; cwd?: string }> = [];
|
|
462
718
|
for (const t of params.tasks) {
|
|
463
719
|
const n = t.count ?? 1;
|
|
@@ -465,138 +721,63 @@ export default function (pi: ExtensionAPI) {
|
|
|
465
721
|
}
|
|
466
722
|
|
|
467
723
|
const concurrency = params.concurrency ?? 4;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
doneCount++;
|
|
480
|
-
onUpdate?.({
|
|
481
|
-
content: [{ type: "text", text: `Parallel: ${doneCount}/${expanded.length} done...` }],
|
|
482
|
-
details: {},
|
|
483
|
-
});
|
|
484
|
-
return { ...result, agentName: t.agent };
|
|
485
|
-
},
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
const successCount = allResults.filter((r) => r.exitCode === 0).length;
|
|
489
|
-
const summaries = allResults.map((r) => {
|
|
490
|
-
const out = getFinalText(r);
|
|
491
|
-
const preview = out.length > 300 ? `${out.slice(0, 300)}...` : out;
|
|
492
|
-
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,
|
|
493
735
|
});
|
|
494
|
-
const totalUsage = allResults.reduce(
|
|
495
|
-
(acc, r) => ({
|
|
496
|
-
input: acc.input + r.usage.input,
|
|
497
|
-
output: acc.output + r.usage.output,
|
|
498
|
-
cost: acc.cost + r.usage.cost,
|
|
499
|
-
turns: acc.turns + r.usage.turns,
|
|
500
|
-
}),
|
|
501
|
-
{ input: 0, output: 0, cost: 0, turns: 0 },
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
return {
|
|
505
|
-
content: [{
|
|
506
|
-
type: "text",
|
|
507
|
-
text: [
|
|
508
|
-
`Parallel: ${successCount}/${allResults.length} succeeded`,
|
|
509
|
-
"",
|
|
510
|
-
summaries.join("\n\n"),
|
|
511
|
-
"",
|
|
512
|
-
formatUsage(totalUsage),
|
|
513
|
-
].join("\n"),
|
|
514
|
-
}],
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// ── Chain mode ────────────────────────────────────────────────────────────
|
|
519
|
-
if (params.chain && params.chain.length > 0) {
|
|
520
|
-
const firstTask = params.chain[0]?.task ?? "";
|
|
521
|
-
let previousOutput = "";
|
|
522
736
|
|
|
523
|
-
|
|
737
|
+
emitParallel(true);
|
|
524
738
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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);
|
|
528
744
|
if (error || !agent) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Resolve task template
|
|
536
|
-
let task = step.task ?? (i === 0 ? firstTask : "{previous}");
|
|
537
|
-
task = task
|
|
538
|
-
.replace(/\{previous\}/g, previousOutput)
|
|
539
|
-
.replace(/\{task\}/g, firstTask);
|
|
540
|
-
|
|
541
|
-
if (onUpdate) {
|
|
542
|
-
onUpdate({
|
|
543
|
-
content: [{
|
|
544
|
-
type: "text",
|
|
545
|
-
text: `Chain step ${i + 1}/${params.chain.length}: ${step.agent}...`,
|
|
546
|
-
}],
|
|
547
|
-
details: {},
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const result = await runAgent(
|
|
552
|
-
agent,
|
|
553
|
-
task,
|
|
554
|
-
step.cwd ?? cwd,
|
|
555
|
-
step.model,
|
|
556
|
-
signal,
|
|
557
|
-
onUpdate,
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
stepResults.push({ ...result, agentName: step.agent, step: i + 1 });
|
|
561
|
-
|
|
562
|
-
if (result.exitCode !== 0) {
|
|
563
|
-
return {
|
|
564
|
-
content: [{
|
|
565
|
-
type: "text",
|
|
566
|
-
text: `Chain failed at step ${i + 1} (${step.agent}): ${result.error ?? "(no output)"}`,
|
|
567
|
-
}],
|
|
568
|
-
isError: true,
|
|
569
|
-
};
|
|
745
|
+
parallelAgents[i]!.status = "error";
|
|
746
|
+
emitParallel(true);
|
|
747
|
+
return { agentName: t.agent, output: "", exitCode: 1, error, model: undefined, toolCalls: [] as ToolCallEntry[], usage: emptyUsage };
|
|
570
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
|
+
});
|
|
571
765
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const last = stepResults[stepResults.length - 1];
|
|
576
|
-
const totalUsage = stepResults.reduce(
|
|
577
|
-
(acc, r) => ({
|
|
578
|
-
input: acc.input + r.usage.input,
|
|
579
|
-
output: acc.output + r.usage.output,
|
|
580
|
-
cost: acc.cost + r.usage.cost,
|
|
581
|
-
turns: acc.turns + r.usage.turns,
|
|
582
|
-
}),
|
|
583
|
-
{ 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,
|
|
584
769
|
);
|
|
770
|
+
const outputs = allResults.map((r) => `[${r.agentName}] ${r.exitCode === 0 ? "✓" : "✗"}\n${getFinalText(r)}`).join("\n\n");
|
|
585
771
|
|
|
586
772
|
return {
|
|
587
|
-
content: [{
|
|
588
|
-
|
|
589
|
-
text: [
|
|
590
|
-
last.output,
|
|
591
|
-
"",
|
|
592
|
-
`Chain: ${stepResults.length} steps · ${formatUsage(totalUsage)}`,
|
|
593
|
-
].join("\n"),
|
|
594
|
-
}],
|
|
773
|
+
content: [{ type: "text", text: outputs }],
|
|
774
|
+
details: { mode: "parallel", parallelAgents, usage: totalUsage, running: false, toolCalls: [] } satisfies SubagentDetails,
|
|
595
775
|
};
|
|
596
776
|
}
|
|
597
777
|
|
|
778
|
+
// ── Chain mode ────────────────────────────────────────────
|
|
598
779
|
// Shouldn't reach here
|
|
599
|
-
return { content: [{ type: "text", text: "Provide agent+task
|
|
780
|
+
return { content: [{ type: "text", text: "Provide agent+task or tasks array." }] };
|
|
600
781
|
},
|
|
601
782
|
});
|
|
602
783
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
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
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"subagent",
|
|
10
|
+
"agents",
|
|
11
|
+
"extension"
|
|
12
|
+
],
|
|
7
13
|
"homepage": "https://github.com/tuansondinh/pi-fast-subagent#readme",
|
|
8
14
|
"repository": {
|
|
9
15
|
"type": "git",
|
|
@@ -28,5 +34,10 @@
|
|
|
28
34
|
"@mariozechner/pi-tui": "*",
|
|
29
35
|
"@sinclair/typebox": "*"
|
|
30
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
|
+
},
|
|
31
42
|
"license": "MIT"
|
|
32
43
|
}
|