@rahularya01/pi-essentials 0.1.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +324 -0
  3. package/examples/mcp.json +30 -0
  4. package/examples/pi-essentials.json +32 -0
  5. package/examples/pi-settings.json +5 -0
  6. package/package.json +88 -0
  7. package/skills/pi-essentials/SKILL.md +50 -0
  8. package/src/config.ts +351 -0
  9. package/src/errors.ts +96 -0
  10. package/src/index.ts +43 -0
  11. package/src/mcp/commands.ts +390 -0
  12. package/src/mcp/config.ts +157 -0
  13. package/src/mcp/credential-store.ts +153 -0
  14. package/src/mcp/index.ts +67 -0
  15. package/src/mcp/manager.ts +941 -0
  16. package/src/mcp/oauth.ts +262 -0
  17. package/src/mcp/proxy-tool.ts +213 -0
  18. package/src/mcp/render.ts +164 -0
  19. package/src/mcp/types.ts +63 -0
  20. package/src/paths.ts +48 -0
  21. package/src/questions/ask.ts +134 -0
  22. package/src/questions/index.ts +72 -0
  23. package/src/questions/render.ts +69 -0
  24. package/src/questions/validate.ts +85 -0
  25. package/src/security/env.ts +132 -0
  26. package/src/security/limits.ts +20 -0
  27. package/src/security/ssrf.ts +237 -0
  28. package/src/subagents/activity.ts +132 -0
  29. package/src/subagents/builtins/oracle.md +11 -0
  30. package/src/subagents/builtins/reviewer.md +11 -0
  31. package/src/subagents/builtins/scout.md +12 -0
  32. package/src/subagents/builtins/worker.md +11 -0
  33. package/src/subagents/discover.ts +54 -0
  34. package/src/subagents/herdr.ts +150 -0
  35. package/src/subagents/index.ts +642 -0
  36. package/src/subagents/inspector-tail.d.mts +1 -0
  37. package/src/subagents/inspector-tail.mjs +140 -0
  38. package/src/subagents/render.ts +464 -0
  39. package/src/subagents/runner.ts +468 -0
  40. package/src/subagents/schema.ts +107 -0
  41. package/src/subagents/types.ts +131 -0
  42. package/src/subagents/worktree.ts +131 -0
  43. package/src/todos/index.ts +170 -0
  44. package/src/todos/render.ts +198 -0
  45. package/src/todos/state.ts +310 -0
  46. package/src/ui/render.ts +215 -0
  47. package/src/web/activity.ts +91 -0
  48. package/src/web/cache.ts +153 -0
  49. package/src/web/extract.ts +75 -0
  50. package/src/web/fetch.ts +167 -0
  51. package/src/web/html-to-markdown.ts +284 -0
  52. package/src/web/http.ts +238 -0
  53. package/src/web/index.ts +214 -0
  54. package/src/web/providers/brave.ts +27 -0
  55. package/src/web/providers/duckduckgo.ts +60 -0
  56. package/src/web/providers/exa.ts +29 -0
  57. package/src/web/providers/jina.ts +25 -0
  58. package/src/web/providers/searxng.ts +29 -0
  59. package/src/web/providers/tavily.ts +31 -0
  60. package/src/web/providers/types.ts +75 -0
  61. package/src/web/render.ts +130 -0
  62. package/src/web/search.ts +108 -0
@@ -0,0 +1,642 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { StringEnum } from "@earendil-works/pi-ai";
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { isKeyRelease, matchesKey, type TUI } from "@earendil-works/pi-tui";
5
+ import { Type } from "typebox";
6
+ import type { ResolvedConfig } from "../config.ts";
7
+ import { toolFailure, toolText } from "../errors.ts";
8
+ import { discoverAgents, formatAgentList } from "./discover.ts";
9
+ import { buildPaneCommand, openInspectorPane } from "./herdr.ts";
10
+ import { FleetPanel, InspectorPanel, renderSubagentCall, renderSubagentResult } from "./render.ts";
11
+ import { emptyUsage, mapLimit, runSubagent, type ActiveRun, type RunUsage, type SubagentRunResult } from "./runner.ts";
12
+ import { effectiveOutputSchema, validateOutputSchema, type JsonSchema } from "./schema.ts";
13
+ import { validateSubagentParams, type AgentDefinition, type AgentScope } from "./types.ts";
14
+ import {
15
+ createTemporaryWorktree,
16
+ finishTemporaryWorktree,
17
+ remapWorktreeCwd,
18
+ type Isolation,
19
+ type TemporaryWorktree,
20
+ } from "./worktree.ts";
21
+
22
+ const IsolationSchema = StringEnum(["none", "worktree"] as const);
23
+ const TaskItem = Type.Object({
24
+ agent: Type.String({ description: "Agent name" }),
25
+ task: Type.String({ description: "Task to delegate" }),
26
+ cwd: Type.Optional(Type.String({ description: "Working directory" })),
27
+ outputSchema: Type.Optional(Type.Any({ description: "TypeBox/JSON object schema for a required structured result" })),
28
+ isolation: Type.Optional(IsolationSchema),
29
+ });
30
+
31
+ interface Job {
32
+ agent: string;
33
+ task: string;
34
+ cwd?: string;
35
+ outputSchema?: unknown;
36
+ isolation?: Isolation;
37
+ }
38
+
39
+ export function aggregateRunUsage(results: SubagentRunResult[]) {
40
+ const total = results.reduce<RunUsage>((usage, result) => {
41
+ usage.input += result.usage.input;
42
+ usage.output += result.usage.output;
43
+ usage.cacheRead += result.usage.cacheRead;
44
+ usage.cacheWrite += result.usage.cacheWrite;
45
+ usage.cost += result.usage.cost;
46
+ usage.costInput = (usage.costInput ?? 0) + (result.usage.costInput ?? 0);
47
+ usage.costOutput = (usage.costOutput ?? 0) + (result.usage.costOutput ?? 0);
48
+ usage.costCacheRead = (usage.costCacheRead ?? 0) + (result.usage.costCacheRead ?? 0);
49
+ usage.costCacheWrite = (usage.costCacheWrite ?? 0) + (result.usage.costCacheWrite ?? 0);
50
+ usage.turns += result.usage.turns;
51
+ return usage;
52
+ }, emptyUsage());
53
+ return {
54
+ input: total.input,
55
+ output: total.output,
56
+ cacheRead: total.cacheRead,
57
+ cacheWrite: total.cacheWrite,
58
+ totalTokens: total.input + total.output + total.cacheRead + total.cacheWrite,
59
+ cost: {
60
+ input: total.costInput ?? 0,
61
+ output: total.costOutput ?? 0,
62
+ cacheRead: total.costCacheRead ?? 0,
63
+ cacheWrite: total.costCacheWrite ?? 0,
64
+ total: total.cost,
65
+ },
66
+ };
67
+ }
68
+
69
+ /** Spinner/elapsed refresh rate for the FleetView widget. */
70
+ const WIDGET_TICK_MS = 120;
71
+
72
+ /** Standalone, dependency-free viewer run inside a Herdr pane; see herdr.ts. */
73
+ const INSPECTOR_TAIL_SCRIPT = fileURLToPath(new URL("./inspector-tail.mjs", import.meta.url));
74
+
75
+ export function registerSubagents(pi: ExtensionAPI, config: ResolvedConfig): void {
76
+ const active = new Map<string, ActiveRun>();
77
+ let spawnedThisSession = 0;
78
+ let ticker: NodeJS.Timeout | undefined;
79
+ let lastCtx: ExtensionContext | undefined;
80
+ let collapsed = false;
81
+ let roster = false;
82
+ let selectedId: string | undefined;
83
+ let mounted = false;
84
+ let inspectOpen = false;
85
+ let tuiRef: TUI | undefined;
86
+ let inputUnsub: (() => void) | undefined;
87
+ const panel = new FleetPanel();
88
+
89
+ const renderFleet = (ctx: ExtensionContext) => {
90
+ lastCtx = ctx;
91
+ if (!ctx.hasUI) return;
92
+ if (selectedId && !active.has(selectedId)) selectedId = undefined;
93
+ if (active.size === 0 || inspectOpen) {
94
+ if (active.size === 0) {
95
+ selectedId = undefined;
96
+ roster = false;
97
+ }
98
+ mounted = false;
99
+ ctx.ui.setWidget("pi-essentials-subagents", undefined);
100
+ return;
101
+ }
102
+ if (!selectedId) selectedId = [...active.keys()][0];
103
+ const runs = [...active.values()];
104
+ panel.setState(runs, {
105
+ collapsed,
106
+ spawned: spawnedThisSession,
107
+ budget: config.subagents.spawnBudget,
108
+ selectedId,
109
+ roster,
110
+ });
111
+ if (!mounted) {
112
+ mounted = true;
113
+ ctx.ui.setWidget(
114
+ "pi-essentials-subagents",
115
+ (tui, theme) => {
116
+ tuiRef = tui;
117
+ panel.setTheme(theme);
118
+ return panel;
119
+ },
120
+ { placement: "belowEditor" },
121
+ );
122
+ } else {
123
+ tuiRef?.requestRender();
124
+ }
125
+ };
126
+
127
+ // Elapsed times would otherwise only advance when a child emits progress.
128
+ const syncTicker = () => {
129
+ if (active.size > 0 && !ticker) {
130
+ ticker = setInterval(() => {
131
+ if (lastCtx) renderFleet(lastCtx);
132
+ }, WIDGET_TICK_MS);
133
+ ticker.unref?.();
134
+ return;
135
+ }
136
+ if (active.size === 0 && ticker) {
137
+ clearInterval(ticker);
138
+ ticker = undefined;
139
+ }
140
+ };
141
+
142
+ const stopAll = () => {
143
+ for (const run of active.values()) run.proc?.kill("SIGTERM");
144
+ active.clear();
145
+ if (ticker) clearInterval(ticker);
146
+ ticker = undefined;
147
+ };
148
+
149
+ pi.on("session_start", async (_event, ctx) => {
150
+ spawnedThisSession = 0;
151
+ stopAll();
152
+ lastCtx = ctx;
153
+ inputUnsub?.();
154
+ if (ctx.hasUI && typeof ctx.ui.onTerminalInput === "function") {
155
+ inputUnsub = ctx.ui.onTerminalInput((data) => handleFleetKeys(ctx, data));
156
+ }
157
+ renderFleet(ctx);
158
+ });
159
+
160
+ pi.on("session_shutdown", async (_event, ctx) => {
161
+ stopAll();
162
+ selectedId = undefined;
163
+ roster = false;
164
+ inspectOpen = false;
165
+ mounted = false;
166
+ inputUnsub?.();
167
+ inputUnsub = undefined;
168
+ if (ctx.hasUI) ctx.ui.setWidget("pi-essentials-subagents", undefined);
169
+ });
170
+
171
+ pi.registerShortcut("ctrl+shift+a", {
172
+ description: "Collapse or expand the pi-essentials subagent fleet panel",
173
+ handler: async (ctx) => {
174
+ collapsed = !collapsed;
175
+ renderFleet(ctx);
176
+ },
177
+ });
178
+
179
+ const resolveRun = (token: string | undefined): ActiveRun | undefined => {
180
+ if (!token) return undefined;
181
+ return active.get(token) ?? [...active.values()].find((run) => run.agent === token);
182
+ };
183
+
184
+ /**
185
+ * Open a running child in a real, separate Herdr pane (read-only) instead of the
186
+ * in-app overlay. Best-effort: Herdr is an optional third-party tool the user
187
+ * installs themselves, so any failure just falls back to notifying, not throwing.
188
+ */
189
+ const openHerdrInspector = async (ctx: ExtensionContext, run: ActiveRun | undefined): Promise<void> => {
190
+ if (!config.subagents.herdr) {
191
+ ctx.ui.notify("Herdr inspector is disabled (subagents.herdr = false).", "warning");
192
+ return;
193
+ }
194
+ if (!run) {
195
+ ctx.ui.notify("Pick a running subagent to open in Herdr.", "warning");
196
+ return;
197
+ }
198
+ if (!run.logFile) {
199
+ ctx.ui.notify(`No live log available yet for ${run.agent}.`, "warning");
200
+ return;
201
+ }
202
+ const command = buildPaneCommand(process.execPath, [INSPECTOR_TAIL_SCRIPT, "--log", run.logFile]);
203
+ const opened = await openInspectorPane({ cwd: ctx.cwd, command });
204
+ ctx.ui.notify(
205
+ opened.ok ? `Opened ${run.agent} in a new Herdr pane (read-only).` : (opened.message ?? "Could not open a Herdr pane."),
206
+ opened.ok ? "info" : "warning",
207
+ );
208
+ };
209
+
210
+ const watchRun = async (ctx: ExtensionContext, token?: string): Promise<void> => {
211
+ if (active.size === 0) {
212
+ ctx.ui.notify("No running subagents.", "warning");
213
+ return;
214
+ }
215
+ let run = resolveRun(token);
216
+ if (!run && selectedId) run = active.get(selectedId);
217
+ if (!run && active.size === 1) run = [...active.values()][0];
218
+ if (!run && ctx.hasUI) {
219
+ const choice = await ctx.ui.select(
220
+ "Watch subagent",
221
+ [...active.values()].map((item) => `${item.id} ${item.agent} ${truncate(item.activity || item.task, 48)}`),
222
+ );
223
+ run = resolveRun(choice?.split(/\s+/)[0]);
224
+ }
225
+ if (!run) {
226
+ ctx.ui.notify(token ? `No running subagent "${token}".` : "Pick a running subagent to watch.", "warning");
227
+ return;
228
+ }
229
+ selectedId = run.id;
230
+ if (!ctx.hasUI) {
231
+ renderFleet(ctx);
232
+ return;
233
+ }
234
+ if (inspectOpen) {
235
+ renderFleet(ctx);
236
+ tuiRef?.requestRender();
237
+ return;
238
+ }
239
+ inspectOpen = true;
240
+ renderFleet(ctx);
241
+ await ctx.ui.custom<undefined>(
242
+ (tui, theme, _kb, done) => {
243
+ tuiRef = tui;
244
+ return new InspectorPanel(
245
+ () => [...active.values()],
246
+ () => selectedId,
247
+ (id) => {
248
+ selectedId = id;
249
+ renderFleet(ctx);
250
+ },
251
+ theme,
252
+ () => {
253
+ inspectOpen = false;
254
+ selectedId = undefined;
255
+ done(undefined);
256
+ renderFleet(ctx);
257
+ },
258
+ tui,
259
+ (selected) => void openHerdrInspector(ctx, selected),
260
+ );
261
+ },
262
+ {
263
+ overlay: true,
264
+ overlayOptions: { anchor: "center", width: "95%", minWidth: 60, maxHeight: "85%", margin: 1 },
265
+ },
266
+ );
267
+ inspectOpen = false;
268
+ roster = false;
269
+ renderFleet(ctx);
270
+ };
271
+
272
+ function handleFleetKeys(ctx: ExtensionContext, data: string): { consume?: boolean } | undefined {
273
+ if (inspectOpen || collapsed || active.size === 0 || isKeyRelease(data) || !ctx.hasUI) return undefined;
274
+ const editorEmpty = (ctx.ui.getEditorText?.() ?? "") === "";
275
+ if (!roster) {
276
+ if (editorEmpty && (matchesKey(data, "down") || matchesKey(data, "left"))) {
277
+ roster = true;
278
+ selectedId ??= [...active.keys()][0];
279
+ renderFleet(ctx);
280
+ return { consume: true };
281
+ }
282
+ return undefined;
283
+ }
284
+ if (!editorEmpty) {
285
+ roster = false;
286
+ renderFleet(ctx);
287
+ return undefined;
288
+ }
289
+ const ids = [...active.keys()];
290
+ const index = Math.max(0, ids.indexOf(selectedId ?? ""));
291
+ if (matchesKey(data, "down") || matchesKey(data, "j")) {
292
+ selectedId = ids[Math.min(ids.length - 1, index + 1)];
293
+ renderFleet(ctx);
294
+ return { consume: true };
295
+ }
296
+ if (matchesKey(data, "up") || matchesKey(data, "k")) {
297
+ if (index <= 0) {
298
+ roster = false;
299
+ renderFleet(ctx);
300
+ return { consume: true };
301
+ }
302
+ selectedId = ids[index - 1];
303
+ renderFleet(ctx);
304
+ return { consume: true };
305
+ }
306
+ if (matchesKey(data, "escape")) {
307
+ roster = false;
308
+ renderFleet(ctx);
309
+ return { consume: true };
310
+ }
311
+ if (matchesKey(data, "return")) {
312
+ void watchRun(ctx, selectedId);
313
+ return { consume: true };
314
+ }
315
+ if (matchesKey(data, "h")) {
316
+ void openHerdrInspector(ctx, resolveRun(selectedId));
317
+ return { consume: true };
318
+ }
319
+ roster = false;
320
+ renderFleet(ctx);
321
+ return undefined;
322
+ }
323
+
324
+ panel.onSelect = (id) => {
325
+ selectedId = id;
326
+ if (lastCtx) {
327
+ renderFleet(lastCtx);
328
+ if (!inspectOpen) void watchRun(lastCtx, id);
329
+ }
330
+ };
331
+
332
+ pi.registerCommand("subagents", {
333
+ description: "List agents and running subagent jobs. Usage: /subagents [watch <id>|pane <id>|cancel <id>|cancel all]",
334
+ handler: async (args, ctx) => {
335
+ const parts = args.trim().split(/\s+/).filter(Boolean);
336
+ if (parts[0] === "watch") {
337
+ await watchRun(ctx, parts[1]);
338
+ return;
339
+ }
340
+ if (parts[0] === "pane") {
341
+ await openHerdrInspector(ctx, resolveRun(parts[1]) ?? (selectedId ? active.get(selectedId) : undefined) ?? (active.size === 1 ? [...active.values()][0] : undefined));
342
+ return;
343
+ }
344
+ if (parts[0] === "cancel") {
345
+ const target = parts[1];
346
+ if (!target) {
347
+ ctx.ui.notify("Usage: /subagents cancel <id|all>", "warning");
348
+ return;
349
+ }
350
+ if (target === "all") {
351
+ const count = active.size;
352
+ stopAll();
353
+ renderFleet(ctx);
354
+ ctx.ui.notify(count > 0 ? `Cancelled ${count} subagent(s)` : "No running subagents.", "info");
355
+ return;
356
+ }
357
+ const run = resolveRun(target);
358
+ if (!run?.proc) {
359
+ ctx.ui.notify(`No running subagent "${target}".`, "warning");
360
+ return;
361
+ }
362
+ run.proc.kill("SIGTERM");
363
+ ctx.ui.notify(`Cancelled ${run.id}`, "info");
364
+ return;
365
+ }
366
+ if (parts.length > 0) {
367
+ ctx.ui.notify("Usage: /subagents [watch <id>|pane <id>|cancel <id|all>]", "warning");
368
+ return;
369
+ }
370
+ if (active.size > 0 && ctx.hasUI) {
371
+ await watchRun(ctx);
372
+ return;
373
+ }
374
+ const agents = discoverAgents(ctx.cwd, "both");
375
+ const running =
376
+ active.size === 0
377
+ ? "No running subagents."
378
+ : [...active.values()]
379
+ .map((r) => `${r.id} ${r.agent} ${Math.round((Date.now() - r.startedAt) / 1000)}s — ${truncate(r.activity || r.task, 60)}`)
380
+ .join("\n");
381
+ const budget = `Spawned ${spawnedThisSession}/${config.subagents.spawnBudget} this session.`;
382
+ ctx.ui.notify(`${running}\n${budget}\n\nAgents:\n${formatAgentList(agents)}`, "info");
383
+ },
384
+ });
385
+
386
+ pi.registerTool({
387
+ name: "subagent",
388
+ label: "Subagent",
389
+ description:
390
+ "Delegate an isolated task to a subagent (scout, reviewer, worker, oracle, or a custom agent). Modes: single {agent,task}, parallel {tasks:[...]}, chain {chain:[...]}. Optional outputSchema requires a structured result; isolation can use a temporary git worktree. Returns only the child's final answer.",
391
+ promptSnippet: "Delegate isolated work to scout, reviewer, worker, or oracle subagents",
392
+ promptGuidelines: [
393
+ "Use subagent for bounded work that should not pollute the parent context, especially recon, review, and second opinions.",
394
+ "Pass only the task the child needs. Do not paste the full parent transcript.",
395
+ "Prefer scout before planning, worker to implement, reviewer to check, and oracle when the decision is risky.",
396
+ ],
397
+ parameters: Type.Object({
398
+ agent: Type.Optional(Type.String({ description: "Agent name for single mode" })),
399
+ task: Type.Optional(Type.String({ description: "Task for single mode" })),
400
+ tasks: Type.Optional(Type.Array(TaskItem, { description: "Parallel tasks" })),
401
+ chain: Type.Optional(Type.Array(TaskItem, { description: "Sequential tasks; use {previous} in later prompts" })),
402
+ agentScope: Type.Optional(
403
+ StringEnum(["user", "project", "both"] as const, {
404
+ description: "Where to look for custom agents. Defaults to user (builtins plus ~/.pi/agent/agents).",
405
+ }),
406
+ ),
407
+ confirmProjectAgents: Type.Optional(Type.Boolean()),
408
+ cwd: Type.Optional(Type.String()),
409
+ outputSchema: Type.Optional(Type.Any({ description: "Default TypeBox/JSON object schema for structured results" })),
410
+ isolation: Type.Optional(IsolationSchema),
411
+ }),
412
+ async execute(_id, params, signal, onUpdate, ctx) {
413
+ const scope: AgentScope = params.agentScope ?? "user";
414
+ const agents = discoverAgents(ctx.cwd, scope);
415
+ const validated = validateSubagentParams(params);
416
+ if ("error" in validated) {
417
+ toolFailure(`${validated.error}\nAvailable agents:\n${formatAgentList(agents)}`, "SUBAGENT_BAD_ARGS");
418
+ }
419
+
420
+ const jobs: Job[] =
421
+ validated.mode === "single"
422
+ ? [{ agent: params.agent ?? "", task: params.task ?? "", cwd: params.cwd, outputSchema: params.outputSchema, isolation: params.isolation }]
423
+ : validated.mode === "parallel"
424
+ ? (params.tasks ?? [])
425
+ : (params.chain ?? []);
426
+
427
+ if (jobs.length === 0) toolFailure("No subagent jobs provided.", "SUBAGENT_BAD_ARGS");
428
+
429
+ let rootSchema: JsonSchema | undefined;
430
+ const jobSchemas: Array<JsonSchema | undefined> = [];
431
+ try {
432
+ rootSchema = params.outputSchema === undefined ? undefined : validateOutputSchema(params.outputSchema);
433
+ for (const job of jobs) jobSchemas.push(effectiveOutputSchema(job.outputSchema, rootSchema));
434
+ } catch (error) {
435
+ toolFailure((error as Error).message, "SUBAGENT_BAD_SCHEMA");
436
+ }
437
+
438
+ const blank = jobs.findIndex((job) => !job.agent?.trim() || !job.task?.trim());
439
+ if (blank >= 0) toolFailure(`Job ${blank + 1} is missing an agent name or task text.`, "SUBAGENT_BAD_ARGS");
440
+
441
+ // Resolve every agent up front so a typo does not consume the spawn budget.
442
+ const unknown = [...new Set(jobs.map((job) => job.agent))].filter((name) => !agents.some((a) => a.name === name));
443
+ if (unknown.length > 0) {
444
+ toolFailure(
445
+ `Unknown agent(s): ${unknown.join(", ")}.\nAvailable agents:\n${formatAgentList(agents)}`,
446
+ "SUBAGENT_UNKNOWN_AGENT",
447
+ );
448
+ }
449
+
450
+ if (validated.mode === "parallel" && jobs.length > config.subagents.maxParallel) {
451
+ toolFailure(
452
+ `Too many parallel tasks (${jobs.length}); the limit is ${config.subagents.maxParallel}.`,
453
+ "SUBAGENT_TOO_MANY",
454
+ );
455
+ }
456
+ const remaining = config.subagents.spawnBudget - spawnedThisSession;
457
+ if (jobs.length > remaining) {
458
+ toolFailure(
459
+ `Subagent spawn budget exhausted: ${spawnedThisSession}/${config.subagents.spawnBudget} used this session, ` +
460
+ `${remaining} left but ${jobs.length} requested. Raise subagents.spawnBudget to allow more.`,
461
+ "SUBAGENT_BUDGET",
462
+ );
463
+ }
464
+
465
+ const projectAgents = [...new Set(jobs.map((job) => job.agent))]
466
+ .map((name) => agents.find((a) => a.name === name))
467
+ .filter((a): a is AgentDefinition => a?.source === "project");
468
+
469
+ if (projectAgents.length > 0 && (params.confirmProjectAgents ?? true) && !ctx.isProjectTrusted()) {
470
+ if (!ctx.hasUI) {
471
+ toolFailure(
472
+ `Refusing to run project-local agent(s) ${projectAgents.map((a) => a.name).join(", ")} in an untrusted project without a way to confirm.`,
473
+ "SUBAGENT_UNTRUSTED",
474
+ );
475
+ }
476
+ const ok = await ctx.ui.confirm(
477
+ "Project subagents",
478
+ `Run project-local agent(s) ${projectAgents.map((a) => a.name).join(", ")}? These prompts come from this repository.`,
479
+ );
480
+ if (!ok) toolFailure("User declined to run project-local subagents.", "SUBAGENT_DECLINED");
481
+ }
482
+
483
+ const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
484
+ const thinkingLevel = ctx.thinkingLevel ? String(ctx.thinkingLevel) : undefined;
485
+
486
+ const isolationFor = (job: Job): Isolation => job.isolation ?? params.isolation ?? "none";
487
+ const sourceCwdFor = (job: Job): string => job.cwd ?? params.cwd ?? ctx.cwd;
488
+
489
+ const runOne = async (
490
+ job: Job,
491
+ previous = "",
492
+ sharedWorktree?: TemporaryWorktree,
493
+ jobIndex = jobs.indexOf(job),
494
+ ): Promise<SubagentRunResult> => {
495
+ const agent = agents.find((a) => a.name === job.agent);
496
+ if (!agent) {
497
+ return {
498
+ id: `missing-${job.agent}`,
499
+ agent: job.agent,
500
+ task: job.task,
501
+ exitCode: 1,
502
+ output: "",
503
+ outputKind: jobSchemas[jobIndex] ? "structured" : "text",
504
+ stderr: "",
505
+ usage: emptyUsage(),
506
+ error: `Unknown agent "${job.agent}".`,
507
+ running: false,
508
+ durationMs: 0,
509
+ };
510
+ }
511
+ const isolation = isolationFor(job);
512
+ const ownedWorktree = isolation === "worktree" && !sharedWorktree
513
+ ? createTemporaryWorktree(sourceCwdFor(job))
514
+ : undefined;
515
+ const worktree = sharedWorktree ?? ownedWorktree;
516
+ let result: SubagentRunResult;
517
+ try {
518
+ const runCwd = worktree && isolation === "worktree"
519
+ ? remapWorktreeCwd(sourceCwdFor(job), worktree)
520
+ : sourceCwdFor(job);
521
+ spawnedThisSession += 1;
522
+ result = await runSubagent({
523
+ agent,
524
+ task: job.task.replaceAll("{previous}", previous),
525
+ cwd: runCwd,
526
+ model,
527
+ thinkingLevel,
528
+ config: config.subagents,
529
+ signal,
530
+ outputSchema: jobSchemas[jobIndex],
531
+ onProgress: (partial) => {
532
+ const live = [...active.values()].map((item) => ({
533
+ id: item.id,
534
+ agent: item.agent,
535
+ task: item.task,
536
+ activity: item.activity,
537
+ events: item.events,
538
+ running: true,
539
+ exitCode: 0,
540
+ }));
541
+ onUpdate?.(
542
+ toolText(
543
+ live.map((item) => `${item.agent}: ${truncate(item.activity || partial, 200)}`).join("\n") || partial,
544
+ { mode: validated.mode, results: live },
545
+ ),
546
+ );
547
+ },
548
+ register: (run) => {
549
+ active.set(run.id, run);
550
+ syncTicker();
551
+ renderFleet(ctx);
552
+ },
553
+ unregister: (id) => {
554
+ active.delete(id);
555
+ syncTicker();
556
+ renderFleet(ctx);
557
+ },
558
+ });
559
+ } finally {
560
+ if (ownedWorktree) {
561
+ const metadata = finishTemporaryWorktree(ownedWorktree);
562
+ if (result!) result.worktree = metadata;
563
+ }
564
+ }
565
+ return result;
566
+ };
567
+
568
+ let results: SubagentRunResult[] = [];
569
+ try {
570
+ if (validated.mode === "chain") {
571
+ let previous = "";
572
+ const firstIsolated = jobs.find((job) => isolationFor(job) === "worktree");
573
+ const sharedWorktree = firstIsolated ? createTemporaryWorktree(sourceCwdFor(firstIsolated)) : undefined;
574
+ try {
575
+ for (let index = 0; index < jobs.length; index++) {
576
+ const job = jobs[index];
577
+ const result = await runOne(job, previous, isolationFor(job) === "worktree" ? sharedWorktree : undefined, index);
578
+ results.push(result);
579
+ if (result.exitCode !== 0) break;
580
+ previous = result.outputKind === "structured"
581
+ ? JSON.stringify(result.structuredOutput)
582
+ : result.output;
583
+ }
584
+ } finally {
585
+ if (sharedWorktree) {
586
+ const metadata = finishTemporaryWorktree(sharedWorktree);
587
+ for (let index = 0; index < results.length; index++) {
588
+ if (isolationFor(jobs[index]) === "worktree") results[index].worktree = metadata;
589
+ }
590
+ }
591
+ }
592
+ } else if (validated.mode === "parallel") {
593
+ results = await mapLimit(jobs, config.subagents.maxConcurrency, (job, index) => runOne(job, "", undefined, index));
594
+ } else {
595
+ results = [await runOne(jobs[0], "", undefined, 0)];
596
+ }
597
+ } finally {
598
+ renderFleet(ctx);
599
+ }
600
+
601
+ const skipped = jobs.length - results.length;
602
+ const text = [
603
+ ...results.map((result) => {
604
+ const status = result.exitCode === 0 ? "" : " (failed)";
605
+ const body = result.exitCode !== 0 && result.error ? result.error : result.output;
606
+ return `## ${result.agent}${status}\n${body}`;
607
+ }),
608
+ ...(skipped > 0 ? [`_${skipped} later chain step(s) skipped after a failure._`] : []),
609
+ ].join("\n\n");
610
+
611
+ if (results.length > 0 && results.every((result) => result.exitCode !== 0)) {
612
+ toolFailure(
613
+ `Every subagent failed.\n\n${text}`,
614
+ results.length === 1 ? "SUBAGENT_FAILED" : "SUBAGENT_ALL_FAILED",
615
+ );
616
+ }
617
+
618
+ return toolText(text || "(no subagent output)", {
619
+ mode: validated.mode,
620
+ skipped,
621
+ results: results.map((r) => ({
622
+ id: r.id,
623
+ agent: r.agent,
624
+ exitCode: r.exitCode,
625
+ durationMs: r.durationMs,
626
+ usage: r.usage,
627
+ model: r.model,
628
+ error: r.error,
629
+ outputKind: r.outputKind,
630
+ structuredOutput: r.structuredOutput,
631
+ worktree: r.worktree,
632
+ })),
633
+ }, aggregateRunUsage(results));
634
+ },
635
+ renderCall: renderSubagentCall,
636
+ renderResult: renderSubagentResult,
637
+ });
638
+ }
639
+
640
+ function truncate(text: string, max: number): string {
641
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
642
+ }
@@ -0,0 +1 @@
1
+ export function formatEvent(event: unknown, state: { textBuffer?: string }): string | undefined;