pi-subagents 0.3.0 → 0.3.1
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 +21 -0
- package/agents.ts +0 -10
- package/async-execution.ts +261 -0
- package/chain-execution.ts +436 -0
- package/execution.ts +352 -0
- package/formatters.ts +111 -0
- package/index.ts +72 -1614
- package/package.json +2 -2
- package/render.ts +283 -0
- package/schemas.ts +90 -0
- package/settings.ts +2 -166
- package/types.ts +166 -0
- package/utils.ts +287 -0
package/index.ts
CHANGED
|
@@ -12,904 +12,41 @@
|
|
|
12
12
|
* { "asyncByDefault": true }
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
16
15
|
import { randomUUID } from "node:crypto";
|
|
17
16
|
import * as fs from "node:fs";
|
|
18
|
-
import { createRequire } from "node:module";
|
|
19
17
|
import * as os from "node:os";
|
|
20
18
|
import * as path from "node:path";
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
23
|
-
import type { Message } from "@mariozechner/pi-ai";
|
|
24
|
-
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition, getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
|
25
|
-
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
26
|
-
import { Type } from "@sinclair/typebox";
|
|
19
|
+
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
20
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
27
21
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
resolveChainTemplatesV2,
|
|
31
|
-
createChainDir,
|
|
32
|
-
removeChainDir,
|
|
33
|
-
cleanupOldChainDirs,
|
|
34
|
-
resolveStepBehavior,
|
|
35
|
-
resolveParallelBehaviors,
|
|
36
|
-
buildChainInstructions,
|
|
37
|
-
createParallelDirs,
|
|
38
|
-
aggregateParallelOutputs,
|
|
39
|
-
isParallelStep,
|
|
40
|
-
isSequentialStep,
|
|
41
|
-
getStepAgents,
|
|
42
|
-
type StepOverrides,
|
|
43
|
-
type ChainStep,
|
|
44
|
-
type SequentialStep,
|
|
45
|
-
type ParallelStep,
|
|
46
|
-
type ParallelTaskResult,
|
|
47
|
-
type ResolvedTemplates,
|
|
48
|
-
} from "./settings.js";
|
|
49
|
-
import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride } from "./chain-clarify.js";
|
|
50
|
-
import {
|
|
51
|
-
appendJsonl,
|
|
52
|
-
cleanupOldArtifacts,
|
|
53
|
-
ensureArtifactsDir,
|
|
54
|
-
getArtifactPaths,
|
|
55
|
-
getArtifactsDir,
|
|
56
|
-
writeArtifact,
|
|
57
|
-
writeMetadata,
|
|
58
|
-
} from "./artifacts.js";
|
|
22
|
+
import { cleanupOldChainDirs, getStepAgents, isParallelStep, type ChainStep, type SequentialStep } from "./settings.js";
|
|
23
|
+
import { cleanupOldArtifacts, getArtifactsDir } from "./artifacts.js";
|
|
59
24
|
import {
|
|
60
25
|
type AgentProgress,
|
|
61
26
|
type ArtifactConfig,
|
|
62
27
|
type ArtifactPaths,
|
|
28
|
+
type AsyncJobState,
|
|
29
|
+
type Details,
|
|
30
|
+
type ExtensionConfig,
|
|
31
|
+
type SingleResult,
|
|
32
|
+
ASYNC_DIR,
|
|
63
33
|
DEFAULT_ARTIFACT_CONFIG,
|
|
64
34
|
DEFAULT_MAX_OUTPUT,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
35
|
+
MAX_CONCURRENCY,
|
|
36
|
+
MAX_PARALLEL,
|
|
37
|
+
POLL_INTERVAL_MS,
|
|
38
|
+
RESULTS_DIR,
|
|
39
|
+
WIDGET_KEY,
|
|
69
40
|
} from "./types.js";
|
|
41
|
+
import { formatDuration } from "./formatters.js";
|
|
42
|
+
import { readStatus, findByPrefix, getFinalOutput, mapConcurrent } from "./utils.js";
|
|
43
|
+
import { runSync } from "./execution.js";
|
|
44
|
+
import { renderWidget, renderSubagentResult } from "./render.js";
|
|
45
|
+
import { SubagentParams, StatusParams } from "./schemas.js";
|
|
46
|
+
import { executeChain } from "./chain-execution.js";
|
|
47
|
+
import { isAsyncAvailable, executeAsyncChain, executeAsyncSingle } from "./async-execution.js";
|
|
70
48
|
|
|
71
|
-
|
|
72
|
-
const MAX_CONCURRENCY = 4;
|
|
73
|
-
const COLLAPSED_ITEMS = 8;
|
|
74
|
-
const RESULTS_DIR = "/tmp/pi-async-subagent-results";
|
|
75
|
-
const ASYNC_DIR = "/tmp/pi-async-subagent-runs";
|
|
76
|
-
const WIDGET_KEY = "subagent-async";
|
|
77
|
-
const POLL_INTERVAL_MS = 1000;
|
|
78
|
-
const MAX_WIDGET_JOBS = 4;
|
|
79
|
-
|
|
80
|
-
const require = createRequire(import.meta.url);
|
|
81
|
-
const jitiCliPath: string | undefined = (() => {
|
|
82
|
-
try {
|
|
83
|
-
return path.join(path.dirname(require.resolve("jiti/package.json")), "lib/jiti-cli.mjs");
|
|
84
|
-
} catch {
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
})();
|
|
88
|
-
|
|
89
|
-
interface Usage {
|
|
90
|
-
input: number;
|
|
91
|
-
output: number;
|
|
92
|
-
cacheRead: number;
|
|
93
|
-
cacheWrite: number;
|
|
94
|
-
cost: number;
|
|
95
|
-
turns: number;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface SingleResult {
|
|
99
|
-
agent: string;
|
|
100
|
-
task: string;
|
|
101
|
-
exitCode: number;
|
|
102
|
-
messages: Message[];
|
|
103
|
-
usage: Usage;
|
|
104
|
-
model?: string;
|
|
105
|
-
error?: string;
|
|
106
|
-
sessionFile?: string;
|
|
107
|
-
// Sharing disabled - module resolution issues
|
|
108
|
-
// shareUrl?: string;
|
|
109
|
-
// gistUrl?: string;
|
|
110
|
-
// shareError?: string;
|
|
111
|
-
progress?: AgentProgress;
|
|
112
|
-
progressSummary?: ProgressSummary;
|
|
113
|
-
artifactPaths?: ArtifactPaths;
|
|
114
|
-
truncation?: TruncationResult;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
interface Details {
|
|
118
|
-
mode: "single" | "parallel" | "chain";
|
|
119
|
-
results: SingleResult[];
|
|
120
|
-
asyncId?: string;
|
|
121
|
-
asyncDir?: string;
|
|
122
|
-
progress?: AgentProgress[];
|
|
123
|
-
progressSummary?: ProgressSummary;
|
|
124
|
-
artifacts?: {
|
|
125
|
-
dir: string;
|
|
126
|
-
files: ArtifactPaths[];
|
|
127
|
-
};
|
|
128
|
-
truncation?: {
|
|
129
|
-
truncated: boolean;
|
|
130
|
-
originalBytes?: number;
|
|
131
|
-
originalLines?: number;
|
|
132
|
-
artifactPath?: string;
|
|
133
|
-
};
|
|
134
|
-
// Chain metadata for observability
|
|
135
|
-
chainAgents?: string[]; // Agent names in order, e.g., ["scout", "planner"]
|
|
136
|
-
totalSteps?: number; // Total steps in chain
|
|
137
|
-
currentStepIndex?: number; // 0-indexed current step (for running chains)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
type DisplayItem = { type: "text"; text: string } | { type: "tool"; name: string; args: Record<string, unknown> };
|
|
141
|
-
|
|
142
|
-
interface TokenUsage {
|
|
143
|
-
input: number;
|
|
144
|
-
output: number;
|
|
145
|
-
total: number;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
interface AsyncStatus {
|
|
149
|
-
runId: string;
|
|
150
|
-
mode: "single" | "chain";
|
|
151
|
-
state: "queued" | "running" | "complete" | "failed";
|
|
152
|
-
startedAt: number;
|
|
153
|
-
endedAt?: number;
|
|
154
|
-
lastUpdate?: number;
|
|
155
|
-
currentStep?: number;
|
|
156
|
-
steps?: Array<{ agent: string; status: string; durationMs?: number; tokens?: TokenUsage }>;
|
|
157
|
-
sessionDir?: string;
|
|
158
|
-
outputFile?: string;
|
|
159
|
-
totalTokens?: TokenUsage;
|
|
160
|
-
sessionFile?: string;
|
|
161
|
-
// Sharing disabled
|
|
162
|
-
// shareUrl?: string;
|
|
163
|
-
// shareError?: string;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
interface AsyncJobState {
|
|
167
|
-
asyncId: string;
|
|
168
|
-
asyncDir: string;
|
|
169
|
-
status: "queued" | "running" | "complete" | "failed";
|
|
170
|
-
mode?: "single" | "chain";
|
|
171
|
-
agents?: string[];
|
|
172
|
-
currentStep?: number;
|
|
173
|
-
stepsTotal?: number;
|
|
174
|
-
startedAt?: number;
|
|
175
|
-
updatedAt?: number;
|
|
176
|
-
sessionDir?: string;
|
|
177
|
-
outputFile?: string;
|
|
178
|
-
totalTokens?: TokenUsage;
|
|
179
|
-
sessionFile?: string;
|
|
180
|
-
// shareUrl?: string;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function formatTokens(n: number): string {
|
|
184
|
-
return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function formatUsage(u: Usage, model?: string): string {
|
|
188
|
-
const parts: string[] = [];
|
|
189
|
-
if (u.turns) parts.push(`${u.turns} turn${u.turns > 1 ? "s" : ""}`);
|
|
190
|
-
if (u.input) parts.push(`in:${formatTokens(u.input)}`);
|
|
191
|
-
if (u.output) parts.push(`out:${formatTokens(u.output)}`);
|
|
192
|
-
if (u.cacheRead) parts.push(`R${formatTokens(u.cacheRead)}`);
|
|
193
|
-
if (u.cacheWrite) parts.push(`W${formatTokens(u.cacheWrite)}`);
|
|
194
|
-
if (u.cost) parts.push(`$${u.cost.toFixed(4)}`);
|
|
195
|
-
if (model) parts.push(model);
|
|
196
|
-
return parts.join(" ");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function formatDuration(ms: number): string {
|
|
200
|
-
if (ms < 1000) return `${ms}ms`;
|
|
201
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
202
|
-
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function buildChainSummary(
|
|
206
|
-
steps: ChainStep[],
|
|
207
|
-
results: SingleResult[],
|
|
208
|
-
chainDir: string,
|
|
209
|
-
status: "completed" | "failed",
|
|
210
|
-
failedStep?: { index: number; error: string },
|
|
211
|
-
): string {
|
|
212
|
-
// Build step names for display
|
|
213
|
-
const stepNames = steps
|
|
214
|
-
.map((s) => (isParallelStep(s) ? `parallel[${s.parallel.length}]` : (s as SequentialStep).agent))
|
|
215
|
-
.join(" → ");
|
|
216
|
-
|
|
217
|
-
// Calculate total duration from results
|
|
218
|
-
const totalDuration = results.reduce((sum, r) => sum + (r.progress?.durationMs || 0), 0);
|
|
219
|
-
const durationStr = formatDuration(totalDuration);
|
|
220
|
-
|
|
221
|
-
// Check for progress.md
|
|
222
|
-
const progressPath = path.join(chainDir, "progress.md");
|
|
223
|
-
const hasProgress = fs.existsSync(progressPath);
|
|
224
|
-
|
|
225
|
-
if (status === "completed") {
|
|
226
|
-
const stepWord = results.length === 1 ? "step" : "steps";
|
|
227
|
-
return `✅ Chain completed: ${stepNames} (${results.length} ${stepWord}, ${durationStr})
|
|
228
|
-
|
|
229
|
-
📋 Progress: ${hasProgress ? progressPath : "(none)"}
|
|
230
|
-
📁 Artifacts: ${chainDir}`;
|
|
231
|
-
} else {
|
|
232
|
-
const stepInfo = failedStep ? ` at step ${failedStep.index + 1}` : "";
|
|
233
|
-
const errorInfo = failedStep?.error ? `: ${failedStep.error}` : "";
|
|
234
|
-
return `❌ Chain failed${stepInfo}${errorInfo}
|
|
235
|
-
|
|
236
|
-
📋 Progress: ${hasProgress ? progressPath : "(none)"}
|
|
237
|
-
📁 Artifacts: ${chainDir}`;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function readStatus(asyncDir: string): AsyncStatus | null {
|
|
242
|
-
const statusPath = path.join(asyncDir, "status.json");
|
|
243
|
-
if (!fs.existsSync(statusPath)) return null;
|
|
244
|
-
try {
|
|
245
|
-
const content = fs.readFileSync(statusPath, "utf-8");
|
|
246
|
-
return JSON.parse(content) as AsyncStatus;
|
|
247
|
-
} catch {
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getOutputTail(outputFile: string | undefined, maxLines: number = 3): string[] {
|
|
253
|
-
if (!outputFile || !fs.existsSync(outputFile)) return [];
|
|
254
|
-
let fd: number | null = null;
|
|
255
|
-
try {
|
|
256
|
-
const stat = fs.statSync(outputFile);
|
|
257
|
-
if (stat.size === 0) return [];
|
|
258
|
-
const tailBytes = 4096;
|
|
259
|
-
const start = Math.max(0, stat.size - tailBytes);
|
|
260
|
-
fd = fs.openSync(outputFile, "r");
|
|
261
|
-
const buffer = Buffer.alloc(Math.min(tailBytes, stat.size));
|
|
262
|
-
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
263
|
-
const content = buffer.toString("utf-8");
|
|
264
|
-
const lines = content.split("\n").filter((l) => l.trim());
|
|
265
|
-
return lines.slice(-maxLines).map((l) => l.slice(0, 80) + (l.length > 80 ? "..." : ""));
|
|
266
|
-
} catch {
|
|
267
|
-
return [];
|
|
268
|
-
} finally {
|
|
269
|
-
if (fd !== null) {
|
|
270
|
-
try {
|
|
271
|
-
fs.closeSync(fd);
|
|
272
|
-
} catch {}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function getLastActivity(outputFile: string | undefined): string {
|
|
278
|
-
if (!outputFile || !fs.existsSync(outputFile)) return "";
|
|
279
|
-
try {
|
|
280
|
-
const stat = fs.statSync(outputFile);
|
|
281
|
-
const ago = Date.now() - stat.mtimeMs;
|
|
282
|
-
if (ago < 1000) return "active now";
|
|
283
|
-
if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
|
|
284
|
-
return `active ${Math.floor(ago / 60000)}m ago`;
|
|
285
|
-
} catch {
|
|
286
|
-
return "";
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
291
|
-
if (!ctx.hasUI) return;
|
|
292
|
-
if (jobs.length === 0) {
|
|
293
|
-
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const theme = ctx.ui.theme;
|
|
298
|
-
const lines: string[] = [];
|
|
299
|
-
lines.push(theme.fg("accent", "Async subagents"));
|
|
300
|
-
|
|
301
|
-
for (const job of jobs.slice(0, MAX_WIDGET_JOBS)) {
|
|
302
|
-
const id = job.asyncId.slice(0, 6);
|
|
303
|
-
const status =
|
|
304
|
-
job.status === "complete"
|
|
305
|
-
? theme.fg("success", "complete")
|
|
306
|
-
: job.status === "failed"
|
|
307
|
-
? theme.fg("error", "failed")
|
|
308
|
-
: theme.fg("warning", "running");
|
|
309
|
-
|
|
310
|
-
const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
|
|
311
|
-
const stepIndex = job.currentStep !== undefined ? job.currentStep + 1 : undefined;
|
|
312
|
-
const stepText = stepIndex !== undefined ? `step ${stepIndex}/${stepsTotal}` : `steps ${stepsTotal}`;
|
|
313
|
-
const endTime = (job.status === "complete" || job.status === "failed") ? (job.updatedAt ?? Date.now()) : Date.now();
|
|
314
|
-
const elapsed = job.startedAt ? formatDuration(endTime - job.startedAt) : "";
|
|
315
|
-
const agentLabel = job.agents ? job.agents.join(" -> ") : (job.mode ?? "single");
|
|
316
|
-
|
|
317
|
-
const tokenText = job.totalTokens ? ` | ${formatTokens(job.totalTokens.total)} tok` : "";
|
|
318
|
-
const activityText = job.status === "running" ? getLastActivity(job.outputFile) : "";
|
|
319
|
-
const activitySuffix = activityText ? ` | ${theme.fg("dim", activityText)}` : "";
|
|
320
|
-
|
|
321
|
-
lines.push(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`);
|
|
322
|
-
|
|
323
|
-
if (job.status === "running" && job.outputFile) {
|
|
324
|
-
const tail = getOutputTail(job.outputFile, 3);
|
|
325
|
-
for (const line of tail) {
|
|
326
|
-
lines.push(theme.fg("dim", ` > ${line}`));
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
ctx.ui.setWidget(WIDGET_KEY, lines);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function findByPrefix(dir: string, prefix: string, suffix?: string): string | null {
|
|
335
|
-
if (!fs.existsSync(dir)) return null;
|
|
336
|
-
const entries = fs.readdirSync(dir).filter((entry) => entry.startsWith(prefix));
|
|
337
|
-
if (suffix) {
|
|
338
|
-
const withSuffix = entries.filter((entry) => entry.endsWith(suffix));
|
|
339
|
-
if (withSuffix.length > 0) return path.join(dir, withSuffix.sort()[0]);
|
|
340
|
-
}
|
|
341
|
-
if (entries.length === 0) return null;
|
|
342
|
-
return path.join(dir, entries.sort()[0]);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function getFinalOutput(messages: Message[]): string {
|
|
346
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
347
|
-
const msg = messages[i];
|
|
348
|
-
if (msg.role === "assistant") {
|
|
349
|
-
for (const part of msg.content) {
|
|
350
|
-
if (part.type === "text") return part.text;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return "";
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
interface ErrorInfo {
|
|
358
|
-
hasError: boolean;
|
|
359
|
-
exitCode?: number;
|
|
360
|
-
errorType?: string;
|
|
361
|
-
details?: string;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function detectSubagentError(messages: Message[]): ErrorInfo {
|
|
365
|
-
for (const msg of messages) {
|
|
366
|
-
if (msg.role === "toolResult" && (msg as any).isError) {
|
|
367
|
-
const text = msg.content.find((c) => c.type === "text");
|
|
368
|
-
const details = text && "text" in text ? text.text : undefined;
|
|
369
|
-
const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
|
|
370
|
-
return {
|
|
371
|
-
hasError: true,
|
|
372
|
-
exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
|
|
373
|
-
errorType: (msg as any).toolName || "tool",
|
|
374
|
-
details: details?.slice(0, 200),
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
for (const msg of messages) {
|
|
380
|
-
if (msg.role !== "toolResult") continue;
|
|
381
|
-
const toolName = (msg as any).toolName;
|
|
382
|
-
if (toolName !== "bash") continue;
|
|
383
|
-
|
|
384
|
-
const text = msg.content.find((c) => c.type === "text");
|
|
385
|
-
if (!text || !("text" in text)) continue;
|
|
386
|
-
const output = text.text;
|
|
387
|
-
|
|
388
|
-
const exitMatch = output.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
|
|
389
|
-
if (exitMatch) {
|
|
390
|
-
const code = parseInt(exitMatch[1], 10);
|
|
391
|
-
if (code !== 0) {
|
|
392
|
-
return { hasError: true, exitCode: code, errorType: "bash", details: output.slice(0, 200) };
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const errorPatterns = [
|
|
397
|
-
/command not found/i,
|
|
398
|
-
/permission denied/i,
|
|
399
|
-
/no such file or directory/i,
|
|
400
|
-
/segmentation fault/i,
|
|
401
|
-
/killed|terminated/i,
|
|
402
|
-
/out of memory/i,
|
|
403
|
-
/connection refused/i,
|
|
404
|
-
/timeout/i,
|
|
405
|
-
];
|
|
406
|
-
for (const pattern of errorPatterns) {
|
|
407
|
-
if (pattern.test(output)) {
|
|
408
|
-
return { hasError: true, exitCode: 1, errorType: "bash", details: output.slice(0, 200) };
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return { hasError: false };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
417
|
-
const items: DisplayItem[] = [];
|
|
418
|
-
for (const msg of messages) {
|
|
419
|
-
if (msg.role === "assistant") {
|
|
420
|
-
for (const part of msg.content) {
|
|
421
|
-
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
422
|
-
else if (part.type === "toolCall") items.push({ type: "tool", name: part.name, args: part.arguments });
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return items;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function shortenPath(p: string): string {
|
|
430
|
-
const home = os.homedir();
|
|
431
|
-
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function formatToolCall(name: string, args: Record<string, unknown>): string {
|
|
435
|
-
switch (name) {
|
|
436
|
-
case "bash":
|
|
437
|
-
return `$ ${((args.command as string) || "").slice(0, 60)}${(args.command as string)?.length > 60 ? "..." : ""}`;
|
|
438
|
-
case "read":
|
|
439
|
-
return `read ${shortenPath((args.path || args.file_path || "") as string)}`;
|
|
440
|
-
case "write":
|
|
441
|
-
return `write ${shortenPath((args.path || args.file_path || "") as string)}`;
|
|
442
|
-
case "edit":
|
|
443
|
-
return `edit ${shortenPath((args.path || args.file_path || "") as string)}`;
|
|
444
|
-
default: {
|
|
445
|
-
const s = JSON.stringify(args);
|
|
446
|
-
return `${name} ${s.slice(0, 40)}${s.length > 40 ? "..." : ""}`;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function extractToolArgsPreview(args: Record<string, unknown>): string {
|
|
452
|
-
const previewKeys = ["command", "path", "file_path", "pattern", "query", "url", "task"];
|
|
453
|
-
for (const key of previewKeys) {
|
|
454
|
-
if (args[key] && typeof args[key] === "string") {
|
|
455
|
-
const value = args[key] as string;
|
|
456
|
-
return value.length > 60 ? `${value.slice(0, 57)}...` : value;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return "";
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function extractTextFromContent(content: unknown): string {
|
|
463
|
-
if (!Array.isArray(content)) return "";
|
|
464
|
-
for (const part of content) {
|
|
465
|
-
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
|
|
466
|
-
return String(part.text);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
return "";
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function writePrompt(agent: string, prompt: string): { dir: string; path: string } {
|
|
473
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
474
|
-
const p = path.join(dir, `${agent.replace(/[^\w.-]/g, "_")}.md`);
|
|
475
|
-
fs.writeFileSync(p, prompt, { mode: 0o600 });
|
|
476
|
-
return { dir, path: p };
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function findLatestSessionFile(sessionDir: string): string | null {
|
|
480
|
-
try {
|
|
481
|
-
const files = fs
|
|
482
|
-
.readdirSync(sessionDir)
|
|
483
|
-
.filter((f) => f.endsWith(".jsonl"))
|
|
484
|
-
.map((f) => path.join(sessionDir, f));
|
|
485
|
-
if (files.length === 0) return null;
|
|
486
|
-
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
487
|
-
return files[0] ?? null;
|
|
488
|
-
} catch {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// HTML export and sharing removed - module resolution issues with global pi installation
|
|
494
|
-
// The session files are still available at the paths shown in the output
|
|
495
|
-
|
|
496
|
-
interface RunSyncOptions {
|
|
497
|
-
cwd?: string;
|
|
498
|
-
signal?: AbortSignal;
|
|
499
|
-
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
500
|
-
maxOutput?: MaxOutputConfig;
|
|
501
|
-
artifactsDir?: string;
|
|
502
|
-
artifactConfig?: ArtifactConfig;
|
|
503
|
-
runId: string;
|
|
504
|
-
index?: number;
|
|
505
|
-
sessionDir?: string;
|
|
506
|
-
share?: boolean;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
async function runSync(
|
|
510
|
-
runtimeCwd: string,
|
|
511
|
-
agents: AgentConfig[],
|
|
512
|
-
agentName: string,
|
|
513
|
-
task: string,
|
|
514
|
-
options: RunSyncOptions,
|
|
515
|
-
): Promise<SingleResult> {
|
|
516
|
-
const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index } = options;
|
|
517
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
518
|
-
if (!agent) {
|
|
519
|
-
return {
|
|
520
|
-
agent: agentName,
|
|
521
|
-
task,
|
|
522
|
-
exitCode: 1,
|
|
523
|
-
messages: [],
|
|
524
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
525
|
-
error: `Unknown agent: ${agentName}`,
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const args = ["--mode", "json", "-p"];
|
|
530
|
-
const shareEnabled = options.share === true;
|
|
531
|
-
const sessionEnabled = Boolean(options.sessionDir) || shareEnabled;
|
|
532
|
-
if (!sessionEnabled) {
|
|
533
|
-
args.push("--no-session");
|
|
534
|
-
}
|
|
535
|
-
if (options.sessionDir) {
|
|
536
|
-
try {
|
|
537
|
-
fs.mkdirSync(options.sessionDir, { recursive: true });
|
|
538
|
-
} catch {}
|
|
539
|
-
args.push("--session-dir", options.sessionDir);
|
|
540
|
-
}
|
|
541
|
-
if (agent.model) args.push("--model", agent.model);
|
|
542
|
-
if (agent.tools?.length) {
|
|
543
|
-
const builtinTools: string[] = [];
|
|
544
|
-
const extensionPaths: string[] = [];
|
|
545
|
-
for (const tool of agent.tools) {
|
|
546
|
-
if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
|
|
547
|
-
extensionPaths.push(tool);
|
|
548
|
-
} else {
|
|
549
|
-
builtinTools.push(tool);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
if (builtinTools.length > 0) {
|
|
553
|
-
args.push("--tools", builtinTools.join(","));
|
|
554
|
-
}
|
|
555
|
-
for (const extPath of extensionPaths) {
|
|
556
|
-
args.push("--extension", extPath);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
let tmpDir: string | null = null;
|
|
561
|
-
if (agent.systemPrompt?.trim()) {
|
|
562
|
-
const tmp = writePrompt(agent.name, agent.systemPrompt);
|
|
563
|
-
tmpDir = tmp.dir;
|
|
564
|
-
args.push("--append-system-prompt", tmp.path);
|
|
565
|
-
}
|
|
566
|
-
args.push(`Task: ${task}`);
|
|
567
|
-
|
|
568
|
-
const result: SingleResult = {
|
|
569
|
-
agent: agentName,
|
|
570
|
-
task,
|
|
571
|
-
exitCode: 0,
|
|
572
|
-
messages: [],
|
|
573
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
const progress: AgentProgress = {
|
|
577
|
-
index: index ?? 0,
|
|
578
|
-
agent: agentName,
|
|
579
|
-
status: "running",
|
|
580
|
-
task,
|
|
581
|
-
recentTools: [],
|
|
582
|
-
recentOutput: [],
|
|
583
|
-
toolCount: 0,
|
|
584
|
-
tokens: 0,
|
|
585
|
-
durationMs: 0,
|
|
586
|
-
};
|
|
587
|
-
result.progress = progress;
|
|
588
|
-
|
|
589
|
-
const startTime = Date.now();
|
|
590
|
-
const jsonlLines: string[] = [];
|
|
591
|
-
|
|
592
|
-
let artifactPathsResult: ArtifactPaths | undefined;
|
|
593
|
-
if (artifactsDir && artifactConfig?.enabled !== false) {
|
|
594
|
-
artifactPathsResult = getArtifactPaths(artifactsDir, runId, agentName, index);
|
|
595
|
-
ensureArtifactsDir(artifactsDir);
|
|
596
|
-
if (artifactConfig?.includeInput !== false) {
|
|
597
|
-
writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const exitCode = await new Promise<number>((resolve) => {
|
|
602
|
-
const proc = spawn("pi", args, { cwd: cwd ?? runtimeCwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
603
|
-
let buf = "";
|
|
604
|
-
|
|
605
|
-
const processLine = (line: string) => {
|
|
606
|
-
if (!line.trim()) return;
|
|
607
|
-
jsonlLines.push(line);
|
|
608
|
-
try {
|
|
609
|
-
const evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
|
|
610
|
-
const now = Date.now();
|
|
611
|
-
progress.durationMs = now - startTime;
|
|
612
|
-
|
|
613
|
-
if (evt.type === "tool_execution_start") {
|
|
614
|
-
progress.toolCount++;
|
|
615
|
-
progress.currentTool = evt.toolName;
|
|
616
|
-
progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
|
|
617
|
-
if (onUpdate)
|
|
618
|
-
onUpdate({
|
|
619
|
-
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
620
|
-
details: { mode: "single", results: [result], progress: [progress] },
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (evt.type === "tool_execution_end") {
|
|
625
|
-
if (progress.currentTool) {
|
|
626
|
-
progress.recentTools.unshift({
|
|
627
|
-
tool: progress.currentTool,
|
|
628
|
-
args: progress.currentToolArgs || "",
|
|
629
|
-
endMs: now,
|
|
630
|
-
});
|
|
631
|
-
if (progress.recentTools.length > 5) {
|
|
632
|
-
progress.recentTools.pop();
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
progress.currentTool = undefined;
|
|
636
|
-
progress.currentToolArgs = undefined;
|
|
637
|
-
if (onUpdate)
|
|
638
|
-
onUpdate({
|
|
639
|
-
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
640
|
-
details: { mode: "single", results: [result], progress: [progress] },
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
if (evt.type === "message_end" && evt.message) {
|
|
645
|
-
result.messages.push(evt.message);
|
|
646
|
-
if (evt.message.role === "assistant") {
|
|
647
|
-
result.usage.turns++;
|
|
648
|
-
const u = evt.message.usage;
|
|
649
|
-
if (u) {
|
|
650
|
-
result.usage.input += u.input || 0;
|
|
651
|
-
result.usage.output += u.output || 0;
|
|
652
|
-
result.usage.cacheRead += u.cacheRead || 0;
|
|
653
|
-
result.usage.cacheWrite += u.cacheWrite || 0;
|
|
654
|
-
result.usage.cost += u.cost?.total || 0;
|
|
655
|
-
progress.tokens = result.usage.input + result.usage.output;
|
|
656
|
-
}
|
|
657
|
-
if (!result.model && evt.message.model) result.model = evt.message.model;
|
|
658
|
-
if (evt.message.errorMessage) result.error = evt.message.errorMessage;
|
|
659
|
-
|
|
660
|
-
const text = extractTextFromContent(evt.message.content);
|
|
661
|
-
if (text) {
|
|
662
|
-
const lines = text
|
|
663
|
-
.split("\n")
|
|
664
|
-
.filter((l) => l.trim())
|
|
665
|
-
.slice(-8);
|
|
666
|
-
progress.recentOutput = lines;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (onUpdate)
|
|
670
|
-
onUpdate({
|
|
671
|
-
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
672
|
-
details: { mode: "single", results: [result], progress: [progress] },
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
if (evt.type === "tool_result_end" && evt.message) {
|
|
676
|
-
result.messages.push(evt.message);
|
|
677
|
-
if (onUpdate)
|
|
678
|
-
onUpdate({
|
|
679
|
-
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
680
|
-
details: { mode: "single", results: [result], progress: [progress] },
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
} catch {}
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
let stderrBuf = "";
|
|
687
|
-
let lastUpdateTime = 0;
|
|
688
|
-
const UPDATE_THROTTLE_MS = 150;
|
|
689
|
-
|
|
690
|
-
proc.stdout.on("data", (d) => {
|
|
691
|
-
buf += d.toString();
|
|
692
|
-
const lines = buf.split("\n");
|
|
693
|
-
buf = lines.pop() || "";
|
|
694
|
-
lines.forEach(processLine);
|
|
695
|
-
|
|
696
|
-
// Throttled periodic update for smoother progress display
|
|
697
|
-
const now = Date.now();
|
|
698
|
-
if (onUpdate && now - lastUpdateTime > UPDATE_THROTTLE_MS) {
|
|
699
|
-
lastUpdateTime = now;
|
|
700
|
-
progress.durationMs = now - startTime;
|
|
701
|
-
onUpdate({
|
|
702
|
-
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
703
|
-
details: { mode: "single", results: [result], progress: [progress] },
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
proc.stderr.on("data", (d) => {
|
|
708
|
-
stderrBuf += d.toString();
|
|
709
|
-
});
|
|
710
|
-
proc.on("close", (code) => {
|
|
711
|
-
if (buf.trim()) processLine(buf);
|
|
712
|
-
if (code !== 0 && stderrBuf.trim() && !result.error) {
|
|
713
|
-
result.error = stderrBuf.trim();
|
|
714
|
-
}
|
|
715
|
-
resolve(code ?? 0);
|
|
716
|
-
});
|
|
717
|
-
proc.on("error", () => resolve(1));
|
|
718
|
-
|
|
719
|
-
if (signal) {
|
|
720
|
-
const kill = () => {
|
|
721
|
-
proc.kill("SIGTERM");
|
|
722
|
-
setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
|
|
723
|
-
};
|
|
724
|
-
if (signal.aborted) kill();
|
|
725
|
-
else signal.addEventListener("abort", kill, { once: true });
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
730
|
-
result.exitCode = exitCode;
|
|
731
|
-
|
|
732
|
-
if (exitCode === 0 && !result.error) {
|
|
733
|
-
const errInfo = detectSubagentError(result.messages);
|
|
734
|
-
if (errInfo.hasError) {
|
|
735
|
-
result.exitCode = errInfo.exitCode ?? 1;
|
|
736
|
-
result.error = errInfo.details
|
|
737
|
-
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
738
|
-
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
progress.status = result.exitCode === 0 ? "completed" : "failed";
|
|
743
|
-
progress.durationMs = Date.now() - startTime;
|
|
744
|
-
if (result.error) {
|
|
745
|
-
progress.error = result.error;
|
|
746
|
-
if (progress.currentTool) {
|
|
747
|
-
progress.failedTool = progress.currentTool;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
result.progress = progress;
|
|
752
|
-
result.progressSummary = {
|
|
753
|
-
toolCount: progress.toolCount,
|
|
754
|
-
tokens: progress.tokens,
|
|
755
|
-
durationMs: progress.durationMs,
|
|
756
|
-
};
|
|
757
|
-
|
|
758
|
-
if (artifactPathsResult && artifactConfig?.enabled !== false) {
|
|
759
|
-
result.artifactPaths = artifactPathsResult;
|
|
760
|
-
const fullOutput = getFinalOutput(result.messages);
|
|
761
|
-
|
|
762
|
-
if (artifactConfig?.includeOutput !== false) {
|
|
763
|
-
writeArtifact(artifactPathsResult.outputPath, fullOutput);
|
|
764
|
-
}
|
|
765
|
-
if (artifactConfig?.includeJsonl !== false) {
|
|
766
|
-
for (const line of jsonlLines) {
|
|
767
|
-
appendJsonl(artifactPathsResult.jsonlPath, line);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
if (artifactConfig?.includeMetadata !== false) {
|
|
771
|
-
writeMetadata(artifactPathsResult.metadataPath, {
|
|
772
|
-
runId,
|
|
773
|
-
agent: agentName,
|
|
774
|
-
task,
|
|
775
|
-
exitCode: result.exitCode,
|
|
776
|
-
usage: result.usage,
|
|
777
|
-
model: result.model,
|
|
778
|
-
durationMs: progress.durationMs,
|
|
779
|
-
toolCount: progress.toolCount,
|
|
780
|
-
error: result.error,
|
|
781
|
-
timestamp: Date.now(),
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (maxOutput) {
|
|
786
|
-
const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
|
|
787
|
-
const truncationResult = truncateOutput(fullOutput, config, artifactPathsResult.outputPath);
|
|
788
|
-
if (truncationResult.truncated) {
|
|
789
|
-
result.truncation = truncationResult;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
} else if (maxOutput) {
|
|
793
|
-
const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
|
|
794
|
-
const fullOutput = getFinalOutput(result.messages);
|
|
795
|
-
const truncationResult = truncateOutput(fullOutput, config);
|
|
796
|
-
if (truncationResult.truncated) {
|
|
797
|
-
result.truncation = truncationResult;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (shareEnabled && options.sessionDir) {
|
|
802
|
-
const sessionFile = findLatestSessionFile(options.sessionDir);
|
|
803
|
-
if (sessionFile) {
|
|
804
|
-
result.sessionFile = sessionFile;
|
|
805
|
-
// HTML export disabled - module resolution issues with global pi installation
|
|
806
|
-
// Users can still access the session file directly
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
return result;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>): Promise<R[]> {
|
|
814
|
-
const results: R[] = new Array(items.length);
|
|
815
|
-
let next = 0;
|
|
816
|
-
await Promise.all(
|
|
817
|
-
Array(Math.min(limit, items.length))
|
|
818
|
-
.fill(0)
|
|
819
|
-
.map(async () => {
|
|
820
|
-
while (next < items.length) {
|
|
821
|
-
const i = next++;
|
|
822
|
-
results[i] = await fn(items[i], i);
|
|
823
|
-
}
|
|
824
|
-
}),
|
|
825
|
-
);
|
|
826
|
-
return results;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const TaskItem = Type.Object({ agent: Type.String(), task: Type.String(), cwd: Type.Optional(Type.String()) });
|
|
830
|
-
|
|
831
|
-
// Sequential chain step (single agent)
|
|
832
|
-
const SequentialStepSchema = Type.Object({
|
|
833
|
-
agent: Type.String(),
|
|
834
|
-
task: Type.Optional(Type.String({ description: "Task template. Use {task}, {previous}, {chain_dir}. Required for first step." })),
|
|
835
|
-
cwd: Type.Optional(Type.String()),
|
|
836
|
-
// Chain behavior overrides
|
|
837
|
-
output: Type.Optional(Type.Union([
|
|
838
|
-
Type.String(),
|
|
839
|
-
Type.Boolean(),
|
|
840
|
-
], { description: "Override output filename (string), or false for text-only" })),
|
|
841
|
-
reads: Type.Optional(Type.Union([
|
|
842
|
-
Type.Array(Type.String()),
|
|
843
|
-
Type.Boolean(),
|
|
844
|
-
], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
|
|
845
|
-
progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
// Parallel task item (within a parallel step)
|
|
849
|
-
const ParallelTaskSchema = Type.Object({
|
|
850
|
-
agent: Type.String(),
|
|
851
|
-
task: Type.Optional(Type.String({ description: "Task template. Defaults to {previous}." })),
|
|
852
|
-
cwd: Type.Optional(Type.String()),
|
|
853
|
-
output: Type.Optional(Type.Union([
|
|
854
|
-
Type.String(),
|
|
855
|
-
Type.Boolean(),
|
|
856
|
-
], { description: "Override output filename (string), or false for text-only" })),
|
|
857
|
-
reads: Type.Optional(Type.Union([
|
|
858
|
-
Type.Array(Type.String()),
|
|
859
|
-
Type.Boolean(),
|
|
860
|
-
], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
|
|
861
|
-
progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
// Parallel chain step (multiple agents running concurrently)
|
|
865
|
-
const ParallelStepSchema = Type.Object({
|
|
866
|
-
parallel: Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
|
|
867
|
-
concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
|
|
868
|
-
failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
// Chain item can be either sequential or parallel
|
|
872
|
-
const ChainItem = Type.Union([SequentialStepSchema, ParallelStepSchema]);
|
|
873
|
-
|
|
874
|
-
const MaxOutputSchema = Type.Optional(
|
|
875
|
-
Type.Object({
|
|
876
|
-
bytes: Type.Optional(Type.Number({ description: "Max bytes (default: 204800)" })),
|
|
877
|
-
lines: Type.Optional(Type.Number({ description: "Max lines (default: 5000)" })),
|
|
878
|
-
}),
|
|
879
|
-
);
|
|
880
|
-
|
|
881
|
-
const Params = Type.Object({
|
|
882
|
-
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode)" })),
|
|
883
|
-
task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
|
|
884
|
-
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
|
|
885
|
-
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: [{agent}, {agent, task:'{previous}'}] - sequential pipeline" })),
|
|
886
|
-
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
887
|
-
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'user')" })),
|
|
888
|
-
cwd: Type.Optional(Type.String()),
|
|
889
|
-
maxOutput: MaxOutputSchema,
|
|
890
|
-
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
|
|
891
|
-
includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
|
|
892
|
-
share: Type.Optional(Type.Boolean({ description: "Create shareable session log (default: true)", default: true })),
|
|
893
|
-
sessionDir: Type.Optional(
|
|
894
|
-
Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
|
|
895
|
-
),
|
|
896
|
-
// Chain clarification TUI
|
|
897
|
-
clarify: Type.Optional(Type.Boolean({ description: "Show TUI to clarify chain templates (default: true for chains). Implies sync mode." })),
|
|
898
|
-
// Solo agent output override
|
|
899
|
-
output: Type.Optional(Type.Union([
|
|
900
|
-
Type.String(),
|
|
901
|
-
Type.Boolean(),
|
|
902
|
-
], { description: "Override output file for single agent (string), or false to disable (uses agent default if omitted)" })),
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
const StatusParams = Type.Object({
|
|
906
|
-
id: Type.Optional(Type.String({ description: "Async run id or prefix" })),
|
|
907
|
-
dir: Type.Optional(Type.String({ description: "Async run directory (overrides id search)" })),
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
interface ExtensionConfig {
|
|
911
|
-
asyncByDefault?: boolean;
|
|
912
|
-
}
|
|
49
|
+
// ExtensionConfig is now imported from ./types.js
|
|
913
50
|
|
|
914
51
|
function loadConfig(): ExtensionConfig {
|
|
915
52
|
const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");
|
|
@@ -997,7 +134,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
997
134
|
.filter((f) => f.endsWith(".json"))
|
|
998
135
|
.forEach(handleResult);
|
|
999
136
|
|
|
1000
|
-
const tool: ToolDefinition<typeof
|
|
137
|
+
const tool: ToolDefinition<typeof SubagentParams, Details> = {
|
|
1001
138
|
name: "subagent",
|
|
1002
139
|
label: "Subagent",
|
|
1003
140
|
description: `Delegate to subagents. Use exactly ONE mode:
|
|
@@ -1005,7 +142,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1005
142
|
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential, {previous} passes output
|
|
1006
143
|
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent
|
|
1007
144
|
For "scout → planner" or multi-step flows, use chain (not multiple single calls).`,
|
|
1008
|
-
parameters:
|
|
145
|
+
parameters: SubagentParams,
|
|
1009
146
|
|
|
1010
147
|
async execute(_id, params, onUpdate, ctx, signal) {
|
|
1011
148
|
const scope: AgentScope = params.agentScope ?? "user";
|
|
@@ -1071,9 +208,11 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
1071
208
|
// First step must have a task
|
|
1072
209
|
const firstStep = params.chain[0] as ChainStep;
|
|
1073
210
|
if (isParallelStep(firstStep)) {
|
|
1074
|
-
|
|
211
|
+
// All tasks in the first parallel step must have tasks (no {previous} to reference)
|
|
212
|
+
const missingTaskIndex = firstStep.parallel.findIndex((t) => !t.task);
|
|
213
|
+
if (missingTaskIndex !== -1) {
|
|
1075
214
|
return {
|
|
1076
|
-
content: [{ type: "text", text:
|
|
215
|
+
content: [{ type: "text", text: `First parallel step: task ${missingTaskIndex + 1} must have a task (no previous output to reference)` }],
|
|
1077
216
|
isError: true,
|
|
1078
217
|
details: { mode: "chain" as const, results: [] },
|
|
1079
218
|
};
|
|
@@ -1110,169 +249,51 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
1110
249
|
}
|
|
1111
250
|
|
|
1112
251
|
if (effectiveAsync) {
|
|
1113
|
-
if (!
|
|
252
|
+
if (!isAsyncAvailable()) {
|
|
1114
253
|
return {
|
|
1115
254
|
content: [{ type: "text", text: "jiti not found" }],
|
|
1116
255
|
isError: true,
|
|
1117
256
|
details: { mode: "single" as const, results: [] },
|
|
1118
257
|
};
|
|
258
|
+
}
|
|
1119
259
|
const id = randomUUID();
|
|
1120
|
-
const
|
|
1121
|
-
try {
|
|
1122
|
-
fs.mkdirSync(asyncDir, { recursive: true });
|
|
1123
|
-
} catch {}
|
|
1124
|
-
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
1125
|
-
|
|
1126
|
-
const spawnRunner = (cfg: object, suffix: string): number | undefined => {
|
|
1127
|
-
const cfgPath = path.join(os.tmpdir(), `pi-async-cfg-${suffix}.json`);
|
|
1128
|
-
fs.writeFileSync(cfgPath, JSON.stringify(cfg));
|
|
1129
|
-
const proc = spawn("node", [jitiCliPath!, runner, cfgPath], {
|
|
1130
|
-
cwd: (cfg as any).cwd ?? ctx.cwd,
|
|
1131
|
-
detached: true,
|
|
1132
|
-
stdio: "ignore",
|
|
1133
|
-
});
|
|
1134
|
-
proc.unref();
|
|
1135
|
-
return proc.pid;
|
|
1136
|
-
};
|
|
260
|
+
const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: currentSessionId! };
|
|
1137
261
|
|
|
1138
262
|
if (hasChain && params.chain) {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
// At this point, all steps are sequential
|
|
1151
|
-
const seqSteps = chainStepsForAsync as SequentialStep[];
|
|
1152
|
-
|
|
1153
|
-
// Validate all agents exist before building steps
|
|
1154
|
-
for (const s of seqSteps) {
|
|
1155
|
-
if (!agents.find((x) => x.name === s.agent)) {
|
|
1156
|
-
return {
|
|
1157
|
-
content: [{ type: "text", text: `Unknown agent: ${s.agent}` }],
|
|
1158
|
-
isError: true,
|
|
1159
|
-
details: { mode: "chain" as const, results: [] },
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
const steps = seqSteps.map((s, i) => {
|
|
1164
|
-
const a = agents.find((x) => x.name === s.agent)!;
|
|
1165
|
-
return {
|
|
1166
|
-
agent: s.agent,
|
|
1167
|
-
// For async, use simple defaults: first step uses inline task, others use {previous}
|
|
1168
|
-
task: s.task ?? (i === 0 ? "{task}" : "{previous}"),
|
|
1169
|
-
cwd: s.cwd,
|
|
1170
|
-
model: a.model,
|
|
1171
|
-
tools: a.tools,
|
|
1172
|
-
systemPrompt: a.systemPrompt?.trim() || null,
|
|
1173
|
-
};
|
|
263
|
+
return executeAsyncChain(id, {
|
|
264
|
+
chain: params.chain as ChainStep[],
|
|
265
|
+
agents,
|
|
266
|
+
ctx: asyncCtx,
|
|
267
|
+
cwd: params.cwd,
|
|
268
|
+
maxOutput: params.maxOutput,
|
|
269
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
270
|
+
artifactConfig,
|
|
271
|
+
shareEnabled,
|
|
272
|
+
sessionRoot,
|
|
1174
273
|
});
|
|
1175
|
-
const pid = spawnRunner(
|
|
1176
|
-
{
|
|
1177
|
-
id,
|
|
1178
|
-
steps,
|
|
1179
|
-
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
1180
|
-
cwd: params.cwd ?? ctx.cwd,
|
|
1181
|
-
placeholder: "{previous}",
|
|
1182
|
-
maxOutput: params.maxOutput,
|
|
1183
|
-
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1184
|
-
artifactConfig,
|
|
1185
|
-
share: shareEnabled,
|
|
1186
|
-
sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
|
|
1187
|
-
asyncDir,
|
|
1188
|
-
sessionId: currentSessionId,
|
|
1189
|
-
},
|
|
1190
|
-
id,
|
|
1191
|
-
);
|
|
1192
|
-
if (pid) {
|
|
1193
|
-
pi.events.emit("subagent_enhanced:started", {
|
|
1194
|
-
id,
|
|
1195
|
-
pid,
|
|
1196
|
-
agent: params.chain[0].agent,
|
|
1197
|
-
task: params.chain[0].task?.slice(0, 50),
|
|
1198
|
-
chain: params.chain.map((s) => s.agent),
|
|
1199
|
-
cwd: params.cwd ?? ctx.cwd,
|
|
1200
|
-
asyncDir,
|
|
1201
|
-
});
|
|
1202
|
-
pi.events.emit("subagent:started", {
|
|
1203
|
-
id,
|
|
1204
|
-
pid,
|
|
1205
|
-
agent: params.chain[0].agent,
|
|
1206
|
-
task: params.chain[0].task?.slice(0, 50),
|
|
1207
|
-
chain: params.chain.map((s) => s.agent),
|
|
1208
|
-
cwd: params.cwd ?? ctx.cwd,
|
|
1209
|
-
asyncDir,
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
return {
|
|
1213
|
-
content: [
|
|
1214
|
-
{ type: "text", text: `Async chain: ${params.chain.map((s) => s.agent).join(" -> ")} [${id}]` },
|
|
1215
|
-
],
|
|
1216
|
-
details: { mode: "chain", results: [], asyncId: id, asyncDir },
|
|
1217
|
-
};
|
|
1218
274
|
}
|
|
1219
275
|
|
|
1220
276
|
if (hasSingle) {
|
|
1221
277
|
const a = agents.find((x) => x.name === params.agent);
|
|
1222
|
-
if (!a)
|
|
278
|
+
if (!a) {
|
|
1223
279
|
return {
|
|
1224
280
|
content: [{ type: "text", text: `Unknown: ${params.agent}` }],
|
|
1225
281
|
isError: true,
|
|
1226
282
|
details: { mode: "single" as const, results: [] },
|
|
1227
283
|
};
|
|
1228
|
-
const pid = spawnRunner(
|
|
1229
|
-
{
|
|
1230
|
-
id,
|
|
1231
|
-
steps: [
|
|
1232
|
-
{
|
|
1233
|
-
agent: params.agent,
|
|
1234
|
-
task: params.task,
|
|
1235
|
-
cwd: params.cwd,
|
|
1236
|
-
model: a.model,
|
|
1237
|
-
tools: a.tools,
|
|
1238
|
-
systemPrompt: a.systemPrompt?.trim() || null,
|
|
1239
|
-
},
|
|
1240
|
-
],
|
|
1241
|
-
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
1242
|
-
cwd: params.cwd ?? ctx.cwd,
|
|
1243
|
-
placeholder: "{previous}",
|
|
1244
|
-
maxOutput: params.maxOutput,
|
|
1245
|
-
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1246
|
-
artifactConfig,
|
|
1247
|
-
share: shareEnabled,
|
|
1248
|
-
sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
|
|
1249
|
-
asyncDir,
|
|
1250
|
-
sessionId: currentSessionId,
|
|
1251
|
-
},
|
|
1252
|
-
id,
|
|
1253
|
-
);
|
|
1254
|
-
if (pid) {
|
|
1255
|
-
pi.events.emit("subagent_enhanced:started", {
|
|
1256
|
-
id,
|
|
1257
|
-
pid,
|
|
1258
|
-
agent: params.agent,
|
|
1259
|
-
task: params.task?.slice(0, 50),
|
|
1260
|
-
cwd: params.cwd ?? ctx.cwd,
|
|
1261
|
-
asyncDir,
|
|
1262
|
-
});
|
|
1263
|
-
pi.events.emit("subagent:started", {
|
|
1264
|
-
id,
|
|
1265
|
-
pid,
|
|
1266
|
-
agent: params.agent,
|
|
1267
|
-
task: params.task?.slice(0, 50),
|
|
1268
|
-
cwd: params.cwd ?? ctx.cwd,
|
|
1269
|
-
asyncDir,
|
|
1270
|
-
});
|
|
1271
284
|
}
|
|
1272
|
-
return {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
285
|
+
return executeAsyncSingle(id, {
|
|
286
|
+
agent: params.agent!,
|
|
287
|
+
task: params.task!,
|
|
288
|
+
agentConfig: a,
|
|
289
|
+
ctx: asyncCtx,
|
|
290
|
+
cwd: params.cwd,
|
|
291
|
+
maxOutput: params.maxOutput,
|
|
292
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
293
|
+
artifactConfig,
|
|
294
|
+
shareEnabled,
|
|
295
|
+
sessionRoot,
|
|
296
|
+
});
|
|
1276
297
|
}
|
|
1277
298
|
}
|
|
1278
299
|
|
|
@@ -1280,363 +301,22 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
1280
301
|
const allArtifactPaths: ArtifactPaths[] = [];
|
|
1281
302
|
|
|
1282
303
|
if (hasChain && params.chain) {
|
|
1283
|
-
//
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
// Create chain directory
|
|
1301
|
-
const chainDir = createChainDir(runId);
|
|
1302
|
-
|
|
1303
|
-
// Check if chain has any parallel steps
|
|
1304
|
-
const hasParallelSteps = chainSteps.some(isParallelStep);
|
|
1305
|
-
|
|
1306
|
-
// Resolve templates using V2 (parallel-aware)
|
|
1307
|
-
const settings = loadSubagentSettings();
|
|
1308
|
-
let templates: ResolvedTemplates = resolveChainTemplatesV2(chainSteps, settings);
|
|
1309
|
-
|
|
1310
|
-
// For TUI: only show if no parallel steps (TUI v1 doesn't support parallel display)
|
|
1311
|
-
// TODO: Update TUI to support parallel steps
|
|
1312
|
-
const shouldClarify = params.clarify !== false && ctx.hasUI && !hasParallelSteps;
|
|
1313
|
-
|
|
1314
|
-
// Behavior overrides from TUI (set if TUI is shown, undefined otherwise)
|
|
1315
|
-
let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
|
|
1316
|
-
|
|
1317
|
-
if (shouldClarify) {
|
|
1318
|
-
// Sequential-only chain: use existing TUI
|
|
1319
|
-
const seqSteps = chainSteps as SequentialStep[];
|
|
1320
|
-
|
|
1321
|
-
// Load agent configs for sequential steps
|
|
1322
|
-
const agentConfigs: AgentConfig[] = [];
|
|
1323
|
-
for (const step of seqSteps) {
|
|
1324
|
-
const config = agents.find((a) => a.name === step.agent);
|
|
1325
|
-
if (!config) {
|
|
1326
|
-
removeChainDir(chainDir);
|
|
1327
|
-
return {
|
|
1328
|
-
content: [{ type: "text", text: `Unknown agent: ${step.agent}` }],
|
|
1329
|
-
isError: true,
|
|
1330
|
-
details: { mode: "chain" as const, results: [] },
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
agentConfigs.push(config);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// Build step overrides
|
|
1337
|
-
const stepOverrides: StepOverrides[] = seqSteps.map((step) => ({
|
|
1338
|
-
output: step.output,
|
|
1339
|
-
reads: step.reads,
|
|
1340
|
-
progress: step.progress,
|
|
1341
|
-
}));
|
|
1342
|
-
|
|
1343
|
-
// Pre-resolve behaviors for TUI display
|
|
1344
|
-
const resolvedBehaviors = agentConfigs.map((config, i) =>
|
|
1345
|
-
resolveStepBehavior(config, stepOverrides[i]!),
|
|
1346
|
-
);
|
|
1347
|
-
|
|
1348
|
-
// Flatten templates for TUI (all strings for sequential)
|
|
1349
|
-
const flatTemplates = templates as string[];
|
|
1350
|
-
|
|
1351
|
-
const result = await ctx.ui.custom<ChainClarifyResult>(
|
|
1352
|
-
(tui, theme, _kb, done) =>
|
|
1353
|
-
new ChainClarifyComponent(
|
|
1354
|
-
tui,
|
|
1355
|
-
theme,
|
|
1356
|
-
agentConfigs,
|
|
1357
|
-
flatTemplates,
|
|
1358
|
-
originalTask,
|
|
1359
|
-
chainDir,
|
|
1360
|
-
resolvedBehaviors,
|
|
1361
|
-
done,
|
|
1362
|
-
),
|
|
1363
|
-
{
|
|
1364
|
-
overlay: true,
|
|
1365
|
-
overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" },
|
|
1366
|
-
},
|
|
1367
|
-
);
|
|
1368
|
-
|
|
1369
|
-
if (!result || !result.confirmed) {
|
|
1370
|
-
removeChainDir(chainDir);
|
|
1371
|
-
return {
|
|
1372
|
-
content: [{ type: "text", text: "Chain cancelled" }],
|
|
1373
|
-
details: { mode: "chain", results: [] },
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
// Update templates from TUI result
|
|
1377
|
-
templates = result.templates;
|
|
1378
|
-
// Store behavior overrides from TUI (used below in sequential step execution)
|
|
1379
|
-
tuiBehaviorOverrides = result.behaviorOverrides;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// Execute chain (handles both sequential and parallel steps)
|
|
1383
|
-
const results: SingleResult[] = [];
|
|
1384
|
-
let prev = "";
|
|
1385
|
-
let globalTaskIndex = 0; // For unique artifact naming
|
|
1386
|
-
let progressCreated = false; // Track if progress.md has been created
|
|
1387
|
-
|
|
1388
|
-
for (let stepIndex = 0; stepIndex < chainSteps.length; stepIndex++) {
|
|
1389
|
-
const step = chainSteps[stepIndex]!;
|
|
1390
|
-
const stepTemplates = templates[stepIndex]!;
|
|
1391
|
-
|
|
1392
|
-
if (isParallelStep(step)) {
|
|
1393
|
-
// === PARALLEL STEP EXECUTION ===
|
|
1394
|
-
const parallelTemplates = stepTemplates as string[];
|
|
1395
|
-
const concurrency = step.concurrency ?? MAX_CONCURRENCY;
|
|
1396
|
-
const failFast = step.failFast ?? false;
|
|
1397
|
-
|
|
1398
|
-
// Create subdirectories for parallel outputs
|
|
1399
|
-
const agentNames = step.parallel.map((t) => t.agent);
|
|
1400
|
-
createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
|
|
1401
|
-
|
|
1402
|
-
// Resolve behaviors for parallel tasks
|
|
1403
|
-
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex);
|
|
1404
|
-
|
|
1405
|
-
// If any parallel task has progress enabled and progress.md hasn't been created,
|
|
1406
|
-
// create it now to avoid race conditions
|
|
1407
|
-
const anyNeedsProgress = parallelBehaviors.some((b) => b.progress);
|
|
1408
|
-
if (anyNeedsProgress && !progressCreated) {
|
|
1409
|
-
const progressPath = path.join(chainDir, "progress.md");
|
|
1410
|
-
fs.writeFileSync(progressPath, "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n");
|
|
1411
|
-
progressCreated = true;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Track if we should abort remaining tasks (for fail-fast)
|
|
1415
|
-
let aborted = false;
|
|
1416
|
-
|
|
1417
|
-
// Execute parallel tasks
|
|
1418
|
-
const parallelResults = await mapConcurrent(
|
|
1419
|
-
step.parallel,
|
|
1420
|
-
concurrency,
|
|
1421
|
-
async (task, taskIndex) => {
|
|
1422
|
-
if (aborted && failFast) {
|
|
1423
|
-
// Return a placeholder for skipped tasks
|
|
1424
|
-
return {
|
|
1425
|
-
agent: task.agent,
|
|
1426
|
-
task: "(skipped)",
|
|
1427
|
-
exitCode: -1,
|
|
1428
|
-
messages: [],
|
|
1429
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
1430
|
-
error: "Skipped due to fail-fast",
|
|
1431
|
-
} as SingleResult;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// Build task string
|
|
1435
|
-
const taskTemplate = parallelTemplates[taskIndex] ?? "{previous}";
|
|
1436
|
-
const templateHasPrevious = taskTemplate.includes("{previous}");
|
|
1437
|
-
let taskStr = taskTemplate;
|
|
1438
|
-
taskStr = taskStr.replace(/\{task\}/g, originalTask);
|
|
1439
|
-
taskStr = taskStr.replace(/\{previous\}/g, prev);
|
|
1440
|
-
taskStr = taskStr.replace(/\{chain_dir\}/g, chainDir);
|
|
1441
|
-
|
|
1442
|
-
// Add chain instructions (include previous summary only if not already in template)
|
|
1443
|
-
const behavior = parallelBehaviors[taskIndex]!;
|
|
1444
|
-
// For parallel, no single "first progress" - each manages independently
|
|
1445
|
-
taskStr += buildChainInstructions(behavior, chainDir, false, templateHasPrevious ? undefined : prev);
|
|
1446
|
-
|
|
1447
|
-
const r = await runSync(ctx.cwd, agents, task.agent, taskStr, {
|
|
1448
|
-
cwd: task.cwd ?? params.cwd,
|
|
1449
|
-
signal,
|
|
1450
|
-
runId,
|
|
1451
|
-
index: globalTaskIndex + taskIndex,
|
|
1452
|
-
sessionDir: sessionDirForIndex(globalTaskIndex + taskIndex),
|
|
1453
|
-
share: shareEnabled,
|
|
1454
|
-
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1455
|
-
artifactConfig,
|
|
1456
|
-
onUpdate: onUpdate
|
|
1457
|
-
? (p) =>
|
|
1458
|
-
onUpdate({
|
|
1459
|
-
...p,
|
|
1460
|
-
details: {
|
|
1461
|
-
mode: "chain",
|
|
1462
|
-
results: [...results, ...(p.details?.results || [])],
|
|
1463
|
-
progress: [...allProgress, ...(p.details?.progress || [])],
|
|
1464
|
-
chainAgents,
|
|
1465
|
-
totalSteps,
|
|
1466
|
-
currentStepIndex: stepIndex,
|
|
1467
|
-
},
|
|
1468
|
-
})
|
|
1469
|
-
: undefined,
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
if (r.exitCode !== 0 && failFast) {
|
|
1473
|
-
aborted = true;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
return r;
|
|
1477
|
-
},
|
|
1478
|
-
);
|
|
1479
|
-
|
|
1480
|
-
// Update global task index
|
|
1481
|
-
globalTaskIndex += step.parallel.length;
|
|
1482
|
-
|
|
1483
|
-
// Collect results and progress
|
|
1484
|
-
for (const r of parallelResults) {
|
|
1485
|
-
results.push(r);
|
|
1486
|
-
if (r.progress) allProgress.push(r.progress);
|
|
1487
|
-
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
// Check for failures (track original task index for better error messages)
|
|
1491
|
-
const failures = parallelResults
|
|
1492
|
-
.map((r, originalIndex) => ({ ...r, originalIndex }))
|
|
1493
|
-
.filter((r) => r.exitCode !== 0 && r.exitCode !== -1);
|
|
1494
|
-
if (failures.length > 0) {
|
|
1495
|
-
const failureSummary = failures
|
|
1496
|
-
.map((f) => `- Task ${f.originalIndex + 1} (${f.agent}): ${f.error || "failed"}`)
|
|
1497
|
-
.join("\n");
|
|
1498
|
-
const errorMsg = `Parallel step ${stepIndex + 1} failed:\n${failureSummary}`;
|
|
1499
|
-
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
1500
|
-
index: stepIndex,
|
|
1501
|
-
error: errorMsg,
|
|
1502
|
-
});
|
|
1503
|
-
return {
|
|
1504
|
-
content: [{ type: "text", text: summary }],
|
|
1505
|
-
details: {
|
|
1506
|
-
mode: "chain",
|
|
1507
|
-
results,
|
|
1508
|
-
progress: params.includeProgress ? allProgress : undefined,
|
|
1509
|
-
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1510
|
-
chainAgents,
|
|
1511
|
-
totalSteps,
|
|
1512
|
-
currentStepIndex: stepIndex,
|
|
1513
|
-
},
|
|
1514
|
-
isError: true,
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
// Aggregate outputs for {previous}
|
|
1519
|
-
const taskResults: ParallelTaskResult[] = parallelResults.map((r, i) => ({
|
|
1520
|
-
agent: r.agent,
|
|
1521
|
-
taskIndex: i,
|
|
1522
|
-
output: getFinalOutput(r.messages),
|
|
1523
|
-
exitCode: r.exitCode,
|
|
1524
|
-
error: r.error,
|
|
1525
|
-
}));
|
|
1526
|
-
prev = aggregateParallelOutputs(taskResults);
|
|
1527
|
-
} else {
|
|
1528
|
-
// === SEQUENTIAL STEP EXECUTION ===
|
|
1529
|
-
const seqStep = step as SequentialStep;
|
|
1530
|
-
const stepTemplate = stepTemplates as string;
|
|
1531
|
-
|
|
1532
|
-
// Get agent config
|
|
1533
|
-
const agentConfig = agents.find((a) => a.name === seqStep.agent);
|
|
1534
|
-
if (!agentConfig) {
|
|
1535
|
-
removeChainDir(chainDir);
|
|
1536
|
-
return {
|
|
1537
|
-
content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
|
|
1538
|
-
isError: true,
|
|
1539
|
-
details: { mode: "chain" as const, results: [] },
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// Build task string (check if template has {previous} before replacement)
|
|
1544
|
-
const templateHasPrevious = stepTemplate.includes("{previous}");
|
|
1545
|
-
let stepTask = stepTemplate;
|
|
1546
|
-
stepTask = stepTask.replace(/\{task\}/g, originalTask);
|
|
1547
|
-
stepTask = stepTask.replace(/\{previous\}/g, prev);
|
|
1548
|
-
stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
|
|
1549
|
-
|
|
1550
|
-
// Resolve behavior (TUI overrides take precedence over step config)
|
|
1551
|
-
const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
|
|
1552
|
-
const stepOverride: StepOverrides = {
|
|
1553
|
-
output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
|
|
1554
|
-
reads: tuiOverride?.reads !== undefined ? tuiOverride.reads : seqStep.reads,
|
|
1555
|
-
progress: tuiOverride?.progress !== undefined ? tuiOverride.progress : seqStep.progress,
|
|
1556
|
-
};
|
|
1557
|
-
const behavior = resolveStepBehavior(agentConfig, stepOverride);
|
|
1558
|
-
|
|
1559
|
-
// Determine if this is the first agent to create progress.md
|
|
1560
|
-
const isFirstProgress = behavior.progress && !progressCreated;
|
|
1561
|
-
if (isFirstProgress) {
|
|
1562
|
-
progressCreated = true;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
// Add chain instructions (include previous summary only if not already in template)
|
|
1566
|
-
stepTask += buildChainInstructions(behavior, chainDir, isFirstProgress, templateHasPrevious ? undefined : prev);
|
|
1567
|
-
|
|
1568
|
-
// Run step
|
|
1569
|
-
const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
1570
|
-
cwd: seqStep.cwd ?? params.cwd,
|
|
1571
|
-
signal,
|
|
1572
|
-
runId,
|
|
1573
|
-
index: globalTaskIndex,
|
|
1574
|
-
sessionDir: sessionDirForIndex(globalTaskIndex),
|
|
1575
|
-
share: shareEnabled,
|
|
1576
|
-
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1577
|
-
artifactConfig,
|
|
1578
|
-
onUpdate: onUpdate
|
|
1579
|
-
? (p) =>
|
|
1580
|
-
onUpdate({
|
|
1581
|
-
...p,
|
|
1582
|
-
details: {
|
|
1583
|
-
mode: "chain",
|
|
1584
|
-
results: [...results, ...(p.details?.results || [])],
|
|
1585
|
-
progress: [...allProgress, ...(p.details?.progress || [])],
|
|
1586
|
-
chainAgents,
|
|
1587
|
-
totalSteps,
|
|
1588
|
-
currentStepIndex: stepIndex,
|
|
1589
|
-
},
|
|
1590
|
-
})
|
|
1591
|
-
: undefined,
|
|
1592
|
-
});
|
|
1593
|
-
|
|
1594
|
-
globalTaskIndex++;
|
|
1595
|
-
results.push(r);
|
|
1596
|
-
if (r.progress) allProgress.push(r.progress);
|
|
1597
|
-
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
1598
|
-
|
|
1599
|
-
// On failure, leave chain_dir for debugging
|
|
1600
|
-
if (r.exitCode !== 0) {
|
|
1601
|
-
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
1602
|
-
index: stepIndex,
|
|
1603
|
-
error: r.error || "Chain failed",
|
|
1604
|
-
});
|
|
1605
|
-
return {
|
|
1606
|
-
content: [{ type: "text", text: summary }],
|
|
1607
|
-
details: {
|
|
1608
|
-
mode: "chain",
|
|
1609
|
-
results,
|
|
1610
|
-
progress: params.includeProgress ? allProgress : undefined,
|
|
1611
|
-
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1612
|
-
chainAgents,
|
|
1613
|
-
totalSteps,
|
|
1614
|
-
currentStepIndex: stepIndex,
|
|
1615
|
-
},
|
|
1616
|
-
isError: true,
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
prev = getFinalOutput(r.messages);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// Chain complete - return summary with paths
|
|
1625
|
-
// Chain dir left for inspection (cleaned up after 24h)
|
|
1626
|
-
const summary = buildChainSummary(chainSteps, results, chainDir, "completed");
|
|
1627
|
-
|
|
1628
|
-
return {
|
|
1629
|
-
content: [{ type: "text", text: summary }],
|
|
1630
|
-
details: {
|
|
1631
|
-
mode: "chain",
|
|
1632
|
-
results,
|
|
1633
|
-
progress: params.includeProgress ? allProgress : undefined,
|
|
1634
|
-
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1635
|
-
chainAgents,
|
|
1636
|
-
totalSteps,
|
|
1637
|
-
// currentStepIndex omitted for completed chains
|
|
1638
|
-
},
|
|
1639
|
-
};
|
|
304
|
+
// Use extracted chain execution module
|
|
305
|
+
return executeChain({
|
|
306
|
+
chain: params.chain as ChainStep[],
|
|
307
|
+
agents,
|
|
308
|
+
ctx,
|
|
309
|
+
signal,
|
|
310
|
+
runId,
|
|
311
|
+
cwd: params.cwd,
|
|
312
|
+
shareEnabled,
|
|
313
|
+
sessionDirForIndex,
|
|
314
|
+
artifactsDir,
|
|
315
|
+
artifactConfig,
|
|
316
|
+
includeProgress: params.includeProgress,
|
|
317
|
+
clarify: params.clarify,
|
|
318
|
+
onUpdate,
|
|
319
|
+
});
|
|
1640
320
|
}
|
|
1641
321
|
|
|
1642
322
|
if (hasTasks && params.tasks) {
|
|
@@ -1775,230 +455,8 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
1775
455
|
);
|
|
1776
456
|
},
|
|
1777
457
|
|
|
1778
|
-
renderResult(result,
|
|
1779
|
-
|
|
1780
|
-
if (!d || !d.results.length) {
|
|
1781
|
-
const t = result.content[0];
|
|
1782
|
-
return new Text(t?.type === "text" ? t.text : "(no output)", 0, 0);
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
const mdTheme = getMarkdownTheme();
|
|
1786
|
-
|
|
1787
|
-
if (d.mode === "single" && d.results.length === 1) {
|
|
1788
|
-
const r = d.results[0];
|
|
1789
|
-
const isRunning = r.progress?.status === "running";
|
|
1790
|
-
const icon = isRunning
|
|
1791
|
-
? theme.fg("warning", "...")
|
|
1792
|
-
: r.exitCode === 0
|
|
1793
|
-
? theme.fg("success", "ok")
|
|
1794
|
-
: theme.fg("error", "X");
|
|
1795
|
-
const output = r.truncation?.text || getFinalOutput(r.messages);
|
|
1796
|
-
|
|
1797
|
-
const progressInfo = isRunning && r.progress
|
|
1798
|
-
? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
|
|
1799
|
-
: r.progressSummary
|
|
1800
|
-
? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tokens, ${formatDuration(r.progressSummary.durationMs)}`
|
|
1801
|
-
: "";
|
|
1802
|
-
|
|
1803
|
-
if (expanded) {
|
|
1804
|
-
const c = new Container();
|
|
1805
|
-
c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, 0, 0));
|
|
1806
|
-
c.addChild(new Spacer(1));
|
|
1807
|
-
c.addChild(
|
|
1808
|
-
new Text(theme.fg("dim", `Task: ${r.task.slice(0, 100)}${r.task.length > 100 ? "..." : ""}`), 0, 0),
|
|
1809
|
-
);
|
|
1810
|
-
c.addChild(new Spacer(1));
|
|
1811
|
-
|
|
1812
|
-
const items = getDisplayItems(r.messages);
|
|
1813
|
-
for (const item of items) {
|
|
1814
|
-
if (item.type === "tool")
|
|
1815
|
-
c.addChild(new Text(theme.fg("muted", formatToolCall(item.name, item.args)), 0, 0));
|
|
1816
|
-
}
|
|
1817
|
-
if (items.length) c.addChild(new Spacer(1));
|
|
1818
|
-
|
|
1819
|
-
if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
|
|
1820
|
-
c.addChild(new Spacer(1));
|
|
1821
|
-
c.addChild(new Text(theme.fg("dim", formatUsage(r.usage, r.model)), 0, 0));
|
|
1822
|
-
if (r.sessionFile) {
|
|
1823
|
-
c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), 0, 0));
|
|
1824
|
-
}
|
|
1825
|
-
// Sharing disabled - session file path shown above
|
|
1826
|
-
|
|
1827
|
-
if (r.artifactPaths) {
|
|
1828
|
-
c.addChild(new Spacer(1));
|
|
1829
|
-
c.addChild(new Text(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), 0, 0));
|
|
1830
|
-
}
|
|
1831
|
-
return c;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
const lines = [`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`];
|
|
1835
|
-
|
|
1836
|
-
if (isRunning && r.progress) {
|
|
1837
|
-
if (r.progress.currentTool) {
|
|
1838
|
-
const toolLine = r.progress.currentToolArgs
|
|
1839
|
-
? `${r.progress.currentTool}: ${r.progress.currentToolArgs.slice(0, 60)}${r.progress.currentToolArgs.length > 60 ? "..." : ""}`
|
|
1840
|
-
: r.progress.currentTool;
|
|
1841
|
-
lines.push(theme.fg("warning", `> ${toolLine}`));
|
|
1842
|
-
}
|
|
1843
|
-
for (const line of r.progress.recentOutput.slice(-3)) {
|
|
1844
|
-
lines.push(theme.fg("dim", ` ${line.slice(0, 80)}${line.length > 80 ? "..." : ""}`));
|
|
1845
|
-
}
|
|
1846
|
-
lines.push(theme.fg("dim", "(ctrl+o to expand)"));
|
|
1847
|
-
} else {
|
|
1848
|
-
const items = getDisplayItems(r.messages).slice(-COLLAPSED_ITEMS);
|
|
1849
|
-
for (const item of items) {
|
|
1850
|
-
if (item.type === "tool") lines.push(theme.fg("muted", formatToolCall(item.name, item.args)));
|
|
1851
|
-
else lines.push(item.text.slice(0, 80) + (item.text.length > 80 ? "..." : ""));
|
|
1852
|
-
}
|
|
1853
|
-
lines.push(theme.fg("dim", formatUsage(r.usage, r.model)));
|
|
1854
|
-
}
|
|
1855
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
1859
|
-
|| d.results.some((r) => r.progress?.status === "running");
|
|
1860
|
-
const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
|
|
1861
|
-
const icon = hasRunning
|
|
1862
|
-
? theme.fg("warning", "...")
|
|
1863
|
-
: ok === d.results.length
|
|
1864
|
-
? theme.fg("success", "ok")
|
|
1865
|
-
: theme.fg("error", "X");
|
|
1866
|
-
|
|
1867
|
-
const totalSummary =
|
|
1868
|
-
d.progressSummary ||
|
|
1869
|
-
d.results.reduce(
|
|
1870
|
-
(acc, r) => {
|
|
1871
|
-
const prog = r.progress || r.progressSummary;
|
|
1872
|
-
if (prog) {
|
|
1873
|
-
acc.toolCount += prog.toolCount;
|
|
1874
|
-
acc.tokens += prog.tokens;
|
|
1875
|
-
acc.durationMs =
|
|
1876
|
-
d.mode === "chain"
|
|
1877
|
-
? acc.durationMs + prog.durationMs
|
|
1878
|
-
: Math.max(acc.durationMs, prog.durationMs);
|
|
1879
|
-
}
|
|
1880
|
-
return acc;
|
|
1881
|
-
},
|
|
1882
|
-
{ toolCount: 0, tokens: 0, durationMs: 0 },
|
|
1883
|
-
);
|
|
1884
|
-
|
|
1885
|
-
const summaryStr =
|
|
1886
|
-
totalSummary.toolCount || totalSummary.tokens
|
|
1887
|
-
? ` | ${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
|
|
1888
|
-
: "";
|
|
1889
|
-
|
|
1890
|
-
const modeLabel = d.mode === "parallel" ? "parallel (no live progress)" : d.mode;
|
|
1891
|
-
// Use chain metadata for accurate step counts
|
|
1892
|
-
const totalStepsCount = d.totalSteps ?? d.results.length;
|
|
1893
|
-
const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : ok + 1;
|
|
1894
|
-
const stepInfo = hasRunning ? ` ${currentStep}/${totalStepsCount}` : ` ${ok}/${totalStepsCount}`;
|
|
1895
|
-
|
|
1896
|
-
// Build chain visualization: "scout → planner" with status icons
|
|
1897
|
-
// Note: Only works correctly for sequential chains. Chains with parallel steps
|
|
1898
|
-
// (indicated by "[agent1+agent2]" format) have multiple results per step,
|
|
1899
|
-
// breaking the 1:1 mapping between chainAgents and results.
|
|
1900
|
-
const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
|
|
1901
|
-
const chainVis = d.chainAgents?.length && !hasParallelInChain
|
|
1902
|
-
? d.chainAgents
|
|
1903
|
-
.map((agent, i) => {
|
|
1904
|
-
const result = d.results[i];
|
|
1905
|
-
const isFailed = result && result.exitCode !== 0 && result.progress?.status !== "running";
|
|
1906
|
-
const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
|
|
1907
|
-
const isCurrent = i === (d.currentStepIndex ?? d.results.length);
|
|
1908
|
-
const icon = isFailed
|
|
1909
|
-
? theme.fg("error", "✗")
|
|
1910
|
-
: isComplete
|
|
1911
|
-
? theme.fg("success", "✓")
|
|
1912
|
-
: isCurrent && hasRunning
|
|
1913
|
-
? theme.fg("warning", "●")
|
|
1914
|
-
: theme.fg("dim", "○");
|
|
1915
|
-
return `${icon}${agent}`;
|
|
1916
|
-
})
|
|
1917
|
-
.join(theme.fg("dim", " → "))
|
|
1918
|
-
: null;
|
|
1919
|
-
|
|
1920
|
-
if (expanded) {
|
|
1921
|
-
const c = new Container();
|
|
1922
|
-
c.addChild(
|
|
1923
|
-
new Text(
|
|
1924
|
-
`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`,
|
|
1925
|
-
0,
|
|
1926
|
-
0,
|
|
1927
|
-
),
|
|
1928
|
-
);
|
|
1929
|
-
// Show chain visualization in expanded view
|
|
1930
|
-
if (chainVis) {
|
|
1931
|
-
c.addChild(new Text(` ${chainVis}`, 0, 0));
|
|
1932
|
-
}
|
|
1933
|
-
for (let i = 0; i < d.results.length; i++) {
|
|
1934
|
-
const r = d.results[i];
|
|
1935
|
-
c.addChild(new Spacer(1));
|
|
1936
|
-
// Check both r.progress and d.progress array for running status
|
|
1937
|
-
const progressFromArray = d.progress?.find((p) => p.index === i);
|
|
1938
|
-
const rProg = r.progress || progressFromArray || r.progressSummary;
|
|
1939
|
-
const rRunning = rProg?.status === "running";
|
|
1940
|
-
const rIcon = rRunning
|
|
1941
|
-
? theme.fg("warning", "...")
|
|
1942
|
-
: r.exitCode === 0
|
|
1943
|
-
? theme.fg("success", "ok")
|
|
1944
|
-
: theme.fg("error", "X");
|
|
1945
|
-
const rProgress = rProg
|
|
1946
|
-
? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}`
|
|
1947
|
-
: "";
|
|
1948
|
-
c.addChild(new Text(`${rIcon} ${theme.bold(r.agent)}${rProgress}`, 0, 0));
|
|
1949
|
-
|
|
1950
|
-
if (rRunning && rProg) {
|
|
1951
|
-
if (rProg.currentTool) {
|
|
1952
|
-
const toolLine = rProg.currentToolArgs
|
|
1953
|
-
? `${rProg.currentTool}: ${rProg.currentToolArgs.slice(0, 50)}${rProg.currentToolArgs.length > 50 ? "..." : ""}`
|
|
1954
|
-
: rProg.currentTool;
|
|
1955
|
-
c.addChild(new Text(theme.fg("warning", ` > ${toolLine}`), 0, 0));
|
|
1956
|
-
}
|
|
1957
|
-
for (const line of rProg.recentOutput.slice(-2)) {
|
|
1958
|
-
c.addChild(new Text(theme.fg("dim", ` ${line.slice(0, 70)}${line.length > 70 ? "..." : ""}`), 0, 0));
|
|
1959
|
-
}
|
|
1960
|
-
} else {
|
|
1961
|
-
const out = r.truncation?.text || getFinalOutput(r.messages);
|
|
1962
|
-
if (out) c.addChild(new Markdown(out, 0, 0, mdTheme));
|
|
1963
|
-
c.addChild(new Text(theme.fg("dim", formatUsage(r.usage, r.model)), 0, 0));
|
|
1964
|
-
if (r.sessionFile) {
|
|
1965
|
-
c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), 0, 0));
|
|
1966
|
-
}
|
|
1967
|
-
// Sharing disabled - session file path shown above
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
if (d.artifacts) {
|
|
1972
|
-
c.addChild(new Spacer(1));
|
|
1973
|
-
c.addChild(new Text(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), 0, 0));
|
|
1974
|
-
}
|
|
1975
|
-
return c;
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
const lines = [`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`];
|
|
1979
|
-
// Show chain visualization if available
|
|
1980
|
-
if (chainVis) {
|
|
1981
|
-
lines.push(` ${chainVis}`);
|
|
1982
|
-
}
|
|
1983
|
-
// Find running progress from d.progress array (more reliable) or d.results
|
|
1984
|
-
const runningProgress = d.progress?.find((p) => p.status === "running")
|
|
1985
|
-
|| d.results.find((r) => r.progress?.status === "running")?.progress;
|
|
1986
|
-
if (runningProgress) {
|
|
1987
|
-
if (runningProgress.currentTool) {
|
|
1988
|
-
const toolLine = runningProgress.currentToolArgs
|
|
1989
|
-
? `${runningProgress.currentTool}: ${runningProgress.currentToolArgs.slice(0, 50)}${runningProgress.currentToolArgs.length > 50 ? "..." : ""}`
|
|
1990
|
-
: runningProgress.currentTool;
|
|
1991
|
-
lines.push(theme.fg("warning", ` > ${toolLine}`));
|
|
1992
|
-
}
|
|
1993
|
-
for (const line of runningProgress.recentOutput.slice(-2)) {
|
|
1994
|
-
lines.push(theme.fg("dim", ` ${line.slice(0, 70)}${line.length > 70 ? "..." : ""}`));
|
|
1995
|
-
}
|
|
1996
|
-
lines.push(theme.fg("dim", "(ctrl+o to expand)"));
|
|
1997
|
-
} else if (hasRunning) {
|
|
1998
|
-
// Fallback: we know something is running but can't find details
|
|
1999
|
-
lines.push(theme.fg("dim", "(ctrl+o to expand)"));
|
|
2000
|
-
}
|
|
2001
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
458
|
+
renderResult(result, options, theme) {
|
|
459
|
+
return renderSubagentResult(result, options, theme);
|
|
2002
460
|
},
|
|
2003
461
|
|
|
2004
462
|
};
|