@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.
- package/CHANGELOG.md +34 -0
- package/package.json +4 -4
- package/src/core/custom-commands/bundled/wt/index.ts +3 -0
- package/src/core/sdk.ts +7 -0
- package/src/core/tools/complete.ts +129 -0
- package/src/core/tools/index.test.ts +9 -1
- package/src/core/tools/index.ts +18 -5
- package/src/core/tools/jtd-to-json-schema.ts +252 -0
- package/src/core/tools/output.ts +125 -14
- package/src/core/tools/read.ts +4 -4
- package/src/core/tools/task/artifacts.ts +6 -9
- package/src/core/tools/task/executor.ts +189 -24
- package/src/core/tools/task/index.ts +23 -18
- package/src/core/tools/task/name-generator.ts +1577 -0
- package/src/core/tools/task/render.ts +137 -8
- package/src/core/tools/task/types.ts +26 -5
- package/src/core/tools/task/worker-protocol.ts +1 -0
- package/src/core/tools/task/worker.ts +136 -14
- package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
- package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
- package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
- package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
- package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
- package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
- package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
- package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
- package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
- package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
- package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
- package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
- package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
- package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
- package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
- package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
- package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
- package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
- package/src/core/tools/web-fetch-handlers/github.ts +424 -0
- package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
- package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
- package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
- package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
- package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
- package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
- package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
- package/src/core/tools/web-fetch-handlers/index.ts +69 -0
- package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
- package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
- package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
- package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
- package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
- package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
- package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
- package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
- package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
- package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
- package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
- package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
- package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
- package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
- package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
- package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
- package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
- package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
- package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
- package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
- package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
- package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
- package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
- package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
- package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
- package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
- package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
- package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
- package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
- package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
- package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
- package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
- package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
- package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
- package/src/core/tools/web-fetch-handlers/types.ts +163 -0
- package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
- package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
- package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
- package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
- package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
- package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
- package/src/core/tools/web-fetch.ts +152 -1324
- package/src/prompts/task.md +14 -50
- package/src/prompts/tools/output.md +2 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/utils/tools-manager.ts +110 -8
package/src/core/tools/output.ts
CHANGED
|
@@ -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: {
|
|
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
|
|
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 (
|
|
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 =
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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);
|
package/src/core/tools/read.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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, `${
|
|
47
|
-
outputPath: path.join(dir, `${
|
|
48
|
-
jsonlPath: path.join(dir, `${
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
resolve(message);
|
|
562
|
+
finalize?.(message);
|
|
477
563
|
}
|
|
478
564
|
};
|
|
479
565
|
const onError = (event: WorkerErrorEvent) => {
|
|
480
|
-
|
|
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.
|
|
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
|
-
|
|
617
|
+
try {
|
|
618
|
+
worker.terminate();
|
|
619
|
+
} catch {
|
|
620
|
+
// Ignore termination errors
|
|
621
|
+
}
|
|
499
622
|
|
|
500
|
-
|
|
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
|
-
|
|
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,
|