pi-subagents 0.28.0 → 0.30.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 (47) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +26 -62
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +29 -35
  5. package/src/agents/agent-management.ts +29 -22
  6. package/src/agents/agent-selection.ts +2 -0
  7. package/src/agents/agent-serializer.ts +5 -10
  8. package/src/agents/agents.ts +339 -47
  9. package/src/agents/chain-serializer.ts +4 -9
  10. package/src/agents/proactive-skills.ts +191 -0
  11. package/src/extension/doctor.ts +4 -3
  12. package/src/extension/fanout-child.ts +1 -3
  13. package/src/extension/index.ts +6 -9
  14. package/src/extension/schemas.ts +63 -26
  15. package/src/intercom/intercom-bridge.ts +11 -1
  16. package/src/intercom/result-intercom.ts +0 -5
  17. package/src/runs/background/async-execution.ts +186 -74
  18. package/src/runs/background/async-resume.ts +53 -5
  19. package/src/runs/background/async-status.ts +4 -1
  20. package/src/runs/background/chain-append.ts +282 -0
  21. package/src/runs/background/chain-root-attachment.ts +161 -0
  22. package/src/runs/background/run-status.ts +2 -7
  23. package/src/runs/background/subagent-runner.ts +160 -219
  24. package/src/runs/foreground/chain-execution.ts +62 -58
  25. package/src/runs/foreground/execution.ts +39 -343
  26. package/src/runs/foreground/subagent-executor.ts +316 -111
  27. package/src/runs/shared/acceptance.ts +605 -22
  28. package/src/runs/shared/chain-outputs.ts +23 -8
  29. package/src/runs/shared/completion-guard.ts +3 -26
  30. package/src/runs/shared/dynamic-fanout.ts +1 -1
  31. package/src/runs/shared/model-fallback.ts +38 -0
  32. package/src/runs/shared/parallel-utils.ts +13 -10
  33. package/src/runs/shared/pi-args.ts +3 -2
  34. package/src/runs/shared/subagent-control.ts +8 -11
  35. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  36. package/src/runs/shared/workflow-graph.ts +2 -6
  37. package/src/shared/atomic-json.ts +68 -11
  38. package/src/shared/settings.ts +1 -0
  39. package/src/shared/types.ts +20 -49
  40. package/src/shared/utils.ts +2 -8
  41. package/src/slash/slash-bridge.ts +3 -1
  42. package/src/slash/slash-commands.ts +1 -1
  43. package/src/tui/render.ts +14 -29
  44. package/src/runs/shared/acceptance-contract.ts +0 -318
  45. package/src/runs/shared/acceptance-evaluation.ts +0 -221
  46. package/src/runs/shared/acceptance-finalization.ts +0 -173
  47. package/src/runs/shared/acceptance-reports.ts +0 -127
@@ -0,0 +1,282 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
5
+ import { appendJsonl } from "../../shared/artifacts.ts";
6
+ import type { AsyncParallelGroupStatus, AsyncStatus, WorkflowGraphNode, WorkflowGraphSnapshot } from "../../shared/types.ts";
7
+ import { readStatus } from "../../shared/utils.ts";
8
+ import type { DynamicRunnerGroup, ParallelStepGroup, RunnerStep, RunnerSubagentStep } from "../shared/parallel-utils.ts";
9
+ import { isDynamicRunnerGroup, isParallelGroup } from "../shared/parallel-utils.ts";
10
+
11
+ const APPEND_REQUESTS_DIR = "append-requests";
12
+
13
+ export interface ChainAppendRequest {
14
+ id: string;
15
+ createdAt: number;
16
+ steps: RunnerStep[];
17
+ }
18
+
19
+ export interface ChainAppendResult {
20
+ request: ChainAppendRequest;
21
+ pendingCount: number;
22
+ }
23
+
24
+ type StatusStep = NonNullable<AsyncStatus["steps"]>[number];
25
+
26
+ function appendDir(asyncDir: string): string {
27
+ return path.join(asyncDir, APPEND_REQUESTS_DIR);
28
+ }
29
+
30
+ function appendRequestPath(asyncDir: string, request: ChainAppendRequest): string {
31
+ return path.join(appendDir(asyncDir), `${request.createdAt}-${request.id}.json`);
32
+ }
33
+
34
+ function listAppendRequestFiles(asyncDir: string): string[] {
35
+ const dir = appendDir(asyncDir);
36
+ try {
37
+ return fs.readdirSync(dir)
38
+ .filter((entry) => entry.endsWith(".json"))
39
+ .map((entry) => path.join(dir, entry))
40
+ .sort();
41
+ } catch (error) {
42
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ export function countPendingChainAppendRequests(asyncDir: string): number {
48
+ return listAppendRequestFiles(asyncDir).length;
49
+ }
50
+
51
+ export function runnerStepOutputNames(steps: RunnerStep[]): string[] {
52
+ const names: string[] = [];
53
+ for (const step of steps) {
54
+ if (isParallelGroup(step)) {
55
+ names.push(...step.parallel.map((task) => task.outputName).filter((name): name is string => Boolean(name)));
56
+ } else if (isDynamicRunnerGroup(step)) {
57
+ if (step.collect.as) names.push(step.collect.as);
58
+ } else if (step.outputName) {
59
+ names.push(step.outputName);
60
+ }
61
+ }
62
+ return names;
63
+ }
64
+
65
+ export function enqueueChainAppendRequest(input: {
66
+ asyncDir: string;
67
+ runId: string;
68
+ steps: RunnerStep[];
69
+ now?: number;
70
+ }): ChainAppendResult {
71
+ const status = readStatus(input.asyncDir);
72
+ if (!status) throw new Error(`No async run status found for '${input.runId}'.`);
73
+ if (status.runId !== input.runId) throw new Error(`Async run id mismatch: expected '${input.runId}', found '${status.runId}'.`);
74
+ if (status.mode !== "chain") throw new Error(`Run '${input.runId}' is ${status.mode}; only active chain runs accept appended steps.`);
75
+ if (status.state !== "running") throw new Error(`Run '${input.runId}' is ${status.state}; only running chain runs accept appended steps.`);
76
+ const stillInProgress = (status.steps ?? []).some((step) => step.status === "running" || step.status === "pending") || (status.pendingAppends ?? 0) > 0;
77
+ if (!stillInProgress) throw new Error(`Run '${input.runId}' has no running or pending chain steps left; append-step must target an in-progress chain.`);
78
+ if (input.steps.length === 0) throw new Error("append-step requires one chain step.");
79
+
80
+ const request: ChainAppendRequest = {
81
+ id: randomUUID(),
82
+ createdAt: input.now ?? Date.now(),
83
+ steps: input.steps,
84
+ };
85
+ fs.mkdirSync(appendDir(input.asyncDir), { recursive: true });
86
+ writeAtomicJson(appendRequestPath(input.asyncDir, request), request);
87
+ const pendingCount = countPendingChainAppendRequests(input.asyncDir);
88
+ const statusPath = path.join(input.asyncDir, "status.json");
89
+ const updatedStatus = { ...status, pendingAppends: pendingCount, lastUpdate: request.createdAt };
90
+ writeAtomicJson(statusPath, updatedStatus);
91
+ appendJsonl(path.join(input.asyncDir, "events.jsonl"), JSON.stringify({
92
+ type: "subagent.chain.append.requested",
93
+ ts: request.createdAt,
94
+ runId: input.runId,
95
+ requestId: request.id,
96
+ stepCount: input.steps.length,
97
+ pendingAppends: pendingCount,
98
+ }));
99
+ return { request, pendingCount };
100
+ }
101
+
102
+ function readAppendRequest(filePath: string): ChainAppendRequest | undefined {
103
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Partial<ChainAppendRequest>;
104
+ if (!raw.id || typeof raw.id !== "string") return undefined;
105
+ if (!Number.isFinite(raw.createdAt)) return undefined;
106
+ if (!Array.isArray(raw.steps) || raw.steps.length === 0) return undefined;
107
+ return { id: raw.id, createdAt: raw.createdAt, steps: raw.steps as RunnerStep[] };
108
+ }
109
+
110
+ export function readPendingChainAppendRequests(asyncDir: string): ChainAppendRequest[] {
111
+ return listAppendRequestFiles(asyncDir)
112
+ .map((filePath) => readAppendRequest(filePath))
113
+ .filter((request): request is ChainAppendRequest => Boolean(request))
114
+ .sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
115
+ }
116
+
117
+ export function consumeChainAppendRequests(asyncDir: string): ChainAppendRequest[] {
118
+ const requests: ChainAppendRequest[] = [];
119
+ for (const filePath of listAppendRequestFiles(asyncDir)) {
120
+ const request = readAppendRequest(filePath);
121
+ try {
122
+ fs.unlinkSync(filePath);
123
+ } catch {
124
+ // The runner should not execute a consumed request twice.
125
+ }
126
+ if (request) requests.push(request);
127
+ }
128
+ return requests.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
129
+ }
130
+
131
+ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
132
+ return {
133
+ agent: task.agent,
134
+ phase: task.phase,
135
+ label: task.label,
136
+ outputName: task.outputName,
137
+ structured: task.structured,
138
+ status: "pending",
139
+ ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
140
+ skills: task.skills,
141
+ model: task.model,
142
+ thinking: task.thinking,
143
+ attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
144
+ recentTools: [],
145
+ recentOutput: [],
146
+ };
147
+ }
148
+
149
+ function statusStepsForRunnerStep(step: RunnerStep): StatusStep[] {
150
+ if (isParallelGroup(step)) return step.parallel.map(statusStepForTask);
151
+ if (isDynamicRunnerGroup(step)) {
152
+ return [{
153
+ agent: `expand:${step.parallel.agent}`,
154
+ phase: step.phase ?? step.parallel.phase,
155
+ label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
156
+ outputName: step.collect.as,
157
+ structured: Boolean(step.collect.outputSchema),
158
+ status: "pending",
159
+ recentTools: [],
160
+ recentOutput: [],
161
+ }];
162
+ }
163
+ return [statusStepForTask(step)];
164
+ }
165
+
166
+ function pushPhase(graph: WorkflowGraphSnapshot, phase: string | undefined, nodeId: string): void {
167
+ if (!phase) return;
168
+ let group = graph.phases.find((candidate) => candidate.title === phase);
169
+ if (!group) {
170
+ group = { title: phase, nodeIds: [] };
171
+ graph.phases.push(group);
172
+ }
173
+ group.nodeIds.push(nodeId);
174
+ }
175
+
176
+ function graphNodeForSequential(step: RunnerSubagentStep, stepIndex: number, flatIndex: number): WorkflowGraphNode {
177
+ return {
178
+ id: `step-${stepIndex}`,
179
+ kind: "step",
180
+ agent: step.agent,
181
+ phase: step.phase,
182
+ label: step.label?.trim() || step.agent || `Step ${stepIndex + 1}`,
183
+ status: "pending",
184
+ flatIndex,
185
+ stepIndex,
186
+ outputName: step.outputName,
187
+ structured: step.structured,
188
+ };
189
+ }
190
+
191
+ function graphNodeForParallel(step: ParallelStepGroup, stepIndex: number, flatIndex: number, graph: WorkflowGraphSnapshot): WorkflowGraphNode {
192
+ const children = step.parallel.map((task, taskIndex) => {
193
+ const childId = `step-${stepIndex}-agent-${taskIndex}`;
194
+ pushPhase(graph, task.phase, childId);
195
+ return {
196
+ id: childId,
197
+ kind: "agent" as const,
198
+ agent: task.agent,
199
+ phase: task.phase,
200
+ label: task.label?.trim() || task.agent || `Agent ${taskIndex + 1}`,
201
+ status: "pending" as const,
202
+ flatIndex: flatIndex + taskIndex,
203
+ stepIndex,
204
+ outputName: task.outputName,
205
+ structured: task.structured,
206
+ };
207
+ });
208
+ return {
209
+ id: `step-${stepIndex}`,
210
+ kind: "parallel-group",
211
+ label: step.parallel.length === 1 ? "Parallel task" : `Parallel group (${step.parallel.length})`,
212
+ status: "pending",
213
+ stepIndex,
214
+ children,
215
+ };
216
+ }
217
+
218
+ function graphNodeForDynamic(step: DynamicRunnerGroup, stepIndex: number): WorkflowGraphNode {
219
+ return {
220
+ id: `step-${stepIndex}`,
221
+ kind: "dynamic-parallel-group",
222
+ label: step.label?.trim() || step.parallel.label?.trim() || `Dynamic fanout (${step.collect.as})`,
223
+ status: "pending",
224
+ stepIndex,
225
+ outputName: step.collect.as,
226
+ structured: Boolean(step.collect.outputSchema),
227
+ dynamic: {
228
+ sourceOutput: step.expand.from.output,
229
+ sourcePath: step.expand.from.path,
230
+ itemName: step.expand.item ?? "item",
231
+ maxItems: step.expand.maxItems,
232
+ collectAs: step.collect.as,
233
+ },
234
+ children: [],
235
+ };
236
+ }
237
+
238
+ function appendWorkflowNode(graph: WorkflowGraphSnapshot | undefined, step: RunnerStep, stepIndex: number, flatIndex: number): void {
239
+ if (!graph) return;
240
+ if (isParallelGroup(step)) {
241
+ graph.nodes.push(graphNodeForParallel(step, stepIndex, flatIndex, graph));
242
+ return;
243
+ }
244
+ if (isDynamicRunnerGroup(step)) {
245
+ graph.nodes.push(graphNodeForDynamic(step, stepIndex));
246
+ return;
247
+ }
248
+ const node = graphNodeForSequential(step, stepIndex, flatIndex);
249
+ graph.nodes.push(node);
250
+ pushPhase(graph, step.phase, node.id);
251
+ }
252
+
253
+ export function appendRunnerStepsToStatus(input: {
254
+ status: AsyncStatus;
255
+ steps: RunnerStep[];
256
+ now?: number;
257
+ pendingAppends?: number;
258
+ }): { addedChainSteps: number; addedFlatSteps: number } {
259
+ let addedChainSteps = 0;
260
+ let addedFlatSteps = 0;
261
+ for (const step of input.steps) {
262
+ const stepIndex = input.status.chainStepCount ?? input.status.steps?.length ?? 0;
263
+ const flatIndex = input.status.steps?.length ?? 0;
264
+ const statusSteps = statusStepsForRunnerStep(step);
265
+ input.status.steps ??= [];
266
+ input.status.steps.push(...statusSteps);
267
+ if (isParallelGroup(step)) {
268
+ input.status.parallelGroups ??= [];
269
+ input.status.parallelGroups.push({ start: flatIndex, count: step.parallel.length, stepIndex });
270
+ } else if (isDynamicRunnerGroup(step)) {
271
+ input.status.parallelGroups ??= [];
272
+ input.status.parallelGroups.push({ start: flatIndex, count: 1, stepIndex });
273
+ }
274
+ appendWorkflowNode(input.status.workflowGraph, step, stepIndex, flatIndex);
275
+ input.status.chainStepCount = stepIndex + 1;
276
+ addedChainSteps++;
277
+ addedFlatSteps += statusSteps.length;
278
+ }
279
+ input.status.pendingAppends = input.pendingAppends ?? 0;
280
+ input.status.lastUpdate = input.now ?? Date.now();
281
+ return { addedChainSteps, addedFlatSteps };
282
+ }
@@ -0,0 +1,161 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AcceptanceLedger, AsyncStatus, ModelAttempt } from "../../shared/types.ts";
4
+ import { readStatus } from "../../shared/utils.ts";
5
+
6
+ export interface ImportedAsyncRoot {
7
+ runId: string;
8
+ asyncDir: string;
9
+ resultPath: string;
10
+ index: number;
11
+ }
12
+
13
+ export interface ImportedAsyncRootResult {
14
+ agent: string;
15
+ output: string;
16
+ success: boolean;
17
+ exitCode: number;
18
+ error?: string;
19
+ sessionFile?: string;
20
+ intercomTarget?: string;
21
+ model?: string;
22
+ attemptedModels?: string[];
23
+ modelAttempts?: ModelAttempt[];
24
+ structuredOutput?: unknown;
25
+ structuredOutputPath?: string;
26
+ structuredOutputSchemaPath?: string;
27
+ acceptance?: AcceptanceLedger;
28
+ }
29
+
30
+ interface AsyncResultFile {
31
+ state?: string;
32
+ success?: boolean;
33
+ summary?: string;
34
+ results?: Array<{
35
+ agent?: string;
36
+ output?: string;
37
+ error?: string;
38
+ success?: boolean;
39
+ sessionFile?: string;
40
+ intercomTarget?: string;
41
+ model?: string;
42
+ attemptedModels?: string[];
43
+ modelAttempts?: ModelAttempt[];
44
+ structuredOutput?: unknown;
45
+ structuredOutputPath?: string;
46
+ structuredOutputSchemaPath?: string;
47
+ acceptance?: AcceptanceLedger;
48
+ }>;
49
+ }
50
+
51
+ const TERMINAL_STATES = new Set(["complete", "failed", "paused"]);
52
+ const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "paused"]);
53
+
54
+ function readResultFile(resultPath: string): AsyncResultFile | undefined {
55
+ try {
56
+ return JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultFile;
57
+ } catch (error) {
58
+ if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
59
+ return undefined;
60
+ }
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ function selectedStatusStep(status: AsyncStatus | null, index: number): NonNullable<AsyncStatus["steps"]>[number] | undefined {
66
+ return status?.steps?.[index];
67
+ }
68
+
69
+ function isTerminalStatus(status: AsyncStatus | null, index: number): boolean {
70
+ if (!status) return false;
71
+ const step = selectedStatusStep(status, index);
72
+ if (step && TERMINAL_STEP_STATUSES.has(step.status)) return true;
73
+ return TERMINAL_STATES.has(status.state);
74
+ }
75
+
76
+ function resultState(result: AsyncResultFile | undefined, child: NonNullable<AsyncResultFile["results"]>[number] | undefined): "complete" | "failed" | "paused" | undefined {
77
+ if (!result) return undefined;
78
+ if (child?.success === true) return "complete";
79
+ if (child?.success === false) return result.state === "paused" ? "paused" : "failed";
80
+ if (result.state === "complete" || result.state === "failed" || result.state === "paused") return result.state;
81
+ if (result.success === true) return "complete";
82
+ if (result.success === false) return "failed";
83
+ return undefined;
84
+ }
85
+
86
+ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus, step: NonNullable<AsyncStatus["steps"]>[number] | undefined): ImportedAsyncRootResult {
87
+ const agent = step?.agent ?? status.steps?.[root.index]?.agent ?? "subagent";
88
+ const message = step?.error ?? status.error ?? `Attached async root ${root.runId} ended without a result file at ${root.resultPath}.`;
89
+ return {
90
+ agent,
91
+ output: message,
92
+ success: false,
93
+ exitCode: 1,
94
+ error: message,
95
+ ...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
96
+ ...(step?.model ? { model: step.model } : {}),
97
+ ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
98
+ ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
99
+ ...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
100
+ ...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
101
+ ...(step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: step.structuredOutputSchemaPath } : {}),
102
+ ...(step?.acceptance ? { acceptance: step.acceptance } : {}),
103
+ };
104
+ }
105
+
106
+ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null, result: AsyncResultFile): ImportedAsyncRootResult {
107
+ const child = result.results?.[root.index];
108
+ const step = selectedStatusStep(status, root.index);
109
+ const state = resultState(result, child);
110
+ const agent = child?.agent ?? step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent";
111
+ const output = child?.output ?? result.summary ?? "";
112
+ const success = state === "complete";
113
+ const error = child?.error ?? (success ? undefined : result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
114
+ return {
115
+ agent,
116
+ output: success ? output : (output || error || ""),
117
+ success,
118
+ exitCode: success ? 0 : 1,
119
+ ...(error ? { error } : {}),
120
+ ...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
121
+ ...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
122
+ ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
123
+ ...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
124
+ ...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
125
+ ...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
126
+ ...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
127
+ ...(child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath } : {}),
128
+ ...(child?.acceptance ?? step?.acceptance ? { acceptance: child?.acceptance ?? step?.acceptance } : {}),
129
+ };
130
+ }
131
+
132
+ export async function waitForImportedAsyncRoot(
133
+ root: ImportedAsyncRoot,
134
+ options: { pollIntervalMs?: number; terminalResultGraceMs?: number; now?: () => number } = {},
135
+ ): Promise<ImportedAsyncRootResult> {
136
+ const pollIntervalMs = options.pollIntervalMs ?? 500;
137
+ const terminalResultGraceMs = options.terminalResultGraceMs ?? 1_000;
138
+ const now = options.now ?? Date.now;
139
+ let terminalSince: number | undefined;
140
+ for (;;) {
141
+ const status = readStatus(root.asyncDir);
142
+ const result = readResultFile(root.resultPath);
143
+ if (result) return buildImportedResult(root, status, result);
144
+ if (isTerminalStatus(status, root.index)) {
145
+ terminalSince ??= now();
146
+ if (now() - terminalSince >= terminalResultGraceMs) {
147
+ return outputFromTerminalStatus(root, status!, selectedStatusStep(status, root.index));
148
+ }
149
+ } else {
150
+ terminalSince = undefined;
151
+ }
152
+ if (!status && !fs.existsSync(root.asyncDir)) {
153
+ throw new Error(`Attached async root '${root.runId}' directory does not exist: ${root.asyncDir}`);
154
+ }
155
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
156
+ }
157
+ }
158
+
159
+ export function resolveAsyncRootResultPath(resultsDir: string, runId: string): string {
160
+ return path.join(resultsDir, `${runId}.json`);
161
+ }
@@ -49,11 +49,6 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
49
49
  return "Resume: unavailable; no child session file was persisted.";
50
50
  }
51
51
 
52
- function formatAcceptanceFinalizationSummary(finalization: NonNullable<NonNullable<AsyncStatus["steps"]>[number]["acceptance"]>["finalization"] | undefined): string {
53
- if (!finalization) return "";
54
- return `, finalization: ${finalization.status} after ${finalization.turns.length}/${finalization.maxTurns} turns`;
55
- }
56
-
57
52
  function stepLineLabel(status: AsyncStatus, index: number): string {
58
53
  const steps = status.steps ?? [];
59
54
  if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
@@ -210,6 +205,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
210
205
  statusActivityText ? `Activity: ${statusActivityText}` : undefined,
211
206
  `Mode: ${status.mode}`,
212
207
  `Progress: ${progressLabel}`,
208
+ status.pendingAppends ? `Pending appends: ${status.pendingAppends}` : undefined,
213
209
  `Started: ${started}`,
214
210
  `Updated: ${updated}`,
215
211
  `Dir: ${asyncDir}`,
@@ -222,8 +218,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
222
218
  const modelThinking = formatModelThinking(step.model, step.thinking);
223
219
  const modelText = modelThinking ? ` (${modelThinking})` : "";
224
220
  const errorText = step.error ? `, error: ${step.error}` : "";
225
- const finalizationText = formatAcceptanceFinalizationSummary(step.acceptance?.finalization);
226
- const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}${finalizationText}` : "";
221
+ const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}` : "";
227
222
  const display = step.label ? `${step.label} (${step.agent})` : step.agent;
228
223
  const phase = step.phase ? `[${step.phase}] ` : "";
229
224
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);