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