@oh-my-pi/pi-coding-agent 3.20.1 → 3.24.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/CHANGELOG.md +107 -8
  2. package/docs/custom-tools.md +3 -3
  3. package/docs/extensions.md +226 -220
  4. package/docs/hooks.md +2 -2
  5. package/docs/sdk.md +50 -53
  6. package/examples/custom-tools/README.md +2 -17
  7. package/examples/extensions/README.md +76 -74
  8. package/examples/extensions/todo.ts +2 -5
  9. package/examples/hooks/custom-compaction.ts +2 -4
  10. package/examples/hooks/handoff.ts +1 -1
  11. package/examples/hooks/qna.ts +1 -1
  12. package/examples/sdk/02-custom-model.ts +1 -1
  13. package/examples/sdk/README.md +7 -11
  14. package/package.json +6 -6
  15. package/src/cli/args.ts +9 -6
  16. package/src/cli/file-processor.ts +1 -1
  17. package/src/cli/list-models.ts +1 -1
  18. package/src/core/agent-session.ts +16 -5
  19. package/src/core/auth-storage.ts +1 -1
  20. package/src/core/compaction/branch-summarization.ts +2 -2
  21. package/src/core/compaction/compaction.ts +2 -2
  22. package/src/core/compaction/utils.ts +1 -1
  23. package/src/core/custom-tools/types.ts +1 -1
  24. package/src/core/custom-tools/wrapper.ts +0 -1
  25. package/src/core/extensions/index.ts +1 -6
  26. package/src/core/extensions/runner.ts +1 -1
  27. package/src/core/extensions/types.ts +1 -1
  28. package/src/core/extensions/wrapper.ts +1 -8
  29. package/src/core/file-mentions.ts +5 -8
  30. package/src/core/hooks/runner.ts +2 -2
  31. package/src/core/hooks/types.ts +1 -1
  32. package/src/core/messages.ts +1 -1
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/model-resolver.ts +1 -1
  35. package/src/core/sdk.ts +64 -105
  36. package/src/core/session-manager.ts +18 -22
  37. package/src/core/settings-manager.ts +66 -1
  38. package/src/core/slash-commands.ts +12 -5
  39. package/src/core/system-prompt.ts +49 -36
  40. package/src/core/title-generator.ts +2 -2
  41. package/src/core/tools/ask.ts +98 -4
  42. package/src/core/tools/bash-interceptor.ts +11 -4
  43. package/src/core/tools/bash.ts +121 -5
  44. package/src/core/tools/context.ts +7 -0
  45. package/src/core/tools/edit-diff.ts +73 -24
  46. package/src/core/tools/edit.ts +221 -34
  47. package/src/core/tools/exa/render.ts +4 -16
  48. package/src/core/tools/find.ts +149 -5
  49. package/src/core/tools/gemini-image.ts +279 -56
  50. package/src/core/tools/git.ts +17 -3
  51. package/src/core/tools/grep.ts +185 -5
  52. package/src/core/tools/index.test.ts +180 -0
  53. package/src/core/tools/index.ts +96 -242
  54. package/src/core/tools/ls.ts +133 -5
  55. package/src/core/tools/lsp/index.ts +32 -29
  56. package/src/core/tools/lsp/render.ts +21 -22
  57. package/src/core/tools/notebook.ts +112 -4
  58. package/src/core/tools/output.ts +175 -15
  59. package/src/core/tools/read.ts +127 -25
  60. package/src/core/tools/render-utils.ts +241 -0
  61. package/src/core/tools/renderers.ts +40 -828
  62. package/src/core/tools/review.ts +26 -25
  63. package/src/core/tools/rulebook.ts +11 -3
  64. package/src/core/tools/task/agents.ts +28 -7
  65. package/src/core/tools/task/discovery.ts +0 -6
  66. package/src/core/tools/task/executor.ts +264 -254
  67. package/src/core/tools/task/index.ts +48 -208
  68. package/src/core/tools/task/render.ts +26 -11
  69. package/src/core/tools/task/types.ts +7 -12
  70. package/src/core/tools/task/worker-protocol.ts +17 -0
  71. package/src/core/tools/task/worker.ts +238 -0
  72. package/src/core/tools/truncate.ts +27 -1
  73. package/src/core/tools/web-fetch.ts +25 -49
  74. package/src/core/tools/web-search/index.ts +132 -46
  75. package/src/core/tools/web-search/providers/anthropic.ts +7 -2
  76. package/src/core/tools/web-search/providers/exa.ts +2 -1
  77. package/src/core/tools/web-search/providers/perplexity.ts +6 -1
  78. package/src/core/tools/web-search/render.ts +6 -4
  79. package/src/core/tools/web-search/types.ts +13 -0
  80. package/src/core/tools/write.ts +96 -14
  81. package/src/core/voice.ts +1 -1
  82. package/src/discovery/helpers.test.ts +1 -1
  83. package/src/index.ts +5 -16
  84. package/src/main.ts +5 -5
  85. package/src/modes/interactive/components/assistant-message.ts +1 -1
  86. package/src/modes/interactive/components/custom-message.ts +1 -1
  87. package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
  88. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  89. package/src/modes/interactive/components/footer.ts +1 -1
  90. package/src/modes/interactive/components/hook-message.ts +1 -1
  91. package/src/modes/interactive/components/model-selector.ts +1 -1
  92. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  93. package/src/modes/interactive/components/settings-defs.ts +49 -0
  94. package/src/modes/interactive/components/status-line.ts +1 -1
  95. package/src/modes/interactive/components/tool-execution.ts +93 -538
  96. package/src/modes/interactive/interactive-mode.ts +19 -7
  97. package/src/modes/interactive/theme/theme.ts +4 -4
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +1 -1
  100. package/src/modes/rpc/rpc-types.ts +1 -1
  101. package/src/prompts/system-prompt.md +4 -0
  102. package/src/prompts/task.md +0 -7
  103. package/src/prompts/tools/gemini-image.md +5 -1
  104. package/src/prompts/tools/output.md +6 -2
  105. package/src/prompts/tools/task.md +68 -0
  106. package/src/prompts/tools/web-fetch.md +1 -0
  107. package/src/prompts/tools/web-search.md +2 -0
  108. package/src/utils/image-convert.ts +8 -2
  109. package/src/utils/image-magick.ts +247 -0
  110. package/src/utils/image-resize.ts +53 -13
  111. package/examples/custom-tools/question/index.ts +0 -84
  112. package/examples/custom-tools/subagent/README.md +0 -172
  113. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  114. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  115. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  116. package/examples/custom-tools/subagent/agents.ts +0 -156
  117. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  118. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  119. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  120. package/examples/custom-tools/subagent/index.ts +0 -1002
  121. package/examples/sdk/05-tools.ts +0 -94
  122. package/examples/sdk/12-full-control.ts +0 -95
  123. package/src/prompts/browser.md +0 -71
@@ -1,28 +1,27 @@
1
1
  /**
2
- * Subprocess execution for subagents.
2
+ * Worker execution for subagents.
3
3
  *
4
- * Spawns `omp` in JSON mode to execute tasks with isolated context.
5
- * Parses JSON events for progress tracking.
4
+ * Runs each subagent in a Bun Worker and forwards AgentEvents for progress tracking.
6
5
  */
7
6
 
8
- import { existsSync, unlinkSync, writeFileSync } from "node:fs";
9
- import { tmpdir } from "node:os";
10
- import * as path from "node:path";
7
+ import { writeFileSync } from "node:fs";
8
+ import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
9
+ import type { EventBus } from "../../event-bus";
11
10
  import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
12
11
  import { resolveModelPattern } from "./model-resolver";
13
- import { resolveOmpCommand } from "./omp-command";
14
12
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
15
13
  import {
16
14
  type AgentDefinition,
17
15
  type AgentProgress,
18
16
  MAX_OUTPUT_BYTES,
19
17
  MAX_OUTPUT_LINES,
20
- OMP_BLOCKED_AGENT_ENV,
21
- OMP_SPAWNS_ENV,
22
18
  type SingleResult,
19
+ TASK_SUBAGENT_EVENT_CHANNEL,
20
+ TASK_SUBAGENT_PROGRESS_CHANNEL,
23
21
  } from "./types";
22
+ import type { SubagentWorkerRequest, SubagentWorkerResponse } from "./worker-protocol";
24
23
 
25
- /** Options for subprocess execution */
24
+ /** Options for worker execution */
26
25
  export interface ExecutorOptions {
27
26
  cwd: string;
28
27
  agent: AgentDefinition;
@@ -36,6 +35,7 @@ export interface ExecutorOptions {
36
35
  sessionFile?: string | null;
37
36
  persistArtifacts?: boolean;
38
37
  artifactsDir?: string;
38
+ eventBus?: EventBus;
39
39
  }
40
40
 
41
41
  /**
@@ -127,7 +127,7 @@ function getUsageTokens(usage: unknown): number {
127
127
  }
128
128
 
129
129
  /**
130
- * Run a single agent as a subprocess.
130
+ * Run a single agent in a worker.
131
131
  */
132
132
  export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
133
133
  const { cwd, agent, task, index, context, modelOverride, signal, onProgress } = options;
@@ -168,33 +168,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
168
168
  };
169
169
  }
170
170
 
171
- // Write system prompt to temp file
172
- const tempDir = tmpdir();
173
- const promptFile = path.join(
174
- tempDir,
175
- `omp-agent-${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.md`,
176
- );
177
-
178
- try {
179
- writeFileSync(promptFile, agent.systemPrompt, "utf-8");
180
- } catch (err) {
181
- return {
182
- index,
183
- agent: agent.name,
184
- agentSource: agent.source,
185
- task,
186
- description: options.description,
187
- exitCode: 1,
188
- output: "",
189
- stderr: `Failed to write prompt file: ${err}`,
190
- truncated: false,
191
- durationMs: Date.now() - startTime,
192
- tokens: 0,
193
- modelOverride,
194
- error: `Failed to write prompt file: ${err}`,
195
- };
196
- }
197
-
198
171
  // Build full task with context
199
172
  const fullTask = context ? `${context}\n\n${task}` : task;
200
173
 
@@ -215,182 +188,197 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
215
188
  }
216
189
  }
217
190
 
218
- // Build args
219
- const args: string[] = ["--mode", "json", "--non-interactive"];
220
-
221
- // Add system prompt
222
- args.push("--append-system-prompt", promptFile);
223
-
224
191
  // Add tools if specified
192
+ let toolNames: string[] | undefined;
225
193
  if (agent.tools && agent.tools.length > 0) {
226
- let toolList = agent.tools;
194
+ toolNames = agent.tools;
227
195
  // Auto-include task tool if spawns defined but task not in tools
228
- if (agent.spawns !== undefined && !toolList.includes("task")) {
229
- toolList = [...toolList, "task"];
196
+ if (agent.spawns !== undefined && !toolNames.includes("task")) {
197
+ toolNames = [...toolNames, "task"];
230
198
  }
231
- args.push("--tools", toolList.join(","));
232
199
  }
233
200
 
234
201
  // Resolve and add model
235
202
  const resolvedModel = resolveModelPattern(modelOverride || agent.model);
236
- if (resolvedModel) {
237
- args.push("--model", resolvedModel);
238
- }
239
-
240
- // Add session options - use subtask-specific session file for real-time streaming
241
- if (subtaskSessionFile) {
242
- args.push("--session", subtaskSessionFile);
243
- } else if (options.sessionFile) {
244
- args.push("--session", options.sessionFile);
245
- } else {
246
- args.push("--no-session");
247
- }
248
-
249
- // Add task as prompt
250
- args.push("--prompt", fullTask);
251
-
252
- // Set up environment - block same-agent recursion unless explicitly recursive
253
- const env = { ...process.env };
254
- if (!agent.recursive) {
255
- env[OMP_BLOCKED_AGENT_ENV] = agent.name;
256
- }
257
-
258
- // Propagate spawn restrictions to subprocess
259
- if (agent.spawns === undefined) {
260
- env[OMP_SPAWNS_ENV] = ""; // No spawns = deny all
261
- } else if (agent.spawns === "*") {
262
- env[OMP_SPAWNS_ENV] = "*";
263
- } else {
264
- env[OMP_SPAWNS_ENV] = agent.spawns.join(",");
265
- }
203
+ const sessionFile = subtaskSessionFile ?? options.sessionFile ?? null;
204
+ const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
266
205
 
267
- // Spawn subprocess
268
- const ompCommand = resolveOmpCommand();
269
- const proc = Bun.spawn([ompCommand.cmd, ...ompCommand.args, ...args], {
270
- cwd,
271
- stdin: "ignore",
272
- stdout: "pipe",
273
- stderr: "pipe",
274
- env,
275
- });
206
+ const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
276
207
 
277
208
  let output = "";
278
209
  let stderr = "";
279
210
  let finalOutput = "";
280
211
  let resolved = false;
281
212
  let pendingTermination = false; // Set when shouldTerminate fires, wait for message_end
282
- const jsonlEvents: string[] = [];
213
+
214
+ // Accumulate usage incrementally from message_end events (no memory for streaming events)
215
+ const accumulatedUsage = {
216
+ input: 0,
217
+ output: 0,
218
+ cacheRead: 0,
219
+ cacheWrite: 0,
220
+ totalTokens: 0,
221
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
222
+ };
223
+ let hasUsage = false;
224
+
225
+ let abortSent = false;
226
+ const requestAbort = () => {
227
+ if (abortSent) return;
228
+ abortSent = true;
229
+ const abortMessage: SubagentWorkerRequest = { type: "abort" };
230
+ worker.postMessage(abortMessage);
231
+ setTimeout(() => {
232
+ if (!resolved) {
233
+ worker.terminate();
234
+ }
235
+ }, 2000);
236
+ };
283
237
 
284
238
  // Handle abort signal
285
239
  const onAbort = () => {
286
- if (!resolved) {
287
- proc.kill(15); // SIGTERM
288
- }
240
+ if (!resolved) requestAbort();
289
241
  };
290
242
  if (signal) {
291
243
  signal.addEventListener("abort", onAbort, { once: true });
292
244
  }
293
245
 
294
- // Parse JSON events from stdout
295
- const reader = proc.stdout.getReader();
296
- const decoder = new TextDecoder();
297
- let buffer = "";
246
+ const emitProgress = () => {
247
+ progress.durationMs = Date.now() - startTime;
248
+ onProgress?.({ ...progress });
249
+ if (options.eventBus) {
250
+ options.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
251
+ index,
252
+ agent: agent.name,
253
+ agentSource: agent.source,
254
+ task,
255
+ progress: { ...progress },
256
+ });
257
+ }
258
+ };
298
259
 
299
- const processLine = (line: string) => {
260
+ const getMessageContent = (message: unknown): unknown => {
261
+ if (message && typeof message === "object" && "content" in message) {
262
+ return (message as { content?: unknown }).content;
263
+ }
264
+ return undefined;
265
+ };
266
+
267
+ const getMessageUsage = (message: unknown): unknown => {
268
+ if (message && typeof message === "object" && "usage" in message) {
269
+ return (message as { usage?: unknown }).usage;
270
+ }
271
+ return undefined;
272
+ };
273
+
274
+ const processEvent = (event: AgentEvent) => {
300
275
  if (resolved) return;
301
276
 
302
- try {
303
- const event = JSON.parse(line);
304
- jsonlEvents.push(line);
305
- const now = Date.now();
306
-
307
- switch (event.type) {
308
- case "tool_execution_start":
309
- progress.toolCount++;
310
- progress.currentTool = event.toolName;
311
- progress.currentToolArgs = extractToolArgsPreview(event.toolArgs || event.args || {});
312
- progress.currentToolStartMs = now;
313
- break;
314
-
315
- case "tool_execution_end": {
316
- if (progress.currentTool) {
317
- progress.recentTools.unshift({
318
- tool: progress.currentTool,
319
- args: progress.currentToolArgs || "",
320
- endMs: now,
277
+ if (options.eventBus) {
278
+ options.eventBus.emit(TASK_SUBAGENT_EVENT_CHANNEL, {
279
+ index,
280
+ agent: agent.name,
281
+ agentSource: agent.source,
282
+ task,
283
+ event,
284
+ });
285
+ }
286
+
287
+ const now = Date.now();
288
+
289
+ switch (event.type) {
290
+ case "tool_execution_start":
291
+ progress.toolCount++;
292
+ progress.currentTool = event.toolName;
293
+ progress.currentToolArgs = extractToolArgsPreview(
294
+ (event as { toolArgs?: Record<string, unknown> }).toolArgs || event.args || {},
295
+ );
296
+ progress.currentToolStartMs = now;
297
+ break;
298
+
299
+ case "tool_execution_end": {
300
+ if (progress.currentTool) {
301
+ progress.recentTools.unshift({
302
+ tool: progress.currentTool,
303
+ args: progress.currentToolArgs || "",
304
+ endMs: now,
305
+ });
306
+ // Keep only last 5
307
+ if (progress.recentTools.length > 5) {
308
+ progress.recentTools.pop();
309
+ }
310
+ }
311
+ progress.currentTool = undefined;
312
+ progress.currentToolArgs = undefined;
313
+ progress.currentToolStartMs = undefined;
314
+
315
+ // Check for registered subagent tool handler
316
+ const handler = subprocessToolRegistry.getHandler(event.toolName);
317
+ const eventArgs = (event as { args?: Record<string, unknown> }).args ?? {};
318
+ if (handler) {
319
+ // Extract data using handler
320
+ if (handler.extractData) {
321
+ const data = handler.extractData({
322
+ toolName: event.toolName,
323
+ toolCallId: event.toolCallId,
324
+ args: eventArgs,
325
+ result: event.result,
326
+ isError: event.isError,
321
327
  });
322
- // Keep only last 5
323
- if (progress.recentTools.length > 5) {
324
- progress.recentTools.pop();
328
+ if (data !== undefined) {
329
+ progress.extractedToolData = progress.extractedToolData || {};
330
+ progress.extractedToolData[event.toolName] = progress.extractedToolData[event.toolName] || [];
331
+ progress.extractedToolData[event.toolName].push(data);
325
332
  }
326
333
  }
327
- progress.currentTool = undefined;
328
- progress.currentToolArgs = undefined;
329
- progress.currentToolStartMs = undefined;
330
-
331
- // Check for registered subprocess tool handler
332
- const handler = subprocessToolRegistry.getHandler(event.toolName);
333
- if (handler) {
334
- // Extract data using handler
335
- if (handler.extractData) {
336
- const data = handler.extractData({
337
- toolName: event.toolName,
338
- toolCallId: event.toolCallId,
339
- args: event.args,
340
- result: event.result,
341
- isError: event.isError,
342
- });
343
- if (data !== undefined) {
344
- progress.extractedToolData = progress.extractedToolData || {};
345
- progress.extractedToolData[event.toolName] = progress.extractedToolData[event.toolName] || [];
346
- progress.extractedToolData[event.toolName].push(data);
347
- }
348
- }
349
334
 
350
- // Check if handler wants to terminate subprocess
351
- if (
352
- handler.shouldTerminate?.({
353
- toolName: event.toolName,
354
- toolCallId: event.toolCallId,
355
- args: event.args,
356
- result: event.result,
357
- isError: event.isError,
358
- })
359
- ) {
360
- // Don't kill immediately - wait for message_end to get token counts
361
- pendingTermination = true;
362
- // Safety timeout in case message_end never arrives
363
- setTimeout(() => {
364
- if (!resolved) {
365
- resolved = true;
366
- proc.kill(15); // SIGTERM
367
- }
368
- }, 2000);
369
- }
335
+ // Check if handler wants to terminate worker
336
+ if (
337
+ handler.shouldTerminate?.({
338
+ toolName: event.toolName,
339
+ toolCallId: event.toolCallId,
340
+ args: eventArgs,
341
+ result: event.result,
342
+ isError: event.isError,
343
+ })
344
+ ) {
345
+ // Don't terminate immediately - wait for message_end to get token counts
346
+ pendingTermination = true;
347
+ // Safety timeout in case message_end never arrives
348
+ setTimeout(() => {
349
+ if (!resolved) {
350
+ requestAbort();
351
+ }
352
+ }, 2000);
370
353
  }
371
- break;
372
354
  }
355
+ break;
356
+ }
373
357
 
374
- case "message_update": {
375
- // Extract text for progress display only (replace, don't accumulate)
376
- const updateContent = event.message?.content || event.content;
377
- if (updateContent && Array.isArray(updateContent)) {
378
- const allText: string[] = [];
379
- for (const block of updateContent) {
380
- if (block.type === "text" && block.text) {
381
- const lines = block.text.split("\n").filter((l: string) => l.trim());
382
- allText.push(...lines);
383
- }
358
+ case "message_update": {
359
+ // Extract text for progress display only (replace, don't accumulate)
360
+ const updateContent =
361
+ getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
362
+ if (updateContent && Array.isArray(updateContent)) {
363
+ const allText: string[] = [];
364
+ for (const block of updateContent) {
365
+ if (block.type === "text" && block.text) {
366
+ const lines = block.text.split("\n").filter((l: string) => l.trim());
367
+ allText.push(...lines);
384
368
  }
385
- // Show last 8 lines from current state (not accumulated)
386
- progress.recentOutput = allText.slice(-8).reverse();
387
369
  }
388
- break;
370
+ // Show last 8 lines from current state (not accumulated)
371
+ progress.recentOutput = allText.slice(-8).reverse();
389
372
  }
373
+ break;
374
+ }
390
375
 
391
- case "message_end": {
392
- // Extract final text content from completed message
393
- const messageContent = event.message?.content || event.content;
376
+ case "message_end": {
377
+ // Extract text from assistant and toolResult messages (not user prompts)
378
+ const role = event.message?.role;
379
+ if (role === "assistant") {
380
+ const messageContent =
381
+ getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
394
382
  if (messageContent && Array.isArray(messageContent)) {
395
383
  for (const block of messageContent) {
396
384
  if (block.type === "text" && block.text) {
@@ -398,97 +386,120 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
398
386
  }
399
387
  }
400
388
  }
401
- // Extract usage (prefer message.usage, fallback to event.usage)
402
- const messageUsage = event.message?.usage || event.usage;
403
- if (messageUsage) {
404
- // Accumulate tokens across messages (not overwrite)
405
- progress.tokens += getUsageTokens(messageUsage);
406
- }
407
- // If pending termination, now we have tokens - terminate
408
- if (pendingTermination && !resolved) {
409
- resolved = true;
410
- proc.kill(15); // SIGTERM
389
+ }
390
+ // Extract and accumulate usage (prefer message.usage, fallback to event.usage)
391
+ const messageUsage = getMessageUsage(event.message) || (event as AgentEvent & { usage?: unknown }).usage;
392
+ if (messageUsage && typeof messageUsage === "object") {
393
+ // Only count assistant messages (not tool results, etc.)
394
+ if (
395
+ role === "assistant" &&
396
+ event.message?.stopReason !== "aborted" &&
397
+ event.message?.stopReason !== "error"
398
+ ) {
399
+ const usageRecord = messageUsage as Record<string, number | undefined>;
400
+ const costRecord = (messageUsage as { cost?: Record<string, number | undefined> }).cost;
401
+ hasUsage = true;
402
+ accumulatedUsage.input += usageRecord.input ?? 0;
403
+ accumulatedUsage.output += usageRecord.output ?? 0;
404
+ accumulatedUsage.cacheRead += usageRecord.cacheRead ?? 0;
405
+ accumulatedUsage.cacheWrite += usageRecord.cacheWrite ?? 0;
406
+ accumulatedUsage.totalTokens += usageRecord.totalTokens ?? 0;
407
+ if (costRecord) {
408
+ accumulatedUsage.cost.input += costRecord.input ?? 0;
409
+ accumulatedUsage.cost.output += costRecord.output ?? 0;
410
+ accumulatedUsage.cost.cacheRead += costRecord.cacheRead ?? 0;
411
+ accumulatedUsage.cost.cacheWrite += costRecord.cacheWrite ?? 0;
412
+ accumulatedUsage.cost.total += costRecord.total ?? 0;
413
+ }
411
414
  }
412
- break;
415
+ // Accumulate tokens for progress display
416
+ progress.tokens += getUsageTokens(messageUsage);
417
+ }
418
+ // If pending termination, now we have tokens - terminate
419
+ if (pendingTermination && !resolved) {
420
+ requestAbort();
413
421
  }
422
+ break;
423
+ }
414
424
 
415
- case "agent_end":
416
- // Extract final content from messages array
417
- if (event.messages && Array.isArray(event.messages)) {
418
- for (const msg of event.messages) {
419
- if (msg.content && Array.isArray(msg.content)) {
420
- for (const block of msg.content) {
421
- if (block.type === "text" && block.text) {
422
- finalOutput += block.text;
423
- }
425
+ case "agent_end":
426
+ // Extract final content from assistant messages only (not user prompts)
427
+ if (event.messages && Array.isArray(event.messages)) {
428
+ for (const msg of event.messages) {
429
+ if ((msg as { role?: string })?.role !== "assistant") continue;
430
+ const messageContent = getMessageContent(msg);
431
+ if (messageContent && Array.isArray(messageContent)) {
432
+ for (const block of messageContent) {
433
+ if (block.type === "text" && block.text) {
434
+ finalOutput += block.text;
424
435
  }
425
436
  }
426
437
  }
427
438
  }
428
- break;
429
- }
430
-
431
- progress.durationMs = now - startTime;
432
- // Clone progress object before passing to callback to prevent mutation during render
433
- onProgress?.({ ...progress });
434
- } catch {
435
- // Ignore non-JSON lines
439
+ }
440
+ break;
436
441
  }
442
+
443
+ emitProgress();
437
444
  };
438
445
 
439
- // Read stdout asynchronously
440
- const stdoutDone = (async () => {
441
- try {
442
- while (true) {
443
- const { done, value } = await reader.read();
444
- if (done) break;
445
- buffer += decoder.decode(value, { stream: true });
446
- const lines = buffer.split("\n");
447
- buffer = lines.pop() || "";
448
- for (const line of lines) {
449
- processLine(line);
450
- }
451
- }
452
- // Process remaining buffer
453
- if (buffer.trim()) {
454
- processLine(buffer);
455
- }
456
- } catch {
457
- // Ignore read errors
458
- }
459
- })();
446
+ const startMessage: SubagentWorkerRequest = {
447
+ type: "start",
448
+ payload: {
449
+ cwd,
450
+ task: fullTask,
451
+ systemPrompt: agent.systemPrompt,
452
+ model: resolvedModel,
453
+ toolNames,
454
+ sessionFile,
455
+ spawnsEnv,
456
+ },
457
+ };
460
458
 
461
- // Capture stderr - Bun.spawn returns ReadableStream, convert to text
462
- const stderrDone = (async () => {
463
- try {
464
- const stderrReader = proc.stderr.getReader();
465
- const stderrDecoder = new TextDecoder();
466
- while (true) {
467
- const { done, value } = await stderrReader.read();
468
- if (done) break;
469
- stderr += stderrDecoder.decode(value, { stream: true });
470
- }
471
- } catch {
472
- // Ignore stderr read errors
473
- }
474
- })();
459
+ interface WorkerMessageEvent<T> {
460
+ data: T;
461
+ }
462
+ interface WorkerErrorEvent {
463
+ message: string;
464
+ }
475
465
 
476
- // Wait for process and stream readers to finish
477
- const exitCode = await proc.exited;
478
- await Promise.all([stdoutDone, stderrDone]);
479
- resolved = true;
466
+ const done = await new Promise<Extract<SubagentWorkerResponse, { type: "done" }>>((resolve) => {
467
+ const onMessage = (event: WorkerMessageEvent<SubagentWorkerResponse>) => {
468
+ const message = event.data;
469
+ if (!message || resolved) return;
470
+ if (message.type === "event") {
471
+ processEvent(message.event);
472
+ return;
473
+ }
474
+ if (message.type === "done") {
475
+ resolved = true;
476
+ resolve(message);
477
+ }
478
+ };
479
+ const onError = (event: WorkerErrorEvent) => {
480
+ if (resolved) return;
481
+ resolved = true;
482
+ resolve({
483
+ type: "done",
484
+ exitCode: 1,
485
+ durationMs: Date.now() - startTime,
486
+ error: event.message,
487
+ });
488
+ };
489
+ worker.addEventListener("message", onMessage);
490
+ worker.addEventListener("error", onError);
491
+ worker.postMessage(startMessage);
492
+ });
480
493
 
481
494
  // Cleanup
482
495
  if (signal) {
483
496
  signal.removeEventListener("abort", onAbort);
484
497
  }
498
+ worker.terminate();
485
499
 
486
- try {
487
- if (existsSync(promptFile)) {
488
- unlinkSync(promptFile);
489
- }
490
- } catch {
491
- // Ignore cleanup errors
500
+ const exitCode = done.exitCode;
501
+ if (done.error) {
502
+ stderr = done.error;
492
503
  }
493
504
 
494
505
  // Use final output if available, otherwise accumulated output
@@ -511,10 +522,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
511
522
  }
512
523
 
513
524
  // Update final progress
514
- const wasAborted = signal?.aborted ?? false;
525
+ const wasAborted = done.aborted || signal?.aborted || false;
515
526
  progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
516
- progress.durationMs = Date.now() - startTime;
517
- onProgress?.(progress);
527
+ emitProgress();
518
528
 
519
529
  return {
520
530
  index,
@@ -531,7 +541,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
531
541
  modelOverride,
532
542
  error: exitCode !== 0 && stderr ? stderr : undefined,
533
543
  aborted: wasAborted,
534
- jsonlEvents,
544
+ usage: hasUsage ? accumulatedUsage : undefined,
535
545
  artifactPaths,
536
546
  extractedToolData: progress.extractedToolData,
537
547
  outputMeta,