oira666_pi-subagent 0.1.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/LICENSE +21 -0
- package/README.md +327 -0
- package/agents/code-architect.md +24 -0
- package/agents/code-reviwer.md +23 -0
- package/agents/code-writer.md +18 -0
- package/agents.ts +185 -0
- package/index.ts +865 -0
- package/package.json +68 -0
- package/render.ts +595 -0
- package/runner.ts +419 -0
- package/types.ts +135 -0
package/runner.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent process runner.
|
|
3
|
+
*
|
|
4
|
+
* Spawns isolated `pi` processes and streams results back via callbacks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
12
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
13
|
+
import type { AgentConfig } from "./agents.js";
|
|
14
|
+
import {
|
|
15
|
+
type DelegationMode,
|
|
16
|
+
type SingleResult,
|
|
17
|
+
type SubagentDetails,
|
|
18
|
+
emptyUsage,
|
|
19
|
+
getFinalOutput,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
|
|
22
|
+
const SIGKILL_TIMEOUT_MS = 5000;
|
|
23
|
+
const SUBAGENT_DEPTH_ENV = "PI_SUBAGENT_DEPTH";
|
|
24
|
+
const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
|
|
25
|
+
const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
|
|
26
|
+
const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
|
|
27
|
+
const PI_OFFLINE_ENV = "PI_OFFLINE";
|
|
28
|
+
|
|
29
|
+
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Temp file helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function writePromptToTempFile(
|
|
36
|
+
agentName: string,
|
|
37
|
+
prompt: string,
|
|
38
|
+
): { dir: string; filePath: string } {
|
|
39
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
40
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
41
|
+
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
42
|
+
fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
43
|
+
return { dir: tmpDir, filePath };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeForkSessionToTempFile(
|
|
47
|
+
agentName: string,
|
|
48
|
+
sessionJsonl: string,
|
|
49
|
+
): { dir: string; filePath: string } {
|
|
50
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
51
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
52
|
+
const filePath = path.join(tmpDir, `fork-${safeName}.jsonl`);
|
|
53
|
+
fs.writeFileSync(filePath, sessionJsonl, { encoding: "utf-8", mode: 0o600 });
|
|
54
|
+
return { dir: tmpDir, filePath };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cleanupTempDir(dir: string | null): void {
|
|
58
|
+
if (!dir) return;
|
|
59
|
+
try {
|
|
60
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
61
|
+
} catch {
|
|
62
|
+
/* ignore */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveExtensionArg(value: string): string {
|
|
67
|
+
if (!value) return value;
|
|
68
|
+
if (value.startsWith("npm:") || value.startsWith("git:")) return value;
|
|
69
|
+
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
|
|
70
|
+
if (path.isAbsolute(value)) return value;
|
|
71
|
+
|
|
72
|
+
const resolved = path.resolve(process.cwd(), value);
|
|
73
|
+
return fs.existsSync(resolved) ? resolved : value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getInheritedExtensionArgs(argv: string[]): string[] {
|
|
77
|
+
const args: string[] = [];
|
|
78
|
+
for (let i = 2; i < argv.length; i++) {
|
|
79
|
+
const arg = argv[i];
|
|
80
|
+
|
|
81
|
+
if (arg === "--no-extensions" || arg === "-ne") {
|
|
82
|
+
args.push("--no-extensions");
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (arg === "--extension" || arg === "-e") {
|
|
87
|
+
const value = argv[i + 1];
|
|
88
|
+
if (value !== undefined) {
|
|
89
|
+
args.push("--extension", resolveExtensionArg(value));
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (arg.startsWith("--extension=")) {
|
|
96
|
+
args.push("--extension", resolveExtensionArg(arg.slice("--extension=".length)));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return args;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// JSON-line stream processing
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function processJsonLine(line: string, result: SingleResult): boolean {
|
|
107
|
+
if (!line.trim()) return false;
|
|
108
|
+
|
|
109
|
+
let event: any;
|
|
110
|
+
try {
|
|
111
|
+
event = JSON.parse(line);
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (event.type === "message_end" && event.message) {
|
|
117
|
+
const msg = event.message as Message;
|
|
118
|
+
result.messages.push(msg);
|
|
119
|
+
|
|
120
|
+
if (msg.role === "assistant") {
|
|
121
|
+
result.usage.turns++;
|
|
122
|
+
const usage = msg.usage;
|
|
123
|
+
if (usage) {
|
|
124
|
+
result.usage.input += usage.input || 0;
|
|
125
|
+
result.usage.output += usage.output || 0;
|
|
126
|
+
result.usage.cacheRead += usage.cacheRead || 0;
|
|
127
|
+
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
128
|
+
result.usage.cost += usage.cost?.total || 0;
|
|
129
|
+
result.usage.contextTokens = usage.totalTokens || 0;
|
|
130
|
+
}
|
|
131
|
+
if (!result.model && msg.model) result.model = msg.model;
|
|
132
|
+
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
133
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
139
|
+
result.messages.push(event.message as Message);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Build pi CLI arguments
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
function buildPiArgs(
|
|
151
|
+
agent: AgentConfig,
|
|
152
|
+
systemPromptPath: string | null,
|
|
153
|
+
task: string,
|
|
154
|
+
delegationMode: DelegationMode,
|
|
155
|
+
forkSessionPath: string | null,
|
|
156
|
+
): string[] {
|
|
157
|
+
const args: string[] = [
|
|
158
|
+
"--mode",
|
|
159
|
+
"json",
|
|
160
|
+
...getInheritedExtensionArgs(process.argv),
|
|
161
|
+
"-p",
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
if (delegationMode === "spawn") {
|
|
165
|
+
args.push("--no-session");
|
|
166
|
+
} else if (forkSessionPath) {
|
|
167
|
+
args.push("--session", forkSessionPath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (agent.model) args.push("--model", agent.model);
|
|
171
|
+
if (agent.thinking) args.push("--thinking", agent.thinking);
|
|
172
|
+
if (agent.tools && agent.tools.length > 0)
|
|
173
|
+
args.push("--tools", agent.tools.join(","));
|
|
174
|
+
if (systemPromptPath) args.push("--append-system-prompt", systemPromptPath);
|
|
175
|
+
args.push(`Task: ${task}`);
|
|
176
|
+
return args;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Public API
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
export interface RunAgentOptions {
|
|
184
|
+
/** Fallback working directory when the task doesn't specify one. */
|
|
185
|
+
cwd: string;
|
|
186
|
+
/** All available agent configs. */
|
|
187
|
+
agents: AgentConfig[];
|
|
188
|
+
/** Name of the agent to run. */
|
|
189
|
+
agentName: string;
|
|
190
|
+
/** Task description. */
|
|
191
|
+
task: string;
|
|
192
|
+
/** Optional override working directory. */
|
|
193
|
+
taskCwd?: string;
|
|
194
|
+
/** Context mode: spawn (fresh) or fork (session snapshot + task). */
|
|
195
|
+
delegationMode: DelegationMode;
|
|
196
|
+
/** Serialized parent session snapshot used when delegationMode is "fork". */
|
|
197
|
+
forkSessionSnapshotJsonl?: string;
|
|
198
|
+
/** Current delegation depth of the caller process. */
|
|
199
|
+
parentDepth: number;
|
|
200
|
+
/** Delegation stack from the caller process (ancestor agent names). */
|
|
201
|
+
parentAgentStack: string[];
|
|
202
|
+
/** Maximum allowed delegation depth to propagate to child processes. */
|
|
203
|
+
maxDepth: number;
|
|
204
|
+
/** Whether cycle prevention should be enforced in child processes. */
|
|
205
|
+
preventCycles: boolean;
|
|
206
|
+
/** Abort signal for cancellation. */
|
|
207
|
+
signal?: AbortSignal;
|
|
208
|
+
/** Streaming update callback. */
|
|
209
|
+
onUpdate?: OnUpdateCallback;
|
|
210
|
+
/** Factory to wrap results into SubagentDetails. */
|
|
211
|
+
makeDetails: (results: SingleResult[]) => SubagentDetails;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Spawn a single subagent process and collect its results.
|
|
216
|
+
*
|
|
217
|
+
* Returns a SingleResult even on failure (exitCode > 0, stderr populated).
|
|
218
|
+
*/
|
|
219
|
+
export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
|
|
220
|
+
const {
|
|
221
|
+
cwd,
|
|
222
|
+
agents,
|
|
223
|
+
agentName,
|
|
224
|
+
task,
|
|
225
|
+
taskCwd,
|
|
226
|
+
delegationMode,
|
|
227
|
+
forkSessionSnapshotJsonl,
|
|
228
|
+
parentDepth,
|
|
229
|
+
parentAgentStack,
|
|
230
|
+
maxDepth,
|
|
231
|
+
preventCycles,
|
|
232
|
+
signal,
|
|
233
|
+
onUpdate,
|
|
234
|
+
makeDetails,
|
|
235
|
+
} = opts;
|
|
236
|
+
|
|
237
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
238
|
+
if (!agent) {
|
|
239
|
+
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
240
|
+
return {
|
|
241
|
+
agent: agentName,
|
|
242
|
+
agentSource: "unknown",
|
|
243
|
+
task,
|
|
244
|
+
exitCode: 1,
|
|
245
|
+
messages: [],
|
|
246
|
+
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
247
|
+
usage: emptyUsage(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (
|
|
252
|
+
delegationMode === "fork" &&
|
|
253
|
+
(!forkSessionSnapshotJsonl || !forkSessionSnapshotJsonl.trim())
|
|
254
|
+
) {
|
|
255
|
+
return {
|
|
256
|
+
agent: agentName,
|
|
257
|
+
agentSource: agent.source,
|
|
258
|
+
task,
|
|
259
|
+
exitCode: 1,
|
|
260
|
+
messages: [],
|
|
261
|
+
stderr:
|
|
262
|
+
"Cannot run in fork mode: missing parent session snapshot context.",
|
|
263
|
+
usage: emptyUsage(),
|
|
264
|
+
model: agent.model,
|
|
265
|
+
stopReason: "error",
|
|
266
|
+
errorMessage:
|
|
267
|
+
"Cannot run in fork mode: missing parent session snapshot context.",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const result: SingleResult = {
|
|
272
|
+
agent: agentName,
|
|
273
|
+
agentSource: agent.source,
|
|
274
|
+
task,
|
|
275
|
+
exitCode: -1,
|
|
276
|
+
messages: [],
|
|
277
|
+
stderr: "",
|
|
278
|
+
usage: emptyUsage(),
|
|
279
|
+
model: agent.model,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const emitUpdate = () => {
|
|
283
|
+
onUpdate?.({
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: "text",
|
|
287
|
+
text: getFinalOutput(result.messages) || "(running...)",
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
details: makeDetails([result]),
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Write system prompt to temp file if needed
|
|
295
|
+
let promptTmpDir: string | null = null;
|
|
296
|
+
let promptTmpPath: string | null = null;
|
|
297
|
+
if (agent.systemPrompt.trim()) {
|
|
298
|
+
const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
299
|
+
promptTmpDir = tmp.dir;
|
|
300
|
+
promptTmpPath = tmp.filePath;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Write forked session snapshot if needed
|
|
304
|
+
let forkSessionTmpDir: string | null = null;
|
|
305
|
+
let forkSessionTmpPath: string | null = null;
|
|
306
|
+
if (delegationMode === "fork" && forkSessionSnapshotJsonl) {
|
|
307
|
+
const tmp = writeForkSessionToTempFile(agent.name, forkSessionSnapshotJsonl);
|
|
308
|
+
forkSessionTmpDir = tmp.dir;
|
|
309
|
+
forkSessionTmpPath = tmp.filePath;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const piArgs = buildPiArgs(
|
|
314
|
+
agent,
|
|
315
|
+
promptTmpPath,
|
|
316
|
+
task,
|
|
317
|
+
delegationMode,
|
|
318
|
+
forkSessionTmpPath,
|
|
319
|
+
);
|
|
320
|
+
let wasAborted = false;
|
|
321
|
+
|
|
322
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
323
|
+
const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
324
|
+
const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
|
|
325
|
+
const propagatedStack = [...parentAgentStack, agentName];
|
|
326
|
+
const proc = spawn("pi", piArgs, {
|
|
327
|
+
cwd: taskCwd ?? cwd,
|
|
328
|
+
shell: false,
|
|
329
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
330
|
+
env: {
|
|
331
|
+
...process.env,
|
|
332
|
+
[SUBAGENT_DEPTH_ENV]: String(nextDepth),
|
|
333
|
+
[SUBAGENT_MAX_DEPTH_ENV]: String(propagatedMaxDepth),
|
|
334
|
+
[SUBAGENT_STACK_ENV]: JSON.stringify(propagatedStack),
|
|
335
|
+
[SUBAGENT_PREVENT_CYCLES_ENV]: preventCycles ? "1" : "0",
|
|
336
|
+
[PI_OFFLINE_ENV]: "1",
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
let buffer = "";
|
|
341
|
+
|
|
342
|
+
const flushLine = (line: string) => {
|
|
343
|
+
if (processJsonLine(line, result)) emitUpdate();
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
347
|
+
buffer += chunk.toString();
|
|
348
|
+
const lines = buffer.split("\n");
|
|
349
|
+
buffer = lines.pop() || "";
|
|
350
|
+
for (const line of lines) flushLine(line);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
354
|
+
result.stderr += chunk.toString();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
proc.on("close", (code) => {
|
|
358
|
+
if (buffer.trim()) flushLine(buffer);
|
|
359
|
+
resolve(code ?? 0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
proc.on("error", () => resolve(1));
|
|
363
|
+
|
|
364
|
+
// Abort handling
|
|
365
|
+
if (signal) {
|
|
366
|
+
const kill = () => {
|
|
367
|
+
wasAborted = true;
|
|
368
|
+
proc.kill("SIGTERM");
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
371
|
+
}, SIGKILL_TIMEOUT_MS);
|
|
372
|
+
};
|
|
373
|
+
if (signal.aborted) kill();
|
|
374
|
+
else signal.addEventListener("abort", kill, { once: true });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
result.exitCode = exitCode;
|
|
379
|
+
if (wasAborted) {
|
|
380
|
+
result.exitCode = 130;
|
|
381
|
+
result.stopReason = "aborted";
|
|
382
|
+
result.errorMessage = "Subagent was aborted.";
|
|
383
|
+
if (!result.stderr.trim()) result.stderr = "Subagent was aborted.";
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
} finally {
|
|
387
|
+
cleanupTempDir(promptTmpDir);
|
|
388
|
+
cleanupTempDir(forkSessionTmpDir);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// Concurrency helper
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Map over items with a bounded number of concurrent workers.
|
|
398
|
+
*/
|
|
399
|
+
export async function mapConcurrent<TIn, TOut>(
|
|
400
|
+
items: TIn[],
|
|
401
|
+
concurrency: number,
|
|
402
|
+
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
403
|
+
): Promise<TOut[]> {
|
|
404
|
+
if (items.length === 0) return [];
|
|
405
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
406
|
+
const results: TOut[] = new Array(items.length);
|
|
407
|
+
let nextIndex = 0;
|
|
408
|
+
|
|
409
|
+
const worker = async () => {
|
|
410
|
+
while (true) {
|
|
411
|
+
const i = nextIndex++;
|
|
412
|
+
if (i >= items.length) return;
|
|
413
|
+
results[i] = await fn(items[i], i);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
418
|
+
return results;
|
|
419
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for the subagent extension.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
6
|
+
|
|
7
|
+
/** Context mode for delegated runs. */
|
|
8
|
+
export type DelegationMode = "spawn" | "fork";
|
|
9
|
+
|
|
10
|
+
/** Default context mode for delegated runs. */
|
|
11
|
+
export const DEFAULT_DELEGATION_MODE: DelegationMode = "spawn";
|
|
12
|
+
|
|
13
|
+
/** Aggregated token usage from a subagent run. */
|
|
14
|
+
export interface UsageStats {
|
|
15
|
+
input: number;
|
|
16
|
+
output: number;
|
|
17
|
+
cacheRead: number;
|
|
18
|
+
cacheWrite: number;
|
|
19
|
+
cost: number;
|
|
20
|
+
contextTokens: number;
|
|
21
|
+
turns: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Result of a single subagent invocation. */
|
|
25
|
+
export interface SingleResult {
|
|
26
|
+
agent: string;
|
|
27
|
+
agentSource: "user" | "project" | "builtin" | "unknown";
|
|
28
|
+
task: string;
|
|
29
|
+
exitCode: number;
|
|
30
|
+
messages: Message[];
|
|
31
|
+
stderr: string;
|
|
32
|
+
usage: UsageStats;
|
|
33
|
+
model?: string;
|
|
34
|
+
stopReason?: string;
|
|
35
|
+
errorMessage?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Metadata attached to every tool result for rendering. */
|
|
39
|
+
export interface SubagentDetails {
|
|
40
|
+
mode: "single" | "parallel";
|
|
41
|
+
delegationMode: DelegationMode;
|
|
42
|
+
projectAgentsDir: string | null;
|
|
43
|
+
results: SingleResult[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Nested subagent tool result captured from a delegated run. */
|
|
47
|
+
export interface NestedSubagentResult {
|
|
48
|
+
details: SubagentDetails;
|
|
49
|
+
isError: boolean;
|
|
50
|
+
toolCallId: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A display-friendly representation of a message part. */
|
|
54
|
+
export type DisplayItem =
|
|
55
|
+
| { type: "text"; text: string }
|
|
56
|
+
| { type: "toolCall"; name: string; args: Record<string, unknown> };
|
|
57
|
+
|
|
58
|
+
/** Create an empty UsageStats object. */
|
|
59
|
+
export function emptyUsage(): UsageStats {
|
|
60
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Sum usage across multiple results. */
|
|
64
|
+
export function aggregateUsage(results: SingleResult[]): UsageStats {
|
|
65
|
+
const total = emptyUsage();
|
|
66
|
+
for (const r of results) {
|
|
67
|
+
total.input += r.usage.input;
|
|
68
|
+
total.output += r.usage.output;
|
|
69
|
+
total.cacheRead += r.usage.cacheRead;
|
|
70
|
+
total.cacheWrite += r.usage.cacheWrite;
|
|
71
|
+
total.cost += r.usage.cost;
|
|
72
|
+
total.turns += r.usage.turns;
|
|
73
|
+
}
|
|
74
|
+
return total;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Whether a result represents an error. */
|
|
78
|
+
export function isResultError(r: SingleResult): boolean {
|
|
79
|
+
return r.exitCode > 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Check whether a value looks like SubagentDetails. */
|
|
83
|
+
export function isSubagentDetails(value: unknown): value is SubagentDetails {
|
|
84
|
+
if (!value || typeof value !== "object") return false;
|
|
85
|
+
const maybe = value as Partial<SubagentDetails>;
|
|
86
|
+
return (maybe.mode === "single" || maybe.mode === "parallel") &&
|
|
87
|
+
(maybe.delegationMode === "spawn" || maybe.delegationMode === "fork") &&
|
|
88
|
+
Array.isArray(maybe.results);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Extract the last assistant text from a message history. */
|
|
92
|
+
export function getFinalOutput(messages: Message[]): string {
|
|
93
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
94
|
+
const msg = messages[i];
|
|
95
|
+
if (msg.role === "assistant") {
|
|
96
|
+
for (const part of msg.content) {
|
|
97
|
+
if (part.type === "text") return part.text;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Extract all display-worthy items from a message history. */
|
|
105
|
+
export function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
106
|
+
const items: DisplayItem[] = [];
|
|
107
|
+
for (const msg of messages) {
|
|
108
|
+
if (msg.role === "assistant") {
|
|
109
|
+
for (const part of msg.content) {
|
|
110
|
+
if (part.type === "text") {
|
|
111
|
+
items.push({ type: "text", text: part.text });
|
|
112
|
+
} else if (part.type === "toolCall") {
|
|
113
|
+
items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return items;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Extract nested subagent tool results from a message history. */
|
|
122
|
+
export function getNestedSubagentResults(messages: Message[]): NestedSubagentResult[] {
|
|
123
|
+
const results: NestedSubagentResult[] = [];
|
|
124
|
+
for (const msg of messages) {
|
|
125
|
+
if (msg.role !== "toolResult") continue;
|
|
126
|
+
if (msg.toolName !== "subagent") continue;
|
|
127
|
+
if (!isSubagentDetails(msg.details)) continue;
|
|
128
|
+
results.push({
|
|
129
|
+
details: msg.details,
|
|
130
|
+
isError: msg.isError,
|
|
131
|
+
toolCallId: msg.toolCallId,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return results;
|
|
135
|
+
}
|