pi-subagents 0.30.0 → 0.31.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 +26 -0
- package/README.md +116 -17
- package/agents/context-builder.md +3 -3
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/scout.md +1 -1
- package/package.json +7 -7
- package/skills/pi-subagents/SKILL.md +5 -0
- package/src/agents/agent-management.ts +170 -6
- package/src/agents/agent-serializer.ts +31 -13
- package/src/agents/agents.ts +207 -23
- package/src/agents/frontmatter.ts +66 -2
- package/src/agents/skills.ts +117 -20
- package/src/extension/doctor.ts +20 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +47 -4
- package/src/extension/schemas.ts +10 -76
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/runs/background/async-execution.ts +14 -4
- package/src/runs/background/async-job-tracker.ts +56 -11
- package/src/runs/background/result-watcher.ts +11 -2
- package/src/runs/background/stale-run-reconciler.ts +9 -4
- package/src/runs/background/subagent-runner.ts +79 -3
- package/src/runs/foreground/chain-execution.ts +26 -2
- package/src/runs/foreground/execution.ts +113 -8
- package/src/runs/foreground/subagent-executor.ts +325 -77
- package/src/runs/shared/acceptance.ts +285 -34
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +4 -2
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +6 -1
- package/src/runs/shared/pi-args.ts +9 -1
- package/src/runs/shared/single-output.ts +15 -1
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +8 -2
- package/src/shared/utils.ts +19 -1
- package/src/slash/prompt-template-bridge.ts +26 -3
- package/src/slash/slash-commands.ts +33 -3
- package/src/tui/render.ts +265 -13
|
@@ -26,6 +26,10 @@ interface AsyncJobTrackerOptions {
|
|
|
26
26
|
now?: () => number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
|
|
30
|
+
const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
|
|
31
|
+
const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
32
|
+
|
|
29
33
|
export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
|
|
30
34
|
ensurePoller: () => void;
|
|
31
35
|
handleStarted: (data: unknown) => void;
|
|
@@ -68,25 +72,24 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
68
72
|
}
|
|
69
73
|
try {
|
|
70
74
|
const stat = fs.fstatSync(fd);
|
|
71
|
-
const
|
|
75
|
+
const savedCursor = job.controlEventCursor;
|
|
76
|
+
let cursor = stat.size < (savedCursor ?? 0) ? 0 : (savedCursor ?? 0);
|
|
77
|
+
const startedFromTail = savedCursor === undefined && stat.size > CONTROL_EVENT_SCAN_WINDOW_BYTES;
|
|
78
|
+
if (startedFromTail) cursor = stat.size - CONTROL_EVENT_SCAN_WINDOW_BYTES;
|
|
72
79
|
if (stat.size <= cursor) return;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (lastNewline === -1) return;
|
|
77
|
-
job.controlEventCursor = cursor + lastNewline + 1;
|
|
78
|
-
for (const line of buffer.subarray(0, lastNewline).toString("utf-8").split("\n")) {
|
|
79
|
-
if (!line.trim()) continue;
|
|
80
|
+
const scanEnd = Math.min(stat.size, cursor + CONTROL_EVENT_SCAN_WINDOW_BYTES);
|
|
81
|
+
const handleLine = (line: string) => {
|
|
82
|
+
if (!line.trim()) return;
|
|
80
83
|
let parsed: unknown;
|
|
81
84
|
try {
|
|
82
85
|
parsed = JSON.parse(line);
|
|
83
86
|
} catch (error) {
|
|
84
87
|
console.error(`Ignoring malformed async control event in '${eventsPath}':`, error);
|
|
85
|
-
|
|
88
|
+
return;
|
|
86
89
|
}
|
|
87
|
-
if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control")
|
|
90
|
+
if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control") return;
|
|
88
91
|
const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
|
|
89
|
-
if (!record.event || !Array.isArray(record.channels))
|
|
92
|
+
if (!record.event || !Array.isArray(record.channels)) return;
|
|
90
93
|
const payload = {
|
|
91
94
|
event: record.event,
|
|
92
95
|
source: "async" as const,
|
|
@@ -104,7 +107,48 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
104
107
|
message: record.intercom.message,
|
|
105
108
|
});
|
|
106
109
|
}
|
|
110
|
+
};
|
|
111
|
+
let readCursor = cursor;
|
|
112
|
+
let lastCompleteCursor = cursor;
|
|
113
|
+
let lineParts: Buffer[] = [];
|
|
114
|
+
let lineBytes = 0;
|
|
115
|
+
let skippingOversizedLine = startedFromTail;
|
|
116
|
+
const appendLineSegment = (segment: Buffer) => {
|
|
117
|
+
if (segment.length === 0 || skippingOversizedLine) return;
|
|
118
|
+
if (lineBytes + segment.length > MAX_CONTROL_EVENT_LINE_BYTES) {
|
|
119
|
+
lineParts = [];
|
|
120
|
+
lineBytes = 0;
|
|
121
|
+
skippingOversizedLine = true;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
lineParts.push(segment);
|
|
125
|
+
lineBytes += segment.length;
|
|
126
|
+
};
|
|
127
|
+
while (readCursor < scanEnd) {
|
|
128
|
+
const toRead = Math.min(CONTROL_EVENT_READ_CHUNK_BYTES, scanEnd - readCursor);
|
|
129
|
+
const buffer = Buffer.alloc(toRead);
|
|
130
|
+
const bytesRead = fs.readSync(fd, buffer, 0, toRead, readCursor);
|
|
131
|
+
if (bytesRead <= 0) break;
|
|
132
|
+
const chunk = bytesRead === buffer.length ? buffer : buffer.subarray(0, bytesRead);
|
|
133
|
+
let lineStart = 0;
|
|
134
|
+
for (let index = 0; index < chunk.length; index++) {
|
|
135
|
+
if (chunk[index] !== 0x0a) continue;
|
|
136
|
+
appendLineSegment(chunk.subarray(lineStart, index));
|
|
137
|
+
if (!skippingOversizedLine && lineBytes > 0) {
|
|
138
|
+
handleLine(Buffer.concat(lineParts, lineBytes).toString("utf-8"));
|
|
139
|
+
}
|
|
140
|
+
lineParts = [];
|
|
141
|
+
lineBytes = 0;
|
|
142
|
+
skippingOversizedLine = false;
|
|
143
|
+
lastCompleteCursor = readCursor + index + 1;
|
|
144
|
+
lineStart = index + 1;
|
|
145
|
+
}
|
|
146
|
+
appendLineSegment(chunk.subarray(lineStart));
|
|
147
|
+
readCursor += bytesRead;
|
|
148
|
+
if (skippingOversizedLine) job.controlEventCursor = readCursor;
|
|
107
149
|
}
|
|
150
|
+
if (lastCompleteCursor > cursor) job.controlEventCursor = lastCompleteCursor;
|
|
151
|
+
else if (scanEnd < stat.size || startedFromTail) job.controlEventCursor = scanEnd;
|
|
108
152
|
} catch (error) {
|
|
109
153
|
console.error(`Failed to read async control events for '${job.asyncDir}':`, error);
|
|
110
154
|
} finally {
|
|
@@ -261,6 +305,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
261
305
|
activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
|
|
262
306
|
startedAt: now,
|
|
263
307
|
updatedAt: now,
|
|
308
|
+
controlEventCursor: 0,
|
|
264
309
|
});
|
|
265
310
|
ensurePoller();
|
|
266
311
|
if (state.lastUiContext) {
|
|
@@ -21,7 +21,7 @@ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-
|
|
|
21
21
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
22
22
|
const POLL_INTERVAL_MS = 3000;
|
|
23
23
|
|
|
24
|
-
type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "watch">;
|
|
24
|
+
type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "realpathSync" | "watch">;
|
|
25
25
|
|
|
26
26
|
type ResultWatcherTimers = {
|
|
27
27
|
setTimeout: typeof setTimeout;
|
|
@@ -91,6 +91,14 @@ function shouldFallBackToPolling(error: unknown): boolean {
|
|
|
91
91
|
return code === "EMFILE" || code === "ENOSPC";
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function resolveNativeWatchDir(fsApi: ResultWatcherFs, resultsDir: string): string {
|
|
95
|
+
try {
|
|
96
|
+
return fsApi.realpathSync.native(resultsDir);
|
|
97
|
+
} catch {
|
|
98
|
+
return resultsDir;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
export function createResultWatcher(
|
|
95
103
|
pi: { events: IntercomEventBus },
|
|
96
104
|
state: SubagentState,
|
|
@@ -264,7 +272,8 @@ export function createResultWatcher(
|
|
|
264
272
|
state.watcherRestartTimer = null;
|
|
265
273
|
}
|
|
266
274
|
try {
|
|
267
|
-
|
|
275
|
+
const watchDir = resolveNativeWatchDir(fsApi, resultsDir);
|
|
276
|
+
state.watcher = fsApi.watch(watchDir, (ev, file) => {
|
|
268
277
|
if (ev !== "rename" || !file) return;
|
|
269
278
|
const fileName = file.toString();
|
|
270
279
|
if (!fileName.endsWith(".json")) return;
|
|
@@ -48,9 +48,14 @@ function isNotFoundError(error: unknown): boolean {
|
|
|
48
48
|
&& (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
function appendJsonlBestEffort(filePath: string, payload: object): void {
|
|
52
|
+
try {
|
|
53
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
54
|
+
fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
|
|
55
|
+
} catch {
|
|
56
|
+
// Repair status/result writes are the important path. A broken or full
|
|
57
|
+
// diagnostic event log must not make stale-run reconciliation fail.
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function readStatusFile(asyncDir: string): AsyncStatus | null {
|
|
@@ -223,7 +228,7 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
|
|
|
223
228
|
const repair = buildFailedRepair(status, asyncDir, now, reason);
|
|
224
229
|
writeAtomicJson(resultPath, repair.result);
|
|
225
230
|
writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
|
|
226
|
-
|
|
231
|
+
appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
|
|
227
232
|
type: "subagent.run.repaired_stale",
|
|
228
233
|
ts: now,
|
|
229
234
|
runId: repair.status.runId,
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import type { Message } from "@earendil-works/pi-ai";
|
|
6
6
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
|
-
import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
7
|
+
import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
8
8
|
import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
9
9
|
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
10
10
|
import {
|
|
@@ -131,6 +131,79 @@ interface StepResult {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
134
|
+
const DEFAULT_MAX_ASYNC_EVENTS_BYTES = 50 * 1024 * 1024;
|
|
135
|
+
const ASYNC_EVENTS_MAX_BYTES_ENV = "PI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES";
|
|
136
|
+
const TRUNCATED_EVENT_TYPE = "subagent.events.truncated";
|
|
137
|
+
const TRUNCATION_MARKER_RESERVE_BYTES = 512;
|
|
138
|
+
|
|
139
|
+
interface AsyncEventLogState {
|
|
140
|
+
bytes: number;
|
|
141
|
+
diagnosticsTruncated: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const asyncEventLogStates = new Map<string, AsyncEventLogState>();
|
|
145
|
+
|
|
146
|
+
function maxAsyncEventsBytes(): number {
|
|
147
|
+
const raw = process.env[ASYNC_EVENTS_MAX_BYTES_ENV];
|
|
148
|
+
if (!raw) return DEFAULT_MAX_ASYNC_EVENTS_BYTES;
|
|
149
|
+
const parsed = Number(raw);
|
|
150
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_MAX_ASYNC_EVENTS_BYTES;
|
|
151
|
+
return Math.floor(parsed);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function eventLogState(filePath: string): AsyncEventLogState {
|
|
155
|
+
let state = asyncEventLogStates.get(filePath);
|
|
156
|
+
if (state) return state;
|
|
157
|
+
let bytes = 0;
|
|
158
|
+
try {
|
|
159
|
+
bytes = fs.statSync(filePath).size;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
162
|
+
// Diagnostic event accounting is best-effort; writes below are also safe.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
state = { bytes, diagnosticsTruncated: false };
|
|
166
|
+
asyncEventLogStates.set(filePath, state);
|
|
167
|
+
return state;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function appendJsonl(filePath: string, line: string): void {
|
|
171
|
+
try {
|
|
172
|
+
appendRawJsonl(filePath, line);
|
|
173
|
+
const state = asyncEventLogStates.get(filePath);
|
|
174
|
+
if (state) state.bytes += Buffer.byteLength(`${line}\n`, "utf-8");
|
|
175
|
+
} catch {
|
|
176
|
+
// Async event logging is diagnostic and must not fail the run.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function appendDiagnosticJsonl(filePath: string, line: string, droppedEventType?: string): void {
|
|
181
|
+
if (!line.trim()) return;
|
|
182
|
+
const state = eventLogState(filePath);
|
|
183
|
+
if (state.diagnosticsTruncated) return;
|
|
184
|
+
const maxBytes = maxAsyncEventsBytes();
|
|
185
|
+
const chunkBytes = Buffer.byteLength(`${line}\n`, "utf-8");
|
|
186
|
+
const diagnosticBudget = Math.max(0, maxBytes - TRUNCATION_MARKER_RESERVE_BYTES);
|
|
187
|
+
if (state.bytes + chunkBytes <= diagnosticBudget) {
|
|
188
|
+
appendJsonl(filePath, line);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const marker = JSON.stringify({
|
|
193
|
+
type: TRUNCATED_EVENT_TYPE,
|
|
194
|
+
ts: Date.now(),
|
|
195
|
+
maxBytes,
|
|
196
|
+
droppedEventType,
|
|
197
|
+
});
|
|
198
|
+
if (state.bytes + Buffer.byteLength(`${marker}\n`, "utf-8") <= maxBytes) {
|
|
199
|
+
appendJsonl(filePath, marker);
|
|
200
|
+
}
|
|
201
|
+
state.diagnosticsTruncated = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
|
|
205
|
+
return event.type !== "message_update";
|
|
206
|
+
}
|
|
134
207
|
|
|
135
208
|
function findLatestSessionFile(sessionDir: string): string | null {
|
|
136
209
|
try {
|
|
@@ -274,14 +347,15 @@ function runPiStreaming(
|
|
|
274
347
|
|
|
275
348
|
const appendChildEvent = (event: Record<string, unknown>) => {
|
|
276
349
|
if (!childEventContext) return;
|
|
277
|
-
|
|
350
|
+
if (!shouldPersistChildEvent(event)) return;
|
|
351
|
+
appendDiagnosticJsonl(childEventContext.eventsPath, JSON.stringify({
|
|
278
352
|
...event,
|
|
279
353
|
subagentSource: "child",
|
|
280
354
|
subagentRunId: childEventContext.runId,
|
|
281
355
|
subagentStepIndex: childEventContext.stepIndex,
|
|
282
356
|
subagentAgent: childEventContext.agent,
|
|
283
357
|
observedAt: Date.now(),
|
|
284
|
-
}));
|
|
358
|
+
}), typeof event.type === "string" ? event.type : undefined);
|
|
285
359
|
};
|
|
286
360
|
|
|
287
361
|
const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
|
|
@@ -676,6 +750,7 @@ async function runSingleStep(
|
|
|
676
750
|
}
|
|
677
751
|
}
|
|
678
752
|
const { args, env, tempDir } = buildPiArgs({
|
|
753
|
+
parentSessionId: step.parentSessionId,
|
|
679
754
|
baseArgs: ["--mode", "json", "-p"],
|
|
680
755
|
task,
|
|
681
756
|
sessionEnabled,
|
|
@@ -684,6 +759,7 @@ async function runSingleStep(
|
|
|
684
759
|
model: candidate,
|
|
685
760
|
inheritProjectContext: step.inheritProjectContext,
|
|
686
761
|
inheritSkills: step.inheritSkills,
|
|
762
|
+
requireReadTool: Boolean(step.skills?.length),
|
|
687
763
|
tools: step.tools,
|
|
688
764
|
extensions: step.extensions,
|
|
689
765
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
@@ -102,6 +102,7 @@ interface ParallelChainRunInput {
|
|
|
102
102
|
globalTaskIndex: number;
|
|
103
103
|
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
104
104
|
sessionFileForIndex?: (idx?: number) => string | undefined;
|
|
105
|
+
sessionFileForTask?: (agentName: string, idx?: number) => string | undefined;
|
|
105
106
|
shareEnabled: boolean;
|
|
106
107
|
artifactConfig: ArtifactConfig;
|
|
107
108
|
artifactsDir: string;
|
|
@@ -136,6 +137,8 @@ interface ParallelChainRunInput {
|
|
|
136
137
|
worktreeSetup?: WorktreeSetup;
|
|
137
138
|
maxSubagentDepth: number;
|
|
138
139
|
nestedRoute?: NestedRouteInfo;
|
|
140
|
+
timeoutMs?: number;
|
|
141
|
+
deadlineAt?: number;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
|
|
@@ -266,6 +269,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
266
269
|
? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
|
|
267
270
|
: undefined;
|
|
268
271
|
const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
|
|
272
|
+
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
269
273
|
cwd: taskCwd,
|
|
270
274
|
signal: input.signal,
|
|
271
275
|
interruptSignal: interruptController.signal,
|
|
@@ -274,7 +278,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
274
278
|
runId: input.runId,
|
|
275
279
|
index: input.globalTaskIndex + taskIndex,
|
|
276
280
|
sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
|
|
277
|
-
sessionFile: input.
|
|
281
|
+
sessionFile: input.sessionFileForTask?.(task.agent, input.globalTaskIndex + taskIndex)
|
|
282
|
+
?? input.sessionFileForIndex?.(input.globalTaskIndex + taskIndex),
|
|
278
283
|
share: input.shareEnabled,
|
|
279
284
|
artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
|
|
280
285
|
artifactConfig: input.artifactConfig,
|
|
@@ -293,6 +298,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
293
298
|
structuredOutput: structuredRuntime,
|
|
294
299
|
acceptance: task.acceptance,
|
|
295
300
|
acceptanceContext: { mode: "chain" },
|
|
301
|
+
timeoutMs: input.timeoutMs,
|
|
302
|
+
deadlineAt: input.deadlineAt,
|
|
296
303
|
onUpdate: input.onUpdate
|
|
297
304
|
? (progressUpdate) => {
|
|
298
305
|
const stepResults = progressUpdate.details?.results || [];
|
|
@@ -365,6 +372,7 @@ interface ChainExecutionParams {
|
|
|
365
372
|
shareEnabled: boolean;
|
|
366
373
|
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
367
374
|
sessionFileForIndex?: (idx?: number) => string | undefined;
|
|
375
|
+
sessionFileForTask?: (agentName: string, idx?: number) => string | undefined;
|
|
368
376
|
artifactsDir: string;
|
|
369
377
|
artifactConfig: ArtifactConfig;
|
|
370
378
|
includeProgress?: boolean;
|
|
@@ -395,6 +403,8 @@ interface ChainExecutionParams {
|
|
|
395
403
|
nestedRoute?: NestedRouteInfo;
|
|
396
404
|
worktreeSetupHook?: string;
|
|
397
405
|
worktreeSetupHookTimeoutMs?: number;
|
|
406
|
+
timeoutMs?: number;
|
|
407
|
+
deadlineAt?: number;
|
|
398
408
|
}
|
|
399
409
|
|
|
400
410
|
interface ChainExecutionResult {
|
|
@@ -422,6 +432,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
422
432
|
shareEnabled,
|
|
423
433
|
sessionDirForIndex,
|
|
424
434
|
sessionFileForIndex,
|
|
435
|
+
sessionFileForTask,
|
|
425
436
|
artifactsDir,
|
|
426
437
|
artifactConfig,
|
|
427
438
|
includeProgress,
|
|
@@ -584,6 +595,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
584
595
|
tuiBehaviorOverrides = result.behaviorOverrides;
|
|
585
596
|
}
|
|
586
597
|
|
|
598
|
+
const deadlineAt = params.deadlineAt ?? (params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined);
|
|
587
599
|
let prev = "";
|
|
588
600
|
let globalTaskIndex = 0;
|
|
589
601
|
let progressCreated = false;
|
|
@@ -649,6 +661,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
649
661
|
globalTaskIndex,
|
|
650
662
|
sessionDirForIndex,
|
|
651
663
|
sessionFileForIndex,
|
|
664
|
+
sessionFileForTask,
|
|
652
665
|
shareEnabled,
|
|
653
666
|
artifactConfig,
|
|
654
667
|
artifactsDir,
|
|
@@ -670,6 +683,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
670
683
|
nestedRoute: params.nestedRoute,
|
|
671
684
|
worktreeSetup,
|
|
672
685
|
maxSubagentDepth: params.maxSubagentDepth,
|
|
686
|
+
timeoutMs: params.timeoutMs,
|
|
687
|
+
deadlineAt,
|
|
673
688
|
});
|
|
674
689
|
globalTaskIndex += step.parallel.length;
|
|
675
690
|
|
|
@@ -739,6 +754,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
739
754
|
output: getSingleResultOutput(result),
|
|
740
755
|
exitCode: result.exitCode,
|
|
741
756
|
error: result.error,
|
|
757
|
+
timedOut: result.timedOut,
|
|
742
758
|
outputTargetPath,
|
|
743
759
|
outputTargetExists: outputTargetPath ? fs.existsSync(outputTargetPath) : undefined,
|
|
744
760
|
};
|
|
@@ -855,6 +871,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
855
871
|
globalTaskIndex,
|
|
856
872
|
sessionDirForIndex,
|
|
857
873
|
sessionFileForIndex,
|
|
874
|
+
sessionFileForTask,
|
|
858
875
|
shareEnabled,
|
|
859
876
|
artifactConfig,
|
|
860
877
|
artifactsDir,
|
|
@@ -875,6 +892,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
875
892
|
foregroundControl,
|
|
876
893
|
nestedRoute: params.nestedRoute,
|
|
877
894
|
maxSubagentDepth: params.maxSubagentDepth,
|
|
895
|
+
timeoutMs: params.timeoutMs,
|
|
896
|
+
deadlineAt,
|
|
878
897
|
});
|
|
879
898
|
globalTaskIndex += dynamicParallelStep.parallel.length;
|
|
880
899
|
|
|
@@ -970,6 +989,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
970
989
|
output: getSingleResultOutput(result),
|
|
971
990
|
exitCode: result.exitCode,
|
|
972
991
|
error: result.error,
|
|
992
|
+
timedOut: result.timedOut,
|
|
973
993
|
}));
|
|
974
994
|
prev = aggregateParallelOutputs(taskResults, (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`);
|
|
975
995
|
} else {
|
|
@@ -1054,6 +1074,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1054
1074
|
? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
|
|
1055
1075
|
: undefined;
|
|
1056
1076
|
const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
1077
|
+
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1057
1078
|
cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
|
|
1058
1079
|
signal,
|
|
1059
1080
|
interruptSignal: interruptController.signal,
|
|
@@ -1062,7 +1083,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1062
1083
|
runId,
|
|
1063
1084
|
index: globalTaskIndex,
|
|
1064
1085
|
sessionDir: sessionDirForIndex(globalTaskIndex),
|
|
1065
|
-
sessionFile:
|
|
1086
|
+
sessionFile: sessionFileForTask?.(seqStep.agent, globalTaskIndex)
|
|
1087
|
+
?? sessionFileForIndex?.(globalTaskIndex),
|
|
1066
1088
|
share: shareEnabled,
|
|
1067
1089
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1068
1090
|
artifactConfig,
|
|
@@ -1081,6 +1103,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1081
1103
|
structuredOutput: structuredRuntime,
|
|
1082
1104
|
acceptance: seqStep.acceptance,
|
|
1083
1105
|
acceptanceContext: { mode: "chain" },
|
|
1106
|
+
timeoutMs: params.timeoutMs,
|
|
1107
|
+
deadlineAt,
|
|
1084
1108
|
onUpdate: onUpdate
|
|
1085
1109
|
? (p) => {
|
|
1086
1110
|
const stepResults = p.details?.results || [];
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
DEFAULT_MAX_OUTPUT,
|
|
24
24
|
INTERCOM_DETACH_REQUEST_EVENT,
|
|
25
25
|
INTERCOM_DETACH_RESPONSE_EVENT,
|
|
26
|
+
type AcceptanceLedger,
|
|
27
|
+
type ResolvedAcceptanceConfig,
|
|
26
28
|
truncateOutput,
|
|
27
29
|
getSubagentDepthEnv,
|
|
28
30
|
} from "../../shared/types.ts";
|
|
@@ -47,7 +49,7 @@ import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
|
|
|
47
49
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
48
50
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
49
51
|
import { readStructuredOutput } from "../shared/structured-output.ts";
|
|
50
|
-
import { captureSingleOutputSnapshot, formatSavedOutputReference, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
52
|
+
import { captureSingleOutputSnapshot, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
51
53
|
import {
|
|
52
54
|
buildModelCandidates,
|
|
53
55
|
formatModelAttemptNote,
|
|
@@ -82,6 +84,34 @@ function sumUsage(target: Usage, source: Usage): void {
|
|
|
82
84
|
target.turns += source.turns;
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
function formatTimeoutMessage(timeoutMs: number): string {
|
|
88
|
+
return `Subagent timed out after ${timeoutMs}ms.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveAttemptTimeout(options: RunSyncOptions): { timeoutMs: number; remainingMs: number; message: string } | undefined {
|
|
92
|
+
if (options.timeoutMs === undefined) return undefined;
|
|
93
|
+
const deadlineAt = options.deadlineAt ?? Date.now() + options.timeoutMs;
|
|
94
|
+
return {
|
|
95
|
+
timeoutMs: options.timeoutMs,
|
|
96
|
+
remainingMs: Math.max(0, deadlineAt - Date.now()),
|
|
97
|
+
message: formatTimeoutMessage(options.timeoutMs),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildTimedOutAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
|
|
102
|
+
return {
|
|
103
|
+
status: acceptance.level === "none" ? "not-required" : "rejected",
|
|
104
|
+
explicit: acceptance.explicit,
|
|
105
|
+
effectiveAcceptance: acceptance,
|
|
106
|
+
inferredReason: acceptance.inferredReason,
|
|
107
|
+
criteria: acceptance.criteria,
|
|
108
|
+
runtimeChecks: acceptance.level === "none"
|
|
109
|
+
? []
|
|
110
|
+
: [{ id: "timeout", status: "failed", message: "Acceptance was not evaluated because the subagent timed out." }],
|
|
111
|
+
verifyRuns: [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
|
|
86
116
|
if (lines.length === 0) return;
|
|
87
117
|
progress.recentOutput.push(...lines.filter((line) => line.trim()));
|
|
@@ -162,6 +192,7 @@ async function runSingleAttempt(
|
|
|
162
192
|
systemPromptMode: agent.systemPromptMode,
|
|
163
193
|
inheritProjectContext: agent.inheritProjectContext,
|
|
164
194
|
inheritSkills: agent.inheritSkills,
|
|
195
|
+
requireReadTool: Boolean(shared.resolvedSkillNames?.length),
|
|
165
196
|
tools: agent.tools,
|
|
166
197
|
extensions: agent.extensions,
|
|
167
198
|
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
@@ -178,6 +209,7 @@ async function runSingleAttempt(
|
|
|
178
209
|
parentControlInbox: options.nestedRoute?.controlInbox,
|
|
179
210
|
parentRootRunId: options.nestedRoute?.rootRunId,
|
|
180
211
|
parentCapabilityToken: options.nestedRoute?.capabilityToken,
|
|
212
|
+
parentSessionId: options.parentSessionId,
|
|
181
213
|
structuredOutput: options.structuredOutput,
|
|
182
214
|
});
|
|
183
215
|
|
|
@@ -227,6 +259,21 @@ async function runSingleAttempt(
|
|
|
227
259
|
lastActivityAt: startTime,
|
|
228
260
|
};
|
|
229
261
|
result.progress = progress;
|
|
262
|
+
const attemptTimeout = resolveAttemptTimeout(options);
|
|
263
|
+
if (attemptTimeout?.remainingMs === 0) {
|
|
264
|
+
result.exitCode = 1;
|
|
265
|
+
result.timedOut = true;
|
|
266
|
+
result.error = attemptTimeout.message;
|
|
267
|
+
result.finalOutput = attemptTimeout.message;
|
|
268
|
+
progress.status = "failed";
|
|
269
|
+
progress.error = attemptTimeout.message;
|
|
270
|
+
result.progressSummary = {
|
|
271
|
+
toolCount: progress.toolCount,
|
|
272
|
+
tokens: progress.tokens,
|
|
273
|
+
durationMs: progress.durationMs,
|
|
274
|
+
};
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
230
277
|
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
231
278
|
let observedMutationAttempt = false;
|
|
232
279
|
|
|
@@ -248,6 +295,23 @@ async function runSingleAttempt(
|
|
|
248
295
|
let removeAbortListener: (() => void) | undefined;
|
|
249
296
|
let removeInterruptListener: (() => void) | undefined;
|
|
250
297
|
let activityTimer: NodeJS.Timeout | undefined;
|
|
298
|
+
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
299
|
+
let timeoutTerminationTimer: NodeJS.Timeout | undefined;
|
|
300
|
+
let timeoutHardKillTimer: NodeJS.Timeout | undefined;
|
|
301
|
+
const clearTimeoutTimers = () => {
|
|
302
|
+
if (timeoutTimer) {
|
|
303
|
+
clearTimeout(timeoutTimer);
|
|
304
|
+
timeoutTimer = undefined;
|
|
305
|
+
}
|
|
306
|
+
if (timeoutTerminationTimer) {
|
|
307
|
+
clearTimeout(timeoutTerminationTimer);
|
|
308
|
+
timeoutTerminationTimer = undefined;
|
|
309
|
+
}
|
|
310
|
+
if (timeoutHardKillTimer) {
|
|
311
|
+
clearTimeout(timeoutHardKillTimer);
|
|
312
|
+
timeoutHardKillTimer = undefined;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
251
315
|
|
|
252
316
|
const detachForIntercom = () => {
|
|
253
317
|
detached = true;
|
|
@@ -316,6 +380,7 @@ async function runSingleAttempt(
|
|
|
316
380
|
settled = true;
|
|
317
381
|
clearFinalDrainTimers();
|
|
318
382
|
clearStdioGuard();
|
|
383
|
+
clearTimeoutTimers();
|
|
319
384
|
if (activityTimer) {
|
|
320
385
|
clearInterval(activityTimer);
|
|
321
386
|
activityTimer = undefined;
|
|
@@ -429,7 +494,8 @@ async function runSingleAttempt(
|
|
|
429
494
|
const fireUpdate = () => {
|
|
430
495
|
if (!options.onUpdate || processClosed) return;
|
|
431
496
|
progress.durationMs = Date.now() - startTime;
|
|
432
|
-
|
|
497
|
+
const output = result.timedOut && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages);
|
|
498
|
+
emitUpdateSnapshot(output || "(running...)");
|
|
433
499
|
};
|
|
434
500
|
|
|
435
501
|
const processLine = (line: string) => {
|
|
@@ -555,6 +621,31 @@ async function runSingleAttempt(
|
|
|
555
621
|
activityTimer.unref?.();
|
|
556
622
|
}
|
|
557
623
|
|
|
624
|
+
if (attemptTimeout) {
|
|
625
|
+
timeoutTimer = setTimeout(() => {
|
|
626
|
+
if (processClosed || settled || detached || interruptedByControl) return;
|
|
627
|
+
result.timedOut = true;
|
|
628
|
+
result.error = attemptTimeout.message;
|
|
629
|
+
result.finalOutput = attemptTimeout.message;
|
|
630
|
+
progress.status = "failed";
|
|
631
|
+
progress.error = attemptTimeout.message;
|
|
632
|
+
progress.durationMs = Date.now() - startTime;
|
|
633
|
+
fireUpdate();
|
|
634
|
+
trySignalChild(proc, "SIGINT");
|
|
635
|
+
timeoutTerminationTimer = setTimeout(() => {
|
|
636
|
+
if (processClosed || settled || detached) return;
|
|
637
|
+
trySignalChild(proc, "SIGTERM");
|
|
638
|
+
}, 1000);
|
|
639
|
+
timeoutTerminationTimer.unref?.();
|
|
640
|
+
timeoutHardKillTimer = setTimeout(() => {
|
|
641
|
+
if (processClosed || settled || detached) return;
|
|
642
|
+
trySignalChild(proc, "SIGKILL");
|
|
643
|
+
}, 4000);
|
|
644
|
+
timeoutHardKillTimer.unref?.();
|
|
645
|
+
}, attemptTimeout.remainingMs);
|
|
646
|
+
timeoutTimer.unref?.();
|
|
647
|
+
}
|
|
648
|
+
|
|
558
649
|
let stderrBuf = "";
|
|
559
650
|
|
|
560
651
|
const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
|
|
@@ -625,7 +716,9 @@ async function runSingleAttempt(
|
|
|
625
716
|
if (options.interruptSignal) {
|
|
626
717
|
const interrupt = () => {
|
|
627
718
|
if (processClosed || detached || settled) return;
|
|
719
|
+
if (result.timedOut) return;
|
|
628
720
|
interruptedByControl = true;
|
|
721
|
+
clearTimeoutTimers();
|
|
629
722
|
progress.status = "running";
|
|
630
723
|
progress.durationMs = Date.now() - startTime;
|
|
631
724
|
result.interrupted = true;
|
|
@@ -710,8 +803,14 @@ async function runSingleAttempt(
|
|
|
710
803
|
durationMs: progress.durationMs,
|
|
711
804
|
};
|
|
712
805
|
|
|
713
|
-
|
|
714
|
-
|
|
806
|
+
const acceptanceOutput = getFinalOutput(result.messages);
|
|
807
|
+
let fullOutput = stripAcceptanceReport(acceptanceOutput);
|
|
808
|
+
if (result.timedOut) {
|
|
809
|
+
const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
|
|
810
|
+
fullOutput = fullOutput.trim()
|
|
811
|
+
? `${timeoutMessage}\n\nPartial output before timeout:\n${fullOutput}`
|
|
812
|
+
: timeoutMessage;
|
|
813
|
+
}
|
|
715
814
|
const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
|
|
716
815
|
? evaluateCompletionMutationGuard({
|
|
717
816
|
agent: agent.name,
|
|
@@ -835,6 +934,7 @@ export async function runSync(
|
|
|
835
934
|
const skillInjection = buildSkillInjection(resolvedSkills);
|
|
836
935
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
|
|
837
936
|
}
|
|
937
|
+
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, options.outputPath);
|
|
838
938
|
|
|
839
939
|
const candidates = buildModelCandidates(
|
|
840
940
|
options.modelOverride ?? agent.model,
|
|
@@ -892,6 +992,9 @@ export async function runSync(
|
|
|
892
992
|
usage: { ...result.usage },
|
|
893
993
|
};
|
|
894
994
|
modelAttempts.push(attempt);
|
|
995
|
+
if (result.timedOut) {
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
895
998
|
if (attemptSucceeded) {
|
|
896
999
|
break;
|
|
897
1000
|
}
|
|
@@ -967,14 +1070,16 @@ export async function runSync(
|
|
|
967
1070
|
if (sessionFile) result.sessionFile = sessionFile;
|
|
968
1071
|
}
|
|
969
1072
|
|
|
970
|
-
|
|
1073
|
+
result.acceptance = result.timedOut
|
|
1074
|
+
? buildTimedOutAcceptanceLedger(effectiveAcceptance)
|
|
1075
|
+
: await evaluateAcceptance({
|
|
971
1076
|
acceptance: effectiveAcceptance,
|
|
972
1077
|
output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
|
|
973
1078
|
cwd: options.cwd ?? runtimeCwd,
|
|
974
1079
|
});
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1080
|
+
const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
|
|
1081
|
+
stripAcceptanceReportsFromMessages(result.messages);
|
|
1082
|
+
if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut) {
|
|
978
1083
|
result.exitCode = 1;
|
|
979
1084
|
result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
|
|
980
1085
|
if (result.progress) {
|