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.
Files changed (52) hide show
  1. package/CHANGELOG.md +30 -3
  2. package/README.md +147 -58
  3. package/install.mjs +2 -1
  4. package/package.json +3 -2
  5. package/skills/pi-subagents/SKILL.md +70 -14
  6. package/src/agents/agent-management.ts +177 -5
  7. package/src/agents/agent-memory.ts +254 -0
  8. package/src/agents/agent-serializer.ts +11 -0
  9. package/src/agents/agents.ts +142 -12
  10. package/src/agents/chain-serializer.ts +27 -2
  11. package/src/extension/doctor.ts +1 -9
  12. package/src/extension/fanout-child.ts +2 -2
  13. package/src/extension/index.ts +65 -90
  14. package/src/extension/rpc.ts +369 -0
  15. package/src/extension/schemas.ts +52 -8
  16. package/src/extension/tool-description.ts +200 -0
  17. package/src/intercom/intercom-bridge.ts +21 -253
  18. package/src/intercom/native-supervisor-channel.ts +510 -0
  19. package/src/runs/background/async-execution.ts +51 -7
  20. package/src/runs/background/async-job-tracker.ts +12 -2
  21. package/src/runs/background/async-status.ts +27 -2
  22. package/src/runs/background/completion-batcher.ts +166 -0
  23. package/src/runs/background/control-channel.ts +106 -1
  24. package/src/runs/background/fleet-view.ts +515 -0
  25. package/src/runs/background/notify.ts +161 -44
  26. package/src/runs/background/result-watcher.ts +1 -2
  27. package/src/runs/background/run-id-resolver.ts +3 -2
  28. package/src/runs/background/run-status.ts +166 -6
  29. package/src/runs/background/scheduled-runs.ts +514 -0
  30. package/src/runs/background/subagent-runner.ts +409 -35
  31. package/src/runs/background/wait.ts +353 -0
  32. package/src/runs/foreground/chain-execution.ts +95 -21
  33. package/src/runs/foreground/execution.ts +150 -21
  34. package/src/runs/foreground/subagent-executor.ts +378 -64
  35. package/src/runs/shared/dynamic-fanout.ts +1 -1
  36. package/src/runs/shared/model-fallback.ts +167 -20
  37. package/src/runs/shared/model-scope.ts +128 -0
  38. package/src/runs/shared/nested-events.ts +31 -0
  39. package/src/runs/shared/parallel-utils.ts +1 -0
  40. package/src/runs/shared/pi-args.ts +30 -1
  41. package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
  42. package/src/runs/shared/tool-budget.ts +74 -0
  43. package/src/runs/shared/turn-budget.ts +52 -0
  44. package/src/shared/artifacts.ts +1 -0
  45. package/src/shared/atomic-json.ts +15 -2
  46. package/src/shared/child-transcript.ts +212 -0
  47. package/src/shared/settings.ts +3 -1
  48. package/src/shared/types.ts +134 -19
  49. package/src/slash/prompt-workflows.ts +330 -0
  50. package/src/slash/slash-commands.ts +16 -2
  51. package/src/tui/render.ts +16 -8
  52. 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 { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
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
- export default function registerSubagentNotify(pi: ExtensionAPI): void {
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
- const now = Date.now();
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 agent = result.agent ?? "unknown";
65
- const summary = typeof result.summary === "string" ? result.summary : "";
66
- const paused = !result.success && (
67
- result.exitCode === 0
68
- || result.state === "paused"
69
- || summary.startsWith("Paused after interrupt.")
70
- );
71
- const status = paused ? "paused" : result.success ? "completed" : "failed";
72
-
73
- const taskInfo =
74
- result.taskIndex !== undefined && result.totalTasks !== undefined
75
- ? ` (${result.taskIndex + 1}/${result.totalTasks})`
76
- : "";
77
-
78
- const sessionLine = result.shareUrl
79
- ? `Session: ${result.shareUrl}`
80
- : result.shareError
81
- ? `Session share error: ${result.shareError}`
82
- : result.sessionFile
83
- ? `Session file: ${result.sessionFile}`
84
- : undefined;
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 && data.sessionId !== state.currentSessionId) return;
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
- return state ? [...state.foregroundControls.keys()] : [];
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
- lines.push(` ${index + 1}. ${step.agent} ${step.status}${activity ? `, ${activity}` : ""}${step.error ? `, error: ${step.error}` : ""}`);
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}`];