pi-subagents 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/README.md +300 -0
- package/agents.ts +172 -0
- package/artifacts.ts +70 -0
- package/chain-clarify.ts +612 -0
- package/index.ts +2186 -0
- package/install.mjs +93 -0
- package/notify.ts +87 -0
- package/package.json +38 -0
- package/settings.ts +492 -0
- package/subagent-runner.ts +608 -0
- package/types.ts +114 -0
package/index.ts
ADDED
|
@@ -0,0 +1,2186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent Tool
|
|
3
|
+
*
|
|
4
|
+
* Full-featured subagent with sync and async modes.
|
|
5
|
+
* - Sync (default): Streams output, renders markdown, tracks usage
|
|
6
|
+
* - Async: Background execution, emits events when done
|
|
7
|
+
*
|
|
8
|
+
* Modes: single (agent + task), parallel (tasks[]), chain (chain[] with {previous})
|
|
9
|
+
* Toggle: async parameter (default: false, configurable via config.json)
|
|
10
|
+
*
|
|
11
|
+
* Config file: ~/.pi/agent/extensions/subagent/config.json
|
|
12
|
+
* { "asyncByDefault": true }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import * as os from "node:os";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
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";
|
|
27
|
+
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
28
|
+
import {
|
|
29
|
+
loadSubagentSettings,
|
|
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";
|
|
59
|
+
import {
|
|
60
|
+
type AgentProgress,
|
|
61
|
+
type ArtifactConfig,
|
|
62
|
+
type ArtifactPaths,
|
|
63
|
+
DEFAULT_ARTIFACT_CONFIG,
|
|
64
|
+
DEFAULT_MAX_OUTPUT,
|
|
65
|
+
type MaxOutputConfig,
|
|
66
|
+
type ProgressSummary,
|
|
67
|
+
type TruncationResult,
|
|
68
|
+
truncateOutput,
|
|
69
|
+
} from "./types.js";
|
|
70
|
+
|
|
71
|
+
const MAX_PARALLEL = 8;
|
|
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
|
+
}
|
|
913
|
+
|
|
914
|
+
function loadConfig(): ExtensionConfig {
|
|
915
|
+
const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");
|
|
916
|
+
try {
|
|
917
|
+
if (fs.existsSync(configPath)) {
|
|
918
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
|
|
919
|
+
}
|
|
920
|
+
} catch {}
|
|
921
|
+
return {};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
925
|
+
fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
|
926
|
+
fs.mkdirSync(ASYNC_DIR, { recursive: true });
|
|
927
|
+
|
|
928
|
+
// Cleanup old chain directories on startup (after 24h)
|
|
929
|
+
cleanupOldChainDirs();
|
|
930
|
+
|
|
931
|
+
const config = loadConfig();
|
|
932
|
+
const asyncByDefault = config.asyncByDefault === true;
|
|
933
|
+
|
|
934
|
+
const tempArtifactsDir = getArtifactsDir(null);
|
|
935
|
+
cleanupOldArtifacts(tempArtifactsDir, DEFAULT_ARTIFACT_CONFIG.cleanupDays);
|
|
936
|
+
let baseCwd = process.cwd();
|
|
937
|
+
let currentSessionId: string | null = null;
|
|
938
|
+
const asyncJobs = new Map<string, AsyncJobState>();
|
|
939
|
+
let lastUiContext: ExtensionContext | null = null;
|
|
940
|
+
let poller: NodeJS.Timeout | null = null;
|
|
941
|
+
|
|
942
|
+
const ensurePoller = () => {
|
|
943
|
+
if (poller) return;
|
|
944
|
+
poller = setInterval(() => {
|
|
945
|
+
if (!lastUiContext || !lastUiContext.hasUI) return;
|
|
946
|
+
if (asyncJobs.size === 0) {
|
|
947
|
+
renderWidget(lastUiContext, []);
|
|
948
|
+
clearInterval(poller);
|
|
949
|
+
poller = null;
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
for (const job of asyncJobs.values()) {
|
|
954
|
+
const status = readStatus(job.asyncDir);
|
|
955
|
+
if (status) {
|
|
956
|
+
job.status = status.state;
|
|
957
|
+
job.mode = status.mode;
|
|
958
|
+
job.currentStep = status.currentStep ?? job.currentStep;
|
|
959
|
+
job.stepsTotal = status.steps?.length ?? job.stepsTotal;
|
|
960
|
+
job.startedAt = status.startedAt ?? job.startedAt;
|
|
961
|
+
job.updatedAt = status.lastUpdate ?? Date.now();
|
|
962
|
+
if (status.steps?.length) {
|
|
963
|
+
job.agents = status.steps.map((step) => step.agent);
|
|
964
|
+
}
|
|
965
|
+
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
966
|
+
job.outputFile = status.outputFile ?? job.outputFile;
|
|
967
|
+
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
968
|
+
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
969
|
+
// job.shareUrl = status.shareUrl ?? job.shareUrl;
|
|
970
|
+
} else {
|
|
971
|
+
job.status = job.status === "queued" ? "running" : job.status;
|
|
972
|
+
job.updatedAt = Date.now();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
renderWidget(lastUiContext, Array.from(asyncJobs.values()));
|
|
977
|
+
}, POLL_INTERVAL_MS);
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
const handleResult = (file: string) => {
|
|
981
|
+
const p = path.join(RESULTS_DIR, file);
|
|
982
|
+
if (!fs.existsSync(p)) return;
|
|
983
|
+
try {
|
|
984
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
985
|
+
if (data.sessionId && data.sessionId !== currentSessionId) return;
|
|
986
|
+
if (!data.sessionId && data.cwd && data.cwd !== baseCwd) return;
|
|
987
|
+
pi.events.emit("subagent:complete", data);
|
|
988
|
+
pi.events.emit("subagent_enhanced:complete", data);
|
|
989
|
+
fs.unlinkSync(p);
|
|
990
|
+
} catch {}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
const watcher = fs.watch(RESULTS_DIR, (ev, file) => {
|
|
994
|
+
if (ev === "rename" && file?.toString().endsWith(".json")) setTimeout(() => handleResult(file.toString()), 50);
|
|
995
|
+
});
|
|
996
|
+
fs.readdirSync(RESULTS_DIR)
|
|
997
|
+
.filter((f) => f.endsWith(".json"))
|
|
998
|
+
.forEach(handleResult);
|
|
999
|
+
|
|
1000
|
+
const tool: ToolDefinition<typeof Params, Details> = {
|
|
1001
|
+
name: "subagent",
|
|
1002
|
+
label: "Subagent",
|
|
1003
|
+
description: `Delegate to subagents. Use exactly ONE mode:
|
|
1004
|
+
• SINGLE: { agent, task } - one task
|
|
1005
|
+
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential, {previous} passes output
|
|
1006
|
+
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent
|
|
1007
|
+
For "scout → planner" or multi-step flows, use chain (not multiple single calls).`,
|
|
1008
|
+
parameters: Params,
|
|
1009
|
+
|
|
1010
|
+
async execute(_id, params, onUpdate, ctx, signal) {
|
|
1011
|
+
const scope: AgentScope = params.agentScope ?? "user";
|
|
1012
|
+
baseCwd = ctx.cwd;
|
|
1013
|
+
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1014
|
+
const agents = discoverAgents(ctx.cwd, scope).agents;
|
|
1015
|
+
const runId = randomUUID().slice(0, 8);
|
|
1016
|
+
const shareEnabled = params.share !== false;
|
|
1017
|
+
const sessionEnabled = shareEnabled || Boolean(params.sessionDir);
|
|
1018
|
+
const sessionRoot = sessionEnabled
|
|
1019
|
+
? params.sessionDir
|
|
1020
|
+
? path.resolve(params.sessionDir)
|
|
1021
|
+
: fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"))
|
|
1022
|
+
: undefined;
|
|
1023
|
+
if (sessionRoot) {
|
|
1024
|
+
try {
|
|
1025
|
+
fs.mkdirSync(sessionRoot, { recursive: true });
|
|
1026
|
+
} catch {}
|
|
1027
|
+
}
|
|
1028
|
+
const sessionDirForIndex = (idx?: number) =>
|
|
1029
|
+
sessionRoot ? path.join(sessionRoot, `run-${idx ?? 0}`) : undefined;
|
|
1030
|
+
|
|
1031
|
+
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
1032
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
1033
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
1034
|
+
|
|
1035
|
+
const requestedAsync = params.async ?? asyncByDefault;
|
|
1036
|
+
const parallelDowngraded = hasTasks && requestedAsync;
|
|
1037
|
+
// clarify implies sync mode (TUI is blocking)
|
|
1038
|
+
// If user requested async without explicit clarify: false, downgrade to sync for chains
|
|
1039
|
+
const effectiveAsync = requestedAsync && !hasTasks && (hasChain ? params.clarify === false : true);
|
|
1040
|
+
|
|
1041
|
+
const artifactConfig: ArtifactConfig = {
|
|
1042
|
+
...DEFAULT_ARTIFACT_CONFIG,
|
|
1043
|
+
enabled: params.artifacts !== false,
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
const sessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1047
|
+
const artifactsDir = effectiveAsync ? tempArtifactsDir : getArtifactsDir(sessionFile);
|
|
1048
|
+
|
|
1049
|
+
if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
|
|
1050
|
+
return {
|
|
1051
|
+
content: [
|
|
1052
|
+
{
|
|
1053
|
+
type: "text",
|
|
1054
|
+
text: `Provide exactly one mode. Agents: ${agents.map((a) => a.name).join(", ") || "none"}`,
|
|
1055
|
+
},
|
|
1056
|
+
],
|
|
1057
|
+
isError: true,
|
|
1058
|
+
details: { mode: "single" as const, results: [] },
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Validate chain early (before async/sync branching)
|
|
1063
|
+
if (hasChain && params.chain) {
|
|
1064
|
+
if (params.chain.length === 0) {
|
|
1065
|
+
return {
|
|
1066
|
+
content: [{ type: "text", text: "Chain must have at least one step" }],
|
|
1067
|
+
isError: true,
|
|
1068
|
+
details: { mode: "chain" as const, results: [] },
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
// First step must have a task
|
|
1072
|
+
const firstStep = params.chain[0] as ChainStep;
|
|
1073
|
+
if (isParallelStep(firstStep)) {
|
|
1074
|
+
if (!firstStep.parallel[0]?.task) {
|
|
1075
|
+
return {
|
|
1076
|
+
content: [{ type: "text", text: "First parallel task must have a task" }],
|
|
1077
|
+
isError: true,
|
|
1078
|
+
details: { mode: "chain" as const, results: [] },
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
} else if (!(firstStep as SequentialStep).task) {
|
|
1082
|
+
return {
|
|
1083
|
+
content: [{ type: "text", text: "First step in chain must have a task" }],
|
|
1084
|
+
isError: true,
|
|
1085
|
+
details: { mode: "chain" as const, results: [] },
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
// Validate all agents exist
|
|
1089
|
+
for (let i = 0; i < params.chain.length; i++) {
|
|
1090
|
+
const step = params.chain[i] as ChainStep;
|
|
1091
|
+
const stepAgents = getStepAgents(step);
|
|
1092
|
+
for (const agentName of stepAgents) {
|
|
1093
|
+
if (!agents.find((a) => a.name === agentName)) {
|
|
1094
|
+
return {
|
|
1095
|
+
content: [{ type: "text", text: `Unknown agent: ${agentName} (step ${i + 1})` }],
|
|
1096
|
+
isError: true,
|
|
1097
|
+
details: { mode: "chain" as const, results: [] },
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// Validate parallel steps have at least one task
|
|
1102
|
+
if (isParallelStep(step) && step.parallel.length === 0) {
|
|
1103
|
+
return {
|
|
1104
|
+
content: [{ type: "text", text: `Parallel step ${i + 1} must have at least one task` }],
|
|
1105
|
+
isError: true,
|
|
1106
|
+
details: { mode: "chain" as const, results: [] },
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (effectiveAsync) {
|
|
1113
|
+
if (!jitiCliPath)
|
|
1114
|
+
return {
|
|
1115
|
+
content: [{ type: "text", text: "jiti not found" }],
|
|
1116
|
+
isError: true,
|
|
1117
|
+
details: { mode: "single" as const, results: [] },
|
|
1118
|
+
};
|
|
1119
|
+
const id = randomUUID();
|
|
1120
|
+
const asyncDir = path.join(ASYNC_DIR, id);
|
|
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
|
+
};
|
|
1137
|
+
|
|
1138
|
+
if (hasChain && params.chain) {
|
|
1139
|
+
// Async mode doesn't support parallel steps (v1 limitation)
|
|
1140
|
+
const chainStepsForAsync = params.chain as ChainStep[];
|
|
1141
|
+
const hasParallelInChain = chainStepsForAsync.some(isParallelStep);
|
|
1142
|
+
if (hasParallelInChain) {
|
|
1143
|
+
return {
|
|
1144
|
+
content: [{ type: "text", text: "Async mode doesn't support chains with parallel steps. Use clarify: true (sync mode) for parallel-in-chain." }],
|
|
1145
|
+
isError: true,
|
|
1146
|
+
details: { mode: "chain" as const, results: [] },
|
|
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
|
+
};
|
|
1174
|
+
});
|
|
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
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (hasSingle) {
|
|
1221
|
+
const a = agents.find((x) => x.name === params.agent);
|
|
1222
|
+
if (!a)
|
|
1223
|
+
return {
|
|
1224
|
+
content: [{ type: "text", text: `Unknown: ${params.agent}` }],
|
|
1225
|
+
isError: true,
|
|
1226
|
+
details: { mode: "single" as const, results: [] },
|
|
1227
|
+
};
|
|
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
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
content: [{ type: "text", text: `Async: ${params.agent} [${id}]` }],
|
|
1274
|
+
details: { mode: "single", results: [], asyncId: id, asyncDir },
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const allProgress: AgentProgress[] = [];
|
|
1280
|
+
const allArtifactPaths: ArtifactPaths[] = [];
|
|
1281
|
+
|
|
1282
|
+
if (hasChain && params.chain) {
|
|
1283
|
+
// Cast chain to typed steps
|
|
1284
|
+
const chainSteps = params.chain as ChainStep[];
|
|
1285
|
+
|
|
1286
|
+
// Compute chain metadata for observability
|
|
1287
|
+
const chainAgents: string[] = chainSteps.map((step) =>
|
|
1288
|
+
isParallelStep(step)
|
|
1289
|
+
? `[${step.parallel.map((t) => t.agent).join("+")}]`
|
|
1290
|
+
: (step as SequentialStep).agent,
|
|
1291
|
+
);
|
|
1292
|
+
const totalSteps = chainSteps.length;
|
|
1293
|
+
|
|
1294
|
+
// Get original task from first step
|
|
1295
|
+
const firstStep = chainSteps[0]!;
|
|
1296
|
+
const originalTask = isParallelStep(firstStep)
|
|
1297
|
+
? firstStep.parallel[0]!.task!
|
|
1298
|
+
: (firstStep as SequentialStep).task!;
|
|
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
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (hasTasks && params.tasks) {
|
|
1643
|
+
if (params.tasks.length > MAX_PARALLEL)
|
|
1644
|
+
return {
|
|
1645
|
+
content: [{ type: "text", text: `Max ${MAX_PARALLEL} tasks` }],
|
|
1646
|
+
isError: true,
|
|
1647
|
+
details: { mode: "single" as const, results: [] },
|
|
1648
|
+
};
|
|
1649
|
+
const results = await mapConcurrent(params.tasks, MAX_CONCURRENCY, async (t, i) =>
|
|
1650
|
+
runSync(ctx.cwd, agents, t.agent, t.task, {
|
|
1651
|
+
cwd: t.cwd ?? params.cwd,
|
|
1652
|
+
signal,
|
|
1653
|
+
runId,
|
|
1654
|
+
index: i,
|
|
1655
|
+
sessionDir: sessionDirForIndex(i),
|
|
1656
|
+
share: shareEnabled,
|
|
1657
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1658
|
+
artifactConfig,
|
|
1659
|
+
maxOutput: params.maxOutput,
|
|
1660
|
+
}),
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
for (const r of results) {
|
|
1664
|
+
if (r.progress) allProgress.push(r.progress);
|
|
1665
|
+
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const ok = results.filter((r) => r.exitCode === 0).length;
|
|
1669
|
+
const downgradeNote = parallelDowngraded ? " (async not supported for parallel)" : "";
|
|
1670
|
+
return {
|
|
1671
|
+
content: [{ type: "text", text: `${ok}/${results.length} succeeded${downgradeNote}` }],
|
|
1672
|
+
details: {
|
|
1673
|
+
mode: "parallel",
|
|
1674
|
+
results,
|
|
1675
|
+
progress: params.includeProgress ? allProgress : undefined,
|
|
1676
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1677
|
+
},
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (hasSingle) {
|
|
1682
|
+
// Look up agent config for output handling
|
|
1683
|
+
const agentConfig = agents.find((a) => a.name === params.agent);
|
|
1684
|
+
// Note: runSync already handles unknown agent, but we need config for output
|
|
1685
|
+
|
|
1686
|
+
let task = params.task!;
|
|
1687
|
+
let outputPath: string | undefined;
|
|
1688
|
+
|
|
1689
|
+
// Check if agent has output and it's not disabled
|
|
1690
|
+
if (agentConfig) {
|
|
1691
|
+
const effectiveOutput =
|
|
1692
|
+
params.output !== undefined ? params.output : agentConfig.output;
|
|
1693
|
+
|
|
1694
|
+
if (effectiveOutput && effectiveOutput !== false) {
|
|
1695
|
+
const outputDir = `/tmp/pi-${agentConfig.name}-${runId}`;
|
|
1696
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1697
|
+
outputPath = `${outputDir}/${effectiveOutput}`;
|
|
1698
|
+
|
|
1699
|
+
// Inject output instruction into task
|
|
1700
|
+
task += `\n\n---\n**Output:** Write your findings to: ${outputPath}`;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const r = await runSync(ctx.cwd, agents, params.agent!, task, {
|
|
1705
|
+
cwd: params.cwd,
|
|
1706
|
+
signal,
|
|
1707
|
+
runId,
|
|
1708
|
+
sessionDir: sessionDirForIndex(0),
|
|
1709
|
+
share: shareEnabled,
|
|
1710
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1711
|
+
artifactConfig,
|
|
1712
|
+
maxOutput: params.maxOutput,
|
|
1713
|
+
onUpdate,
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
if (r.progress) allProgress.push(r.progress);
|
|
1717
|
+
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
1718
|
+
|
|
1719
|
+
// Get output and append file path if applicable
|
|
1720
|
+
let output = r.truncation?.text || getFinalOutput(r.messages);
|
|
1721
|
+
if (outputPath && r.exitCode === 0) {
|
|
1722
|
+
output += `\n\n📄 Output saved to: ${outputPath}`;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (r.exitCode !== 0)
|
|
1726
|
+
return {
|
|
1727
|
+
content: [{ type: "text", text: r.error || "Failed" }],
|
|
1728
|
+
details: {
|
|
1729
|
+
mode: "single",
|
|
1730
|
+
results: [r],
|
|
1731
|
+
progress: params.includeProgress ? allProgress : undefined,
|
|
1732
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1733
|
+
truncation: r.truncation,
|
|
1734
|
+
},
|
|
1735
|
+
isError: true,
|
|
1736
|
+
};
|
|
1737
|
+
return {
|
|
1738
|
+
content: [{ type: "text", text: output || "(no output)" }],
|
|
1739
|
+
details: {
|
|
1740
|
+
mode: "single",
|
|
1741
|
+
results: [r],
|
|
1742
|
+
progress: params.includeProgress ? allProgress : undefined,
|
|
1743
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1744
|
+
truncation: r.truncation,
|
|
1745
|
+
},
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
return {
|
|
1750
|
+
content: [{ type: "text", text: "Invalid params" }],
|
|
1751
|
+
isError: true,
|
|
1752
|
+
details: { mode: "single" as const, results: [] },
|
|
1753
|
+
};
|
|
1754
|
+
},
|
|
1755
|
+
|
|
1756
|
+
renderCall(args, theme) {
|
|
1757
|
+
const isParallel = (args.tasks?.length ?? 0) > 0;
|
|
1758
|
+
const asyncLabel = args.async === true && !isParallel ? theme.fg("warning", " [async]") : "";
|
|
1759
|
+
if (args.chain?.length)
|
|
1760
|
+
return new Text(
|
|
1761
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}`,
|
|
1762
|
+
0,
|
|
1763
|
+
0,
|
|
1764
|
+
);
|
|
1765
|
+
if (isParallel)
|
|
1766
|
+
return new Text(
|
|
1767
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}parallel (${args.tasks!.length})`,
|
|
1768
|
+
0,
|
|
1769
|
+
0,
|
|
1770
|
+
);
|
|
1771
|
+
return new Text(
|
|
1772
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
|
|
1773
|
+
0,
|
|
1774
|
+
0,
|
|
1775
|
+
);
|
|
1776
|
+
},
|
|
1777
|
+
|
|
1778
|
+
renderResult(result, { expanded }, theme) {
|
|
1779
|
+
const d = result.details;
|
|
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);
|
|
2002
|
+
},
|
|
2003
|
+
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
const statusTool: ToolDefinition<typeof StatusParams, Details> = {
|
|
2007
|
+
name: "subagent_status",
|
|
2008
|
+
label: "Subagent Status",
|
|
2009
|
+
description: "Inspect async subagent run status and artifacts",
|
|
2010
|
+
parameters: StatusParams,
|
|
2011
|
+
|
|
2012
|
+
async execute(_id, params) {
|
|
2013
|
+
let asyncDir: string | null = null;
|
|
2014
|
+
let resolvedId = params.id;
|
|
2015
|
+
|
|
2016
|
+
if (params.dir) {
|
|
2017
|
+
asyncDir = path.resolve(params.dir);
|
|
2018
|
+
} else if (params.id) {
|
|
2019
|
+
const direct = path.join(ASYNC_DIR, params.id);
|
|
2020
|
+
if (fs.existsSync(direct)) {
|
|
2021
|
+
asyncDir = direct;
|
|
2022
|
+
} else {
|
|
2023
|
+
const match = findByPrefix(ASYNC_DIR, params.id);
|
|
2024
|
+
if (match) {
|
|
2025
|
+
asyncDir = match;
|
|
2026
|
+
resolvedId = path.basename(match);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const resultPath =
|
|
2032
|
+
params.id && !asyncDir ? findByPrefix(RESULTS_DIR, params.id, ".json") : null;
|
|
2033
|
+
|
|
2034
|
+
if (!asyncDir && !resultPath) {
|
|
2035
|
+
return {
|
|
2036
|
+
content: [{ type: "text", text: "Async run not found. Provide id or dir." }],
|
|
2037
|
+
isError: true,
|
|
2038
|
+
details: { mode: "single" as const, results: [] },
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
if (asyncDir) {
|
|
2043
|
+
const status = readStatus(asyncDir);
|
|
2044
|
+
const logPath = path.join(asyncDir, `subagent-log-${resolvedId ?? "unknown"}.md`);
|
|
2045
|
+
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
2046
|
+
if (status) {
|
|
2047
|
+
const stepsTotal = status.steps?.length ?? 1;
|
|
2048
|
+
const current = status.currentStep !== undefined ? status.currentStep + 1 : undefined;
|
|
2049
|
+
const stepLine =
|
|
2050
|
+
current !== undefined ? `Step: ${current}/${stepsTotal}` : `Steps: ${stepsTotal}`;
|
|
2051
|
+
const started = new Date(status.startedAt).toISOString();
|
|
2052
|
+
const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
|
|
2053
|
+
|
|
2054
|
+
const lines = [
|
|
2055
|
+
`Run: ${status.runId}`,
|
|
2056
|
+
`State: ${status.state}`,
|
|
2057
|
+
`Mode: ${status.mode}`,
|
|
2058
|
+
stepLine,
|
|
2059
|
+
`Started: ${started}`,
|
|
2060
|
+
`Updated: ${updated}`,
|
|
2061
|
+
`Dir: ${asyncDir}`,
|
|
2062
|
+
];
|
|
2063
|
+
if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
|
|
2064
|
+
// Sharing disabled - session file path shown above
|
|
2065
|
+
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
2066
|
+
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
2067
|
+
|
|
2068
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
if (resultPath) {
|
|
2073
|
+
try {
|
|
2074
|
+
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
2075
|
+
const data = JSON.parse(raw) as { id?: string; success?: boolean; summary?: string };
|
|
2076
|
+
const status = data.success ? "complete" : "failed";
|
|
2077
|
+
const lines = [`Run: ${data.id ?? params.id}`, `State: ${status}`, `Result: ${resultPath}`];
|
|
2078
|
+
if (data.summary) lines.push("", data.summary);
|
|
2079
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
|
|
2080
|
+
} catch {}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
return {
|
|
2084
|
+
content: [{ type: "text", text: "Status file not found." }],
|
|
2085
|
+
isError: true,
|
|
2086
|
+
details: { mode: "single" as const, results: [] },
|
|
2087
|
+
};
|
|
2088
|
+
},
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
pi.registerTool(tool);
|
|
2092
|
+
pi.registerTool(statusTool);
|
|
2093
|
+
|
|
2094
|
+
pi.events.on("subagent:started", (data) => {
|
|
2095
|
+
const info = data as {
|
|
2096
|
+
id?: string;
|
|
2097
|
+
asyncDir?: string;
|
|
2098
|
+
agent?: string;
|
|
2099
|
+
chain?: string[];
|
|
2100
|
+
};
|
|
2101
|
+
if (!info.id) return;
|
|
2102
|
+
const asyncDir = info.asyncDir ?? path.join(ASYNC_DIR, info.id);
|
|
2103
|
+
const agents = info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
|
|
2104
|
+
const now = Date.now();
|
|
2105
|
+
asyncJobs.set(info.id, {
|
|
2106
|
+
asyncId: info.id,
|
|
2107
|
+
asyncDir,
|
|
2108
|
+
status: "queued",
|
|
2109
|
+
mode: info.chain ? "chain" : "single",
|
|
2110
|
+
agents,
|
|
2111
|
+
stepsTotal: agents?.length,
|
|
2112
|
+
startedAt: now,
|
|
2113
|
+
updatedAt: now,
|
|
2114
|
+
});
|
|
2115
|
+
if (lastUiContext) {
|
|
2116
|
+
renderWidget(lastUiContext, Array.from(asyncJobs.values()));
|
|
2117
|
+
ensurePoller();
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
pi.events.on("subagent:complete", (data) => {
|
|
2122
|
+
const result = data as { id?: string; success?: boolean; asyncDir?: string };
|
|
2123
|
+
const asyncId = result.id;
|
|
2124
|
+
if (!asyncId) return;
|
|
2125
|
+
const job = asyncJobs.get(asyncId);
|
|
2126
|
+
if (job) {
|
|
2127
|
+
job.status = result.success ? "complete" : "failed";
|
|
2128
|
+
job.updatedAt = Date.now();
|
|
2129
|
+
if (result.asyncDir) job.asyncDir = result.asyncDir;
|
|
2130
|
+
}
|
|
2131
|
+
if (lastUiContext) {
|
|
2132
|
+
renderWidget(lastUiContext, Array.from(asyncJobs.values()));
|
|
2133
|
+
}
|
|
2134
|
+
setTimeout(() => {
|
|
2135
|
+
asyncJobs.delete(asyncId);
|
|
2136
|
+
if (lastUiContext) renderWidget(lastUiContext, Array.from(asyncJobs.values()));
|
|
2137
|
+
}, 10000);
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
pi.on("tool_result", (event, ctx) => {
|
|
2141
|
+
if (event.toolName !== "subagent") return;
|
|
2142
|
+
if (!ctx.hasUI) return;
|
|
2143
|
+
lastUiContext = ctx;
|
|
2144
|
+
if (asyncJobs.size > 0) {
|
|
2145
|
+
renderWidget(ctx, Array.from(asyncJobs.values()));
|
|
2146
|
+
ensurePoller();
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2150
|
+
pi.on("session_start", (_event, ctx) => {
|
|
2151
|
+
baseCwd = ctx.cwd;
|
|
2152
|
+
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2153
|
+
asyncJobs.clear();
|
|
2154
|
+
if (ctx.hasUI) {
|
|
2155
|
+
lastUiContext = ctx;
|
|
2156
|
+
renderWidget(ctx, []);
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
pi.on("session_switch", (_event, ctx) => {
|
|
2160
|
+
baseCwd = ctx.cwd;
|
|
2161
|
+
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2162
|
+
asyncJobs.clear();
|
|
2163
|
+
if (ctx.hasUI) {
|
|
2164
|
+
lastUiContext = ctx;
|
|
2165
|
+
renderWidget(ctx, []);
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
pi.on("session_branch", (_event, ctx) => {
|
|
2169
|
+
baseCwd = ctx.cwd;
|
|
2170
|
+
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2171
|
+
asyncJobs.clear();
|
|
2172
|
+
if (ctx.hasUI) {
|
|
2173
|
+
lastUiContext = ctx;
|
|
2174
|
+
renderWidget(ctx, []);
|
|
2175
|
+
}
|
|
2176
|
+
});
|
|
2177
|
+
pi.on("session_shutdown", () => {
|
|
2178
|
+
watcher.close();
|
|
2179
|
+
if (poller) clearInterval(poller);
|
|
2180
|
+
poller = null;
|
|
2181
|
+
asyncJobs.clear();
|
|
2182
|
+
if (lastUiContext?.hasUI) {
|
|
2183
|
+
lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
}
|