@narumitw/pi-subagents 0.13.0 → 0.14.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 +137 -12
- package/package.json +3 -3
- package/src/agents.ts +17 -0
- package/src/config-ui.ts +271 -0
- package/src/context.ts +129 -0
- package/src/execution.ts +453 -0
- package/src/in-process-transport.ts +598 -0
- package/src/limits.ts +62 -0
- package/src/orchestration.ts +164 -0
- package/src/params.ts +59 -0
- package/src/persistence.ts +207 -0
- package/src/protocol.ts +76 -0
- package/src/registry.ts +759 -0
- package/src/render.ts +521 -0
- package/src/runner.ts +496 -0
- package/src/settings.ts +166 -0
- package/src/stateful-prompt.ts +43 -0
- package/src/stateful.ts +838 -0
- package/src/subagents.ts +36 -1682
- package/src/subprocess-transport.ts +50 -0
- package/src/transport.ts +30 -0
- package/src/workspace.ts +116 -0
package/src/subagents.ts
CHANGED
|
@@ -12,808 +12,16 @@
|
|
|
12
12
|
* Uses JSON mode to capture structured output from subagents.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import type {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
type ExtensionAPI,
|
|
24
|
-
getAgentDir,
|
|
25
|
-
getMarkdownTheme,
|
|
26
|
-
withFileMutationQueue,
|
|
27
|
-
DynamicBorder,
|
|
28
|
-
} from "@earendil-works/pi-coding-agent";
|
|
29
|
-
import {
|
|
30
|
-
Container,
|
|
31
|
-
Markdown,
|
|
32
|
-
Spacer,
|
|
33
|
-
Text,
|
|
34
|
-
type SelectItem,
|
|
35
|
-
SelectList,
|
|
36
|
-
Key,
|
|
37
|
-
matchesKey,
|
|
38
|
-
truncateToWidth,
|
|
39
|
-
} from "@earendil-works/pi-tui";
|
|
40
|
-
import { Type } from "typebox";
|
|
41
|
-
import {
|
|
42
|
-
THINKING_LEVELS,
|
|
43
|
-
type AgentConfig,
|
|
44
|
-
type AgentScope,
|
|
45
|
-
type AgentSource,
|
|
46
|
-
type SubagentAgentConfig,
|
|
47
|
-
type SubagentSettings,
|
|
48
|
-
type SubagentThinkingLevel,
|
|
49
|
-
discoverAgents,
|
|
50
|
-
isThinkingLevel,
|
|
51
|
-
} from "./agents.js";
|
|
52
|
-
|
|
53
|
-
const MAX_PARALLEL_TASKS = 8;
|
|
54
|
-
const MAX_CONCURRENCY = 4;
|
|
55
|
-
const COLLAPSED_ITEM_COUNT = 10;
|
|
56
|
-
const DEFAULT_TIMEOUT_MS = parsePositiveInteger(process.env.PI_SUBAGENT_TIMEOUT_MS) ?? 10 * 60 * 1000;
|
|
57
|
-
const KILL_GRACE_MS = 5000;
|
|
58
|
-
const STATUS_KEY = "subagents";
|
|
59
|
-
const activeStatuses = new Map<string, string>();
|
|
60
|
-
|
|
61
|
-
export function parsePositiveInteger(value: string | undefined): number | undefined {
|
|
62
|
-
if (!value) return undefined;
|
|
63
|
-
const parsed = Number.parseInt(value, 10);
|
|
64
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface StatusContext {
|
|
68
|
-
ui: { setStatus: (key: string, value: string | undefined) => void };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function startSubagentStatus(ctx: StatusContext, toolCallId: string, status: string) {
|
|
72
|
-
let cleared = false;
|
|
73
|
-
|
|
74
|
-
const update = (nextStatus: string) => {
|
|
75
|
-
if (cleared) return;
|
|
76
|
-
activeStatuses.set(toolCallId, nextStatus);
|
|
77
|
-
publishSubagentStatus(ctx);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
update(status);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
update,
|
|
84
|
-
clear() {
|
|
85
|
-
if (cleared) return;
|
|
86
|
-
cleared = true;
|
|
87
|
-
activeStatuses.delete(toolCallId);
|
|
88
|
-
publishSubagentStatus(ctx);
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function publishSubagentStatus(ctx: StatusContext) {
|
|
94
|
-
const statuses = [...activeStatuses.values()];
|
|
95
|
-
if (statuses.length === 0) {
|
|
96
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const suffix = statuses.length > 1 ? ` +${statuses.length - 1}` : "";
|
|
101
|
-
ctx.ui.setStatus(STATUS_KEY, `${statuses[0]}${suffix}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function singleStatus(agent: string): string {
|
|
105
|
-
return `${agent}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function chainStatus(step: number, total: number, agent?: string): string {
|
|
109
|
-
return `chain ${step}/${total}${agent ? ` ${agent}` : ""}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function parallelStatus(done: number, total: number, running: number): string {
|
|
113
|
-
return `parallel ${done}/${total} done${running > 0 ? ` ${running} running` : ""}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function fanInStatus(agent: string): string {
|
|
117
|
-
return `fan-in ${agent}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function formatTokens(count: number): string {
|
|
121
|
-
if (count < 1000) return count.toString();
|
|
122
|
-
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
123
|
-
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
124
|
-
return `${(count / 1000000).toFixed(1)}M`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function formatUsageStats(
|
|
128
|
-
usage: {
|
|
129
|
-
input: number;
|
|
130
|
-
output: number;
|
|
131
|
-
cacheRead: number;
|
|
132
|
-
cacheWrite: number;
|
|
133
|
-
cost: number;
|
|
134
|
-
contextTokens?: number;
|
|
135
|
-
turns?: number;
|
|
136
|
-
},
|
|
137
|
-
model?: string,
|
|
138
|
-
thinkingLevel?: SubagentThinkingLevel,
|
|
139
|
-
): string {
|
|
140
|
-
const parts: string[] = [];
|
|
141
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
142
|
-
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
143
|
-
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
144
|
-
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
145
|
-
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
146
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
147
|
-
if (usage.contextTokens && usage.contextTokens > 0) {
|
|
148
|
-
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
149
|
-
}
|
|
150
|
-
if (model) parts.push(model);
|
|
151
|
-
if (thinkingLevel) parts.push(`thinking:${thinkingLevel}`);
|
|
152
|
-
return parts.join(" ");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function formatToolCall(
|
|
156
|
-
toolName: string,
|
|
157
|
-
args: Record<string, unknown>,
|
|
158
|
-
themeFg: (color: any, text: string) => string,
|
|
159
|
-
): string {
|
|
160
|
-
const shortenPath = (p: string) => {
|
|
161
|
-
const home = os.homedir();
|
|
162
|
-
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
switch (toolName) {
|
|
166
|
-
case "bash": {
|
|
167
|
-
const command = (args.command as string) || "...";
|
|
168
|
-
const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
|
|
169
|
-
return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
|
|
170
|
-
}
|
|
171
|
-
case "read": {
|
|
172
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
173
|
-
const filePath = shortenPath(rawPath);
|
|
174
|
-
const offset = args.offset as number | undefined;
|
|
175
|
-
const limit = args.limit as number | undefined;
|
|
176
|
-
let text = themeFg("accent", filePath);
|
|
177
|
-
if (offset !== undefined || limit !== undefined) {
|
|
178
|
-
const startLine = offset ?? 1;
|
|
179
|
-
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
180
|
-
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
181
|
-
}
|
|
182
|
-
return themeFg("muted", "read ") + text;
|
|
183
|
-
}
|
|
184
|
-
case "write": {
|
|
185
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
186
|
-
const filePath = shortenPath(rawPath);
|
|
187
|
-
const content = (args.content || "") as string;
|
|
188
|
-
const lines = content.split("\n").length;
|
|
189
|
-
let text = themeFg("muted", "write ") + themeFg("accent", filePath);
|
|
190
|
-
if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
|
|
191
|
-
return text;
|
|
192
|
-
}
|
|
193
|
-
case "edit": {
|
|
194
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
195
|
-
return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
|
|
196
|
-
}
|
|
197
|
-
case "ls": {
|
|
198
|
-
const rawPath = (args.path || ".") as string;
|
|
199
|
-
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
|
|
200
|
-
}
|
|
201
|
-
case "find": {
|
|
202
|
-
const pattern = (args.pattern || "*") as string;
|
|
203
|
-
const rawPath = (args.path || ".") as string;
|
|
204
|
-
return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`);
|
|
205
|
-
}
|
|
206
|
-
case "grep": {
|
|
207
|
-
const pattern = (args.pattern || "") as string;
|
|
208
|
-
const rawPath = (args.path || ".") as string;
|
|
209
|
-
return (
|
|
210
|
-
themeFg("muted", "grep ") +
|
|
211
|
-
themeFg("accent", `/${pattern}/`) +
|
|
212
|
-
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
default: {
|
|
216
|
-
const argsStr = JSON.stringify(args);
|
|
217
|
-
const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
|
|
218
|
-
return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
interface UsageStats {
|
|
224
|
-
input: number;
|
|
225
|
-
output: number;
|
|
226
|
-
cacheRead: number;
|
|
227
|
-
cacheWrite: number;
|
|
228
|
-
cost: number;
|
|
229
|
-
contextTokens: number;
|
|
230
|
-
turns: number;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
interface SingleResult {
|
|
234
|
-
agent: string;
|
|
235
|
-
agentSource: AgentSource | "unknown";
|
|
236
|
-
task: string;
|
|
237
|
-
exitCode: number;
|
|
238
|
-
messages: Message[];
|
|
239
|
-
stderr: string;
|
|
240
|
-
usage: UsageStats;
|
|
241
|
-
model?: string;
|
|
242
|
-
thinkingLevel?: SubagentThinkingLevel;
|
|
243
|
-
stopReason?: string;
|
|
244
|
-
errorMessage?: string;
|
|
245
|
-
step?: number;
|
|
246
|
-
finalOutput?: string;
|
|
247
|
-
timedOut?: boolean;
|
|
248
|
-
timeoutMs?: number;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
interface SubagentDetails {
|
|
252
|
-
mode: "single" | "parallel" | "chain";
|
|
253
|
-
agentScope: AgentScope;
|
|
254
|
-
projectAgentsDir: string | null;
|
|
255
|
-
results: SingleResult[];
|
|
256
|
-
aggregator?: SingleResult;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function getFinalOutput(messages: Message[]): string {
|
|
260
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
261
|
-
const msg = messages[i];
|
|
262
|
-
if (msg.role === "assistant") {
|
|
263
|
-
for (const part of msg.content) {
|
|
264
|
-
if (part.type === "text") return part.text;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return "";
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function getResultFinalOutput(result: SingleResult): string {
|
|
272
|
-
return result.finalOutput ?? getFinalOutput(result.messages);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function buildFanInContext(results: SingleResult[]): string {
|
|
276
|
-
return results
|
|
277
|
-
.map((result, index) => {
|
|
278
|
-
const status = result.exitCode === 0 ? "completed" : result.exitCode === -1 ? "running" : "failed";
|
|
279
|
-
const output = getResultFinalOutput(result);
|
|
280
|
-
const error = result.errorMessage || result.stderr.trim();
|
|
281
|
-
return [
|
|
282
|
-
`## Result ${index + 1}: ${result.agent} (${status})`,
|
|
283
|
-
`Task: ${result.task}`,
|
|
284
|
-
output ? `Output:\n${output}` : error ? `Error:\n${error}` : "Output: (no output)",
|
|
285
|
-
].join("\n\n");
|
|
286
|
-
})
|
|
287
|
-
.join("\n\n---\n\n");
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, any> };
|
|
291
|
-
|
|
292
|
-
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
293
|
-
const items: DisplayItem[] = [];
|
|
294
|
-
for (const msg of messages) {
|
|
295
|
-
if (msg.role === "assistant") {
|
|
296
|
-
for (const part of msg.content) {
|
|
297
|
-
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
298
|
-
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return items;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
306
|
-
items: TIn[],
|
|
307
|
-
concurrency: number,
|
|
308
|
-
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
309
|
-
): Promise<TOut[]> {
|
|
310
|
-
if (items.length === 0) return [];
|
|
311
|
-
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
312
|
-
const results: TOut[] = new Array(items.length);
|
|
313
|
-
let nextIndex = 0;
|
|
314
|
-
const workers = new Array(limit).fill(null).map(async () => {
|
|
315
|
-
while (true) {
|
|
316
|
-
const current = nextIndex++;
|
|
317
|
-
if (current >= items.length) return;
|
|
318
|
-
results[current] = await fn(items[current], current);
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
await Promise.all(workers);
|
|
322
|
-
return results;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
|
|
326
|
-
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
|
327
|
-
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
328
|
-
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
329
|
-
await withFileMutationQueue(filePath, async () => {
|
|
330
|
-
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
331
|
-
});
|
|
332
|
-
return { dir: tmpDir, filePath };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export function buildPiArgs(options: {
|
|
336
|
-
model?: string;
|
|
337
|
-
thinkingLevel?: SubagentThinkingLevel;
|
|
338
|
-
tools?: string[];
|
|
339
|
-
systemPromptPath?: string;
|
|
340
|
-
task: string;
|
|
341
|
-
}): string[] {
|
|
342
|
-
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
343
|
-
if (options.model) args.push("--model", options.model);
|
|
344
|
-
if (options.thinkingLevel) args.push("--thinking", options.thinkingLevel);
|
|
345
|
-
if (Array.isArray(options.tools)) {
|
|
346
|
-
if (options.tools.length > 0) args.push("--tools", options.tools.join(","));
|
|
347
|
-
else args.push("--no-tools");
|
|
348
|
-
}
|
|
349
|
-
if (options.systemPromptPath) args.push("--append-system-prompt", options.systemPromptPath);
|
|
350
|
-
args.push(`Task: ${options.task}`);
|
|
351
|
-
return args;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
355
|
-
const currentScript = process.argv[1];
|
|
356
|
-
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
357
|
-
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
358
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
362
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
363
|
-
if (!isGenericRuntime) {
|
|
364
|
-
return { command: process.execPath, args };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return { command: "pi", args };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function terminateProcess(proc: ReturnType<typeof spawn>) {
|
|
371
|
-
if (proc.killed) return;
|
|
372
|
-
if (process.platform !== "win32" && proc.pid) {
|
|
373
|
-
try {
|
|
374
|
-
process.kill(-proc.pid, "SIGTERM");
|
|
375
|
-
} catch {
|
|
376
|
-
proc.kill("SIGTERM");
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
proc.kill("SIGTERM");
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
setTimeout(() => {
|
|
383
|
-
if (proc.killed) return;
|
|
384
|
-
if (process.platform !== "win32" && proc.pid) {
|
|
385
|
-
try {
|
|
386
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
387
|
-
} catch {
|
|
388
|
-
proc.kill("SIGKILL");
|
|
389
|
-
}
|
|
390
|
-
} else {
|
|
391
|
-
proc.kill("SIGKILL");
|
|
392
|
-
}
|
|
393
|
-
}, KILL_GRACE_MS).unref();
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
397
|
-
|
|
398
|
-
async function runSingleAgent(
|
|
399
|
-
defaultCwd: string,
|
|
400
|
-
agents: AgentConfig[],
|
|
401
|
-
agentName: string,
|
|
402
|
-
task: string,
|
|
403
|
-
cwd: string | undefined,
|
|
404
|
-
step: number | undefined,
|
|
405
|
-
signal: AbortSignal | undefined,
|
|
406
|
-
thinkingLevel: SubagentThinkingLevel | undefined,
|
|
407
|
-
timeoutMs: number,
|
|
408
|
-
onUpdate: OnUpdateCallback | undefined,
|
|
409
|
-
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
410
|
-
): Promise<SingleResult> {
|
|
411
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
412
|
-
|
|
413
|
-
if (!agent) {
|
|
414
|
-
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
415
|
-
return {
|
|
416
|
-
agent: agentName,
|
|
417
|
-
agentSource: "unknown",
|
|
418
|
-
task,
|
|
419
|
-
exitCode: 1,
|
|
420
|
-
messages: [],
|
|
421
|
-
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
422
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
423
|
-
thinkingLevel,
|
|
424
|
-
step,
|
|
425
|
-
finalOutput: "",
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
let tmpPromptDir: string | null = null;
|
|
430
|
-
let tmpPromptPath: string | null = null;
|
|
431
|
-
|
|
432
|
-
const currentResult: SingleResult = {
|
|
433
|
-
agent: agentName,
|
|
434
|
-
agentSource: agent.source,
|
|
435
|
-
task,
|
|
436
|
-
exitCode: 0,
|
|
437
|
-
messages: [],
|
|
438
|
-
stderr: "",
|
|
439
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
440
|
-
model: agent.model ?? undefined,
|
|
441
|
-
thinkingLevel,
|
|
442
|
-
step,
|
|
443
|
-
timeoutMs,
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const emitUpdate = () => {
|
|
447
|
-
currentResult.finalOutput = getFinalOutput(currentResult.messages);
|
|
448
|
-
if (onUpdate) {
|
|
449
|
-
onUpdate({
|
|
450
|
-
content: [{ type: "text", text: currentResult.finalOutput || "(running...)" }],
|
|
451
|
-
details: makeDetails([currentResult]),
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
try {
|
|
457
|
-
if (agent.systemPrompt.trim()) {
|
|
458
|
-
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
459
|
-
tmpPromptDir = tmp.dir;
|
|
460
|
-
tmpPromptPath = tmp.filePath;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const args = buildPiArgs({
|
|
464
|
-
model: agent.model,
|
|
465
|
-
thinkingLevel,
|
|
466
|
-
tools: agent.tools,
|
|
467
|
-
systemPromptPath: tmpPromptPath ?? undefined,
|
|
468
|
-
task,
|
|
469
|
-
});
|
|
470
|
-
let wasAborted = false;
|
|
471
|
-
let timedOut = false;
|
|
472
|
-
|
|
473
|
-
const exitCode = await new Promise<number>((resolve) => {
|
|
474
|
-
const invocation = getPiInvocation(args);
|
|
475
|
-
let settled = false;
|
|
476
|
-
const finish = (code: number) => {
|
|
477
|
-
if (settled) return;
|
|
478
|
-
settled = true;
|
|
479
|
-
clearTimeout(timeout);
|
|
480
|
-
resolve(code);
|
|
481
|
-
};
|
|
482
|
-
const proc = spawn(invocation.command, invocation.args, {
|
|
483
|
-
cwd: cwd ?? defaultCwd,
|
|
484
|
-
detached: process.platform !== "win32",
|
|
485
|
-
shell: false,
|
|
486
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
487
|
-
});
|
|
488
|
-
let buffer = "";
|
|
489
|
-
const timeout = setTimeout(() => {
|
|
490
|
-
timedOut = true;
|
|
491
|
-
currentResult.timedOut = true;
|
|
492
|
-
currentResult.stopReason = "timeout";
|
|
493
|
-
currentResult.errorMessage = `Subagent timed out after ${timeoutMs}ms`;
|
|
494
|
-
currentResult.stderr += `${currentResult.stderr ? "\n" : ""}Subagent timed out after ${timeoutMs}ms.`;
|
|
495
|
-
emitUpdate();
|
|
496
|
-
terminateProcess(proc);
|
|
497
|
-
}, timeoutMs);
|
|
498
|
-
timeout.unref();
|
|
499
|
-
|
|
500
|
-
const processLine = (line: string) => {
|
|
501
|
-
if (!line.trim()) return;
|
|
502
|
-
let event: any;
|
|
503
|
-
try {
|
|
504
|
-
event = JSON.parse(line);
|
|
505
|
-
} catch {
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (event.type === "message_end" && event.message) {
|
|
510
|
-
const msg = event.message as Message;
|
|
511
|
-
currentResult.messages.push(msg);
|
|
512
|
-
|
|
513
|
-
if (msg.role === "assistant") {
|
|
514
|
-
currentResult.usage.turns++;
|
|
515
|
-
const usage = msg.usage;
|
|
516
|
-
if (usage) {
|
|
517
|
-
currentResult.usage.input += usage.input || 0;
|
|
518
|
-
currentResult.usage.output += usage.output || 0;
|
|
519
|
-
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
520
|
-
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
521
|
-
currentResult.usage.cost += usage.cost?.total || 0;
|
|
522
|
-
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
523
|
-
}
|
|
524
|
-
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
|
525
|
-
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
526
|
-
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
527
|
-
}
|
|
528
|
-
emitUpdate();
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (event.type === "tool_result_end" && event.message) {
|
|
532
|
-
currentResult.messages.push(event.message as Message);
|
|
533
|
-
emitUpdate();
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
proc.stdout.on("data", (data) => {
|
|
538
|
-
buffer += data.toString();
|
|
539
|
-
const lines = buffer.split("\n");
|
|
540
|
-
buffer = lines.pop() || "";
|
|
541
|
-
for (const line of lines) processLine(line);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
proc.stderr.on("data", (data) => {
|
|
545
|
-
currentResult.stderr += data.toString();
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
proc.on("close", (code) => {
|
|
549
|
-
if (buffer.trim()) processLine(buffer);
|
|
550
|
-
finish(timedOut ? 124 : (code ?? 0));
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
proc.on("error", (error) => {
|
|
554
|
-
currentResult.errorMessage = error.message;
|
|
555
|
-
currentResult.stderr += `${currentResult.stderr ? "\n" : ""}${error.message}`;
|
|
556
|
-
finish(1);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
if (signal) {
|
|
560
|
-
const killProc = () => {
|
|
561
|
-
wasAborted = true;
|
|
562
|
-
currentResult.stopReason = "aborted";
|
|
563
|
-
currentResult.errorMessage = "Subagent was aborted";
|
|
564
|
-
terminateProcess(proc);
|
|
565
|
-
};
|
|
566
|
-
if (signal.aborted) killProc();
|
|
567
|
-
else signal.addEventListener("abort", killProc, { once: true });
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
currentResult.exitCode = exitCode;
|
|
572
|
-
currentResult.finalOutput = getFinalOutput(currentResult.messages);
|
|
573
|
-
if (wasAborted && !timedOut) throw new Error("Subagent was aborted");
|
|
574
|
-
return currentResult;
|
|
575
|
-
} finally {
|
|
576
|
-
if (tmpPromptPath)
|
|
577
|
-
try {
|
|
578
|
-
fs.unlinkSync(tmpPromptPath);
|
|
579
|
-
} catch {
|
|
580
|
-
/* ignore */
|
|
581
|
-
}
|
|
582
|
-
if (tmpPromptDir)
|
|
583
|
-
try {
|
|
584
|
-
fs.rmdirSync(tmpPromptDir);
|
|
585
|
-
} catch {
|
|
586
|
-
/* ignore */
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const TimeoutMs = Type.Number({
|
|
592
|
-
description:
|
|
593
|
-
"Hard timeout in milliseconds for each subagent subprocess. Defaults to PI_SUBAGENT_TIMEOUT_MS or 600000.",
|
|
594
|
-
minimum: 1,
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
const ThinkingLevelSchema = StringEnum(THINKING_LEVELS, {
|
|
598
|
-
description: "Pi thinking level for the subagent process: off, minimal, low, medium, high, or xhigh.",
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
const TaskItem = Type.Object({
|
|
602
|
-
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
603
|
-
task: Type.String({ description: "Task to delegate to the agent" }),
|
|
604
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
605
|
-
timeoutMs: Type.Optional(TimeoutMs),
|
|
606
|
-
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
const ChainItem = Type.Object({
|
|
610
|
-
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
611
|
-
task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
|
|
612
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
613
|
-
timeoutMs: Type.Optional(TimeoutMs),
|
|
614
|
-
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
const AggregatorItem = Type.Object({
|
|
618
|
-
agent: Type.String({ description: "Name of the fan-in agent to invoke after parallel tasks complete" }),
|
|
619
|
-
task: Type.String({ description: "Fan-in task. Use {previous} to include all parallel outputs." }),
|
|
620
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the aggregator process" })),
|
|
621
|
-
timeoutMs: Type.Optional(TimeoutMs),
|
|
622
|
-
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
626
|
-
description: 'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
|
|
627
|
-
default: "user",
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
const SubagentParams = Type.Object({
|
|
631
|
-
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
|
|
632
|
-
task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
|
|
633
|
-
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
634
|
-
chain: Type.Optional(Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" })),
|
|
635
|
-
aggregator: Type.Optional(AggregatorItem),
|
|
636
|
-
agentScope: Type.Optional(AgentScopeSchema),
|
|
637
|
-
confirmProjectAgents: Type.Optional(
|
|
638
|
-
Type.Boolean({ description: "Prompt before running project-local agents. Default: true.", default: true }),
|
|
639
|
-
),
|
|
640
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
641
|
-
timeoutMs: Type.Optional(TimeoutMs),
|
|
642
|
-
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
// ---- Settings helpers ----
|
|
646
|
-
|
|
647
|
-
function hasOwn(obj: object, key: PropertyKey): boolean {
|
|
648
|
-
return Object.hasOwn(obj, key);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
652
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function isStringArray(value: unknown): value is string[] {
|
|
656
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function isPositiveNumber(value: unknown): value is number {
|
|
660
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 1;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
export function normalizeAgentSettings(value: unknown): SubagentAgentConfig | undefined {
|
|
664
|
-
if (!isPlainObject(value)) return undefined;
|
|
665
|
-
|
|
666
|
-
const config: SubagentAgentConfig = {};
|
|
667
|
-
let hasKnownField = false;
|
|
668
|
-
|
|
669
|
-
if (hasOwn(value, "tools")) {
|
|
670
|
-
if (!isStringArray(value.tools)) return undefined;
|
|
671
|
-
config.tools = value.tools;
|
|
672
|
-
hasKnownField = true;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (hasOwn(value, "model")) {
|
|
676
|
-
if (value.model !== null && typeof value.model !== "string") return undefined;
|
|
677
|
-
config.model = value.model;
|
|
678
|
-
hasKnownField = true;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (hasOwn(value, "thinkingLevel")) {
|
|
682
|
-
if (value.thinkingLevel !== null && !isThinkingLevel(value.thinkingLevel)) return undefined;
|
|
683
|
-
config.thinkingLevel = value.thinkingLevel;
|
|
684
|
-
hasKnownField = true;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (hasOwn(value, "timeoutMs")) {
|
|
688
|
-
if (value.timeoutMs !== null && !isPositiveNumber(value.timeoutMs)) return undefined;
|
|
689
|
-
config.timeoutMs = value.timeoutMs;
|
|
690
|
-
hasKnownField = true;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
return hasKnownField ? config : undefined;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
export function normalizeSubagentSettings(value: unknown): SubagentSettings | undefined {
|
|
697
|
-
if (!isPlainObject(value)) return undefined;
|
|
698
|
-
if (!hasOwn(value, "agents")) return {};
|
|
699
|
-
if (!isPlainObject(value.agents)) return undefined;
|
|
700
|
-
|
|
701
|
-
const agents: Record<string, SubagentAgentConfig> = {};
|
|
702
|
-
for (const [name, rawConfig] of Object.entries(value.agents)) {
|
|
703
|
-
const config = normalizeAgentSettings(rawConfig);
|
|
704
|
-
if (config) agents[name] = config;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
return Object.keys(agents).length > 0 ? { agents } : {};
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function readSubagentSettings(): SubagentSettings | undefined {
|
|
711
|
-
const configPath = path.join(getAgentDir(), "pi-subagents-config.json");
|
|
712
|
-
if (!fs.existsSync(configPath)) return undefined;
|
|
713
|
-
try {
|
|
714
|
-
return normalizeSubagentSettings(JSON.parse(fs.readFileSync(configPath, "utf-8")));
|
|
715
|
-
} catch {
|
|
716
|
-
return undefined;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
function saveSubagentConfig(settings: SubagentSettings): void {
|
|
721
|
-
const agentDir = getAgentDir();
|
|
722
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
723
|
-
|
|
724
|
-
const configPath = path.join(agentDir, "pi-subagents-config.json");
|
|
725
|
-
fs.writeFileSync(configPath, `${JSON.stringify(settings, null, "\t")}\n`, "utf-8");
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
export function uniqueToolNames(tools: string[]): string[] {
|
|
729
|
-
return [...new Set(tools)];
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
export function sameToolSet(left: string[], right: string[]): boolean {
|
|
733
|
-
const leftSet = new Set(left);
|
|
734
|
-
const rightSet = new Set(right);
|
|
735
|
-
if (leftSet.size !== rightSet.size) return false;
|
|
736
|
-
return [...leftSet].every((tool) => rightSet.has(tool));
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
export function resolveSubagentThinkingLevel(
|
|
740
|
-
agents: readonly Pick<AgentConfig, "name" | "thinkingLevel">[],
|
|
741
|
-
agentName: string,
|
|
742
|
-
topLevelThinkingLevel?: SubagentThinkingLevel,
|
|
743
|
-
localThinkingLevel?: SubagentThinkingLevel,
|
|
744
|
-
): SubagentThinkingLevel | undefined {
|
|
745
|
-
return localThinkingLevel ?? topLevelThinkingLevel ?? agents.find((agent) => agent.name === agentName)?.thinkingLevel;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function hasAnyAgentOverride(config: SubagentAgentConfig): boolean {
|
|
749
|
-
return (
|
|
750
|
-
hasOwn(config, "tools") ||
|
|
751
|
-
hasOwn(config, "model") ||
|
|
752
|
-
hasOwn(config, "thinkingLevel") ||
|
|
753
|
-
hasOwn(config, "timeoutMs")
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// ---- Tool toggle component ----
|
|
758
|
-
|
|
759
|
-
class ToolToggleList {
|
|
760
|
-
private items: { name: string; selected: boolean }[];
|
|
761
|
-
private cursor = 0;
|
|
762
|
-
private cachedWidth?: number;
|
|
763
|
-
private cachedLines?: string[];
|
|
764
|
-
onDone?: (selected: string[]) => void;
|
|
765
|
-
onCancel?: () => void;
|
|
766
|
-
|
|
767
|
-
constructor(tools: string[], selected: Set<string>) {
|
|
768
|
-
this.items = tools.map((name) => ({ name, selected: selected.has(name) }));
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
private getSelectedNames(): string[] {
|
|
772
|
-
return this.items.filter((i) => i.selected).map((i) => i.name);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
handleInput(data: string): void {
|
|
776
|
-
if (matchesKey(data, Key.escape)) {
|
|
777
|
-
this.onCancel?.();
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
if (data === "s" || data === "S") {
|
|
781
|
-
this.onDone?.(this.getSelectedNames());
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
if (this.items.length === 0) return;
|
|
785
|
-
|
|
786
|
-
if (matchesKey(data, Key.up) && this.cursor > 0) {
|
|
787
|
-
this.cursor--;
|
|
788
|
-
this.invalidate();
|
|
789
|
-
} else if (matchesKey(data, Key.down) && this.cursor < this.items.length - 1) {
|
|
790
|
-
this.cursor++;
|
|
791
|
-
this.invalidate();
|
|
792
|
-
} else if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
|
|
793
|
-
this.items[this.cursor].selected = !this.items[this.cursor].selected;
|
|
794
|
-
this.invalidate();
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
render(width: number): string[] {
|
|
799
|
-
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
800
|
-
this.cachedWidth = width;
|
|
801
|
-
this.cachedLines = this.items.map((item, i) => {
|
|
802
|
-
const pointer = i === this.cursor ? ">" : " ";
|
|
803
|
-
const check = item.selected ? "✓" : "○";
|
|
804
|
-
return truncateToWidth(`${pointer} ${check} ${item.name}`, width);
|
|
805
|
-
});
|
|
806
|
-
return this.cachedLines;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
invalidate(): void {
|
|
810
|
-
this.cachedWidth = undefined;
|
|
811
|
-
this.cachedLines = undefined;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { registerSubagentConfigCommand } from "./config-ui.js";
|
|
17
|
+
import { SubagentParams } from "./params.js";
|
|
18
|
+
import { executeSubagent } from "./execution.js";
|
|
19
|
+
import { renderSubagentCall, renderSubagentResult } from "./render.js";
|
|
20
|
+
import type { SubagentDetails } from "./runner.js";
|
|
21
|
+
import { registerStatefulSubagents } from "./stateful.js";
|
|
814
22
|
|
|
815
23
|
export default function (pi: ExtensionAPI) {
|
|
816
|
-
pi.registerTool({
|
|
24
|
+
pi.registerTool<typeof SubagentParams, SubagentDetails>({
|
|
817
25
|
name: "subagent",
|
|
818
26
|
label: "Subagent",
|
|
819
27
|
description: [
|
|
@@ -827,8 +35,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
827
35
|
"Decide whether to spawn 0, 1, or multiple subagents for independent research, review, verification, or multi-step work in isolated Pi processes.",
|
|
828
36
|
promptGuidelines: [
|
|
829
37
|
"Use subagent only when delegation fits; the main agent should decide how many subagents to spawn from task shape instead of waiting for the user to specify a count.",
|
|
830
|
-
"Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work,
|
|
831
|
-
"
|
|
38
|
+
"Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work, tasks requiring frequent user back-and-forth, or critical-path work needed for the main agent's next action.",
|
|
39
|
+
"A single blocking subagent call should be used only when independent context, high-volume output isolation, or an external review is worth waiting for; otherwise do the task in the main agent.",
|
|
40
|
+
"For one-shot parallel work, use a single subagent call with tasks instead of repeated subagent_spawn calls, even when the user explicitly requests multiple agents.",
|
|
41
|
+
"When subagent_spawn is available, use it only when detached delegation has a concrete parallel, isolation, or specialization benefit; otherwise keep the work in the main agent.",
|
|
42
|
+
"After detached spawn, continue useful non-overlapping work when available, or call subagent_wait when coordination is the only useful next action; do not yield permanently while delegated work remains unresolved.",
|
|
43
|
+
"Consume detached completion messages and synthesize their results before finishing; interrupt or close agents that are no longer needed.",
|
|
832
44
|
"Use subagent parallel mode with 2-4 parallel read-only subagents when work has broad independent branches; prefer scout or reviewer for fan-out and add an aggregator when synthesis helps.",
|
|
833
45
|
"Use more than 4 subagent tasks only when clearly justified by distinct independent branches, and stay within the existing hard max 8 parallel tasks.",
|
|
834
46
|
"Do not use subagent parallel mode for write-heavy implementation touching the same files or shared state; serialize those changes in the main agent or one worker.",
|
|
@@ -838,891 +50,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
838
50
|
parameters: SubagentParams,
|
|
839
51
|
|
|
840
52
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
841
|
-
|
|
842
|
-
const config = readSubagentSettings();
|
|
843
|
-
const discovery = discoverAgents(ctx.cwd, agentScope, config);
|
|
844
|
-
const agents = discovery.agents;
|
|
845
|
-
const confirmProjectAgents = params.confirmProjectAgents ?? true;
|
|
846
|
-
const resolveTimeoutMs = (agentName: string, localTimeoutMs?: number) =>
|
|
847
|
-
localTimeoutMs ??
|
|
848
|
-
params.timeoutMs ??
|
|
849
|
-
agents.find((agent) => agent.name === agentName)?.timeoutMs ??
|
|
850
|
-
DEFAULT_TIMEOUT_MS;
|
|
851
|
-
const resolveThinkingLevel = (agentName: string, localThinkingLevel?: SubagentThinkingLevel) =>
|
|
852
|
-
resolveSubagentThinkingLevel(agents, agentName, params.thinkingLevel, localThinkingLevel);
|
|
853
|
-
|
|
854
|
-
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
855
|
-
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
856
|
-
const hasSingle = Boolean(params.agent && params.task);
|
|
857
|
-
const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
|
|
858
|
-
|
|
859
|
-
const makeDetails =
|
|
860
|
-
(mode: "single" | "parallel" | "chain") =>
|
|
861
|
-
(results: SingleResult[], aggregator?: SingleResult): SubagentDetails => ({
|
|
862
|
-
mode,
|
|
863
|
-
agentScope,
|
|
864
|
-
projectAgentsDir: discovery.projectAgentsDir,
|
|
865
|
-
results,
|
|
866
|
-
aggregator,
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
if (modeCount !== 1 || (params.aggregator && !hasTasks)) {
|
|
870
|
-
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
871
|
-
const reason =
|
|
872
|
-
modeCount !== 1
|
|
873
|
-
? "Provide exactly one mode."
|
|
874
|
-
: "Aggregator is only valid with parallel tasks.";
|
|
875
|
-
return {
|
|
876
|
-
content: [
|
|
877
|
-
{
|
|
878
|
-
type: "text",
|
|
879
|
-
text: `Invalid parameters. ${reason}\nAvailable agents: ${available}`,
|
|
880
|
-
},
|
|
881
|
-
],
|
|
882
|
-
details: makeDetails("single")([]),
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
|
|
887
|
-
const requestedAgentNames = new Set<string>();
|
|
888
|
-
if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
|
|
889
|
-
if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
|
|
890
|
-
if (params.aggregator) requestedAgentNames.add(params.aggregator.agent);
|
|
891
|
-
if (params.agent) requestedAgentNames.add(params.agent);
|
|
892
|
-
|
|
893
|
-
const projectAgentsRequested = Array.from(requestedAgentNames)
|
|
894
|
-
.map((name) => agents.find((a) => a.name === name))
|
|
895
|
-
.filter((a): a is AgentConfig => a?.source === "project");
|
|
896
|
-
|
|
897
|
-
if (projectAgentsRequested.length > 0) {
|
|
898
|
-
const names = projectAgentsRequested.map((a) => a.name).join(", ");
|
|
899
|
-
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
900
|
-
const ok = await ctx.ui.confirm(
|
|
901
|
-
"Run project-local agents?",
|
|
902
|
-
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
903
|
-
);
|
|
904
|
-
if (!ok)
|
|
905
|
-
return {
|
|
906
|
-
content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
|
|
907
|
-
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (params.chain && params.chain.length > 0) {
|
|
913
|
-
const results: SingleResult[] = [];
|
|
914
|
-
let previousOutput = "";
|
|
915
|
-
const status = startSubagentStatus(ctx, toolCallId, chainStatus(0, params.chain.length));
|
|
916
|
-
|
|
917
|
-
try {
|
|
918
|
-
for (let i = 0; i < params.chain.length; i++) {
|
|
919
|
-
const step = params.chain[i];
|
|
920
|
-
status.update(chainStatus(i + 1, params.chain.length, step.agent));
|
|
921
|
-
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
|
922
|
-
|
|
923
|
-
// Create update callback that includes all previous results
|
|
924
|
-
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
925
|
-
? (partial) => {
|
|
926
|
-
// Combine completed results with current streaming result
|
|
927
|
-
const currentResult = partial.details?.results[0];
|
|
928
|
-
if (currentResult) {
|
|
929
|
-
const allResults = [...results, currentResult];
|
|
930
|
-
onUpdate({
|
|
931
|
-
content: partial.content,
|
|
932
|
-
details: makeDetails("chain")(allResults),
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
: undefined;
|
|
937
|
-
|
|
938
|
-
const result = await runSingleAgent(
|
|
939
|
-
ctx.cwd,
|
|
940
|
-
agents,
|
|
941
|
-
step.agent,
|
|
942
|
-
taskWithContext,
|
|
943
|
-
step.cwd,
|
|
944
|
-
i + 1,
|
|
945
|
-
signal,
|
|
946
|
-
resolveThinkingLevel(step.agent, step.thinkingLevel),
|
|
947
|
-
resolveTimeoutMs(step.agent, step.timeoutMs),
|
|
948
|
-
chainUpdate,
|
|
949
|
-
makeDetails("chain"),
|
|
950
|
-
);
|
|
951
|
-
results.push(result);
|
|
952
|
-
|
|
953
|
-
const isError =
|
|
954
|
-
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
955
|
-
if (isError) {
|
|
956
|
-
const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
957
|
-
return {
|
|
958
|
-
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
959
|
-
details: makeDetails("chain")(results),
|
|
960
|
-
isError: true,
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
previousOutput = getResultFinalOutput(result);
|
|
964
|
-
}
|
|
965
|
-
return {
|
|
966
|
-
content: [{ type: "text", text: getResultFinalOutput(results[results.length - 1]) || "(no output)" }],
|
|
967
|
-
details: makeDetails("chain")(results),
|
|
968
|
-
};
|
|
969
|
-
} finally {
|
|
970
|
-
status.clear();
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
if (params.tasks && params.tasks.length > 0) {
|
|
975
|
-
if (params.tasks.length > MAX_PARALLEL_TASKS)
|
|
976
|
-
return {
|
|
977
|
-
content: [
|
|
978
|
-
{
|
|
979
|
-
type: "text",
|
|
980
|
-
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
981
|
-
},
|
|
982
|
-
],
|
|
983
|
-
details: makeDetails("parallel")([]),
|
|
984
|
-
};
|
|
985
|
-
|
|
986
|
-
const status = startSubagentStatus(ctx, toolCallId, parallelStatus(0, params.tasks.length, params.tasks.length));
|
|
987
|
-
|
|
988
|
-
try {
|
|
989
|
-
// Track all results for streaming updates
|
|
990
|
-
const allResults: SingleResult[] = new Array(params.tasks.length);
|
|
991
|
-
|
|
992
|
-
// Initialize placeholder results
|
|
993
|
-
for (let i = 0; i < params.tasks.length; i++) {
|
|
994
|
-
allResults[i] = {
|
|
995
|
-
agent: params.tasks[i].agent,
|
|
996
|
-
agentSource: "unknown",
|
|
997
|
-
task: params.tasks[i].task,
|
|
998
|
-
exitCode: -1, // -1 = still running
|
|
999
|
-
messages: [],
|
|
1000
|
-
stderr: "",
|
|
1001
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
1002
|
-
thinkingLevel: resolveThinkingLevel(params.tasks[i].agent, params.tasks[i].thinkingLevel),
|
|
1003
|
-
finalOutput: "",
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
let doneCount = 0;
|
|
1008
|
-
let runningCount = params.tasks.length;
|
|
1009
|
-
|
|
1010
|
-
const emitParallelUpdate = () => {
|
|
1011
|
-
status.update(parallelStatus(doneCount, allResults.length, runningCount));
|
|
1012
|
-
if (onUpdate) {
|
|
1013
|
-
onUpdate({
|
|
1014
|
-
content: [
|
|
1015
|
-
{
|
|
1016
|
-
type: "text",
|
|
1017
|
-
text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...`,
|
|
1018
|
-
},
|
|
1019
|
-
],
|
|
1020
|
-
details: makeDetails("parallel")([...allResults]),
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
1026
|
-
const result = await runSingleAgent(
|
|
1027
|
-
ctx.cwd,
|
|
1028
|
-
agents,
|
|
1029
|
-
t.agent,
|
|
1030
|
-
t.task,
|
|
1031
|
-
t.cwd,
|
|
1032
|
-
undefined,
|
|
1033
|
-
signal,
|
|
1034
|
-
resolveThinkingLevel(t.agent, t.thinkingLevel),
|
|
1035
|
-
resolveTimeoutMs(t.agent, t.timeoutMs),
|
|
1036
|
-
// Per-task update callback
|
|
1037
|
-
(partial) => {
|
|
1038
|
-
if (partial.details?.results[0]) {
|
|
1039
|
-
allResults[index] = { ...partial.details.results[0], exitCode: -1 };
|
|
1040
|
-
emitParallelUpdate();
|
|
1041
|
-
}
|
|
1042
|
-
},
|
|
1043
|
-
makeDetails("parallel"),
|
|
1044
|
-
);
|
|
1045
|
-
allResults[index] = result;
|
|
1046
|
-
doneCount += 1;
|
|
1047
|
-
runningCount -= 1;
|
|
1048
|
-
emitParallelUpdate();
|
|
1049
|
-
return result;
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
let aggregatorResult: SingleResult | undefined;
|
|
1053
|
-
if (params.aggregator) {
|
|
1054
|
-
const aggregator = params.aggregator;
|
|
1055
|
-
status.update(fanInStatus(aggregator.agent));
|
|
1056
|
-
const fanInContext = buildFanInContext(results);
|
|
1057
|
-
const aggregatorTask = aggregator.task.includes("{previous}")
|
|
1058
|
-
? aggregator.task.replace(/\{previous\}/g, fanInContext)
|
|
1059
|
-
: `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`;
|
|
1060
|
-
aggregatorResult = await runSingleAgent(
|
|
1061
|
-
ctx.cwd,
|
|
1062
|
-
agents,
|
|
1063
|
-
aggregator.agent,
|
|
1064
|
-
aggregatorTask,
|
|
1065
|
-
aggregator.cwd,
|
|
1066
|
-
undefined,
|
|
1067
|
-
signal,
|
|
1068
|
-
resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel),
|
|
1069
|
-
resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
|
|
1070
|
-
(partial) => {
|
|
1071
|
-
status.update(fanInStatus(aggregator.agent));
|
|
1072
|
-
if (onUpdate && partial.details?.results[0]) {
|
|
1073
|
-
onUpdate({
|
|
1074
|
-
content: partial.content,
|
|
1075
|
-
details: makeDetails("parallel")(results, partial.details.results[0]),
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
},
|
|
1079
|
-
makeDetails("parallel"),
|
|
1080
|
-
);
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
1084
|
-
const summaries = results.map((r) => {
|
|
1085
|
-
const output = getResultFinalOutput(r);
|
|
1086
|
-
const error = r.errorMessage || r.stderr.trim();
|
|
1087
|
-
const summaryText = output || error;
|
|
1088
|
-
const preview = summaryText.slice(0, 160) + (summaryText.length > 160 ? "..." : "");
|
|
1089
|
-
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
|
|
1090
|
-
});
|
|
1091
|
-
const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
|
|
1092
|
-
const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
|
|
1093
|
-
return {
|
|
1094
|
-
content: [
|
|
1095
|
-
{
|
|
1096
|
-
type: "text",
|
|
1097
|
-
text: aggregatorResult
|
|
1098
|
-
? aggregatorOutput || aggregatorError || `(aggregator ${aggregatorResult.agent} produced no output)`
|
|
1099
|
-
: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
1100
|
-
},
|
|
1101
|
-
],
|
|
1102
|
-
details: makeDetails("parallel")(results, aggregatorResult),
|
|
1103
|
-
isError: aggregatorResult
|
|
1104
|
-
? aggregatorResult.exitCode !== 0 ||
|
|
1105
|
-
aggregatorResult.stopReason === "error" ||
|
|
1106
|
-
aggregatorResult.stopReason === "aborted"
|
|
1107
|
-
: undefined,
|
|
1108
|
-
};
|
|
1109
|
-
} finally {
|
|
1110
|
-
status.clear();
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
if (params.agent && params.task) {
|
|
1115
|
-
const status = startSubagentStatus(ctx, toolCallId, singleStatus(params.agent));
|
|
1116
|
-
|
|
1117
|
-
try {
|
|
1118
|
-
const result = await runSingleAgent(
|
|
1119
|
-
ctx.cwd,
|
|
1120
|
-
agents,
|
|
1121
|
-
params.agent,
|
|
1122
|
-
params.task,
|
|
1123
|
-
params.cwd,
|
|
1124
|
-
undefined,
|
|
1125
|
-
signal,
|
|
1126
|
-
resolveThinkingLevel(params.agent, params.thinkingLevel),
|
|
1127
|
-
resolveTimeoutMs(params.agent, params.timeoutMs),
|
|
1128
|
-
onUpdate,
|
|
1129
|
-
makeDetails("single"),
|
|
1130
|
-
);
|
|
1131
|
-
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
1132
|
-
if (isError) {
|
|
1133
|
-
const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
1134
|
-
return {
|
|
1135
|
-
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
1136
|
-
details: makeDetails("single")([result]),
|
|
1137
|
-
isError: true,
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
return {
|
|
1141
|
-
content: [{ type: "text", text: getResultFinalOutput(result) || "(no output)" }],
|
|
1142
|
-
details: makeDetails("single")([result]),
|
|
1143
|
-
};
|
|
1144
|
-
} finally {
|
|
1145
|
-
status.clear();
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
1150
|
-
return {
|
|
1151
|
-
content: [{ type: "text", text: `Invalid parameters. Available agents: ${available}` }],
|
|
1152
|
-
details: makeDetails("single")([]),
|
|
1153
|
-
};
|
|
53
|
+
return executeSubagent(toolCallId, params, signal, onUpdate, ctx);
|
|
1154
54
|
},
|
|
1155
55
|
|
|
1156
|
-
renderCall(args, theme
|
|
1157
|
-
|
|
1158
|
-
if (args.chain && args.chain.length > 0) {
|
|
1159
|
-
let text =
|
|
1160
|
-
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
1161
|
-
theme.fg("accent", `chain (${args.chain.length} steps)`) +
|
|
1162
|
-
theme.fg("muted", ` [${scope}]`);
|
|
1163
|
-
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
|
1164
|
-
const step = args.chain[i];
|
|
1165
|
-
// Clean up {previous} placeholder for display
|
|
1166
|
-
const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
|
|
1167
|
-
const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
|
|
1168
|
-
text +=
|
|
1169
|
-
"\n " +
|
|
1170
|
-
theme.fg("muted", `${i + 1}.`) +
|
|
1171
|
-
" " +
|
|
1172
|
-
theme.fg("accent", step.agent) +
|
|
1173
|
-
theme.fg("dim", ` ${preview}`);
|
|
1174
|
-
}
|
|
1175
|
-
if (args.chain.length > 3) text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
|
|
1176
|
-
return new Text(text, 0, 0);
|
|
1177
|
-
}
|
|
1178
|
-
if (args.tasks && args.tasks.length > 0) {
|
|
1179
|
-
let text =
|
|
1180
|
-
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
1181
|
-
theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
|
|
1182
|
-
theme.fg("muted", ` [${scope}]`);
|
|
1183
|
-
for (const t of args.tasks.slice(0, 3)) {
|
|
1184
|
-
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
|
|
1185
|
-
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
|
|
1186
|
-
}
|
|
1187
|
-
if (args.tasks.length > 3) text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
|
|
1188
|
-
if (args.aggregator) {
|
|
1189
|
-
const preview =
|
|
1190
|
-
args.aggregator.task.length > 40 ? `${args.aggregator.task.slice(0, 40)}...` : args.aggregator.task;
|
|
1191
|
-
text += `\n ${theme.fg("muted", "fan-in → ")}${theme.fg("accent", args.aggregator.agent)}${theme.fg(
|
|
1192
|
-
"dim",
|
|
1193
|
-
` ${preview}`,
|
|
1194
|
-
)}`;
|
|
1195
|
-
}
|
|
1196
|
-
return new Text(text, 0, 0);
|
|
1197
|
-
}
|
|
1198
|
-
const agentName = args.agent || "...";
|
|
1199
|
-
const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
|
|
1200
|
-
let text =
|
|
1201
|
-
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
1202
|
-
theme.fg("accent", agentName) +
|
|
1203
|
-
theme.fg("muted", ` [${scope}]`);
|
|
1204
|
-
text += `\n ${theme.fg("dim", preview)}`;
|
|
1205
|
-
return new Text(text, 0, 0);
|
|
56
|
+
renderCall(args, theme) {
|
|
57
|
+
return renderSubagentCall(args, theme);
|
|
1206
58
|
},
|
|
1207
59
|
|
|
1208
|
-
renderResult(result,
|
|
1209
|
-
|
|
1210
|
-
if (!details || details.results.length === 0) {
|
|
1211
|
-
const text = result.content[0];
|
|
1212
|
-
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
const mdTheme = getMarkdownTheme();
|
|
1216
|
-
|
|
1217
|
-
const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
|
|
1218
|
-
const toShow = limit ? items.slice(-limit) : items;
|
|
1219
|
-
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
|
1220
|
-
let text = "";
|
|
1221
|
-
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
|
1222
|
-
for (const item of toShow) {
|
|
1223
|
-
if (item.type === "text") {
|
|
1224
|
-
const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
|
|
1225
|
-
text += `${theme.fg("toolOutput", preview)}\n`;
|
|
1226
|
-
} else {
|
|
1227
|
-
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return text.trimEnd();
|
|
1231
|
-
};
|
|
1232
|
-
|
|
1233
|
-
if (details.mode === "single" && details.results.length === 1) {
|
|
1234
|
-
const r = details.results[0];
|
|
1235
|
-
const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
|
1236
|
-
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
1237
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1238
|
-
const finalOutput = getResultFinalOutput(r);
|
|
1239
|
-
|
|
1240
|
-
if (expanded) {
|
|
1241
|
-
const container = new Container();
|
|
1242
|
-
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
1243
|
-
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
1244
|
-
container.addChild(new Text(header, 0, 0));
|
|
1245
|
-
if (isError && r.errorMessage)
|
|
1246
|
-
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
1247
|
-
container.addChild(new Spacer(1));
|
|
1248
|
-
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
1249
|
-
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
1250
|
-
container.addChild(new Spacer(1));
|
|
1251
|
-
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
1252
|
-
if (displayItems.length === 0 && !finalOutput) {
|
|
1253
|
-
container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
1254
|
-
} else {
|
|
1255
|
-
for (const item of displayItems) {
|
|
1256
|
-
if (item.type === "toolCall")
|
|
1257
|
-
container.addChild(
|
|
1258
|
-
new Text(
|
|
1259
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
1260
|
-
0,
|
|
1261
|
-
0,
|
|
1262
|
-
),
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
1265
|
-
if (finalOutput) {
|
|
1266
|
-
container.addChild(new Spacer(1));
|
|
1267
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
const usageStr = formatUsageStats(r.usage, r.model, r.thinkingLevel);
|
|
1271
|
-
if (usageStr) {
|
|
1272
|
-
container.addChild(new Spacer(1));
|
|
1273
|
-
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
1274
|
-
}
|
|
1275
|
-
return container;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
1279
|
-
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
1280
|
-
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
1281
|
-
else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1282
|
-
else {
|
|
1283
|
-
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
|
|
1284
|
-
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1285
|
-
}
|
|
1286
|
-
const usageStr = formatUsageStats(r.usage, r.model, r.thinkingLevel);
|
|
1287
|
-
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
1288
|
-
return new Text(text, 0, 0);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
const aggregateUsage = (results: SingleResult[]) => {
|
|
1292
|
-
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
1293
|
-
for (const r of results) {
|
|
1294
|
-
total.input += r.usage.input;
|
|
1295
|
-
total.output += r.usage.output;
|
|
1296
|
-
total.cacheRead += r.usage.cacheRead;
|
|
1297
|
-
total.cacheWrite += r.usage.cacheWrite;
|
|
1298
|
-
total.cost += r.usage.cost;
|
|
1299
|
-
total.turns += r.usage.turns;
|
|
1300
|
-
}
|
|
1301
|
-
return total;
|
|
1302
|
-
};
|
|
1303
|
-
|
|
1304
|
-
if (details.mode === "chain") {
|
|
1305
|
-
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
1306
|
-
const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1307
|
-
|
|
1308
|
-
if (expanded) {
|
|
1309
|
-
const container = new Container();
|
|
1310
|
-
container.addChild(
|
|
1311
|
-
new Text(
|
|
1312
|
-
icon +
|
|
1313
|
-
" " +
|
|
1314
|
-
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
1315
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`),
|
|
1316
|
-
0,
|
|
1317
|
-
0,
|
|
1318
|
-
),
|
|
1319
|
-
);
|
|
1320
|
-
|
|
1321
|
-
for (const r of details.results) {
|
|
1322
|
-
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1323
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1324
|
-
const finalOutput = getResultFinalOutput(r);
|
|
1325
|
-
|
|
1326
|
-
container.addChild(new Spacer(1));
|
|
1327
|
-
container.addChild(
|
|
1328
|
-
new Text(
|
|
1329
|
-
`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
|
|
1330
|
-
0,
|
|
1331
|
-
0,
|
|
1332
|
-
),
|
|
1333
|
-
);
|
|
1334
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
1335
|
-
|
|
1336
|
-
// Show tool calls
|
|
1337
|
-
for (const item of displayItems) {
|
|
1338
|
-
if (item.type === "toolCall") {
|
|
1339
|
-
container.addChild(
|
|
1340
|
-
new Text(
|
|
1341
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
1342
|
-
0,
|
|
1343
|
-
0,
|
|
1344
|
-
),
|
|
1345
|
-
);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// Show final output as markdown
|
|
1350
|
-
if (finalOutput) {
|
|
1351
|
-
container.addChild(new Spacer(1));
|
|
1352
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
const stepUsage = formatUsageStats(r.usage, r.model, r.thinkingLevel);
|
|
1356
|
-
if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1360
|
-
if (usageStr) {
|
|
1361
|
-
container.addChild(new Spacer(1));
|
|
1362
|
-
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
1363
|
-
}
|
|
1364
|
-
return container;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
// Collapsed view
|
|
1368
|
-
let text =
|
|
1369
|
-
icon +
|
|
1370
|
-
" " +
|
|
1371
|
-
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
1372
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`);
|
|
1373
|
-
for (const r of details.results) {
|
|
1374
|
-
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1375
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1376
|
-
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
1377
|
-
if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1378
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1379
|
-
}
|
|
1380
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1381
|
-
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
1382
|
-
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1383
|
-
return new Text(text, 0, 0);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
if (details.mode === "parallel") {
|
|
1387
|
-
const running = details.results.filter((r) => r.exitCode === -1).length;
|
|
1388
|
-
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
1389
|
-
const failCount = details.results.filter((r) => r.exitCode > 0).length;
|
|
1390
|
-
const aggregator = details.aggregator;
|
|
1391
|
-
const aggregatorRunning = aggregator?.exitCode === -1;
|
|
1392
|
-
const aggregatorFailed = aggregator ? aggregator.exitCode > 0 || aggregator.stopReason === "error" : false;
|
|
1393
|
-
const isRunning = running > 0 || aggregatorRunning;
|
|
1394
|
-
const icon = isRunning
|
|
1395
|
-
? theme.fg("warning", "⏳")
|
|
1396
|
-
: failCount > 0 || aggregatorFailed
|
|
1397
|
-
? theme.fg("warning", "◐")
|
|
1398
|
-
: theme.fg("success", "✓");
|
|
1399
|
-
const status = isRunning
|
|
1400
|
-
? aggregatorRunning
|
|
1401
|
-
? `${successCount + failCount}/${details.results.length} done, fan-in running`
|
|
1402
|
-
: `${successCount + failCount}/${details.results.length} done, ${running} running`
|
|
1403
|
-
: aggregator
|
|
1404
|
-
? `${successCount}/${details.results.length} tasks + fan-in`
|
|
1405
|
-
: `${successCount}/${details.results.length} tasks`;
|
|
1406
|
-
|
|
1407
|
-
if (expanded && !isRunning) {
|
|
1408
|
-
const container = new Container();
|
|
1409
|
-
container.addChild(
|
|
1410
|
-
new Text(
|
|
1411
|
-
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
|
1412
|
-
0,
|
|
1413
|
-
0,
|
|
1414
|
-
),
|
|
1415
|
-
);
|
|
1416
|
-
|
|
1417
|
-
for (const r of details.results) {
|
|
1418
|
-
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1419
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1420
|
-
const finalOutput = getResultFinalOutput(r);
|
|
1421
|
-
|
|
1422
|
-
container.addChild(new Spacer(1));
|
|
1423
|
-
container.addChild(
|
|
1424
|
-
new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
|
|
1425
|
-
);
|
|
1426
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
1427
|
-
|
|
1428
|
-
// Show tool calls
|
|
1429
|
-
for (const item of displayItems) {
|
|
1430
|
-
if (item.type === "toolCall") {
|
|
1431
|
-
container.addChild(
|
|
1432
|
-
new Text(
|
|
1433
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
1434
|
-
0,
|
|
1435
|
-
0,
|
|
1436
|
-
),
|
|
1437
|
-
);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// Show final output as markdown
|
|
1442
|
-
if (finalOutput) {
|
|
1443
|
-
container.addChild(new Spacer(1));
|
|
1444
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
const taskUsage = formatUsageStats(r.usage, r.model, r.thinkingLevel);
|
|
1448
|
-
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
if (aggregator) {
|
|
1452
|
-
const rIcon = aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1453
|
-
const displayItems = getDisplayItems(aggregator.messages);
|
|
1454
|
-
const finalOutput = getResultFinalOutput(aggregator);
|
|
1455
|
-
|
|
1456
|
-
container.addChild(new Spacer(1));
|
|
1457
|
-
container.addChild(
|
|
1458
|
-
new Text(
|
|
1459
|
-
`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`,
|
|
1460
|
-
0,
|
|
1461
|
-
0,
|
|
1462
|
-
),
|
|
1463
|
-
);
|
|
1464
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0));
|
|
1465
|
-
for (const item of displayItems) {
|
|
1466
|
-
if (item.type === "toolCall") {
|
|
1467
|
-
container.addChild(
|
|
1468
|
-
new Text(
|
|
1469
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
1470
|
-
0,
|
|
1471
|
-
0,
|
|
1472
|
-
),
|
|
1473
|
-
);
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
if (finalOutput) {
|
|
1477
|
-
container.addChild(new Spacer(1));
|
|
1478
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1479
|
-
}
|
|
1480
|
-
const fanInUsage = formatUsageStats(aggregator.usage, aggregator.model, aggregator.thinkingLevel);
|
|
1481
|
-
if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
1485
|
-
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
1486
|
-
if (usageStr) {
|
|
1487
|
-
container.addChild(new Spacer(1));
|
|
1488
|
-
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
1489
|
-
}
|
|
1490
|
-
return container;
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// Collapsed view (or still running)
|
|
1494
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
1495
|
-
for (const r of details.results) {
|
|
1496
|
-
const rIcon =
|
|
1497
|
-
r.exitCode === -1
|
|
1498
|
-
? theme.fg("warning", "⏳")
|
|
1499
|
-
: r.exitCode === 0
|
|
1500
|
-
? theme.fg("success", "✓")
|
|
1501
|
-
: theme.fg("error", "✗");
|
|
1502
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1503
|
-
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
1504
|
-
if (displayItems.length === 0)
|
|
1505
|
-
text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
1506
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1507
|
-
}
|
|
1508
|
-
if (aggregator) {
|
|
1509
|
-
const rIcon =
|
|
1510
|
-
aggregator.exitCode === -1
|
|
1511
|
-
? theme.fg("warning", "⏳")
|
|
1512
|
-
: aggregator.exitCode === 0
|
|
1513
|
-
? theme.fg("success", "✓")
|
|
1514
|
-
: theme.fg("error", "✗");
|
|
1515
|
-
const displayItems = getDisplayItems(aggregator.messages);
|
|
1516
|
-
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
|
|
1517
|
-
if (displayItems.length === 0)
|
|
1518
|
-
text += `\n${theme.fg("muted", aggregator.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
1519
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1520
|
-
}
|
|
1521
|
-
if (!isRunning) {
|
|
1522
|
-
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
1523
|
-
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
1524
|
-
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
1525
|
-
}
|
|
1526
|
-
if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1527
|
-
return new Text(text, 0, 0);
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
const text = result.content[0];
|
|
1531
|
-
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
60
|
+
renderResult(result, options, theme) {
|
|
61
|
+
return renderSubagentResult(result, options, theme);
|
|
1532
62
|
},
|
|
1533
63
|
});
|
|
1534
64
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
description: "Configure which tools each subagent can use",
|
|
1539
|
-
handler: async (_args, ctx) => {
|
|
1540
|
-
if (!ctx.hasUI) {
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
// Get current settings
|
|
1545
|
-
const currentSettings = readSubagentSettings() ?? {};
|
|
1546
|
-
const currentAgents = currentSettings.agents ?? {};
|
|
1547
|
-
|
|
1548
|
-
// Discover agents to show which ones are available
|
|
1549
|
-
const discovery = discoverAgents(ctx.cwd, "user", currentSettings);
|
|
1550
|
-
const agents = discovery.agents;
|
|
1551
|
-
|
|
1552
|
-
if (agents.length === 0) {
|
|
1553
|
-
ctx.ui.notify("No agents found", "warning");
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
// Loop: agent selection → tool toggle (Esc in tools returns here)
|
|
1558
|
-
while (true) {
|
|
1559
|
-
// Step 1: pick an agent to configure
|
|
1560
|
-
const agentItems: SelectItem[] = agents.map((a) => {
|
|
1561
|
-
const cfg = currentAgents[a.name];
|
|
1562
|
-
const hasToolsOverride = cfg ? hasOwn(cfg, "tools") : false;
|
|
1563
|
-
const toolSummary = hasToolsOverride
|
|
1564
|
-
? cfg?.tools && cfg.tools.length > 0
|
|
1565
|
-
? cfg.tools.join(", ")
|
|
1566
|
-
: "none"
|
|
1567
|
-
: "defaults";
|
|
1568
|
-
return {
|
|
1569
|
-
value: a.name,
|
|
1570
|
-
label: a.name,
|
|
1571
|
-
description: `${a.source} · tools: ${toolSummary}`,
|
|
1572
|
-
};
|
|
1573
|
-
});
|
|
1574
|
-
|
|
1575
|
-
const agentName = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
1576
|
-
const container = new Container();
|
|
1577
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1578
|
-
container.addChild(
|
|
1579
|
-
new Text(theme.fg("accent", theme.bold("Subagent Tool Configuration")), 1, 0),
|
|
1580
|
-
);
|
|
1581
|
-
container.addChild(new Spacer(1));
|
|
1582
|
-
container.addChild(
|
|
1583
|
-
new Text(theme.fg("muted", "Select an agent to configure its allowed tools:"), 1, 0),
|
|
1584
|
-
);
|
|
1585
|
-
container.addChild(new Spacer(1));
|
|
1586
|
-
const selectList = new SelectList(agentItems, Math.min(agentItems.length + 2, 15), {
|
|
1587
|
-
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
1588
|
-
selectedText: (t: string) => theme.fg("accent", t),
|
|
1589
|
-
description: (t: string) => theme.fg("muted", t),
|
|
1590
|
-
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
1591
|
-
noMatch: (t: string) => theme.fg("warning", t),
|
|
1592
|
-
});
|
|
1593
|
-
selectList.onSelect = (item) => done(item.value);
|
|
1594
|
-
selectList.onCancel = () => done(null);
|
|
1595
|
-
container.addChild(selectList);
|
|
1596
|
-
container.addChild(
|
|
1597
|
-
new Text(theme.fg("dim", "↑↓ navigate · enter select · esc cancel"), 1, 0),
|
|
1598
|
-
);
|
|
1599
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1600
|
-
return {
|
|
1601
|
-
render: (w: number) => container.render(w),
|
|
1602
|
-
invalidate: () => container.invalidate(),
|
|
1603
|
-
handleInput: (data: string) => {
|
|
1604
|
-
selectList.handleInput(data);
|
|
1605
|
-
tui.requestRender();
|
|
1606
|
-
},
|
|
1607
|
-
};
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
if (!agentName) return;
|
|
1611
|
-
|
|
1612
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
1613
|
-
if (!agent) return;
|
|
1614
|
-
|
|
1615
|
-
// Step 2: toggle tools for the selected agent
|
|
1616
|
-
// Discover without overrides to get original built-in/frontmatter defaults.
|
|
1617
|
-
// The main discovery above applies saved overrides, so agent.tools is already
|
|
1618
|
-
// overridden — using it for the reset-to-default comparison would match the
|
|
1619
|
-
// override against itself and silently delete it on a no-op save.
|
|
1620
|
-
const defaultDiscovery = discoverAgents(ctx.cwd, "user");
|
|
1621
|
-
const defaultTools = defaultDiscovery.agents.find((a) => a.name === agentName)?.tools;
|
|
1622
|
-
const currentAgentSettings = currentAgents[agentName];
|
|
1623
|
-
const configuredTools =
|
|
1624
|
-
currentAgentSettings && hasOwn(currentAgentSettings, "tools")
|
|
1625
|
-
? (currentAgentSettings.tools ?? [])
|
|
1626
|
-
: undefined;
|
|
1627
|
-
|
|
1628
|
-
// Get all available tools from pi's registry
|
|
1629
|
-
const allTools = uniqueToolNames(pi.getAllTools().map((t) => t.name)).sort((a, b) =>
|
|
1630
|
-
a.localeCompare(b),
|
|
1631
|
-
);
|
|
1632
|
-
const currentTools = uniqueToolNames(configuredTools ?? defaultTools ?? allTools);
|
|
1633
|
-
// Sort: currently selected tools first, then rest alphabetically. Preserve
|
|
1634
|
-
// unavailable configured tools so saving does not silently drop them.
|
|
1635
|
-
const currentSet = new Set(currentTools);
|
|
1636
|
-
const selectedFirst = [...currentTools, ...allTools.filter((t) => !currentSet.has(t))];
|
|
1637
|
-
|
|
1638
|
-
const selectedTools = await ctx.ui.custom<string[] | null>((tui, theme, _kb, done) => {
|
|
1639
|
-
const toggleList = new ToolToggleList(selectedFirst, currentSet);
|
|
1640
|
-
|
|
1641
|
-
const container = new Container();
|
|
1642
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1643
|
-
container.addChild(
|
|
1644
|
-
new Text(
|
|
1645
|
-
theme.fg("accent", theme.bold(`${agentName} tools`)) +
|
|
1646
|
-
theme.fg("muted", ` (${agent.source})`),
|
|
1647
|
-
1,
|
|
1648
|
-
0,
|
|
1649
|
-
),
|
|
1650
|
-
);
|
|
1651
|
-
container.addChild(new Spacer(1));
|
|
1652
|
-
container.addChild(
|
|
1653
|
-
new Text(theme.fg("muted", "Toggle tools with Enter/Space. S to save, Esc to cancel."), 1, 0),
|
|
1654
|
-
);
|
|
1655
|
-
container.addChild(new Spacer(1));
|
|
1656
|
-
|
|
1657
|
-
const listContainer = new Container();
|
|
1658
|
-
listContainer.addChild({
|
|
1659
|
-
render: (w: number) => toggleList.render(w),
|
|
1660
|
-
invalidate: () => toggleList.invalidate(),
|
|
1661
|
-
});
|
|
1662
|
-
container.addChild(listContainer);
|
|
1663
|
-
|
|
1664
|
-
container.addChild(new Spacer(1));
|
|
1665
|
-
container.addChild(
|
|
1666
|
-
new Text(theme.fg("dim", "↑↓ navigate · enter/space toggle · S save · esc cancel"), 1, 0),
|
|
1667
|
-
);
|
|
1668
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1669
|
-
|
|
1670
|
-
toggleList.onDone = (tools) => done(tools);
|
|
1671
|
-
toggleList.onCancel = () => done(null);
|
|
1672
|
-
|
|
1673
|
-
return {
|
|
1674
|
-
render: (w: number) => container.render(w),
|
|
1675
|
-
invalidate: () => container.invalidate(),
|
|
1676
|
-
handleInput: (data: string) => {
|
|
1677
|
-
toggleList.handleInput(data);
|
|
1678
|
-
tui.requestRender();
|
|
1679
|
-
},
|
|
1680
|
-
};
|
|
1681
|
-
});
|
|
1682
|
-
|
|
1683
|
-
// null means user cancelled — loop back to agent selection
|
|
1684
|
-
if (selectedTools === null) continue;
|
|
1685
|
-
|
|
1686
|
-
// Save to global settings
|
|
1687
|
-
const updatedAgents = { ...currentAgents };
|
|
1688
|
-
let restoredDefaults = false;
|
|
1689
|
-
|
|
1690
|
-
const isSameAsDefault =
|
|
1691
|
-
defaultTools === undefined
|
|
1692
|
-
? sameToolSet(selectedTools, allTools)
|
|
1693
|
-
: sameToolSet(selectedTools, defaultTools);
|
|
1694
|
-
|
|
1695
|
-
if (isSameAsDefault) {
|
|
1696
|
-
// Tools match defaults — remove only the tools override.
|
|
1697
|
-
// Keep other settings (model, timeoutMs) if present.
|
|
1698
|
-
const existing = updatedAgents[agentName];
|
|
1699
|
-
if (existing) {
|
|
1700
|
-
const nextConfig = { ...existing };
|
|
1701
|
-
delete nextConfig.tools;
|
|
1702
|
-
if (hasAnyAgentOverride(nextConfig)) updatedAgents[agentName] = nextConfig;
|
|
1703
|
-
else delete updatedAgents[agentName];
|
|
1704
|
-
}
|
|
1705
|
-
restoredDefaults = true;
|
|
1706
|
-
} else {
|
|
1707
|
-
updatedAgents[agentName] = {
|
|
1708
|
-
...updatedAgents[agentName],
|
|
1709
|
-
tools: selectedTools,
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
const newSettings: SubagentSettings = {
|
|
1714
|
-
...currentSettings,
|
|
1715
|
-
agents: Object.keys(updatedAgents).length > 0 ? updatedAgents : undefined,
|
|
1716
|
-
};
|
|
1717
|
-
|
|
1718
|
-
saveSubagentConfig(newSettings);
|
|
1719
|
-
const message = restoredDefaults
|
|
1720
|
-
? `${agentName}: defaults restored`
|
|
1721
|
-
: `${agentName}: ${selectedTools.length} tool${selectedTools.length !== 1 ? "s" : ""} configured`;
|
|
1722
|
-
ctx.ui.notify(message, "info");
|
|
1723
|
-
// Saved — exit the loop
|
|
1724
|
-
break;
|
|
1725
|
-
}
|
|
1726
|
-
},
|
|
65
|
+
pi.on("tool_result", (event) => {
|
|
66
|
+
if (event.toolName !== "subagent") return;
|
|
67
|
+
if ((event.details as (SubagentDetails & { isError?: boolean }) | undefined)?.isError) return { isError: true };
|
|
1727
68
|
});
|
|
1728
|
-
|
|
69
|
+
|
|
70
|
+
registerSubagentConfigCommand(pi);
|
|
71
|
+
registerStatefulSubagents(pi);
|
|
72
|
+
}
|
|
73
|
+
export { formatTokens, formatUsageStats } from "./render.js";
|
|
74
|
+
export { buildPiArgs } from "./runner.js";
|
|
75
|
+
export {
|
|
76
|
+
normalizeAgentSettings,
|
|
77
|
+
normalizeSubagentSettings,
|
|
78
|
+
resolveSubagentThinkingLevel,
|
|
79
|
+
sameToolSet,
|
|
80
|
+
uniqueToolNames,
|
|
81
|
+
} from "./settings.js";
|
|
82
|
+
export { parsePositiveInteger } from "./execution.js";
|