@oh-my-pi/pi-coding-agent 3.24.0 → 3.30.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 (97) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +4 -4
  3. package/src/core/custom-commands/bundled/wt/index.ts +3 -0
  4. package/src/core/sdk.ts +7 -0
  5. package/src/core/tools/complete.ts +129 -0
  6. package/src/core/tools/index.test.ts +9 -1
  7. package/src/core/tools/index.ts +18 -5
  8. package/src/core/tools/jtd-to-json-schema.ts +252 -0
  9. package/src/core/tools/output.ts +125 -14
  10. package/src/core/tools/read.ts +4 -4
  11. package/src/core/tools/task/artifacts.ts +6 -9
  12. package/src/core/tools/task/executor.ts +189 -24
  13. package/src/core/tools/task/index.ts +23 -18
  14. package/src/core/tools/task/name-generator.ts +1577 -0
  15. package/src/core/tools/task/render.ts +137 -8
  16. package/src/core/tools/task/types.ts +26 -5
  17. package/src/core/tools/task/worker-protocol.ts +1 -0
  18. package/src/core/tools/task/worker.ts +136 -14
  19. package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
  20. package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
  21. package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
  22. package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
  23. package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
  24. package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
  25. package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
  26. package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
  27. package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
  28. package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
  29. package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
  30. package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
  31. package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
  32. package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
  33. package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
  34. package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
  35. package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
  36. package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
  37. package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
  38. package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
  39. package/src/core/tools/web-fetch-handlers/github.ts +424 -0
  40. package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
  41. package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
  42. package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
  43. package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
  44. package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
  45. package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
  46. package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
  47. package/src/core/tools/web-fetch-handlers/index.ts +69 -0
  48. package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
  49. package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
  50. package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
  51. package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
  52. package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
  53. package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
  54. package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
  55. package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
  56. package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
  57. package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
  58. package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
  59. package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
  60. package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
  61. package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
  62. package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
  63. package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
  64. package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
  65. package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
  66. package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
  67. package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
  68. package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
  69. package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
  70. package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
  71. package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
  72. package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
  73. package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
  74. package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
  75. package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
  76. package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
  77. package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
  78. package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
  79. package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
  80. package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
  81. package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
  82. package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
  83. package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
  84. package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
  85. package/src/core/tools/web-fetch-handlers/types.ts +163 -0
  86. package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
  87. package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
  88. package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
  89. package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
  90. package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
  91. package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
  92. package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
  93. package/src/core/tools/web-fetch.ts +152 -1324
  94. package/src/prompts/task.md +14 -50
  95. package/src/prompts/tools/output.md +2 -1
  96. package/src/prompts/tools/task.md +3 -1
  97. package/src/utils/tools-manager.ts +110 -8
@@ -36,6 +36,11 @@ const outputSchema = Type.Object({
36
36
  description: "Output format: raw (default), json (structured), stripped (no ANSI)",
37
37
  }),
38
38
  ),
39
+ query: Type.Optional(
40
+ Type.String({
41
+ description: "jq-like query for JSON outputs (e.g., .result.items[0].name). Requires JSON output.",
42
+ }),
43
+ ),
39
44
  offset: Type.Optional(
40
45
  Type.Number({
41
46
  description: "Line number to start reading from (1-indexed)",
@@ -70,6 +75,7 @@ interface OutputEntry {
70
75
  provenance?: OutputProvenance;
71
76
  previewLines?: string[];
72
77
  range?: OutputRange;
78
+ query?: string;
73
79
  }
74
80
 
75
81
  export interface OutputToolDetails {
@@ -83,6 +89,77 @@ function stripAnsi(text: string): string {
83
89
  return text.replace(/\x1b\[[0-9;]*m/g, "");
84
90
  }
85
91
 
92
+ function parseQuery(query: string): Array<string | number> {
93
+ let input = query.trim();
94
+ if (!input) return [];
95
+ if (input.startsWith(".")) input = input.slice(1);
96
+ if (!input) return [];
97
+
98
+ const tokens: Array<string | number> = [];
99
+ let i = 0;
100
+
101
+ const isIdentChar = (ch: string) => /[A-Za-z0-9_-]/.test(ch);
102
+
103
+ while (i < input.length) {
104
+ const ch = input[i];
105
+ if (ch === ".") {
106
+ i++;
107
+ continue;
108
+ }
109
+ if (ch === "[") {
110
+ const closeIndex = input.indexOf("]", i + 1);
111
+ if (closeIndex === -1) {
112
+ throw new Error(`Invalid query: missing ] in ${query}`);
113
+ }
114
+ const raw = input.slice(i + 1, closeIndex).trim();
115
+ if (!raw) {
116
+ throw new Error(`Invalid query: empty [] in ${query}`);
117
+ }
118
+ const quote = raw[0];
119
+ if ((quote === '"' || quote === "'") && raw.endsWith(quote)) {
120
+ let inner = raw.slice(1, -1);
121
+ inner = inner.replace(/\\(["'\\])/g, "$1");
122
+ tokens.push(inner);
123
+ } else if (/^\d+$/.test(raw)) {
124
+ tokens.push(Number(raw));
125
+ } else {
126
+ tokens.push(raw);
127
+ }
128
+ i = closeIndex + 1;
129
+ continue;
130
+ }
131
+
132
+ const start = i;
133
+ while (i < input.length && isIdentChar(input[i])) {
134
+ i++;
135
+ }
136
+ if (start === i) {
137
+ throw new Error(`Invalid query: unexpected token '${input[i]}' in ${query}`);
138
+ }
139
+ const ident = input.slice(start, i);
140
+ tokens.push(ident);
141
+ }
142
+
143
+ return tokens;
144
+ }
145
+
146
+ function applyQuery(data: unknown, query: string): unknown {
147
+ const tokens = parseQuery(query);
148
+ let current: unknown = data;
149
+ for (const token of tokens) {
150
+ if (current === null || current === undefined) return undefined;
151
+ if (typeof token === "number") {
152
+ if (!Array.isArray(current)) return undefined;
153
+ current = current[token];
154
+ continue;
155
+ }
156
+ if (typeof current !== "object") return undefined;
157
+ const record = current as Record<string, unknown>;
158
+ current = record[token];
159
+ }
160
+ return current;
161
+ }
162
+
86
163
  /** List available output IDs in artifacts directory */
87
164
  function listAvailableOutputs(artifactsDir: string): string[] {
88
165
  try {
@@ -128,7 +205,13 @@ export function createOutputTool(session: ToolSession): AgentTool<typeof outputS
128
205
  parameters: outputSchema,
129
206
  execute: async (
130
207
  _toolCallId: string,
131
- params: { ids: string[]; format?: "raw" | "json" | "stripped"; offset?: number; limit?: number },
208
+ params: {
209
+ ids: string[];
210
+ format?: "raw" | "json" | "stripped";
211
+ query?: string;
212
+ offset?: number;
213
+ limit?: number;
214
+ },
132
215
  ): Promise<{ content: TextContent[]; details: OutputToolDetails }> => {
133
216
  const sessionFile = session.getSessionFile();
134
217
 
@@ -150,7 +233,15 @@ export function createOutputTool(session: ToolSession): AgentTool<typeof outputS
150
233
  const outputs: OutputEntry[] = [];
151
234
  const notFound: string[] = [];
152
235
  const outputContentById = new Map<string, string>();
153
- const format = params.format ?? "raw";
236
+ const query = params.query?.trim();
237
+ const wantsQuery = query !== undefined && query.length > 0;
238
+ const format = params.format ?? (wantsQuery ? "json" : "raw");
239
+
240
+ if (wantsQuery && (params.offset !== undefined || params.limit !== undefined)) {
241
+ throw new Error("query cannot be combined with offset/limit");
242
+ }
243
+
244
+ const queryResults: Array<{ id: string; value: unknown }> = [];
154
245
 
155
246
  for (const id of params.ids) {
156
247
  const outputPath = path.join(artifactsDir, `${id}.out.md`);
@@ -168,7 +259,22 @@ export function createOutputTool(session: ToolSession): AgentTool<typeof outputS
168
259
  let selectedContent = rawContent;
169
260
  let range: OutputRange | undefined;
170
261
 
171
- if (params.offset !== undefined || params.limit !== undefined) {
262
+ if (wantsQuery && query) {
263
+ let jsonValue: unknown;
264
+ try {
265
+ jsonValue = JSON.parse(rawContent);
266
+ } catch (err) {
267
+ const message = err instanceof Error ? err.message : String(err);
268
+ throw new Error(`Output ${id} is not valid JSON: ${message}`);
269
+ }
270
+ const value = applyQuery(jsonValue, query);
271
+ queryResults.push({ id, value });
272
+ try {
273
+ selectedContent = JSON.stringify(value, null, 2) ?? "null";
274
+ } catch {
275
+ selectedContent = String(value);
276
+ }
277
+ } else if (params.offset !== undefined || params.limit !== undefined) {
172
278
  const startLine = Math.max(1, params.offset ?? 1);
173
279
  if (startLine > totalLines) {
174
280
  throw new Error(
@@ -186,11 +292,12 @@ export function createOutputTool(session: ToolSession): AgentTool<typeof outputS
186
292
  outputs.push({
187
293
  id,
188
294
  path: outputPath,
189
- lineCount: totalLines,
190
- charCount: totalChars,
295
+ lineCount: wantsQuery ? selectedContent.split("\n").length : totalLines,
296
+ charCount: wantsQuery ? selectedContent.length : totalChars,
191
297
  provenance: parseOutputProvenance(id),
192
298
  previewLines: extractPreviewLines(selectedContent, 4),
193
299
  range,
300
+ query: query,
194
301
  });
195
302
  }
196
303
 
@@ -212,15 +319,17 @@ export function createOutputTool(session: ToolSession): AgentTool<typeof outputS
212
319
  let contentText: string;
213
320
 
214
321
  if (format === "json") {
215
- const jsonData = outputs.map((o) => ({
216
- id: o.id,
217
- lineCount: o.lineCount,
218
- charCount: o.charCount,
219
- provenance: o.provenance,
220
- previewLines: o.previewLines,
221
- range: o.range,
222
- content: outputContentById.get(o.id) ?? "",
223
- }));
322
+ const jsonData = wantsQuery
323
+ ? queryResults
324
+ : outputs.map((o) => ({
325
+ id: o.id,
326
+ lineCount: o.lineCount,
327
+ charCount: o.charCount,
328
+ provenance: o.provenance,
329
+ previewLines: o.previewLines,
330
+ range: o.range,
331
+ content: outputContentById.get(o.id) ?? "",
332
+ }));
224
333
  contentText = JSON.stringify(jsonData, null, 2);
225
334
  } else {
226
335
  // raw or stripped
@@ -257,6 +366,7 @@ export function createOutputTool(session: ToolSession): AgentTool<typeof outputS
257
366
  interface OutputRenderArgs {
258
367
  ids: string[];
259
368
  format?: "raw" | "json" | "stripped";
369
+ query?: string;
260
370
  offset?: number;
261
371
  limit?: number;
262
372
  }
@@ -285,6 +395,7 @@ export const outputToolRenderer = {
285
395
 
286
396
  const meta: string[] = [];
287
397
  if (args.format && args.format !== "raw") meta.push(`format:${args.format}`);
398
+ if (args.query) meta.push(`query:${args.query}`);
288
399
  if (args.offset !== undefined) meta.push(`offset:${args.offset}`);
289
400
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
290
401
  text += formatMeta(meta, uiTheme);
@@ -311,10 +311,10 @@ async function findReadPathSuggestions(
311
311
  return { suggestions, scopeLabel, truncated };
312
312
  }
313
313
 
314
- function convertWithMarkitdown(filePath: string): { content: string; ok: boolean; error?: string } {
315
- const cmd = Bun.which("markitdown");
314
+ async function convertWithMarkitdown(filePath: string): Promise<{ content: string; ok: boolean; error?: string }> {
315
+ const cmd = await ensureTool("markitdown", true);
316
316
  if (!cmd) {
317
- return { content: "", ok: false, error: "markitdown not found" };
317
+ return { content: "", ok: false, error: "markitdown not found (uv/pip unavailable)" };
318
318
  }
319
319
 
320
320
  const result = Bun.spawnSync([cmd, filePath], {
@@ -449,7 +449,7 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
449
449
  }
450
450
  } else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
451
451
  // Convert document via markitdown
452
- const result = convertWithMarkitdown(absolutePath);
452
+ const result = await convertWithMarkitdown(absolutePath);
453
453
  if (result.ok) {
454
454
  // Apply truncation to converted content
455
455
  const truncation = truncateHead(result.content);
@@ -38,14 +38,12 @@ export function ensureArtifactsDir(dir: string): void {
38
38
  */
39
39
  export function getArtifactPaths(
40
40
  dir: string,
41
- agentName: string,
42
- index: number,
41
+ taskId: string,
43
42
  ): { inputPath: string; outputPath: string; jsonlPath: string } {
44
- const base = `${agentName}_${index}`;
45
43
  return {
46
- inputPath: path.join(dir, `${base}.in.md`),
47
- outputPath: path.join(dir, `${base}.out.md`),
48
- jsonlPath: path.join(dir, `${base}.jsonl`),
44
+ inputPath: path.join(dir, `${taskId}.in.md`),
45
+ outputPath: path.join(dir, `${taskId}.out.md`),
46
+ jsonlPath: path.join(dir, `${taskId}.jsonl`),
49
47
  };
50
48
  }
51
49
 
@@ -54,15 +52,14 @@ export function getArtifactPaths(
54
52
  */
55
53
  export async function writeArtifacts(
56
54
  dir: string,
57
- agentName: string,
58
- index: number,
55
+ taskId: string,
59
56
  input: string,
60
57
  output: string,
61
58
  jsonlEvents?: string[],
62
59
  ): Promise<{ inputPath: string; outputPath: string; jsonlPath?: string }> {
63
60
  ensureArtifactsDir(dir);
64
61
 
65
- const paths = getArtifactPaths(dir, agentName, index);
62
+ const paths = getArtifactPaths(dir, taskId);
66
63
 
67
64
  // Write input
68
65
  await fs.promises.writeFile(paths.inputPath, input, "utf-8");
@@ -28,8 +28,10 @@ export interface ExecutorOptions {
28
28
  task: string;
29
29
  description?: string;
30
30
  index: number;
31
+ taskId: string;
31
32
  context?: string;
32
33
  modelOverride?: string;
34
+ outputSchema?: unknown;
33
35
  signal?: AbortSignal;
34
36
  onProgress?: (progress: AgentProgress) => void;
35
37
  sessionFile?: string | null;
@@ -130,12 +132,13 @@ function getUsageTokens(usage: unknown): number {
130
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,
@@ -177,7 +181,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
177
181
 
178
182
  if (options.artifactsDir) {
179
183
  ensureArtifactsDir(options.artifactsDir);
180
- artifactPaths = getArtifactPaths(options.artifactsDir, agent.name, index);
184
+ artifactPaths = getArtifactPaths(options.artifactsDir, taskId);
181
185
  subtaskSessionFile = artifactPaths.jsonlPath;
182
186
 
183
187
  // Write input file immediately (real-time visibility)
@@ -203,13 +207,39 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
203
207
  const sessionFile = subtaskSessionFile ?? options.sessionFile ?? null;
204
208
  const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
205
209
 
206
- const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
210
+ let worker: Worker;
211
+ try {
212
+ worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
213
+ } catch (err) {
214
+ return {
215
+ index,
216
+ taskId,
217
+ agent: agent.name,
218
+ agentSource: agent.source,
219
+ task,
220
+ description: options.description,
221
+ exitCode: 1,
222
+ output: "",
223
+ stderr: `Failed to create worker: ${err instanceof Error ? err.message : String(err)}`,
224
+ truncated: false,
225
+ durationMs: Date.now() - startTime,
226
+ tokens: 0,
227
+ modelOverride,
228
+ error: `Failed to create worker: ${err instanceof Error ? err.message : String(err)}`,
229
+ };
230
+ }
207
231
 
208
232
  let output = "";
209
233
  let stderr = "";
210
234
  let finalOutput = "";
211
235
  let resolved = false;
212
236
  let pendingTermination = false; // Set when shouldTerminate fires, wait for message_end
237
+ type AbortReason = "signal" | "terminate";
238
+ let abortSent = false;
239
+ let abortReason: AbortReason | undefined;
240
+ let abortTerminateTimer: ReturnType<typeof setTimeout> | undefined;
241
+ let pendingTerminationTimer: ReturnType<typeof setTimeout> | undefined;
242
+ let finalize: ((message: Extract<SubagentWorkerResponse, { type: "done" }>) => void) | null = null;
213
243
 
214
244
  // Accumulate usage incrementally from message_end events (no memory for streaming events)
215
245
  const accumulatedUsage = {
@@ -222,22 +252,55 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
222
252
  };
223
253
  let hasUsage = false;
224
254
 
225
- let abortSent = false;
226
- const requestAbort = () => {
227
- if (abortSent) return;
255
+ const clearTimers = (): void => {
256
+ if (abortTerminateTimer) clearTimeout(abortTerminateTimer);
257
+ abortTerminateTimer = undefined;
258
+ if (pendingTerminationTimer) clearTimeout(pendingTerminationTimer);
259
+ pendingTerminationTimer = undefined;
260
+ };
261
+
262
+ const requestAbort = (reason: AbortReason) => {
263
+ if (abortSent) {
264
+ if (reason === "signal" && abortReason !== "signal") {
265
+ abortReason = "signal";
266
+ }
267
+ return;
268
+ }
269
+ if (resolved) return;
228
270
  abortSent = true;
271
+ abortReason = reason;
272
+ if (pendingTerminationTimer) clearTimeout(pendingTerminationTimer);
273
+ pendingTerminationTimer = undefined;
229
274
  const abortMessage: SubagentWorkerRequest = { type: "abort" };
230
- worker.postMessage(abortMessage);
231
- setTimeout(() => {
275
+ try {
276
+ worker.postMessage(abortMessage);
277
+ } catch {
278
+ // Worker already terminated, nothing to do
279
+ }
280
+ if (abortTerminateTimer) clearTimeout(abortTerminateTimer);
281
+ abortTerminateTimer = setTimeout(() => {
232
282
  if (!resolved) {
233
- worker.terminate();
283
+ try {
284
+ worker.terminate();
285
+ } catch {
286
+ // Ignore termination errors
287
+ }
288
+ if (finalize && !resolved) {
289
+ finalize({
290
+ type: "done",
291
+ exitCode: 1,
292
+ durationMs: Date.now() - startTime,
293
+ error: reason === "signal" ? "Aborted" : "Worker terminated after tool completion",
294
+ aborted: reason === "signal",
295
+ });
296
+ }
234
297
  }
235
298
  }, 2000);
236
299
  };
237
300
 
238
301
  // Handle abort signal
239
302
  const onAbort = () => {
240
- if (!resolved) requestAbort();
303
+ if (!resolved) requestAbort("signal");
241
304
  };
242
305
  if (signal) {
243
306
  signal.addEventListener("abort", onAbort, { once: true });
@@ -345,9 +408,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
345
408
  // Don't terminate immediately - wait for message_end to get token counts
346
409
  pendingTermination = true;
347
410
  // Safety timeout in case message_end never arrives
348
- setTimeout(() => {
411
+ if (pendingTerminationTimer) clearTimeout(pendingTerminationTimer);
412
+ pendingTerminationTimer = setTimeout(() => {
349
413
  if (!resolved) {
350
- requestAbort();
414
+ requestAbort("terminate");
351
415
  }
352
416
  }, 2000);
353
417
  }
@@ -417,7 +481,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
417
481
  }
418
482
  // If pending termination, now we have tokens - terminate
419
483
  if (pendingTermination && !resolved) {
420
- requestAbort();
484
+ requestAbort("terminate");
421
485
  }
422
486
  break;
423
487
  }
@@ -451,6 +515,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
451
515
  systemPrompt: agent.systemPrompt,
452
516
  model: resolvedModel,
453
517
  toolNames,
518
+ outputSchema,
454
519
  sessionFile,
455
520
  spawnsEnv,
456
521
  },
@@ -464,46 +529,145 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
464
529
  }
465
530
 
466
531
  const done = await new Promise<Extract<SubagentWorkerResponse, { type: "done" }>>((resolve) => {
532
+ const cleanup = () => {
533
+ worker.removeEventListener("message", onMessage);
534
+ worker.removeEventListener("error", onError);
535
+ worker.removeEventListener("close", onClose);
536
+ worker.removeEventListener("messageerror", onMessageError);
537
+ clearTimers();
538
+ };
539
+ finalize = (message) => {
540
+ if (resolved) return;
541
+ resolved = true;
542
+ cleanup();
543
+ resolve(message);
544
+ };
467
545
  const onMessage = (event: WorkerMessageEvent<SubagentWorkerResponse>) => {
468
546
  const message = event.data;
469
547
  if (!message || resolved) return;
470
548
  if (message.type === "event") {
471
- processEvent(message.event);
549
+ try {
550
+ processEvent(message.event);
551
+ } catch (err) {
552
+ finalize?.({
553
+ type: "done",
554
+ exitCode: 1,
555
+ durationMs: Date.now() - startTime,
556
+ error: `Failed to process worker event: ${err instanceof Error ? err.message : String(err)}`,
557
+ });
558
+ }
472
559
  return;
473
560
  }
474
561
  if (message.type === "done") {
475
- resolved = true;
476
- resolve(message);
562
+ finalize?.(message);
477
563
  }
478
564
  };
479
565
  const onError = (event: WorkerErrorEvent) => {
480
- if (resolved) return;
481
- resolved = true;
482
- resolve({
566
+ finalize?.({
483
567
  type: "done",
484
568
  exitCode: 1,
485
569
  durationMs: Date.now() - startTime,
486
570
  error: event.message,
487
571
  });
488
572
  };
573
+ const onMessageError = () => {
574
+ finalize?.({
575
+ type: "done",
576
+ exitCode: 1,
577
+ durationMs: Date.now() - startTime,
578
+ error: "Worker message deserialization failed",
579
+ });
580
+ };
581
+ const onClose = () => {
582
+ // Worker terminated unexpectedly (crashed or was killed without sending done)
583
+ const abortMessage =
584
+ abortSent && abortReason === "signal"
585
+ ? "Worker terminated after abort"
586
+ : abortSent
587
+ ? "Worker terminated after tool completion"
588
+ : "Worker terminated unexpectedly";
589
+ finalize?.({
590
+ type: "done",
591
+ exitCode: 1,
592
+ durationMs: Date.now() - startTime,
593
+ error: abortMessage,
594
+ aborted: abortReason === "signal",
595
+ });
596
+ };
489
597
  worker.addEventListener("message", onMessage);
490
598
  worker.addEventListener("error", onError);
491
- worker.postMessage(startMessage);
599
+ worker.addEventListener("close", onClose);
600
+ worker.addEventListener("messageerror", onMessageError);
601
+ try {
602
+ worker.postMessage(startMessage);
603
+ } catch (err) {
604
+ finalize({
605
+ type: "done",
606
+ exitCode: 1,
607
+ durationMs: Date.now() - startTime,
608
+ error: `Failed to start worker: ${err instanceof Error ? err.message : String(err)}`,
609
+ });
610
+ }
492
611
  });
493
612
 
494
613
  // Cleanup
495
614
  if (signal) {
496
615
  signal.removeEventListener("abort", onAbort);
497
616
  }
498
- worker.terminate();
617
+ try {
618
+ worker.terminate();
619
+ } catch {
620
+ // Ignore termination errors
621
+ }
499
622
 
500
- const exitCode = done.exitCode;
623
+ let exitCode = done.exitCode;
501
624
  if (done.error) {
502
625
  stderr = done.error;
503
626
  }
504
627
 
505
628
  // Use final output if available, otherwise accumulated output
506
- const rawOutput = finalOutput || output;
629
+ let rawOutput = finalOutput || output;
630
+ let abortedViaComplete = false;
631
+ const completeItems = progress.extractedToolData?.complete as
632
+ | Array<{ data?: unknown; status?: "success" | "aborted"; error?: string }>
633
+ | undefined;
634
+ const hasComplete = Array.isArray(completeItems) && completeItems.length > 0;
635
+ if (hasComplete) {
636
+ const lastComplete = completeItems[completeItems.length - 1];
637
+ if (lastComplete?.status === "aborted") {
638
+ // Agent explicitly aborted via complete tool - clean exit with error info
639
+ abortedViaComplete = true;
640
+ exitCode = 0;
641
+ stderr = lastComplete.error || "Subagent aborted task";
642
+ try {
643
+ rawOutput = JSON.stringify({ aborted: true, error: lastComplete.error }, null, 2);
644
+ } catch {
645
+ rawOutput = `{"aborted":true,"error":"${lastComplete.error || "Unknown error"}"}`;
646
+ }
647
+ } else {
648
+ // Normal successful completion
649
+ let completeData = lastComplete?.data ?? null;
650
+ // Handle double-stringified JSON (subagent returned JSON string instead of object)
651
+ if (typeof completeData === "string" && (completeData.startsWith("{") || completeData.startsWith("["))) {
652
+ try {
653
+ completeData = JSON.parse(completeData);
654
+ } catch {
655
+ // Not valid JSON, keep as string
656
+ }
657
+ }
658
+ try {
659
+ rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
660
+ } catch (err) {
661
+ const errorMessage = err instanceof Error ? err.message : String(err);
662
+ rawOutput = `{"error":"Failed to serialize complete data: ${errorMessage}"}`;
663
+ }
664
+ exitCode = 0;
665
+ stderr = "";
666
+ }
667
+ } else {
668
+ const warning = "SYSTEM WARNING: Subagent exited without calling complete tool after 3 reminders.";
669
+ rawOutput = rawOutput ? `${warning}\n\n${rawOutput}` : warning;
670
+ }
507
671
  const { text: truncatedOutput, truncated } = truncateOutput(rawOutput);
508
672
 
509
673
  // Write output artifact (input and jsonl already written in real-time)
@@ -522,12 +686,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
522
686
  }
523
687
 
524
688
  // Update final progress
525
- const wasAborted = done.aborted || signal?.aborted || false;
689
+ const wasAborted = abortedViaComplete || (!hasComplete && (done.aborted || signal?.aborted || false));
526
690
  progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
527
691
  emitProgress();
528
692
 
529
693
  return {
530
694
  index,
695
+ taskId,
531
696
  agent: agent.name,
532
697
  agentSource: agent.source,
533
698
  task,