pi-subagents 0.3.0 → 0.3.2
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 +35 -0
- package/agents.ts +0 -10
- package/async-execution.ts +261 -0
- package/chain-execution.ts +444 -0
- package/execution.ts +383 -0
- package/formatters.ts +111 -0
- package/index.ts +92 -1615
- package/package.json +2 -2
- package/render.ts +308 -0
- package/schemas.ts +90 -0
- package/settings.ts +2 -166
- package/types.ts +166 -0
- package/utils.ts +325 -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");
|
|
@@ -936,6 +73,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
936
73
|
let baseCwd = process.cwd();
|
|
937
74
|
let currentSessionId: string | null = null;
|
|
938
75
|
const asyncJobs = new Map<string, AsyncJobState>();
|
|
76
|
+
const cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>(); // Track cleanup timeouts
|
|
939
77
|
let lastUiContext: ExtensionContext | null = null;
|
|
940
78
|
let poller: NodeJS.Timeout | null = null;
|
|
941
79
|
|
|
@@ -951,6 +89,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
951
89
|
}
|
|
952
90
|
|
|
953
91
|
for (const job of asyncJobs.values()) {
|
|
92
|
+
// Skip status reads for finished jobs - they won't change
|
|
93
|
+
if (job.status === "complete" || job.status === "failed") {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
954
96
|
const status = readStatus(job.asyncDir);
|
|
955
97
|
if (status) {
|
|
956
98
|
job.status = status.state;
|
|
@@ -997,7 +139,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
997
139
|
.filter((f) => f.endsWith(".json"))
|
|
998
140
|
.forEach(handleResult);
|
|
999
141
|
|
|
1000
|
-
const tool: ToolDefinition<typeof
|
|
142
|
+
const tool: ToolDefinition<typeof SubagentParams, Details> = {
|
|
1001
143
|
name: "subagent",
|
|
1002
144
|
label: "Subagent",
|
|
1003
145
|
description: `Delegate to subagents. Use exactly ONE mode:
|
|
@@ -1005,7 +147,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1005
147
|
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential, {previous} passes output
|
|
1006
148
|
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent
|
|
1007
149
|
For "scout → planner" or multi-step flows, use chain (not multiple single calls).`,
|
|
1008
|
-
parameters:
|
|
150
|
+
parameters: SubagentParams,
|
|
1009
151
|
|
|
1010
152
|
async execute(_id, params, onUpdate, ctx, signal) {
|
|
1011
153
|
const scope: AgentScope = params.agentScope ?? "user";
|
|
@@ -1071,9 +213,11 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
1071
213
|
// First step must have a task
|
|
1072
214
|
const firstStep = params.chain[0] as ChainStep;
|
|
1073
215
|
if (isParallelStep(firstStep)) {
|
|
1074
|
-
|
|
216
|
+
// All tasks in the first parallel step must have tasks (no {previous} to reference)
|
|
217
|
+
const missingTaskIndex = firstStep.parallel.findIndex((t) => !t.task);
|
|
218
|
+
if (missingTaskIndex !== -1) {
|
|
1075
219
|
return {
|
|
1076
|
-
content: [{ type: "text", text:
|
|
220
|
+
content: [{ type: "text", text: `First parallel step: task ${missingTaskIndex + 1} must have a task (no previous output to reference)` }],
|
|
1077
221
|
isError: true,
|
|
1078
222
|
details: { mode: "chain" as const, results: [] },
|
|
1079
223
|
};
|
|
@@ -1110,169 +254,51 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
1110
254
|
}
|
|
1111
255
|
|
|
1112
256
|
if (effectiveAsync) {
|
|
1113
|
-
if (!
|
|
257
|
+
if (!isAsyncAvailable()) {
|
|
1114
258
|
return {
|
|
1115
259
|
content: [{ type: "text", text: "jiti not found" }],
|
|
1116
260
|
isError: true,
|
|
1117
261
|
details: { mode: "single" as const, results: [] },
|
|
1118
262
|
};
|
|
263
|
+
}
|
|
1119
264
|
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
|
-
};
|
|
265
|
+
const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: currentSessionId! };
|
|
1137
266
|
|
|
1138
267
|
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
|
-
};
|
|
268
|
+
return executeAsyncChain(id, {
|
|
269
|
+
chain: params.chain as ChainStep[],
|
|
270
|
+
agents,
|
|
271
|
+
ctx: asyncCtx,
|
|
272
|
+
cwd: params.cwd,
|
|
273
|
+
maxOutput: params.maxOutput,
|
|
274
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
275
|
+
artifactConfig,
|
|
276
|
+
shareEnabled,
|
|
277
|
+
sessionRoot,
|
|
1174
278
|
});
|
|
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
279
|
}
|
|
1219
280
|
|
|
1220
281
|
if (hasSingle) {
|
|
1221
282
|
const a = agents.find((x) => x.name === params.agent);
|
|
1222
|
-
if (!a)
|
|
283
|
+
if (!a) {
|
|
1223
284
|
return {
|
|
1224
285
|
content: [{ type: "text", text: `Unknown: ${params.agent}` }],
|
|
1225
286
|
isError: true,
|
|
1226
287
|
details: { mode: "single" as const, results: [] },
|
|
1227
288
|
};
|
|
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
289
|
}
|
|
1272
|
-
return {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
290
|
+
return executeAsyncSingle(id, {
|
|
291
|
+
agent: params.agent!,
|
|
292
|
+
task: params.task!,
|
|
293
|
+
agentConfig: a,
|
|
294
|
+
ctx: asyncCtx,
|
|
295
|
+
cwd: params.cwd,
|
|
296
|
+
maxOutput: params.maxOutput,
|
|
297
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
298
|
+
artifactConfig,
|
|
299
|
+
shareEnabled,
|
|
300
|
+
sessionRoot,
|
|
301
|
+
});
|
|
1276
302
|
}
|
|
1277
303
|
}
|
|
1278
304
|
|
|
@@ -1280,363 +306,22 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
1280
306
|
const allArtifactPaths: ArtifactPaths[] = [];
|
|
1281
307
|
|
|
1282
308
|
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
|
-
};
|
|
309
|
+
// Use extracted chain execution module
|
|
310
|
+
return executeChain({
|
|
311
|
+
chain: params.chain as ChainStep[],
|
|
312
|
+
agents,
|
|
313
|
+
ctx,
|
|
314
|
+
signal,
|
|
315
|
+
runId,
|
|
316
|
+
cwd: params.cwd,
|
|
317
|
+
shareEnabled,
|
|
318
|
+
sessionDirForIndex,
|
|
319
|
+
artifactsDir,
|
|
320
|
+
artifactConfig,
|
|
321
|
+
includeProgress: params.includeProgress,
|
|
322
|
+
clarify: params.clarify,
|
|
323
|
+
onUpdate,
|
|
324
|
+
});
|
|
1640
325
|
}
|
|
1641
326
|
|
|
1642
327
|
if (hasTasks && params.tasks) {
|
|
@@ -1775,230 +460,8 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
1775
460
|
);
|
|
1776
461
|
},
|
|
1777
462
|
|
|
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);
|
|
463
|
+
renderResult(result, options, theme) {
|
|
464
|
+
return renderSubagentResult(result, options, theme);
|
|
2002
465
|
},
|
|
2003
466
|
|
|
2004
467
|
};
|
|
@@ -2131,10 +594,13 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
2131
594
|
if (lastUiContext) {
|
|
2132
595
|
renderWidget(lastUiContext, Array.from(asyncJobs.values()));
|
|
2133
596
|
}
|
|
2134
|
-
|
|
597
|
+
// Schedule cleanup after 10 seconds (track timer for cleanup on shutdown)
|
|
598
|
+
const timer = setTimeout(() => {
|
|
599
|
+
cleanupTimers.delete(asyncId);
|
|
2135
600
|
asyncJobs.delete(asyncId);
|
|
2136
601
|
if (lastUiContext) renderWidget(lastUiContext, Array.from(asyncJobs.values()));
|
|
2137
602
|
}, 10000);
|
|
603
|
+
cleanupTimers.set(asyncId, timer);
|
|
2138
604
|
});
|
|
2139
605
|
|
|
2140
606
|
pi.on("tool_result", (event, ctx) => {
|
|
@@ -2150,6 +616,8 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
2150
616
|
pi.on("session_start", (_event, ctx) => {
|
|
2151
617
|
baseCwd = ctx.cwd;
|
|
2152
618
|
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
619
|
+
for (const timer of cleanupTimers.values()) clearTimeout(timer);
|
|
620
|
+
cleanupTimers.clear();
|
|
2153
621
|
asyncJobs.clear();
|
|
2154
622
|
if (ctx.hasUI) {
|
|
2155
623
|
lastUiContext = ctx;
|
|
@@ -2159,6 +627,8 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
2159
627
|
pi.on("session_switch", (_event, ctx) => {
|
|
2160
628
|
baseCwd = ctx.cwd;
|
|
2161
629
|
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
630
|
+
for (const timer of cleanupTimers.values()) clearTimeout(timer);
|
|
631
|
+
cleanupTimers.clear();
|
|
2162
632
|
asyncJobs.clear();
|
|
2163
633
|
if (ctx.hasUI) {
|
|
2164
634
|
lastUiContext = ctx;
|
|
@@ -2168,6 +638,8 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
2168
638
|
pi.on("session_branch", (_event, ctx) => {
|
|
2169
639
|
baseCwd = ctx.cwd;
|
|
2170
640
|
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
641
|
+
for (const timer of cleanupTimers.values()) clearTimeout(timer);
|
|
642
|
+
cleanupTimers.clear();
|
|
2171
643
|
asyncJobs.clear();
|
|
2172
644
|
if (ctx.hasUI) {
|
|
2173
645
|
lastUiContext = ctx;
|
|
@@ -2178,6 +650,11 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
2178
650
|
watcher.close();
|
|
2179
651
|
if (poller) clearInterval(poller);
|
|
2180
652
|
poller = null;
|
|
653
|
+
// Clear all pending cleanup timers
|
|
654
|
+
for (const timer of cleanupTimers.values()) {
|
|
655
|
+
clearTimeout(timer);
|
|
656
|
+
}
|
|
657
|
+
cleanupTimers.clear();
|
|
2181
658
|
asyncJobs.clear();
|
|
2182
659
|
if (lastUiContext?.hasUI) {
|
|
2183
660
|
lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
|