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.
- package/CHANGELOG.md +17 -0
- package/README.md +9 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +25 -0
- package/src/agents/agent-management.ts +19 -2
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +36 -1
- package/src/agents/proactive-skills.ts +191 -0
- package/src/extension/fanout-child.ts +1 -1
- package/src/extension/index.ts +3 -1
- package/src/extension/schemas.ts +32 -5
- package/src/runs/background/async-execution.ts +166 -63
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/subagent-runner.ts +79 -8
- package/src/runs/foreground/execution.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +288 -12
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +3 -2
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/shared/types.ts +10 -1
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
|
@@ -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
|
-
|
|
1474
|
+
while (true) {
|
|
1408
1475
|
if (interrupted) break;
|
|
1409
|
-
|
|
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:
|
|
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:
|
|
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
|
|
2135
|
-
|
|
2204
|
+
const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
|
|
2205
|
+
const agentName = finalFlatAgents.length === 1
|
|
2206
|
+
? finalFlatAgents[0]!
|
|
2136
2207
|
: resultMode === "parallel"
|
|
2137
|
-
? `parallel:${
|
|
2138
|
-
: `chain:${
|
|
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,
|