pi-subagents 0.29.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.
@@ -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
+ }
@@ -205,6 +205,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
205
205
  statusActivityText ? `Activity: ${statusActivityText}` : undefined,
206
206
  `Mode: ${status.mode}`,
207
207
  `Progress: ${progressLabel}`,
208
+ status.pendingAppends ? `Pending appends: ${status.pendingAppends}` : undefined,
208
209
  `Started: ${started}`,
209
210
  `Updated: ${updated}`,
210
211
  `Dir: ${asyncDir}`,
@@ -78,6 +78,8 @@ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
78
78
  import { writeInitialProgressFile } from "../../shared/settings.ts";
79
79
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
80
80
  import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
81
+ import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
82
+ import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
81
83
 
82
84
  interface SubagentRunConfig {
83
85
  id: string;
@@ -601,6 +603,30 @@ async function runSingleStep(
601
603
  structuredOutputSchemaPath?: string;
602
604
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
603
605
  }> {
606
+ if (step.importAsyncRoot) {
607
+ const imported = await waitForImportedAsyncRoot(step.importAsyncRoot);
608
+ try {
609
+ fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
610
+ } catch {
611
+ // Output files are observability only for imported roots.
612
+ }
613
+ return {
614
+ agent: imported.agent,
615
+ output: imported.output,
616
+ exitCode: imported.exitCode,
617
+ error: imported.error,
618
+ sessionFile: imported.sessionFile,
619
+ intercomTarget: imported.intercomTarget,
620
+ model: imported.model,
621
+ attemptedModels: imported.attemptedModels,
622
+ modelAttempts: imported.modelAttempts,
623
+ structuredOutput: imported.structuredOutput,
624
+ structuredOutputPath: imported.structuredOutputPath,
625
+ structuredOutputSchemaPath: imported.structuredOutputSchemaPath,
626
+ acceptance: imported.acceptance,
627
+ };
628
+ }
629
+
604
630
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
605
631
  ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
606
632
  : undefined);
@@ -660,6 +686,7 @@ async function runSingleStep(
660
686
  inheritSkills: step.inheritSkills,
661
687
  tools: step.tools,
662
688
  extensions: step.extensions,
689
+ subagentOnlyExtensions: step.subagentOnlyExtensions,
663
690
  systemPrompt: step.systemPrompt,
664
691
  systemPromptMode: step.systemPromptMode,
665
692
  mcpDirectTools: step.mcpDirectTools,
@@ -1112,6 +1139,45 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1112
1139
  writeAtomicJson(statusPath, statusPayload);
1113
1140
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1114
1141
  };
1142
+ const consumePendingAppendRequests = (): void => {
1143
+ if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
1144
+ const requests = consumeChainAppendRequests(asyncDir);
1145
+ if (requests.length === 0) {
1146
+ const pendingAppends = countPendingChainAppendRequests(asyncDir);
1147
+ if ((statusPayload.pendingAppends ?? 0) !== pendingAppends) {
1148
+ statusPayload.pendingAppends = pendingAppends;
1149
+ statusPayload.lastUpdate = Date.now();
1150
+ writeStatusPayload();
1151
+ }
1152
+ return;
1153
+ }
1154
+ const appendedSteps = requests.flatMap((request) => request.steps);
1155
+ steps.push(...appendedSteps);
1156
+ const now = Date.now();
1157
+ const pendingAppends = countPendingChainAppendRequests(asyncDir);
1158
+ const added = appendRunnerStepsToStatus({
1159
+ status: statusPayload,
1160
+ steps: appendedSteps,
1161
+ now,
1162
+ pendingAppends,
1163
+ });
1164
+ mutatingFailureStates.push(...Array.from({ length: added.addedFlatSteps }, () => createMutatingFailureState()));
1165
+ pendingToolResults.push(...Array.from({ length: added.addedFlatSteps }, () => undefined));
1166
+ if (config.childIntercomTargets) {
1167
+ config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
1168
+ }
1169
+ writeStatusPayload();
1170
+ for (const request of requests) {
1171
+ appendJsonl(eventsPath, JSON.stringify({
1172
+ type: "subagent.chain.append.accepted",
1173
+ ts: now,
1174
+ runId: id,
1175
+ requestId: request.id,
1176
+ stepCount: request.steps.length,
1177
+ pendingAppends,
1178
+ }));
1179
+ }
1180
+ };
1115
1181
  const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
1116
1182
  const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
1117
1183
  if (!groupNode) return;
@@ -1403,10 +1469,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1403
1469
  );
1404
1470
 
1405
1471
  let flatIndex = 0;
1472
+ let stepCursor = 0;
1406
1473
 
1407
- for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
1474
+ while (true) {
1408
1475
  if (interrupted) break;
1409
- const step = steps[stepIndex];
1476
+ consumePendingAppendRequests();
1477
+ if (stepCursor >= steps.length) break;
1478
+ const stepIndex = stepCursor++;
1479
+ const step = steps[stepIndex]!;
1410
1480
 
1411
1481
  if (isDynamicRunnerGroup(step)) {
1412
1482
  const groupStartFlatIndex = flatIndex;
@@ -1835,7 +1905,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1835
1905
  outputs,
1836
1906
  sessionDir: taskSessionDir,
1837
1907
  artifactsDir, artifactConfig, id,
1838
- flatIndex: fi, flatStepCount: flatSteps.length,
1908
+ flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
1839
1909
  outputFile: path.join(asyncDir, `output-${fi}.log`),
1840
1910
  piPackageRoot: config.piPackageRoot,
1841
1911
  piArgv1: config.piArgv1,
@@ -2000,7 +2070,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2000
2070
  outputs,
2001
2071
  sessionDir: config.sessionDir,
2002
2072
  artifactsDir, artifactConfig, id,
2003
- flatIndex, flatStepCount: flatSteps.length,
2073
+ flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
2004
2074
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
2005
2075
  piPackageRoot: config.piPackageRoot,
2006
2076
  piArgv1: config.piArgv1,
@@ -2131,11 +2201,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2131
2201
  }
2132
2202
 
2133
2203
  const resultMode = config.resultMode ?? statusPayload.mode;
2134
- const agentName = flatSteps.length === 1
2135
- ? flatSteps[0].agent
2204
+ const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
2205
+ const agentName = finalFlatAgents.length === 1
2206
+ ? finalFlatAgents[0]!
2136
2207
  : resultMode === "parallel"
2137
- ? `parallel:${flatSteps.map((s) => s.agent).join("+")}`
2138
- : `chain:${flatSteps.map((s) => s.agent).join("->")}`;
2208
+ ? `parallel:${finalFlatAgents.join("+")}`
2209
+ : `chain:${finalFlatAgents.join("->")}`;
2139
2210
  let sessionFile: string | undefined;
2140
2211
  let shareUrl: string | undefined;
2141
2212
  let gistUrl: string | undefined;
@@ -164,6 +164,7 @@ async function runSingleAttempt(
164
164
  inheritSkills: agent.inheritSkills,
165
165
  tools: agent.tools,
166
166
  extensions: agent.extensions,
167
+ subagentOnlyExtensions: agent.subagentOnlyExtensions,
167
168
  systemPrompt: shared.systemPrompt,
168
169
  mcpDirectTools: agent.mcpDirectTools,
169
170
  cwd: options.cwd ?? runtimeCwd,