@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.
- package/CHANGELOG.md +55 -1
- package/docs/sdk.md +47 -50
- package/examples/custom-tools/README.md +0 -15
- package/examples/hooks/custom-compaction.ts +1 -3
- package/examples/sdk/README.md +6 -10
- package/package.json +5 -5
- package/src/cli/args.ts +9 -6
- package/src/core/agent-session.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +3 -0
- package/src/core/custom-tools/wrapper.ts +0 -1
- package/src/core/extensions/index.ts +1 -6
- package/src/core/extensions/wrapper.ts +0 -7
- package/src/core/file-mentions.ts +5 -8
- package/src/core/sdk.ts +48 -111
- package/src/core/session-manager.ts +7 -0
- package/src/core/system-prompt.ts +22 -33
- package/src/core/tools/ask.ts +14 -7
- package/src/core/tools/bash-interceptor.ts +4 -4
- package/src/core/tools/bash.ts +19 -9
- package/src/core/tools/complete.ts +131 -0
- package/src/core/tools/context.ts +7 -0
- package/src/core/tools/edit.ts +8 -15
- package/src/core/tools/exa/render.ts +4 -16
- package/src/core/tools/find.ts +7 -18
- package/src/core/tools/git.ts +13 -3
- package/src/core/tools/grep.ts +7 -18
- package/src/core/tools/index.test.ts +188 -0
- package/src/core/tools/index.ts +106 -236
- package/src/core/tools/jtd-to-json-schema.ts +274 -0
- package/src/core/tools/ls.ts +4 -9
- package/src/core/tools/lsp/index.ts +32 -29
- package/src/core/tools/lsp/render.ts +7 -28
- package/src/core/tools/notebook.ts +3 -5
- package/src/core/tools/output.ts +130 -31
- package/src/core/tools/read.ts +8 -19
- package/src/core/tools/review.ts +0 -18
- package/src/core/tools/rulebook.ts +8 -2
- package/src/core/tools/task/agents.ts +28 -7
- package/src/core/tools/task/artifacts.ts +6 -9
- package/src/core/tools/task/discovery.ts +0 -6
- package/src/core/tools/task/executor.ts +306 -257
- package/src/core/tools/task/index.ts +65 -235
- package/src/core/tools/task/name-generator.ts +247 -0
- package/src/core/tools/task/render.ts +158 -19
- package/src/core/tools/task/types.ts +13 -11
- package/src/core/tools/task/worker-protocol.ts +18 -0
- package/src/core/tools/task/worker.ts +270 -0
- package/src/core/tools/web-fetch.ts +4 -36
- package/src/core/tools/web-search/index.ts +2 -1
- package/src/core/tools/web-search/render.ts +1 -4
- package/src/core/tools/write.ts +7 -15
- package/src/discovery/helpers.test.ts +1 -1
- package/src/index.ts +5 -16
- package/src/main.ts +4 -4
- package/src/modes/interactive/theme/theme.ts +4 -4
- package/src/prompts/task.md +14 -57
- package/src/prompts/tools/output.md +4 -3
- package/src/prompts/tools/task.md +70 -0
- package/examples/custom-tools/question/index.ts +0 -84
- package/examples/custom-tools/subagent/README.md +0 -172
- package/examples/custom-tools/subagent/agents/planner.md +0 -37
- package/examples/custom-tools/subagent/agents/scout.md +0 -50
- package/examples/custom-tools/subagent/agents/worker.md +0 -24
- package/examples/custom-tools/subagent/agents.ts +0 -156
- package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
- package/examples/custom-tools/subagent/commands/implement.md +0 -10
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
- package/examples/custom-tools/subagent/index.ts +0 -1002
- package/examples/sdk/05-tools.ts +0 -94
- package/examples/sdk/12-full-control.ts +0 -95
- package/src/prompts/browser.md +0 -71
|
@@ -1,41 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Worker execution for subagents.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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 {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
198
|
+
toolNames = agent.tools;
|
|
227
199
|
// Auto-include task tool if spawns defined but task not in tools
|
|
228
|
-
if (agent.spawns !== undefined && !
|
|
229
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
278
|
+
const processEvent = (event: AgentEvent) => {
|
|
300
279
|
if (resolved) return;
|
|
301
280
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
progress.
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
|
563
|
+
const wasAborted = abortedViaComplete || (!hasComplete && (done.aborted || signal?.aborted || false));
|
|
515
564
|
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
516
|
-
|
|
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
|
-
|
|
583
|
+
usage: hasUsage ? accumulatedUsage : undefined,
|
|
535
584
|
artifactPaths,
|
|
536
585
|
extractedToolData: progress.extractedToolData,
|
|
537
586
|
outputMeta,
|