@ryan_nookpi/pi-extension-subagent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/above-widget.ts +20 -0
- package/agents.ts +233 -0
- package/claude-args.ts +103 -0
- package/claude-sdk-runner.ts +223 -0
- package/claude-sdk-stream-adapter.ts +49 -0
- package/claude-sidecar-writer.ts +107 -0
- package/claude-stream-parser.ts +400 -0
- package/cli.ts +549 -0
- package/commands.ts +2295 -0
- package/config.ts +119 -0
- package/constants.ts +115 -0
- package/context-limits.ts +132 -0
- package/display-task.ts +98 -0
- package/escalation.ts +159 -0
- package/format.ts +188 -0
- package/group-pending.ts +90 -0
- package/index.ts +200 -0
- package/invocation-queue.ts +29 -0
- package/live-preview.ts +39 -0
- package/package.json +95 -0
- package/persisted-session.ts +185 -0
- package/replay.ts +461 -0
- package/retry.ts +164 -0
- package/run-utils.ts +203 -0
- package/runner.ts +979 -0
- package/session.ts +319 -0
- package/store.ts +143 -0
- package/tool-execute.ts +1932 -0
- package/tool-render.ts +145 -0
- package/types.ts +193 -0
- package/utils/agent-utils.ts +315 -0
- package/utils/format-utils.ts +255 -0
- package/utils/short-label.ts +78 -0
- package/utils/string-utils.ts +229 -0
- package/utils/time-utils.ts +105 -0
- package/widget.ts +294 -0
package/tool-execute.ts
ADDED
|
@@ -0,0 +1,1932 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: subagent tool execution bridges untyped session state, UI hooks, and tool payloads. */
|
|
2
|
+
/**
|
|
3
|
+
* Subagent tool execute handler — extracted from commands.ts for modularity.
|
|
4
|
+
*
|
|
5
|
+
* createSubagentToolExecute(pi, store) returns the async execute function
|
|
6
|
+
* that is passed to pi.registerTool({ execute: ... }).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { discoverAgents } from "./agents.js";
|
|
12
|
+
import { parseSubagentToolCommand, SUBAGENT_CLI_HELP_TEXT } from "./cli.js";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_TURN_COUNT,
|
|
15
|
+
IDLE_RUN_WARNING_THRESHOLD,
|
|
16
|
+
MAX_BATCH_RUNS,
|
|
17
|
+
MAX_CHAIN_STEPS,
|
|
18
|
+
MAX_CONCURRENT_ASYNC_SUBAGENT_RUNS,
|
|
19
|
+
MAX_LISTED_RUNS,
|
|
20
|
+
STATUS_OUTPUT_PREVIEW_MAX_CHARS,
|
|
21
|
+
SUBAGENT_POLL_COOLDOWN_MS,
|
|
22
|
+
SUBAGENT_STRONG_WAIT_MESSAGE,
|
|
23
|
+
} from "./constants.js";
|
|
24
|
+
import { isContextOverflowText } from "./context-limits.js";
|
|
25
|
+
import {
|
|
26
|
+
buildSubagentDisplayTaskFallback,
|
|
27
|
+
createDisplayTaskRefreshToken,
|
|
28
|
+
isDisplayTaskRefreshTokenCurrent,
|
|
29
|
+
shouldSummarizeSubagentTask,
|
|
30
|
+
summarizeSubagentDisplayTask,
|
|
31
|
+
} from "./display-task.js";
|
|
32
|
+
import { ESCALATION_EXIT_CODE, readAndConsumeEscalation } from "./escalation.js";
|
|
33
|
+
import {
|
|
34
|
+
formatContextUsageBar,
|
|
35
|
+
formatUsageStats,
|
|
36
|
+
getUsedContextPercent,
|
|
37
|
+
resolveContextWindow,
|
|
38
|
+
truncateLines,
|
|
39
|
+
} from "./format.js";
|
|
40
|
+
import { clearPendingGroupCompletion, upsertPendingGroupCompletion } from "./group-pending.js";
|
|
41
|
+
import { enqueueSubagentInvocation } from "./invocation-queue.js";
|
|
42
|
+
import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session.js";
|
|
43
|
+
import { clearFinishedRuns, formatCommandRunSummary, removeRun, trimCommandRunHistory } from "./run-utils.js";
|
|
44
|
+
import { getFinalOutput, getLastNonEmptyLine, runSingleAgent } from "./runner.js";
|
|
45
|
+
import {
|
|
46
|
+
buildMainContextText,
|
|
47
|
+
buildPipelineReferenceSection,
|
|
48
|
+
makeSubagentSessionFile,
|
|
49
|
+
stripTaskEchoFromMainContext,
|
|
50
|
+
wrapTaskWithMainContext,
|
|
51
|
+
wrapTaskWithPipelineContext,
|
|
52
|
+
} from "./session.js";
|
|
53
|
+
import { type SubagentStore, updateRunFromResult } from "./store.js";
|
|
54
|
+
import type {
|
|
55
|
+
BatchOrChainItem,
|
|
56
|
+
CommandRunState,
|
|
57
|
+
OnUpdateCallback,
|
|
58
|
+
PendingCompletion,
|
|
59
|
+
PipelineStepResult,
|
|
60
|
+
SingleResult,
|
|
61
|
+
SubagentDetails,
|
|
62
|
+
SubagentLaunchSummary,
|
|
63
|
+
} from "./types.js";
|
|
64
|
+
import type { ShortLabelContext } from "./utils/short-label.js";
|
|
65
|
+
import { updateCommandRunsWidget, type WidgetRenderCtx } from "./widget.js";
|
|
66
|
+
|
|
67
|
+
type SessionToolCall = {
|
|
68
|
+
name: string;
|
|
69
|
+
argsText: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type SessionTurnToolCalls = {
|
|
73
|
+
turn: number;
|
|
74
|
+
toolCalls: SessionToolCall[];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type SessionDetailSummary = {
|
|
78
|
+
finalOutput: string;
|
|
79
|
+
turns: SessionTurnToolCalls[];
|
|
80
|
+
error?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type ResultFailureDiagnosis = {
|
|
84
|
+
failed: boolean;
|
|
85
|
+
reason?: string;
|
|
86
|
+
/** Set when the failure was caused by exceeding the model context window. */
|
|
87
|
+
contextOverflow?: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type AssistantTextPart = { type: "text"; text: string };
|
|
91
|
+
type AssistantToolCallPart = { type: "toolCall"; name?: string; arguments?: unknown };
|
|
92
|
+
type AssistantContentPart = AssistantTextPart | AssistantToolCallPart;
|
|
93
|
+
type AssistantMessageEntry = { type?: string; message?: { role?: string; content?: unknown } };
|
|
94
|
+
|
|
95
|
+
type LaunchMode = "single" | "batch" | "chain";
|
|
96
|
+
|
|
97
|
+
type RunLaunchConfig = {
|
|
98
|
+
agent: string;
|
|
99
|
+
taskForDisplay: string;
|
|
100
|
+
taskForAgent: string;
|
|
101
|
+
inheritMainContext: boolean;
|
|
102
|
+
originSessionFile: string;
|
|
103
|
+
continuedFromRunId?: number;
|
|
104
|
+
batchId?: string;
|
|
105
|
+
pipelineId?: string;
|
|
106
|
+
pipelineStepIndex?: number;
|
|
107
|
+
existingRunState?: CommandRunState;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
type FinalizedRun = {
|
|
111
|
+
runState: CommandRunState;
|
|
112
|
+
result?: SingleResult;
|
|
113
|
+
isError: boolean;
|
|
114
|
+
rawOutput: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type SubagentExecuteResult = {
|
|
118
|
+
content: { type: "text"; text: string }[];
|
|
119
|
+
details: SubagentDetails;
|
|
120
|
+
isError?: boolean;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type SubagentToolExecuteContext = {
|
|
124
|
+
cwd: string;
|
|
125
|
+
hasUI?: boolean;
|
|
126
|
+
model?: ShortLabelContext["model"];
|
|
127
|
+
modelRegistry?: {
|
|
128
|
+
getAll: () => Array<{ provider: string; id: string; contextWindow?: number }>;
|
|
129
|
+
getApiKeyAndHeaders?: (model: NonNullable<ShortLabelContext["model"]>) => Promise<{
|
|
130
|
+
ok: boolean;
|
|
131
|
+
apiKey?: string;
|
|
132
|
+
headers?: Record<string, string>;
|
|
133
|
+
}>;
|
|
134
|
+
};
|
|
135
|
+
sessionManager: {
|
|
136
|
+
getSessionFile?: () => string | undefined;
|
|
137
|
+
getEntries: () => unknown[];
|
|
138
|
+
};
|
|
139
|
+
registerDispose?: (cb: () => void) => void;
|
|
140
|
+
ui?: {
|
|
141
|
+
setWidget: (...args: any[]) => void;
|
|
142
|
+
notify?: (message: string, type?: "info" | "warning" | "error") => void;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function refreshDisplayTaskInBackground(
|
|
147
|
+
runState: CommandRunState,
|
|
148
|
+
rawTask: string,
|
|
149
|
+
ctx: SubagentToolExecuteContext,
|
|
150
|
+
store: SubagentStore,
|
|
151
|
+
): void {
|
|
152
|
+
if (!shouldSummarizeSubagentTask(rawTask, runState.displayTask ?? "")) return;
|
|
153
|
+
if (!ctx.model || !ctx.modelRegistry?.getApiKeyAndHeaders) return;
|
|
154
|
+
|
|
155
|
+
const refreshToken = createDisplayTaskRefreshToken(runState);
|
|
156
|
+
void summarizeSubagentDisplayTask(rawTask, {
|
|
157
|
+
model: ctx.model,
|
|
158
|
+
modelRegistry: {
|
|
159
|
+
getApiKeyAndHeaders: ctx.modelRegistry.getApiKeyAndHeaders,
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
.then((displayTask) => {
|
|
163
|
+
if (!displayTask || runState.removed || store.disposed) return;
|
|
164
|
+
if (!isDisplayTaskRefreshTokenCurrent(runState, refreshToken)) return;
|
|
165
|
+
if (displayTask === runState.displayTask) return;
|
|
166
|
+
runState.displayTask = displayTask;
|
|
167
|
+
const originSessionFile = store.globalLiveRuns.get(runState.id)?.originSessionFile;
|
|
168
|
+
appendDisplayTaskUpdate(originSessionFile, {
|
|
169
|
+
runId: runState.id,
|
|
170
|
+
task: runState.task,
|
|
171
|
+
displayTask,
|
|
172
|
+
startedAt: runState.startedAt,
|
|
173
|
+
});
|
|
174
|
+
updateCommandRunsWidget(store);
|
|
175
|
+
})
|
|
176
|
+
.catch(() => {
|
|
177
|
+
// Ignore background summary failures.
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function stringifyToolCallArguments(args: unknown): string {
|
|
182
|
+
if (args === undefined || args === null) return "";
|
|
183
|
+
if (typeof args === "string") return args;
|
|
184
|
+
try {
|
|
185
|
+
return JSON.stringify(args);
|
|
186
|
+
} catch {
|
|
187
|
+
return String(args);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getAssistantTextPart(content: unknown): string {
|
|
192
|
+
if (typeof content === "string") return content;
|
|
193
|
+
if (!Array.isArray(content)) return "";
|
|
194
|
+
for (const part of content as AssistantContentPart[]) {
|
|
195
|
+
if (part?.type === "text" && typeof part.text === "string") {
|
|
196
|
+
return part.text;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return "";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseSessionDetailSummary(sessionFile?: string): SessionDetailSummary {
|
|
203
|
+
if (!sessionFile) {
|
|
204
|
+
return { finalOutput: "", turns: [], error: "Session file is not available for this run." };
|
|
205
|
+
}
|
|
206
|
+
if (!fs.existsSync(sessionFile)) {
|
|
207
|
+
return {
|
|
208
|
+
finalOutput: "",
|
|
209
|
+
turns: [],
|
|
210
|
+
error:
|
|
211
|
+
`Session file not found: ${sessionFile}. ` +
|
|
212
|
+
"The subagent likely exited before producing any persisted message (turn=0 / no output).",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let raw = "";
|
|
217
|
+
try {
|
|
218
|
+
raw = fs.readFileSync(sessionFile, "utf-8");
|
|
219
|
+
} catch (error: unknown) {
|
|
220
|
+
const message = error instanceof Error ? error.message : "Unknown read error";
|
|
221
|
+
return { finalOutput: "", turns: [], error: `Failed to read session file: ${message}` };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const assistantMessages: Array<{ content?: unknown }> = [];
|
|
225
|
+
const turns: SessionTurnToolCalls[] = [];
|
|
226
|
+
|
|
227
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
228
|
+
if (!line.trim()) continue;
|
|
229
|
+
|
|
230
|
+
let entry: AssistantMessageEntry;
|
|
231
|
+
try {
|
|
232
|
+
entry = JSON.parse(line);
|
|
233
|
+
} catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (entry?.type !== "message" || !entry.message || entry.message.role !== "assistant") continue;
|
|
238
|
+
|
|
239
|
+
assistantMessages.push(entry.message);
|
|
240
|
+
const turn = assistantMessages.length;
|
|
241
|
+
const toolCalls: SessionToolCall[] = [];
|
|
242
|
+
const content = entry.message.content;
|
|
243
|
+
|
|
244
|
+
if (Array.isArray(content)) {
|
|
245
|
+
for (const part of content as AssistantContentPart[]) {
|
|
246
|
+
if (part?.type !== "toolCall") continue;
|
|
247
|
+
const name = typeof part.name === "string" ? part.name : "tool";
|
|
248
|
+
const argsText = stringifyToolCallArguments(part.arguments);
|
|
249
|
+
toolCalls.push({ name, argsText });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (toolCalls.length > 0) {
|
|
254
|
+
turns.push({ turn, toolCalls });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let finalOutput = "";
|
|
259
|
+
for (let i = assistantMessages.length - 1; i >= 0; i--) {
|
|
260
|
+
const text = getAssistantTextPart(assistantMessages[i]?.content);
|
|
261
|
+
if (text) {
|
|
262
|
+
finalOutput = text;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { finalOutput, turns };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function diagnoseResultFailure(result: SingleResult): ResultFailureDiagnosis {
|
|
271
|
+
if (result.exitCode !== 0 || result.stopReason === "error") {
|
|
272
|
+
const overflowText = result.errorMessage || result.stderr || getFinalOutput(result.messages);
|
|
273
|
+
if (isContextOverflowText(overflowText)) {
|
|
274
|
+
const turns = result.usage?.turns ?? 0;
|
|
275
|
+
return {
|
|
276
|
+
failed: true,
|
|
277
|
+
contextOverflow: true,
|
|
278
|
+
reason:
|
|
279
|
+
`Subagent stopped after exceeding the model context window (${turns} turn(s) completed). ` +
|
|
280
|
+
"Partial findings recovered below. To finish, narrow the task scope, split it, or use a larger-context model.",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (result.exitCode !== 0) return { failed: true, reason: `Subagent process exited with code ${result.exitCode}.` };
|
|
285
|
+
if (result.stopReason === "error")
|
|
286
|
+
return { failed: true, reason: result.errorMessage || "Subagent reported stopReason=error." };
|
|
287
|
+
if (result.stopReason === "aborted") return { failed: true, reason: "Subagent execution was aborted." };
|
|
288
|
+
|
|
289
|
+
const finalOutput = getFinalOutput(result.messages).trim();
|
|
290
|
+
const hasAssistantText = finalOutput.length > 0;
|
|
291
|
+
if (hasAssistantText) return { failed: false };
|
|
292
|
+
|
|
293
|
+
const stderr = (result.stderr || "").trim();
|
|
294
|
+
if (result.messages.length === 0) {
|
|
295
|
+
return {
|
|
296
|
+
failed: true,
|
|
297
|
+
reason:
|
|
298
|
+
"Subagent returned no messages (turn=0). " +
|
|
299
|
+
(stderr ? `stderr: ${stderr}` : "No stderr captured. Child process may have exited before producing output."),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
failed: true,
|
|
305
|
+
reason: `Subagent finished without assistant text output. ${stderr ? `stderr: ${stderr}` : "No stderr captured."}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function formatRunDetailOutput(run: CommandRunState): string {
|
|
310
|
+
const sessionSummary = parseSessionDetailSummary(run.sessionFile);
|
|
311
|
+
const runOutput = run.lastOutput?.trim() ? run.lastOutput : "";
|
|
312
|
+
const sessionOutput = sessionSummary.finalOutput?.trim() ? sessionSummary.finalOutput : "";
|
|
313
|
+
const lineOutput = run.lastLine?.trim() ? run.lastLine : "";
|
|
314
|
+
const output = runOutput || sessionOutput || lineOutput || "(no output)";
|
|
315
|
+
const lines: string[] = [formatCommandRunSummary(run), `Prompt: ${run.task}`];
|
|
316
|
+
|
|
317
|
+
if (run.runtime) lines.push(`Runtime: ${run.runtime}`);
|
|
318
|
+
if (run.runtime === "claude" && run.claudeSessionId) lines.push(`Claude Session: ${run.claudeSessionId}`);
|
|
319
|
+
if (run.sessionFile) lines.push(`Session: ${run.sessionFile}`);
|
|
320
|
+
if (run.thoughtText) lines.push(`Thought: ${run.thoughtText}`);
|
|
321
|
+
|
|
322
|
+
lines.push("", "Result:", output, "", "Tool calls by turn:");
|
|
323
|
+
|
|
324
|
+
if (sessionSummary.error) {
|
|
325
|
+
lines.push(`- (session parse error) ${sessionSummary.error}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (sessionSummary.turns.length === 0) {
|
|
329
|
+
lines.push("- (no tool calls)");
|
|
330
|
+
} else {
|
|
331
|
+
for (const turn of sessionSummary.turns) {
|
|
332
|
+
lines.push(`Turn ${turn.turn}:`);
|
|
333
|
+
for (const toolCall of turn.toolCalls) {
|
|
334
|
+
lines.push(` - ${toolCall.name}${toolCall.argsText ? ` ${toolCall.argsText}` : ""}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getRunCounts(store: SubagentStore): { running: number; idle: number } {
|
|
343
|
+
const dedupedRunning = new Map<number, CommandRunState>();
|
|
344
|
+
|
|
345
|
+
for (const [runId, run] of store.commandRuns) {
|
|
346
|
+
if (run.removed) continue;
|
|
347
|
+
dedupedRunning.set(runId, run);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const [runId, entry] of store.globalLiveRuns) {
|
|
351
|
+
if (entry.runState.removed) continue;
|
|
352
|
+
dedupedRunning.set(runId, entry.runState);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const running = Array.from(dedupedRunning.values()).filter((run) => run.status === "running").length;
|
|
356
|
+
const idle = Array.from(store.commandRuns.values()).filter((run) => !run.removed && run.status !== "running").length;
|
|
357
|
+
return { running, idle };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function formatIdleRunWarning(idleRunCount: number): string {
|
|
361
|
+
return (
|
|
362
|
+
`⚠️ Idle subagent runs: ${idleRunCount}. ` +
|
|
363
|
+
`There are at least ${IDLE_RUN_WARNING_THRESHOLD} completed or failed runs that have not been removed. ` +
|
|
364
|
+
"Remove unneeded runs with `subagent remove <runId|all>`."
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function getCurrentSessionFile(ctx: SubagentToolExecuteContext): string {
|
|
369
|
+
try {
|
|
370
|
+
const raw = ctx.sessionManager.getSessionFile?.() ?? "";
|
|
371
|
+
return typeof raw === "string" ? raw.replace(/[\r\n\t]+/g, "").trim() : "";
|
|
372
|
+
} catch {
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isInOriginSession(ctx: SubagentToolExecuteContext, originSessionFile: string): boolean {
|
|
378
|
+
const currentSessionFile = getCurrentSessionFile(ctx);
|
|
379
|
+
return !currentSessionFile || !originSessionFile || currentSessionFile === originSessionFile;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function createEmptyDetails(
|
|
383
|
+
mode: LaunchMode,
|
|
384
|
+
inheritMainContext: boolean,
|
|
385
|
+
projectAgentsDir: string | null,
|
|
386
|
+
launches: SubagentLaunchSummary[] = [],
|
|
387
|
+
): SubagentDetails {
|
|
388
|
+
return {
|
|
389
|
+
mode,
|
|
390
|
+
inheritMainContext,
|
|
391
|
+
projectAgentsDir,
|
|
392
|
+
results: [],
|
|
393
|
+
launches,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function buildRunStartMessage(runState: CommandRunState, status: "started" | "resumed") {
|
|
398
|
+
const contextLabel = runState.contextMode === "main" ? "main context" : "dedicated sub-session";
|
|
399
|
+
return {
|
|
400
|
+
customType: "subagent-tool" as const,
|
|
401
|
+
content:
|
|
402
|
+
`[subagent:${runState.agent}#${runState.id}] ${status}` +
|
|
403
|
+
`\nContext: ${contextLabel} · turn ${runState.turnCount}`,
|
|
404
|
+
display: false,
|
|
405
|
+
details: {
|
|
406
|
+
runId: runState.id,
|
|
407
|
+
agent: runState.agent,
|
|
408
|
+
task: runState.task,
|
|
409
|
+
displayTask: runState.displayTask,
|
|
410
|
+
continuedFromRunId: runState.continuedFromRunId,
|
|
411
|
+
turnCount: runState.turnCount,
|
|
412
|
+
contextMode: runState.contextMode,
|
|
413
|
+
sessionFile: runState.sessionFile,
|
|
414
|
+
persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
|
|
415
|
+
status,
|
|
416
|
+
startedAt: runState.startedAt,
|
|
417
|
+
elapsedMs: runState.elapsedMs,
|
|
418
|
+
lastActivityAt: runState.lastActivityAt,
|
|
419
|
+
thoughtText: runState.thoughtText,
|
|
420
|
+
batchId: runState.batchId,
|
|
421
|
+
pipelineId: runState.pipelineId,
|
|
422
|
+
pipelineStepIndex: runState.pipelineStepIndex,
|
|
423
|
+
runtime: runState.runtime,
|
|
424
|
+
claudeSessionId: runState.claudeSessionId,
|
|
425
|
+
claudeProjectDir: runState.claudeProjectDir,
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?: boolean }) {
|
|
431
|
+
const { runState, result, isError, rawOutput } = finalized;
|
|
432
|
+
const usage = result ? formatUsageStats(result.usage, result.model) : "";
|
|
433
|
+
return {
|
|
434
|
+
customType: "subagent-tool" as const,
|
|
435
|
+
content:
|
|
436
|
+
`[subagent:${runState.agent}#${runState.id}] ${isError ? "failed" : "completed"}` +
|
|
437
|
+
`\nPrompt: ${truncateLines(runState.task, 2)}` +
|
|
438
|
+
(usage ? `\nUsage: ${usage}` : "") +
|
|
439
|
+
(runState.thoughtText ? `\nThought: ${runState.thoughtText}` : "") +
|
|
440
|
+
`\n\n${rawOutput}`,
|
|
441
|
+
display: options?.display ?? true,
|
|
442
|
+
details: {
|
|
443
|
+
runId: runState.id,
|
|
444
|
+
agent: runState.agent,
|
|
445
|
+
task: runState.task,
|
|
446
|
+
displayTask: runState.displayTask,
|
|
447
|
+
continuedFromRunId: runState.continuedFromRunId,
|
|
448
|
+
turnCount: runState.turnCount,
|
|
449
|
+
contextMode: runState.contextMode,
|
|
450
|
+
sessionFile: runState.sessionFile,
|
|
451
|
+
persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
|
|
452
|
+
startedAt: runState.startedAt,
|
|
453
|
+
elapsedMs: runState.elapsedMs,
|
|
454
|
+
lastActivityAt: runState.lastActivityAt,
|
|
455
|
+
exitCode: result?.exitCode,
|
|
456
|
+
usage: result?.usage,
|
|
457
|
+
model: result?.model,
|
|
458
|
+
source: result?.agentSource,
|
|
459
|
+
thoughtText: runState.thoughtText,
|
|
460
|
+
status: runState.status,
|
|
461
|
+
batchId: runState.batchId,
|
|
462
|
+
pipelineId: runState.pipelineId,
|
|
463
|
+
pipelineStepIndex: runState.pipelineStepIndex,
|
|
464
|
+
runtime: runState.runtime,
|
|
465
|
+
claudeSessionId: runState.claudeSessionId,
|
|
466
|
+
claudeProjectDir: runState.claudeProjectDir,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function buildEscalationMessage(runState: CommandRunState, escalationMessage: string, result: SingleResult) {
|
|
472
|
+
const usage = formatUsageStats(result.usage, result.model);
|
|
473
|
+
return {
|
|
474
|
+
customType: "subagent-tool" as const,
|
|
475
|
+
content:
|
|
476
|
+
`[subagent:${runState.agent}#${runState.id}] escalated` +
|
|
477
|
+
`\nPrompt: ${truncateLines(runState.task, 2)}` +
|
|
478
|
+
(usage ? `\nUsage: ${usage}` : "") +
|
|
479
|
+
`\n\n[ESCALATION] ${escalationMessage}`,
|
|
480
|
+
display: true,
|
|
481
|
+
details: {
|
|
482
|
+
runId: runState.id,
|
|
483
|
+
agent: runState.agent,
|
|
484
|
+
task: runState.task,
|
|
485
|
+
displayTask: runState.displayTask,
|
|
486
|
+
status: "error" as const,
|
|
487
|
+
persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
|
|
488
|
+
startedAt: runState.startedAt,
|
|
489
|
+
elapsedMs: runState.elapsedMs,
|
|
490
|
+
lastActivityAt: runState.lastActivityAt,
|
|
491
|
+
exitCode: result.exitCode,
|
|
492
|
+
usage: result.usage,
|
|
493
|
+
model: result.model,
|
|
494
|
+
batchId: runState.batchId,
|
|
495
|
+
pipelineId: runState.pipelineId,
|
|
496
|
+
pipelineStepIndex: runState.pipelineStepIndex,
|
|
497
|
+
runtime: runState.runtime,
|
|
498
|
+
claudeSessionId: runState.claudeSessionId,
|
|
499
|
+
claudeProjectDir: runState.claudeProjectDir,
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function buildStrongWaitMessage(runId: number): string {
|
|
505
|
+
return `Run #${runId} is still running.\n${SUBAGENT_STRONG_WAIT_MESSAGE}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function finalizeRunState(runState: CommandRunState, result: SingleResult): FinalizedRun {
|
|
509
|
+
updateRunFromResult(runState, result);
|
|
510
|
+
|
|
511
|
+
if (runState.autoAbortReason) {
|
|
512
|
+
runState.status = "error";
|
|
513
|
+
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
514
|
+
runState.lastOutput = runState.autoAbortReason;
|
|
515
|
+
runState.lastLine = runState.autoAbortReason;
|
|
516
|
+
return { runState, result, isError: true, rawOutput: runState.autoAbortReason };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (result.exitCode === ESCALATION_EXIT_CODE && runState.sessionFile) {
|
|
520
|
+
const escalation = readAndConsumeEscalation(runState.sessionFile);
|
|
521
|
+
const escalationMsg = escalation?.message ?? "Subagent escalated without a message.";
|
|
522
|
+
runState.status = "error";
|
|
523
|
+
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
524
|
+
runState.lastOutput = `[ESCALATION] ${escalationMsg}`;
|
|
525
|
+
runState.lastLine = `[ESCALATION] ${escalationMsg}`;
|
|
526
|
+
return {
|
|
527
|
+
runState,
|
|
528
|
+
result,
|
|
529
|
+
isError: true,
|
|
530
|
+
rawOutput: `[ESCALATION] ${escalationMsg}`,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const failure = diagnoseResultFailure(result);
|
|
535
|
+
const isError = failure.failed;
|
|
536
|
+
runState.status = isError ? "error" : "done";
|
|
537
|
+
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
538
|
+
let rawOutput: string;
|
|
539
|
+
if (isError && failure.contextOverflow) {
|
|
540
|
+
const partial = getFinalOutput(result.messages).trim();
|
|
541
|
+
rawOutput = [
|
|
542
|
+
failure.reason,
|
|
543
|
+
partial ? `\n\n--- Partial findings (last output before cutoff) ---\n${partial}` : "",
|
|
544
|
+
`\n\n(Run \`subagent detail ${runState.id}\` for the full tool-call trace.)`,
|
|
545
|
+
].join("");
|
|
546
|
+
} else if (isError) {
|
|
547
|
+
rawOutput =
|
|
548
|
+
failure.reason || result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
549
|
+
} else {
|
|
550
|
+
rawOutput = getFinalOutput(result.messages) || "(no output)";
|
|
551
|
+
}
|
|
552
|
+
runState.lastOutput = rawOutput;
|
|
553
|
+
runState.lastLine = getLastNonEmptyLine(rawOutput) || rawOutput;
|
|
554
|
+
|
|
555
|
+
return { runState, result, isError, rawOutput };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function formatBatchSummary(batchId: string, runs: CommandRunState[], terminalStatus: "completed" | "error"): string {
|
|
559
|
+
const headerStatus = runs.map((run) => `#${run.id} ${run.status === "done" ? "done" : "error"}`).join(", ");
|
|
560
|
+
const body = runs
|
|
561
|
+
.map((run) => {
|
|
562
|
+
const summary = run.lastOutput?.trim() || run.lastLine?.trim() || "(no output)";
|
|
563
|
+
return `#${run.id} ${run.agent}\n- ${summary}`;
|
|
564
|
+
})
|
|
565
|
+
.join("\n\n");
|
|
566
|
+
return `[subagent-batch#${batchId}] ${terminalStatus}\nRuns: ${headerStatus}\n\n${body}`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function formatPipelineSummary(
|
|
570
|
+
pipelineId: string,
|
|
571
|
+
stepResults: PipelineStepResult[],
|
|
572
|
+
terminalStatus: "completed" | "stopped" | "error",
|
|
573
|
+
): string {
|
|
574
|
+
const steps = stepResults
|
|
575
|
+
.map(
|
|
576
|
+
(step, index) =>
|
|
577
|
+
`Step ${index + 1} · #${step.runId} ${step.agent} · ${step.status}\nTask: ${step.task}\n${step.output}`,
|
|
578
|
+
)
|
|
579
|
+
.join("\n\n");
|
|
580
|
+
return `[subagent-chain#${pipelineId}] ${terminalStatus}\n\n${steps}`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function toLaunchSummary(
|
|
584
|
+
runState: Pick<CommandRunState, "agent" | "id" | "batchId" | "pipelineId" | "pipelineStepIndex">,
|
|
585
|
+
mode: SubagentLaunchSummary["mode"],
|
|
586
|
+
): SubagentLaunchSummary {
|
|
587
|
+
return {
|
|
588
|
+
agent: runState.agent,
|
|
589
|
+
mode,
|
|
590
|
+
runId: runState.id,
|
|
591
|
+
batchId: runState.batchId,
|
|
592
|
+
pipelineId: runState.pipelineId,
|
|
593
|
+
stepIndex: runState.pipelineStepIndex,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function buildRunAnalyticsSummary(
|
|
598
|
+
runState: Pick<
|
|
599
|
+
CommandRunState,
|
|
600
|
+
"id" | "agent" | "status" | "elapsedMs" | "model" | "batchId" | "pipelineId" | "pipelineStepIndex" | "runtime"
|
|
601
|
+
>,
|
|
602
|
+
): Record<string, unknown> {
|
|
603
|
+
return {
|
|
604
|
+
runId: runState.id,
|
|
605
|
+
agent: runState.agent,
|
|
606
|
+
status: runState.status,
|
|
607
|
+
elapsedMs: runState.elapsedMs,
|
|
608
|
+
model: runState.model,
|
|
609
|
+
batchId: runState.batchId,
|
|
610
|
+
pipelineId: runState.pipelineId,
|
|
611
|
+
stepIndex: runState.pipelineStepIndex,
|
|
612
|
+
runtime: runState.runtime,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function makePendingCompletion(message: PendingCompletion["message"], triggerTurn = true): PendingCompletion {
|
|
617
|
+
return {
|
|
618
|
+
message,
|
|
619
|
+
options: { deliverAs: "followUp", triggerTurn },
|
|
620
|
+
createdAt: Date.now(),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function isInteractiveTuiContext(ctx: SubagentToolExecuteContext): boolean {
|
|
625
|
+
return Boolean(ctx.hasUI && process.stdin.isTTY && process.stdout.isTTY);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function finalizeRunError(runState: CommandRunState, error: unknown): FinalizedRun {
|
|
629
|
+
runState.status = "error";
|
|
630
|
+
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
631
|
+
runState.lastLine =
|
|
632
|
+
runState.autoAbortReason ?? (error instanceof Error ? error.message : "Subagent execution failed");
|
|
633
|
+
runState.lastOutput = runState.lastLine;
|
|
634
|
+
return {
|
|
635
|
+
runState,
|
|
636
|
+
isError: true,
|
|
637
|
+
rawOutput: runState.lastLine,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore) {
|
|
642
|
+
const execute = async (
|
|
643
|
+
_toolCallId: string,
|
|
644
|
+
params: Record<string, any>,
|
|
645
|
+
signal: AbortSignal | undefined,
|
|
646
|
+
_onUpdate: OnUpdateCallback | undefined,
|
|
647
|
+
ctx: SubagentToolExecuteContext,
|
|
648
|
+
): Promise<SubagentExecuteResult> => {
|
|
649
|
+
const knownRunIds = Array.from(store.commandRuns.values())
|
|
650
|
+
.filter((run) => !run.removed)
|
|
651
|
+
.map((run) => run.id);
|
|
652
|
+
const parsedCommand = parseSubagentToolCommand(params.command, { knownRunIds });
|
|
653
|
+
|
|
654
|
+
const hasRunningRuns = Array.from(store.commandRuns.values()).some((r) => !r.removed && r.status === "running");
|
|
655
|
+
const isLaunchAction =
|
|
656
|
+
parsedCommand.type === "params" &&
|
|
657
|
+
(parsedCommand.params.asyncAction === "run" ||
|
|
658
|
+
parsedCommand.params.asyncAction === "batch" ||
|
|
659
|
+
parsedCommand.params.asyncAction === "chain" ||
|
|
660
|
+
parsedCommand.params.asyncAction === "list");
|
|
661
|
+
const shouldAutoClearFinishedRuns = isLaunchAction || !hasRunningRuns;
|
|
662
|
+
|
|
663
|
+
if (shouldAutoClearFinishedRuns) {
|
|
664
|
+
const clearedRunIds = clearFinishedRuns(store, {
|
|
665
|
+
ctx,
|
|
666
|
+
pi,
|
|
667
|
+
updateWidget: true,
|
|
668
|
+
removalReason: "tool-auto-clear",
|
|
669
|
+
});
|
|
670
|
+
for (const runId of clearedRunIds) {
|
|
671
|
+
store.recentLaunchTimestamps.delete(runId);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (parsedCommand.type === "error") {
|
|
676
|
+
return {
|
|
677
|
+
content: [{ type: "text", text: `${parsedCommand.message}\n\n${SUBAGENT_CLI_HELP_TEXT}` }],
|
|
678
|
+
details: createEmptyDetails("single", false, null),
|
|
679
|
+
isError: true,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (parsedCommand.type === "help") {
|
|
684
|
+
return {
|
|
685
|
+
content: [{ type: "text", text: SUBAGENT_CLI_HELP_TEXT }],
|
|
686
|
+
details: createEmptyDetails("single", false, null),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const discovery = discoverAgents(ctx.cwd);
|
|
691
|
+
const agents = discovery.agents;
|
|
692
|
+
|
|
693
|
+
if (parsedCommand.type === "agents") {
|
|
694
|
+
if (agents.length === 0) {
|
|
695
|
+
return {
|
|
696
|
+
content: [{ type: "text", text: "No subagents found." }],
|
|
697
|
+
details: createEmptyDetails("single", false, discovery.projectAgentsDir),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const lines = agents.map((agent) => {
|
|
702
|
+
const model = agent.model ?? "(inherit current model)";
|
|
703
|
+
const thinking = agent.thinking ?? "(inherit current thinking)";
|
|
704
|
+
const tools = agent.tools && agent.tools.length > 0 ? agent.tools.join(",") : "default";
|
|
705
|
+
const description = agent.description ? ` · ${agent.description}` : "";
|
|
706
|
+
return `${agent.name} [${agent.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}`;
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
content: [{ type: "text", text: `Available subagents\n\n${lines.join("\n")}` }],
|
|
711
|
+
details: createEmptyDetails("single", false, discovery.projectAgentsDir),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const cmdParams = parsedCommand.params;
|
|
716
|
+
const asyncAction = cmdParams.asyncAction ?? "run";
|
|
717
|
+
const contextMode = cmdParams.contextMode ?? "isolated";
|
|
718
|
+
const inheritMainContext = contextMode === "main";
|
|
719
|
+
const rawMainContext = inheritMainContext ? buildMainContextText(ctx) : { text: "", totalMessageCount: 0 };
|
|
720
|
+
const mainContextText = rawMainContext.text;
|
|
721
|
+
const totalMessageCount = rawMainContext.totalMessageCount;
|
|
722
|
+
const mainSessionFile = inheritMainContext ? getCurrentSessionFile(ctx) || undefined : undefined;
|
|
723
|
+
const originSessionFile = getCurrentSessionFile(ctx);
|
|
724
|
+
|
|
725
|
+
const runCounts = getRunCounts(store);
|
|
726
|
+
const idleRunWarning =
|
|
727
|
+
runCounts.idle >= IDLE_RUN_WARNING_THRESHOLD ? formatIdleRunWarning(runCounts.idle) : undefined;
|
|
728
|
+
const withIdleRunWarning = (text: string): string => (idleRunWarning ? `${idleRunWarning}\n\n${text}` : text);
|
|
729
|
+
|
|
730
|
+
if (idleRunWarning && ctx.hasUI) {
|
|
731
|
+
ctx.ui?.notify?.(idleRunWarning, "warning");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const hasBatch = asyncAction === "batch";
|
|
735
|
+
const hasChain = asyncAction === "chain";
|
|
736
|
+
const hasSingle = asyncAction === "run" || asyncAction === "continue";
|
|
737
|
+
const mode: LaunchMode = hasBatch ? "batch" : hasChain ? "chain" : "single";
|
|
738
|
+
const shouldRunAsync = isInteractiveTuiContext(ctx);
|
|
739
|
+
const makeDetails = (
|
|
740
|
+
modeOverride: LaunchMode = mode,
|
|
741
|
+
results: SingleResult[] = [],
|
|
742
|
+
launches: SubagentLaunchSummary[] = [],
|
|
743
|
+
): SubagentDetails => ({
|
|
744
|
+
mode: modeOverride,
|
|
745
|
+
inheritMainContext,
|
|
746
|
+
projectAgentsDir: discovery.projectAgentsDir,
|
|
747
|
+
results,
|
|
748
|
+
launches,
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
if ((hasSingle || hasBatch || hasChain) && inheritMainContext && !mainSessionFile) {
|
|
752
|
+
return {
|
|
753
|
+
content: [
|
|
754
|
+
{
|
|
755
|
+
type: "text",
|
|
756
|
+
text: "contextMode=main requires an active main session. Current session is unavailable (e.g. --no-session).",
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
details: makeDetails(),
|
|
760
|
+
isError: true,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Early validation: check that all requested agent names exist before launching
|
|
765
|
+
if (hasSingle || hasBatch || hasChain) {
|
|
766
|
+
const requestedNames: string[] = [];
|
|
767
|
+
if (hasSingle) {
|
|
768
|
+
const name = (cmdParams.agent as string | undefined) ?? (cmdParams.continueFromRunId ? undefined : "worker");
|
|
769
|
+
if (name) requestedNames.push(name);
|
|
770
|
+
}
|
|
771
|
+
if (hasBatch && Array.isArray(cmdParams.runs)) {
|
|
772
|
+
for (const item of cmdParams.runs as BatchOrChainItem[]) requestedNames.push(item.agent);
|
|
773
|
+
}
|
|
774
|
+
if (hasChain && Array.isArray(cmdParams.steps)) {
|
|
775
|
+
for (const step of cmdParams.steps as BatchOrChainItem[]) requestedNames.push(step.agent);
|
|
776
|
+
}
|
|
777
|
+
const unknownNames = [...new Set(requestedNames)].filter((name) => !agents.some((a) => a.name === name));
|
|
778
|
+
if (unknownNames.length > 0) {
|
|
779
|
+
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
780
|
+
return {
|
|
781
|
+
content: [
|
|
782
|
+
{
|
|
783
|
+
type: "text",
|
|
784
|
+
text: `❌ Unknown agent${unknownNames.length > 1 ? "s" : ""}: ${unknownNames.map((n) => `"${n}"`).join(", ")}.\n\nAvailable agents: ${available}`,
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
details: makeDetails(),
|
|
788
|
+
isError: true,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (asyncAction === "list") {
|
|
794
|
+
// Anti-polling guard: block list while any run is within launch cooldown
|
|
795
|
+
const now = Date.now();
|
|
796
|
+
const cooldownRunId = Array.from(store.commandRuns.values()).find((run) => {
|
|
797
|
+
if (run.status !== "running") return false;
|
|
798
|
+
const launchedAt = store.recentLaunchTimestamps.get(run.id);
|
|
799
|
+
return typeof launchedAt === "number" && now - launchedAt <= SUBAGENT_POLL_COOLDOWN_MS;
|
|
800
|
+
})?.id;
|
|
801
|
+
if (cooldownRunId !== undefined) {
|
|
802
|
+
return {
|
|
803
|
+
content: [{ type: "text", text: buildStrongWaitMessage(cooldownRunId) }],
|
|
804
|
+
details: makeDetails("single"),
|
|
805
|
+
isError: true,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const allRuns = Array.from(store.commandRuns.values()).sort((a, b) => b.id - a.id);
|
|
810
|
+
if (allRuns.length === 0) {
|
|
811
|
+
return {
|
|
812
|
+
content: [{ type: "text", text: withIdleRunWarning("No subagent runs found.") }],
|
|
813
|
+
details: makeDetails("single"),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const visibleRuns = allRuns.slice(0, MAX_LISTED_RUNS);
|
|
817
|
+
const hiddenCount = allRuns.length - visibleRuns.length;
|
|
818
|
+
const lines = visibleRuns.map((run) => {
|
|
819
|
+
const contextWindow = resolveContextWindow(ctx, run.model);
|
|
820
|
+
const usedPercent = getUsedContextPercent(run.usage?.contextTokens, contextWindow);
|
|
821
|
+
const usageSuffix = usedPercent === undefined ? "" : ` usage:${formatContextUsageBar(usedPercent)}`;
|
|
822
|
+
const taskPreview = truncateLines(run.task, 2).replace(/\n/g, "\n ");
|
|
823
|
+
return `${formatCommandRunSummary(run)}${usageSuffix}\n ${taskPreview}`;
|
|
824
|
+
});
|
|
825
|
+
const header =
|
|
826
|
+
hiddenCount > 0
|
|
827
|
+
? `Subagent runs (showing ${visibleRuns.length} of ${allRuns.length}, oldest ${hiddenCount} hidden)`
|
|
828
|
+
: "Subagent runs";
|
|
829
|
+
return {
|
|
830
|
+
content: [{ type: "text", text: withIdleRunWarning(`${header}\n\n${lines.join("\n\n")}`) }],
|
|
831
|
+
details: makeDetails("single"),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const rawRunIds = Array.isArray(cmdParams.runIds) ? cmdParams.runIds : undefined;
|
|
836
|
+
const invalidRunIds = (rawRunIds ?? []).filter((value) => !Number.isInteger(value));
|
|
837
|
+
if (invalidRunIds.length > 0) {
|
|
838
|
+
return {
|
|
839
|
+
content: [{ type: "text", text: "runIds must be an array of integer run IDs." }],
|
|
840
|
+
details: makeDetails("single"),
|
|
841
|
+
isError: true,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const runIdsFromArray = ((rawRunIds ?? []) as number[]).filter((value) => Number.isInteger(value));
|
|
846
|
+
const hasRunId = Number.isInteger(cmdParams.runId);
|
|
847
|
+
const hasRunIds = runIdsFromArray.length > 0;
|
|
848
|
+
const isBulkAction = asyncAction === "abort" || asyncAction === "remove";
|
|
849
|
+
|
|
850
|
+
if (!hasSingle && !hasBatch && !hasChain) {
|
|
851
|
+
if (!isBulkAction && rawRunIds !== undefined) {
|
|
852
|
+
return {
|
|
853
|
+
content: [{ type: "text", text: `asyncAction=${asyncAction} does not support runIds. Use runId.` }],
|
|
854
|
+
details: makeDetails("single"),
|
|
855
|
+
isError: true,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (hasRunId && hasRunIds) {
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: "text", text: "Use either runId or runIds, not both." }],
|
|
862
|
+
details: makeDetails("single"),
|
|
863
|
+
isError: true,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (!hasRunId && !hasRunIds) {
|
|
868
|
+
const required = isBulkAction ? "runId or runIds" : "runId";
|
|
869
|
+
return {
|
|
870
|
+
content: [{ type: "text", text: `asyncAction=${asyncAction} requires ${required}.` }],
|
|
871
|
+
details: makeDetails("single"),
|
|
872
|
+
isError: true,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const targetRunIds = hasRunIds ? Array.from(new Set(runIdsFromArray)) : [cmdParams.runId as number];
|
|
877
|
+
const firstRunId = targetRunIds[0];
|
|
878
|
+
|
|
879
|
+
if (asyncAction === "status" || asyncAction === "detail") {
|
|
880
|
+
const run = store.commandRuns.get(firstRunId);
|
|
881
|
+
if (!run) {
|
|
882
|
+
return {
|
|
883
|
+
content: [{ type: "text", text: `Unknown subagent run #${firstRunId}.` }],
|
|
884
|
+
details: makeDetails("single"),
|
|
885
|
+
isError: true,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const launchedAt = store.recentLaunchTimestamps.get(run.id);
|
|
890
|
+
const withinCooldown =
|
|
891
|
+
run.status === "running" &&
|
|
892
|
+
typeof launchedAt === "number" &&
|
|
893
|
+
Date.now() - launchedAt <= SUBAGENT_POLL_COOLDOWN_MS;
|
|
894
|
+
if (withinCooldown) {
|
|
895
|
+
return {
|
|
896
|
+
content: [{ type: "text", text: buildStrongWaitMessage(run.id) }],
|
|
897
|
+
details: makeDetails("single"),
|
|
898
|
+
isError: true,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (asyncAction === "status") {
|
|
903
|
+
const output = run.lastOutput ?? run.lastLine ?? "(no output yet)";
|
|
904
|
+
const preview =
|
|
905
|
+
output.length > STATUS_OUTPUT_PREVIEW_MAX_CHARS
|
|
906
|
+
? `${output.slice(0, STATUS_OUTPUT_PREVIEW_MAX_CHARS)}\n\n... [truncated]`
|
|
907
|
+
: output;
|
|
908
|
+
return {
|
|
909
|
+
content: [{ type: "text", text: `${formatCommandRunSummary(run)}\n${run.task}\n\n${preview}` }],
|
|
910
|
+
details: makeDetails("single"),
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (run.status === "running") {
|
|
915
|
+
return {
|
|
916
|
+
content: [
|
|
917
|
+
{ type: "text", text: `Subagent run #${run.id} is still running. detail is available after completion.` },
|
|
918
|
+
],
|
|
919
|
+
details: makeDetails("single"),
|
|
920
|
+
isError: true,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
content: [{ type: "text", text: formatRunDetailOutput(run) }],
|
|
926
|
+
details: makeDetails("single"),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (asyncAction === "abort") {
|
|
931
|
+
const aborting: number[] = [];
|
|
932
|
+
const notRunning: number[] = [];
|
|
933
|
+
const unknown: number[] = [];
|
|
934
|
+
|
|
935
|
+
for (const runId of targetRunIds) {
|
|
936
|
+
const run = store.commandRuns.get(runId);
|
|
937
|
+
if (!run) {
|
|
938
|
+
unknown.push(runId);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const abortCtrl = run.abortController ?? store.globalLiveRuns.get(run.id)?.abortController;
|
|
943
|
+
if (run.status !== "running" || !abortCtrl) {
|
|
944
|
+
notRunning.push(runId);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
run.lastLine = "Aborting by subagent tool...";
|
|
949
|
+
run.lastOutput = run.lastLine;
|
|
950
|
+
abortCtrl.abort();
|
|
951
|
+
aborting.push(runId);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (aborting.length > 0) {
|
|
955
|
+
updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (targetRunIds.length === 1 && aborting.length === 1 && notRunning.length === 0 && unknown.length === 0) {
|
|
959
|
+
return {
|
|
960
|
+
content: [{ type: "text", text: `Aborting subagent run #${aborting[0]}...` }],
|
|
961
|
+
details: makeDetails("single"),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const lines: string[] = [];
|
|
966
|
+
if (aborting.length > 0) lines.push(`Aborting: ${aborting.map((id) => `#${id}`).join(", ")}.`);
|
|
967
|
+
if (notRunning.length > 0) lines.push(`Not running: ${notRunning.map((id) => `#${id}`).join(", ")}.`);
|
|
968
|
+
if (unknown.length > 0) lines.push(`Unknown: ${unknown.map((id) => `#${id}`).join(", ")}.`);
|
|
969
|
+
if (lines.length === 0) lines.push("No subagent runs matched.");
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
973
|
+
details: makeDetails("single"),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (asyncAction === "remove") {
|
|
978
|
+
const removed: number[] = [];
|
|
979
|
+
const abortedWhileRemoving: number[] = [];
|
|
980
|
+
const unknown: number[] = [];
|
|
981
|
+
|
|
982
|
+
for (const runId of targetRunIds) {
|
|
983
|
+
const run = store.commandRuns.get(runId);
|
|
984
|
+
if (!run) {
|
|
985
|
+
unknown.push(runId);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const { removed: didRemove, aborted } = removeRun(store, run.id, {
|
|
990
|
+
ctx,
|
|
991
|
+
pi,
|
|
992
|
+
reason: "Aborting by subagent tool remove...",
|
|
993
|
+
removalReason: "tool-remove",
|
|
994
|
+
updateWidget: false,
|
|
995
|
+
});
|
|
996
|
+
if (!didRemove) {
|
|
997
|
+
unknown.push(runId);
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
removed.push(runId);
|
|
1002
|
+
store.recentLaunchTimestamps.delete(runId);
|
|
1003
|
+
if (aborted) abortedWhileRemoving.push(runId);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (removed.length > 0) {
|
|
1007
|
+
updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (targetRunIds.length === 1 && removed.length === 1 && unknown.length === 0) {
|
|
1011
|
+
return {
|
|
1012
|
+
content: [
|
|
1013
|
+
{
|
|
1014
|
+
type: "text",
|
|
1015
|
+
text:
|
|
1016
|
+
abortedWhileRemoving.length > 0
|
|
1017
|
+
? `Removed subagent run #${removed[0]} (aborting in background).`
|
|
1018
|
+
: `Removed subagent run #${removed[0]}.`,
|
|
1019
|
+
},
|
|
1020
|
+
],
|
|
1021
|
+
details: makeDetails("single"),
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const lines: string[] = [];
|
|
1026
|
+
if (removed.length > 0) lines.push(`Removed: ${removed.map((id) => `#${id}`).join(", ")}.`);
|
|
1027
|
+
if (abortedWhileRemoving.length > 0)
|
|
1028
|
+
lines.push(`Aborting in background: ${abortedWhileRemoving.map((id) => `#${id}`).join(", ")}.`);
|
|
1029
|
+
if (unknown.length > 0) lines.push(`Unknown: ${unknown.map((id) => `#${id}`).join(", ")}.`);
|
|
1030
|
+
if (lines.length === 0) lines.push("No subagent runs matched.");
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1034
|
+
details: makeDetails("single"),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const requestedLaunchCount = hasBatch
|
|
1040
|
+
? Array.isArray(cmdParams.runs)
|
|
1041
|
+
? cmdParams.runs.length
|
|
1042
|
+
: 0
|
|
1043
|
+
: hasChain
|
|
1044
|
+
? 1
|
|
1045
|
+
: 1;
|
|
1046
|
+
if (runCounts.running + requestedLaunchCount > MAX_CONCURRENT_ASYNC_SUBAGENT_RUNS) {
|
|
1047
|
+
return {
|
|
1048
|
+
content: [
|
|
1049
|
+
{
|
|
1050
|
+
type: "text",
|
|
1051
|
+
text: withIdleRunWarning(
|
|
1052
|
+
`Too many running subagent runs (${runCounts.running}). Max is ${MAX_CONCURRENT_ASYNC_SUBAGENT_RUNS}. Wait for completion, abort unnecessary runs, or remove stale runs before starting more runs.`,
|
|
1053
|
+
),
|
|
1054
|
+
},
|
|
1055
|
+
],
|
|
1056
|
+
details: makeDetails(),
|
|
1057
|
+
isError: true,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const parentAbortCleanups = new Map<number, () => void>();
|
|
1062
|
+
|
|
1063
|
+
function clearParentAbortListener(runId: number): void {
|
|
1064
|
+
const cleanup = parentAbortCleanups.get(runId);
|
|
1065
|
+
if (!cleanup) return;
|
|
1066
|
+
cleanup();
|
|
1067
|
+
parentAbortCleanups.delete(runId);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function linkParentAbortSignal(runId: number, abortController: AbortController): void {
|
|
1071
|
+
clearParentAbortListener(runId);
|
|
1072
|
+
if (!signal || shouldRunAsync) return;
|
|
1073
|
+
|
|
1074
|
+
const abortFromParent = () => abortController.abort();
|
|
1075
|
+
if (signal.aborted) {
|
|
1076
|
+
abortFromParent();
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
signal.addEventListener("abort", abortFromParent, { once: true });
|
|
1081
|
+
parentAbortCleanups.set(runId, () => signal.removeEventListener("abort", abortFromParent));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function clearRunAbortState(runState: CommandRunState): void {
|
|
1085
|
+
clearParentAbortListener(runState.id);
|
|
1086
|
+
runState.abortController = undefined;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function registerRunLaunch(config: RunLaunchConfig): CommandRunState {
|
|
1090
|
+
const initialDisplayTask = buildSubagentDisplayTaskFallback(config.taskForDisplay);
|
|
1091
|
+
let runState: CommandRunState;
|
|
1092
|
+
if (config.existingRunState) {
|
|
1093
|
+
runState = config.existingRunState;
|
|
1094
|
+
runState.agent = config.agent;
|
|
1095
|
+
runState.task = config.taskForDisplay;
|
|
1096
|
+
runState.displayTask = initialDisplayTask;
|
|
1097
|
+
runState.status = "running";
|
|
1098
|
+
runState.startedAt = Date.now();
|
|
1099
|
+
runState.lastActivityAt = Date.now();
|
|
1100
|
+
runState.elapsedMs = 0;
|
|
1101
|
+
runState.toolCalls = 0;
|
|
1102
|
+
runState.lastLine = "";
|
|
1103
|
+
runState.lastOutput = "";
|
|
1104
|
+
runState.usage = undefined;
|
|
1105
|
+
runState.model = undefined;
|
|
1106
|
+
runState.removed = false;
|
|
1107
|
+
runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
|
|
1108
|
+
runState.contextMode = runState.contextMode ?? (config.inheritMainContext ? "main" : "sub");
|
|
1109
|
+
runState.continuedFromRunId = config.continuedFromRunId;
|
|
1110
|
+
runState.sessionFile = runState.sessionFile ?? makeSubagentSessionFile(runState.id);
|
|
1111
|
+
runState.persistedSessionBaseOffset = getSessionFileSize(runState.sessionFile);
|
|
1112
|
+
runState.source = "tool";
|
|
1113
|
+
} else {
|
|
1114
|
+
const runId = store.nextCommandRunId++;
|
|
1115
|
+
runState = {
|
|
1116
|
+
id: runId,
|
|
1117
|
+
agent: config.agent,
|
|
1118
|
+
task: config.taskForDisplay,
|
|
1119
|
+
displayTask: initialDisplayTask,
|
|
1120
|
+
status: "running",
|
|
1121
|
+
startedAt: Date.now(),
|
|
1122
|
+
lastActivityAt: Date.now(),
|
|
1123
|
+
elapsedMs: 0,
|
|
1124
|
+
toolCalls: 0,
|
|
1125
|
+
lastLine: "",
|
|
1126
|
+
lastOutput: "",
|
|
1127
|
+
continuedFromRunId: config.continuedFromRunId,
|
|
1128
|
+
turnCount: DEFAULT_TURN_COUNT,
|
|
1129
|
+
sessionFile: makeSubagentSessionFile(runId),
|
|
1130
|
+
persistedSessionBaseOffset: getSessionFileSize(makeSubagentSessionFile(runId)),
|
|
1131
|
+
removed: false,
|
|
1132
|
+
contextMode: config.inheritMainContext ? "main" : "sub",
|
|
1133
|
+
source: "tool",
|
|
1134
|
+
batchId: config.batchId,
|
|
1135
|
+
pipelineId: config.pipelineId,
|
|
1136
|
+
pipelineStepIndex: config.pipelineStepIndex,
|
|
1137
|
+
};
|
|
1138
|
+
store.commandRuns.set(runId, runState);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
runState.batchId = config.batchId;
|
|
1142
|
+
runState.pipelineId = config.pipelineId;
|
|
1143
|
+
runState.pipelineStepIndex = config.pipelineStepIndex;
|
|
1144
|
+
const abortController = new AbortController();
|
|
1145
|
+
runState.abortController = abortController;
|
|
1146
|
+
linkParentAbortSignal(runState.id, abortController);
|
|
1147
|
+
store.globalLiveRuns.set(runState.id, {
|
|
1148
|
+
runState,
|
|
1149
|
+
abortController,
|
|
1150
|
+
originSessionFile: config.originSessionFile,
|
|
1151
|
+
});
|
|
1152
|
+
store.recentLaunchTimestamps.set(runState.id, runState.startedAt);
|
|
1153
|
+
store.commandWidgetCtx = ctx as WidgetRenderCtx;
|
|
1154
|
+
updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
|
|
1155
|
+
refreshDisplayTaskInBackground(runState, config.taskForDisplay, ctx, store);
|
|
1156
|
+
return runState;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function cleanupRunAfterFinalDelivery(runId: number) {
|
|
1160
|
+
clearParentAbortListener(runId);
|
|
1161
|
+
store.globalLiveRuns.delete(runId);
|
|
1162
|
+
store.recentLaunchTimestamps.delete(runId);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
|
|
1166
|
+
let claudeCheckpointSent = !!runState.claudeSessionId;
|
|
1167
|
+
return enqueueSubagentInvocation(() =>
|
|
1168
|
+
runSingleAgent(
|
|
1169
|
+
ctx.cwd,
|
|
1170
|
+
agents,
|
|
1171
|
+
runState.agent,
|
|
1172
|
+
taskForAgent,
|
|
1173
|
+
runState.pipelineStepIndex,
|
|
1174
|
+
runState.abortController?.signal,
|
|
1175
|
+
(partial) => {
|
|
1176
|
+
if (runState.removed || store.disposed) return;
|
|
1177
|
+
const current = partial.details?.results?.[0];
|
|
1178
|
+
if (!current) return;
|
|
1179
|
+
updateRunFromResult(runState, current);
|
|
1180
|
+
if (shouldRunAsync && !claudeCheckpointSent && runState.claudeSessionId) {
|
|
1181
|
+
claudeCheckpointSent = true;
|
|
1182
|
+
pi.sendMessage(buildRunStartMessage(runState, "started"), {
|
|
1183
|
+
deliverAs: "followUp",
|
|
1184
|
+
triggerTurn: false,
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
updateCommandRunsWidget(store);
|
|
1188
|
+
},
|
|
1189
|
+
(results) => makeDetails(mode, results),
|
|
1190
|
+
{
|
|
1191
|
+
sessionFile: runState.sessionFile,
|
|
1192
|
+
resumeSessionId: runState.claudeSessionId,
|
|
1193
|
+
sidecarSessionFile: runState.sessionFile,
|
|
1194
|
+
persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
|
|
1195
|
+
},
|
|
1196
|
+
),
|
|
1197
|
+
).then((result) => finalizeRunState(runState, result));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (hasSingle) {
|
|
1201
|
+
const continuationRunId = Number.isInteger(cmdParams.runId) ? (cmdParams.runId as number) : undefined;
|
|
1202
|
+
let continueFromRun: CommandRunState | undefined;
|
|
1203
|
+
if (continuationRunId !== undefined) {
|
|
1204
|
+
continueFromRun = store.commandRuns.get(continuationRunId);
|
|
1205
|
+
if (!continueFromRun) {
|
|
1206
|
+
return {
|
|
1207
|
+
content: [
|
|
1208
|
+
{
|
|
1209
|
+
type: "text",
|
|
1210
|
+
text: withIdleRunWarning(
|
|
1211
|
+
`Unknown subagent run #${continuationRunId}. Use \`subagent runs\` to see available runs.`,
|
|
1212
|
+
),
|
|
1213
|
+
},
|
|
1214
|
+
],
|
|
1215
|
+
details: makeDetails("single"),
|
|
1216
|
+
isError: true,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
if (continueFromRun.status === "running") {
|
|
1220
|
+
return {
|
|
1221
|
+
content: [
|
|
1222
|
+
{
|
|
1223
|
+
type: "text",
|
|
1224
|
+
text: withIdleRunWarning(
|
|
1225
|
+
`Subagent #${continuationRunId} is still running. Wait for it to finish or abort it first.`,
|
|
1226
|
+
),
|
|
1227
|
+
},
|
|
1228
|
+
],
|
|
1229
|
+
details: makeDetails("single"),
|
|
1230
|
+
isError: true,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (continueFromRun.runtime === "claude") {
|
|
1235
|
+
if (!continueFromRun.claudeSessionId) {
|
|
1236
|
+
return {
|
|
1237
|
+
content: [
|
|
1238
|
+
{
|
|
1239
|
+
type: "text",
|
|
1240
|
+
text: `Cannot resume Claude run #${continuationRunId}: no claudeSessionId found. The session metadata was lost or never captured.`,
|
|
1241
|
+
},
|
|
1242
|
+
],
|
|
1243
|
+
details: makeDetails("single"),
|
|
1244
|
+
isError: true,
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (!continueFromRun.claudeProjectDir || continueFromRun.claudeProjectDir !== ctx.cwd) {
|
|
1248
|
+
return {
|
|
1249
|
+
content: [
|
|
1250
|
+
{
|
|
1251
|
+
type: "text",
|
|
1252
|
+
text: `Cannot resume Claude run #${continuationRunId}: claudeProjectDir mismatch. Expected "${continueFromRun.claudeProjectDir ?? "(none)"}", current cwd is "${ctx.cwd}".`,
|
|
1253
|
+
},
|
|
1254
|
+
],
|
|
1255
|
+
details: makeDetails("single"),
|
|
1256
|
+
isError: true,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const resolvedAgent = (cmdParams.agent ?? continueFromRun?.agent ?? "worker") as string;
|
|
1263
|
+
const rawTask = typeof cmdParams.task === "string" ? cmdParams.task : "";
|
|
1264
|
+
if (!rawTask.trim()) {
|
|
1265
|
+
return {
|
|
1266
|
+
content: [{ type: "text", text: withIdleRunWarning("subagent run/continue requires task.") }],
|
|
1267
|
+
details: makeDetails("single"),
|
|
1268
|
+
isError: true,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const taskForDisplay = continueFromRun ? `[continue #${continueFromRun.id}] ${rawTask}` : rawTask;
|
|
1273
|
+
const taskForAgent = wrapTaskWithMainContext(rawTask, stripTaskEchoFromMainContext(mainContextText, rawTask), {
|
|
1274
|
+
mainSessionFile,
|
|
1275
|
+
totalMessageCount,
|
|
1276
|
+
});
|
|
1277
|
+
const runState = registerRunLaunch({
|
|
1278
|
+
agent: resolvedAgent,
|
|
1279
|
+
taskForDisplay,
|
|
1280
|
+
taskForAgent,
|
|
1281
|
+
inheritMainContext,
|
|
1282
|
+
originSessionFile,
|
|
1283
|
+
continuedFromRunId: continuationRunId,
|
|
1284
|
+
existingRunState: continueFromRun,
|
|
1285
|
+
});
|
|
1286
|
+
const launchSummary = toLaunchSummary(runState, continueFromRun ? "continue" : "run");
|
|
1287
|
+
const startedState = continueFromRun ? "resumed" : "started";
|
|
1288
|
+
|
|
1289
|
+
if (!shouldRunAsync) {
|
|
1290
|
+
let finalized: FinalizedRun;
|
|
1291
|
+
try {
|
|
1292
|
+
finalized = await launchRunInBackground(runState, taskForAgent);
|
|
1293
|
+
} catch (error: unknown) {
|
|
1294
|
+
finalized = finalizeRunError(runState, error);
|
|
1295
|
+
} finally {
|
|
1296
|
+
clearRunAbortState(runState);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const message =
|
|
1300
|
+
finalized.result?.exitCode === ESCALATION_EXIT_CODE
|
|
1301
|
+
? buildEscalationMessage(runState, finalized.rawOutput.replace(/^\[ESCALATION\]\s*/, ""), finalized.result)
|
|
1302
|
+
: buildRunCompletionMessage(finalized);
|
|
1303
|
+
cleanupRunAfterFinalDelivery(runState.id);
|
|
1304
|
+
trimCommandRunHistory(store, {
|
|
1305
|
+
maxRuns: 10,
|
|
1306
|
+
ctx,
|
|
1307
|
+
pi,
|
|
1308
|
+
updateWidget: false,
|
|
1309
|
+
removalReason: "trim",
|
|
1310
|
+
});
|
|
1311
|
+
updateCommandRunsWidget(store);
|
|
1312
|
+
|
|
1313
|
+
return {
|
|
1314
|
+
content: [{ type: "text", text: withIdleRunWarning(message.content) }],
|
|
1315
|
+
details: makeDetails("single", finalized.result ? [finalized.result] : [], [launchSummary]),
|
|
1316
|
+
isError: finalized.isError,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
pi.sendMessage(buildRunStartMessage(runState, startedState), { deliverAs: "followUp", triggerTurn: false });
|
|
1321
|
+
ctx.ui?.notify?.(
|
|
1322
|
+
`${continueFromRun ? `Resumed subagent #${runState.id}` : `Started subagent #${runState.id}`}: ${resolvedAgent}`,
|
|
1323
|
+
"info",
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
void (async () => {
|
|
1327
|
+
try {
|
|
1328
|
+
const finalized = await launchRunInBackground(runState, taskForAgent);
|
|
1329
|
+
if (runState.removed || store.disposed) return;
|
|
1330
|
+
updateCommandRunsWidget(store);
|
|
1331
|
+
|
|
1332
|
+
if (finalized.result?.exitCode === ESCALATION_EXIT_CODE) {
|
|
1333
|
+
const escalationMsg = finalized.rawOutput.replace(/^\[ESCALATION\]\s*/, "");
|
|
1334
|
+
const message = buildEscalationMessage(runState, escalationMsg, finalized.result);
|
|
1335
|
+
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1336
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1337
|
+
cleanupRunAfterFinalDelivery(runState.id);
|
|
1338
|
+
} else {
|
|
1339
|
+
const entry = store.globalLiveRuns.get(runState.id);
|
|
1340
|
+
if (entry) entry.pendingCompletion = makePendingCompletion(message, true);
|
|
1341
|
+
}
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const completionMessage = buildRunCompletionMessage(finalized);
|
|
1346
|
+
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1347
|
+
pi.sendMessage(completionMessage, { deliverAs: "followUp", triggerTurn: true });
|
|
1348
|
+
cleanupRunAfterFinalDelivery(runState.id);
|
|
1349
|
+
} else {
|
|
1350
|
+
const entry = store.globalLiveRuns.get(runState.id);
|
|
1351
|
+
if (entry) entry.pendingCompletion = makePendingCompletion(completionMessage, true);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
ctx.ui?.notify?.(
|
|
1355
|
+
finalized.isError
|
|
1356
|
+
? `subagent tool run #${runState.id} (${resolvedAgent}) failed`
|
|
1357
|
+
: `subagent tool run #${runState.id} (${resolvedAgent}) completed`,
|
|
1358
|
+
finalized.isError ? "error" : "info",
|
|
1359
|
+
);
|
|
1360
|
+
} catch (error: unknown) {
|
|
1361
|
+
if (runState.removed || store.disposed) return;
|
|
1362
|
+
const finalized = finalizeRunError(runState, error);
|
|
1363
|
+
const errorMessage = buildRunCompletionMessage(finalized);
|
|
1364
|
+
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1365
|
+
pi.sendMessage(errorMessage, { deliverAs: "followUp", triggerTurn: true });
|
|
1366
|
+
cleanupRunAfterFinalDelivery(runState.id);
|
|
1367
|
+
} else {
|
|
1368
|
+
const entry = store.globalLiveRuns.get(runState.id);
|
|
1369
|
+
if (entry) entry.pendingCompletion = makePendingCompletion(errorMessage, true);
|
|
1370
|
+
}
|
|
1371
|
+
ctx.ui?.notify?.(`subagent tool run #${runState.id} failed: ${runState.lastLine}`, "error");
|
|
1372
|
+
updateCommandRunsWidget(store);
|
|
1373
|
+
} finally {
|
|
1374
|
+
clearRunAbortState(runState);
|
|
1375
|
+
if (!store.disposed) {
|
|
1376
|
+
trimCommandRunHistory(store, {
|
|
1377
|
+
maxRuns: 10,
|
|
1378
|
+
ctx,
|
|
1379
|
+
pi,
|
|
1380
|
+
updateWidget: false,
|
|
1381
|
+
removalReason: "trim",
|
|
1382
|
+
});
|
|
1383
|
+
updateCommandRunsWidget(store);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
})();
|
|
1387
|
+
|
|
1388
|
+
return {
|
|
1389
|
+
content: [
|
|
1390
|
+
{
|
|
1391
|
+
type: "text",
|
|
1392
|
+
text: withIdleRunWarning(
|
|
1393
|
+
`${continueFromRun ? `Resumed async subagent run #${runState.id}` : `Started async subagent run #${runState.id}`} (${resolvedAgent}). ${SUBAGENT_STRONG_WAIT_MESSAGE}`,
|
|
1394
|
+
),
|
|
1395
|
+
},
|
|
1396
|
+
],
|
|
1397
|
+
details: makeDetails("single", [], [launchSummary]),
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (hasBatch) {
|
|
1402
|
+
const runs = Array.isArray(cmdParams.runs) ? (cmdParams.runs as BatchOrChainItem[]) : [];
|
|
1403
|
+
if (runs.length < 2) {
|
|
1404
|
+
return {
|
|
1405
|
+
content: [{ type: "text", text: "batch requires at least 2 runs." }],
|
|
1406
|
+
details: makeDetails("batch"),
|
|
1407
|
+
isError: true,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
if (runs.length > MAX_BATCH_RUNS) {
|
|
1411
|
+
return {
|
|
1412
|
+
content: [{ type: "text", text: `batch supports at most ${MAX_BATCH_RUNS} runs.` }],
|
|
1413
|
+
details: makeDetails("batch"),
|
|
1414
|
+
isError: true,
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const batchId = `b_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1419
|
+
const runStates = runs.map((item, index) => {
|
|
1420
|
+
const taskForAgent = wrapTaskWithMainContext(
|
|
1421
|
+
item.task,
|
|
1422
|
+
stripTaskEchoFromMainContext(mainContextText, item.task),
|
|
1423
|
+
{ mainSessionFile, totalMessageCount },
|
|
1424
|
+
);
|
|
1425
|
+
const runState = registerRunLaunch({
|
|
1426
|
+
agent: item.agent,
|
|
1427
|
+
taskForDisplay: item.task,
|
|
1428
|
+
taskForAgent,
|
|
1429
|
+
inheritMainContext,
|
|
1430
|
+
originSessionFile,
|
|
1431
|
+
batchId,
|
|
1432
|
+
pipelineStepIndex: index,
|
|
1433
|
+
});
|
|
1434
|
+
return { runState, taskForAgent };
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
store.batchGroups.set(batchId, {
|
|
1438
|
+
batchId,
|
|
1439
|
+
runIds: runStates.map(({ runState }) => runState.id),
|
|
1440
|
+
completedRunIds: new Set(),
|
|
1441
|
+
failedRunIds: new Set(),
|
|
1442
|
+
originSessionFile,
|
|
1443
|
+
createdAt: Date.now(),
|
|
1444
|
+
pendingResults: new Map(),
|
|
1445
|
+
});
|
|
1446
|
+
updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
|
|
1447
|
+
|
|
1448
|
+
if (!shouldRunAsync) {
|
|
1449
|
+
const finalizedRuns = await Promise.all(
|
|
1450
|
+
runStates.map(async ({ runState, taskForAgent }) => {
|
|
1451
|
+
try {
|
|
1452
|
+
return await launchRunInBackground(runState, taskForAgent);
|
|
1453
|
+
} catch (error: unknown) {
|
|
1454
|
+
return finalizeRunError(runState, error);
|
|
1455
|
+
} finally {
|
|
1456
|
+
clearRunAbortState(runState);
|
|
1457
|
+
}
|
|
1458
|
+
}),
|
|
1459
|
+
);
|
|
1460
|
+
const orderedRuns = runStates.map(({ runState }) => runState);
|
|
1461
|
+
const hasError = finalizedRuns.some((finalized) => finalized.isError);
|
|
1462
|
+
const content = formatBatchSummary(batchId, orderedRuns, hasError ? "error" : "completed");
|
|
1463
|
+
for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
|
|
1464
|
+
clearPendingGroupCompletion("batch", batchId);
|
|
1465
|
+
store.batchGroups.delete(batchId);
|
|
1466
|
+
trimCommandRunHistory(store, {
|
|
1467
|
+
maxRuns: 10,
|
|
1468
|
+
ctx,
|
|
1469
|
+
pi,
|
|
1470
|
+
updateWidget: false,
|
|
1471
|
+
removalReason: "trim",
|
|
1472
|
+
});
|
|
1473
|
+
updateCommandRunsWidget(store);
|
|
1474
|
+
|
|
1475
|
+
return {
|
|
1476
|
+
content: [{ type: "text", text: withIdleRunWarning(content) }],
|
|
1477
|
+
details: makeDetails(
|
|
1478
|
+
"batch",
|
|
1479
|
+
finalizedRuns
|
|
1480
|
+
.map((finalized) => finalized.result)
|
|
1481
|
+
.filter((result): result is SingleResult => Boolean(result)),
|
|
1482
|
+
runStates.map(({ runState }) => toLaunchSummary(runState, "batch")),
|
|
1483
|
+
),
|
|
1484
|
+
isError: hasError,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
for (const { runState, taskForAgent } of runStates) {
|
|
1489
|
+
void (async () => {
|
|
1490
|
+
try {
|
|
1491
|
+
const finalized = await launchRunInBackground(runState, taskForAgent);
|
|
1492
|
+
|
|
1493
|
+
const batch = store.batchGroups.get(batchId);
|
|
1494
|
+
if (!batch) return;
|
|
1495
|
+
batch.completedRunIds.add(runState.id);
|
|
1496
|
+
if (finalized.isError) batch.failedRunIds.add(runState.id);
|
|
1497
|
+
batch.pendingResults.set(runState.id, finalized.rawOutput);
|
|
1498
|
+
updateCommandRunsWidget(store);
|
|
1499
|
+
|
|
1500
|
+
if (batch.completedRunIds.size === batch.runIds.length) {
|
|
1501
|
+
const orderedRuns = batch.runIds
|
|
1502
|
+
.map((runId) => store.commandRuns.get(runId))
|
|
1503
|
+
.filter((run): run is CommandRunState => Boolean(run));
|
|
1504
|
+
const batchTerminalStatus = batch.failedRunIds.size > 0 ? "error" : "completed";
|
|
1505
|
+
const content = formatBatchSummary(batchId, orderedRuns, batchTerminalStatus);
|
|
1506
|
+
const message = {
|
|
1507
|
+
customType: "subagent-tool" as const,
|
|
1508
|
+
content,
|
|
1509
|
+
display: true,
|
|
1510
|
+
details: {
|
|
1511
|
+
batchId,
|
|
1512
|
+
runIds: batch.runIds,
|
|
1513
|
+
status: batch.failedRunIds.size > 0 ? "error" : "done",
|
|
1514
|
+
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1515
|
+
},
|
|
1516
|
+
};
|
|
1517
|
+
if (isInOriginSession(ctx, batch.originSessionFile)) {
|
|
1518
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1519
|
+
clearPendingGroupCompletion("batch", batchId);
|
|
1520
|
+
for (const runId of batch.runIds) cleanupRunAfterFinalDelivery(runId);
|
|
1521
|
+
store.batchGroups.delete(batchId);
|
|
1522
|
+
} else {
|
|
1523
|
+
batch.pendingCompletion = makePendingCompletion(message, true);
|
|
1524
|
+
upsertPendingGroupCompletion({
|
|
1525
|
+
scope: "batch",
|
|
1526
|
+
groupId: batchId,
|
|
1527
|
+
originSessionFile: batch.originSessionFile,
|
|
1528
|
+
runIds: batch.runIds,
|
|
1529
|
+
pendingCompletion: batch.pendingCompletion,
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
trimCommandRunHistory(store, {
|
|
1533
|
+
maxRuns: 10,
|
|
1534
|
+
ctx,
|
|
1535
|
+
pi,
|
|
1536
|
+
updateWidget: false,
|
|
1537
|
+
removalReason: "trim",
|
|
1538
|
+
});
|
|
1539
|
+
ctx.ui?.notify?.(
|
|
1540
|
+
batch.failedRunIds.size > 0
|
|
1541
|
+
? `subagent batch ${batchId} finished with errors`
|
|
1542
|
+
: `subagent batch ${batchId} completed`,
|
|
1543
|
+
batch.failedRunIds.size > 0 ? "error" : "info",
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
} catch (error: unknown) {
|
|
1547
|
+
runState.status = "error";
|
|
1548
|
+
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
1549
|
+
runState.lastLine = error instanceof Error ? error.message : "Subagent execution failed";
|
|
1550
|
+
runState.lastOutput = runState.lastLine;
|
|
1551
|
+
const batch = store.batchGroups.get(batchId);
|
|
1552
|
+
if (!batch) return;
|
|
1553
|
+
batch.completedRunIds.add(runState.id);
|
|
1554
|
+
batch.failedRunIds.add(runState.id);
|
|
1555
|
+
batch.pendingResults.set(runState.id, runState.lastLine);
|
|
1556
|
+
if (batch.completedRunIds.size === batch.runIds.length) {
|
|
1557
|
+
const orderedRuns = batch.runIds
|
|
1558
|
+
.map((runId) => store.commandRuns.get(runId))
|
|
1559
|
+
.filter((run): run is CommandRunState => Boolean(run));
|
|
1560
|
+
const message = {
|
|
1561
|
+
customType: "subagent-tool" as const,
|
|
1562
|
+
content: formatBatchSummary(batchId, orderedRuns, "error"),
|
|
1563
|
+
display: true,
|
|
1564
|
+
details: {
|
|
1565
|
+
batchId,
|
|
1566
|
+
runIds: batch.runIds,
|
|
1567
|
+
status: "error",
|
|
1568
|
+
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1569
|
+
},
|
|
1570
|
+
};
|
|
1571
|
+
if (isInOriginSession(ctx, batch.originSessionFile)) {
|
|
1572
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1573
|
+
clearPendingGroupCompletion("batch", batchId);
|
|
1574
|
+
for (const runId of batch.runIds) cleanupRunAfterFinalDelivery(runId);
|
|
1575
|
+
store.batchGroups.delete(batchId);
|
|
1576
|
+
} else {
|
|
1577
|
+
batch.pendingCompletion = makePendingCompletion(message, true);
|
|
1578
|
+
upsertPendingGroupCompletion({
|
|
1579
|
+
scope: "batch",
|
|
1580
|
+
groupId: batchId,
|
|
1581
|
+
originSessionFile: batch.originSessionFile,
|
|
1582
|
+
runIds: batch.runIds,
|
|
1583
|
+
pendingCompletion: batch.pendingCompletion,
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
updateCommandRunsWidget(store);
|
|
1588
|
+
}
|
|
1589
|
+
})();
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
return {
|
|
1593
|
+
content: [
|
|
1594
|
+
{
|
|
1595
|
+
type: "text",
|
|
1596
|
+
text: withIdleRunWarning(
|
|
1597
|
+
`Started async subagent batch ${batchId} (${runStates.length} runs). ${SUBAGENT_STRONG_WAIT_MESSAGE}`,
|
|
1598
|
+
),
|
|
1599
|
+
},
|
|
1600
|
+
],
|
|
1601
|
+
details: makeDetails(
|
|
1602
|
+
"batch",
|
|
1603
|
+
[],
|
|
1604
|
+
runStates.map(({ runState }) => toLaunchSummary(runState, "batch")),
|
|
1605
|
+
),
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (hasChain) {
|
|
1610
|
+
const steps = Array.isArray(cmdParams.steps) ? (cmdParams.steps as BatchOrChainItem[]) : [];
|
|
1611
|
+
if (steps.length < 2) {
|
|
1612
|
+
return {
|
|
1613
|
+
content: [{ type: "text", text: "chain requires at least 2 steps." }],
|
|
1614
|
+
details: makeDetails("chain"),
|
|
1615
|
+
isError: true,
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
if (steps.length > MAX_CHAIN_STEPS) {
|
|
1619
|
+
return {
|
|
1620
|
+
content: [{ type: "text", text: `chain supports at most ${MAX_CHAIN_STEPS} steps.` }],
|
|
1621
|
+
details: makeDetails("chain"),
|
|
1622
|
+
isError: true,
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const pipelineId = `p_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1627
|
+
const chainLaunches: SubagentLaunchSummary[] = [];
|
|
1628
|
+
store.pipelines.set(pipelineId, {
|
|
1629
|
+
pipelineId,
|
|
1630
|
+
currentIndex: 0,
|
|
1631
|
+
stepRunIds: [],
|
|
1632
|
+
stepResults: [],
|
|
1633
|
+
originSessionFile,
|
|
1634
|
+
createdAt: Date.now(),
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
if (!shouldRunAsync) {
|
|
1638
|
+
let previousOutput = "";
|
|
1639
|
+
let terminalStatus: "completed" | "stopped" | "error" = "completed";
|
|
1640
|
+
const finalizedRuns: FinalizedRun[] = [];
|
|
1641
|
+
const pipeline = store.pipelines.get(pipelineId);
|
|
1642
|
+
if (!pipeline) {
|
|
1643
|
+
return {
|
|
1644
|
+
content: [{ type: "text", text: "Subagent chain failed to initialize." }],
|
|
1645
|
+
details: makeDetails("chain"),
|
|
1646
|
+
isError: true,
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
try {
|
|
1651
|
+
for (let index = 0; index < steps.length; index++) {
|
|
1652
|
+
pipeline.currentIndex = index;
|
|
1653
|
+
const step = steps[index];
|
|
1654
|
+
const pipelineReferenceSection =
|
|
1655
|
+
index > 0
|
|
1656
|
+
? buildPipelineReferenceSection(previousOutput, {
|
|
1657
|
+
agent: steps[index - 1]?.agent,
|
|
1658
|
+
task: steps[index - 1]?.task,
|
|
1659
|
+
stepNumber: index,
|
|
1660
|
+
totalSteps: steps.length,
|
|
1661
|
+
})
|
|
1662
|
+
: "";
|
|
1663
|
+
let taskForAgent = step.task;
|
|
1664
|
+
if (inheritMainContext) {
|
|
1665
|
+
taskForAgent = wrapTaskWithMainContext(
|
|
1666
|
+
step.task,
|
|
1667
|
+
stripTaskEchoFromMainContext(mainContextText, step.task),
|
|
1668
|
+
{
|
|
1669
|
+
mainSessionFile,
|
|
1670
|
+
totalMessageCount,
|
|
1671
|
+
referenceSections: pipelineReferenceSection ? [pipelineReferenceSection] : undefined,
|
|
1672
|
+
},
|
|
1673
|
+
);
|
|
1674
|
+
} else if (pipelineReferenceSection) {
|
|
1675
|
+
taskForAgent = wrapTaskWithPipelineContext(step.task, previousOutput, {
|
|
1676
|
+
agent: steps[index - 1]?.agent,
|
|
1677
|
+
task: steps[index - 1]?.task,
|
|
1678
|
+
stepNumber: index,
|
|
1679
|
+
totalSteps: steps.length,
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const runState = registerRunLaunch({
|
|
1684
|
+
agent: step.agent,
|
|
1685
|
+
taskForDisplay: step.task,
|
|
1686
|
+
taskForAgent,
|
|
1687
|
+
inheritMainContext,
|
|
1688
|
+
originSessionFile,
|
|
1689
|
+
pipelineId,
|
|
1690
|
+
pipelineStepIndex: index,
|
|
1691
|
+
});
|
|
1692
|
+
pipeline.stepRunIds.push(runState.id);
|
|
1693
|
+
chainLaunches.push(toLaunchSummary(runState, "chain"));
|
|
1694
|
+
|
|
1695
|
+
let finalized: FinalizedRun;
|
|
1696
|
+
try {
|
|
1697
|
+
finalized = await launchRunInBackground(runState, taskForAgent);
|
|
1698
|
+
} catch (error: unknown) {
|
|
1699
|
+
finalized = finalizeRunError(runState, error);
|
|
1700
|
+
} finally {
|
|
1701
|
+
clearRunAbortState(runState);
|
|
1702
|
+
}
|
|
1703
|
+
finalizedRuns.push(finalized);
|
|
1704
|
+
|
|
1705
|
+
if (runState.removed) {
|
|
1706
|
+
terminalStatus = finalized.isError ? "error" : "stopped";
|
|
1707
|
+
pipeline.stepResults.push({
|
|
1708
|
+
runId: runState.id,
|
|
1709
|
+
agent: runState.agent,
|
|
1710
|
+
task: step.task,
|
|
1711
|
+
output: finalized.rawOutput || "Run removed before pipeline completion.",
|
|
1712
|
+
status: "error",
|
|
1713
|
+
});
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
pipeline.stepResults.push({
|
|
1718
|
+
runId: runState.id,
|
|
1719
|
+
agent: runState.agent,
|
|
1720
|
+
task: step.task,
|
|
1721
|
+
output: finalized.rawOutput,
|
|
1722
|
+
status: finalized.isError ? "error" : "done",
|
|
1723
|
+
});
|
|
1724
|
+
previousOutput = finalized.rawOutput;
|
|
1725
|
+
updateCommandRunsWidget(store);
|
|
1726
|
+
|
|
1727
|
+
if (finalized.isError) {
|
|
1728
|
+
terminalStatus = "error";
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
} catch (error: unknown) {
|
|
1733
|
+
terminalStatus = "error";
|
|
1734
|
+
pipeline.stepResults.push({
|
|
1735
|
+
runId: -1,
|
|
1736
|
+
agent: "pipeline",
|
|
1737
|
+
task: "internal error",
|
|
1738
|
+
output: error instanceof Error ? error.message : "Subagent execution failed",
|
|
1739
|
+
status: "error",
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
1744
|
+
if (terminalStatus === "completed" && hasError) terminalStatus = "error";
|
|
1745
|
+
const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
|
|
1746
|
+
for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
|
|
1747
|
+
clearPendingGroupCompletion("chain", pipelineId);
|
|
1748
|
+
store.pipelines.delete(pipelineId);
|
|
1749
|
+
trimCommandRunHistory(store, {
|
|
1750
|
+
maxRuns: 10,
|
|
1751
|
+
ctx,
|
|
1752
|
+
pi,
|
|
1753
|
+
updateWidget: false,
|
|
1754
|
+
removalReason: "trim",
|
|
1755
|
+
});
|
|
1756
|
+
updateCommandRunsWidget(store);
|
|
1757
|
+
|
|
1758
|
+
return {
|
|
1759
|
+
content: [{ type: "text", text: withIdleRunWarning(content) }],
|
|
1760
|
+
details: makeDetails(
|
|
1761
|
+
"chain",
|
|
1762
|
+
finalizedRuns
|
|
1763
|
+
.map((finalized) => finalized.result)
|
|
1764
|
+
.filter((result): result is SingleResult => Boolean(result)),
|
|
1765
|
+
[...chainLaunches],
|
|
1766
|
+
),
|
|
1767
|
+
isError: terminalStatus !== "completed",
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
void (async () => {
|
|
1772
|
+
let previousOutput = "";
|
|
1773
|
+
let terminalStatus: "completed" | "stopped" | "error" = "completed";
|
|
1774
|
+
try {
|
|
1775
|
+
for (let index = 0; index < steps.length; index++) {
|
|
1776
|
+
const pipeline = store.pipelines.get(pipelineId);
|
|
1777
|
+
if (!pipeline) return;
|
|
1778
|
+
pipeline.currentIndex = index;
|
|
1779
|
+
|
|
1780
|
+
const step = steps[index];
|
|
1781
|
+
const pipelineReferenceSection =
|
|
1782
|
+
index > 0
|
|
1783
|
+
? buildPipelineReferenceSection(previousOutput, {
|
|
1784
|
+
agent: steps[index - 1]?.agent,
|
|
1785
|
+
task: steps[index - 1]?.task,
|
|
1786
|
+
stepNumber: index,
|
|
1787
|
+
totalSteps: steps.length,
|
|
1788
|
+
})
|
|
1789
|
+
: "";
|
|
1790
|
+
let taskForAgent = step.task;
|
|
1791
|
+
if (inheritMainContext) {
|
|
1792
|
+
taskForAgent = wrapTaskWithMainContext(
|
|
1793
|
+
step.task,
|
|
1794
|
+
stripTaskEchoFromMainContext(mainContextText, step.task),
|
|
1795
|
+
{
|
|
1796
|
+
mainSessionFile,
|
|
1797
|
+
totalMessageCount,
|
|
1798
|
+
referenceSections: pipelineReferenceSection ? [pipelineReferenceSection] : undefined,
|
|
1799
|
+
},
|
|
1800
|
+
);
|
|
1801
|
+
} else if (pipelineReferenceSection) {
|
|
1802
|
+
taskForAgent = wrapTaskWithPipelineContext(step.task, previousOutput, {
|
|
1803
|
+
agent: steps[index - 1]?.agent,
|
|
1804
|
+
task: steps[index - 1]?.task,
|
|
1805
|
+
stepNumber: index,
|
|
1806
|
+
totalSteps: steps.length,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
const runState = registerRunLaunch({
|
|
1811
|
+
agent: step.agent,
|
|
1812
|
+
taskForDisplay: step.task,
|
|
1813
|
+
taskForAgent,
|
|
1814
|
+
inheritMainContext,
|
|
1815
|
+
originSessionFile,
|
|
1816
|
+
pipelineId,
|
|
1817
|
+
pipelineStepIndex: index,
|
|
1818
|
+
});
|
|
1819
|
+
pipeline.stepRunIds.push(runState.id);
|
|
1820
|
+
chainLaunches.push(toLaunchSummary(runState, "chain"));
|
|
1821
|
+
|
|
1822
|
+
const finalized = await launchRunInBackground(runState, taskForAgent);
|
|
1823
|
+
if (runState.removed) {
|
|
1824
|
+
terminalStatus = finalized.isError ? "error" : "stopped";
|
|
1825
|
+
pipeline.stepResults.push({
|
|
1826
|
+
runId: runState.id,
|
|
1827
|
+
agent: runState.agent,
|
|
1828
|
+
task: step.task,
|
|
1829
|
+
output: finalized.rawOutput || "Run removed before pipeline completion.",
|
|
1830
|
+
status: "error",
|
|
1831
|
+
});
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const stepResult: PipelineStepResult = {
|
|
1836
|
+
runId: runState.id,
|
|
1837
|
+
agent: runState.agent,
|
|
1838
|
+
task: step.task,
|
|
1839
|
+
output: finalized.rawOutput,
|
|
1840
|
+
status: finalized.isError ? "error" : "done",
|
|
1841
|
+
};
|
|
1842
|
+
pipeline.stepResults.push(stepResult);
|
|
1843
|
+
previousOutput = finalized.rawOutput;
|
|
1844
|
+
updateCommandRunsWidget(store);
|
|
1845
|
+
|
|
1846
|
+
if (finalized.isError) {
|
|
1847
|
+
terminalStatus = "error";
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
} catch (error: unknown) {
|
|
1852
|
+
terminalStatus = "error";
|
|
1853
|
+
const pipeline = store.pipelines.get(pipelineId);
|
|
1854
|
+
if (pipeline) {
|
|
1855
|
+
pipeline.stepResults.push({
|
|
1856
|
+
runId: -1,
|
|
1857
|
+
agent: "pipeline",
|
|
1858
|
+
task: "internal error",
|
|
1859
|
+
output: error instanceof Error ? error.message : "Subagent execution failed",
|
|
1860
|
+
status: "error",
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
} finally {
|
|
1864
|
+
const pipeline = store.pipelines.get(pipelineId);
|
|
1865
|
+
if (pipeline) {
|
|
1866
|
+
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
1867
|
+
if (terminalStatus === "completed" && hasError) {
|
|
1868
|
+
terminalStatus = "error";
|
|
1869
|
+
}
|
|
1870
|
+
const orderedRuns = pipeline.stepRunIds
|
|
1871
|
+
.map((runId) => store.commandRuns.get(runId))
|
|
1872
|
+
.filter((run): run is CommandRunState => Boolean(run));
|
|
1873
|
+
const message = {
|
|
1874
|
+
customType: "subagent-tool" as const,
|
|
1875
|
+
content: formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus),
|
|
1876
|
+
display: true,
|
|
1877
|
+
details: {
|
|
1878
|
+
pipelineId,
|
|
1879
|
+
stepRunIds: pipeline.stepRunIds,
|
|
1880
|
+
status: terminalStatus === "completed" ? "done" : terminalStatus,
|
|
1881
|
+
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1882
|
+
},
|
|
1883
|
+
};
|
|
1884
|
+
if (isInOriginSession(ctx, pipeline.originSessionFile)) {
|
|
1885
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1886
|
+
clearPendingGroupCompletion("chain", pipelineId);
|
|
1887
|
+
for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
|
|
1888
|
+
store.pipelines.delete(pipelineId);
|
|
1889
|
+
} else {
|
|
1890
|
+
pipeline.pendingCompletion = makePendingCompletion(message, true);
|
|
1891
|
+
upsertPendingGroupCompletion({
|
|
1892
|
+
scope: "chain",
|
|
1893
|
+
groupId: pipelineId,
|
|
1894
|
+
originSessionFile: pipeline.originSessionFile,
|
|
1895
|
+
runIds: pipeline.stepRunIds,
|
|
1896
|
+
pendingCompletion: pipeline.pendingCompletion,
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
trimCommandRunHistory(store, {
|
|
1900
|
+
maxRuns: 10,
|
|
1901
|
+
ctx,
|
|
1902
|
+
pi,
|
|
1903
|
+
updateWidget: false,
|
|
1904
|
+
removalReason: "trim",
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
updateCommandRunsWidget(store);
|
|
1908
|
+
}
|
|
1909
|
+
})();
|
|
1910
|
+
|
|
1911
|
+
return {
|
|
1912
|
+
content: [
|
|
1913
|
+
{
|
|
1914
|
+
type: "text",
|
|
1915
|
+
text: withIdleRunWarning(
|
|
1916
|
+
`Started async subagent chain ${pipelineId} (${steps.length} steps). ${SUBAGENT_STRONG_WAIT_MESSAGE}`,
|
|
1917
|
+
),
|
|
1918
|
+
},
|
|
1919
|
+
],
|
|
1920
|
+
details: makeDetails("chain", [], [...chainLaunches]),
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
return {
|
|
1925
|
+
content: [{ type: "text", text: withIdleRunWarning("Invalid subagent invocation.") }],
|
|
1926
|
+
details: makeDetails(),
|
|
1927
|
+
isError: true,
|
|
1928
|
+
};
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
return execute;
|
|
1932
|
+
}
|