pi-subagents 0.24.3 → 0.25.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 +26 -5
- package/README.md +19 -11
- package/package.json +4 -8
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +46 -10
- package/src/agents/agent-management.ts +5 -0
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +30 -6
- package/src/agents/skills.ts +25 -23
- package/src/extension/config.ts +16 -0
- package/src/extension/fanout-child.ts +170 -0
- package/src/extension/index.ts +13 -25
- package/src/intercom/intercom-bridge.ts +2 -1
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +107 -7
- package/src/runs/background/async-job-tracker.ts +57 -14
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +60 -30
- 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 +79 -3
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +66 -18
- package/src/runs/foreground/chain-execution.ts +6 -0
- package/src/runs/foreground/execution.ts +21 -5
- package/src/runs/foreground/subagent-executor.ts +314 -18
- package/src/runs/shared/completion-guard.ts +23 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -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 +1 -0
- package/src/runs/shared/pi-args.ts +67 -5
- package/src/runs/shared/run-history.ts +12 -7
- package/src/runs/shared/single-output.ts +12 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
- package/src/shared/artifacts.ts +2 -2
- package/src/shared/types.ts +95 -0
- package/src/shared/utils.ts +11 -1
- package/src/tui/render.ts +254 -153
|
@@ -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 {
|
|
@@ -57,10 +62,53 @@ function stepLineLabel(status: AsyncStatus, index: number): string {
|
|
|
57
62
|
return `Step ${index + 1}`;
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
function nestedRunDisplayName(run: NestedRunSummary): string {
|
|
66
|
+
if (run.agent) return run.agent;
|
|
67
|
+
if (run.agents?.length) return run.agents.join(", ");
|
|
68
|
+
return run.id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): string {
|
|
72
|
+
const lines = [
|
|
73
|
+
`Nested run: ${run.id}`,
|
|
74
|
+
`Root: ${rootRunId}`,
|
|
75
|
+
`Parent: ${run.parentRunId}${run.parentStepIndex !== undefined ? ` step ${run.parentStepIndex + 1}` : ""}`,
|
|
76
|
+
`State: ${run.state}`,
|
|
77
|
+
run.activityState || run.lastActivityAt ? `Activity: ${formatActivityLabel(run.lastActivityAt, run.activityState)}` : undefined,
|
|
78
|
+
run.mode ? `Mode: ${run.mode}` : undefined,
|
|
79
|
+
`Agent: ${nestedRunDisplayName(run)}`,
|
|
80
|
+
run.currentStep !== undefined ? `Progress: step ${run.currentStep + 1}/${run.chainStepCount ?? run.steps?.length ?? 1}` : undefined,
|
|
81
|
+
run.asyncDir ? `Dir: ${run.asyncDir}` : undefined,
|
|
82
|
+
run.sessionFile ? `Session: ${run.sessionFile}` : undefined,
|
|
83
|
+
run.error ? `Error: ${run.error}` : undefined,
|
|
84
|
+
].filter((line): line is string => Boolean(line));
|
|
85
|
+
if (run.path.length) {
|
|
86
|
+
lines.push(`Path: ${run.path.map((part) => `${part.runId}${part.stepIndex !== undefined ? `:${part.stepIndex + 1}` : ""}${part.agent ? `:${part.agent}` : ""}`).join(" > ")} > ${run.id}`);
|
|
87
|
+
}
|
|
88
|
+
if (run.steps?.length) {
|
|
89
|
+
lines.push("Steps:");
|
|
90
|
+
for (const [index, step] of run.steps.entries()) {
|
|
91
|
+
const activity = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
|
|
92
|
+
lines.push(` ${index + 1}. ${step.agent} ${step.status}${activity ? `, ${activity}` : ""}${step.error ? `, error: ${step.error}` : ""}`);
|
|
93
|
+
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true }));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
lines.push(...formatNestedRunStatusLines(run.children, { indent: " ", commandHints: true }));
|
|
97
|
+
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}" })`);
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
60
101
|
export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDeps = {}): AgentToolResult<Details> {
|
|
61
102
|
const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
|
|
62
103
|
const resultsDir = deps.resultsDir ?? RESULTS_DIR;
|
|
63
104
|
if (!params.id && !params.runId && !params.dir) {
|
|
105
|
+
if (deps.nested) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text", text: "Child-safe subagent status requires an id when no foreground run is active." }],
|
|
108
|
+
isError: true,
|
|
109
|
+
details: { mode: "single", results: [] },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
64
112
|
try {
|
|
65
113
|
const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], resultsDir, kill: deps.kill, now: deps.now });
|
|
66
114
|
return {
|
|
@@ -79,7 +127,20 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
79
127
|
|
|
80
128
|
let location;
|
|
81
129
|
try {
|
|
82
|
-
|
|
130
|
+
const requestedId = params.id ?? params.runId;
|
|
131
|
+
if (!params.dir && requestedId) {
|
|
132
|
+
const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
133
|
+
if (resolved?.kind === "nested") {
|
|
134
|
+
reconcileNestedAsyncDescendants(resolved.match.route, { resultsDir, kill: deps.kill, now: deps.now });
|
|
135
|
+
const refreshed = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
136
|
+
const nested = refreshed?.kind === "nested" ? refreshed : resolved;
|
|
137
|
+
return { content: [{ type: "text", text: formatNestedExactStatus(nested.match.rootRunId, nested.match.run) }], details: { mode: "single", results: [] } };
|
|
138
|
+
}
|
|
139
|
+
if (resolved?.kind === "async") location = resolved.location;
|
|
140
|
+
else location = { asyncDir: null, resultPath: null, resolvedId: requestedId };
|
|
141
|
+
} else {
|
|
142
|
+
location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
|
|
143
|
+
}
|
|
83
144
|
} catch (error) {
|
|
84
145
|
const message = error instanceof Error ? error.message : String(error);
|
|
85
146
|
return {
|
|
@@ -115,6 +176,16 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
115
176
|
const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
|
|
116
177
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
117
178
|
if (status) {
|
|
179
|
+
let nestedChildren: NestedRunSummary[] = [];
|
|
180
|
+
let nestedWarning: string | undefined;
|
|
181
|
+
try {
|
|
182
|
+
const nestedRoute = findNestedRouteForRootId(status.runId);
|
|
183
|
+
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir, kill: deps.kill, now: deps.now });
|
|
184
|
+
nestedChildren = projectNestedRegistryForRoot(status.runId)?.children ?? [];
|
|
185
|
+
attachRootChildrenToSteps(status.runId, status.steps, nestedChildren);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
188
|
+
}
|
|
118
189
|
const outputPath = formatAsyncRunOutputPath({ asyncDir, outputFile: status.outputFile });
|
|
119
190
|
const progressLabel = formatAsyncRunProgressLabel({
|
|
120
191
|
mode: status.mode,
|
|
@@ -147,12 +218,17 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
147
218
|
const modelText = modelThinking ? ` (${modelThinking})` : "";
|
|
148
219
|
const errorText = step.error ? `, error: ${step.error}` : "";
|
|
149
220
|
lines.push(`${stepLineLabel(status, index)}: ${step.agent} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
|
|
221
|
+
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
|
|
150
222
|
const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
|
|
151
223
|
if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
|
|
152
224
|
if (step.status === "running") {
|
|
153
225
|
lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
|
|
154
226
|
}
|
|
155
227
|
}
|
|
228
|
+
const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
229
|
+
const unattached = nestedChildren.filter((child) => !attached.has(child.id));
|
|
230
|
+
lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
|
|
231
|
+
if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
|
|
156
232
|
if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
|
|
157
233
|
if (status.state !== "running") {
|
|
158
234
|
lines.push(formatResumeGuidance(status.runId, status.steps ?? [], status.sessionFile));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
-
import { RESULTS_DIR, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode } from "../../shared/types.ts";
|
|
4
|
+
import { RESULTS_DIR, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
|
|
5
5
|
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
6
|
+
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent, type NestedRoute } from "../shared/nested-events.ts";
|
|
6
7
|
|
|
7
8
|
export type PidLiveness = "alive" | "dead" | "unknown";
|
|
8
9
|
|
|
@@ -233,6 +234,50 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
|
|
|
233
234
|
return { status: repair.status, repaired: true, resultPath, message: repair.message };
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
function terminal(state: AsyncStatus["state"]): boolean {
|
|
238
|
+
return state === "complete" || state === "failed" || state === "paused";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function* nestedRuns(children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
|
|
242
|
+
for (const child of children ?? []) {
|
|
243
|
+
yield child;
|
|
244
|
+
yield* nestedRuns(child.children);
|
|
245
|
+
yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function reconcileNestedAsyncDescendants(route: NestedRoute, options: ReconcileAsyncRunOptions = {}): void {
|
|
250
|
+
const registry = projectNestedEvents(route);
|
|
251
|
+
for (const run of nestedRuns(registry.children)) {
|
|
252
|
+
if (run.state !== "running" && run.state !== "queued") continue;
|
|
253
|
+
const asyncDir = resolveNestedAsyncDir(route.rootRunId, run);
|
|
254
|
+
if (!asyncDir) continue;
|
|
255
|
+
const result = reconcileAsyncRun(asyncDir, {
|
|
256
|
+
...options,
|
|
257
|
+
resultsDir: path.join(options.resultsDir ?? RESULTS_DIR, "nested", route.rootRunId),
|
|
258
|
+
});
|
|
259
|
+
const status = result.status;
|
|
260
|
+
if (!status) continue;
|
|
261
|
+
if (!result.repaired && !terminal(status.state)) continue;
|
|
262
|
+
const ts = options.now?.() ?? Date.now();
|
|
263
|
+
writeNestedEvent(route, {
|
|
264
|
+
type: terminal(status.state) ? "subagent.nested.completed" : "subagent.nested.updated",
|
|
265
|
+
ts,
|
|
266
|
+
parentRunId: run.parentRunId,
|
|
267
|
+
parentStepIndex: run.parentStepIndex,
|
|
268
|
+
child: nestedSummaryFromAsyncStatus(status, asyncDir, {
|
|
269
|
+
id: run.id,
|
|
270
|
+
parentRunId: run.parentRunId,
|
|
271
|
+
parentStepIndex: run.parentStepIndex,
|
|
272
|
+
depth: run.depth,
|
|
273
|
+
path: run.path,
|
|
274
|
+
mode: run.mode,
|
|
275
|
+
ts,
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
236
281
|
export function checkPidLiveness(pid: number, kill: KillFn = process.kill): PidLiveness {
|
|
237
282
|
try {
|
|
238
283
|
kill(pid, 0);
|