shariq-pi-extensions 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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/docs/ARCHITECTURE.md +54 -0
  5. package/docs/DEVELOPMENT.md +88 -0
  6. package/docs/EXTENSIONS.md +89 -0
  7. package/extensions/antigravity-provider/LICENSE +21 -0
  8. package/extensions/antigravity-provider/README.md +23 -0
  9. package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
  10. package/extensions/antigravity-provider/antigravity/index.ts +67 -0
  11. package/extensions/antigravity-provider/antigravity/models.ts +184 -0
  12. package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
  13. package/extensions/antigravity-provider/index.ts +3 -0
  14. package/extensions/ask-user/README.md +21 -0
  15. package/extensions/ask-user/index.ts +417 -0
  16. package/extensions/background-terminals/README.md +43 -0
  17. package/extensions/background-terminals/index.ts +400 -0
  18. package/extensions/background-terminals/src/manager.ts +480 -0
  19. package/extensions/background-terminals/src/output-buffer.ts +110 -0
  20. package/extensions/background-terminals/src/presentation.ts +74 -0
  21. package/extensions/background-terminals/src/types.ts +47 -0
  22. package/extensions/background-terminals/src/ui.ts +478 -0
  23. package/extensions/context-usage/LICENSE +21 -0
  24. package/extensions/context-usage/README.md +20 -0
  25. package/extensions/context-usage/index.ts +2006 -0
  26. package/extensions/context-usage/lib/ansi.ts +60 -0
  27. package/extensions/context-usage/lib/boundary.ts +27 -0
  28. package/extensions/context-usage/lib/chat.ts +190 -0
  29. package/extensions/context-usage/lib/config.ts +30 -0
  30. package/extensions/context-usage/lib/fmt.ts +59 -0
  31. package/extensions/context-usage/lib/style.ts +260 -0
  32. package/extensions/copy-all/README.md +15 -0
  33. package/extensions/copy-all/index.ts +72 -0
  34. package/extensions/factory-provider/API_KEYS.md +17 -0
  35. package/extensions/factory-provider/AUTH.md +15 -0
  36. package/extensions/factory-provider/README.md +90 -0
  37. package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
  38. package/extensions/factory-provider/UPDATING.md +97 -0
  39. package/extensions/factory-provider/factory/api-keys.ts +407 -0
  40. package/extensions/factory-provider/factory/auth.ts +132 -0
  41. package/extensions/factory-provider/factory/constants.ts +15 -0
  42. package/extensions/factory-provider/factory/dashboard.ts +363 -0
  43. package/extensions/factory-provider/factory/droid.ts +84 -0
  44. package/extensions/factory-provider/factory/limits.ts +342 -0
  45. package/extensions/factory-provider/factory/models.ts +283 -0
  46. package/extensions/factory-provider/factory/responses.ts +331 -0
  47. package/extensions/factory-provider/factory/warm-theme.ts +53 -0
  48. package/extensions/factory-provider/factory/websocket.ts +130 -0
  49. package/extensions/factory-provider/index.ts +204 -0
  50. package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
  51. package/extensions/firecrawl-web/README.md +31 -0
  52. package/extensions/firecrawl-web/auth.ts +92 -0
  53. package/extensions/firecrawl-web/client.ts +238 -0
  54. package/extensions/firecrawl-web/index.ts +96 -0
  55. package/extensions/firecrawl-web/output.ts +125 -0
  56. package/extensions/git-info/README.md +25 -0
  57. package/extensions/git-info/index.ts +162 -0
  58. package/extensions/git-info/src/git.ts +206 -0
  59. package/extensions/git-info/src/ui.ts +291 -0
  60. package/extensions/goal/README.md +44 -0
  61. package/extensions/goal/constants.ts +4 -0
  62. package/extensions/goal/extension.ts +1064 -0
  63. package/extensions/goal/index.ts +2 -0
  64. package/extensions/goal/types.ts +56 -0
  65. package/extensions/goal/ui.ts +334 -0
  66. package/extensions/orchestration/README.md +58 -0
  67. package/extensions/orchestration/dashboard.ts +192 -0
  68. package/extensions/orchestration/engine.ts +706 -0
  69. package/extensions/orchestration/index.ts +243 -0
  70. package/extensions/orchestration/prompts.ts +124 -0
  71. package/extensions/orchestration/protocol.ts +116 -0
  72. package/extensions/orchestration/settings-ui.ts +41 -0
  73. package/extensions/orchestration/settings.ts +59 -0
  74. package/extensions/orchestration/storage.ts +49 -0
  75. package/extensions/orchestration/types.ts +105 -0
  76. package/extensions/performance-status/README.md +5 -0
  77. package/extensions/performance-status/index.ts +171 -0
  78. package/extensions/pi-memory/README.md +53 -0
  79. package/extensions/pi-memory/index.ts +326 -0
  80. package/extensions/pi-memory/src/config.ts +75 -0
  81. package/extensions/pi-memory/src/database.ts +617 -0
  82. package/extensions/pi-memory/src/extraction.ts +178 -0
  83. package/extensions/pi-memory/src/project.ts +80 -0
  84. package/extensions/pi-memory/src/projection.ts +59 -0
  85. package/extensions/pi-memory/src/redaction.ts +60 -0
  86. package/extensions/pi-memory/src/retrieval.ts +30 -0
  87. package/extensions/pi-memory/src/service.ts +194 -0
  88. package/extensions/pi-memory/src/session.ts +146 -0
  89. package/extensions/pi-memory/src/types.ts +82 -0
  90. package/extensions/shared/README.md +18 -0
  91. package/extensions/shared/activity-status.ts +31 -0
  92. package/extensions/shared/child-session.ts +150 -0
  93. package/extensions/shared/context-utilization.ts +47 -0
  94. package/extensions/shared/dashboard-state.ts +99 -0
  95. package/extensions/shared/tool-call-timeout.ts +104 -0
  96. package/extensions/shared/tui-dashboard.ts +139 -0
  97. package/extensions/shell-shortcuts/README.md +17 -0
  98. package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
  99. package/extensions/shell-shortcuts/index.ts +6 -0
  100. package/extensions/subagents/README.md +89 -0
  101. package/extensions/subagents/index.ts +1386 -0
  102. package/extensions/subagents/src/backend.ts +69 -0
  103. package/extensions/subagents/src/backends/pi.ts +781 -0
  104. package/extensions/subagents/src/catalog.ts +85 -0
  105. package/extensions/subagents/src/config.ts +241 -0
  106. package/extensions/subagents/src/context.ts +57 -0
  107. package/extensions/subagents/src/coordinator.ts +64 -0
  108. package/extensions/subagents/src/domain.ts +294 -0
  109. package/extensions/subagents/src/format.ts +74 -0
  110. package/extensions/subagents/src/manager.ts +813 -0
  111. package/extensions/subagents/src/prompt.ts +94 -0
  112. package/extensions/subagents/src/result-delivery.ts +20 -0
  113. package/extensions/subagents/src/runtime.ts +48 -0
  114. package/extensions/subagents/src/ui/takeover.ts +695 -0
  115. package/extensions/subagents/src/ui/transcript.ts +182 -0
  116. package/extensions/subagents/src/worktree.ts +175 -0
  117. package/extensions/web-fetch/README.md +13 -0
  118. package/extensions/web-fetch/index.ts +441 -0
  119. package/package.json +117 -0
  120. package/skills/background-terminals/SKILL.md +42 -0
  121. package/skills/orchestration/SKILL.md +33 -0
  122. package/skills/subagents/SKILL.md +89 -0
  123. package/themes/ember-warm-dark.json +85 -0
@@ -0,0 +1,1386 @@
1
+ /**
2
+ * Pi-only subagents behind one Effect-managed runtime and one canonical tool
3
+ * surface. In-process Pi child sessions inherit local tools/config, deliver
4
+ * results asynchronously, and can be inspected or taken over through the
5
+ * `/subagents` dashboard.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import { randomUUID } from "node:crypto";
10
+ import * as path from "node:path";
11
+ import { StringEnum } from "@earendil-works/pi-ai";
12
+ import type {
13
+ ExtensionAPI,
14
+ ExtensionContext,
15
+ ExtensionUIContext,
16
+ } from "@earendil-works/pi-coding-agent";
17
+ import {
18
+ DEFAULT_MAX_BYTES,
19
+ DEFAULT_MAX_LINES,
20
+ formatSize,
21
+ getAgentDir,
22
+ getMarkdownTheme,
23
+ ProjectTrustStore,
24
+ sessionEntryToContextMessages,
25
+ truncateHead,
26
+ } from "@earendil-works/pi-coding-agent";
27
+ import { Markdown, Text } from "@earendil-works/pi-tui";
28
+ import { Type } from "typebox";
29
+ import {
30
+ formatElapsed,
31
+ latestText,
32
+ REASONING_EFFORTS,
33
+ type PeerMessage,
34
+ type SpawnTask,
35
+ type SubagentOrigin,
36
+ type SubagentSnapshot,
37
+ } from "./src/domain.ts";
38
+ import {
39
+ formatActivityStatus,
40
+ formatContextUtilization,
41
+ } from "./src/format.ts";
42
+ import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
43
+ import {
44
+ buildSubagentResultMessage,
45
+ buildSubagentSpawnResult,
46
+ SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS,
47
+ SUBAGENT_CANCEL_TOOL_DESCRIPTION,
48
+ SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS,
49
+ SUBAGENT_CHECK_TOOL_DESCRIPTION,
50
+ SUBAGENT_LIST_TOOL_DESCRIPTION,
51
+ SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS,
52
+ SUBAGENT_SPAWN_PROMPT_GUIDELINES,
53
+ SUBAGENT_SPAWN_PROMPT_SNIPPET,
54
+ SUBAGENT_SPAWN_TOOL_DESCRIPTION,
55
+ SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS,
56
+ SUBAGENT_WAIT_TOOL_DESCRIPTION,
57
+ WORKTREE_ISOLATION_DESCRIPTION,
58
+ } from "./src/prompt.ts";
59
+ import { createDeferredResultDelivery } from "./src/result-delivery.ts";
60
+ import {
61
+ CAPABILITY_MODES,
62
+ ISOLATION_MODES,
63
+ loadConfigDocument,
64
+ loadSubagentConfig,
65
+ resolveProfile,
66
+ saveConfigDocument,
67
+ type CapabilityMode,
68
+ type ConfigScope,
69
+ type IsolationMode,
70
+ } from "./src/config.ts";
71
+ import { buildTaskPrompt, forkConversation, parseForkTurns } from "./src/context.ts";
72
+ import {
73
+ applyAgentWorktree,
74
+ applyAgentWorktreeTo,
75
+ createAgentWorktree,
76
+ discardAgentWorktree,
77
+ inspectAgentWorktree,
78
+ integrateAgentWorktree,
79
+ WORKTREE_ACTIONS,
80
+ type WorktreeAction,
81
+ type WorktreeInfo,
82
+ } from "./src/worktree.ts";
83
+ import {
84
+ createSubagentRuntime,
85
+ runTool,
86
+ type SubagentRuntime,
87
+ } from "./src/runtime.ts";
88
+ import {
89
+ openPeerMessageViewer,
90
+ openSubagentPicker,
91
+ openSubagentTakeover,
92
+ } from "./src/ui/takeover.ts";
93
+ import {
94
+ isCoordinatorRequest,
95
+ SUBAGENT_COORDINATOR_REQUEST,
96
+ type SubagentCoordinator,
97
+ } from "./src/coordinator.ts";
98
+ import {
99
+ allocateSubagentId,
100
+ loadSubagentCatalog,
101
+ upsertSubagentCatalog,
102
+ type ArchivedSubagent,
103
+ } from "./src/catalog.ts";
104
+
105
+ const SUBAGENT_OUTPUT_MAX_BYTES = 24 * 1024;
106
+ const WAIT_OUTPUT_MAX_BYTES = 48 * 1024;
107
+ const WAIT_PER_AGENT_MAX_BYTES = 16 * 1024;
108
+ const SUBAGENT_RECORD_TYPE = "pi-subagent-record";
109
+ const SUBAGENT_PEER_MESSAGE_TYPE = "pi-subagent-peer-message";
110
+ const BTW_ENTRY_TYPE = "pi-subagent-btw-result";
111
+ const BTW_TITLE_MAX_LENGTH = 60;
112
+
113
+ interface BtwEntryData {
114
+ id: string;
115
+ title: string;
116
+ status: SubagentSnapshot["status"];
117
+ answer: string;
118
+ errorText?: string;
119
+ }
120
+
121
+ interface WaitAgentDetails {
122
+ pendingQuestions: string[];
123
+ results: Array<{
124
+ id: string;
125
+ title: string;
126
+ status: SubagentSnapshot["status"];
127
+ }>;
128
+ pending: string[];
129
+ }
130
+
131
+ function waitAgentDetails(
132
+ values: Partial<WaitAgentDetails> = {},
133
+ ): WaitAgentDetails {
134
+ return {
135
+ pendingQuestions: values.pendingQuestions ?? [],
136
+ results: values.results ?? [],
137
+ pending: values.pending ?? [],
138
+ };
139
+ }
140
+
141
+ export function deriveBtwTitle(prompt: string): string {
142
+ const firstLine = prompt.split("\n").find((line) => line.trim())?.trim().replace(/\s+/g, " ") ?? "";
143
+ if (!firstLine) return "by the way";
144
+ const codePoints = [...firstLine];
145
+ return codePoints.length <= BTW_TITLE_MAX_LENGTH
146
+ ? firstLine
147
+ : `${codePoints.slice(0, BTW_TITLE_MAX_LENGTH - 1).join("")}…`;
148
+ }
149
+
150
+ function describeSubagent(snap: SubagentSnapshot) {
151
+ const details = [
152
+ snap.meta.modelLabel ?? "?",
153
+ formatContextUtilization(snap.usage),
154
+ formatElapsed(snap),
155
+ snap.cwd,
156
+ ].filter(Boolean);
157
+ return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
158
+ }
159
+
160
+ function truncatedOutput(
161
+ snap: SubagentSnapshot,
162
+ maxBytes = SUBAGENT_OUTPUT_MAX_BYTES,
163
+ ): string {
164
+ const output = snap.finalText || "(no output)";
165
+ const truncation = truncateHead(output, {
166
+ maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
167
+ maxLines: Math.min(600, DEFAULT_MAX_LINES),
168
+ });
169
+ let text = truncation.content;
170
+ if (truncation.truncated) {
171
+ text += `\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)} shown. Full transcript in session file: ${snap.meta.sessionFilePath ?? "?"}]`;
172
+ }
173
+ return text;
174
+ }
175
+
176
+ /**
177
+ * Same-directory children inherit the live parent decision. An alternate cwd
178
+ * is trusted only when pi's persisted trust store explicitly trusts it (or a
179
+ * containing directory); unreadable/invalid trust data fails closed.
180
+ */
181
+ function resolveChildProjectTrust(options: {
182
+ parentCwd: string;
183
+ childCwd: string;
184
+ parentTrusted: boolean;
185
+ }) {
186
+ if (path.resolve(options.childCwd) === path.resolve(options.parentCwd)) {
187
+ return options.parentTrusted;
188
+ }
189
+ try {
190
+ const trustStore = new ProjectTrustStore(getAgentDir());
191
+ return trustStore.get(options.childCwd) === true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+
197
+ export default function (pi: ExtensionAPI) {
198
+ let runtime: SubagentRuntime | undefined;
199
+ let managerPromise: Promise<SubagentManagerShape> | undefined;
200
+ let sessionContext: ExtensionContext | undefined;
201
+ let ui: ExtensionUIContext | undefined;
202
+ let unsubStatus: (() => void) | undefined;
203
+ const resultDelivery = createDeferredResultDelivery<SubagentSnapshot>();
204
+ const archived = new Map<string, ArchivedSubagent>();
205
+ const peerHistory: PeerMessage[] = [];
206
+ const pendingQuestions = new Map<
207
+ string,
208
+ {
209
+ title: string;
210
+ question: string;
211
+ resolve: (reply: string) => void;
212
+ reject: (error: Error) => void;
213
+ }
214
+ >();
215
+ const pendingQuestionText = () =>
216
+ [...pendingQuestions.entries()]
217
+ .map(([id, pending]) => `- ${pending.title}: ${pending.question} (question_id: ${id})`)
218
+ .join("\n");
219
+
220
+ const parentBridge = (
221
+ title: string,
222
+ manager: SubagentManagerShape,
223
+ senderId: string,
224
+ ) => ({
225
+ notify(message: string) {
226
+ pi.sendMessage(
227
+ {
228
+ customType: "subagent-parent-message",
229
+ content: `Pi subagent "${title}" sent an update:\n\n${message}`,
230
+ display: true,
231
+ details: { title, kind: "message" },
232
+ },
233
+ { deliverAs: "followUp", triggerTurn: true },
234
+ );
235
+ },
236
+ listPeers() {
237
+ return manager.view.list().map((peer) => ({
238
+ id: peer.id,
239
+ title: peer.title,
240
+ status: peer.status,
241
+ }));
242
+ },
243
+ async messagePeer(id: string, message: string) {
244
+ const target = manager.view.get(id);
245
+ if (!target) throw new Error(`Unknown peer agent "${id}".`);
246
+ const base = {
247
+ id: randomUUID(),
248
+ sentAt: Date.now(),
249
+ sender: title,
250
+ senderId,
251
+ targetId: id,
252
+ targetTitle: target.title,
253
+ message: message.trim(),
254
+ } as const;
255
+ try {
256
+ await runTool(
257
+ getRuntime(),
258
+ manager.send(id, `[Peer agent "${title}"] ${message.trim()}`),
259
+ );
260
+ const record: PeerMessage = { ...base, status: "delivered" };
261
+ manager.view.recordPeerMessage(record);
262
+ peerHistory.push(record);
263
+ pi.appendEntry(SUBAGENT_PEER_MESSAGE_TYPE, record);
264
+ } catch (error) {
265
+ const record: PeerMessage = {
266
+ ...base,
267
+ status: "failed",
268
+ error: error instanceof Error ? error.message : String(error),
269
+ };
270
+ manager.view.recordPeerMessage(record);
271
+ peerHistory.push(record);
272
+ pi.appendEntry(SUBAGENT_PEER_MESSAGE_TYPE, record);
273
+ throw error;
274
+ }
275
+ },
276
+ ask(question: string, signal?: AbortSignal) {
277
+ const id = randomUUID();
278
+ return new Promise<string>((resolve, reject) => {
279
+ if (signal?.aborted) {
280
+ reject(new Error("Parent question aborted."));
281
+ return;
282
+ }
283
+ const cleanup = () => signal?.removeEventListener("abort", onAbort);
284
+ const onAbort = () => {
285
+ pendingQuestions.delete(id);
286
+ cleanup();
287
+ reject(new Error("Parent question aborted."));
288
+ };
289
+ signal?.addEventListener("abort", onAbort, { once: true });
290
+ pendingQuestions.set(id, {
291
+ title,
292
+ question,
293
+ resolve: (reply) => {
294
+ cleanup();
295
+ resolve(reply);
296
+ },
297
+ reject: (error) => {
298
+ cleanup();
299
+ reject(error);
300
+ },
301
+ });
302
+ pi.sendMessage(
303
+ {
304
+ customType: "subagent-parent-message",
305
+ content:
306
+ `Pi subagent "${title}" is blocked and asks:\n\n${question}\n\n` +
307
+ `Reply with reply_question(question_id: "${id}", reply: "...").`,
308
+ display: true,
309
+ details: { title, kind: "question", questionId: id },
310
+ },
311
+ { deliverAs: "followUp", triggerTurn: true },
312
+ );
313
+ });
314
+ },
315
+ });
316
+
317
+ const getRuntime = () => (runtime ??= createSubagentRuntime());
318
+
319
+ /** Resolve the manager service once per runtime and wire the extension hooks. */
320
+ const getManager = () => {
321
+ managerPromise ??= getRuntime()
322
+ .runPromise(SubagentManager)
323
+ .then((manager) => {
324
+ manager.view.setOnSettled(onSettled);
325
+ for (const message of peerHistory) manager.view.recordPeerMessage(message);
326
+ unsubStatus?.();
327
+ unsubStatus = manager.view.subscribe(() => updateStatus(manager));
328
+ updateStatus(manager);
329
+ return manager;
330
+ });
331
+ return managerPromise;
332
+ };
333
+
334
+ const persistSnapshot = (snap: SubagentSnapshot) => {
335
+ const sessionFile = snap.meta.sessionFilePath;
336
+ if (!sessionFile) return;
337
+ const record: ArchivedSubagent = {
338
+ id: snap.id,
339
+ title: snap.title,
340
+ cwd: snap.cwd,
341
+ status: snap.status,
342
+ sessionFile,
343
+ model: snap.meta.modelLabel,
344
+ agentType: snap.meta.agentType,
345
+ persona: snap.meta.persona,
346
+ capability: snap.meta.capability,
347
+ isolation: snap.meta.isolation,
348
+ worktree: snap.meta.worktree,
349
+ updatedAt: Date.now(),
350
+ };
351
+ archived.set(record.id, record);
352
+ upsertSubagentCatalog(record);
353
+ pi.appendEntry(SUBAGENT_RECORD_TYPE, record);
354
+ };
355
+
356
+ interface SpawnOptions {
357
+ message: string;
358
+ taskName?: string;
359
+ cwd?: string;
360
+ model?: string;
361
+ thinking?: (typeof REASONING_EFFORTS)[number];
362
+ readonly?: boolean;
363
+ agentType?: string;
364
+ persona?: string;
365
+ capability?: CapabilityMode;
366
+ isolation?: IsolationMode;
367
+ forkTurns?: string;
368
+ resumeFrom?: string;
369
+ origin?: SubagentOrigin;
370
+ concurrencyGroup?: string;
371
+ maxConcurrent?: number;
372
+ }
373
+
374
+ interface PreparedSpawn {
375
+ task?: SpawnTask;
376
+ resumed?: SubagentSnapshot;
377
+ cleanup(): Promise<void>;
378
+ }
379
+
380
+ const preparePiAgent = async (
381
+ manager: SubagentManagerShape,
382
+ ctx: ExtensionContext,
383
+ options: SpawnOptions,
384
+ ): Promise<PreparedSpawn> => {
385
+ const config = loadSubagentConfig(ctx.cwd, ctx.isProjectTrusted());
386
+ const profile = resolveProfile(config, {
387
+ agentType: options.agentType,
388
+ persona: options.persona,
389
+ capability: options.readonly ? "read-only" : options.capability,
390
+ model: options.model,
391
+ thinking: options.thinking,
392
+ isolation: options.isolation,
393
+ });
394
+ const liveSource = options.resumeFrom ? manager.view.get(options.resumeFrom) : undefined;
395
+ const archivedSource = options.resumeFrom ? archived.get(options.resumeFrom) : undefined;
396
+ const title =
397
+ options.taskName?.trim().slice(0, 160) ||
398
+ liveSource?.title ||
399
+ archivedSource?.title ||
400
+ profile.agentType;
401
+ if (liveSource?.status === "running") {
402
+ throw new Error(`Cannot resume running subagent "${options.resumeFrom}"; use send_message instead.`);
403
+ }
404
+ if (liveSource) {
405
+ await runTool(
406
+ getRuntime(),
407
+ manager.send(liveSource.id, buildTaskPrompt(options.message, profile.instructions, profile.persona)),
408
+ );
409
+ return { resumed: manager.view.get(liveSource.id)!, cleanup: async () => {} };
410
+ }
411
+ const resumeSessionFile = archivedSource?.sessionFile;
412
+ if (options.resumeFrom && (!resumeSessionFile || !fs.existsSync(resumeSessionFile))) {
413
+ throw new Error(`Unknown or unavailable resumable subagent "${options.resumeFrom}".`);
414
+ }
415
+
416
+ let cwd = path.resolve(ctx.cwd, options.cwd ?? ".");
417
+ const resumedWorktree = archivedSource?.worktree;
418
+ if (resumeSessionFile) cwd = resumedWorktree?.path ?? archivedSource?.cwd ?? cwd;
419
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
420
+ throw new Error(`cwd is not a directory: ${cwd}`);
421
+ }
422
+ if (
423
+ profile.isolation === "worktree" &&
424
+ options.cwd &&
425
+ !resumeSessionFile &&
426
+ options.origin !== "orchestration"
427
+ ) {
428
+ throw new Error('cwd and isolation="worktree" are mutually exclusive.');
429
+ }
430
+
431
+ let worktree = resumedWorktree;
432
+ let createdWorktree = false;
433
+ if (profile.isolation === "worktree" && !worktree) {
434
+ worktree = await createAgentWorktree(
435
+ (command, args, execOptions) => pi.exec(command, args, execOptions),
436
+ cwd,
437
+ title,
438
+ );
439
+ cwd = worktree.path;
440
+ createdWorktree = true;
441
+ }
442
+ const cleanup = async () => {
443
+ if (createdWorktree && worktree) {
444
+ await discardAgentWorktree(
445
+ (command, args, execOptions) => pi.exec(command, args, execOptions),
446
+ worktree,
447
+ ).catch(() => undefined);
448
+ }
449
+ };
450
+ const preferredId = resumeSessionFile ? options.resumeFrom! : allocateSubagentId();
451
+ const inheritedMessages = resumeSessionFile
452
+ ? []
453
+ : forkConversation(
454
+ ctx.sessionManager.buildContextEntries().flatMap(sessionEntryToContextMessages),
455
+ parseForkTurns(options.forkTurns),
456
+ );
457
+ return {
458
+ cleanup,
459
+ task: {
460
+ prompt: buildTaskPrompt(options.message, profile.instructions, profile.persona),
461
+ title,
462
+ origin: options.origin ?? "model",
463
+ cwd,
464
+ model: resumeSessionFile ? options.model : profile.model,
465
+ reasoningEffort: resumeSessionFile ? options.thinking : profile.thinking,
466
+ capability: profile.capability,
467
+ agentType: profile.agentType,
468
+ persona: profile.persona,
469
+ isolation: profile.isolation,
470
+ maxConcurrent: options.maxConcurrent ?? config.maxConcurrent,
471
+ concurrencyGroup: options.concurrencyGroup,
472
+ worktree,
473
+ resumeSessionFile,
474
+ preferredId,
475
+ parent: {
476
+ parentCwd: ctx.cwd,
477
+ bridge: parentBridge(title, manager, preferredId),
478
+ projectTrusted: resolveChildProjectTrust({
479
+ parentCwd: ctx.cwd,
480
+ childCwd: cwd,
481
+ parentTrusted: ctx.isProjectTrusted(),
482
+ }),
483
+ inheritedModel: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
484
+ inheritedThinkingLevel: pi.getThinkingLevel(),
485
+ inheritedMessages,
486
+ parentSessionFile: ctx.sessionManager.getSessionFile(),
487
+ modelRegistry: ctx.modelRegistry,
488
+ },
489
+ },
490
+ };
491
+ };
492
+
493
+ const spawnPiAgent = async (
494
+ manager: SubagentManagerShape,
495
+ ctx: ExtensionContext,
496
+ options: SpawnOptions,
497
+ ) => {
498
+ const prepared = await preparePiAgent(manager, ctx, options);
499
+ if (prepared.resumed) return prepared.resumed;
500
+ try {
501
+ const snap = await runTool(getRuntime(), manager.spawn("pi", prepared.task!));
502
+ persistSnapshot(snap);
503
+ return snap;
504
+ } catch (error) {
505
+ await prepared.cleanup();
506
+ throw error;
507
+ }
508
+ };
509
+
510
+ const updateStatus = (manager: SubagentManagerShape) => {
511
+ if (!ui) return;
512
+ const subs = manager.view.list();
513
+ if (subs.length === 0) {
514
+ ui.setStatus("subagents", undefined);
515
+ return;
516
+ }
517
+ const running = subs.filter((snap) => snap.status === "running").length;
518
+ const failed = subs.filter((snap) => snap.status === "error").length;
519
+ const done = subs.length - running - failed;
520
+ ui.setStatus(
521
+ "subagents",
522
+ formatActivityStatus(ui.theme, { running, done, failed }),
523
+ );
524
+ };
525
+
526
+ const deliverResult = (snap: SubagentSnapshot) => {
527
+ pi.sendMessage(
528
+ {
529
+ customType: "subagent-result",
530
+ content: buildSubagentResultMessage({
531
+ id: snap.id,
532
+ title: snap.title,
533
+ status: snap.status,
534
+ errorText: snap.errorText,
535
+ output: truncatedOutput(snap),
536
+ }),
537
+ display: true,
538
+ details: { id: snap.id, title: snap.title, status: snap.status },
539
+ },
540
+ { deliverAs: "followUp", triggerTurn: true },
541
+ );
542
+ };
543
+
544
+ const flushResults = () => {
545
+ for (const snap of resultDelivery.drain()) deliverResult(snap);
546
+ };
547
+
548
+ const onSettled = (snap: SubagentSnapshot, consumed: boolean) => {
549
+ persistSnapshot(snap);
550
+ if (snap.meta.origin === "orchestration") {
551
+ resultDelivery.consume([snap.id]);
552
+ return;
553
+ }
554
+ if (snap.meta.origin === "btw") {
555
+ resultDelivery.consume([snap.id]);
556
+ pi.appendEntry<BtwEntryData>(BTW_ENTRY_TYPE, {
557
+ id: snap.id,
558
+ title: snap.title,
559
+ status: snap.status,
560
+ answer: truncatedOutput(snap),
561
+ errorText: snap.errorText,
562
+ });
563
+ return;
564
+ }
565
+ if (consumed) {
566
+ resultDelivery.consume([snap.id]);
567
+ return;
568
+ }
569
+ // Keep the result retractable while the parent is working. A later
570
+ // wait_agent can consume it before agent_settled flushes follow-ups.
571
+ // Defer a copy: the live snapshot keeps mutating if the subagent is
572
+ // restarted before the deferred result flushes.
573
+ resultDelivery.defer({ ...snap, meta: { ...snap.meta } });
574
+ if (sessionContext?.isIdle()) flushResults();
575
+ };
576
+
577
+ const coordinator: SubagentCoordinator = {
578
+ async spawn(ctx, options) {
579
+ const manager = await getManager();
580
+ const snapshot = await spawnPiAgent(manager, ctx, {
581
+ ...options,
582
+ origin: "orchestration",
583
+ });
584
+ manager.view.retain(snapshot.id, true);
585
+ return snapshot;
586
+ },
587
+ async send(id, message) {
588
+ const manager = await getManager();
589
+ await runTool(getRuntime(), manager.send(id, message));
590
+ const snapshot = manager.view.get(id);
591
+ if (!snapshot) throw new Error(`Subagent "${id}" is no longer tracked.`);
592
+ return snapshot;
593
+ },
594
+ async cancel(ids) {
595
+ const manager = await getManager();
596
+ await runTool(getRuntime(), manager.cancel(ids));
597
+ },
598
+ async get(id) {
599
+ return (await getManager()).view.get(id);
600
+ },
601
+ async list() {
602
+ return (await getManager()).view.list();
603
+ },
604
+ async subscribe(listener) {
605
+ return (await getManager()).view.subscribe(listener);
606
+ },
607
+ async apply(id, targetCwd) {
608
+ const manager = await getManager();
609
+ const snapshot = manager.view.get(id);
610
+ if (!snapshot?.meta.worktree) throw new Error(`Subagent "${id}" has no worktree.`);
611
+ if (snapshot.status === "running") throw new Error(`Subagent "${id}" is still running.`);
612
+ const exec = (command: string, args: string[], options?: Parameters<typeof pi.exec>[2]) =>
613
+ pi.exec(command, args, options);
614
+ return targetCwd
615
+ ? applyAgentWorktreeTo(exec, snapshot.meta.worktree, targetCwd)
616
+ : applyAgentWorktree(exec, snapshot.meta.worktree);
617
+ },
618
+ async discard(id) {
619
+ const manager = await getManager();
620
+ const snapshot = manager.view.get(id);
621
+ if (!snapshot?.meta.worktree) return;
622
+ await discardAgentWorktree(
623
+ (command, args, options) => pi.exec(command, args, options),
624
+ snapshot.meta.worktree,
625
+ );
626
+ manager.view.clearWorktree(id);
627
+ const updated = manager.view.get(id);
628
+ if (updated) persistSnapshot(updated);
629
+ manager.view.retain(id, false);
630
+ },
631
+ async release(id) {
632
+ const manager = await getManager();
633
+ manager.view.retain(id, false);
634
+ },
635
+ };
636
+ const unsubscribeCoordinator = pi.events.on(
637
+ SUBAGENT_COORDINATOR_REQUEST,
638
+ (request) => {
639
+ if (isCoordinatorRequest(request)) request.accept(coordinator);
640
+ },
641
+ );
642
+
643
+ pi.on("session_start", (_event, ctx) => {
644
+ sessionContext = ctx;
645
+ archived.clear();
646
+ peerHistory.length = 0;
647
+ for (const record of loadSubagentCatalog().values()) archived.set(record.id, record);
648
+ for (const entry of ctx.sessionManager.getBranch()) {
649
+ if (entry.type !== "custom") continue;
650
+ if (entry.customType === SUBAGENT_RECORD_TYPE) {
651
+ const record = entry.data as ArchivedSubagent | undefined;
652
+ if (record?.id && record.sessionFile) {
653
+ const existing = archived.get(record.id);
654
+ if (!existing || existing.updatedAt <= record.updatedAt) archived.set(record.id, record);
655
+ }
656
+ } else if (entry.customType === SUBAGENT_PEER_MESSAGE_TYPE) {
657
+ const message = entry.data as PeerMessage | undefined;
658
+ if (message?.id && message.targetId) peerHistory.push(message);
659
+ }
660
+ }
661
+ if (peerHistory.length > 256) peerHistory.splice(0, peerHistory.length - 256);
662
+ if (ctx.hasUI) ui = ctx.ui;
663
+ });
664
+
665
+ pi.on("agent_settled", flushResults);
666
+
667
+ pi.on("session_shutdown", async () => {
668
+ sessionContext = undefined;
669
+ unsubscribeCoordinator();
670
+ resultDelivery.clear();
671
+ for (const pending of pendingQuestions.values()) {
672
+ pending.reject(new Error("Parent Pi session shut down before replying."));
673
+ }
674
+ pendingQuestions.clear();
675
+ unsubStatus?.();
676
+ unsubStatus = undefined;
677
+ ui?.setStatus("subagents", undefined);
678
+ const closing = runtime;
679
+ runtime = undefined;
680
+ managerPromise = undefined;
681
+ // Disposing the runtime runs the manager finalizer and closes all child
682
+ // Pi session scopes.
683
+ await closing?.dispose();
684
+ });
685
+
686
+ // --- Tools -------------------------------------------------------------
687
+
688
+ pi.registerTool({
689
+ name: "spawn_agent",
690
+ label: "Spawn Pi Subagent",
691
+ description: SUBAGENT_SPAWN_TOOL_DESCRIPTION,
692
+ promptSnippet: SUBAGENT_SPAWN_PROMPT_SNIPPET,
693
+ promptGuidelines: [
694
+ ...SUBAGENT_SPAWN_PROMPT_GUIDELINES,
695
+ "After spawn_agent starts a child, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch it run; completion automatically starts the next parent turn.",
696
+ ],
697
+ parameters: Type.Object({
698
+ message: Type.String({
699
+ description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.prompt,
700
+ }),
701
+ task_name: Type.Optional(
702
+ Type.String({
703
+ description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.name,
704
+ }),
705
+ ),
706
+ cwd: Type.Optional(
707
+ Type.String({
708
+ description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.workingDir,
709
+ }),
710
+ ),
711
+ model: Type.Optional(
712
+ Type.String({
713
+ description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.model,
714
+ }),
715
+ ),
716
+ thinking: Type.Optional(
717
+ StringEnum(REASONING_EFFORTS, {
718
+ description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
719
+ }),
720
+ ),
721
+ readonly: Type.Optional(
722
+ Type.Boolean({ description: "Compatibility alias for capability=read-only" }),
723
+ ),
724
+ agent_type: Type.Optional(
725
+ Type.String({ description: "Agent profile: general-purpose, explore, plan, or a configured custom profile" }),
726
+ ),
727
+ persona: Type.Optional(
728
+ Type.String({ description: "Configured behavioral persona overlay" }),
729
+ ),
730
+ capability: Type.Optional(
731
+ StringEnum(CAPABILITY_MODES, { description: "Tool policy: read-only, read-write, execute, or all" }),
732
+ ),
733
+ isolation: Type.Optional(
734
+ StringEnum(ISOLATION_MODES, { description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.isolation }),
735
+ ),
736
+ fork_turns: Type.Optional(
737
+ Type.String({ description: 'Parent context to inherit: "none" (default), "all", or a positive number of recent user turns' }),
738
+ ),
739
+ resume_from: Type.Optional(
740
+ Type.String({ description: "Completed subagent id to continue with its full transcript and tool state" }),
741
+ ),
742
+ }),
743
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
744
+ const manager = await getManager();
745
+ const snap = await spawnPiAgent(manager, ctx, {
746
+ message: params.message,
747
+ taskName: params.task_name,
748
+ cwd: params.cwd,
749
+ model: params.model,
750
+ thinking: params.thinking,
751
+ readonly: params.readonly,
752
+ agentType: params.agent_type,
753
+ persona: params.persona,
754
+ capability: params.capability,
755
+ isolation: params.isolation,
756
+ forkTurns: params.fork_turns,
757
+ resumeFrom: params.resume_from,
758
+ });
759
+
760
+ return {
761
+ content: [
762
+ {
763
+ type: "text",
764
+ text: buildSubagentSpawnResult({
765
+ id: snap.id,
766
+ title: snap.title,
767
+ modelLabel: snap.meta.modelLabel ?? "?",
768
+ cwd: snap.cwd,
769
+ agentType: snap.meta.agentType,
770
+ capability: snap.meta.capability,
771
+ isolation: snap.meta.isolation,
772
+ resumed: !!params.resume_from,
773
+ }),
774
+ },
775
+ ],
776
+ details: {
777
+ id: snap.id,
778
+ title: snap.title,
779
+ cwd: snap.cwd,
780
+ model: snap.meta.modelLabel,
781
+ agentType: snap.meta.agentType,
782
+ persona: snap.meta.persona,
783
+ capability: snap.meta.capability,
784
+ isolation: snap.meta.isolation,
785
+ worktree: snap.meta.worktree,
786
+ resumedFrom: params.resume_from,
787
+ },
788
+ };
789
+ },
790
+ });
791
+
792
+ pi.registerTool({
793
+ name: "wait_agent",
794
+ label: "Collect Pi Subagent Results",
795
+ description: SUBAGENT_WAIT_TOOL_DESCRIPTION,
796
+ parameters: Type.Object({
797
+ ids: Type.Optional(
798
+ Type.Array(Type.String(), {
799
+ maxItems: 64,
800
+ description: SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS.ids,
801
+ }),
802
+ ),
803
+ }),
804
+ async execute(_toolCallId, params) {
805
+ const manager = await getManager();
806
+ if (pendingQuestions.size > 0) {
807
+ return {
808
+ content: [{ type: "text", text: `A Pi subagent needs guidance:\n${pendingQuestionText()}` }],
809
+ details: waitAgentDetails({
810
+ pendingQuestions: [...pendingQuestions.keys()],
811
+ }),
812
+ };
813
+ }
814
+ const requested = params.ids?.length
815
+ ? params.ids
816
+ : manager.view.list().filter((snap) => snap.status === "running").map((snap) => snap.id);
817
+ const ids = [...new Set(requested)];
818
+ if (ids.length === 0) {
819
+ return {
820
+ content: [{ type: "text", text: "No running Pi subagents." }],
821
+ details: waitAgentDetails(),
822
+ };
823
+ }
824
+ const known = manager.view.list().map((snap) => snap.id);
825
+ const unknown = ids.filter((id) => !manager.view.get(id));
826
+ if (unknown.length > 0) {
827
+ throw new Error(
828
+ `Unknown subagent id(s): ${unknown.join(", ")}. Known: ${known.join(", ") || "none"}.`,
829
+ );
830
+ }
831
+
832
+ const snapshots = ids.map((id) => manager.view.get(id)!);
833
+ const settled = snapshots.filter((snap) => snap.status !== "running");
834
+ const pending = snapshots.filter((snap) => snap.status === "running");
835
+ resultDelivery.consume(settled.map((snap) => snap.id));
836
+
837
+ const sections: string[] = [];
838
+ let remainingBytes = WAIT_OUTPUT_MAX_BYTES;
839
+ for (const snap of settled) {
840
+ const verb = snap.status === "error" ? "failed" : "finished";
841
+ let section = `## ${snap.id} "${snap.title}" ${verb}`;
842
+ if (snap.errorText) section += `\nError: ${snap.errorText}`;
843
+ const headerBytes = Buffer.byteLength(section, "utf8") + 2;
844
+ const outputBudget = Math.max(
845
+ 512,
846
+ Math.min(WAIT_PER_AGENT_MAX_BYTES, remainingBytes - headerBytes),
847
+ );
848
+ section += `\n\n${truncatedOutput(snap, outputBudget)}`;
849
+ const sectionBytes = Buffer.byteLength(section, "utf8");
850
+ if (sectionBytes > remainingBytes) {
851
+ sections.push(
852
+ `## ${snap.id} "${snap.title}"\n\n[omitted: total result limit reached]`,
853
+ );
854
+ break;
855
+ }
856
+ sections.push(section);
857
+ remainingBytes -= sectionBytes;
858
+ }
859
+ if (pending.length > 0) {
860
+ sections.push(
861
+ `Still running in the background: ${pending.map((snap) => snap.id).join(", ")}. ` +
862
+ "Their completion notices will arrive automatically; continue useful work or end the turn instead of polling.",
863
+ );
864
+ }
865
+
866
+ const combined = sections.join("\n\n---\n\n");
867
+ const bounded = truncateHead(combined, {
868
+ maxBytes: WAIT_OUTPUT_MAX_BYTES - 128,
869
+ maxLines: DEFAULT_MAX_LINES,
870
+ });
871
+ const text = bounded.truncated
872
+ ? `${bounded.content}\n\n[result output truncated at the total output limit]`
873
+ : bounded.content;
874
+ return {
875
+ content: [{ type: "text", text }],
876
+ details: waitAgentDetails({
877
+ results: snapshots.map((snap) => ({
878
+ id: snap.id,
879
+ title: snap.title,
880
+ status: snap.status,
881
+ })),
882
+ pending: pending.map((snap) => snap.id),
883
+ }),
884
+ };
885
+ },
886
+ });
887
+
888
+ pi.registerTool({
889
+ name: "close_agent",
890
+ label: "Close Pi Subagents",
891
+ description: SUBAGENT_CANCEL_TOOL_DESCRIPTION,
892
+ parameters: Type.Object({
893
+ ids: Type.Array(Type.String(), {
894
+ description: SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS.ids,
895
+ }),
896
+ }),
897
+ async execute(_toolCallId, params) {
898
+ const manager = await getManager();
899
+ const ids = [...new Set(params.ids)];
900
+ if (ids.length === 0)
901
+ throw new Error("Provide at least one subagent id.");
902
+
903
+ const known = manager.view.list().map((snap) => snap.id);
904
+ const unknown = ids.filter((id) => !manager.view.get(id));
905
+ if (unknown.length > 0) {
906
+ throw new Error(
907
+ `Unknown subagent id(s): ${unknown.join(", ")}. Known: ${known.join(", ") || "none"}.`,
908
+ );
909
+ }
910
+
911
+ const report = await runTool(getRuntime(), manager.cancel(ids));
912
+
913
+ const lines = report.map((entry) =>
914
+ entry.cancelled
915
+ ? `Cancelled ${entry.id} "${entry.title}".`
916
+ : `${entry.id} "${entry.title}" was already ${entry.status}.`,
917
+ );
918
+
919
+ return {
920
+ content: [{ type: "text", text: lines.join("\n") }],
921
+ details: {
922
+ results: report.map((entry) => ({
923
+ id: entry.id,
924
+ title: entry.title,
925
+ status: entry.status,
926
+ })),
927
+ },
928
+ };
929
+ },
930
+ });
931
+
932
+ pi.registerTool({
933
+ name: "check_agent",
934
+ label: "Check Pi Subagent",
935
+ description: SUBAGENT_CHECK_TOOL_DESCRIPTION,
936
+ parameters: Type.Object({
937
+ id: Type.String({
938
+ description: SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS.id,
939
+ }),
940
+ }),
941
+ async execute(_toolCallId, params) {
942
+ const manager = await getManager();
943
+ const snap = manager.view.get(params.id);
944
+ if (!snap) {
945
+ const known = manager.view.list().map((s) => s.id);
946
+ throw new Error(
947
+ `Unknown subagent id "${params.id}". Known: ${known.join(", ") || "none"}.`,
948
+ );
949
+ }
950
+
951
+ let text = `${describeSubagent(snap)}\nTurns: ${snap.turns}`;
952
+ if (snap.errorText) text += `\nError: ${snap.errorText}`;
953
+
954
+ const output = latestText(snap);
955
+ if (output) {
956
+ const preview = truncateHead(output, { maxBytes: 2048, maxLines: 20 });
957
+ text += `\n\nLatest output:\n${preview.content}`;
958
+ if (preview.truncated) text += "\n[...]";
959
+ } else if (snap.status === "running") {
960
+ text += "\n\n(no text output yet)";
961
+ }
962
+
963
+ return {
964
+ content: [{ type: "text", text }],
965
+ details: { id: snap.id, status: snap.status, turns: snap.turns },
966
+ };
967
+ },
968
+ });
969
+
970
+ pi.registerTool({
971
+ name: "list_agent_profiles",
972
+ label: "List Subagent Profiles",
973
+ description: "List available subagent profiles, personas, capability defaults, and the active concurrency limit.",
974
+ parameters: Type.Object({}),
975
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
976
+ const config = loadSubagentConfig(ctx.cwd, ctx.isProjectTrusted());
977
+ const profileLines = Object.entries(config.profiles).map(
978
+ ([name, profile]) =>
979
+ `profile ${name}: ${profile.description ?? "no description"}; capability=${profile.capability ?? "all"}; isolation=${profile.isolation ?? "none"}`,
980
+ );
981
+ const personaLines = Object.entries(config.personas).map(
982
+ ([name, persona]) => `persona ${name}: ${persona.description ?? "no description"}`,
983
+ );
984
+ return {
985
+ content: [{
986
+ type: "text",
987
+ text: [`maxConcurrent=${config.maxConcurrent}`, ...profileLines, ...personaLines].join("\n"),
988
+ }],
989
+ details: { maxConcurrent: config.maxConcurrent, profiles: config.profiles, personas: config.personas },
990
+ };
991
+ },
992
+ });
993
+
994
+ pi.registerTool({
995
+ name: "list_agents",
996
+ label: "List Pi Subagents",
997
+ description: SUBAGENT_LIST_TOOL_DESCRIPTION,
998
+ parameters: Type.Object({}),
999
+ async execute() {
1000
+ const manager = await getManager();
1001
+ const subs = manager.view.list();
1002
+ const liveIds = new Set(subs.map((snap) => snap.id));
1003
+ const resumable = [...archived.values()].filter((record) => !liveIds.has(record.id));
1004
+ const lines = subs.map((snap) => describeSubagent(snap));
1005
+ lines.push(...resumable.map((record) => `${record.id} [archived:${record.status}] "${record.title}" (${record.model ?? "?"}, ${record.cwd})`));
1006
+ return {
1007
+ content: [{ type: "text", text: lines.length ? lines.join("\n") : "No subagents." }],
1008
+ details: {
1009
+ subagents: [
1010
+ ...subs.map((snap) => ({ id: snap.id, title: snap.title, status: snap.status })),
1011
+ ...resumable.map((record) => ({ id: record.id, title: record.title, status: `archived:${record.status}` })),
1012
+ ],
1013
+ },
1014
+ };
1015
+ },
1016
+ });
1017
+
1018
+ pi.registerTool({
1019
+ name: "send_message",
1020
+ label: "Message Pi Subagent",
1021
+ description: "Guide an active subagent or start its next turn when idle.",
1022
+ parameters: Type.Object({
1023
+ id: Type.String({ description: "Subagent id" }),
1024
+ message: Type.String({ description: "Guidance or follow-up" }),
1025
+ }),
1026
+ async execute(_toolCallId, params) {
1027
+ const manager = await getManager();
1028
+ const snap = manager.view.get(params.id);
1029
+ if (!snap) throw new Error(`Unknown Pi subagent id "${params.id}".`);
1030
+ await runTool(getRuntime(), manager.send(params.id, params.message));
1031
+ return {
1032
+ content: [{ type: "text", text: `Sent message to ${params.id} "${snap.title}".` }],
1033
+ details: { id: params.id, status: manager.view.get(params.id)?.status },
1034
+ };
1035
+ },
1036
+ });
1037
+
1038
+ pi.registerTool({
1039
+ name: "apply_agent_changes",
1040
+ label: "Manage Isolated Agent Changes",
1041
+ description: "Inspect, patch, cherry-pick, merge, or permanently discard a completed isolated agent worktree. Cherry-pick and merge are preflighted in a temporary worktree before the source repository changes.",
1042
+ parameters: Type.Object({
1043
+ id: Type.String({ description: "Worktree-isolated subagent id" }),
1044
+ action: Type.Optional(StringEnum(WORKTREE_ACTIONS, {
1045
+ description: "inspect, patch (default), cherry-pick, merge, or discard",
1046
+ })),
1047
+ }),
1048
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1049
+ const manager = await getManager();
1050
+ const snap = manager.view.get(params.id);
1051
+ const record = archived.get(params.id);
1052
+ const worktree = snap?.meta.worktree ?? record?.worktree;
1053
+ if (!worktree) throw new Error(`Subagent "${params.id}" has no isolated worktree.`);
1054
+ const action: WorktreeAction = params.action ?? "patch";
1055
+ if (snap?.status === "running" && action !== "inspect") {
1056
+ throw new Error(`Subagent "${params.id}" is still running; wait or close it before ${action}.`);
1057
+ }
1058
+ const exec = (command: string, args: string[], execOptions?: Parameters<typeof pi.exec>[2]) =>
1059
+ pi.exec(command, args, execOptions);
1060
+ let result: Record<string, unknown>;
1061
+ if (action === "inspect") {
1062
+ result = { action, ...(await inspectAgentWorktree(exec, worktree)) };
1063
+ } else if (action === "patch") {
1064
+ result = await applyAgentWorktree(exec, worktree);
1065
+ } else if (action === "discard") {
1066
+ if (ctx.hasUI) {
1067
+ const confirmed = await ctx.ui.confirm(
1068
+ "Discard isolated subagent worktree?",
1069
+ `This permanently deletes ${worktree.path} and branch ${worktree.branch}.`,
1070
+ );
1071
+ if (!confirmed) throw new Error("Worktree discard cancelled.");
1072
+ }
1073
+ await discardAgentWorktree(exec, worktree);
1074
+ result = { action, changed: false, discarded: true, files: [] };
1075
+ manager.view.clearWorktree(params.id);
1076
+ if (record) {
1077
+ const updated = { ...record, worktree: undefined, updatedAt: Date.now() };
1078
+ archived.set(params.id, updated);
1079
+ upsertSubagentCatalog(updated);
1080
+ pi.appendEntry(SUBAGENT_RECORD_TYPE, updated);
1081
+ }
1082
+ } else {
1083
+ result = await integrateAgentWorktree(exec, worktree, action, snap?.title ?? record?.title ?? params.id);
1084
+ }
1085
+ const files = Array.isArray(result.files) ? result.files as string[] : [];
1086
+ const text = action === "inspect"
1087
+ ? `${params.id}: ${files.length} changed file(s), ${(result.commits as string[] | undefined)?.length ?? 0} commit(s), dirty=${String(result.dirty)}`
1088
+ : action === "discard"
1089
+ ? `Discarded the isolated worktree for ${params.id}.`
1090
+ : result.changed
1091
+ ? `${action} applied ${files.length} file(s) from ${params.id}:\n${files.join("\n")}`
1092
+ : `${params.id} has no changes to ${action}.`;
1093
+ return { content: [{ type: "text", text }], details: { id: params.id, ...result, worktree } };
1094
+ },
1095
+ });
1096
+
1097
+ pi.registerTool({
1098
+ name: "reply_question",
1099
+ label: "Reply to Pi Subagent",
1100
+ description: "Answer a subagent's blocking ask_parent question.",
1101
+ parameters: Type.Object({
1102
+ question_id: Type.Optional(Type.String({ description: "Question id; omit when exactly one is pending" })),
1103
+ reply: Type.String({ description: "Answer or guidance" }),
1104
+ }),
1105
+ async execute(_toolCallId, params) {
1106
+ const id = params.question_id ?? (pendingQuestions.size === 1 ? [...pendingQuestions.keys()][0] : undefined);
1107
+ if (!id) throw new Error(`Specify question_id; ${pendingQuestions.size} questions are pending.`);
1108
+ const pending = pendingQuestions.get(id);
1109
+ if (!pending) throw new Error(`Unknown or already answered question id "${id}".`);
1110
+ pendingQuestions.delete(id);
1111
+ pending.resolve(params.reply);
1112
+ return {
1113
+ content: [{ type: "text", text: `Replied to Pi subagent "${pending.title}".` }],
1114
+ details: { questionId: id, title: pending.title },
1115
+ };
1116
+ },
1117
+ });
1118
+
1119
+ pi.registerTool({
1120
+ name: "task",
1121
+ label: "Start Pi Subagent Tasks",
1122
+ description: "Start independent subagent tasks together in the background and return their ids immediately. Completion notices automatically start the next parent turn, so the parent should end its current turn when no independent work remains instead of checking status.",
1123
+ promptGuidelines: [
1124
+ "After task starts children, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch them run; completion automatically starts the next parent turn.",
1125
+ ],
1126
+ parameters: Type.Object({
1127
+ tasks: Type.Array(
1128
+ Type.Object({
1129
+ message: Type.String({ description: "Standalone task prompt" }),
1130
+ task_name: Type.Optional(Type.String({ description: "Short name" })),
1131
+ cwd: Type.Optional(Type.String({ description: "Working directory" })),
1132
+ model: Type.Optional(Type.String({ description: "Model hint" })),
1133
+ thinking: Type.Optional(StringEnum(REASONING_EFFORTS)),
1134
+ readonly: Type.Optional(Type.Boolean({ description: "Compatibility alias for read-only" })),
1135
+ agent_type: Type.Optional(Type.String({ description: "Agent profile" })),
1136
+ persona: Type.Optional(Type.String({ description: "Persona overlay" })),
1137
+ capability: Type.Optional(StringEnum(CAPABILITY_MODES)),
1138
+ isolation: Type.Optional(StringEnum(ISOLATION_MODES, { description: WORKTREE_ISOLATION_DESCRIPTION })),
1139
+ fork_turns: Type.Optional(Type.String({ description: '"none", "all", or recent turn count' })),
1140
+ }),
1141
+ { minItems: 1, maxItems: 50 },
1142
+ ),
1143
+ }),
1144
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1145
+ const manager = await getManager();
1146
+ const config = loadSubagentConfig(ctx.cwd, ctx.isProjectTrusted());
1147
+ if (params.tasks.length > config.maxConcurrent) {
1148
+ throw new Error(
1149
+ `Requested ${params.tasks.length} tasks, but maxConcurrent is ${config.maxConcurrent}.`,
1150
+ );
1151
+ }
1152
+ const prepared: PreparedSpawn[] = [];
1153
+ try {
1154
+ for (const item of params.tasks) {
1155
+ prepared.push(await preparePiAgent(manager, ctx, {
1156
+ message: item.message,
1157
+ taskName: item.task_name,
1158
+ cwd: item.cwd,
1159
+ model: item.model,
1160
+ thinking: item.thinking,
1161
+ readonly: item.readonly,
1162
+ agentType: item.agent_type,
1163
+ persona: item.persona,
1164
+ capability: item.capability,
1165
+ isolation: item.isolation,
1166
+ forkTurns: item.fork_turns,
1167
+ }));
1168
+ }
1169
+ } catch (error) {
1170
+ await Promise.all(prepared.map((item) => item.cleanup()));
1171
+ throw error;
1172
+ }
1173
+ let spawned: ReadonlyArray<SubagentSnapshot>;
1174
+ try {
1175
+ spawned = await runTool(
1176
+ getRuntime(),
1177
+ manager.spawnBatch("pi", prepared.map((item) => item.task!)),
1178
+ );
1179
+ } catch (error) {
1180
+ await Promise.all(prepared.map((item) => item.cleanup()));
1181
+ throw error;
1182
+ }
1183
+ for (const snap of spawned) persistSnapshot(snap);
1184
+ const lines = spawned.map(
1185
+ (snap) => `- ${describeSubagent(snap)}`,
1186
+ );
1187
+ return {
1188
+ content: [{
1189
+ type: "text",
1190
+ text:
1191
+ `Started ${spawned.length} Pi subagent${spawned.length === 1 ? "" : "s"} in the background:\n` +
1192
+ `${lines.join("\n")}\n\n` +
1193
+ "Continue useful parent work or end the turn. Completion notices will arrive automatically.",
1194
+ }],
1195
+ details: {
1196
+ agents: spawned.map((snap) => ({ id: snap.id, title: snap.title, status: snap.status })),
1197
+ },
1198
+ };
1199
+ },
1200
+ });
1201
+
1202
+ // --- Result message rendering ------------------------------------------
1203
+
1204
+ pi.registerMessageRenderer(
1205
+ "subagent-result",
1206
+ (message, { expanded }, theme) => {
1207
+ const details = (message.details ?? {}) as {
1208
+ id?: string;
1209
+ title?: string;
1210
+ status?: string;
1211
+ };
1212
+ const failed = details.status === "error";
1213
+ const icon = failed ? theme.fg("error", "x") : theme.fg("success", "■");
1214
+ const header =
1215
+ `${icon} ` +
1216
+ theme.fg("accent", theme.bold(`subagent ${details.id ?? "?"}`)) +
1217
+ theme.fg(
1218
+ "muted",
1219
+ ` · ${details.title ?? ""} · ${failed ? "failed" : "finished"}`,
1220
+ );
1221
+
1222
+ const content =
1223
+ typeof message.content === "string" ? message.content : "";
1224
+ // Remove only the summary line. The following Error line (when present)
1225
+ // is part of the actual result and must remain visible.
1226
+ const body = content.split("\n").slice(1).join("\n").trim();
1227
+
1228
+ if (expanded) {
1229
+ const md = new Markdown(`${body}`, 0, 0, getMarkdownTheme());
1230
+ const container = new Text(header, 0, 0);
1231
+ return {
1232
+ render: (width: number) => [
1233
+ ...container.render(width),
1234
+ ...md.render(width),
1235
+ ],
1236
+ invalidate: () => {
1237
+ container.invalidate();
1238
+ md.invalidate();
1239
+ },
1240
+ };
1241
+ }
1242
+
1243
+ const previewLines = body.split("\n").slice(0, 8);
1244
+ let text = header;
1245
+ for (const line of previewLines)
1246
+ text += `\n${theme.fg("toolOutput", line)}`;
1247
+ if (body.split("\n").length > 8)
1248
+ text += `\n${theme.fg("dim", "... (ctrl+o to expand)")}`;
1249
+ return new Text(text, 0, 0);
1250
+ },
1251
+ );
1252
+
1253
+ pi.registerEntryRenderer<BtwEntryData>(
1254
+ BTW_ENTRY_TYPE,
1255
+ (entry, { expanded }, theme) => {
1256
+ const data = entry.data;
1257
+ if (!data) return new Text(theme.fg("warning", "By-the-way result unavailable"), 0, 0);
1258
+ const failed = data.status === "error";
1259
+ const header =
1260
+ `${theme.fg(failed ? "error" : "success", "■")} ` +
1261
+ theme.fg("accent", theme.bold(`by the way · ${data.title}`)) +
1262
+ theme.fg("muted", ` · ${failed ? "failed" : "answered"} · ${data.id}`);
1263
+ const body = [data.errorText ? `Error: ${data.errorText}` : "", data.answer]
1264
+ .filter(Boolean)
1265
+ .join("\n\n");
1266
+ if (expanded) {
1267
+ const markdown = new Markdown(body, 0, 0, getMarkdownTheme());
1268
+ const title = new Text(header, 0, 0);
1269
+ return {
1270
+ render: (width: number) => [...title.render(width), ...markdown.render(width)],
1271
+ invalidate: () => {
1272
+ title.invalidate();
1273
+ markdown.invalidate();
1274
+ },
1275
+ };
1276
+ }
1277
+ const bodyLines = body.split("\n");
1278
+ let text = header;
1279
+ for (const line of bodyLines.slice(0, 8)) text += `\n${theme.fg("toolOutput", line)}`;
1280
+ if (bodyLines.length > 8) text += `\n${theme.fg("dim", "... (ctrl+o to expand)")}`;
1281
+ return new Text(text, 0, 0);
1282
+ },
1283
+ );
1284
+
1285
+ // --- Commands -----------------------------------------------------------
1286
+
1287
+ pi.registerCommand("btw", {
1288
+ description: "Ask a read-only side question in a context-aware Pi subagent",
1289
+ handler: async (args, ctx) => {
1290
+ if (ctx.mode !== "tui") {
1291
+ if (ctx.hasUI) ctx.ui.notify("By-the-way questions require the TUI", "error");
1292
+ return;
1293
+ }
1294
+ let question = args.trim();
1295
+ if (!question) {
1296
+ question = (await ctx.ui.input("By the way", "Ask a side question…"))?.trim() ?? "";
1297
+ }
1298
+ if (!question) return;
1299
+ const manager = await getManager();
1300
+ let snap: SubagentSnapshot;
1301
+ try {
1302
+ snap = await spawnPiAgent(manager, ctx, {
1303
+ message:
1304
+ "Answer this side question directly. Investigate only as far as needed, do not modify files, and distinguish verified evidence from inference.\n\n" +
1305
+ question,
1306
+ taskName: deriveBtwTitle(question),
1307
+ agentType: "explore",
1308
+ capability: "execute",
1309
+ isolation: "none",
1310
+ forkTurns: "4",
1311
+ origin: "btw",
1312
+ });
1313
+ } catch (error) {
1314
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1315
+ return;
1316
+ }
1317
+ await openSubagentTakeover(ctx, manager.view, snap.id);
1318
+ },
1319
+ });
1320
+
1321
+ pi.registerCommand("subagents", {
1322
+ description: "Inspect agents, peer messages, profiles, or configuration",
1323
+ getArgumentCompletions: (prefix) =>
1324
+ ["agents", "peers", "profiles", "config"]
1325
+ .filter((value) => value.startsWith(prefix.trim()))
1326
+ .map((value) => ({ value, label: value })),
1327
+ handler: async (args, ctx) => {
1328
+ if (ctx.mode !== "tui") {
1329
+ if (ctx.hasUI) ctx.ui.notify("Subagent UI is only available in the TUI", "error");
1330
+ return;
1331
+ }
1332
+ const action = args.trim() || "agents";
1333
+ if (action === "config") {
1334
+ let scope: ConfigScope = "global";
1335
+ if (ctx.isProjectTrusted()) {
1336
+ const selected = await ctx.ui.select("Edit subagent configuration", ["global", "project"]);
1337
+ if (!selected) return;
1338
+ scope = selected as ConfigScope;
1339
+ }
1340
+ const edited = await ctx.ui.editor(
1341
+ `Edit ${scope} subagent configuration`,
1342
+ loadConfigDocument(scope, ctx.cwd),
1343
+ );
1344
+ if (edited === undefined) return;
1345
+ try {
1346
+ saveConfigDocument(scope, ctx.cwd, edited);
1347
+ ctx.ui.notify(`Saved ${scope} subagent configuration`, "info");
1348
+ } catch (error) {
1349
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1350
+ }
1351
+ return;
1352
+ }
1353
+ const manager = await getManager();
1354
+ if (action === "peers") {
1355
+ await openPeerMessageViewer(ctx, manager.view);
1356
+ return;
1357
+ }
1358
+ if (action === "profiles") {
1359
+ const config = loadSubagentConfig(ctx.cwd, ctx.isProjectTrusted());
1360
+ const entries = [
1361
+ ...Object.entries(config.profiles).map(([name, value]) => ({
1362
+ label: `profile: ${name}`,
1363
+ text: JSON.stringify(value, null, 2),
1364
+ })),
1365
+ ...Object.entries(config.personas).map(([name, value]) => ({
1366
+ label: `persona: ${name}`,
1367
+ text: JSON.stringify(value, null, 2),
1368
+ })),
1369
+ ];
1370
+ const selected = await ctx.ui.select("Subagent profiles and personas", entries.map((entry) => entry.label));
1371
+ const entry = entries.find((candidate) => candidate.label === selected);
1372
+ if (entry) await ctx.ui.editor(`View ${entry.label} · edit via /subagents config`, entry.text);
1373
+ return;
1374
+ }
1375
+ if (action !== "agents") {
1376
+ ctx.ui.notify('Use /subagents agents, peers, profiles, or config', "error");
1377
+ return;
1378
+ }
1379
+ if (manager.view.size() === 0) {
1380
+ ctx.ui.notify("No subagents yet. The agent spawns them with spawn_agent.", "info");
1381
+ return;
1382
+ }
1383
+ await openSubagentPicker(ctx, manager.view);
1384
+ },
1385
+ });
1386
+ }