@oh-my-pi/pi-coding-agent 3.21.0 → 3.25.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 (71) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/docs/sdk.md +47 -50
  3. package/examples/custom-tools/README.md +0 -15
  4. package/examples/hooks/custom-compaction.ts +1 -3
  5. package/examples/sdk/README.md +6 -10
  6. package/package.json +5 -5
  7. package/src/cli/args.ts +9 -6
  8. package/src/core/agent-session.ts +3 -3
  9. package/src/core/custom-commands/bundled/wt/index.ts +3 -0
  10. package/src/core/custom-tools/wrapper.ts +0 -1
  11. package/src/core/extensions/index.ts +1 -6
  12. package/src/core/extensions/wrapper.ts +0 -7
  13. package/src/core/file-mentions.ts +5 -8
  14. package/src/core/sdk.ts +48 -111
  15. package/src/core/session-manager.ts +7 -0
  16. package/src/core/system-prompt.ts +22 -33
  17. package/src/core/tools/ask.ts +14 -7
  18. package/src/core/tools/bash-interceptor.ts +4 -4
  19. package/src/core/tools/bash.ts +19 -9
  20. package/src/core/tools/complete.ts +131 -0
  21. package/src/core/tools/context.ts +7 -0
  22. package/src/core/tools/edit.ts +8 -15
  23. package/src/core/tools/exa/render.ts +4 -16
  24. package/src/core/tools/find.ts +7 -18
  25. package/src/core/tools/git.ts +13 -3
  26. package/src/core/tools/grep.ts +7 -18
  27. package/src/core/tools/index.test.ts +188 -0
  28. package/src/core/tools/index.ts +106 -236
  29. package/src/core/tools/jtd-to-json-schema.ts +274 -0
  30. package/src/core/tools/ls.ts +4 -9
  31. package/src/core/tools/lsp/index.ts +32 -29
  32. package/src/core/tools/lsp/render.ts +7 -28
  33. package/src/core/tools/notebook.ts +3 -5
  34. package/src/core/tools/output.ts +130 -31
  35. package/src/core/tools/read.ts +8 -19
  36. package/src/core/tools/review.ts +0 -18
  37. package/src/core/tools/rulebook.ts +8 -2
  38. package/src/core/tools/task/agents.ts +28 -7
  39. package/src/core/tools/task/artifacts.ts +6 -9
  40. package/src/core/tools/task/discovery.ts +0 -6
  41. package/src/core/tools/task/executor.ts +306 -257
  42. package/src/core/tools/task/index.ts +65 -235
  43. package/src/core/tools/task/name-generator.ts +247 -0
  44. package/src/core/tools/task/render.ts +158 -19
  45. package/src/core/tools/task/types.ts +13 -11
  46. package/src/core/tools/task/worker-protocol.ts +18 -0
  47. package/src/core/tools/task/worker.ts +270 -0
  48. package/src/core/tools/web-fetch.ts +4 -36
  49. package/src/core/tools/web-search/index.ts +2 -1
  50. package/src/core/tools/web-search/render.ts +1 -4
  51. package/src/core/tools/write.ts +7 -15
  52. package/src/discovery/helpers.test.ts +1 -1
  53. package/src/index.ts +5 -16
  54. package/src/main.ts +4 -4
  55. package/src/modes/interactive/theme/theme.ts +4 -4
  56. package/src/prompts/task.md +14 -57
  57. package/src/prompts/tools/output.md +4 -3
  58. package/src/prompts/tools/task.md +70 -0
  59. package/examples/custom-tools/question/index.ts +0 -84
  60. package/examples/custom-tools/subagent/README.md +0 -172
  61. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  62. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  63. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  64. package/examples/custom-tools/subagent/agents.ts +0 -156
  65. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  66. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  67. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  68. package/examples/custom-tools/subagent/index.ts +0 -1002
  69. package/examples/sdk/05-tools.ts +0 -94
  70. package/examples/sdk/12-full-control.ts +0 -95
  71. package/src/prompts/browser.md +0 -71
@@ -1,41 +1,43 @@
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;
29
28
  task: string;
30
29
  description?: string;
31
30
  index: number;
31
+ taskId: string;
32
32
  context?: string;
33
33
  modelOverride?: string;
34
+ outputSchema?: unknown;
34
35
  signal?: AbortSignal;
35
36
  onProgress?: (progress: AgentProgress) => void;
36
37
  sessionFile?: string | null;
37
38
  persistArtifacts?: boolean;
38
39
  artifactsDir?: string;
40
+ eventBus?: EventBus;
39
41
  }
40
42
 
41
43
  /**
@@ -127,15 +129,16 @@ function getUsageTokens(usage: unknown): number {
127
129
  }
128
130
 
129
131
  /**
130
- * Run a single agent as a subprocess.
132
+ * Run a single agent in a worker.
131
133
  */
132
134
  export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
133
- const { cwd, agent, task, index, context, modelOverride, signal, onProgress } = options;
135
+ const { cwd, agent, task, index, taskId, context, modelOverride, outputSchema, signal, onProgress } = options;
134
136
  const startTime = Date.now();
135
137
 
136
138
  // Initialize progress
137
139
  const progress: AgentProgress = {
138
140
  index,
141
+ taskId,
139
142
  agent: agent.name,
140
143
  agentSource: agent.source,
141
144
  status: "running",
@@ -153,6 +156,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
153
156
  if (signal?.aborted) {
154
157
  return {
155
158
  index,
159
+ taskId,
156
160
  agent: agent.name,
157
161
  agentSource: agent.source,
158
162
  task,
@@ -168,33 +172,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
168
172
  };
169
173
  }
170
174
 
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
175
  // Build full task with context
199
176
  const fullTask = context ? `${context}\n\n${task}` : task;
200
177
 
@@ -204,7 +181,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
204
181
 
205
182
  if (options.artifactsDir) {
206
183
  ensureArtifactsDir(options.artifactsDir);
207
- artifactPaths = getArtifactPaths(options.artifactsDir, agent.name, index);
184
+ artifactPaths = getArtifactPaths(options.artifactsDir, taskId);
208
185
  subtaskSessionFile = artifactPaths.jsonlPath;
209
186
 
210
187
  // Write input file immediately (real-time visibility)
@@ -215,182 +192,197 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
215
192
  }
216
193
  }
217
194
 
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
195
  // Add tools if specified
196
+ let toolNames: string[] | undefined;
225
197
  if (agent.tools && agent.tools.length > 0) {
226
- let toolList = agent.tools;
198
+ toolNames = agent.tools;
227
199
  // Auto-include task tool if spawns defined but task not in tools
228
- if (agent.spawns !== undefined && !toolList.includes("task")) {
229
- toolList = [...toolList, "task"];
200
+ if (agent.spawns !== undefined && !toolNames.includes("task")) {
201
+ toolNames = [...toolNames, "task"];
230
202
  }
231
- args.push("--tools", toolList.join(","));
232
203
  }
233
204
 
234
205
  // Resolve and add model
235
206
  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
- }
207
+ const sessionFile = subtaskSessionFile ?? options.sessionFile ?? null;
208
+ const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
257
209
 
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
- }
266
-
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
- });
210
+ const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
276
211
 
277
212
  let output = "";
278
213
  let stderr = "";
279
214
  let finalOutput = "";
280
215
  let resolved = false;
281
216
  let pendingTermination = false; // Set when shouldTerminate fires, wait for message_end
282
- const jsonlEvents: string[] = [];
217
+
218
+ // Accumulate usage incrementally from message_end events (no memory for streaming events)
219
+ const accumulatedUsage = {
220
+ input: 0,
221
+ output: 0,
222
+ cacheRead: 0,
223
+ cacheWrite: 0,
224
+ totalTokens: 0,
225
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
226
+ };
227
+ let hasUsage = false;
228
+
229
+ let abortSent = false;
230
+ const requestAbort = () => {
231
+ if (abortSent) return;
232
+ abortSent = true;
233
+ const abortMessage: SubagentWorkerRequest = { type: "abort" };
234
+ worker.postMessage(abortMessage);
235
+ setTimeout(() => {
236
+ if (!resolved) {
237
+ worker.terminate();
238
+ }
239
+ }, 2000);
240
+ };
283
241
 
284
242
  // Handle abort signal
285
243
  const onAbort = () => {
286
- if (!resolved) {
287
- proc.kill(15); // SIGTERM
288
- }
244
+ if (!resolved) requestAbort();
289
245
  };
290
246
  if (signal) {
291
247
  signal.addEventListener("abort", onAbort, { once: true });
292
248
  }
293
249
 
294
- // Parse JSON events from stdout
295
- const reader = proc.stdout.getReader();
296
- const decoder = new TextDecoder();
297
- let buffer = "";
250
+ const emitProgress = () => {
251
+ progress.durationMs = Date.now() - startTime;
252
+ onProgress?.({ ...progress });
253
+ if (options.eventBus) {
254
+ options.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
255
+ index,
256
+ agent: agent.name,
257
+ agentSource: agent.source,
258
+ task,
259
+ progress: { ...progress },
260
+ });
261
+ }
262
+ };
263
+
264
+ const getMessageContent = (message: unknown): unknown => {
265
+ if (message && typeof message === "object" && "content" in message) {
266
+ return (message as { content?: unknown }).content;
267
+ }
268
+ return undefined;
269
+ };
270
+
271
+ const getMessageUsage = (message: unknown): unknown => {
272
+ if (message && typeof message === "object" && "usage" in message) {
273
+ return (message as { usage?: unknown }).usage;
274
+ }
275
+ return undefined;
276
+ };
298
277
 
299
- const processLine = (line: string) => {
278
+ const processEvent = (event: AgentEvent) => {
300
279
  if (resolved) return;
301
280
 
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,
281
+ if (options.eventBus) {
282
+ options.eventBus.emit(TASK_SUBAGENT_EVENT_CHANNEL, {
283
+ index,
284
+ agent: agent.name,
285
+ agentSource: agent.source,
286
+ task,
287
+ event,
288
+ });
289
+ }
290
+
291
+ const now = Date.now();
292
+
293
+ switch (event.type) {
294
+ case "tool_execution_start":
295
+ progress.toolCount++;
296
+ progress.currentTool = event.toolName;
297
+ progress.currentToolArgs = extractToolArgsPreview(
298
+ (event as { toolArgs?: Record<string, unknown> }).toolArgs || event.args || {},
299
+ );
300
+ progress.currentToolStartMs = now;
301
+ break;
302
+
303
+ case "tool_execution_end": {
304
+ if (progress.currentTool) {
305
+ progress.recentTools.unshift({
306
+ tool: progress.currentTool,
307
+ args: progress.currentToolArgs || "",
308
+ endMs: now,
309
+ });
310
+ // Keep only last 5
311
+ if (progress.recentTools.length > 5) {
312
+ progress.recentTools.pop();
313
+ }
314
+ }
315
+ progress.currentTool = undefined;
316
+ progress.currentToolArgs = undefined;
317
+ progress.currentToolStartMs = undefined;
318
+
319
+ // Check for registered subagent tool handler
320
+ const handler = subprocessToolRegistry.getHandler(event.toolName);
321
+ const eventArgs = (event as { args?: Record<string, unknown> }).args ?? {};
322
+ if (handler) {
323
+ // Extract data using handler
324
+ if (handler.extractData) {
325
+ const data = handler.extractData({
326
+ toolName: event.toolName,
327
+ toolCallId: event.toolCallId,
328
+ args: eventArgs,
329
+ result: event.result,
330
+ isError: event.isError,
321
331
  });
322
- // Keep only last 5
323
- if (progress.recentTools.length > 5) {
324
- progress.recentTools.pop();
332
+ if (data !== undefined) {
333
+ progress.extractedToolData = progress.extractedToolData || {};
334
+ progress.extractedToolData[event.toolName] = progress.extractedToolData[event.toolName] || [];
335
+ progress.extractedToolData[event.toolName].push(data);
325
336
  }
326
337
  }
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
338
 
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
- }
339
+ // Check if handler wants to terminate worker
340
+ if (
341
+ handler.shouldTerminate?.({
342
+ toolName: event.toolName,
343
+ toolCallId: event.toolCallId,
344
+ args: eventArgs,
345
+ result: event.result,
346
+ isError: event.isError,
347
+ })
348
+ ) {
349
+ // Don't terminate immediately - wait for message_end to get token counts
350
+ pendingTermination = true;
351
+ // Safety timeout in case message_end never arrives
352
+ setTimeout(() => {
353
+ if (!resolved) {
354
+ requestAbort();
355
+ }
356
+ }, 2000);
370
357
  }
371
- break;
372
358
  }
359
+ break;
360
+ }
373
361
 
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
- }
362
+ case "message_update": {
363
+ // Extract text for progress display only (replace, don't accumulate)
364
+ const updateContent =
365
+ getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
366
+ if (updateContent && Array.isArray(updateContent)) {
367
+ const allText: string[] = [];
368
+ for (const block of updateContent) {
369
+ if (block.type === "text" && block.text) {
370
+ const lines = block.text.split("\n").filter((l: string) => l.trim());
371
+ allText.push(...lines);
384
372
  }
385
- // Show last 8 lines from current state (not accumulated)
386
- progress.recentOutput = allText.slice(-8).reverse();
387
373
  }
388
- break;
374
+ // Show last 8 lines from current state (not accumulated)
375
+ progress.recentOutput = allText.slice(-8).reverse();
389
376
  }
377
+ break;
378
+ }
390
379
 
391
- case "message_end": {
392
- // Extract final text content from completed message
393
- const messageContent = event.message?.content || event.content;
380
+ case "message_end": {
381
+ // Extract text from assistant and toolResult messages (not user prompts)
382
+ const role = event.message?.role;
383
+ if (role === "assistant") {
384
+ const messageContent =
385
+ getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
394
386
  if (messageContent && Array.isArray(messageContent)) {
395
387
  for (const block of messageContent) {
396
388
  if (block.type === "text" && block.text) {
@@ -398,101 +390,158 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
398
390
  }
399
391
  }
400
392
  }
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
393
+ }
394
+ // Extract and accumulate usage (prefer message.usage, fallback to event.usage)
395
+ const messageUsage = getMessageUsage(event.message) || (event as AgentEvent & { usage?: unknown }).usage;
396
+ if (messageUsage && typeof messageUsage === "object") {
397
+ // Only count assistant messages (not tool results, etc.)
398
+ if (
399
+ role === "assistant" &&
400
+ event.message?.stopReason !== "aborted" &&
401
+ event.message?.stopReason !== "error"
402
+ ) {
403
+ const usageRecord = messageUsage as Record<string, number | undefined>;
404
+ const costRecord = (messageUsage as { cost?: Record<string, number | undefined> }).cost;
405
+ hasUsage = true;
406
+ accumulatedUsage.input += usageRecord.input ?? 0;
407
+ accumulatedUsage.output += usageRecord.output ?? 0;
408
+ accumulatedUsage.cacheRead += usageRecord.cacheRead ?? 0;
409
+ accumulatedUsage.cacheWrite += usageRecord.cacheWrite ?? 0;
410
+ accumulatedUsage.totalTokens += usageRecord.totalTokens ?? 0;
411
+ if (costRecord) {
412
+ accumulatedUsage.cost.input += costRecord.input ?? 0;
413
+ accumulatedUsage.cost.output += costRecord.output ?? 0;
414
+ accumulatedUsage.cost.cacheRead += costRecord.cacheRead ?? 0;
415
+ accumulatedUsage.cost.cacheWrite += costRecord.cacheWrite ?? 0;
416
+ accumulatedUsage.cost.total += costRecord.total ?? 0;
417
+ }
411
418
  }
412
- break;
419
+ // Accumulate tokens for progress display
420
+ progress.tokens += getUsageTokens(messageUsage);
413
421
  }
422
+ // If pending termination, now we have tokens - terminate
423
+ if (pendingTermination && !resolved) {
424
+ requestAbort();
425
+ }
426
+ break;
427
+ }
414
428
 
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
- }
429
+ case "agent_end":
430
+ // Extract final content from assistant messages only (not user prompts)
431
+ if (event.messages && Array.isArray(event.messages)) {
432
+ for (const msg of event.messages) {
433
+ if ((msg as { role?: string })?.role !== "assistant") continue;
434
+ const messageContent = getMessageContent(msg);
435
+ if (messageContent && Array.isArray(messageContent)) {
436
+ for (const block of messageContent) {
437
+ if (block.type === "text" && block.text) {
438
+ finalOutput += block.text;
424
439
  }
425
440
  }
426
441
  }
427
442
  }
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
443
+ }
444
+ break;
436
445
  }
446
+
447
+ emitProgress();
437
448
  };
438
449
 
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
- })();
450
+ const startMessage: SubagentWorkerRequest = {
451
+ type: "start",
452
+ payload: {
453
+ cwd,
454
+ task: fullTask,
455
+ systemPrompt: agent.systemPrompt,
456
+ model: resolvedModel,
457
+ toolNames,
458
+ outputSchema,
459
+ sessionFile,
460
+ spawnsEnv,
461
+ },
462
+ };
460
463
 
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
- })();
464
+ interface WorkerMessageEvent<T> {
465
+ data: T;
466
+ }
467
+ interface WorkerErrorEvent {
468
+ message: string;
469
+ }
475
470
 
476
- // Wait for process and stream readers to finish
477
- const exitCode = await proc.exited;
478
- await Promise.all([stdoutDone, stderrDone]);
479
- resolved = true;
471
+ const done = await new Promise<Extract<SubagentWorkerResponse, { type: "done" }>>((resolve) => {
472
+ const onMessage = (event: WorkerMessageEvent<SubagentWorkerResponse>) => {
473
+ const message = event.data;
474
+ if (!message || resolved) return;
475
+ if (message.type === "event") {
476
+ processEvent(message.event);
477
+ return;
478
+ }
479
+ if (message.type === "done") {
480
+ resolved = true;
481
+ resolve(message);
482
+ }
483
+ };
484
+ const onError = (event: WorkerErrorEvent) => {
485
+ if (resolved) return;
486
+ resolved = true;
487
+ resolve({
488
+ type: "done",
489
+ exitCode: 1,
490
+ durationMs: Date.now() - startTime,
491
+ error: event.message,
492
+ });
493
+ };
494
+ worker.addEventListener("message", onMessage);
495
+ worker.addEventListener("error", onError);
496
+ worker.postMessage(startMessage);
497
+ });
480
498
 
481
499
  // Cleanup
482
500
  if (signal) {
483
501
  signal.removeEventListener("abort", onAbort);
484
502
  }
503
+ worker.terminate();
485
504
 
486
- try {
487
- if (existsSync(promptFile)) {
488
- unlinkSync(promptFile);
489
- }
490
- } catch {
491
- // Ignore cleanup errors
505
+ let exitCode = done.exitCode;
506
+ if (done.error) {
507
+ stderr = done.error;
492
508
  }
493
509
 
494
510
  // Use final output if available, otherwise accumulated output
495
- const rawOutput = finalOutput || output;
511
+ let rawOutput = finalOutput || output;
512
+ let abortedViaComplete = false;
513
+ const completeItems = progress.extractedToolData?.complete as
514
+ | Array<{ data?: unknown; status?: "success" | "aborted"; error?: string }>
515
+ | undefined;
516
+ const hasComplete = Array.isArray(completeItems) && completeItems.length > 0;
517
+ if (hasComplete) {
518
+ const lastComplete = completeItems[completeItems.length - 1];
519
+ if (lastComplete?.status === "aborted") {
520
+ // Agent explicitly aborted via complete tool - clean exit with error info
521
+ abortedViaComplete = true;
522
+ exitCode = 0;
523
+ stderr = lastComplete.error || "Subagent aborted task";
524
+ try {
525
+ rawOutput = JSON.stringify({ aborted: true, error: lastComplete.error }, null, 2);
526
+ } catch {
527
+ rawOutput = `{"aborted":true,"error":"${lastComplete.error || "Unknown error"}"}`;
528
+ }
529
+ } else {
530
+ // Normal successful completion
531
+ const completeData = lastComplete?.data ?? null;
532
+ try {
533
+ rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
534
+ } catch (err) {
535
+ const errorMessage = err instanceof Error ? err.message : String(err);
536
+ rawOutput = `{"error":"Failed to serialize complete data: ${errorMessage}"}`;
537
+ }
538
+ exitCode = 0;
539
+ stderr = "";
540
+ }
541
+ } else {
542
+ const warning = "SYSTEM WARNING: Subagent exited without calling complete tool after 3 reminders.";
543
+ rawOutput = rawOutput ? `${warning}\n\n${rawOutput}` : warning;
544
+ }
496
545
  const { text: truncatedOutput, truncated } = truncateOutput(rawOutput);
497
546
 
498
547
  // Write output artifact (input and jsonl already written in real-time)
@@ -511,13 +560,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
511
560
  }
512
561
 
513
562
  // Update final progress
514
- const wasAborted = signal?.aborted ?? false;
563
+ const wasAborted = abortedViaComplete || (!hasComplete && (done.aborted || signal?.aborted || false));
515
564
  progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
516
- progress.durationMs = Date.now() - startTime;
517
- onProgress?.(progress);
565
+ emitProgress();
518
566
 
519
567
  return {
520
568
  index,
569
+ taskId,
521
570
  agent: agent.name,
522
571
  agentSource: agent.source,
523
572
  task,
@@ -531,7 +580,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
531
580
  modelOverride,
532
581
  error: exitCode !== 0 && stderr ? stderr : undefined,
533
582
  aborted: wasAborted,
534
- jsonlEvents,
583
+ usage: hasUsage ? accumulatedUsage : undefined,
535
584
  artifactPaths,
536
585
  extractedToolData: progress.extractedToolData,
537
586
  outputMeta,