pi-subagents 0.24.4 → 0.27.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 +29 -0
- package/README.md +145 -27
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +71 -20
- package/src/agents/agent-management.ts +57 -15
- package/src/agents/agent-serializer.ts +3 -2
- package/src/agents/agents.ts +47 -16
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +171 -0
- package/src/extension/index.ts +7 -2
- package/src/extension/schemas.ts +138 -5
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +185 -10
- package/src/runs/background/async-job-tracker.ts +41 -6
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +71 -31
- package/src/runs/background/result-watcher.ts +111 -54
- package/src/runs/background/run-id-resolver.ts +83 -0
- package/src/runs/background/run-status.ts +89 -4
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +648 -42
- package/src/runs/foreground/chain-execution.ts +331 -118
- package/src/runs/foreground/execution.ts +226 -10
- package/src/runs/foreground/subagent-executor.ts +377 -14
- package/src/runs/shared/acceptance-contract.ts +291 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +161 -0
- package/src/runs/shared/acceptance-reports.ts +127 -0
- package/src/runs/shared/acceptance.ts +22 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +26 -3
- package/src/runs/shared/dynamic-fanout.ts +293 -0
- package/src/runs/shared/nested-events.ts +819 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +115 -0
- package/src/runs/shared/parallel-utils.ts +31 -1
- package/src/runs/shared/pi-args.ts +73 -5
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
- package/src/runs/shared/workflow-graph.ts +206 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +345 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +268 -43
|
@@ -2,14 +2,20 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
5
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
6
6
|
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot } from "../shared/nested-events.ts";
|
|
8
|
+
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
7
9
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
8
|
-
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
10
|
+
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
9
11
|
|
|
10
12
|
interface AsyncRunStepSummary {
|
|
11
13
|
index: number;
|
|
12
14
|
agent: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
phase?: string;
|
|
17
|
+
outputName?: string;
|
|
18
|
+
structured?: boolean;
|
|
13
19
|
status: AsyncJobStep["status"];
|
|
14
20
|
activityState?: ActivityState;
|
|
15
21
|
lastActivityAt?: number;
|
|
@@ -28,6 +34,7 @@ interface AsyncRunStepSummary {
|
|
|
28
34
|
thinking?: string;
|
|
29
35
|
attemptedModels?: string[];
|
|
30
36
|
error?: string;
|
|
37
|
+
children?: NestedRunSummary[];
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export interface AsyncRunSummary {
|
|
@@ -55,6 +62,8 @@ export interface AsyncRunSummary {
|
|
|
55
62
|
outputFile?: string;
|
|
56
63
|
totalTokens?: TokenUsage;
|
|
57
64
|
sessionFile?: string;
|
|
65
|
+
nestedChildren?: NestedRunSummary[];
|
|
66
|
+
nestedWarnings?: string[];
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
interface AsyncRunListOptions {
|
|
@@ -112,7 +121,7 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
|
|
|
112
121
|
};
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
|
|
124
|
+
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = []): AsyncRunSummary {
|
|
116
125
|
if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
|
|
117
126
|
throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
|
|
118
127
|
}
|
|
@@ -120,6 +129,46 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
120
129
|
const steps = status.steps ?? [];
|
|
121
130
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
122
131
|
const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
132
|
+
let nestedChildren: NestedRunSummary[] = [];
|
|
133
|
+
if (nestedWarnings.length === 0) {
|
|
134
|
+
try {
|
|
135
|
+
nestedChildren = projectNestedRegistryForRoot(status.runId || path.basename(asyncDir))?.children ?? [];
|
|
136
|
+
} catch (error) {
|
|
137
|
+
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const summarizedSteps = steps.map((step, index) => {
|
|
141
|
+
const stepActivityState = step.activityState;
|
|
142
|
+
const stepLastActivityAt = step.lastActivityAt;
|
|
143
|
+
return {
|
|
144
|
+
index,
|
|
145
|
+
agent: step.agent,
|
|
146
|
+
...(step.label ? { label: step.label } : {}),
|
|
147
|
+
...(step.phase ? { phase: step.phase } : {}),
|
|
148
|
+
...(step.outputName ? { outputName: step.outputName } : {}),
|
|
149
|
+
...(step.structured ? { structured: step.structured } : {}),
|
|
150
|
+
status: step.status,
|
|
151
|
+
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
152
|
+
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
153
|
+
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
154
|
+
...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
|
|
155
|
+
...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
156
|
+
...(step.currentPath ? { currentPath: step.currentPath } : {}),
|
|
157
|
+
...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
|
|
158
|
+
...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
|
|
159
|
+
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
160
|
+
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
161
|
+
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
162
|
+
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
163
|
+
...(step.skills ? { skills: step.skills } : {}),
|
|
164
|
+
...(step.model ? { model: step.model } : {}),
|
|
165
|
+
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
166
|
+
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
167
|
+
...(step.error ? { error: step.error } : {}),
|
|
168
|
+
...(step.children?.length ? { children: step.children } : {}),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
attachRootChildrenToSteps(status.runId || path.basename(asyncDir), summarizedSteps, nestedChildren);
|
|
123
172
|
return {
|
|
124
173
|
id: status.runId || path.basename(asyncDir),
|
|
125
174
|
asyncDir,
|
|
@@ -140,32 +189,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
140
189
|
currentStep: status.currentStep,
|
|
141
190
|
...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
|
|
142
191
|
...(parallelGroups.length ? { parallelGroups } : {}),
|
|
143
|
-
steps:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
index,
|
|
148
|
-
agent: step.agent,
|
|
149
|
-
status: step.status,
|
|
150
|
-
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
151
|
-
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
152
|
-
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
153
|
-
...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
|
|
154
|
-
...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
155
|
-
...(step.currentPath ? { currentPath: step.currentPath } : {}),
|
|
156
|
-
...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
|
|
157
|
-
...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
|
|
158
|
-
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
159
|
-
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
160
|
-
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
161
|
-
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
162
|
-
...(step.skills ? { skills: step.skills } : {}),
|
|
163
|
-
...(step.model ? { model: step.model } : {}),
|
|
164
|
-
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
165
|
-
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
166
|
-
...(step.error ? { error: step.error } : {}),
|
|
167
|
-
};
|
|
168
|
-
}),
|
|
192
|
+
steps: summarizedSteps,
|
|
193
|
+
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
194
|
+
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
169
195
|
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
170
196
|
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
171
197
|
...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
|
|
@@ -212,7 +238,14 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
212
238
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
213
239
|
const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
214
240
|
if (!status) continue;
|
|
215
|
-
const
|
|
241
|
+
const nestedWarnings: string[] = [];
|
|
242
|
+
try {
|
|
243
|
+
const nestedRoute = findNestedRouteForRootId(status.runId || path.basename(asyncDir));
|
|
244
|
+
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
245
|
+
} catch (error) {
|
|
246
|
+
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
247
|
+
}
|
|
248
|
+
const summary = statusToSummary(asyncDir, status, nestedWarnings);
|
|
216
249
|
if (allowedStates && !allowedStates.has(summary.state)) continue;
|
|
217
250
|
if (options.sessionId && summary.sessionId !== options.sessionId) continue;
|
|
218
251
|
runs.push(summary);
|
|
@@ -234,7 +267,9 @@ function formatActivityFacts(input: { activityState?: ActivityState; lastActivit
|
|
|
234
267
|
}
|
|
235
268
|
|
|
236
269
|
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
237
|
-
const
|
|
270
|
+
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
271
|
+
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
272
|
+
const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
|
|
238
273
|
const activity = formatActivityFacts(step);
|
|
239
274
|
if (activity) parts.push(activity);
|
|
240
275
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
@@ -285,7 +320,12 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
|
|
|
285
320
|
lines.push(`- ${formatRunHeader(run)}`);
|
|
286
321
|
for (const step of run.steps) {
|
|
287
322
|
lines.push(` ${formatStepLine(step)}`);
|
|
323
|
+
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
|
|
288
324
|
}
|
|
325
|
+
const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
326
|
+
const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
327
|
+
lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
|
|
328
|
+
for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
|
|
289
329
|
const outputPath = formatAsyncRunOutputPath(run);
|
|
290
330
|
if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
|
|
291
331
|
if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
|
|
@@ -5,13 +5,18 @@ import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
|
5
5
|
import {
|
|
6
6
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
7
7
|
type IntercomEventBus,
|
|
8
|
+
type NestedRunSummary,
|
|
9
|
+
type SubagentResultIntercomChild,
|
|
8
10
|
type SubagentState,
|
|
9
11
|
} from "../../shared/types.ts";
|
|
10
12
|
import {
|
|
13
|
+
attachNestedChildrenToResultChildren,
|
|
11
14
|
buildSubagentResultIntercomPayload,
|
|
15
|
+
compactNestedResultChildren,
|
|
12
16
|
deliverSubagentResultIntercomEvent,
|
|
13
17
|
resolveSubagentResultStatus,
|
|
14
18
|
} from "../../intercom/result-intercom.ts";
|
|
19
|
+
import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
|
|
15
20
|
|
|
16
21
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
17
22
|
const POLL_INTERVAL_MS = 3000;
|
|
@@ -30,6 +35,47 @@ type ResultWatcherDeps = {
|
|
|
30
35
|
timers?: ResultWatcherTimers;
|
|
31
36
|
};
|
|
32
37
|
|
|
38
|
+
type ResultFileChild = {
|
|
39
|
+
agent?: string;
|
|
40
|
+
output?: string;
|
|
41
|
+
error?: string;
|
|
42
|
+
success?: boolean;
|
|
43
|
+
sessionFile?: string;
|
|
44
|
+
artifactPaths?: { outputPath?: string };
|
|
45
|
+
intercomTarget?: string;
|
|
46
|
+
children?: unknown;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type ResultFileData = {
|
|
50
|
+
id?: string;
|
|
51
|
+
runId?: string;
|
|
52
|
+
agent?: string;
|
|
53
|
+
success?: boolean;
|
|
54
|
+
state?: string;
|
|
55
|
+
mode?: string;
|
|
56
|
+
summary?: string;
|
|
57
|
+
results?: ResultFileChild[];
|
|
58
|
+
nestedChildren?: unknown;
|
|
59
|
+
sessionId?: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
sessionFile?: string;
|
|
62
|
+
asyncDir?: string;
|
|
63
|
+
intercomTarget?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function sanitizeNestedResultChildren(value: unknown, resultPath: string, label: string): NestedRunSummary[] | undefined {
|
|
67
|
+
if (value === undefined) return undefined;
|
|
68
|
+
if (!Array.isArray(value)) {
|
|
69
|
+
console.error(`Ignoring invalid nested children in subagent result file '${resultPath}' at ${label}: expected an array.`);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const children = value.map((child) => sanitizeSummary(child)).filter((child): child is NestedRunSummary => Boolean(child));
|
|
73
|
+
if (children.length !== value.length) {
|
|
74
|
+
console.error(`Ignoring ${value.length - children.length} invalid nested child record(s) in subagent result file '${resultPath}' at ${label}.`);
|
|
75
|
+
}
|
|
76
|
+
return children.length ? children : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
33
79
|
function getErrorCode(error: unknown): string | undefined {
|
|
34
80
|
return typeof error === "object" && error !== null && "code" in error
|
|
35
81
|
? (error as NodeJS.ErrnoException).code
|
|
@@ -63,32 +109,21 @@ export function createResultWatcher(
|
|
|
63
109
|
const resultPath = path.join(resultsDir, file);
|
|
64
110
|
if (!fsApi.existsSync(resultPath)) return;
|
|
65
111
|
try {
|
|
66
|
-
const data = JSON.parse(fsApi.readFileSync(resultPath, "utf-8")) as
|
|
67
|
-
id?: string;
|
|
68
|
-
runId?: string;
|
|
69
|
-
agent?: string;
|
|
70
|
-
success?: boolean;
|
|
71
|
-
state?: string;
|
|
72
|
-
mode?: string;
|
|
73
|
-
summary?: string;
|
|
74
|
-
results?: Array<{
|
|
75
|
-
agent?: string;
|
|
76
|
-
output?: string;
|
|
77
|
-
error?: string;
|
|
78
|
-
success?: boolean;
|
|
79
|
-
sessionFile?: string;
|
|
80
|
-
artifactPaths?: { outputPath?: string };
|
|
81
|
-
intercomTarget?: string;
|
|
82
|
-
}>;
|
|
83
|
-
sessionId?: string;
|
|
84
|
-
cwd?: string;
|
|
85
|
-
sessionFile?: string;
|
|
86
|
-
asyncDir?: string;
|
|
87
|
-
intercomTarget?: string;
|
|
88
|
-
};
|
|
112
|
+
const data = JSON.parse(fsApi.readFileSync(resultPath, "utf-8")) as ResultFileData;
|
|
89
113
|
if (data.sessionId && data.sessionId !== state.currentSessionId) return;
|
|
90
114
|
if (!data.sessionId && data.cwd && (!state.baseCwd || data.cwd !== state.baseCwd)) return;
|
|
91
115
|
|
|
116
|
+
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
117
|
+
const hasExplicitNestedChildren = data.nestedChildren !== undefined;
|
|
118
|
+
let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
|
|
119
|
+
if (!nestedChildren?.length && !hasExplicitNestedChildren) {
|
|
120
|
+
try {
|
|
121
|
+
nestedChildren = compactNestedResultChildren(projectNestedRegistryForRoot(runId)?.children);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Failed to enrich subagent result file '${resultPath}' with nested registry children; will retry later:`, error);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
92
127
|
const now = Date.now();
|
|
93
128
|
const completionKey = buildCompletionKey(data, `result:${file}`);
|
|
94
129
|
if (markSeenWithTtl(state.completionSeen, completionKey, now, completionTtlMs)) {
|
|
@@ -96,45 +131,49 @@ export function createResultWatcher(
|
|
|
96
131
|
return;
|
|
97
132
|
}
|
|
98
133
|
|
|
134
|
+
const hasResultChildren = Array.isArray(data.results) && data.results.length > 0;
|
|
135
|
+
const resultChildren = hasResultChildren
|
|
136
|
+
? data.results!
|
|
137
|
+
: [{
|
|
138
|
+
agent: data.agent,
|
|
139
|
+
output: data.summary,
|
|
140
|
+
success: data.success,
|
|
141
|
+
}];
|
|
142
|
+
const normalizedChildren = attachNestedChildrenToResultChildren(runId, resultChildren.map((result = {}, index): SubagentResultIntercomChild => {
|
|
143
|
+
const baseOutput = result.output ?? data.summary;
|
|
144
|
+
const hasRealOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
|
|
145
|
+
const output = hasRealOutput ? baseOutput : "(no output)";
|
|
146
|
+
const summary = result.success === false && result.error
|
|
147
|
+
? `${result.error}${hasRealOutput ? `\n\nOutput:\n${baseOutput}` : ""}`
|
|
148
|
+
: output;
|
|
149
|
+
const sessionPath = result.sessionFile ?? (resultChildren.length === 1 ? data.sessionFile : undefined);
|
|
150
|
+
const childNestedChildren = sanitizeNestedResultChildren(result.children, resultPath, `results[${index}].children`);
|
|
151
|
+
return {
|
|
152
|
+
agent: result.agent ?? data.agent ?? `step-${index + 1}`,
|
|
153
|
+
status: resolveSubagentResultStatus({
|
|
154
|
+
success: result.success,
|
|
155
|
+
state: data.state === "paused" || typeof result.success !== "boolean" ? data.state : undefined,
|
|
156
|
+
}),
|
|
157
|
+
summary,
|
|
158
|
+
index,
|
|
159
|
+
artifactPath: result.artifactPaths?.outputPath,
|
|
160
|
+
...(typeof sessionPath === "string" && fsApi.existsSync(sessionPath) ? { sessionPath } : {}),
|
|
161
|
+
...(result.intercomTarget ? { intercomTarget: result.intercomTarget } : {}),
|
|
162
|
+
...(childNestedChildren ? { children: childNestedChildren } : {}),
|
|
163
|
+
};
|
|
164
|
+
}), nestedChildren);
|
|
165
|
+
|
|
99
166
|
const intercomTarget = data.intercomTarget?.trim();
|
|
100
167
|
if (intercomTarget) {
|
|
101
|
-
const childResults = Array.isArray(data.results) && data.results.length > 0
|
|
102
|
-
? data.results
|
|
103
|
-
: [{
|
|
104
|
-
agent: data.agent,
|
|
105
|
-
output: data.summary,
|
|
106
|
-
success: data.success,
|
|
107
|
-
}];
|
|
108
|
-
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
109
168
|
const mode = data.mode === "single" || data.mode === "parallel" || data.mode === "chain"
|
|
110
169
|
? data.mode
|
|
111
|
-
:
|
|
170
|
+
: resultChildren.length > 1 ? "chain" : "single";
|
|
112
171
|
const payload = buildSubagentResultIntercomPayload({
|
|
113
172
|
to: intercomTarget,
|
|
114
173
|
runId,
|
|
115
174
|
mode,
|
|
116
175
|
source: "async",
|
|
117
|
-
children:
|
|
118
|
-
const baseOutput = result.output ?? data.summary;
|
|
119
|
-
const hasRealOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
|
|
120
|
-
const output = hasRealOutput ? baseOutput : "(no output)";
|
|
121
|
-
const summary = result.success === false && result.error
|
|
122
|
-
? `${result.error}${hasRealOutput ? `\n\nOutput:\n${baseOutput}` : ""}`
|
|
123
|
-
: output;
|
|
124
|
-
const sessionPath = result.sessionFile ?? (childResults.length === 1 ? data.sessionFile : undefined);
|
|
125
|
-
return {
|
|
126
|
-
agent: result.agent ?? data.agent ?? `step-${index + 1}`,
|
|
127
|
-
status: resolveSubagentResultStatus({
|
|
128
|
-
success: result.success,
|
|
129
|
-
state: data.state === "paused" || typeof result.success !== "boolean" ? data.state : undefined,
|
|
130
|
-
}),
|
|
131
|
-
summary,
|
|
132
|
-
index,
|
|
133
|
-
artifactPath: result.artifactPaths?.outputPath,
|
|
134
|
-
...(typeof sessionPath === "string" && fsApi.existsSync(sessionPath) ? { sessionPath } : {}),
|
|
135
|
-
intercomTarget: result.intercomTarget,
|
|
136
|
-
};
|
|
137
|
-
}),
|
|
176
|
+
children: normalizedChildren,
|
|
138
177
|
asyncId: data.id,
|
|
139
178
|
asyncDir: data.asyncDir,
|
|
140
179
|
});
|
|
@@ -144,7 +183,25 @@ export function createResultWatcher(
|
|
|
144
183
|
}
|
|
145
184
|
}
|
|
146
185
|
|
|
147
|
-
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
186
|
+
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
|
|
187
|
+
...data,
|
|
188
|
+
runId,
|
|
189
|
+
...(nestedChildren?.length ? { nestedChildren } : {}),
|
|
190
|
+
...(Array.isArray(data.results) ? {
|
|
191
|
+
results: hasResultChildren
|
|
192
|
+
? normalizedChildren.map((child, index) => ({
|
|
193
|
+
...data.results![index],
|
|
194
|
+
agent: child.agent,
|
|
195
|
+
status: child.status,
|
|
196
|
+
summary: child.summary,
|
|
197
|
+
index: child.index,
|
|
198
|
+
artifactPath: child.artifactPath,
|
|
199
|
+
sessionPath: child.sessionPath,
|
|
200
|
+
children: child.children,
|
|
201
|
+
}))
|
|
202
|
+
: [],
|
|
203
|
+
} : {}),
|
|
204
|
+
});
|
|
148
205
|
fsApi.unlinkSync(resultPath);
|
|
149
206
|
} catch (error) {
|
|
150
207
|
if (isNotFoundError(error)) return;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { ASYNC_DIR, RESULTS_DIR, type SubagentState } from "../../shared/types.ts";
|
|
4
|
+
import { findAsyncRunPrefixMatches, type AsyncRunLocation } from "./async-resume.ts";
|
|
5
|
+
import { assertSafeNestedId, findNestedRunMatchesById, type NestedRoute, type NestedRunMatch, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
6
|
+
|
|
7
|
+
export type ResolvedSubagentRunId =
|
|
8
|
+
| { kind: "foreground"; id: string }
|
|
9
|
+
| { kind: "async"; id: string; location: AsyncRunLocation }
|
|
10
|
+
| { kind: "nested"; id: string; match: NestedRunMatch };
|
|
11
|
+
|
|
12
|
+
export interface ResolveSubagentRunIdDeps {
|
|
13
|
+
state?: SubagentState;
|
|
14
|
+
asyncDirRoot?: string;
|
|
15
|
+
resultsDir?: string;
|
|
16
|
+
nested?: NestedRunResolutionScope;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string): AsyncRunLocation | undefined {
|
|
20
|
+
const asyncDir = path.join(asyncDirRoot, id);
|
|
21
|
+
const resultPath = path.join(resultsDir, `${id}.json`);
|
|
22
|
+
if (!fs.existsSync(asyncDir) && !fs.existsSync(resultPath)) return undefined;
|
|
23
|
+
return {
|
|
24
|
+
asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
|
|
25
|
+
resultPath: fs.existsSync(resultPath) ? resultPath : null,
|
|
26
|
+
resolvedId: id,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function foregroundIds(state: SubagentState | undefined): string[] {
|
|
31
|
+
return state ? [...state.foregroundControls.keys()] : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
|
|
35
|
+
if (!state) return undefined;
|
|
36
|
+
const routes: NestedRoute[] = [];
|
|
37
|
+
const seen = new Set<string>();
|
|
38
|
+
const add = (route: NestedRoute | undefined) => {
|
|
39
|
+
if (!route) return;
|
|
40
|
+
const key = `${route.rootRunId}:${route.eventSink}:${route.controlInbox}`;
|
|
41
|
+
if (seen.has(key)) return;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
routes.push(route);
|
|
44
|
+
};
|
|
45
|
+
for (const control of state.foregroundControls.values()) add(control.nestedRoute as NestedRoute | undefined);
|
|
46
|
+
for (const job of state.asyncJobs.values()) add(job.nestedRoute as NestedRoute | undefined);
|
|
47
|
+
return { routes };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function asyncPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
|
|
51
|
+
return findAsyncRunPrefixMatches(prefix, asyncDirRoot, resultsDir);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps = {}): ResolvedSubagentRunId | undefined {
|
|
55
|
+
assertSafeNestedId("id", id);
|
|
56
|
+
const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
|
|
57
|
+
const resultsDir = deps.resultsDir ?? RESULTS_DIR;
|
|
58
|
+
|
|
59
|
+
const nestedScope = deps.nested ?? nestedScopeFromState(deps.state);
|
|
60
|
+
if (deps.state?.foregroundControls.has(id)) return { kind: "foreground", id };
|
|
61
|
+
const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
|
|
62
|
+
if (exactAsync) return { kind: "async", id, location: exactAsync };
|
|
63
|
+
const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
|
|
64
|
+
if (exactNested.length > 1) throw new Error(`Nested run id '${id}' is ambiguous across authorized registries. Provide the full id after stale registries are cleaned up.`);
|
|
65
|
+
if (exactNested[0]) return { kind: "nested", id, match: exactNested[0] };
|
|
66
|
+
|
|
67
|
+
const matches: ResolvedSubagentRunId[] = [];
|
|
68
|
+
for (const foregroundId of foregroundIds(deps.state).filter((candidate) => candidate.startsWith(id))) {
|
|
69
|
+
matches.push({ kind: "foreground", id: foregroundId });
|
|
70
|
+
}
|
|
71
|
+
for (const match of asyncPrefixMatches(id, asyncDirRoot, resultsDir)) {
|
|
72
|
+
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
73
|
+
}
|
|
74
|
+
for (const match of findNestedRunMatchesById(id, nestedScope ? { prefix: true, scope: nestedScope } : { prefix: true })) {
|
|
75
|
+
matches.push({ kind: "nested", id: match.run.id, match });
|
|
76
|
+
}
|
|
77
|
+
const unique = new Map(matches.map((match) => [`${match.kind}:${match.id}`, match]));
|
|
78
|
+
const values = [...unique.values()];
|
|
79
|
+
if (values.length > 1) {
|
|
80
|
+
throw new Error(`Ambiguous subagent run id prefix '${id}' matched: ${values.map((match) => `${match.kind}:${match.id}`).join(", ")}. Provide a longer id.`);
|
|
81
|
+
}
|
|
82
|
+
return values[0];
|
|
83
|
+
}
|
|
@@ -2,13 +2,16 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
|
|
5
|
+
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
5
6
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
6
7
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
7
|
-
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details } from "../../shared/types.ts";
|
|
8
|
+
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details, type NestedRunSummary, type SubagentState } from "../../shared/types.ts";
|
|
8
9
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
9
10
|
import { resolveAsyncRunLocation } from "./async-resume.ts";
|
|
11
|
+
import { resolveSubagentRunId } from "./run-id-resolver.ts";
|
|
10
12
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
11
|
-
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
13
|
+
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
14
|
+
import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
12
15
|
|
|
13
16
|
interface RunStatusParams {
|
|
14
17
|
action?: "status";
|
|
@@ -22,6 +25,8 @@ interface RunStatusDeps {
|
|
|
22
25
|
resultsDir?: string;
|
|
23
26
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
24
27
|
now?: () => number;
|
|
28
|
+
state?: SubagentState;
|
|
29
|
+
nested?: NestedRunResolutionScope;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
function hasExistingSessionFile(value: unknown): value is string {
|
|
@@ -44,6 +49,11 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
|
|
|
44
49
|
return "Resume: unavailable; no child session file was persisted.";
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function formatAcceptanceFinalizationSummary(finalization: NonNullable<NonNullable<AsyncStatus["steps"]>[number]["acceptance"]>["finalization"] | undefined): string {
|
|
53
|
+
if (!finalization) return "";
|
|
54
|
+
return `, finalization: ${finalization.status} after ${finalization.turns.length}/${finalization.maxTurns} turns`;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
function stepLineLabel(status: AsyncStatus, index: number): string {
|
|
48
58
|
const steps = status.steps ?? [];
|
|
49
59
|
if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
|
|
@@ -57,10 +67,53 @@ function stepLineLabel(status: AsyncStatus, index: number): string {
|
|
|
57
67
|
return `Step ${index + 1}`;
|
|
58
68
|
}
|
|
59
69
|
|
|
70
|
+
function nestedRunDisplayName(run: NestedRunSummary): string {
|
|
71
|
+
if (run.agent) return run.agent;
|
|
72
|
+
if (run.agents?.length) return run.agents.join(", ");
|
|
73
|
+
return run.id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): string {
|
|
77
|
+
const lines = [
|
|
78
|
+
`Nested run: ${run.id}`,
|
|
79
|
+
`Root: ${rootRunId}`,
|
|
80
|
+
`Parent: ${run.parentRunId}${run.parentStepIndex !== undefined ? ` step ${run.parentStepIndex + 1}` : ""}`,
|
|
81
|
+
`State: ${run.state}`,
|
|
82
|
+
run.activityState || run.lastActivityAt ? `Activity: ${formatActivityLabel(run.lastActivityAt, run.activityState)}` : undefined,
|
|
83
|
+
run.mode ? `Mode: ${run.mode}` : undefined,
|
|
84
|
+
`Agent: ${nestedRunDisplayName(run)}`,
|
|
85
|
+
run.currentStep !== undefined ? `Progress: step ${run.currentStep + 1}/${run.chainStepCount ?? run.steps?.length ?? 1}` : undefined,
|
|
86
|
+
run.asyncDir ? `Dir: ${run.asyncDir}` : undefined,
|
|
87
|
+
run.sessionFile ? `Session: ${run.sessionFile}` : undefined,
|
|
88
|
+
run.error ? `Error: ${run.error}` : undefined,
|
|
89
|
+
].filter((line): line is string => Boolean(line));
|
|
90
|
+
if (run.path.length) {
|
|
91
|
+
lines.push(`Path: ${run.path.map((part) => `${part.runId}${part.stepIndex !== undefined ? `:${part.stepIndex + 1}` : ""}${part.agent ? `:${part.agent}` : ""}`).join(" > ")} > ${run.id}`);
|
|
92
|
+
}
|
|
93
|
+
if (run.steps?.length) {
|
|
94
|
+
lines.push("Steps:");
|
|
95
|
+
for (const [index, step] of run.steps.entries()) {
|
|
96
|
+
const activity = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
|
|
97
|
+
lines.push(` ${index + 1}. ${step.agent} ${step.status}${activity ? `, ${activity}` : ""}${step.error ? `, error: ${step.error}` : ""}`);
|
|
98
|
+
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true }));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
lines.push(...formatNestedRunStatusLines(run.children, { indent: " ", commandHints: true }));
|
|
102
|
+
lines.push("Commands:", ` Status: subagent({ action: "status", id: "${run.id}" })`, ` Interrupt: subagent({ action: "interrupt", id: "${run.id}" })`, ` Resume: subagent({ action: "resume", id: "${run.id}", message: "..." })`, ` Root status: subagent({ action: "status", id: "${rootRunId}" })`);
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
60
106
|
export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDeps = {}): AgentToolResult<Details> {
|
|
61
107
|
const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
|
|
62
108
|
const resultsDir = deps.resultsDir ?? RESULTS_DIR;
|
|
63
109
|
if (!params.id && !params.runId && !params.dir) {
|
|
110
|
+
if (deps.nested) {
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text", text: "Child-safe subagent status requires an id when no foreground run is active." }],
|
|
113
|
+
isError: true,
|
|
114
|
+
details: { mode: "single", results: [] },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
64
117
|
try {
|
|
65
118
|
const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], resultsDir, kill: deps.kill, now: deps.now });
|
|
66
119
|
return {
|
|
@@ -79,7 +132,20 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
79
132
|
|
|
80
133
|
let location;
|
|
81
134
|
try {
|
|
82
|
-
|
|
135
|
+
const requestedId = params.id ?? params.runId;
|
|
136
|
+
if (!params.dir && requestedId) {
|
|
137
|
+
const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
138
|
+
if (resolved?.kind === "nested") {
|
|
139
|
+
reconcileNestedAsyncDescendants(resolved.match.route, { resultsDir, kill: deps.kill, now: deps.now });
|
|
140
|
+
const refreshed = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
141
|
+
const nested = refreshed?.kind === "nested" ? refreshed : resolved;
|
|
142
|
+
return { content: [{ type: "text", text: formatNestedExactStatus(nested.match.rootRunId, nested.match.run) }], details: { mode: "single", results: [] } };
|
|
143
|
+
}
|
|
144
|
+
if (resolved?.kind === "async") location = resolved.location;
|
|
145
|
+
else location = { asyncDir: null, resultPath: null, resolvedId: requestedId };
|
|
146
|
+
} else {
|
|
147
|
+
location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
|
|
148
|
+
}
|
|
83
149
|
} catch (error) {
|
|
84
150
|
const message = error instanceof Error ? error.message : String(error);
|
|
85
151
|
return {
|
|
@@ -115,6 +181,16 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
115
181
|
const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
|
|
116
182
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
117
183
|
if (status) {
|
|
184
|
+
let nestedChildren: NestedRunSummary[] = [];
|
|
185
|
+
let nestedWarning: string | undefined;
|
|
186
|
+
try {
|
|
187
|
+
const nestedRoute = findNestedRouteForRootId(status.runId);
|
|
188
|
+
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir, kill: deps.kill, now: deps.now });
|
|
189
|
+
nestedChildren = projectNestedRegistryForRoot(status.runId)?.children ?? [];
|
|
190
|
+
attachRootChildrenToSteps(status.runId, status.steps, nestedChildren);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
193
|
+
}
|
|
118
194
|
const outputPath = formatAsyncRunOutputPath({ asyncDir, outputFile: status.outputFile });
|
|
119
195
|
const progressLabel = formatAsyncRunProgressLabel({
|
|
120
196
|
mode: status.mode,
|
|
@@ -146,13 +222,22 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
146
222
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
147
223
|
const modelText = modelThinking ? ` (${modelThinking})` : "";
|
|
148
224
|
const errorText = step.error ? `, error: ${step.error}` : "";
|
|
149
|
-
|
|
225
|
+
const finalizationText = formatAcceptanceFinalizationSummary(step.acceptance?.finalization);
|
|
226
|
+
const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}${finalizationText}` : "";
|
|
227
|
+
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
228
|
+
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
229
|
+
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);
|
|
230
|
+
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
|
|
150
231
|
const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
|
|
151
232
|
if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
|
|
152
233
|
if (step.status === "running") {
|
|
153
234
|
lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
|
|
154
235
|
}
|
|
155
236
|
}
|
|
237
|
+
const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
238
|
+
const unattached = nestedChildren.filter((child) => !attached.has(child.id));
|
|
239
|
+
lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
|
|
240
|
+
if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
|
|
156
241
|
if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
|
|
157
242
|
if (status.state !== "running") {
|
|
158
243
|
lines.push(formatResumeGuidance(status.runId, status.steps ?? [], status.sessionFile));
|