@chaotic1988/pi-subagent 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 +46 -0
- package/agent.ts +163 -0
- package/docs/user-guide.md +127 -0
- package/index.ts +587 -0
- package/package.json +30 -0
- package/runner.ts +295 -0
- package/skills/authoring-agent-specs/SKILL.md +147 -0
- package/tests/agent.test.ts +74 -0
- package/tests/runner.test.ts +111 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +17 -0
package/index.ts
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent Tool - Delegate tasks to specialized agents.
|
|
3
|
+
*
|
|
4
|
+
* Creates in-process SDK sessions for each subagent invocation,
|
|
5
|
+
* giving it an isolated context window.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* - Single: { agent: "name", task: "..." }
|
|
9
|
+
* - Parallel: { tasks: [{ agent: "name", task: "..." }, ...] }
|
|
10
|
+
*
|
|
11
|
+
* Sessions are persisted to disk for observability.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
16
|
+
import { AuthStorage, type ExtensionAPI, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
18
|
+
import { Type } from "typebox";
|
|
19
|
+
import { type AgentConfig, discoverAgents, formatAgentList } from "./agent.js";
|
|
20
|
+
import {
|
|
21
|
+
type SingleResult,
|
|
22
|
+
runAgent,
|
|
23
|
+
runParallel,
|
|
24
|
+
} from "./runner.js";
|
|
25
|
+
|
|
26
|
+
const MAX_PARALLEL_TASKS = 8;
|
|
27
|
+
const COLLAPSED_ITEM_COUNT = 10;
|
|
28
|
+
|
|
29
|
+
function formatTokens(count: number): string {
|
|
30
|
+
if (count < 1000) return count.toString();
|
|
31
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
32
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
33
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatUsageStats(
|
|
37
|
+
usage: {
|
|
38
|
+
input: number;
|
|
39
|
+
output: number;
|
|
40
|
+
cacheRead: number;
|
|
41
|
+
cacheWrite: number;
|
|
42
|
+
cost: number;
|
|
43
|
+
contextTokens?: number;
|
|
44
|
+
turns?: number;
|
|
45
|
+
},
|
|
46
|
+
model?: string,
|
|
47
|
+
): string {
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
50
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
51
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
52
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
53
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
54
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
55
|
+
if (usage.contextTokens && usage.contextTokens > 0) {
|
|
56
|
+
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
57
|
+
}
|
|
58
|
+
if (model) parts.push(model);
|
|
59
|
+
return parts.join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatToolCall(
|
|
63
|
+
toolName: string,
|
|
64
|
+
args: Record<string, unknown>,
|
|
65
|
+
themeFg: (color: any, text: string) => string,
|
|
66
|
+
): string {
|
|
67
|
+
const shortenPath = (p: string) => {
|
|
68
|
+
const home = os.homedir();
|
|
69
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
switch (toolName) {
|
|
73
|
+
case "bash": {
|
|
74
|
+
const command = (args.command as string) || "...";
|
|
75
|
+
const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
|
|
76
|
+
return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
|
|
77
|
+
}
|
|
78
|
+
case "read": {
|
|
79
|
+
const rawPath = (args.file_path || args.path || "...") as string;
|
|
80
|
+
const filePath = shortenPath(rawPath);
|
|
81
|
+
const offset = args.offset as number | undefined;
|
|
82
|
+
const limit = args.limit as number | undefined;
|
|
83
|
+
let text = themeFg("accent", filePath);
|
|
84
|
+
if (offset !== undefined || limit !== undefined) {
|
|
85
|
+
const startLine = offset ?? 1;
|
|
86
|
+
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
87
|
+
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
88
|
+
}
|
|
89
|
+
return themeFg("muted", "read ") + text;
|
|
90
|
+
}
|
|
91
|
+
case "write": {
|
|
92
|
+
const rawPath = (args.file_path || args.path || "...") as string;
|
|
93
|
+
const filePath = shortenPath(rawPath);
|
|
94
|
+
const content = (args.content || "") as string;
|
|
95
|
+
const lines = content.split("\n").length;
|
|
96
|
+
let text = themeFg("muted", "write ") + themeFg("accent", filePath);
|
|
97
|
+
if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
|
|
98
|
+
return text;
|
|
99
|
+
}
|
|
100
|
+
case "edit": {
|
|
101
|
+
const rawPath = (args.file_path || args.path || "...") as string;
|
|
102
|
+
return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
|
|
103
|
+
}
|
|
104
|
+
case "ls": {
|
|
105
|
+
const rawPath = (args.path || ".") as string;
|
|
106
|
+
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
|
|
107
|
+
}
|
|
108
|
+
case "find": {
|
|
109
|
+
const pattern = (args.pattern || "*") as string;
|
|
110
|
+
const rawPath = (args.path || ".") as string;
|
|
111
|
+
return (
|
|
112
|
+
themeFg("muted", "find ") +
|
|
113
|
+
themeFg("accent", pattern) +
|
|
114
|
+
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
case "grep": {
|
|
118
|
+
const pattern = (args.pattern || "") as string;
|
|
119
|
+
const rawPath = (args.path || ".") as string;
|
|
120
|
+
return (
|
|
121
|
+
themeFg("muted", "grep ") +
|
|
122
|
+
themeFg("accent", `/${pattern}/`) +
|
|
123
|
+
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
default: {
|
|
127
|
+
const argsStr = JSON.stringify(args);
|
|
128
|
+
const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
|
|
129
|
+
return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface SubagentDetails {
|
|
135
|
+
mode: "single" | "parallel";
|
|
136
|
+
results: SingleResult[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getFinalOutput(messages: Message[]): string {
|
|
140
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
141
|
+
const msg = messages[i];
|
|
142
|
+
if (msg.role === "assistant") {
|
|
143
|
+
for (const part of msg.content) {
|
|
144
|
+
if (part.type === "text") return part.text;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type DisplayItem =
|
|
152
|
+
| { type: "text"; text: string }
|
|
153
|
+
| { type: "toolCall"; name: string; args: Record<string, any> };
|
|
154
|
+
|
|
155
|
+
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
156
|
+
const items: DisplayItem[] = [];
|
|
157
|
+
for (const msg of messages) {
|
|
158
|
+
if (msg.role === "assistant") {
|
|
159
|
+
for (const part of msg.content) {
|
|
160
|
+
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
161
|
+
else if (part.type === "toolCall")
|
|
162
|
+
items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return items;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const TaskItem = Type.Object({
|
|
170
|
+
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
171
|
+
task: Type.String({ description: "Task to delegate to the agent" }),
|
|
172
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const SubagentParams = Type.Object({
|
|
176
|
+
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
|
|
177
|
+
task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
|
|
178
|
+
tasks: Type.Optional(
|
|
179
|
+
Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" }),
|
|
180
|
+
),
|
|
181
|
+
cwd: Type.Optional(
|
|
182
|
+
Type.String({ description: "Working directory for the agent process (single mode)" }),
|
|
183
|
+
),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const MAX_AGENTS_IN_DESCRIPTION = 20;
|
|
187
|
+
|
|
188
|
+
function buildDescription(agents: AgentConfig[]): string {
|
|
189
|
+
const base =
|
|
190
|
+
"Delegate tasks to specialized subagents with isolated context. " +
|
|
191
|
+
"Modes: single (agent + task), parallel (tasks array).";
|
|
192
|
+
|
|
193
|
+
if (agents.length === 0) {
|
|
194
|
+
return `${base} No agents discovered.`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { text, remaining } = formatAgentList(agents, MAX_AGENTS_IN_DESCRIPTION);
|
|
198
|
+
const suffix = remaining > 0 ? ` ... and ${remaining} more` : "";
|
|
199
|
+
return `${base} Available agents: ${text}${suffix}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default function (pi: ExtensionAPI) {
|
|
203
|
+
let cachedAgents: AgentConfig[] = [];
|
|
204
|
+
let authStorage: AuthStorage;
|
|
205
|
+
|
|
206
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
207
|
+
cachedAgents = discoverAgents(ctx);
|
|
208
|
+
authStorage = AuthStorage.create();
|
|
209
|
+
|
|
210
|
+
pi.registerCommand("agents", {
|
|
211
|
+
description: "List available agents",
|
|
212
|
+
handler: async (_args, ctx) => {
|
|
213
|
+
if (cachedAgents.length === 0) {
|
|
214
|
+
ctx.ui.notify("No agents discovered.", "info");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const lines = cachedAgents.map(
|
|
218
|
+
(a) => `${a.name} (${a.source}): ${a.description}`,
|
|
219
|
+
);
|
|
220
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
pi.registerTool({
|
|
225
|
+
name: "delegate",
|
|
226
|
+
label: "Delegate",
|
|
227
|
+
description: buildDescription(cachedAgents),
|
|
228
|
+
promptSnippet: "Delegate tasks to subagents",
|
|
229
|
+
parameters: SubagentParams,
|
|
230
|
+
|
|
231
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
232
|
+
const agents = cachedAgents;
|
|
233
|
+
|
|
234
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
235
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
236
|
+
const modeCount = Number(hasTasks) + Number(hasSingle);
|
|
237
|
+
|
|
238
|
+
const makeDetails =
|
|
239
|
+
(mode: "single" | "parallel") =>
|
|
240
|
+
(results: SingleResult[]): SubagentDetails => ({
|
|
241
|
+
mode,
|
|
242
|
+
results,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (modeCount !== 1) {
|
|
246
|
+
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: `Invalid parameters. Provide exactly one mode.\nAvailable agents: ${available}`,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
details: makeDetails("single")([]),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (params.tasks && params.tasks.length > 0) {
|
|
259
|
+
if (params.tasks.length > MAX_PARALLEL_TASKS)
|
|
260
|
+
return {
|
|
261
|
+
content: [
|
|
262
|
+
{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
details: makeDetails("parallel")([]),
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const results = await runParallel(
|
|
271
|
+
ctx.cwd,
|
|
272
|
+
agents,
|
|
273
|
+
params.tasks,
|
|
274
|
+
ctx.modelRegistry,
|
|
275
|
+
authStorage,
|
|
276
|
+
signal,
|
|
277
|
+
(allResults) => {
|
|
278
|
+
if (!onUpdate) return;
|
|
279
|
+
const running = allResults.filter((r) => r.exitCode === -1).length;
|
|
280
|
+
const done = allResults.filter((r) => r.exitCode !== -1).length;
|
|
281
|
+
onUpdate({
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: `Parallel: ${done}/${allResults.length} done, ${running} running...`,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
details: makeDetails("parallel")(allResults),
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
294
|
+
const summaries = results.map((r) => {
|
|
295
|
+
const output = getFinalOutput(r.messages);
|
|
296
|
+
const preview = output.slice(0, 100) + (output.length > 100 ? "..." : "");
|
|
297
|
+
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
details: makeDetails("parallel")(results),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (params.agent && params.task) {
|
|
311
|
+
const agent = agents.find((a) => a.name === params.agent);
|
|
312
|
+
if (!agent) {
|
|
313
|
+
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: "text",
|
|
318
|
+
text: `Unknown agent: "${params.agent}". Available agents: ${available}.`,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
details: makeDetails("single")([]),
|
|
322
|
+
isError: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const result = await runAgent(
|
|
327
|
+
agent,
|
|
328
|
+
params.task,
|
|
329
|
+
params.cwd ?? ctx.cwd,
|
|
330
|
+
ctx.modelRegistry,
|
|
331
|
+
authStorage,
|
|
332
|
+
signal,
|
|
333
|
+
onUpdate
|
|
334
|
+
? (partial) => {
|
|
335
|
+
onUpdate({
|
|
336
|
+
content: [
|
|
337
|
+
{
|
|
338
|
+
type: "text",
|
|
339
|
+
text: getFinalOutput(partial.messages) || "(running...)",
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
details: makeDetails("single")([partial]),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
: undefined,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const isError =
|
|
349
|
+
result.exitCode !== 0 ||
|
|
350
|
+
result.stopReason === "error" ||
|
|
351
|
+
result.stopReason === "aborted";
|
|
352
|
+
if (isError) {
|
|
353
|
+
const errorMsg =
|
|
354
|
+
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
355
|
+
return {
|
|
356
|
+
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
357
|
+
details: makeDetails("single")([result]),
|
|
358
|
+
isError: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: "text", text: getFinalOutput(result.messages) || "(no output)" }],
|
|
363
|
+
details: makeDetails("single")([result]),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
368
|
+
return {
|
|
369
|
+
content: [{ type: "text", text: `Invalid parameters. Available agents: ${available}` }],
|
|
370
|
+
details: makeDetails("single")([]),
|
|
371
|
+
};
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
renderCall(args, theme) {
|
|
375
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
376
|
+
let text =
|
|
377
|
+
theme.fg("toolTitle", theme.bold("delegate ")) +
|
|
378
|
+
theme.fg("accent", `parallel (${args.tasks.length} tasks)`);
|
|
379
|
+
for (const t of args.tasks.slice(0, 3)) {
|
|
380
|
+
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
|
|
381
|
+
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
|
|
382
|
+
}
|
|
383
|
+
if (args.tasks.length > 3)
|
|
384
|
+
text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
|
|
385
|
+
return new Text(text, 0, 0);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const agentName = args.agent || "...";
|
|
389
|
+
const preview = args.task
|
|
390
|
+
? args.task.length > 60
|
|
391
|
+
? `${args.task.slice(0, 60)}...`
|
|
392
|
+
: args.task
|
|
393
|
+
: "...";
|
|
394
|
+
const text =
|
|
395
|
+
theme.fg("toolTitle", theme.bold("delegate ")) +
|
|
396
|
+
theme.fg("accent", agentName) +
|
|
397
|
+
`\n ${theme.fg("dim", preview)}`;
|
|
398
|
+
return new Text(text, 0, 0);
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
renderResult(result, { expanded }, theme) {
|
|
402
|
+
const details = result.details as SubagentDetails | undefined;
|
|
403
|
+
if (!details || details.results.length === 0) {
|
|
404
|
+
const text = result.content[0];
|
|
405
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const mdTheme = getMarkdownTheme();
|
|
409
|
+
|
|
410
|
+
const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
|
|
411
|
+
const toShow = limit ? items.slice(-limit) : items;
|
|
412
|
+
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
|
413
|
+
let text = "";
|
|
414
|
+
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
|
415
|
+
for (const item of toShow) {
|
|
416
|
+
if (item.type === "text") {
|
|
417
|
+
const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
|
|
418
|
+
text += `${theme.fg("toolOutput", preview)}\n`;
|
|
419
|
+
} else {
|
|
420
|
+
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return text.trimEnd();
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
if (details.mode === "single" && details.results.length === 1) {
|
|
427
|
+
const r = details.results[0];
|
|
428
|
+
const isError =
|
|
429
|
+
r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
|
430
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
431
|
+
const displayItems = getDisplayItems(r.messages);
|
|
432
|
+
const finalOutput = getFinalOutput(r.messages);
|
|
433
|
+
|
|
434
|
+
if (expanded) {
|
|
435
|
+
const container = new Container();
|
|
436
|
+
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
437
|
+
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
438
|
+
container.addChild(new Text(header, 0, 0));
|
|
439
|
+
if (isError && r.errorMessage)
|
|
440
|
+
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
441
|
+
container.addChild(new Spacer(1));
|
|
442
|
+
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
443
|
+
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
444
|
+
container.addChild(new Spacer(1));
|
|
445
|
+
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
446
|
+
if (displayItems.length === 0 && !finalOutput) {
|
|
447
|
+
container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
448
|
+
} else {
|
|
449
|
+
for (const item of displayItems) {
|
|
450
|
+
if (item.type === "toolCall")
|
|
451
|
+
container.addChild(
|
|
452
|
+
new Text(
|
|
453
|
+
theme.fg("muted", "→ ") +
|
|
454
|
+
formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
455
|
+
0,
|
|
456
|
+
0,
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (finalOutput) {
|
|
461
|
+
container.addChild(new Spacer(1));
|
|
462
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const usageStr = formatUsageStats(r.usage, r.model);
|
|
466
|
+
if (usageStr) {
|
|
467
|
+
container.addChild(new Spacer(1));
|
|
468
|
+
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
469
|
+
}
|
|
470
|
+
return container;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
474
|
+
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
475
|
+
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
476
|
+
else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
477
|
+
else {
|
|
478
|
+
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
|
|
479
|
+
if (displayItems.length > COLLAPSED_ITEM_COUNT)
|
|
480
|
+
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
481
|
+
}
|
|
482
|
+
const usageStr = formatUsageStats(r.usage, r.model);
|
|
483
|
+
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
484
|
+
return new Text(text, 0, 0);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Parallel mode
|
|
488
|
+
const aggregateUsage = (results: SingleResult[]) => {
|
|
489
|
+
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
490
|
+
for (const r of results) {
|
|
491
|
+
total.input += r.usage.input;
|
|
492
|
+
total.output += r.usage.output;
|
|
493
|
+
total.cacheRead += r.usage.cacheRead;
|
|
494
|
+
total.cacheWrite += r.usage.cacheWrite;
|
|
495
|
+
total.cost += r.usage.cost;
|
|
496
|
+
total.turns += r.usage.turns;
|
|
497
|
+
}
|
|
498
|
+
return total;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const running = details.results.filter((r) => r.exitCode === -1).length;
|
|
502
|
+
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
503
|
+
const failCount = details.results.filter((r) => r.exitCode > 0).length;
|
|
504
|
+
const isRunning = running > 0;
|
|
505
|
+
const icon = isRunning
|
|
506
|
+
? theme.fg("warning", "⏳")
|
|
507
|
+
: failCount > 0
|
|
508
|
+
? theme.fg("warning", "◐")
|
|
509
|
+
: theme.fg("success", "✓");
|
|
510
|
+
const status = isRunning
|
|
511
|
+
? `${successCount + failCount}/${details.results.length} done, ${running} running`
|
|
512
|
+
: `${successCount}/${details.results.length} tasks`;
|
|
513
|
+
|
|
514
|
+
if (expanded && !isRunning) {
|
|
515
|
+
const container = new Container();
|
|
516
|
+
container.addChild(
|
|
517
|
+
new Text(
|
|
518
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
|
519
|
+
0,
|
|
520
|
+
0,
|
|
521
|
+
),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
for (const r of details.results) {
|
|
525
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
526
|
+
const displayItems = getDisplayItems(r.messages);
|
|
527
|
+
const finalOutput = getFinalOutput(r.messages);
|
|
528
|
+
|
|
529
|
+
container.addChild(new Spacer(1));
|
|
530
|
+
container.addChild(
|
|
531
|
+
new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
|
|
532
|
+
);
|
|
533
|
+
container.addChild(
|
|
534
|
+
new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0),
|
|
535
|
+
);
|
|
536
|
+
for (const item of displayItems) {
|
|
537
|
+
if (item.type === "toolCall")
|
|
538
|
+
container.addChild(
|
|
539
|
+
new Text(
|
|
540
|
+
theme.fg("muted", "→ ") +
|
|
541
|
+
formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
542
|
+
0,
|
|
543
|
+
0,
|
|
544
|
+
),
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (finalOutput) {
|
|
548
|
+
container.addChild(new Spacer(1));
|
|
549
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
550
|
+
}
|
|
551
|
+
const taskUsage = formatUsageStats(r.usage, r.model);
|
|
552
|
+
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
556
|
+
if (usageStr) {
|
|
557
|
+
container.addChild(new Spacer(1));
|
|
558
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
559
|
+
}
|
|
560
|
+
return container;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Collapsed view (or still running)
|
|
564
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
565
|
+
for (const r of details.results) {
|
|
566
|
+
const rIcon =
|
|
567
|
+
r.exitCode === -1
|
|
568
|
+
? theme.fg("warning", "⏳")
|
|
569
|
+
: r.exitCode === 0
|
|
570
|
+
? theme.fg("success", "✓")
|
|
571
|
+
: theme.fg("error", "✗");
|
|
572
|
+
const displayItems = getDisplayItems(r.messages);
|
|
573
|
+
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
574
|
+
if (displayItems.length === 0)
|
|
575
|
+
text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
576
|
+
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
577
|
+
}
|
|
578
|
+
if (!isRunning) {
|
|
579
|
+
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
580
|
+
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
581
|
+
}
|
|
582
|
+
if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
583
|
+
return new Text(text, 0, 0);
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chaotic1988/pi-subagent",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"pi-package"
|
|
6
|
+
],
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./index.ts"
|
|
10
|
+
],
|
|
11
|
+
"skills": [
|
|
12
|
+
"./skills"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "vitest run"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@earendil-works/pi-agent-core": "*",
|
|
20
|
+
"@earendil-works/pi-ai": "*",
|
|
21
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
22
|
+
"@earendil-works/pi-tui": "*",
|
|
23
|
+
"typebox": "*"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22",
|
|
27
|
+
"typescript": "^6.0.3",
|
|
28
|
+
"vitest": "^4.1.0"
|
|
29
|
+
}
|
|
30
|
+
}
|