@dyyz1993/pi-coding-agent 0.74.24 → 0.74.27

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 (157) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +3 -0
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/session-manager.d.ts +5 -0
  6. package/dist/core/session-manager.d.ts.map +1 -1
  7. package/dist/core/session-manager.js +8 -0
  8. package/dist/core/session-manager.js.map +1 -1
  9. package/dist/extensions/agent-permissions/index.ts +235 -0
  10. package/dist/extensions/ask-tools/index.ts +115 -0
  11. package/dist/extensions/auto-memory/contract.d.ts +51 -0
  12. package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
  13. package/dist/extensions/auto-memory/contract.js +2 -0
  14. package/dist/extensions/auto-memory/contract.js.map +1 -0
  15. package/dist/extensions/auto-memory/contract.ts +56 -0
  16. package/dist/extensions/auto-memory/index.ts +969 -0
  17. package/dist/extensions/auto-memory/prompts.ts +202 -0
  18. package/dist/extensions/auto-memory/skip-rules.ts +297 -0
  19. package/dist/extensions/auto-memory/utils.ts +208 -0
  20. package/dist/extensions/auto-session-title/index.ts +83 -0
  21. package/dist/extensions/bash-ext/contract.d.ts +79 -0
  22. package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
  23. package/dist/extensions/bash-ext/contract.js +2 -0
  24. package/dist/extensions/bash-ext/contract.js.map +1 -0
  25. package/dist/extensions/bash-ext/contract.ts +69 -0
  26. package/dist/extensions/bash-ext/index.ts +858 -0
  27. package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
  28. package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
  29. package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
  30. package/dist/extensions/claude-hooks-compat/index.ts +178 -0
  31. package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
  32. package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
  33. package/dist/extensions/claude-hooks-compat/types.ts +77 -0
  34. package/dist/extensions/compaction-manager/config.ts +47 -0
  35. package/dist/extensions/compaction-manager/context-fold.ts +63 -0
  36. package/dist/extensions/compaction-manager/index.ts +151 -0
  37. package/dist/extensions/compaction-manager/microcompact.ts +49 -0
  38. package/dist/extensions/compaction-manager/reactive.ts +9 -0
  39. package/dist/extensions/compaction-manager/session-memory.ts +48 -0
  40. package/dist/extensions/coordinator/INTEGRATION.md +376 -0
  41. package/dist/extensions/coordinator/handler.test.ts +277 -0
  42. package/dist/extensions/coordinator/handler.ts +189 -0
  43. package/dist/extensions/coordinator/index.ts +261 -0
  44. package/dist/extensions/coordinator/types.d.ts +100 -0
  45. package/dist/extensions/coordinator/types.d.ts.map +1 -0
  46. package/dist/extensions/coordinator/types.js +2 -0
  47. package/dist/extensions/coordinator/types.js.map +1 -0
  48. package/dist/extensions/coordinator/types.ts +72 -0
  49. package/dist/extensions/file-snapshot/index.ts +131 -0
  50. package/dist/extensions/file-time-guard/README.md +133 -0
  51. package/dist/extensions/file-time-guard/config.ts +13 -0
  52. package/dist/extensions/file-time-guard/index.ts +171 -0
  53. package/dist/extensions/hooks-engine/index.ts +117 -0
  54. package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
  55. package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
  56. package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
  57. package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
  58. package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
  59. package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
  60. package/dist/extensions/lsp/lsp/contract.js +2 -0
  61. package/dist/extensions/lsp/lsp/contract.js.map +1 -0
  62. package/dist/extensions/lsp/lsp/contract.ts +103 -0
  63. package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
  64. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
  65. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
  66. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
  67. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
  68. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
  69. package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
  70. package/dist/extensions/lsp/lsp/index.ts +310 -0
  71. package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
  72. package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
  73. package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
  74. package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
  75. package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
  76. package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
  77. package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
  78. package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
  79. package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
  80. package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
  81. package/dist/extensions/message-bridge/GUIDE.md +210 -0
  82. package/dist/extensions/message-bridge/index.ts +222 -0
  83. package/dist/extensions/output-guard/index.ts +446 -0
  84. package/dist/extensions/preview/index.ts +278 -0
  85. package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
  86. package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
  87. package/dist/extensions/rules-engine/cache.js +232 -0
  88. package/dist/extensions/rules-engine/cache.ts +38 -0
  89. package/dist/extensions/rules-engine/config.js +63 -0
  90. package/dist/extensions/rules-engine/config.ts +70 -0
  91. package/dist/extensions/rules-engine/index.js +1530 -0
  92. package/dist/extensions/rules-engine/index.ts +552 -0
  93. package/dist/extensions/rules-engine/injector.js +68 -0
  94. package/dist/extensions/rules-engine/injector.ts +74 -0
  95. package/dist/extensions/rules-engine/loader.js +179 -0
  96. package/dist/extensions/rules-engine/loader.ts +205 -0
  97. package/dist/extensions/rules-engine/matcher.js +534 -0
  98. package/dist/extensions/rules-engine/matcher.ts +52 -0
  99. package/dist/extensions/rules-engine/types.d.ts +156 -0
  100. package/dist/extensions/rules-engine/types.d.ts.map +1 -0
  101. package/dist/extensions/rules-engine/types.js +2 -0
  102. package/dist/extensions/rules-engine/types.js.map +1 -0
  103. package/dist/extensions/rules-engine/types.ts +169 -0
  104. package/dist/extensions/session-supervisor/checker.ts +116 -0
  105. package/dist/extensions/session-supervisor/config.ts +45 -0
  106. package/dist/extensions/session-supervisor/index.ts +726 -0
  107. package/dist/extensions/session-supervisor/prompts.ts +132 -0
  108. package/dist/extensions/session-supervisor/scheduler.ts +69 -0
  109. package/dist/extensions/session-supervisor/types.ts +215 -0
  110. package/dist/extensions/subagent/README.md +172 -0
  111. package/dist/extensions/subagent/agents/explorer.md +25 -0
  112. package/dist/extensions/subagent/agents/guide.md +27 -0
  113. package/dist/extensions/subagent/agents/planner.md +37 -0
  114. package/dist/extensions/subagent/agents/reviewer.md +35 -0
  115. package/dist/extensions/subagent/agents/scout.md +50 -0
  116. package/dist/extensions/subagent/agents/verification.md +35 -0
  117. package/dist/extensions/subagent/agents/worker.md +24 -0
  118. package/dist/extensions/subagent/agents.ts +25 -0
  119. package/dist/extensions/subagent/index.ts +987 -0
  120. package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
  121. package/dist/extensions/subagent/prompts/implement.md +10 -0
  122. package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
  123. package/dist/extensions/subagent-ext/contract.d.ts +2 -0
  124. package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
  125. package/dist/extensions/subagent-ext/contract.js +2 -0
  126. package/dist/extensions/subagent-ext/contract.js.map +1 -0
  127. package/dist/extensions/subagent-ext/contract.ts +1 -0
  128. package/dist/extensions/subagent-ext/index.ts +347 -0
  129. package/dist/extensions/subagent-shared/contract.d.ts +25 -0
  130. package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
  131. package/dist/extensions/subagent-shared/contract.js +2 -0
  132. package/dist/extensions/subagent-shared/contract.js.map +1 -0
  133. package/dist/extensions/subagent-shared/contract.ts +28 -0
  134. package/dist/extensions/subagent-shared/index.ts +4 -0
  135. package/dist/extensions/subagent-shared/render.ts +166 -0
  136. package/dist/extensions/subagent-shared/types.ts +35 -0
  137. package/dist/extensions/subagent-shared/utils.ts +112 -0
  138. package/dist/extensions/subagent-v2/contract.d.ts +2 -0
  139. package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
  140. package/dist/extensions/subagent-v2/contract.js +2 -0
  141. package/dist/extensions/subagent-v2/contract.js.map +1 -0
  142. package/dist/extensions/subagent-v2/contract.ts +1 -0
  143. package/dist/extensions/subagent-v2/index.ts +599 -0
  144. package/dist/extensions/todo-ext/contract.d.ts +27 -0
  145. package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
  146. package/dist/extensions/todo-ext/contract.js +2 -0
  147. package/dist/extensions/todo-ext/contract.js.map +1 -0
  148. package/dist/extensions/todo-ext/contract.ts +30 -0
  149. package/dist/extensions/todo-ext/index.ts +419 -0
  150. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  151. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  152. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  153. package/examples/extensions/sandbox/package-lock.json +2 -2
  154. package/examples/extensions/sandbox/package.json +1 -1
  155. package/examples/extensions/with-deps/package-lock.json +2 -2
  156. package/examples/extensions/with-deps/package.json +1 -1
  157. package/package.json +6 -5
@@ -0,0 +1,599 @@
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 AgentScope,
8
+ type ExtensionAPI,
9
+ RpcClient,
10
+ createTypedChannel,
11
+ discoverAgents,
12
+ } from "@dyyz1993/pi-coding-agent";
13
+ import { Text } from "@dyyz1993/pi-tui";
14
+ import { Type } from "typebox";
15
+ import {
16
+ accumulateUsage,
17
+ cleanupTempFiles,
18
+ type SingleResult,
19
+ type SubagentEventPayload,
20
+ type UsageStats,
21
+ formatUsageStats,
22
+ getFinalOutput,
23
+ makeUsage,
24
+ renderSingleResult,
25
+ writePromptToTempFile,
26
+ } from "../subagent-shared/index.js";
27
+ import type { SubagentChannelContract } from "../subagent-shared/contract.js";
28
+
29
+ const STEER_GRACE_MS = 30_000;
30
+
31
+ interface SubagentDetails {
32
+ agentScope: AgentScope;
33
+ projectAgentsDir: string | null;
34
+ result: SingleResult | null;
35
+ }
36
+
37
+ interface BackgroundTask {
38
+ taskId: string;
39
+ client: RpcClient;
40
+ sessionId: string;
41
+ sessionPath: string;
42
+ startedAt: number;
43
+ }
44
+
45
+ const backgroundTasks = new Map<string, BackgroundTask>();
46
+
47
+ function sessionDir(): string {
48
+ const dir = path.join(os.tmpdir(), "pi-subagent-v2-sessions");
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ return dir;
51
+ }
52
+
53
+ function subscribeToClient(
54
+ client: RpcClient,
55
+ result: SingleResult,
56
+ onEventData: (event: unknown, meta: Record<string, unknown>) => void,
57
+ meta: Record<string, unknown>,
58
+ onMessage?: () => void,
59
+ ): () => void {
60
+ return client.onEvent((event) => {
61
+ onEventData(event, meta);
62
+ if (event.type === "message_end" && event.message) {
63
+ const msg = event.message as Message;
64
+ result.messages.push(msg);
65
+ accumulateUsage(result, msg);
66
+ onMessage?.();
67
+ }
68
+ });
69
+ }
70
+
71
+ async function runWithTimeout(
72
+ client: RpcClient,
73
+ prompt: string,
74
+ timeoutMs: number,
75
+ signal?: AbortSignal,
76
+ ): Promise<"done" | "timeout" | "aborted"> {
77
+ const promptTimeout = timeoutMs - STEER_GRACE_MS;
78
+
79
+ const completionPromise = (async () => {
80
+ await client.prompt(prompt);
81
+ await client.waitForIdle(promptTimeout);
82
+ })();
83
+
84
+ const timeoutPromise = new Promise<"timeout">((resolve) => {
85
+ setTimeout(() => resolve("timeout"), promptTimeout);
86
+ });
87
+
88
+ const promises: Promise<"done" | "timeout" | "aborted">[] = [
89
+ completionPromise.then(() => "done" as const),
90
+ timeoutPromise,
91
+ ];
92
+
93
+ if (signal) {
94
+ if (signal.aborted) return "aborted";
95
+ promises.push(
96
+ new Promise<"aborted">((resolve) => {
97
+ signal.addEventListener("abort", () => resolve("aborted"), { once: true });
98
+ }),
99
+ );
100
+ }
101
+
102
+ return Promise.race(promises);
103
+ }
104
+
105
+ async function handleGracePeriod(client: RpcClient, result: SingleResult): Promise<void> {
106
+ await client.steer("Please summarize your findings and wrap up now. You have 30 seconds remaining.");
107
+ await Promise.race([
108
+ new Promise<void>((resolve) => {
109
+ const sub = client.onEvent((event) => {
110
+ if (event.type === "agent_end") {
111
+ sub();
112
+ resolve();
113
+ }
114
+ });
115
+ }),
116
+ new Promise<void>((resolve) => setTimeout(resolve, STEER_GRACE_MS)),
117
+ ]);
118
+ result.stopReason = "timeout";
119
+ result.exitCode = 1;
120
+ }
121
+
122
+ const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
123
+ description: 'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
124
+ default: "user",
125
+ });
126
+
127
+ const SubagentParams = Type.Object({
128
+ agent: Type.String({ description: "Name of the agent to invoke" }),
129
+ task: Type.String({ description: "Task instruction to delegate to the agent" }),
130
+ background: Type.Optional(Type.Boolean({ description: "Run in background mode. Default: false.", default: false })),
131
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 300.", default: 300 })),
132
+ cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
133
+ agentScope: Type.Optional(AgentScopeSchema),
134
+ confirmProjectAgents: Type.Optional(
135
+ Type.Boolean({ description: "Prompt before running project-local agents. Default: true.", default: true }),
136
+ ),
137
+ });
138
+
139
+ const SubagentResumeParams = Type.Object({
140
+ sessionId: Type.Optional(Type.String({ description: "Session ID from previous run" })),
141
+ sessionPath: Type.Optional(Type.String({ description: "Path to the saved session file" })),
142
+ instruction: Type.Optional(Type.String({ description: "Additional instruction for the resumed agent" })),
143
+ background: Type.Optional(Type.Boolean({ description: "Run in background mode. Default: false.", default: false })),
144
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 300.", default: 300 })),
145
+ });
146
+
147
+ export default function (pi: ExtensionAPI) {
148
+ const rawChannel = pi.registerChannel("subagent");
149
+ const channel = createTypedChannel<SubagentChannelContract>(rawChannel).server;
150
+
151
+ pi.registerTool({
152
+ name: "subagent",
153
+ label: "Subagent",
154
+ description: [
155
+ "Delegate a task to a specialized subagent with isolated context using RPC mode.",
156
+ "Agents are discovered from ~/.pi/agent/agents/ (user) and .pi/agents/ (project).",
157
+ 'Use agentScope to control discovery: "user" (default), "project", or "both".',
158
+ "Set background: true to run without blocking. The parent is notified on completion.",
159
+ "Sessions are persisted for later resume via subagent_resume.",
160
+ ].join(" "),
161
+ parameters: SubagentParams,
162
+
163
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
164
+ const agentScope: AgentScope = params.agentScope ?? "user";
165
+ const discovery = discoverAgents(ctx.cwd, agentScope);
166
+ const agents = discovery.agents;
167
+ const timeoutMs = Math.max((params.timeout ?? 300) * 1000, STEER_GRACE_MS + 10_000);
168
+ const background = params.background ?? false;
169
+
170
+ const details: SubagentDetails = {
171
+ agentScope,
172
+ projectAgentsDir: discovery.projectAgentsDir,
173
+ result: null,
174
+ };
175
+
176
+ if (
177
+ (agentScope === "project" || agentScope === "both") &&
178
+ (params.confirmProjectAgents ?? true) &&
179
+ ctx.hasUI
180
+ ) {
181
+ const agent = agents.find((a) => a.name === params.agent);
182
+ if (agent?.source === "project") {
183
+ const dir = discovery.projectAgentsDir ?? "(unknown)";
184
+ const ok = await ctx.ui.confirm(
185
+ "Run project-local agent?",
186
+ `Agent: ${agent.name}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
187
+ );
188
+ if (!ok)
189
+ return {
190
+ content: [{ type: "text", text: "Canceled: project-local agent not approved." }],
191
+ details,
192
+ };
193
+ }
194
+ }
195
+
196
+ const agent = agents.find((a) => a.name === params.agent);
197
+ if (!agent) {
198
+ const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
199
+ return {
200
+ content: [{ type: "text", text: `Unknown agent: "${params.agent}". Available agents: ${available}.` }],
201
+ details,
202
+ };
203
+ }
204
+
205
+ const sessionId = `subagent-v2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
206
+ const sessionPath = path.join(sessionDir(), `${sessionId}.json`);
207
+ const startedAt = Date.now();
208
+
209
+ let tmpPromptDir: string | null = null;
210
+ let tmpPromptPath: string | null = null;
211
+
212
+ const extraArgs: string[] = ["--session", sessionPath];
213
+ if (agent.systemPrompt.trim()) {
214
+ const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt, "pi-subagent-v2-");
215
+ tmpPromptDir = tmp.dir;
216
+ tmpPromptPath = tmp.filePath;
217
+ extraArgs.push("--append-system-prompt", tmpPromptPath);
218
+ }
219
+ if (agent.tools && agent.tools.length > 0) {
220
+ extraArgs.push("--tools", agent.tools.join(","));
221
+ }
222
+
223
+ const currentResult: SingleResult = {
224
+ agent: params.agent,
225
+ agentSource: agent.source,
226
+ task: params.task,
227
+ exitCode: 0,
228
+ messages: [],
229
+ stderr: "",
230
+ usage: makeUsage(),
231
+ model: agent.model,
232
+ sessionPath,
233
+ };
234
+
235
+ const emitUpdate = () => {
236
+ if (onUpdate) {
237
+ onUpdate({
238
+ content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
239
+ details: { ...details, result: { ...currentResult } },
240
+ });
241
+ }
242
+ };
243
+
244
+ const client = new RpcClient({
245
+ cwd: params.cwd ?? ctx.cwd,
246
+ provider: ctx.model?.provider || undefined,
247
+ model: agent.model,
248
+ args: extraArgs,
249
+ });
250
+
251
+ if (background) {
252
+ const taskId = `bg-${sessionId}`;
253
+
254
+ const startBg = async () => {
255
+ try {
256
+ await client.start();
257
+ if (agent.tools && agent.tools.length > 0) await client.setActiveTools(agent.tools);
258
+
259
+ const unsubscribe = subscribeToClient(
260
+ client,
261
+ currentResult,
262
+ (event, meta) => channel.emit("event", { event, ...meta } as SubagentEventPayload),
263
+ { sessionId, taskId },
264
+ );
265
+
266
+ const raceResult = await runWithTimeout(client, params.task, timeoutMs);
267
+ if (raceResult === "timeout") await handleGracePeriod(client, currentResult);
268
+ unsubscribe();
269
+
270
+ if (currentResult.exitCode === 0) {
271
+ currentResult.exitCode = currentResult.stopReason === "error" ? 1 : 0;
272
+ }
273
+ } catch (err) {
274
+ currentResult.exitCode = 1;
275
+ currentResult.errorMessage = err instanceof Error ? err.message : String(err);
276
+ currentResult.stderr = client.getStderr();
277
+ } finally {
278
+ await client.stop();
279
+ cleanupTempFiles(tmpPromptPath, tmpPromptDir, "subagent-v2");
280
+ backgroundTasks.delete(taskId);
281
+
282
+ const finalText = getFinalOutput(currentResult.messages) || "(no output)";
283
+ pi.appendEntry("subagent", {
284
+ toolCallId,
285
+ sessionId,
286
+ sessionPath,
287
+ description: params.agent,
288
+ instruction: params.task,
289
+ startedAt,
290
+ completedAt: Date.now(),
291
+ exitCode: currentResult.exitCode,
292
+ finalText,
293
+ });
294
+
295
+ const isCrash = currentResult.exitCode !== 0;
296
+ const summary = finalText.slice(0, 200);
297
+ const msg = isCrash
298
+ ? `子任务中断:${params.agent} — ${currentResult.errorMessage || summary}`
299
+ : `子任务完成:${params.agent} — ${summary}`;
300
+ try {
301
+ pi.sendUserMessage(msg, { deliverAs: "followUp" });
302
+ } catch (err) {
303
+ console.debug("[subagent-v2] followUp delivery failed:", err instanceof Error ? err.message : err);
304
+ pi.sendUserMessage(msg);
305
+ }
306
+ }
307
+ };
308
+
309
+ backgroundTasks.set(taskId, { taskId, client, sessionId, sessionPath, startedAt });
310
+ startBg();
311
+
312
+ return {
313
+ content: [{ type: "text", text: `Started background task: ${taskId}` }],
314
+ details: { agentScope, projectAgentsDir: discovery.projectAgentsDir, result: null },
315
+ };
316
+ }
317
+
318
+ let wasAborted = false;
319
+
320
+ try {
321
+ await client.start();
322
+ if (agent.tools && agent.tools.length > 0) await client.setActiveTools(agent.tools);
323
+
324
+ const unsubscribe = subscribeToClient(
325
+ client,
326
+ currentResult,
327
+ (event, meta) => channel.emit("event", { event, ...meta } as SubagentEventPayload),
328
+ { sessionId },
329
+ emitUpdate,
330
+ );
331
+
332
+ const raceResult = await runWithTimeout(client, params.task, timeoutMs, signal);
333
+
334
+ if (raceResult === "aborted") {
335
+ wasAborted = true;
336
+ await client.abort();
337
+ currentResult.stopReason = "aborted";
338
+ currentResult.exitCode = 1;
339
+ } else if (raceResult === "timeout") {
340
+ await handleGracePeriod(client, currentResult);
341
+ }
342
+
343
+ unsubscribe();
344
+ if (currentResult.exitCode === 0 && !wasAborted) {
345
+ currentResult.exitCode = currentResult.stopReason === "error" ? 1 : 0;
346
+ }
347
+ } catch (err) {
348
+ currentResult.exitCode = 1;
349
+ currentResult.errorMessage = err instanceof Error ? err.message : String(err);
350
+ currentResult.stderr = client.getStderr();
351
+ } finally {
352
+ await client.stop();
353
+ cleanupTempFiles(tmpPromptPath, tmpPromptDir, "subagent-v2");
354
+ }
355
+
356
+ const finalText = getFinalOutput(currentResult.messages) || "(no output)";
357
+
358
+ pi.appendEntry("subagent", {
359
+ toolCallId,
360
+ sessionId,
361
+ sessionPath,
362
+ description: params.agent,
363
+ instruction: params.task,
364
+ startedAt,
365
+ completedAt: Date.now(),
366
+ exitCode: currentResult.exitCode,
367
+ finalText,
368
+ });
369
+
370
+ const isError =
371
+ currentResult.exitCode !== 0 ||
372
+ currentResult.stopReason === "error" ||
373
+ currentResult.stopReason === "aborted" ||
374
+ currentResult.stopReason === "timeout";
375
+ if (isError) {
376
+ let errorMsg = currentResult.errorMessage || currentResult.stderr || finalText || "(no output)";
377
+ if (currentResult.sessionPath) {
378
+ errorMsg += `\n\nSession saved: ${currentResult.sessionPath}\nTo resume: use subagent_resume with sessionPath="${currentResult.sessionPath}"`;
379
+ }
380
+ return {
381
+ content: [{ type: "text", text: `Agent ${currentResult.stopReason || "failed"}: ${errorMsg}` }],
382
+ details: { ...details, result: currentResult },
383
+ isError: true,
384
+ };
385
+ }
386
+
387
+ return {
388
+ content: [{ type: "text", text: finalText }],
389
+ details: { ...details, result: currentResult },
390
+ };
391
+ },
392
+
393
+ renderCall(args, theme, _context) {
394
+ const scope: AgentScope = args.agentScope ?? "user";
395
+ const bg = args.background ? theme.fg("warning", " [bg]") : "";
396
+ const agentName = args.agent || "...";
397
+ const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
398
+ let text =
399
+ theme.fg("toolTitle", theme.bold("subagent ")) +
400
+ theme.fg("accent", agentName) +
401
+ theme.fg("muted", ` [${scope}]`) +
402
+ bg;
403
+ text += `\n ${theme.fg("dim", preview)}`;
404
+ return new Text(text, 0, 0);
405
+ },
406
+
407
+ renderResult(result, { expanded }, theme, _context) {
408
+ const details = result.details as SubagentDetails | undefined;
409
+ if (!details?.result) {
410
+ const text = result.content[0];
411
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
412
+ }
413
+ return renderSingleResult(details.result, expanded, theme);
414
+ },
415
+ });
416
+
417
+ pi.registerTool({
418
+ name: "subagent_resume",
419
+ label: "Subagent Resume",
420
+ description: "Resume a previously interrupted subagent session. The agent continues from where it left off.",
421
+ parameters: SubagentResumeParams,
422
+
423
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
424
+ const sPath = params.sessionPath;
425
+ if (!sPath) {
426
+ return {
427
+ content: [{ type: "text", text: "sessionPath is required." }],
428
+ details: { agentScope: "user" as AgentScope, projectAgentsDir: null, result: null },
429
+ };
430
+ }
431
+
432
+ if (!fs.existsSync(sPath)) {
433
+ return {
434
+ content: [{ type: "text", text: `Session file not found: ${sPath}` }],
435
+ details: { agentScope: "user" as AgentScope, projectAgentsDir: null, result: null },
436
+ };
437
+ }
438
+
439
+ const timeoutMs = Math.max((params.timeout ?? 300) * 1000, STEER_GRACE_MS + 10_000);
440
+ const background = params.background ?? false;
441
+
442
+ const currentResult: SingleResult = {
443
+ agent: "(resumed)",
444
+ agentSource: "unknown",
445
+ task: params.instruction ?? "(resume)",
446
+ exitCode: 0,
447
+ messages: [],
448
+ stderr: "",
449
+ usage: makeUsage(),
450
+ sessionPath: sPath,
451
+ };
452
+
453
+ const details: SubagentDetails = { agentScope: "user", projectAgentsDir: null, result: null };
454
+
455
+ const emitUpdate = () => {
456
+ if (onUpdate) {
457
+ onUpdate({
458
+ content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(resuming...)" }],
459
+ details: { ...details, result: { ...currentResult } },
460
+ });
461
+ }
462
+ };
463
+
464
+ const client = new RpcClient({
465
+ cwd: ctx.cwd,
466
+ provider: ctx.model?.provider || undefined,
467
+ args: ["--session", sPath, "-c"],
468
+ });
469
+
470
+ const sessionId = params.sessionId ?? `resume-${Date.now()}`;
471
+ const resumePrompt = params.instruction ?? "Please continue from where you left off.";
472
+
473
+ if (background) {
474
+ const taskId = `bg-resume-${sessionId}`;
475
+ const startedAt = Date.now();
476
+
477
+ const startBg = async () => {
478
+ try {
479
+ await client.start();
480
+ const unsubscribe = subscribeToClient(
481
+ client,
482
+ currentResult,
483
+ (event, meta) => channel.emit("event", { event, ...meta } as SubagentEventPayload),
484
+ { sessionId, taskId },
485
+ );
486
+
487
+ const raceResult = await runWithTimeout(client, resumePrompt, timeoutMs);
488
+ if (raceResult === "timeout") await handleGracePeriod(client, currentResult);
489
+ unsubscribe();
490
+
491
+ if (currentResult.exitCode === 0) {
492
+ currentResult.exitCode = currentResult.stopReason === "error" ? 1 : 0;
493
+ }
494
+ } catch (err) {
495
+ currentResult.exitCode = 1;
496
+ currentResult.errorMessage = err instanceof Error ? err.message : String(err);
497
+ currentResult.stderr = client.getStderr();
498
+ } finally {
499
+ await client.stop();
500
+ backgroundTasks.delete(taskId);
501
+
502
+ const finalText = getFinalOutput(currentResult.messages) || "(no output)";
503
+ pi.appendEntry("subagent", {
504
+ toolCallId,
505
+ sessionId,
506
+ sessionPath: sPath,
507
+ description: "(resumed)",
508
+ instruction: params.instruction ?? "(resume)",
509
+ startedAt,
510
+ completedAt: Date.now(),
511
+ exitCode: currentResult.exitCode,
512
+ finalText,
513
+ });
514
+
515
+ const isCrash = currentResult.exitCode !== 0;
516
+ const summary = finalText.slice(0, 200);
517
+ const msg = isCrash
518
+ ? `子任务中断:(resumed) — ${currentResult.errorMessage || summary}`
519
+ : `子任务完成:(resumed) — ${summary}`;
520
+ try {
521
+ pi.sendUserMessage(msg, { deliverAs: "followUp" });
522
+ } catch (err) {
523
+ console.debug("[subagent-v2] resumed followUp delivery failed:", err instanceof Error ? err.message : err);
524
+ pi.sendUserMessage(msg);
525
+ }
526
+ }
527
+ };
528
+
529
+ backgroundTasks.set(taskId, { taskId, client, sessionId, sessionPath: sPath, startedAt });
530
+ startBg();
531
+
532
+ return {
533
+ content: [{ type: "text", text: `Started background resume task: ${taskId}` }],
534
+ details: { agentScope: "user", projectAgentsDir: null, result: null },
535
+ };
536
+ }
537
+
538
+ let wasAborted = false;
539
+
540
+ try {
541
+ await client.start();
542
+ const unsubscribe = subscribeToClient(
543
+ client,
544
+ currentResult,
545
+ (event, meta) => channel.emit("event", { event, ...meta } as SubagentEventPayload),
546
+ { sessionId },
547
+ emitUpdate,
548
+ );
549
+
550
+ const raceResult = await runWithTimeout(client, resumePrompt, timeoutMs, signal);
551
+
552
+ if (raceResult === "aborted") {
553
+ wasAborted = true;
554
+ await client.abort();
555
+ currentResult.stopReason = "aborted";
556
+ currentResult.exitCode = 1;
557
+ } else if (raceResult === "timeout") {
558
+ await handleGracePeriod(client, currentResult);
559
+ }
560
+
561
+ unsubscribe();
562
+ if (currentResult.exitCode === 0 && !wasAborted) {
563
+ currentResult.exitCode = currentResult.stopReason === "error" ? 1 : 0;
564
+ }
565
+ } catch (err) {
566
+ currentResult.exitCode = 1;
567
+ currentResult.errorMessage = err instanceof Error ? err.message : String(err);
568
+ currentResult.stderr = client.getStderr();
569
+ } finally {
570
+ await client.stop();
571
+ }
572
+
573
+ let finalText = getFinalOutput(currentResult.messages) || "(no output)";
574
+ if (currentResult.exitCode !== 0 && currentResult.sessionPath) {
575
+ finalText += `\n\nSession saved: ${currentResult.sessionPath}\nTo resume again: use subagent_resume with sessionPath="${currentResult.sessionPath}"`;
576
+ }
577
+
578
+ return {
579
+ content: [{ type: "text", text: finalText }],
580
+ details: { ...details, result: currentResult },
581
+ };
582
+ },
583
+
584
+ renderCall(args, theme, _context) {
585
+ const bg = args.background ? theme.fg("warning", " [bg]") : "";
586
+ const sPath = args.sessionPath ?? args.sessionId ?? "...";
587
+ return new Text(theme.fg("toolTitle", theme.bold("subagent_resume ")) + theme.fg("accent", sPath) + bg, 0, 0);
588
+ },
589
+
590
+ renderResult(result, { expanded }, theme, _context) {
591
+ const details = result.details as SubagentDetails | undefined;
592
+ if (!details?.result) {
593
+ const text = result.content[0];
594
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
595
+ }
596
+ return renderSingleResult(details.result, expanded, theme);
597
+ },
598
+ });
599
+ }
@@ -0,0 +1,27 @@
1
+ import type { ChannelContract } from "@dyyz1993/pi-coding-agent";
2
+ export declare const TODO_CHANNEL_NAME = "todo";
3
+ export interface TodoItem {
4
+ id: number;
5
+ text: string;
6
+ done: boolean;
7
+ deleted?: boolean;
8
+ priority?: "high" | "medium" | "low";
9
+ }
10
+ export interface TodoChannelEvent {
11
+ action: string;
12
+ todos: TodoItem[];
13
+ timestamp: number;
14
+ }
15
+ export interface TodoChannelContract extends ChannelContract {
16
+ methods?: Record<string, never>;
17
+ events: {
18
+ restored: TodoChannelEvent;
19
+ list: TodoChannelEvent;
20
+ add: TodoChannelEvent;
21
+ toggle: TodoChannelEvent;
22
+ remove: TodoChannelEvent;
23
+ clear: TodoChannelEvent;
24
+ error: TodoChannelEvent;
25
+ };
26
+ }
27
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEjE,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAExC,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAoB,SAAQ,eAAe;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAChC,MAAM,EAAE;QACP,QAAQ,EAAE,gBAAgB,CAAC;QAC3B,IAAI,EAAE,gBAAgB,CAAC;QACvB,GAAG,EAAE,gBAAgB,CAAC;QACtB,MAAM,EAAE,gBAAgB,CAAC;QACzB,MAAM,EAAE,gBAAgB,CAAC;QACzB,KAAK,EAAE,gBAAgB,CAAC;QACxB,KAAK,EAAE,gBAAgB,CAAC;KACxB,CAAC;CACF","sourcesContent":["import type { ChannelContract } from \"@dyyz1993/pi-coding-agent\";\n\nexport const TODO_CHANNEL_NAME = \"todo\";\n\nexport interface TodoItem {\n\tid: number;\n\ttext: string;\n\tdone: boolean;\n\tdeleted?: boolean;\n\tpriority?: \"high\" | \"medium\" | \"low\";\n}\n\nexport interface TodoChannelEvent {\n\taction: string;\n\ttodos: TodoItem[];\n\ttimestamp: number;\n}\n\nexport interface TodoChannelContract extends ChannelContract {\n\tmethods?: Record<string, never>;\n\tevents: {\n\t\trestored: TodoChannelEvent;\n\t\tlist: TodoChannelEvent;\n\t\tadd: TodoChannelEvent;\n\t\ttoggle: TodoChannelEvent;\n\t\tremove: TodoChannelEvent;\n\t\tclear: TodoChannelEvent;\n\t\terror: TodoChannelEvent;\n\t};\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export const TODO_CHANNEL_NAME = "todo";
2
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["contract.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC","sourcesContent":["import type { ChannelContract } from \"@dyyz1993/pi-coding-agent\";\n\nexport const TODO_CHANNEL_NAME = \"todo\";\n\nexport interface TodoItem {\n\tid: number;\n\ttext: string;\n\tdone: boolean;\n\tdeleted?: boolean;\n\tpriority?: \"high\" | \"medium\" | \"low\";\n}\n\nexport interface TodoChannelEvent {\n\taction: string;\n\ttodos: TodoItem[];\n\ttimestamp: number;\n}\n\nexport interface TodoChannelContract extends ChannelContract {\n\tmethods?: Record<string, never>;\n\tevents: {\n\t\trestored: TodoChannelEvent;\n\t\tlist: TodoChannelEvent;\n\t\tadd: TodoChannelEvent;\n\t\ttoggle: TodoChannelEvent;\n\t\tremove: TodoChannelEvent;\n\t\tclear: TodoChannelEvent;\n\t\terror: TodoChannelEvent;\n\t};\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import type { ChannelContract } from "@dyyz1993/pi-coding-agent";
2
+
3
+ export const TODO_CHANNEL_NAME = "todo";
4
+
5
+ export interface TodoItem {
6
+ id: number;
7
+ text: string;
8
+ done: boolean;
9
+ deleted?: boolean;
10
+ priority?: "high" | "medium" | "low";
11
+ }
12
+
13
+ export interface TodoChannelEvent {
14
+ action: string;
15
+ todos: TodoItem[];
16
+ timestamp: number;
17
+ }
18
+
19
+ export interface TodoChannelContract extends ChannelContract {
20
+ methods?: Record<string, never>;
21
+ events: {
22
+ restored: TodoChannelEvent;
23
+ list: TodoChannelEvent;
24
+ add: TodoChannelEvent;
25
+ toggle: TodoChannelEvent;
26
+ remove: TodoChannelEvent;
27
+ clear: TodoChannelEvent;
28
+ error: TodoChannelEvent;
29
+ };
30
+ }