@dyyz1993/pi-coding-agent 0.69.17 → 0.69.23

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 (113) hide show
  1. package/dist/core/agent-session.d.ts +12 -1
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +208 -0
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/extensions/client-channel.d.ts +61 -0
  6. package/dist/core/extensions/client-channel.d.ts.map +1 -0
  7. package/dist/core/extensions/client-channel.js +67 -0
  8. package/dist/core/extensions/client-channel.js.map +1 -0
  9. package/dist/core/extensions/index.d.ts +3 -2
  10. package/dist/core/extensions/index.d.ts.map +1 -1
  11. package/dist/core/extensions/index.js +1 -0
  12. package/dist/core/extensions/index.js.map +1 -1
  13. package/dist/core/extensions/loader.d.ts.map +1 -1
  14. package/dist/core/extensions/loader.js +13 -0
  15. package/dist/core/extensions/loader.js.map +1 -1
  16. package/dist/core/extensions/runner.d.ts +1 -0
  17. package/dist/core/extensions/runner.d.ts.map +1 -1
  18. package/dist/core/extensions/runner.js +8 -0
  19. package/dist/core/extensions/runner.js.map +1 -1
  20. package/dist/core/extensions/types.d.ts +49 -0
  21. package/dist/core/extensions/types.d.ts.map +1 -1
  22. package/dist/core/extensions/types.js.map +1 -1
  23. package/dist/core/include-resolver.d.ts +18 -0
  24. package/dist/core/include-resolver.d.ts.map +1 -0
  25. package/dist/core/include-resolver.js +304 -0
  26. package/dist/core/include-resolver.js.map +1 -0
  27. package/dist/core/resource-loader.d.ts.map +1 -1
  28. package/dist/core/resource-loader.js +17 -4
  29. package/dist/core/resource-loader.js.map +1 -1
  30. package/dist/core/session-manager.d.ts +8 -4
  31. package/dist/core/session-manager.d.ts.map +1 -1
  32. package/dist/core/session-manager.js +29 -6
  33. package/dist/core/session-manager.js.map +1 -1
  34. package/dist/core/storage.d.ts +24 -0
  35. package/dist/core/storage.d.ts.map +1 -0
  36. package/dist/core/storage.js +55 -0
  37. package/dist/core/storage.js.map +1 -0
  38. package/dist/core/tools/path-security.d.ts +15 -0
  39. package/dist/core/tools/path-security.d.ts.map +1 -0
  40. package/dist/core/tools/path-security.js +76 -0
  41. package/dist/core/tools/path-security.js.map +1 -0
  42. package/dist/core/tools/strip-markdown.d.ts +2 -0
  43. package/dist/core/tools/strip-markdown.d.ts.map +1 -0
  44. package/dist/core/tools/strip-markdown.js +8 -0
  45. package/dist/core/tools/strip-markdown.js.map +1 -0
  46. package/dist/index.d.ts +5 -4
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +3 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.js +1 -0
  52. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/dist/modes/rpc/rpc-client-types.d.ts +1 -0
  54. package/dist/modes/rpc/rpc-client-types.d.ts.map +1 -1
  55. package/dist/modes/rpc/rpc-client-types.js.map +1 -1
  56. package/dist/modes/rpc/rpc-client.d.ts +1 -0
  57. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  58. package/dist/modes/rpc/rpc-client.js +3 -0
  59. package/dist/modes/rpc/rpc-client.js.map +1 -1
  60. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  61. package/dist/modes/rpc/rpc-mode.js +5 -0
  62. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  63. package/dist/modes/rpc/rpc-types.d.ts +9 -0
  64. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  65. package/dist/modes/rpc/rpc-types.js.map +1 -1
  66. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  67. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  68. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  69. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  70. package/examples/extensions/with-deps/package-lock.json +2 -2
  71. package/examples/extensions/with-deps/package.json +1 -1
  72. package/package.json +9 -5
  73. package/dist/rules-engine/cache.d.ts +0 -4
  74. package/dist/rules-engine/cache.d.ts.map +0 -1
  75. package/dist/rules-engine/cache.js +0 -32
  76. package/dist/rules-engine/cache.js.map +0 -1
  77. package/dist/rules-engine/config.d.ts +0 -8
  78. package/dist/rules-engine/config.d.ts.map +0 -1
  79. package/dist/rules-engine/config.js +0 -56
  80. package/dist/rules-engine/config.js.map +0 -1
  81. package/dist/rules-engine/index.d.ts +0 -10
  82. package/dist/rules-engine/index.d.ts.map +0 -1
  83. package/dist/rules-engine/index.js +0 -393
  84. package/dist/rules-engine/index.js.map +0 -1
  85. package/dist/rules-engine/injector.d.ts +0 -5
  86. package/dist/rules-engine/injector.d.ts.map +0 -1
  87. package/dist/rules-engine/injector.js +0 -57
  88. package/dist/rules-engine/injector.js.map +0 -1
  89. package/dist/rules-engine/loader.d.ts +0 -8
  90. package/dist/rules-engine/loader.d.ts.map +0 -1
  91. package/dist/rules-engine/loader.js +0 -190
  92. package/dist/rules-engine/loader.js.map +0 -1
  93. package/dist/rules-engine/matcher.d.ts +0 -3
  94. package/dist/rules-engine/matcher.d.ts.map +0 -1
  95. package/dist/rules-engine/matcher.js +0 -48
  96. package/dist/rules-engine/matcher.js.map +0 -1
  97. package/dist/rules-engine/types.d.ts +0 -150
  98. package/dist/rules-engine/types.d.ts.map +0 -1
  99. package/dist/rules-engine/types.js +0 -2
  100. package/dist/rules-engine/types.js.map +0 -1
  101. package/examples/extensions/auto-session-title.ts +0 -82
  102. package/examples/extensions/file-snapshot.ts +0 -417
  103. package/examples/extensions/subagent/README.md +0 -172
  104. package/examples/extensions/subagent/agents/planner.md +0 -37
  105. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  106. package/examples/extensions/subagent/agents/scout.md +0 -50
  107. package/examples/extensions/subagent/agents/worker.md +0 -24
  108. package/examples/extensions/subagent/agents.ts +0 -126
  109. package/examples/extensions/subagent/index.ts +0 -987
  110. package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
  111. package/examples/extensions/subagent/prompts/implement.md +0 -10
  112. package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
  113. package/examples/extensions/subagent-v2/index.ts +0 -849
@@ -1,849 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as os from "node:os";
3
- import * as path from "node:path";
4
- import type { Message } from "@dyyz1993/pi-ai";
5
- import { StringEnum } from "@dyyz1993/pi-ai";
6
- import {
7
- type ExtensionAPI,
8
- getMarkdownTheme,
9
- type Theme,
10
- type ThemeColor,
11
- withFileMutationQueue,
12
- } from "@dyyz1993/pi-coding-agent";
13
- import { type Component, Container, Markdown, Spacer, Text } from "@dyyz1993/pi-tui";
14
- import { Type } from "typebox";
15
- import { ServerChannel } from "../../../src/core/extensions/server-channel.js";
16
- import { RpcClient } from "../../../src/modes/rpc/rpc-client.js";
17
- import { type AgentScope, discoverAgents } from "../subagent/agents.js";
18
-
19
- const STEER_GRACE_MS = 30_000;
20
- const COLLAPSED_ITEM_COUNT = 10;
21
-
22
- function formatTokens(count: number): string {
23
- if (count < 1000) return count.toString();
24
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
25
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
26
- return `${(count / 1000000).toFixed(1)}M`;
27
- }
28
-
29
- function formatUsageStats(
30
- usage: {
31
- input: number;
32
- output: number;
33
- cacheRead: number;
34
- cacheWrite: number;
35
- cost: number;
36
- contextTokens?: number;
37
- turns?: number;
38
- },
39
- model?: string,
40
- ): string {
41
- const parts: string[] = [];
42
- if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
43
- if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
44
- if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
45
- if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
46
- if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
47
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
48
- if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
49
- if (model) parts.push(model);
50
- return parts.join(" ");
51
- }
52
-
53
- function formatToolCall(
54
- toolName: string,
55
- args: Record<string, unknown>,
56
- themeFg: (color: string, text: string) => string,
57
- ): string {
58
- const shortenPath = (p: string) => {
59
- const home = os.homedir();
60
- return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
61
- };
62
-
63
- switch (toolName) {
64
- case "bash": {
65
- const command = (args.command as string) || "...";
66
- const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
67
- return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
68
- }
69
- case "read": {
70
- const rawPath = (args.file_path || args.path || "...") as string;
71
- const filePath = shortenPath(rawPath);
72
- const offset = args.offset as number | undefined;
73
- const limit = args.limit as number | undefined;
74
- let text = themeFg("accent", filePath);
75
- if (offset !== undefined || limit !== undefined) {
76
- const startLine = offset ?? 1;
77
- const endLine = limit !== undefined ? startLine + limit - 1 : "";
78
- text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
79
- }
80
- return themeFg("muted", "read ") + text;
81
- }
82
- case "write": {
83
- const rawPath = (args.file_path || args.path || "...") as string;
84
- const filePath = shortenPath(rawPath);
85
- const content = (args.content || "") as string;
86
- const lines = content.split("\n").length;
87
- let text = themeFg("muted", "write ") + themeFg("accent", filePath);
88
- if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
89
- return text;
90
- }
91
- case "edit": {
92
- const rawPath = (args.file_path || args.path || "...") as string;
93
- return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
94
- }
95
- case "ls": {
96
- const rawPath = (args.path || ".") as string;
97
- return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
98
- }
99
- case "find": {
100
- const pattern = (args.pattern || "*") as string;
101
- const rawPath = (args.path || ".") as string;
102
- return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`);
103
- }
104
- case "grep": {
105
- const pattern = (args.pattern || "") as string;
106
- const rawPath = (args.path || ".") as string;
107
- return (
108
- themeFg("muted", "grep ") +
109
- themeFg("accent", `/${pattern}/`) +
110
- themeFg("dim", ` in ${shortenPath(rawPath)}`)
111
- );
112
- }
113
- default: {
114
- const argsStr = JSON.stringify(args);
115
- const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
116
- return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
117
- }
118
- }
119
- }
120
-
121
- interface UsageStats {
122
- input: number;
123
- output: number;
124
- cacheRead: number;
125
- cacheWrite: number;
126
- cost: number;
127
- contextTokens: number;
128
- turns: number;
129
- }
130
-
131
- interface SingleResult {
132
- agent: string;
133
- agentSource: "user" | "project" | "unknown";
134
- task: string;
135
- exitCode: number;
136
- messages: Message[];
137
- stderr: string;
138
- usage: UsageStats;
139
- model?: string;
140
- stopReason?: string;
141
- errorMessage?: string;
142
- sessionPath?: string;
143
- }
144
-
145
- interface SubagentDetails {
146
- agentScope: AgentScope;
147
- projectAgentsDir: string | null;
148
- result: SingleResult | null;
149
- }
150
-
151
- interface BackgroundTask {
152
- taskId: string;
153
- client: RpcClient;
154
- sessionId: string;
155
- sessionPath: string;
156
- startedAt: number;
157
- }
158
-
159
- const backgroundTasks = new Map<string, BackgroundTask>();
160
-
161
- function getFinalOutput(messages: Message[]): string {
162
- for (let i = messages.length - 1; i >= 0; i--) {
163
- const msg = messages[i];
164
- if (msg.role === "assistant") {
165
- for (const part of msg.content) {
166
- if (part.type === "text") return part.text;
167
- }
168
- }
169
- }
170
- return "";
171
- }
172
-
173
- type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, unknown> };
174
-
175
- function getDisplayItems(messages: Message[]): DisplayItem[] {
176
- const items: DisplayItem[] = [];
177
- for (const msg of messages) {
178
- if (msg.role === "assistant") {
179
- for (const part of msg.content) {
180
- if (part.type === "text") items.push({ type: "text", text: part.text });
181
- else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
182
- }
183
- }
184
- }
185
- return items;
186
- }
187
-
188
- async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
189
- const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-v2-"));
190
- const safeName = agentName.replace(/[^\w.-]+/g, "_");
191
- const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
192
- await withFileMutationQueue(filePath, async () => {
193
- await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
194
- });
195
- return { dir: tmpDir, filePath };
196
- }
197
-
198
- function sessionDir(): string {
199
- const dir = path.join(os.tmpdir(), "pi-subagent-v2-sessions");
200
- fs.mkdirSync(dir, { recursive: true });
201
- return dir;
202
- }
203
-
204
- function makeUsage(): UsageStats {
205
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
206
- }
207
-
208
- function accumulateUsage(result: SingleResult, msg: Message): void {
209
- if (msg.role !== "assistant") return;
210
- result.usage.turns++;
211
- const usage = msg.usage;
212
- if (usage) {
213
- result.usage.input += usage.input || 0;
214
- result.usage.output += usage.output || 0;
215
- result.usage.cacheRead += usage.cacheRead || 0;
216
- result.usage.cacheWrite += usage.cacheWrite || 0;
217
- result.usage.cost += usage.cost?.total || 0;
218
- result.usage.contextTokens = usage.totalTokens || 0;
219
- }
220
- if (!result.model && msg.model) result.model = msg.model;
221
- if (msg.stopReason) result.stopReason = msg.stopReason;
222
- if (msg.errorMessage) result.errorMessage = msg.errorMessage;
223
- }
224
-
225
- function subscribeToClient(
226
- client: RpcClient,
227
- result: SingleResult,
228
- onEventData: (event: unknown, meta: Record<string, unknown>) => void,
229
- meta: Record<string, unknown>,
230
- onMessage?: () => void,
231
- ): () => void {
232
- return client.onEvent((event) => {
233
- onEventData(event, meta);
234
- if (event.type === "message_end" && event.message) {
235
- const msg = event.message as Message;
236
- result.messages.push(msg);
237
- accumulateUsage(result, msg);
238
- onMessage?.();
239
- }
240
- });
241
- }
242
-
243
- async function runWithTimeout(
244
- client: RpcClient,
245
- prompt: string,
246
- timeoutMs: number,
247
- signal?: AbortSignal,
248
- ): Promise<"done" | "timeout" | "aborted"> {
249
- const promptTimeout = timeoutMs - STEER_GRACE_MS;
250
-
251
- const completionPromise = (async () => {
252
- await client.prompt(prompt);
253
- await client.waitForIdle(promptTimeout);
254
- })();
255
-
256
- const timeoutPromise = new Promise<"timeout">((resolve) => {
257
- setTimeout(() => resolve("timeout"), promptTimeout);
258
- });
259
-
260
- const promises: Promise<"done" | "timeout" | "aborted">[] = [
261
- completionPromise.then(() => "done" as const),
262
- timeoutPromise,
263
- ];
264
-
265
- if (signal) {
266
- if (signal.aborted) return "aborted";
267
- promises.push(
268
- new Promise<"aborted">((resolve) => {
269
- signal.addEventListener("abort", () => resolve("aborted"), { once: true });
270
- }),
271
- );
272
- }
273
-
274
- return Promise.race(promises);
275
- }
276
-
277
- async function handleGracePeriod(client: RpcClient, result: SingleResult): Promise<void> {
278
- await client.steer("Please summarize your findings and wrap up now. You have 30 seconds remaining.");
279
- await Promise.race([
280
- new Promise<void>((resolve) => {
281
- const sub = client.onEvent((event) => {
282
- if (event.type === "agent_end") {
283
- sub();
284
- resolve();
285
- }
286
- });
287
- }),
288
- new Promise<void>((resolve) => setTimeout(resolve, STEER_GRACE_MS)),
289
- ]);
290
- result.stopReason = "timeout";
291
- result.exitCode = 1;
292
- }
293
-
294
- function cleanupTempFiles(tmpPromptPath: string | null, tmpPromptDir: string | null): void {
295
- if (tmpPromptPath)
296
- try {
297
- fs.unlinkSync(tmpPromptPath);
298
- } catch {}
299
- if (tmpPromptDir)
300
- try {
301
- fs.rmdirSync(tmpPromptDir);
302
- } catch {}
303
- }
304
-
305
- const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
306
- description: 'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
307
- default: "user",
308
- });
309
-
310
- const SubagentParams = Type.Object({
311
- agent: Type.String({ description: "Name of the agent to invoke" }),
312
- task: Type.String({ description: "Task instruction to delegate to the agent" }),
313
- background: Type.Optional(Type.Boolean({ description: "Run in background mode. Default: false.", default: false })),
314
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 300.", default: 300 })),
315
- cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
316
- agentScope: Type.Optional(AgentScopeSchema),
317
- confirmProjectAgents: Type.Optional(
318
- Type.Boolean({ description: "Prompt before running project-local agents. Default: true.", default: true }),
319
- ),
320
- });
321
-
322
- const SubagentResumeParams = Type.Object({
323
- sessionId: Type.Optional(Type.String({ description: "Session ID from previous run" })),
324
- sessionPath: Type.Optional(Type.String({ description: "Path to the saved session file" })),
325
- instruction: Type.Optional(Type.String({ description: "Additional instruction for the resumed agent" })),
326
- background: Type.Optional(Type.Boolean({ description: "Run in background mode. Default: false.", default: false })),
327
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 300.", default: 300 })),
328
- });
329
-
330
- function renderSingleResult(r: SingleResult, expanded: boolean, theme: Theme): Component {
331
- const mdTheme = getMarkdownTheme();
332
- const fg = (c: string, t: string) => theme.fg(c as ThemeColor, t);
333
- const isError =
334
- r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted" || r.stopReason === "timeout";
335
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
336
- const displayItems = getDisplayItems(r.messages);
337
- const finalOutput = getFinalOutput(r.messages);
338
-
339
- const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
340
- const toShow = limit ? items.slice(-limit) : items;
341
- const skipped = limit && items.length > limit ? items.length - limit : 0;
342
- let text = "";
343
- if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
344
- for (const item of toShow) {
345
- if (item.type === "text") {
346
- const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
347
- text += `${theme.fg("toolOutput", preview)}\n`;
348
- } else {
349
- text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, fg)}\n`;
350
- }
351
- }
352
- return text.trimEnd();
353
- };
354
-
355
- if (expanded) {
356
- const container = new Container();
357
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
358
- if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
359
- container.addChild(new Text(header, 0, 0));
360
- if (isError && r.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
361
- container.addChild(new Spacer(1));
362
- container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
363
- container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
364
- container.addChild(new Spacer(1));
365
- container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
366
- if (displayItems.length === 0 && !finalOutput) {
367
- container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
368
- } else {
369
- for (const item of displayItems) {
370
- if (item.type === "toolCall")
371
- container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, fg), 0, 0));
372
- }
373
- if (finalOutput) {
374
- container.addChild(new Spacer(1));
375
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
376
- }
377
- }
378
- const usageStr = formatUsageStats(r.usage, r.model);
379
- if (usageStr) {
380
- container.addChild(new Spacer(1));
381
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
382
- }
383
- return container;
384
- }
385
-
386
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
387
- if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
388
- if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
389
- else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
390
- else {
391
- text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
392
- if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
393
- }
394
- const usageStr = formatUsageStats(r.usage, r.model);
395
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
396
- return new Text(text, 0, 0);
397
- }
398
-
399
- export default function (pi: ExtensionAPI) {
400
- const rawChannel = pi.registerChannel("subagent");
401
- const channel = new ServerChannel(rawChannel);
402
-
403
- pi.registerTool({
404
- name: "subagent",
405
- label: "Subagent",
406
- description: [
407
- "Delegate a task to a specialized subagent with isolated context using RPC mode.",
408
- "Agents are discovered from ~/.pi/agent/agents/ (user) and .pi/agents/ (project).",
409
- 'Use agentScope to control discovery: "user" (default), "project", or "both".',
410
- "Set background: true to run without blocking. The parent is notified on completion.",
411
- "Sessions are persisted for later resume via subagent_resume.",
412
- ].join(" "),
413
- parameters: SubagentParams,
414
-
415
- async execute(toolCallId, params, signal, onUpdate, ctx) {
416
- const agentScope: AgentScope = params.agentScope ?? "user";
417
- const discovery = discoverAgents(ctx.cwd, agentScope);
418
- const agents = discovery.agents;
419
- const timeoutMs = Math.max((params.timeout ?? 300) * 1000, STEER_GRACE_MS + 10_000);
420
- const background = params.background ?? false;
421
-
422
- const details: SubagentDetails = {
423
- agentScope,
424
- projectAgentsDir: discovery.projectAgentsDir,
425
- result: null,
426
- };
427
-
428
- if (
429
- (agentScope === "project" || agentScope === "both") &&
430
- (params.confirmProjectAgents ?? true) &&
431
- ctx.hasUI
432
- ) {
433
- const agent = agents.find((a) => a.name === params.agent);
434
- if (agent?.source === "project") {
435
- const dir = discovery.projectAgentsDir ?? "(unknown)";
436
- const ok = await ctx.ui.confirm(
437
- "Run project-local agent?",
438
- `Agent: ${agent.name}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
439
- );
440
- if (!ok)
441
- return {
442
- content: [{ type: "text", text: "Canceled: project-local agent not approved." }],
443
- details,
444
- };
445
- }
446
- }
447
-
448
- const agent = agents.find((a) => a.name === params.agent);
449
- if (!agent) {
450
- const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
451
- return {
452
- content: [{ type: "text", text: `Unknown agent: "${params.agent}". Available agents: ${available}.` }],
453
- details,
454
- };
455
- }
456
-
457
- const sessionId = `subagent-v2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
458
- const sessionPath = path.join(sessionDir(), `${sessionId}.json`);
459
- const startedAt = Date.now();
460
-
461
- let tmpPromptDir: string | null = null;
462
- let tmpPromptPath: string | null = null;
463
-
464
- const extraArgs: string[] = ["--session", sessionPath];
465
- if (agent.systemPrompt.trim()) {
466
- const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
467
- tmpPromptDir = tmp.dir;
468
- tmpPromptPath = tmp.filePath;
469
- extraArgs.push("--append-system-prompt", tmpPromptPath);
470
- }
471
- if (agent.tools && agent.tools.length > 0) {
472
- extraArgs.push("--tools", agent.tools.join(","));
473
- }
474
-
475
- const currentResult: SingleResult = {
476
- agent: params.agent,
477
- agentSource: agent.source,
478
- task: params.task,
479
- exitCode: 0,
480
- messages: [],
481
- stderr: "",
482
- usage: makeUsage(),
483
- model: agent.model,
484
- sessionPath,
485
- };
486
-
487
- const emitUpdate = () => {
488
- if (onUpdate) {
489
- onUpdate({
490
- content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
491
- details: { ...details, result: { ...currentResult } },
492
- });
493
- }
494
- };
495
-
496
- const client = new RpcClient({
497
- cwd: params.cwd ?? ctx.cwd,
498
- provider: ctx.model?.provider || undefined,
499
- model: agent.model,
500
- args: extraArgs,
501
- });
502
-
503
- if (background) {
504
- const taskId = `bg-${sessionId}`;
505
-
506
- const startBg = async () => {
507
- try {
508
- await client.start();
509
- if (agent.tools && agent.tools.length > 0) await client.setActiveTools(agent.tools);
510
-
511
- const unsubscribe = subscribeToClient(
512
- client,
513
- currentResult,
514
- (event, meta) => channel.emit("event", { event, ...meta }),
515
- { sessionId, taskId },
516
- );
517
-
518
- const raceResult = await runWithTimeout(client, params.task, timeoutMs);
519
- if (raceResult === "timeout") await handleGracePeriod(client, currentResult);
520
- unsubscribe();
521
-
522
- if (currentResult.exitCode === 0) {
523
- currentResult.exitCode = currentResult.stopReason === "error" ? 1 : 0;
524
- }
525
- } catch (err) {
526
- currentResult.exitCode = 1;
527
- currentResult.errorMessage = err instanceof Error ? err.message : String(err);
528
- currentResult.stderr = client.getStderr();
529
- } finally {
530
- await client.stop();
531
- cleanupTempFiles(tmpPromptPath, tmpPromptDir);
532
- backgroundTasks.delete(taskId);
533
-
534
- const finalText = getFinalOutput(currentResult.messages) || "(no output)";
535
- pi.appendEntry("subagent", {
536
- toolCallId,
537
- sessionId,
538
- sessionPath,
539
- description: params.agent,
540
- instruction: params.task,
541
- startedAt,
542
- completedAt: Date.now(),
543
- exitCode: currentResult.exitCode,
544
- finalText,
545
- });
546
-
547
- const isCrash = currentResult.exitCode !== 0;
548
- const summary = finalText.slice(0, 200);
549
- const msg = isCrash
550
- ? `子任务中断:${params.agent} — ${currentResult.errorMessage || summary}`
551
- : `子任务完成:${params.agent} — ${summary}`;
552
- try {
553
- pi.sendUserMessage(msg, { deliverAs: "followUp" });
554
- } catch {
555
- pi.sendUserMessage(msg);
556
- }
557
- }
558
- };
559
-
560
- backgroundTasks.set(taskId, { taskId, client, sessionId, sessionPath, startedAt });
561
- startBg();
562
-
563
- return {
564
- content: [{ type: "text", text: `Started background task: ${taskId}` }],
565
- details: { agentScope, projectAgentsDir: discovery.projectAgentsDir, result: null },
566
- };
567
- }
568
-
569
- let wasAborted = false;
570
-
571
- try {
572
- await client.start();
573
- if (agent.tools && agent.tools.length > 0) await client.setActiveTools(agent.tools);
574
-
575
- const unsubscribe = subscribeToClient(
576
- client,
577
- currentResult,
578
- (event, meta) => channel.emit("event", { event, ...meta }),
579
- { sessionId },
580
- emitUpdate,
581
- );
582
-
583
- const raceResult = await runWithTimeout(client, params.task, timeoutMs, signal);
584
-
585
- if (raceResult === "aborted") {
586
- wasAborted = true;
587
- await client.abort();
588
- currentResult.stopReason = "aborted";
589
- currentResult.exitCode = 1;
590
- } else if (raceResult === "timeout") {
591
- await handleGracePeriod(client, currentResult);
592
- }
593
-
594
- unsubscribe();
595
- if (currentResult.exitCode === 0 && !wasAborted) {
596
- currentResult.exitCode = currentResult.stopReason === "error" ? 1 : 0;
597
- }
598
- } catch (err) {
599
- currentResult.exitCode = 1;
600
- currentResult.errorMessage = err instanceof Error ? err.message : String(err);
601
- currentResult.stderr = client.getStderr();
602
- } finally {
603
- await client.stop();
604
- cleanupTempFiles(tmpPromptPath, tmpPromptDir);
605
- }
606
-
607
- const finalText = getFinalOutput(currentResult.messages) || "(no output)";
608
-
609
- pi.appendEntry("subagent", {
610
- toolCallId,
611
- sessionId,
612
- sessionPath,
613
- description: params.agent,
614
- instruction: params.task,
615
- startedAt,
616
- completedAt: Date.now(),
617
- exitCode: currentResult.exitCode,
618
- finalText,
619
- });
620
-
621
- const isError =
622
- currentResult.exitCode !== 0 ||
623
- currentResult.stopReason === "error" ||
624
- currentResult.stopReason === "aborted" ||
625
- currentResult.stopReason === "timeout";
626
- if (isError) {
627
- let errorMsg = currentResult.errorMessage || currentResult.stderr || finalText || "(no output)";
628
- if (currentResult.sessionPath) {
629
- errorMsg += `\n\nSession saved: ${currentResult.sessionPath}\nTo resume: use subagent_resume with sessionPath="${currentResult.sessionPath}"`;
630
- }
631
- return {
632
- content: [{ type: "text", text: `Agent ${currentResult.stopReason || "failed"}: ${errorMsg}` }],
633
- details: { ...details, result: currentResult },
634
- isError: true,
635
- };
636
- }
637
-
638
- return {
639
- content: [{ type: "text", text: finalText }],
640
- details: { ...details, result: currentResult },
641
- };
642
- },
643
-
644
- renderCall(args, theme, _context) {
645
- const scope: AgentScope = args.agentScope ?? "user";
646
- const bg = args.background ? theme.fg("warning", " [bg]") : "";
647
- const agentName = args.agent || "...";
648
- const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
649
- let text =
650
- theme.fg("toolTitle", theme.bold("subagent ")) +
651
- theme.fg("accent", agentName) +
652
- theme.fg("muted", ` [${scope}]`) +
653
- bg;
654
- text += `\n ${theme.fg("dim", preview)}`;
655
- return new Text(text, 0, 0);
656
- },
657
-
658
- renderResult(result, { expanded }, theme, _context) {
659
- const details = result.details as SubagentDetails | undefined;
660
- if (!details?.result) {
661
- const text = result.content[0];
662
- return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
663
- }
664
- return renderSingleResult(details.result, expanded, theme);
665
- },
666
- });
667
-
668
- pi.registerTool({
669
- name: "subagent_resume",
670
- label: "Subagent Resume",
671
- description: "Resume a previously interrupted subagent session. The agent continues from where it left off.",
672
- parameters: SubagentResumeParams,
673
-
674
- async execute(toolCallId, params, signal, onUpdate, ctx) {
675
- const sPath = params.sessionPath;
676
- if (!sPath) {
677
- return {
678
- content: [{ type: "text", text: "sessionPath is required." }],
679
- details: { agentScope: "user" as AgentScope, projectAgentsDir: null, result: null },
680
- };
681
- }
682
-
683
- if (!fs.existsSync(sPath)) {
684
- return {
685
- content: [{ type: "text", text: `Session file not found: ${sPath}` }],
686
- details: { agentScope: "user" as AgentScope, projectAgentsDir: null, result: null },
687
- };
688
- }
689
-
690
- const timeoutMs = Math.max((params.timeout ?? 300) * 1000, STEER_GRACE_MS + 10_000);
691
- const background = params.background ?? false;
692
-
693
- const currentResult: SingleResult = {
694
- agent: "(resumed)",
695
- agentSource: "unknown",
696
- task: params.instruction ?? "(resume)",
697
- exitCode: 0,
698
- messages: [],
699
- stderr: "",
700
- usage: makeUsage(),
701
- sessionPath: sPath,
702
- };
703
-
704
- const details: SubagentDetails = { agentScope: "user", projectAgentsDir: null, result: null };
705
-
706
- const emitUpdate = () => {
707
- if (onUpdate) {
708
- onUpdate({
709
- content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(resuming...)" }],
710
- details: { ...details, result: { ...currentResult } },
711
- });
712
- }
713
- };
714
-
715
- const client = new RpcClient({
716
- cwd: ctx.cwd,
717
- provider: ctx.model?.provider || undefined,
718
- args: ["--session", sPath, "-c"],
719
- });
720
-
721
- const sessionId = params.sessionId ?? `resume-${Date.now()}`;
722
- const resumePrompt = params.instruction ?? "Please continue from where you left off.";
723
-
724
- if (background) {
725
- const taskId = `bg-resume-${sessionId}`;
726
- const startedAt = Date.now();
727
-
728
- const startBg = async () => {
729
- try {
730
- await client.start();
731
- const unsubscribe = subscribeToClient(
732
- client,
733
- currentResult,
734
- (event, meta) => channel.emit("event", { event, ...meta }),
735
- { sessionId, taskId },
736
- );
737
-
738
- const raceResult = await runWithTimeout(client, resumePrompt, timeoutMs);
739
- if (raceResult === "timeout") await handleGracePeriod(client, currentResult);
740
- unsubscribe();
741
-
742
- if (currentResult.exitCode === 0) {
743
- currentResult.exitCode = currentResult.stopReason === "error" ? 1 : 0;
744
- }
745
- } catch (err) {
746
- currentResult.exitCode = 1;
747
- currentResult.errorMessage = err instanceof Error ? err.message : String(err);
748
- currentResult.stderr = client.getStderr();
749
- } finally {
750
- await client.stop();
751
- backgroundTasks.delete(taskId);
752
-
753
- const finalText = getFinalOutput(currentResult.messages) || "(no output)";
754
- pi.appendEntry("subagent", {
755
- toolCallId,
756
- sessionId,
757
- sessionPath: sPath,
758
- description: "(resumed)",
759
- instruction: params.instruction ?? "(resume)",
760
- startedAt,
761
- completedAt: Date.now(),
762
- exitCode: currentResult.exitCode,
763
- finalText,
764
- });
765
-
766
- const isCrash = currentResult.exitCode !== 0;
767
- const summary = finalText.slice(0, 200);
768
- const msg = isCrash
769
- ? `子任务中断:(resumed) — ${currentResult.errorMessage || summary}`
770
- : `子任务完成:(resumed) — ${summary}`;
771
- try {
772
- pi.sendUserMessage(msg, { deliverAs: "followUp" });
773
- } catch {
774
- pi.sendUserMessage(msg);
775
- }
776
- }
777
- };
778
-
779
- backgroundTasks.set(taskId, { taskId, client, sessionId, sessionPath: sPath, startedAt });
780
- startBg();
781
-
782
- return {
783
- content: [{ type: "text", text: `Started background resume task: ${taskId}` }],
784
- details: { agentScope: "user", projectAgentsDir: null, result: null },
785
- };
786
- }
787
-
788
- let wasAborted = false;
789
-
790
- try {
791
- await client.start();
792
- const unsubscribe = subscribeToClient(
793
- client,
794
- currentResult,
795
- (event, meta) => channel.emit("event", { event, ...meta }),
796
- { sessionId },
797
- emitUpdate,
798
- );
799
-
800
- const raceResult = await runWithTimeout(client, resumePrompt, timeoutMs, signal);
801
-
802
- if (raceResult === "aborted") {
803
- wasAborted = true;
804
- await client.abort();
805
- currentResult.stopReason = "aborted";
806
- currentResult.exitCode = 1;
807
- } else if (raceResult === "timeout") {
808
- await handleGracePeriod(client, currentResult);
809
- }
810
-
811
- unsubscribe();
812
- if (currentResult.exitCode === 0 && !wasAborted) {
813
- currentResult.exitCode = currentResult.stopReason === "error" ? 1 : 0;
814
- }
815
- } catch (err) {
816
- currentResult.exitCode = 1;
817
- currentResult.errorMessage = err instanceof Error ? err.message : String(err);
818
- currentResult.stderr = client.getStderr();
819
- } finally {
820
- await client.stop();
821
- }
822
-
823
- let finalText = getFinalOutput(currentResult.messages) || "(no output)";
824
- if (currentResult.exitCode !== 0 && currentResult.sessionPath) {
825
- finalText += `\n\nSession saved: ${currentResult.sessionPath}\nTo resume again: use subagent_resume with sessionPath="${currentResult.sessionPath}"`;
826
- }
827
-
828
- return {
829
- content: [{ type: "text", text: finalText }],
830
- details: { ...details, result: currentResult },
831
- };
832
- },
833
-
834
- renderCall(args, theme, _context) {
835
- const bg = args.background ? theme.fg("warning", " [bg]") : "";
836
- const sPath = args.sessionPath ?? args.sessionId ?? "...";
837
- return new Text(theme.fg("toolTitle", theme.bold("subagent_resume ")) + theme.fg("accent", sPath) + bg, 0, 0);
838
- },
839
-
840
- renderResult(result, { expanded }, theme, _context) {
841
- const details = result.details as SubagentDetails | undefined;
842
- if (!details?.result) {
843
- const text = result.content[0];
844
- return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
845
- }
846
- return renderSingleResult(details.result, expanded, theme);
847
- },
848
- });
849
- }