pi-subagents 0.32.0 → 0.33.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 +30 -3
- package/README.md +147 -58
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +70 -14
- package/src/agents/agent-management.ts +177 -5
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +142 -12
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/doctor.ts +1 -9
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +65 -90
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +52 -8
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +51 -7
- package/src/runs/background/async-job-tracker.ts +12 -2
- package/src/runs/background/async-status.ts +27 -2
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +106 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +166 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/subagent-runner.ts +409 -35
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +95 -21
- package/src/runs/foreground/execution.ts +150 -21
- package/src/runs/foreground/subagent-executor.ts +378 -64
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +167 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +31 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +30 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/shared/artifacts.ts +1 -0
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +134 -19
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +16 -2
- package/src/tui/render.ts +16 -8
- package/src/extension/companion-suggestions.ts +0 -359
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subagent completion notifications.
|
|
3
|
+
*
|
|
4
|
+
* Successful (completed) async results are held briefly and emitted as a
|
|
5
|
+
* single grouped message when sibling jobs finish within a short window (see
|
|
6
|
+
* `completion-batcher.ts`). Failed and paused results bypass grouping and fire
|
|
7
|
+
* immediately, flushing any held successes first, so failure and attention
|
|
8
|
+
* signals are never delayed.
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
12
|
import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
7
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type CompletionBatchConfig,
|
|
15
|
+
type CompletionBatcher,
|
|
16
|
+
createCompletionBatcher,
|
|
17
|
+
resolveCompletionBatchConfig,
|
|
18
|
+
} from "./completion-batcher.ts";
|
|
19
|
+
import { SUBAGENT_ASYNC_COMPLETE_EVENT, type SubagentState } from "../../shared/types.ts";
|
|
8
20
|
|
|
9
21
|
interface ChainStepResult {
|
|
10
22
|
agent: string;
|
|
@@ -31,6 +43,7 @@ interface SubagentResult {
|
|
|
31
43
|
state?: string;
|
|
32
44
|
timestamp: number;
|
|
33
45
|
durationMs?: number;
|
|
46
|
+
cwd?: string;
|
|
34
47
|
sessionFile?: string;
|
|
35
48
|
shareUrl?: string;
|
|
36
49
|
gistUrl?: string;
|
|
@@ -38,10 +51,116 @@ interface SubagentResult {
|
|
|
38
51
|
results?: ChainStepResult[];
|
|
39
52
|
taskIndex?: number;
|
|
40
53
|
totalTasks?: number;
|
|
54
|
+
sessionId?: string | null;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
interface NotifyTimerApi {
|
|
58
|
+
setTimeout(handler: () => void, delayMs: number): unknown;
|
|
59
|
+
clearTimeout(handle: unknown): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RegisterSubagentNotifyOptions {
|
|
63
|
+
batchConfig?: CompletionBatchConfig;
|
|
64
|
+
timers?: NotifyTimerApi;
|
|
65
|
+
now?: () => number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
|
|
69
|
+
if (!details.sessionValue) return undefined;
|
|
70
|
+
return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatSingleCompletion(details: SubagentNotifyDetails): string {
|
|
74
|
+
const sessionLine = formatSessionLine(details);
|
|
75
|
+
return [
|
|
76
|
+
`Background task ${details.status}: **${details.agent}**${details.taskInfo ?? ""}`,
|
|
77
|
+
"",
|
|
78
|
+
details.resultPreview.trim() ? details.resultPreview : "(no output)",
|
|
79
|
+
sessionLine ? "" : undefined,
|
|
80
|
+
sessionLine,
|
|
81
|
+
]
|
|
82
|
+
.filter((line) => line !== undefined)
|
|
83
|
+
.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatGroupedCompletion(details: SubagentNotifyDetails[]): string {
|
|
87
|
+
const header = `Background tasks completed (${details.length}): ${details.map((d) => `**${d.agent}**${d.taskInfo ?? ""}`).join(", ")}`;
|
|
88
|
+
const blocks: string[] = [header, ""];
|
|
89
|
+
for (let index = 0; index < details.length; index++) {
|
|
90
|
+
const detail = details[index];
|
|
91
|
+
if (!detail) continue;
|
|
92
|
+
const sessionLine = formatSessionLine(detail);
|
|
93
|
+
blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}`);
|
|
94
|
+
blocks.push(detail.resultPreview.trim() ? detail.resultPreview : "(no output)");
|
|
95
|
+
if (sessionLine) blocks.push(sessionLine);
|
|
96
|
+
blocks.push("");
|
|
97
|
+
}
|
|
98
|
+
return blocks.join("\n").trimEnd();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sendCompletion(pi: Pick<ExtensionAPI, "sendMessage">, details: SubagentNotifyDetails[]): void {
|
|
102
|
+
if (details.length === 0) return;
|
|
103
|
+
const content = details.length === 1
|
|
104
|
+
? formatSingleCompletion(details[0]!)
|
|
105
|
+
: formatGroupedCompletion(details);
|
|
106
|
+
pi.sendMessage(
|
|
107
|
+
{
|
|
108
|
+
customType: "subagent-notify",
|
|
109
|
+
content,
|
|
110
|
+
display: true,
|
|
111
|
+
},
|
|
112
|
+
{ triggerTurn: true },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function completionBatchKey(result: SubagentResult): string {
|
|
117
|
+
const sessionId = typeof result.sessionId === "string" ? result.sessionId.trim() : "";
|
|
118
|
+
if (sessionId) return `session:${sessionId}`;
|
|
119
|
+
const cwd = typeof result.cwd === "string" ? result.cwd.trim() : "";
|
|
120
|
+
return cwd ? `cwd:${cwd}` : "unknown";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDetails {
|
|
124
|
+
const agent = result.agent ?? "unknown";
|
|
125
|
+
const summary = typeof result.summary === "string" ? result.summary : "";
|
|
126
|
+
const paused = !result.success && (
|
|
127
|
+
result.exitCode === 0
|
|
128
|
+
|| result.state === "paused"
|
|
129
|
+
|| summary.startsWith("Paused after interrupt.")
|
|
130
|
+
);
|
|
131
|
+
const status = paused ? "paused" : result.success ? "completed" : "failed";
|
|
132
|
+
|
|
133
|
+
const taskInfo =
|
|
134
|
+
result.taskIndex !== undefined && result.totalTasks !== undefined
|
|
135
|
+
? ` (${result.taskIndex + 1}/${result.totalTasks})`
|
|
136
|
+
: undefined;
|
|
137
|
+
|
|
138
|
+
const session =
|
|
139
|
+
result.shareUrl
|
|
140
|
+
? { label: "Session", value: result.shareUrl }
|
|
141
|
+
: result.shareError
|
|
142
|
+
? { label: "Session share error", value: result.shareError }
|
|
143
|
+
: result.sessionFile
|
|
144
|
+
? { label: "Session file", value: result.sessionFile }
|
|
145
|
+
: undefined;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
agent,
|
|
149
|
+
status,
|
|
150
|
+
...(taskInfo ? { taskInfo } : {}),
|
|
151
|
+
resultPreview: summary,
|
|
152
|
+
...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
|
|
153
|
+
...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default function registerSubagentNotify(
|
|
158
|
+
pi: ExtensionAPI,
|
|
159
|
+
state: Pick<SubagentState, "currentSessionId">,
|
|
160
|
+
options: RegisterSubagentNotifyOptions = {},
|
|
161
|
+
): void {
|
|
44
162
|
const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
|
|
163
|
+
const batcherStoreKey = "__pi_subagents_notify_batcher__";
|
|
45
164
|
const globalStore = globalThis as Record<string, unknown>;
|
|
46
165
|
const previousUnsubscribe = globalStore[unsubscribeStoreKey];
|
|
47
166
|
if (typeof previousUnsubscribe === "function") {
|
|
@@ -51,57 +170,55 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
|
|
|
51
170
|
// Best effort cleanup for stale handlers from an older reload.
|
|
52
171
|
}
|
|
53
172
|
}
|
|
173
|
+
const previousBatcher = globalStore[batcherStoreKey];
|
|
174
|
+
if (previousBatcher && typeof (previousBatcher as { dispose?: () => void }).dispose === "function") {
|
|
175
|
+
try {
|
|
176
|
+
(previousBatcher as { dispose: () => void }).dispose();
|
|
177
|
+
} catch {
|
|
178
|
+
// Best effort cleanup for a stale batcher from an older reload.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
54
181
|
|
|
55
182
|
const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
|
|
56
183
|
const ttlMs = 10 * 60 * 1000;
|
|
184
|
+
const nowFn = options.now ?? Date.now;
|
|
185
|
+
const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
|
|
186
|
+
const batchers = new Map<string, CompletionBatcher<SubagentNotifyDetails>>();
|
|
187
|
+
globalStore[batcherStoreKey] = {
|
|
188
|
+
dispose() {
|
|
189
|
+
for (const batcher of batchers.values()) batcher.dispose();
|
|
190
|
+
batchers.clear();
|
|
191
|
+
},
|
|
192
|
+
};
|
|
57
193
|
|
|
58
194
|
const handleComplete = (data: unknown) => {
|
|
59
195
|
const result = data as SubagentResult;
|
|
60
|
-
|
|
196
|
+
if (typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return;
|
|
197
|
+
const now = nowFn();
|
|
61
198
|
const key = buildCompletionKey(result, "notify");
|
|
62
199
|
if (markSeenWithTtl(seen, key, now, ttlMs)) return;
|
|
63
200
|
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const displaySummary = summary.trim() ? summary : "(no output)";
|
|
87
|
-
const content = [
|
|
88
|
-
`Background task ${status}: **${agent}**${taskInfo}`,
|
|
89
|
-
"",
|
|
90
|
-
displaySummary,
|
|
91
|
-
sessionLine ? "" : undefined,
|
|
92
|
-
sessionLine,
|
|
93
|
-
]
|
|
94
|
-
.filter((line) => line !== undefined)
|
|
95
|
-
.join("\n");
|
|
96
|
-
|
|
97
|
-
pi.sendMessage(
|
|
98
|
-
{
|
|
99
|
-
customType: "subagent-notify",
|
|
100
|
-
content,
|
|
101
|
-
display: true,
|
|
102
|
-
},
|
|
103
|
-
{ triggerTurn: true },
|
|
104
|
-
);
|
|
201
|
+
const details = buildCompletionDetails(result);
|
|
202
|
+
const batchKey = completionBatchKey(result);
|
|
203
|
+
let batcher = batchers.get(batchKey);
|
|
204
|
+
if (!batcher) {
|
|
205
|
+
batcher = createCompletionBatcher<SubagentNotifyDetails>({
|
|
206
|
+
config: batchConfig,
|
|
207
|
+
emit: (items) => sendCompletion(pi, items),
|
|
208
|
+
...(options.timers ? { timers: options.timers } : {}),
|
|
209
|
+
now: nowFn,
|
|
210
|
+
});
|
|
211
|
+
batchers.set(batchKey, batcher);
|
|
212
|
+
}
|
|
213
|
+
if (details.status !== "completed") {
|
|
214
|
+
// Failures and paused runs bypass grouping. Flush any held
|
|
215
|
+
// successes for the same owner first so they are not stranded
|
|
216
|
+
// behind this signal, then emit the non-completion result immediately.
|
|
217
|
+
batcher.flush();
|
|
218
|
+
sendCompletion(pi, [details]);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
batcher.push(details);
|
|
105
222
|
};
|
|
106
223
|
|
|
107
224
|
globalStore[unsubscribeStoreKey] = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
|
|
@@ -118,8 +118,7 @@ export function createResultWatcher(
|
|
|
118
118
|
if (!fsApi.existsSync(resultPath)) return;
|
|
119
119
|
try {
|
|
120
120
|
const data = JSON.parse(fsApi.readFileSync(resultPath, "utf-8")) as ResultFileData;
|
|
121
|
-
if (data.sessionId
|
|
122
|
-
if (!data.sessionId && data.cwd && (!state.baseCwd || data.cwd !== state.baseCwd)) return;
|
|
121
|
+
if (typeof data.sessionId !== "string" || data.sessionId !== state.currentSessionId) return;
|
|
123
122
|
|
|
124
123
|
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
125
124
|
const hasExplicitNestedChildren = data.nestedChildren !== undefined;
|
|
@@ -28,7 +28,8 @@ function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function foregroundIds(state: SubagentState | undefined): string[] {
|
|
31
|
-
|
|
31
|
+
if (!state) return [];
|
|
32
|
+
return [...new Set([...state.foregroundControls.keys(), ...(state.foregroundRuns?.keys() ?? [])])];
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
|
|
@@ -57,7 +58,7 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
57
58
|
const resultsDir = deps.resultsDir ?? RESULTS_DIR;
|
|
58
59
|
|
|
59
60
|
const nestedScope = deps.nested ?? nestedScopeFromState(deps.state);
|
|
60
|
-
if (deps.state?.foregroundControls.has(id)) return { kind: "foreground", id };
|
|
61
|
+
if (deps.state?.foregroundControls.has(id) || deps.state?.foregroundRuns?.has(id)) return { kind: "foreground", id };
|
|
61
62
|
const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
|
|
62
63
|
if (exactAsync) return { kind: "async", id, location: exactAsync };
|
|
63
64
|
const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
|
|
@@ -2,10 +2,11 @@ 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 { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
|
|
5
6
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
6
7
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
7
8
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
8
|
-
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details, type NestedRunSummary, type SubagentState } from "../../shared/types.ts";
|
|
9
|
+
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details, type ForegroundResumeRun, type NestedRunSummary, type SubagentState } from "../../shared/types.ts";
|
|
9
10
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
10
11
|
import { resolveAsyncRunLocation } from "./async-resume.ts";
|
|
11
12
|
import { resolveSubagentRunId } from "./run-id-resolver.ts";
|
|
@@ -18,6 +19,9 @@ interface RunStatusParams {
|
|
|
18
19
|
id?: string;
|
|
19
20
|
runId?: string;
|
|
20
21
|
dir?: string;
|
|
22
|
+
index?: number;
|
|
23
|
+
view?: "fleet" | "transcript";
|
|
24
|
+
lines?: number;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
interface RunStatusDeps {
|
|
@@ -27,6 +31,7 @@ interface RunStatusDeps {
|
|
|
27
31
|
now?: () => number;
|
|
28
32
|
state?: SubagentState;
|
|
29
33
|
nested?: NestedRunResolutionScope;
|
|
34
|
+
sessionRoots?: string[];
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
function hasExistingSessionFile(value: unknown): value is string {
|
|
@@ -68,6 +73,87 @@ function nestedRunDisplayName(run: NestedRunSummary): string {
|
|
|
68
73
|
return run.id;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
function formatSteeringSummary(input: { steerCount?: number; lastSteerAt?: number }): string | undefined {
|
|
77
|
+
const parts: string[] = [];
|
|
78
|
+
if (input.steerCount !== undefined) parts.push(`${input.steerCount} steer${input.steerCount === 1 ? "" : "s"}`);
|
|
79
|
+
if (typeof input.lastSteerAt === "number" && Number.isFinite(input.lastSteerAt)) parts.push(`last ${new Date(input.lastSteerAt).toISOString()}`);
|
|
80
|
+
return parts.length ? parts.join(", ") : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rememberedForegroundChildOutput(child: ForegroundResumeRun["children"][number]): string {
|
|
84
|
+
const outputPath = child.artifactPaths?.outputPath;
|
|
85
|
+
if (outputPath && fs.existsSync(outputPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const artifactOutput = fs.readFileSync(outputPath, "utf-8").trim();
|
|
88
|
+
if (artifactOutput) return artifactOutput;
|
|
89
|
+
} catch {
|
|
90
|
+
// Fall back to the remembered snapshot below.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return child.finalOutput ?? "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
|
|
97
|
+
const lines = [
|
|
98
|
+
`Run: ${run.runId}`,
|
|
99
|
+
"State: remembered foreground",
|
|
100
|
+
`Mode: ${run.mode}`,
|
|
101
|
+
`Updated: ${new Date(run.updatedAt).toISOString()}`,
|
|
102
|
+
`Cwd: ${run.cwd}`,
|
|
103
|
+
];
|
|
104
|
+
for (const child of run.children) {
|
|
105
|
+
const output = rememberedForegroundChildOutput(child).trim().split(/\r?\n/).find((line) => line.trim());
|
|
106
|
+
const parts = [
|
|
107
|
+
`${child.index + 1}. ${child.agent} ${child.status}`,
|
|
108
|
+
child.exitCode !== undefined ? `exit ${child.exitCode}` : undefined,
|
|
109
|
+
child.detachedReason ? `detached: ${child.detachedReason}` : undefined,
|
|
110
|
+
output ? `output: ${output.slice(0, 160)}` : undefined,
|
|
111
|
+
].filter(Boolean);
|
|
112
|
+
lines.push(parts.join(", "));
|
|
113
|
+
if (child.sessionFile) lines.push(` Session: ${child.sessionFile}`);
|
|
114
|
+
if (child.transcriptPath) lines.push(` Transcript: ${child.transcriptPath}`);
|
|
115
|
+
if (child.artifactPaths?.outputPath) lines.push(` Output: ${child.artifactPaths.outputPath}`);
|
|
116
|
+
if (child.transcriptError) lines.push(` Transcript warning: ${child.transcriptError}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push("", `Status: subagent({ action: "status", id: "${run.runId}" })`);
|
|
119
|
+
if (run.children.length === 1) lines.push(`Transcript: subagent({ action: "status", id: "${run.runId}", view: "transcript" })`);
|
|
120
|
+
else lines.push(`Transcript: subagent({ action: "status", id: "${run.runId}", index: 0, view: "transcript" })`);
|
|
121
|
+
const resumable = run.children.find((child) => child.status !== "detached" && hasExistingSessionFile(child.sessionFile));
|
|
122
|
+
if (resumable) {
|
|
123
|
+
lines.push(run.children.length === 1
|
|
124
|
+
? `Revive: subagent({ action: "resume", id: "${run.runId}", message: "..." })`
|
|
125
|
+
: `Revive child: subagent({ action: "resume", id: "${run.runId}", index: ${resumable.index}, message: "..." })`);
|
|
126
|
+
} else if (run.children.some((child) => child.status === "detached")) {
|
|
127
|
+
lines.push("Recovery: child detached for intercom coordination; status will show recovered output after the child exits when Pi can observe it.");
|
|
128
|
+
} else {
|
|
129
|
+
lines.push("Resume: unavailable; no child session file was persisted.");
|
|
130
|
+
}
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options: { index?: number; lines?: number }): string {
|
|
135
|
+
let index = options.index;
|
|
136
|
+
if (index !== undefined && !Number.isInteger(index)) throw new Error("Transcript index must be an integer.");
|
|
137
|
+
if (index === undefined && run.children.length === 1) index = 0;
|
|
138
|
+
if (index === undefined) return `Transcript view requires index for foreground run '${run.runId}' with ${run.children.length} children.`;
|
|
139
|
+
if (index < 0 || index >= run.children.length) throw new Error(`Transcript index ${index} is out of range for ${run.children.length} foreground children.`);
|
|
140
|
+
const child = run.children[index]!;
|
|
141
|
+
const lineLimit = Math.max(1, Math.min(options.lines ?? 80, 1000));
|
|
142
|
+
const outputLines = rememberedForegroundChildOutput(child).split(/\r?\n/).filter((line) => line.trim()).slice(-lineLimit);
|
|
143
|
+
const lines = [
|
|
144
|
+
`Run: ${run.runId}`,
|
|
145
|
+
`State: ${child.status}`,
|
|
146
|
+
`Child: ${index} (${child.agent})`,
|
|
147
|
+
child.sessionFile ? `Session: ${child.sessionFile}` : undefined,
|
|
148
|
+
child.transcriptPath ? `Transcript: ${child.transcriptPath}` : undefined,
|
|
149
|
+
child.artifactPaths?.outputPath ? `Output: ${child.artifactPaths.outputPath}` : undefined,
|
|
150
|
+
].filter((line): line is string => Boolean(line));
|
|
151
|
+
lines.push("Result transcript tail:");
|
|
152
|
+
if (outputLines.length === 0) lines.push(" (no recovered final output available yet)");
|
|
153
|
+
else for (const line of outputLines) lines.push(` ${line}`);
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
71
157
|
function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): string {
|
|
72
158
|
const lines = [
|
|
73
159
|
`Nested run: ${run.id}`,
|
|
@@ -78,6 +164,7 @@ function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): stri
|
|
|
78
164
|
run.mode ? `Mode: ${run.mode}` : undefined,
|
|
79
165
|
`Agent: ${nestedRunDisplayName(run)}`,
|
|
80
166
|
run.currentStep !== undefined ? `Progress: step ${run.currentStep + 1}/${run.chainStepCount ?? run.steps?.length ?? 1}` : undefined,
|
|
167
|
+
run.turnBudget ? `Turn budget: ${run.turnBudget.turnCount}/${run.turnBudget.maxTurns}+${run.turnBudget.graceTurns} (${run.turnBudget.outcome})` : undefined,
|
|
81
168
|
run.asyncDir ? `Dir: ${run.asyncDir}` : undefined,
|
|
82
169
|
run.sessionFile ? `Session: ${run.sessionFile}` : undefined,
|
|
83
170
|
run.error ? `Error: ${run.error}` : undefined,
|
|
@@ -89,18 +176,30 @@ function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): stri
|
|
|
89
176
|
lines.push("Steps:");
|
|
90
177
|
for (const [index, step] of run.steps.entries()) {
|
|
91
178
|
const activity = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
|
|
92
|
-
|
|
179
|
+
const budget = step.turnBudget ? `, turn budget: ${step.turnBudget.turnCount}/${step.turnBudget.maxTurns}+${step.turnBudget.graceTurns} (${step.turnBudget.outcome})` : "";
|
|
180
|
+
lines.push(` ${index + 1}. ${step.agent} ${step.status}${activity ? `, ${activity}` : ""}${budget}${step.error ? `, error: ${step.error}` : ""}`);
|
|
93
181
|
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true }));
|
|
94
182
|
}
|
|
95
183
|
}
|
|
96
184
|
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}" })`);
|
|
185
|
+
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: "..." })`, ` Steer: subagent({ action: "steer", id: "${run.id}", message: "..." })`, ` Root status: subagent({ action: "status", id: "${rootRunId}" })`);
|
|
98
186
|
return lines.join("\n");
|
|
99
187
|
}
|
|
100
188
|
|
|
101
189
|
export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDeps = {}): AgentToolResult<Details> {
|
|
102
190
|
const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
|
|
103
191
|
const resultsDir = deps.resultsDir ?? RESULTS_DIR;
|
|
192
|
+
const currentSessionId = deps.state?.currentSessionId ?? undefined;
|
|
193
|
+
if (params.view && params.view !== "fleet" && params.view !== "transcript") {
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text", text: `Unknown status view: ${params.view}. Valid: fleet, transcript.` }],
|
|
196
|
+
isError: true,
|
|
197
|
+
details: { mode: "single", results: [] },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (params.view === "fleet") {
|
|
201
|
+
return inspectSubagentFleet(params, { asyncDirRoot, resultsDir, kill: deps.kill, now: deps.now, state: deps.state, childSafe: Boolean(deps.nested) });
|
|
202
|
+
}
|
|
104
203
|
if (!params.id && !params.runId && !params.dir) {
|
|
105
204
|
if (deps.nested) {
|
|
106
205
|
return {
|
|
@@ -110,7 +209,15 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
110
209
|
};
|
|
111
210
|
}
|
|
112
211
|
try {
|
|
113
|
-
const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], resultsDir, kill: deps.kill, now: deps.now });
|
|
212
|
+
const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionId: currentSessionId, resultsDir, kill: deps.kill, now: deps.now });
|
|
213
|
+
if (params.view === "transcript") {
|
|
214
|
+
if (runs.length === 1) return inspectSubagentStatus({ ...params, id: runs[0]!.id }, deps);
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text", text: runs.length === 0 ? "No active async run transcript is available." : `Transcript view requires an id when ${runs.length} active async runs exist. Use subagent({ action: "status", view: "fleet" }) to choose one.` }],
|
|
217
|
+
isError: true,
|
|
218
|
+
details: { mode: "single", results: [] },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
114
221
|
return {
|
|
115
222
|
content: [{ type: "text", text: formatAsyncRunList(runs) }],
|
|
116
223
|
details: { mode: "single", results: [] },
|
|
@@ -130,10 +237,32 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
130
237
|
const requestedId = params.id ?? params.runId;
|
|
131
238
|
if (!params.dir && requestedId) {
|
|
132
239
|
const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
240
|
+
if (resolved?.kind === "foreground") {
|
|
241
|
+
const run = deps.state?.foregroundRuns?.get(resolved.id);
|
|
242
|
+
if (run) {
|
|
243
|
+
try {
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: "text", text: params.view === "transcript" ? formatRememberedForegroundTranscript(run, { index: params.index, lines: params.lines }) : formatRememberedForegroundStatus(run) }],
|
|
246
|
+
details: { mode: "single", results: [] },
|
|
247
|
+
};
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "single", results: [] } };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
133
254
|
if (resolved?.kind === "nested") {
|
|
134
255
|
reconcileNestedAsyncDescendants(resolved.match.route, { resultsDir, kill: deps.kill, now: deps.now });
|
|
135
256
|
const refreshed = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
136
257
|
const nested = refreshed?.kind === "nested" ? refreshed : resolved;
|
|
258
|
+
if (params.view === "transcript") {
|
|
259
|
+
try {
|
|
260
|
+
return { content: [{ type: "text", text: formatNestedRunTranscript(nested.match.run, { index: params.index, lines: params.lines, sessionRoots: deps.sessionRoots }) }], details: { mode: "single", results: [] } };
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "single", results: [] } };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
137
266
|
return { content: [{ type: "text", text: formatNestedExactStatus(nested.match.rootRunId, nested.match.run) }], details: { mode: "single", results: [] } };
|
|
138
267
|
}
|
|
139
268
|
if (resolved?.kind === "async") location = resolved.location;
|
|
@@ -176,6 +305,21 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
176
305
|
const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
|
|
177
306
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
178
307
|
if (status) {
|
|
308
|
+
if (params.view === "transcript") {
|
|
309
|
+
if (currentSessionId && status.sessionId !== currentSessionId) {
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session." }],
|
|
312
|
+
isError: true,
|
|
313
|
+
details: { mode: "single", results: [] },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
return { content: [{ type: "text", text: formatAsyncRunTranscript(status, asyncDir, { index: params.index, lines: params.lines, sessionRoots: deps.sessionRoots }) }], details: { mode: "single", results: [] } };
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
320
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "single", results: [] } };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
179
323
|
let nestedChildren: NestedRunSummary[] = [];
|
|
180
324
|
let nestedWarning: string | undefined;
|
|
181
325
|
try {
|
|
@@ -198,17 +342,20 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
198
342
|
const started = new Date(status.startedAt).toISOString();
|
|
199
343
|
const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
|
|
200
344
|
const statusActivityText = status.state === "running" ? formatActivityLabel(status.lastActivityAt, status.activityState) : undefined;
|
|
345
|
+
const steeringText = formatSteeringSummary(status);
|
|
201
346
|
|
|
202
347
|
const lines = [
|
|
203
348
|
`Run: ${status.runId}`,
|
|
204
349
|
`State: ${status.state}`,
|
|
205
350
|
status.error ? `Error: ${status.error}` : undefined,
|
|
206
351
|
statusActivityText ? `Activity: ${statusActivityText}` : undefined,
|
|
352
|
+
steeringText ? `Steering: ${steeringText}` : undefined,
|
|
207
353
|
`Mode: ${status.mode}`,
|
|
208
354
|
`Progress: ${progressLabel}`,
|
|
209
355
|
status.pendingAppends ? `Pending appends: ${status.pendingAppends}` : undefined,
|
|
210
356
|
`Started: ${started}`,
|
|
211
357
|
`Updated: ${updated}`,
|
|
358
|
+
status.turnBudget ? `Turn budget: ${status.turnBudget.turnCount}/${status.turnBudget.maxTurns}+${status.turnBudget.graceTurns} (${status.turnBudget.outcome})` : undefined,
|
|
212
359
|
`Dir: ${asyncDir}`,
|
|
213
360
|
outputPath ? `Output: ${outputPath}` : undefined,
|
|
214
361
|
reconciliation.message ? `Diagnosis: ${reconciliation.message}` : undefined,
|
|
@@ -218,16 +365,20 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
218
365
|
const stepActivityText = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
|
|
219
366
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
220
367
|
const modelText = modelThinking ? ` (${modelThinking})` : "";
|
|
368
|
+
const steeringText = formatSteeringSummary(step);
|
|
369
|
+
const steeringSuffix = steeringText ? `, steering: ${steeringText}` : "";
|
|
221
370
|
const errorText = step.error ? `, error: ${step.error}` : "";
|
|
222
371
|
const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}` : "";
|
|
372
|
+
const budgetText = step.turnBudget ? `, turn budget: ${step.turnBudget.turnCount}/${step.turnBudget.maxTurns}+${step.turnBudget.graceTurns} (${step.turnBudget.outcome})` : "";
|
|
223
373
|
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
224
374
|
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
225
|
-
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);
|
|
375
|
+
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
|
|
226
376
|
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
|
|
227
377
|
const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
|
|
228
378
|
if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
|
|
229
379
|
if (step.status === "running") {
|
|
230
380
|
lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
|
|
381
|
+
lines.push(` Steer: subagent({ action: "steer", id: "${status.runId}", index: ${index}, message: "..." })`);
|
|
231
382
|
}
|
|
232
383
|
}
|
|
233
384
|
const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
@@ -235,6 +386,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
235
386
|
lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
|
|
236
387
|
if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
|
|
237
388
|
if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
|
|
389
|
+
if (status.state === "running") lines.push(`Steer running child: subagent({ action: "steer", id: "${status.runId}", message: "..." })`);
|
|
238
390
|
if (status.state !== "running") {
|
|
239
391
|
lines.push(formatResumeGuidance(status.runId, status.steps ?? [], status.sessionFile));
|
|
240
392
|
}
|
|
@@ -248,7 +400,15 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
248
400
|
if (resultPath) {
|
|
249
401
|
try {
|
|
250
402
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
251
|
-
const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; exitCode?: number; state?: string; sessionFile?: string; results?: Array<{ agent?: string; sessionFile?: string }> };
|
|
403
|
+
const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; sessionFile?: string; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null }> };
|
|
404
|
+
if (params.view === "transcript") {
|
|
405
|
+
try {
|
|
406
|
+
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
407
|
+
} catch (error) {
|
|
408
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
409
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "single", results: [] } };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
252
412
|
const status = data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
253
413
|
const runId = data.runId ?? data.id ?? resolvedId;
|
|
254
414
|
const lines = [`Run: ${runId}`, `State: ${status}`, `Result: ${resultPath}`];
|