pi-subagents 0.34.0 → 0.35.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 +78 -9
- package/README.md +213 -32
- package/index.ts +1 -0
- package/install.mjs +1 -1
- package/package.json +23 -8
- package/prompts/review-loop.md +3 -1
- package/skills/pi-subagents/SKILL.md +87 -25
- package/src/agents/agent-management.ts +82 -15
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +91 -49
- package/src/agents/frontmatter.ts +67 -13
- package/src/agents/skills.ts +25 -12
- package/src/api/background-work.ts +197 -0
- package/src/api/delegation.ts +158 -0
- package/src/extension/chain-validation.ts +165 -0
- package/src/extension/doctor.ts +15 -0
- package/src/extension/fanout-child.ts +3 -1
- package/src/extension/index.ts +65 -124
- package/src/extension/rpc.ts +10 -2
- package/src/extension/schemas.ts +18 -14
- package/src/extension/steering-notices.ts +35 -0
- package/src/extension/tool-description.ts +20 -9
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/native-supervisor-channel.ts +9 -1
- package/src/intercom/result-intercom.ts +4 -0
- package/src/runs/background/async-execution.ts +293 -44
- package/src/runs/background/async-job-tracker.ts +56 -9
- package/src/runs/background/async-resume.ts +159 -52
- package/src/runs/background/async-status.ts +25 -18
- package/src/runs/background/auto-drain.ts +67 -0
- package/src/runs/background/chain-root-attachment.ts +16 -8
- package/src/runs/background/control-channel.ts +260 -13
- package/src/runs/background/fleet-view.ts +23 -2
- package/src/runs/background/notify.ts +79 -10
- package/src/runs/background/result-watcher.ts +12 -9
- package/src/runs/background/run-id-resolver.ts +14 -2
- package/src/runs/background/run-status.ts +23 -15
- package/src/runs/background/scheduled-runs.ts +3 -0
- package/src/runs/background/stale-run-reconciler.ts +32 -10
- package/src/runs/background/steering.ts +237 -0
- package/src/runs/background/subagent-runner.ts +898 -236
- package/src/runs/background/subagent-wait.ts +484 -0
- package/src/runs/background/top-level-async.ts +2 -1
- package/src/runs/background/wait-config.ts +36 -0
- package/src/runs/background/wait-tool.ts +26 -0
- package/src/runs/foreground/async-steering-action.ts +230 -0
- package/src/runs/foreground/chain-clarify.ts +22 -6
- package/src/runs/foreground/chain-execution.ts +50 -32
- package/src/runs/foreground/execution.ts +308 -94
- package/src/runs/foreground/subagent-executor.ts +592 -268
- package/src/runs/shared/acceptance.ts +355 -97
- package/src/runs/shared/child-protocol.ts +121 -0
- package/src/runs/shared/completion-guard.ts +8 -127
- package/src/runs/shared/dynamic-fanout.ts +6 -4
- package/src/runs/shared/model-fallback.ts +36 -0
- package/src/runs/shared/nested-events.ts +9 -4
- package/src/runs/shared/nested-render.ts +4 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +34 -7
- package/src/runs/shared/pi-spawn.ts +18 -12
- package/src/runs/shared/session-lease.ts +279 -0
- package/src/runs/shared/single-output.ts +61 -6
- package/src/runs/shared/spawn-budget.ts +128 -0
- package/src/runs/shared/subagent-control.ts +10 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
- package/src/runs/shared/task-intent.ts +176 -0
- package/src/runs/shared/tool-availability.ts +65 -0
- package/src/runs/shared/turn-budget.ts +49 -4
- package/src/shared/atomic-json.ts +4 -1
- package/src/shared/fork-context.ts +28 -3
- package/src/shared/model-info.ts +7 -4
- package/src/shared/status-format.ts +7 -1
- package/src/shared/types.ts +203 -25
- package/src/shared/utils.ts +35 -7
- package/src/slash/delegation-adapters.ts +457 -0
- package/src/slash/delegation-request.ts +103 -0
- package/src/slash/prompt-template-bridge.ts +167 -344
- package/src/slash/slash-commands.ts +239 -6
- package/src/slash/subagents-admin.ts +428 -0
- package/src/slash/subagents-editor.ts +86 -0
- package/src/tui/fleet.ts +405 -0
- package/src/tui/render.ts +90 -16
- package/src/watchdog/change-signature.ts +127 -0
- package/src/watchdog/child-status.ts +205 -0
- package/src/watchdog/emission-guard.ts +123 -0
- package/src/watchdog/lsp-diagnostics.ts +532 -0
- package/src/watchdog/model-selection.ts +167 -0
- package/src/watchdog/register-child.ts +117 -0
- package/src/watchdog/register-main.ts +433 -0
- package/src/watchdog/render.ts +54 -0
- package/src/watchdog/review.ts +293 -0
- package/src/watchdog/runtime.ts +712 -0
- package/src/watchdog/settings.ts +528 -0
- package/src/watchdog/tool-actions.ts +155 -0
- package/src/watchdog/turn-delta.ts +161 -0
- package/src/watchdog/types.ts +188 -0
- package/src/watchdog/warning-format.ts +73 -0
- package/src/runs/background/wait.ts +0 -394
|
@@ -5,10 +5,10 @@ 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
7
|
import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
8
|
-
import { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, enqueueStepSteer, stepSteerInboxDir, watchAsyncControlInbox, type SteerRequest } from "./control-channel.ts";
|
|
8
|
+
import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest } from "./control-channel.ts";
|
|
9
9
|
import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
10
10
|
import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
11
|
-
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
11
|
+
import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
12
12
|
import {
|
|
13
13
|
type ActivityState,
|
|
14
14
|
type ArtifactConfig,
|
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
type TurnBudgetState,
|
|
29
29
|
type Usage,
|
|
30
30
|
type WorkflowGraphSnapshot,
|
|
31
|
+
type SteeringStatus,
|
|
32
|
+
type SteeringTargetState,
|
|
33
|
+
type SteeringTargetStatus,
|
|
31
34
|
DEFAULT_MAX_OUTPUT,
|
|
32
35
|
type MaxOutputConfig,
|
|
33
36
|
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -57,9 +60,11 @@ import {
|
|
|
57
60
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
58
61
|
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
59
62
|
import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
|
|
63
|
+
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
60
64
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
61
65
|
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
62
66
|
import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
67
|
+
import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
|
|
63
68
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
64
69
|
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
|
|
65
70
|
import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
@@ -88,11 +93,23 @@ import {
|
|
|
88
93
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
89
94
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
90
95
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
91
|
-
import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
96
|
+
import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
92
97
|
import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
93
98
|
import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
|
|
94
|
-
import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState,
|
|
99
|
+
import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
|
|
95
100
|
import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
|
|
101
|
+
import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
|
|
102
|
+
import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
|
|
103
|
+
import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
|
|
104
|
+
import {
|
|
105
|
+
CHILD_WATCHDOG_CONFIG_ENV,
|
|
106
|
+
acceptChildWatchdogEvent,
|
|
107
|
+
childWatchdogIsActive,
|
|
108
|
+
decodeChildWatchdogConfig,
|
|
109
|
+
isChildWatchdogStatusEvent,
|
|
110
|
+
resolveChildWatchdogConfig,
|
|
111
|
+
type ChildWatchdogStateSnapshot,
|
|
112
|
+
} from "../../watchdog/child-status.ts";
|
|
96
113
|
|
|
97
114
|
interface SubagentRunConfig {
|
|
98
115
|
id: string;
|
|
@@ -126,6 +143,7 @@ interface SubagentRunConfig {
|
|
|
126
143
|
deadlineAt?: number;
|
|
127
144
|
turnBudget?: ResolvedTurnBudget;
|
|
128
145
|
toolBudget?: ResolvedToolBudget;
|
|
146
|
+
revivalLease?: SessionLeaseRequest;
|
|
129
147
|
/** Global cap on simultaneously-running subagent tasks within this run. */
|
|
130
148
|
globalConcurrencyLimit?: number;
|
|
131
149
|
}
|
|
@@ -134,11 +152,13 @@ interface StepResult {
|
|
|
134
152
|
agent: string;
|
|
135
153
|
output: string;
|
|
136
154
|
error?: string;
|
|
155
|
+
protocolError?: ProtocolOutputLimit;
|
|
137
156
|
success: boolean;
|
|
138
157
|
exitCode?: number | null;
|
|
139
158
|
skipped?: boolean;
|
|
140
159
|
interrupted?: boolean;
|
|
141
160
|
timedOut?: boolean;
|
|
161
|
+
stopped?: boolean;
|
|
142
162
|
turnBudget?: TurnBudgetState;
|
|
143
163
|
turnBudgetExceeded?: boolean;
|
|
144
164
|
wrapUpRequested?: boolean;
|
|
@@ -158,6 +178,7 @@ interface StepResult {
|
|
|
158
178
|
structuredOutputPath?: string;
|
|
159
179
|
structuredOutputSchemaPath?: string;
|
|
160
180
|
acceptance?: import("../../shared/types.ts").AcceptanceLedger;
|
|
181
|
+
watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
@@ -291,11 +312,13 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
|
|
|
291
312
|
}
|
|
292
313
|
}
|
|
293
314
|
|
|
294
|
-
function
|
|
295
|
-
|
|
296
|
-
const hasToolCall = Array.isArray(message.content)
|
|
315
|
+
function assistantStartsToolCall(message: Message): boolean {
|
|
316
|
+
return Array.isArray(message.content)
|
|
297
317
|
&& message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
298
|
-
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isTerminalAssistantStop(message: Message): boolean {
|
|
321
|
+
return (message as { stopReason?: string }).stopReason === "stop" && !assistantStartsToolCall(message);
|
|
299
322
|
}
|
|
300
323
|
|
|
301
324
|
function resetStepLiveDetail(step: RunnerStatusStep): void {
|
|
@@ -335,6 +358,7 @@ interface ChildEvent {
|
|
|
335
358
|
message?: ChildMessage;
|
|
336
359
|
toolName?: string;
|
|
337
360
|
args?: Record<string, unknown>;
|
|
361
|
+
willRetry?: unknown;
|
|
338
362
|
}
|
|
339
363
|
|
|
340
364
|
interface RunPiStreamingResult {
|
|
@@ -344,15 +368,18 @@ interface RunPiStreamingResult {
|
|
|
344
368
|
usage: Usage;
|
|
345
369
|
model?: string;
|
|
346
370
|
error?: string;
|
|
371
|
+
protocolError?: ProtocolOutputLimit;
|
|
347
372
|
finalOutput: string;
|
|
348
373
|
interrupted?: boolean;
|
|
349
374
|
timedOut?: boolean;
|
|
375
|
+
stopped?: boolean;
|
|
350
376
|
turnBudget?: TurnBudgetState;
|
|
351
377
|
turnBudgetExceeded?: boolean;
|
|
352
378
|
wrapUpRequested?: boolean;
|
|
353
379
|
toolBudget?: ToolBudgetState;
|
|
354
380
|
toolBudgetBlocked?: boolean;
|
|
355
381
|
observedMutationAttempt?: boolean;
|
|
382
|
+
watchdog?: ChildWatchdogStateSnapshot;
|
|
356
383
|
}
|
|
357
384
|
|
|
358
385
|
function runPiStreaming(
|
|
@@ -369,9 +396,13 @@ function runPiStreaming(
|
|
|
369
396
|
transcriptWriter?: ChildTranscriptWriter,
|
|
370
397
|
registerTimeout?: (interrupt: (() => void) | undefined) => void,
|
|
371
398
|
timeoutMessage?: string,
|
|
399
|
+
registerStop?: (stop: (() => void) | undefined) => void,
|
|
400
|
+
stopMessage?: string,
|
|
372
401
|
registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
|
|
402
|
+
onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
|
|
373
403
|
): Promise<RunPiStreamingResult> {
|
|
374
404
|
return new Promise((resolve) => {
|
|
405
|
+
onWriterProcess?.({ state: "spawning" });
|
|
375
406
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
376
407
|
const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
|
|
377
408
|
const spawnSpec = getPiSpawnCommand(args, {
|
|
@@ -384,21 +415,35 @@ function runPiStreaming(
|
|
|
384
415
|
env: spawnEnv,
|
|
385
416
|
windowsHide: true,
|
|
386
417
|
});
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
let stderrBuf = "";
|
|
418
|
+
const stderrTail = createBoundedByteTail();
|
|
419
|
+
const rawStdoutTail = createBoundedByteTail();
|
|
390
420
|
const messages: Message[] = [];
|
|
391
421
|
const usage = emptyUsage();
|
|
392
422
|
let model: string | undefined;
|
|
393
|
-
let
|
|
423
|
+
let writerRegistrationError: string | undefined;
|
|
424
|
+
if (typeof child.pid === "number") {
|
|
425
|
+
try {
|
|
426
|
+
onWriterProcess?.({ state: "running", pid: child.pid });
|
|
427
|
+
} catch (writerError) {
|
|
428
|
+
writerRegistrationError = `Failed to record revived Pi writer ownership: ${writerError instanceof Error ? writerError.message : String(writerError)}`;
|
|
429
|
+
trySignalChild(child, "SIGKILL");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
let error: string | undefined = writerRegistrationError;
|
|
394
433
|
let assistantError: string | undefined;
|
|
395
434
|
let interrupted = false;
|
|
396
435
|
let timedOut = false;
|
|
436
|
+
let stopped = false;
|
|
397
437
|
let turnBudgetExceeded = false;
|
|
398
438
|
let turnBudgetMessage: string | undefined;
|
|
399
439
|
let turnBudget: TurnBudgetState | undefined;
|
|
400
440
|
let observedMutationAttempt = false;
|
|
401
|
-
const
|
|
441
|
+
const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
|
|
442
|
+
let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
|
|
443
|
+
let applyChildLifecycle = (_action: ChildLifecycleAction): void => {};
|
|
444
|
+
const updateChildWatchdogState = (snapshot: ChildWatchdogStateSnapshot): void => {
|
|
445
|
+
childWatchdogState = snapshot;
|
|
446
|
+
};
|
|
402
447
|
|
|
403
448
|
const writeOutputLine = (line: string) => {
|
|
404
449
|
if (!line.trim()) return;
|
|
@@ -436,7 +481,7 @@ function runPiStreaming(
|
|
|
436
481
|
try {
|
|
437
482
|
event = JSON.parse(line) as ChildEvent;
|
|
438
483
|
} catch {
|
|
439
|
-
|
|
484
|
+
rawStdoutTail.push(`${line}\n`);
|
|
440
485
|
writeOutputLine(line);
|
|
441
486
|
appendChildLine("subagent.child.stdout", line);
|
|
442
487
|
return;
|
|
@@ -444,6 +489,38 @@ function runPiStreaming(
|
|
|
444
489
|
|
|
445
490
|
appendChildEvent(event);
|
|
446
491
|
transcriptWriter?.writeChildEvent(event);
|
|
492
|
+
if (event.type === "agent_settled") agentSettledReceived = true;
|
|
493
|
+
applyChildLifecycle(projectChildLifecycle(event));
|
|
494
|
+
|
|
495
|
+
if (isChildWatchdogStatusEvent(event)) {
|
|
496
|
+
if (!childWatchdogConfig) return;
|
|
497
|
+
const next = acceptChildWatchdogEvent({
|
|
498
|
+
current: childWatchdogState,
|
|
499
|
+
event,
|
|
500
|
+
runId: childEventContext?.runId,
|
|
501
|
+
agent: childEventContext?.agent,
|
|
502
|
+
childIndex: childEventContext?.stepIndex,
|
|
503
|
+
});
|
|
504
|
+
if (!next) return;
|
|
505
|
+
updateChildWatchdogState(next);
|
|
506
|
+
onChildEvent?.(event);
|
|
507
|
+
if (childWatchdogIsActive(next)) {
|
|
508
|
+
if (finalDrainTimer) {
|
|
509
|
+
clearTimeout(finalDrainTimer);
|
|
510
|
+
finalDrainTimer = undefined;
|
|
511
|
+
}
|
|
512
|
+
if (finalHardKillTimer) {
|
|
513
|
+
clearTimeout(finalHardKillTimer);
|
|
514
|
+
finalHardKillTimer = undefined;
|
|
515
|
+
}
|
|
516
|
+
armWatchdogTail();
|
|
517
|
+
} else {
|
|
518
|
+
clearWatchdogTailTimer();
|
|
519
|
+
if (cleanTerminalAssistantStopReceived || agentSettledReceived) startFinalDrain();
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
447
524
|
onChildEvent?.(event);
|
|
448
525
|
|
|
449
526
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
@@ -473,24 +550,11 @@ function runPiStreaming(
|
|
|
473
550
|
if (isTerminalAssistantStop(event.message)) {
|
|
474
551
|
if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
|
|
475
552
|
cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
|
|
476
|
-
|
|
553
|
+
applyChildLifecycle(projectChildLifecycle(event, true));
|
|
477
554
|
}
|
|
478
555
|
}
|
|
479
556
|
};
|
|
480
557
|
|
|
481
|
-
const processStderrText = (text: string) => {
|
|
482
|
-
stderr += text;
|
|
483
|
-
stderrBuf += text;
|
|
484
|
-
outputStream.write(text);
|
|
485
|
-
if (!childEventContext) return;
|
|
486
|
-
const lines = stderrBuf.split("\n");
|
|
487
|
-
stderrBuf = lines.pop() || "";
|
|
488
|
-
for (const line of lines) {
|
|
489
|
-
if (!line.trim()) continue;
|
|
490
|
-
appendChildLine("subagent.child.stderr", line);
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
|
|
494
558
|
// Guard both cases that can leave the parent waiting on `close` forever:
|
|
495
559
|
// a lingering stdio holder after `exit`, or a child that never exits.
|
|
496
560
|
const FINAL_STOP_GRACE_MS = 1000;
|
|
@@ -499,35 +563,68 @@ function runPiStreaming(
|
|
|
499
563
|
let childExited = false;
|
|
500
564
|
let forcedTerminationSignal = false;
|
|
501
565
|
let cleanTerminalAssistantStopReceived = false;
|
|
566
|
+
let agentSettledReceived = false;
|
|
502
567
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
503
568
|
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
569
|
+
let watchdogTailTimer: NodeJS.Timeout | undefined;
|
|
504
570
|
let timeoutHardKillTimer: NodeJS.Timeout | undefined;
|
|
505
571
|
let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
|
|
506
572
|
let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
|
|
573
|
+
let protocolHardKillTimer: NodeJS.Timeout | undefined;
|
|
574
|
+
let protocolError: ProtocolOutputLimit | undefined;
|
|
507
575
|
let settled = false;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
576
|
+
applyChildLifecycle = (action: ChildLifecycleAction): void => {
|
|
577
|
+
if (action === "cancel-drain") {
|
|
578
|
+
if (finalDrainTimer) {
|
|
579
|
+
clearTimeout(finalDrainTimer);
|
|
580
|
+
finalDrainTimer = undefined;
|
|
581
|
+
}
|
|
582
|
+
if (finalHardKillTimer) {
|
|
583
|
+
clearTimeout(finalHardKillTimer);
|
|
584
|
+
finalHardKillTimer = undefined;
|
|
585
|
+
}
|
|
586
|
+
clearWatchdogTailTimer();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (action === "start-drain") startFinalDrain();
|
|
590
|
+
};
|
|
591
|
+
const failProtocol = (limit: ProtocolOutputLimit): void => {
|
|
592
|
+
if (protocolError) return;
|
|
593
|
+
protocolError = limit;
|
|
594
|
+
error = formatProtocolOutputLimit(limit);
|
|
595
|
+
if (!childExited) {
|
|
596
|
+
trySignalChild(child, "SIGTERM");
|
|
597
|
+
protocolHardKillTimer = setTimeout(() => {
|
|
598
|
+
if (!settled) trySignalChild(child, "SIGKILL");
|
|
599
|
+
}, 3000);
|
|
600
|
+
protocolHardKillTimer.unref?.();
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const stdoutReader = createBoundedLineReader({ onLine: processStdoutLine, onLimit: failProtocol });
|
|
604
|
+
const stderrReader = createBoundedLineReader({
|
|
605
|
+
stream: "stderr",
|
|
606
|
+
maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
|
|
607
|
+
onLine: (line) => appendChildLine("subagent.child.stderr", line),
|
|
608
|
+
onLimit: (limit) => appendChildLine("subagent.child.stderr", formatProtocolOutputLimit(limit)),
|
|
515
609
|
});
|
|
516
|
-
|
|
610
|
+
const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
|
|
611
|
+
child.stdout.on("data", (chunk: Buffer) => stdoutReader.push(chunk));
|
|
517
612
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
518
|
-
|
|
613
|
+
stderrTail.push(chunk);
|
|
614
|
+
stderrReader.push(chunk);
|
|
615
|
+
outputStream.write(chunk);
|
|
519
616
|
});
|
|
520
617
|
registerInterrupt?.(() => {
|
|
521
|
-
if (settled || timedOut) return;
|
|
618
|
+
if (settled || timedOut || stopped) return;
|
|
522
619
|
interrupted = true;
|
|
523
620
|
if (!error) error = "Interrupted. Waiting for explicit next action.";
|
|
524
621
|
trySignalChild(child, "SIGINT");
|
|
525
622
|
setTimeout(() => {
|
|
526
|
-
if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
|
|
623
|
+
if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
|
|
527
624
|
}, 1000).unref?.();
|
|
528
625
|
});
|
|
529
626
|
registerTimeout?.(() => {
|
|
530
|
-
if (settled || timedOut) return;
|
|
627
|
+
if (settled || timedOut || stopped) return;
|
|
531
628
|
timedOut = true;
|
|
532
629
|
interrupted = false;
|
|
533
630
|
error = timeoutMessage ?? "Subagent timed out.";
|
|
@@ -537,8 +634,19 @@ function runPiStreaming(
|
|
|
537
634
|
}, TIMEOUT_HARD_KILL_MS);
|
|
538
635
|
timeoutHardKillTimer.unref?.();
|
|
539
636
|
});
|
|
637
|
+
registerStop?.(() => {
|
|
638
|
+
if (settled || timedOut || stopped) return;
|
|
639
|
+
stopped = true;
|
|
640
|
+
interrupted = false;
|
|
641
|
+
error = stopMessage ?? "Subagent stopped by user.";
|
|
642
|
+
trySignalChild(child, "SIGTERM");
|
|
643
|
+
timeoutHardKillTimer = setTimeout(() => {
|
|
644
|
+
if (!settled) trySignalChild(child, "SIGKILL");
|
|
645
|
+
}, TIMEOUT_HARD_KILL_MS);
|
|
646
|
+
timeoutHardKillTimer.unref?.();
|
|
647
|
+
});
|
|
540
648
|
registerTurnBudgetAbort?.((message, state) => {
|
|
541
|
-
if (settled || timedOut || turnBudgetExceeded) return;
|
|
649
|
+
if (settled || timedOut || stopped || turnBudgetExceeded) return;
|
|
542
650
|
turnBudgetExceeded = true;
|
|
543
651
|
turnBudgetMessage = message;
|
|
544
652
|
turnBudget = state;
|
|
@@ -546,11 +654,11 @@ function runPiStreaming(
|
|
|
546
654
|
error = message;
|
|
547
655
|
trySignalChild(child, "SIGINT");
|
|
548
656
|
turnBudgetTerminationTimer = setTimeout(() => {
|
|
549
|
-
if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
|
|
657
|
+
if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
|
|
550
658
|
}, 1000);
|
|
551
659
|
turnBudgetTerminationTimer.unref?.();
|
|
552
660
|
turnBudgetHardKillTimer = setTimeout(() => {
|
|
553
|
-
if (!settled && !timedOut) trySignalChild(child, "SIGKILL");
|
|
661
|
+
if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGKILL");
|
|
554
662
|
}, 4000);
|
|
555
663
|
turnBudgetHardKillTimer.unref?.();
|
|
556
664
|
});
|
|
@@ -563,6 +671,7 @@ function runPiStreaming(
|
|
|
563
671
|
clearTimeout(finalHardKillTimer);
|
|
564
672
|
finalHardKillTimer = undefined;
|
|
565
673
|
}
|
|
674
|
+
clearWatchdogTailTimer();
|
|
566
675
|
if (timeoutHardKillTimer) {
|
|
567
676
|
clearTimeout(timeoutHardKillTimer);
|
|
568
677
|
timeoutHardKillTimer = undefined;
|
|
@@ -575,16 +684,24 @@ function runPiStreaming(
|
|
|
575
684
|
clearTimeout(turnBudgetHardKillTimer);
|
|
576
685
|
turnBudgetHardKillTimer = undefined;
|
|
577
686
|
}
|
|
687
|
+
if (protocolHardKillTimer) {
|
|
688
|
+
clearTimeout(protocolHardKillTimer);
|
|
689
|
+
protocolHardKillTimer = undefined;
|
|
690
|
+
}
|
|
578
691
|
};
|
|
579
692
|
function startFinalDrain(): void {
|
|
693
|
+
if (childWatchdogIsActive(childWatchdogState)) {
|
|
694
|
+
armWatchdogTail();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
580
697
|
if (childExited || finalDrainTimer || settled) return;
|
|
581
698
|
finalDrainTimer = setTimeout(() => {
|
|
582
699
|
if (settled) return;
|
|
583
700
|
const termSent = trySignalChild(child, "SIGTERM");
|
|
584
701
|
if (!termSent) return;
|
|
585
702
|
forcedTerminationSignal = true;
|
|
586
|
-
if (!cleanTerminalAssistantStopReceived && !error && !assistantError) {
|
|
587
|
-
error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its
|
|
703
|
+
if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
|
|
704
|
+
error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
|
|
588
705
|
}
|
|
589
706
|
finalHardKillTimer = setTimeout(() => {
|
|
590
707
|
if (settled) return;
|
|
@@ -594,51 +711,92 @@ function runPiStreaming(
|
|
|
594
711
|
}, FINAL_STOP_GRACE_MS);
|
|
595
712
|
finalDrainTimer.unref?.();
|
|
596
713
|
}
|
|
714
|
+
function clearWatchdogTailTimer(): void {
|
|
715
|
+
if (watchdogTailTimer) {
|
|
716
|
+
clearTimeout(watchdogTailTimer);
|
|
717
|
+
watchdogTailTimer = undefined;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function armWatchdogTail(): void {
|
|
721
|
+
if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled) return;
|
|
722
|
+
watchdogTailTimer = setTimeout(() => {
|
|
723
|
+
watchdogTailTimer = undefined;
|
|
724
|
+
updateChildWatchdogState({
|
|
725
|
+
phase: "stale",
|
|
726
|
+
seq: (childWatchdogState?.seq ?? 0) + 1,
|
|
727
|
+
lastUpdate: Date.now(),
|
|
728
|
+
followUpPending: false,
|
|
729
|
+
reason: "child watchdog tail timeout",
|
|
730
|
+
timedOut: true,
|
|
731
|
+
});
|
|
732
|
+
startFinalDrain();
|
|
733
|
+
}, childWatchdogConfig?.watchdogTailTimeoutMs ?? 120_000);
|
|
734
|
+
watchdogTailTimer.unref?.();
|
|
735
|
+
}
|
|
597
736
|
child.on("exit", () => {
|
|
598
737
|
childExited = true;
|
|
599
738
|
clearDrainTimers();
|
|
600
739
|
});
|
|
601
740
|
child.on("close", (exitCode, signal) => {
|
|
602
741
|
settled = true;
|
|
742
|
+
try {
|
|
743
|
+
onWriterProcess?.({ state: "none" });
|
|
744
|
+
} catch {
|
|
745
|
+
// The runner still owns and releases the lease during finalization.
|
|
746
|
+
}
|
|
603
747
|
registerInterrupt?.(undefined);
|
|
604
748
|
registerTimeout?.(undefined);
|
|
749
|
+
registerStop?.(undefined);
|
|
605
750
|
registerTurnBudgetAbort?.(undefined);
|
|
606
751
|
clearDrainTimers();
|
|
607
752
|
clearStdioGuard();
|
|
608
|
-
|
|
609
|
-
|
|
753
|
+
stdoutReader.end();
|
|
754
|
+
stderrReader.end();
|
|
610
755
|
outputStream.end();
|
|
611
|
-
const
|
|
756
|
+
const stderr = stderrTail.text();
|
|
757
|
+
const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
|
|
612
758
|
const finalError = error ?? assistantError;
|
|
613
|
-
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
|
|
759
|
+
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
|
|
614
760
|
resolve({
|
|
615
761
|
stderr,
|
|
616
|
-
exitCode: timedOut ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
762
|
+
exitCode: timedOut || stopped ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
617
763
|
messages,
|
|
618
764
|
usage,
|
|
619
765
|
model,
|
|
620
|
-
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
621
|
-
|
|
766
|
+
error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
767
|
+
protocolError,
|
|
768
|
+
finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
|
|
622
769
|
interrupted,
|
|
623
770
|
timedOut,
|
|
771
|
+
stopped,
|
|
624
772
|
turnBudget,
|
|
625
773
|
turnBudgetExceeded,
|
|
626
|
-
wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
|
|
774
|
+
wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
|
|
627
775
|
observedMutationAttempt,
|
|
776
|
+
watchdog: childWatchdogState,
|
|
628
777
|
});
|
|
629
778
|
});
|
|
630
779
|
|
|
631
780
|
child.on("error", (spawnError) => {
|
|
632
781
|
settled = true;
|
|
782
|
+
try {
|
|
783
|
+
onWriterProcess?.({ state: "none" });
|
|
784
|
+
} catch {
|
|
785
|
+
// The runner still owns and releases the lease during finalization.
|
|
786
|
+
}
|
|
633
787
|
registerInterrupt?.(undefined);
|
|
634
788
|
registerTimeout?.(undefined);
|
|
789
|
+
registerStop?.(undefined);
|
|
635
790
|
registerTurnBudgetAbort?.(undefined);
|
|
636
791
|
clearDrainTimers();
|
|
637
792
|
clearStdioGuard();
|
|
793
|
+
stdoutReader.end();
|
|
794
|
+
stderrReader.end();
|
|
638
795
|
outputStream.end();
|
|
639
|
-
const
|
|
796
|
+
const stderr = stderrTail.text();
|
|
797
|
+
const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
|
|
640
798
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
641
|
-
resolve({ stderr, exitCode: 1, messages, usage, model, error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined, observedMutationAttempt });
|
|
799
|
+
resolve({ stderr, exitCode: 1, messages, usage, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState });
|
|
642
800
|
});
|
|
643
801
|
});
|
|
644
802
|
}
|
|
@@ -764,20 +922,26 @@ interface SingleStepContext {
|
|
|
764
922
|
flatStepCount: number;
|
|
765
923
|
outputFile: string;
|
|
766
924
|
steerInboxDir?: string;
|
|
925
|
+
steerCapabilityPath?: string;
|
|
926
|
+
steerAckDir?: string;
|
|
767
927
|
transcriptPath?: string;
|
|
768
928
|
piPackageRoot?: string;
|
|
769
929
|
piArgv1?: string;
|
|
770
930
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void;
|
|
771
931
|
registerTimeout?: (interrupt: (() => void) | undefined) => void;
|
|
932
|
+
registerStop?: (stop: (() => void) | undefined) => void;
|
|
772
933
|
registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void;
|
|
773
934
|
timeoutSignal?: AbortSignal;
|
|
935
|
+
stopSignal?: AbortSignal;
|
|
774
936
|
timeoutMessage?: string;
|
|
937
|
+
stopMessage?: string;
|
|
775
938
|
turnBudget?: ResolvedTurnBudget;
|
|
776
939
|
childIntercomTarget?: string;
|
|
777
940
|
orchestratorIntercomTarget?: string;
|
|
778
941
|
nestedRoute?: NestedRouteInfo;
|
|
779
942
|
onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
|
|
780
943
|
onChildEvent?: (event: ChildEvent) => void;
|
|
944
|
+
onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
|
|
781
945
|
skipAcceptance?: () => boolean;
|
|
782
946
|
}
|
|
783
947
|
|
|
@@ -798,6 +962,7 @@ async function runSingleStep(
|
|
|
798
962
|
transcriptError?: string;
|
|
799
963
|
interrupted?: boolean;
|
|
800
964
|
timedOut?: boolean;
|
|
965
|
+
stopped?: boolean;
|
|
801
966
|
turnBudget?: TurnBudgetState;
|
|
802
967
|
turnBudgetExceeded?: boolean;
|
|
803
968
|
wrapUpRequested?: boolean;
|
|
@@ -813,6 +978,7 @@ async function runSingleStep(
|
|
|
813
978
|
}> {
|
|
814
979
|
if (step.importAsyncRoot) {
|
|
815
980
|
let importTimedOut = false;
|
|
981
|
+
let importStopped = false;
|
|
816
982
|
ctx.registerTimeout?.(() => {
|
|
817
983
|
importTimedOut = true;
|
|
818
984
|
let pid: number | undefined;
|
|
@@ -827,36 +993,54 @@ async function runSingleStep(
|
|
|
827
993
|
// The parent runner's own timeout result is authoritative for the attached step.
|
|
828
994
|
}
|
|
829
995
|
});
|
|
996
|
+
ctx.registerStop?.(() => {
|
|
997
|
+
importStopped = true;
|
|
998
|
+
let pid: number | undefined;
|
|
999
|
+
try {
|
|
1000
|
+
pid = readStatus(step.importAsyncRoot!.asyncDir)?.pid;
|
|
1001
|
+
} catch {
|
|
1002
|
+
pid = undefined;
|
|
1003
|
+
}
|
|
1004
|
+
try {
|
|
1005
|
+
deliverStopRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-stop" });
|
|
1006
|
+
} catch {
|
|
1007
|
+
// The parent runner's own stopped result is authoritative for the attached step.
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
830
1010
|
try {
|
|
831
1011
|
const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, {
|
|
832
|
-
shouldAbort: () => importTimedOut || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true,
|
|
833
|
-
timeoutMessage: ctx.timeoutMessage,
|
|
1012
|
+
shouldAbort: () => importTimedOut || importStopped || ctx.timeoutSignal?.aborted === true || ctx.stopSignal?.aborted === true || ctx.skipAcceptance?.() === true,
|
|
1013
|
+
timeoutMessage: importStopped || ctx.stopSignal?.aborted === true ? ctx.stopMessage : ctx.timeoutMessage,
|
|
834
1014
|
});
|
|
835
1015
|
try {
|
|
836
1016
|
fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
|
|
837
1017
|
} catch {
|
|
838
1018
|
// Output files are observability only for imported roots.
|
|
839
1019
|
}
|
|
840
|
-
const
|
|
1020
|
+
const stopped = importStopped || imported.stopped === true || ctx.stopSignal?.aborted === true;
|
|
1021
|
+
const timedOut = !stopped && (importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true);
|
|
1022
|
+
const message = stopped ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.";
|
|
841
1023
|
return {
|
|
842
1024
|
agent: imported.agent,
|
|
843
|
-
output: timedOut
|
|
844
|
-
exitCode: timedOut ? 1 : imported.exitCode,
|
|
845
|
-
error: timedOut
|
|
1025
|
+
output: timedOut || stopped ? message : imported.output,
|
|
1026
|
+
exitCode: timedOut || stopped ? 1 : imported.exitCode,
|
|
1027
|
+
error: timedOut || stopped ? message : imported.error,
|
|
846
1028
|
timedOut: timedOut ? true : undefined,
|
|
1029
|
+
stopped: stopped ? true : undefined,
|
|
847
1030
|
sessionFile: imported.sessionFile,
|
|
848
1031
|
intercomTarget: imported.intercomTarget,
|
|
849
1032
|
model: imported.model,
|
|
850
1033
|
attemptedModels: imported.attemptedModels,
|
|
851
1034
|
modelAttempts: imported.modelAttempts,
|
|
852
1035
|
totalCost: imported.totalCost,
|
|
853
|
-
structuredOutput: timedOut ? undefined : imported.structuredOutput,
|
|
854
|
-
structuredOutputPath: timedOut ? undefined : imported.structuredOutputPath,
|
|
855
|
-
structuredOutputSchemaPath: timedOut ? undefined : imported.structuredOutputSchemaPath,
|
|
856
|
-
acceptance: timedOut ? undefined : imported.acceptance,
|
|
1036
|
+
structuredOutput: timedOut || stopped ? undefined : imported.structuredOutput,
|
|
1037
|
+
structuredOutputPath: timedOut || stopped ? undefined : imported.structuredOutputPath,
|
|
1038
|
+
structuredOutputSchemaPath: timedOut || stopped ? undefined : imported.structuredOutputSchemaPath,
|
|
1039
|
+
acceptance: timedOut || stopped ? undefined : imported.acceptance,
|
|
857
1040
|
};
|
|
858
1041
|
} finally {
|
|
859
1042
|
ctx.registerTimeout?.(undefined);
|
|
1043
|
+
ctx.registerStop?.(undefined);
|
|
860
1044
|
}
|
|
861
1045
|
}
|
|
862
1046
|
|
|
@@ -865,7 +1049,7 @@ async function runSingleStep(
|
|
|
865
1049
|
: undefined);
|
|
866
1050
|
const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
867
1051
|
let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
|
|
868
|
-
task = resolveOutputReferences(task, ctx.outputs
|
|
1052
|
+
if (ctx.outputs) task = resolveOutputReferences(task, ctx.outputs);
|
|
869
1053
|
const taskForCompletionGuard = task;
|
|
870
1054
|
if (step.effectiveAcceptance) {
|
|
871
1055
|
const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
|
|
@@ -924,7 +1108,16 @@ async function runSingleStep(
|
|
|
924
1108
|
// Missing/stale structured-output files are handled after the child exits.
|
|
925
1109
|
}
|
|
926
1110
|
}
|
|
927
|
-
const
|
|
1111
|
+
const watchdogConfig = resolveWatchdogConfig(step.cwd ?? ctx.cwd);
|
|
1112
|
+
const childWatchdog = watchdogConfig.ok
|
|
1113
|
+
? resolveChildWatchdogConfig({
|
|
1114
|
+
config: watchdogConfig.config,
|
|
1115
|
+
agent: step.agent,
|
|
1116
|
+
runId: ctx.id,
|
|
1117
|
+
childIndex: ctx.flatIndex,
|
|
1118
|
+
})
|
|
1119
|
+
: undefined;
|
|
1120
|
+
const { args, env, tempDir, toolDiagnosticPath } = buildPiArgs({
|
|
928
1121
|
parentSessionId: step.parentSessionId,
|
|
929
1122
|
baseArgs: ["--mode", "json", "-p"],
|
|
930
1123
|
task,
|
|
@@ -953,8 +1146,12 @@ async function runSingleStep(
|
|
|
953
1146
|
parentRootRunId: ctx.nestedRoute?.rootRunId,
|
|
954
1147
|
parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
|
|
955
1148
|
steerInboxDir: ctx.steerInboxDir,
|
|
1149
|
+
steerCapabilityPath: ctx.steerCapabilityPath,
|
|
1150
|
+
steerAckDir: ctx.steerAckDir,
|
|
956
1151
|
structuredOutput: effectiveStructuredOutput,
|
|
957
1152
|
toolBudget: step.toolBudget,
|
|
1153
|
+
childWatchdog,
|
|
1154
|
+
waitToolEnabled: step.waitToolEnabled,
|
|
958
1155
|
});
|
|
959
1156
|
const run = await runPiStreaming(
|
|
960
1157
|
args,
|
|
@@ -970,7 +1167,10 @@ async function runSingleStep(
|
|
|
970
1167
|
transcriptWriter,
|
|
971
1168
|
ctx.registerTimeout,
|
|
972
1169
|
ctx.timeoutMessage,
|
|
1170
|
+
ctx.registerStop,
|
|
1171
|
+
ctx.stopMessage,
|
|
973
1172
|
ctx.registerTurnBudgetAbort,
|
|
1173
|
+
ctx.onWriterProcess,
|
|
974
1174
|
);
|
|
975
1175
|
if (run.turnBudget) turnBudget = run.turnBudget;
|
|
976
1176
|
else if (ctx.turnBudget) {
|
|
@@ -980,25 +1180,32 @@ async function runSingleStep(
|
|
|
980
1180
|
if (turnCount > 0 && turnCount < ctx.turnBudget.maxTurns) {
|
|
981
1181
|
turnBudget = { ...ctx.turnBudget, outcome: "within-budget", turnCount };
|
|
982
1182
|
} else if (turnCount >= ctx.turnBudget.maxTurns) {
|
|
983
|
-
|
|
1183
|
+
const decision = turnBudgetDecision(
|
|
984
1184
|
ctx.turnBudget,
|
|
985
1185
|
turnCount,
|
|
986
|
-
|
|
1186
|
+
lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false,
|
|
1187
|
+
lastAssistantMessage ? assistantStartsToolCall(lastAssistantMessage) : false,
|
|
987
1188
|
);
|
|
1189
|
+
turnBudget = decision === "defer"
|
|
1190
|
+
? turnBudgetDeferredState(ctx.turnBudget, turnCount)
|
|
1191
|
+
: turnBudgetState(ctx.turnBudget, turnCount, decision === "abort");
|
|
988
1192
|
}
|
|
989
1193
|
}
|
|
1194
|
+
const toolAvailabilityError = run.exitCode === 0 && !run.error
|
|
1195
|
+
? readChildToolDiagnosticError(toolDiagnosticPath)
|
|
1196
|
+
: undefined;
|
|
990
1197
|
cleanupTempDir(tempDir);
|
|
991
1198
|
|
|
992
|
-
const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
|
|
1199
|
+
const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError ? detectSubagentError(run.messages) : null;
|
|
993
1200
|
const missingStructuredOutput = effectiveStructuredOutput
|
|
994
1201
|
? !fs.existsSync(effectiveStructuredOutput.outputPath)
|
|
995
1202
|
: false;
|
|
996
|
-
const emptyOutputError = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
|
|
1203
|
+
const emptyOutputError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
|
|
997
1204
|
? "Subagent produced no output (possible model cold-start or empty response)."
|
|
998
1205
|
: undefined;
|
|
999
1206
|
let structuredOutput: unknown;
|
|
1000
1207
|
let structuredError: string | undefined;
|
|
1001
|
-
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
|
|
1208
|
+
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError) {
|
|
1002
1209
|
const structured = readStructuredOutput({
|
|
1003
1210
|
schema: effectiveStructuredOutput.schema,
|
|
1004
1211
|
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
@@ -1007,7 +1214,7 @@ async function runSingleStep(
|
|
|
1007
1214
|
if (structured.error) structuredError = structured.error;
|
|
1008
1215
|
else structuredOutput = structured.value;
|
|
1009
1216
|
}
|
|
1010
|
-
const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
|
|
1217
|
+
const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
|
|
1011
1218
|
? evaluateCompletionMutationGuard({
|
|
1012
1219
|
agent: step.agent,
|
|
1013
1220
|
task: taskForCompletionGuard,
|
|
@@ -1020,18 +1227,17 @@ async function runSingleStep(
|
|
|
1020
1227
|
const completionGuardError = completionGuardTriggered
|
|
1021
1228
|
? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
|
|
1022
1229
|
: undefined;
|
|
1023
|
-
const effectiveExitCode = completionGuardTriggered
|
|
1230
|
+
const effectiveExitCode = toolAvailabilityError || completionGuardTriggered || structuredError
|
|
1024
1231
|
? 1
|
|
1025
|
-
:
|
|
1026
|
-
? 1
|
|
1027
|
-
: hiddenError?.hasError
|
|
1232
|
+
: hiddenError?.hasError
|
|
1028
1233
|
? (hiddenError.exitCode ?? 1)
|
|
1029
1234
|
: emptyOutputError
|
|
1030
1235
|
? 1
|
|
1031
1236
|
: run.error && run.exitCode === 0
|
|
1032
1237
|
? 1
|
|
1033
1238
|
: run.exitCode;
|
|
1034
|
-
const error =
|
|
1239
|
+
const error = toolAvailabilityError
|
|
1240
|
+
?? completionGuardError
|
|
1035
1241
|
?? structuredError
|
|
1036
1242
|
?? (hiddenError?.hasError
|
|
1037
1243
|
? hiddenError.details
|
|
@@ -1057,7 +1263,7 @@ async function runSingleStep(
|
|
|
1057
1263
|
}
|
|
1058
1264
|
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
|
|
1059
1265
|
if (run.turnBudgetExceeded) break;
|
|
1060
|
-
if (run.timedOut || ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1266
|
+
if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1061
1267
|
if (attempt.success || completionGuardTriggered) break;
|
|
1062
1268
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
1063
1269
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
@@ -1068,21 +1274,29 @@ async function runSingleStep(
|
|
|
1068
1274
|
const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
|
|
1069
1275
|
? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
|
|
1070
1276
|
: { fullOutput: outputForPersistence };
|
|
1071
|
-
const output = resolvedOutput.fullOutput;
|
|
1277
|
+
const output = stripAcceptanceReport(resolvedOutput.fullOutput);
|
|
1072
1278
|
const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
|
|
1073
1279
|
let outputForSummary = output;
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
if (
|
|
1280
|
+
if (attemptNotes.length > 0) {
|
|
1281
|
+
outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
|
|
1282
|
+
}
|
|
1283
|
+
if (finalResult?.stopped && !outputForSummary.trim()) {
|
|
1284
|
+
outputForSummary = ctx.stopMessage ?? "Subagent stopped by user.";
|
|
1285
|
+
} else if (!finalResult?.timedOut && !finalResult?.stopped && finalResult?.turnBudgetExceeded && turnBudget) {
|
|
1078
1286
|
outputForSummary = formatTurnBudgetOutput(turnBudgetExceededMessage(turnBudget, turnBudget.turnCount), outputForSummary);
|
|
1079
|
-
} else if (!finalResult?.timedOut && turnBudget?.outcome === "
|
|
1287
|
+
} else if (!finalResult?.timedOut && !finalResult?.stopped && turnBudget?.outcome === "termination-deferred") {
|
|
1288
|
+
const note = turnBudgetDeferredNote(turnBudget, turnBudget.terminationDeferredAtTurn ?? turnBudget.turnCount);
|
|
1289
|
+
outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
|
|
1290
|
+
} else if (!finalResult?.timedOut && !finalResult?.stopped && turnBudget?.outcome === "wrap-up-requested") {
|
|
1080
1291
|
const note = turnBudgetSoftNote(turnBudget, turnBudget.wrapUpRequestedAtTurn ?? turnBudget.turnCount);
|
|
1081
1292
|
outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
|
|
1082
1293
|
}
|
|
1083
1294
|
const outputForAcceptance = rawOutput;
|
|
1084
|
-
|
|
1085
|
-
|
|
1295
|
+
const childWrittenOutput = step.outputPath
|
|
1296
|
+
? extractChildWrittenOutput(finalResult?.messages, step.outputPath, step.cwd ?? ctx.cwd)
|
|
1297
|
+
: undefined;
|
|
1298
|
+
const finalizedOutput = finalizeSingleOutput({
|
|
1299
|
+
fullOutput: outputForSummary,
|
|
1086
1300
|
outputPath: step.outputPath,
|
|
1087
1301
|
outputMode: step.outputMode,
|
|
1088
1302
|
exitCode: finalResult?.exitCode ?? 1,
|
|
@@ -1091,28 +1305,42 @@ async function runSingleStep(
|
|
|
1091
1305
|
saveError: resolvedOutput.saveError,
|
|
1092
1306
|
});
|
|
1093
1307
|
outputForSummary = finalizedOutput.displayOutput;
|
|
1094
|
-
const acceptance = step.effectiveAcceptance && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1308
|
+
const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
|
|
1309
|
+
? await evaluateAcceptance({
|
|
1310
|
+
acceptance: step.effectiveAcceptance,
|
|
1311
|
+
output: outputForAcceptance,
|
|
1312
|
+
fileOutput: childWrittenOutput !== undefined && step.outputPath
|
|
1313
|
+
? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only" }
|
|
1314
|
+
: undefined,
|
|
1315
|
+
cwd: step.cwd ?? ctx.cwd,
|
|
1316
|
+
signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
|
|
1317
|
+
abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
|
|
1318
|
+
})
|
|
1102
1319
|
: undefined;
|
|
1103
|
-
const
|
|
1320
|
+
const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
|
|
1321
|
+
const timedOutAfterAcceptance = !stoppedAfterAcceptance && (finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true);
|
|
1104
1322
|
const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
|
|
1105
|
-
const effectiveAcceptance =
|
|
1323
|
+
const effectiveAcceptance = step.effectiveAcceptance
|
|
1324
|
+
? stoppedAfterAcceptance
|
|
1325
|
+
? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." })
|
|
1326
|
+
: timedOutAfterAcceptance
|
|
1327
|
+
? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." })
|
|
1328
|
+
: turnBudgetExceeded
|
|
1329
|
+
? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." })
|
|
1330
|
+
: acceptance
|
|
1331
|
+
: undefined;
|
|
1106
1332
|
const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
|
|
1107
|
-
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !turnBudgetExceeded;
|
|
1108
|
-
const effectiveFinalExitCode = timedOutAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
1109
|
-
const effectiveFinalError =
|
|
1110
|
-
? ctx.
|
|
1111
|
-
:
|
|
1112
|
-
?
|
|
1113
|
-
:
|
|
1114
|
-
?
|
|
1115
|
-
:
|
|
1333
|
+
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded;
|
|
1334
|
+
const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
1335
|
+
const effectiveFinalError = stoppedAfterAcceptance
|
|
1336
|
+
? ctx.stopMessage ?? "Subagent stopped by user."
|
|
1337
|
+
: timedOutAfterAcceptance
|
|
1338
|
+
? ctx.timeoutMessage ?? "Subagent timed out."
|
|
1339
|
+
: turnBudgetExceeded
|
|
1340
|
+
? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
|
|
1341
|
+
: acceptanceCanFailRun
|
|
1342
|
+
? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
|
|
1343
|
+
: finalResult?.error;
|
|
1116
1344
|
|
|
1117
1345
|
if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
|
|
1118
1346
|
if (ctx.artifactConfig?.includeOutput !== false) {
|
|
@@ -1129,6 +1357,8 @@ async function runSingleStep(
|
|
|
1129
1357
|
model: finalResult?.model,
|
|
1130
1358
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
1131
1359
|
modelAttempts,
|
|
1360
|
+
error: effectiveFinalError,
|
|
1361
|
+
acceptance: effectiveAcceptance,
|
|
1132
1362
|
...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
|
|
1133
1363
|
transcriptError: transcriptWriter?.getError(),
|
|
1134
1364
|
skills: step.skills,
|
|
@@ -1144,6 +1374,7 @@ async function runSingleStep(
|
|
|
1144
1374
|
output: outputForSummary,
|
|
1145
1375
|
exitCode: effectiveFinalExitCode,
|
|
1146
1376
|
error: effectiveFinalError,
|
|
1377
|
+
protocolError: finalResult?.protocolError,
|
|
1147
1378
|
sessionFile: step.sessionFile,
|
|
1148
1379
|
intercomTarget: ctx.childIntercomTarget,
|
|
1149
1380
|
model: finalResult?.model,
|
|
@@ -1153,18 +1384,20 @@ async function runSingleStep(
|
|
|
1153
1384
|
artifactPaths,
|
|
1154
1385
|
transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
|
|
1155
1386
|
transcriptError: transcriptWriter?.getError(),
|
|
1156
|
-
interrupted: timedOutAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
|
|
1387
|
+
interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
|
|
1157
1388
|
timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
|
|
1389
|
+
stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
|
|
1158
1390
|
turnBudget,
|
|
1159
1391
|
turnBudgetExceeded: turnBudgetExceeded || undefined,
|
|
1160
|
-
wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
|
|
1392
|
+
wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
|
|
1161
1393
|
toolBudget,
|
|
1162
1394
|
toolBudgetBlocked: toolBudgetBlocked || undefined,
|
|
1163
1395
|
completionGuardTriggered: completionGuardTriggeredFinal,
|
|
1164
|
-
structuredOutput: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
1165
|
-
structuredOutputPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
|
|
1166
|
-
structuredOutputSchemaPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
|
|
1396
|
+
structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
1397
|
+
structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
|
|
1398
|
+
structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
|
|
1167
1399
|
acceptance: effectiveAcceptance,
|
|
1400
|
+
watchdog: finalResult?.watchdog,
|
|
1168
1401
|
};
|
|
1169
1402
|
}
|
|
1170
1403
|
|
|
@@ -1312,7 +1545,26 @@ function resolveAsyncStepTranscriptPath(input: {
|
|
|
1312
1545
|
|
|
1313
1546
|
type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
|
|
1314
1547
|
|
|
1315
|
-
|
|
1548
|
+
function combinedAbortSignal(signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
|
|
1549
|
+
const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal));
|
|
1550
|
+
if (activeSignals.length === 0) return undefined;
|
|
1551
|
+
if (activeSignals.length === 1) return activeSignals[0];
|
|
1552
|
+
const controller = new AbortController();
|
|
1553
|
+
const abort = (): void => controller.abort();
|
|
1554
|
+
for (const signal of activeSignals) {
|
|
1555
|
+
if (signal.aborted) {
|
|
1556
|
+
abort();
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1559
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1560
|
+
}
|
|
1561
|
+
return controller.signal;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
async function runSubagent(
|
|
1565
|
+
config: SubagentRunConfig,
|
|
1566
|
+
onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
|
|
1567
|
+
): Promise<void> {
|
|
1316
1568
|
const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
|
|
1317
1569
|
config;
|
|
1318
1570
|
const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
@@ -1328,16 +1580,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1328
1580
|
const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
1329
1581
|
const activeChildInterrupts = new Map<number, () => void>();
|
|
1330
1582
|
const activeChildTimeouts = new Map<number, () => void>();
|
|
1583
|
+
const activeChildStops = new Map<number, () => void>();
|
|
1331
1584
|
const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
|
|
1332
1585
|
const pendingStepSteers: SteerRequest[] = [];
|
|
1586
|
+
const steeringCapabilities = new Map<number, SteerCapability>();
|
|
1333
1587
|
let interrupted = false;
|
|
1334
1588
|
let currentActivityState: ActivityState | undefined;
|
|
1335
1589
|
let activityTimer: NodeJS.Timeout | undefined;
|
|
1336
1590
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
1337
1591
|
let timedOut = false;
|
|
1592
|
+
let stopped = false;
|
|
1338
1593
|
let turnBudgetExceeded = false;
|
|
1339
1594
|
const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
|
|
1595
|
+
const stopMessage = "Subagent stopped by user.";
|
|
1340
1596
|
const timeoutAbortController = new AbortController();
|
|
1597
|
+
const stopAbortController = new AbortController();
|
|
1341
1598
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
1342
1599
|
let latestSessionFile: string | undefined;
|
|
1343
1600
|
|
|
@@ -1417,7 +1674,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1417
1674
|
runId: id,
|
|
1418
1675
|
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
1419
1676
|
mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
|
|
1677
|
+
...(config.nestedRoute ? { isNested: true } : {}),
|
|
1420
1678
|
state: "running",
|
|
1679
|
+
steering: createSteeringStatus(),
|
|
1421
1680
|
lastActivityAt: overallStartTime,
|
|
1422
1681
|
startedAt: overallStartTime,
|
|
1423
1682
|
lastUpdate: overallStartTime,
|
|
@@ -1464,9 +1723,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1464
1723
|
const refreshWorkflowGraph = (): void => {
|
|
1465
1724
|
if (!config.workflowGraph) return;
|
|
1466
1725
|
const graph = structuredClone(statusPayload.workflowGraph ?? config.workflowGraph);
|
|
1467
|
-
const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "detached" => {
|
|
1726
|
+
const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "stopped" | "detached" => {
|
|
1468
1727
|
if (status === "complete" || status === "completed") return "completed";
|
|
1469
|
-
if (status === "running" || status === "failed" || status === "paused" || status === "pending") return status;
|
|
1728
|
+
if (status === "running" || status === "failed" || status === "paused" || status === "stopped" || status === "pending") return status;
|
|
1470
1729
|
return "pending";
|
|
1471
1730
|
};
|
|
1472
1731
|
const updateNode = (node: NonNullable<typeof graph.nodes>[number]): void => {
|
|
@@ -1483,10 +1742,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1483
1742
|
if (node.children?.length) {
|
|
1484
1743
|
if (node.children.every((child) => child.status === "completed")) node.status = "completed";
|
|
1485
1744
|
else if (node.children.some((child) => child.status === "running")) node.status = "running";
|
|
1745
|
+
else if (node.children.some((child) => child.status === "stopped")) node.status = "stopped";
|
|
1486
1746
|
else if (node.children.some((child) => child.status === "failed")) node.status = "failed";
|
|
1487
1747
|
else if (node.children.some((child) => child.status === "paused")) node.status = "paused";
|
|
1488
1748
|
}
|
|
1489
|
-
if (node.error) node.status = "failed";
|
|
1749
|
+
if (node.error && node.status !== "stopped") node.status = "failed";
|
|
1490
1750
|
};
|
|
1491
1751
|
for (const node of graph.nodes) updateNode(node);
|
|
1492
1752
|
statusPayload.workflowGraph = graph;
|
|
@@ -1512,6 +1772,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1512
1772
|
activeChildTimeouts.set(flatIndex, interrupt);
|
|
1513
1773
|
if (timedOut) interrupt();
|
|
1514
1774
|
};
|
|
1775
|
+
const registerStepStop = (flatIndex: number, stop: (() => void) | undefined): void => {
|
|
1776
|
+
if (!stop) {
|
|
1777
|
+
activeChildStops.delete(flatIndex);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
activeChildStops.set(flatIndex, stop);
|
|
1781
|
+
if (stopped) stop();
|
|
1782
|
+
};
|
|
1515
1783
|
const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
|
|
1516
1784
|
if (!abort) {
|
|
1517
1785
|
activeChildTurnBudgetAborts.delete(flatIndex);
|
|
@@ -1525,6 +1793,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1525
1793
|
const timeoutActiveChildren = (): void => {
|
|
1526
1794
|
for (const interrupt of [...activeChildTimeouts.values()]) interrupt();
|
|
1527
1795
|
};
|
|
1796
|
+
const stopActiveChildren = (): void => {
|
|
1797
|
+
for (const stop of [...activeChildStops.values()]) stop();
|
|
1798
|
+
};
|
|
1528
1799
|
const nestedRuns = function* (children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
|
|
1529
1800
|
for (const child of children ?? []) {
|
|
1530
1801
|
yield child;
|
|
@@ -1563,6 +1834,37 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1563
1834
|
}
|
|
1564
1835
|
}
|
|
1565
1836
|
};
|
|
1837
|
+
const stopNestedAsyncDescendants = (): void => {
|
|
1838
|
+
if (!config.nestedRoute) return;
|
|
1839
|
+
let registry: ReturnType<typeof projectNestedEvents>;
|
|
1840
|
+
try {
|
|
1841
|
+
registry = projectNestedEvents(config.nestedRoute);
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1844
|
+
type: "subagent.nested.stop_failed",
|
|
1845
|
+
ts: Date.now(),
|
|
1846
|
+
runId: id,
|
|
1847
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1848
|
+
}));
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
for (const run of nestedRuns(registry.children)) {
|
|
1852
|
+
if (run.state !== "running" && run.state !== "queued") continue;
|
|
1853
|
+
const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
|
|
1854
|
+
if (!nestedAsyncDir) continue;
|
|
1855
|
+
try {
|
|
1856
|
+
deliverStopRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-stop" });
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1859
|
+
type: "subagent.nested.stop_failed",
|
|
1860
|
+
ts: Date.now(),
|
|
1861
|
+
runId: id,
|
|
1862
|
+
targetRunId: run.id,
|
|
1863
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1864
|
+
}));
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1566
1868
|
const timeoutNestedAsyncDescendants = (): void => {
|
|
1567
1869
|
if (!config.nestedRoute) return;
|
|
1568
1870
|
let registry: ReturnType<typeof projectNestedEvents>;
|
|
@@ -1607,6 +1909,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1607
1909
|
exitCode: 1,
|
|
1608
1910
|
timedOut: true,
|
|
1609
1911
|
});
|
|
1912
|
+
const stoppedStepResult = (agent: string): SingleStepResult => ({
|
|
1913
|
+
agent,
|
|
1914
|
+
output: stopMessage,
|
|
1915
|
+
error: stopMessage,
|
|
1916
|
+
exitCode: 1,
|
|
1917
|
+
stopped: true,
|
|
1918
|
+
});
|
|
1610
1919
|
const consumePendingAppendRequests = (): void => {
|
|
1611
1920
|
if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
|
|
1612
1921
|
const requests = consumeChainAppendRequests(asyncDir);
|
|
@@ -1646,7 +1955,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1646
1955
|
}));
|
|
1647
1956
|
}
|
|
1648
1957
|
};
|
|
1649
|
-
const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
|
|
1958
|
+
const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running" | "stopped", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
|
|
1650
1959
|
const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
|
|
1651
1960
|
if (!groupNode) return;
|
|
1652
1961
|
groupNode.status = status;
|
|
@@ -1737,48 +2046,125 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1737
2046
|
appendControlEvent(event);
|
|
1738
2047
|
return true;
|
|
1739
2048
|
};
|
|
2049
|
+
const steeringMarkerPath = (requestId: string): string => path.join(asyncDir, "control", "steer-recovery", `${Buffer.from(requestId).toString("base64url")}.json`);
|
|
2050
|
+
const markSteeringAttention = (index: number): void => {
|
|
2051
|
+
const step = statusPayload.steps[index];
|
|
2052
|
+
if (step) step.activityState = "needs_attention";
|
|
2053
|
+
statusPayload.activityState = "needs_attention";
|
|
2054
|
+
};
|
|
2055
|
+
const emitSteeringEvent = (type: string, request: SteerRequest, index?: number, extra: Record<string, unknown> = {}): void => {
|
|
2056
|
+
appendJsonl(eventsPath, JSON.stringify({ type, ts: Date.now(), runId: id, requestId: request.id, ...(index !== undefined ? { index } : {}), ...extra }));
|
|
2057
|
+
};
|
|
2058
|
+
const emitSteeringNotice = (requestId: string, state: "failed" | "partial" | "recovered", message: string): void => {
|
|
2059
|
+
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.steering.notice", ts: Date.now(), runId: id, requestId, state, message, ...(config.sessionId ? { currentSessionId: config.sessionId } : {}) }));
|
|
2060
|
+
};
|
|
2061
|
+
const recordSteeringLifecycle = (request: SteerRequest, targets: Array<{ index: number; state: SteeringTargetState; reason?: string }>): void => {
|
|
2062
|
+
const lifecycle = steeringStatus(statusPayload);
|
|
2063
|
+
recordSteeringRequest(lifecycle, { id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets });
|
|
2064
|
+
for (const target of targets) {
|
|
2065
|
+
const step = statusPayload.steps[target.index];
|
|
2066
|
+
if (!step) continue;
|
|
2067
|
+
step.steering ??= createSteeringStatus();
|
|
2068
|
+
recordSteeringRequest(step.steering, { id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets: [target] });
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
const updateSteeringLifecycleTarget = (
|
|
2072
|
+
requestId: string,
|
|
2073
|
+
index: number,
|
|
2074
|
+
state: SteeringTargetState,
|
|
2075
|
+
now: number,
|
|
2076
|
+
fields: Pick<SteeringTargetStatus, "reason" | "replacementRunId"> = {},
|
|
2077
|
+
): SteeringTargetStatus | undefined => {
|
|
2078
|
+
const updated = updateSteeringTarget(steeringStatus(statusPayload), requestId, index, state, now, fields);
|
|
2079
|
+
const step = statusPayload.steps[index];
|
|
2080
|
+
if (step?.steering) updateSteeringTarget(step.steering, requestId, index, state, now, fields);
|
|
2081
|
+
return updated;
|
|
2082
|
+
};
|
|
2083
|
+
const emitTerminalSteeringNotice = (requestId: string, failureMessage: string): void => {
|
|
2084
|
+
const state = terminalSteeringNoticeState(steeringStatus(statusPayload), requestId);
|
|
2085
|
+
if (state === "partial") emitSteeringNotice(requestId, "partial", `Steering partially delivered for run ${id}.`);
|
|
2086
|
+
else if (state === "failed") emitSteeringNotice(requestId, "failed", failureMessage);
|
|
2087
|
+
};
|
|
1740
2088
|
const deliverSteerRequest = (request: SteerRequest): void => {
|
|
1741
|
-
if (statusPayload.state !== "running")
|
|
2089
|
+
if (statusPayload.state !== "running") {
|
|
2090
|
+
const reason = `run became ${statusPayload.state} before steering request was consumed`;
|
|
2091
|
+
const indexes = request.targetIndex !== undefined
|
|
2092
|
+
? [request.targetIndex]
|
|
2093
|
+
: request.targetIndexes?.length
|
|
2094
|
+
? request.targetIndexes
|
|
2095
|
+
: statusPayload.steps.map((_, index) => index);
|
|
2096
|
+
const targets = indexes.map((index) => ({ index, state: "failed" as const, reason }));
|
|
2097
|
+
recordSteeringLifecycle(request, targets);
|
|
2098
|
+
emitSteeringEvent("subagent.steer.requested", request, undefined, { targets });
|
|
2099
|
+
for (const target of targets) emitSteeringEvent("subagent.steer.failed", request, target.index, { reason });
|
|
2100
|
+
emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: ${reason}.`);
|
|
2101
|
+
statusPayload.lastUpdate = Date.now();
|
|
2102
|
+
writeStatusPayload();
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
1742
2105
|
const runningIndexes = statusPayload.steps
|
|
1743
2106
|
.map((step, index) => ({ step, index }))
|
|
1744
2107
|
.filter(({ step }) => step.status === "running")
|
|
1745
2108
|
.map(({ index }) => index);
|
|
1746
|
-
const targets = request.targetIndex !== undefined
|
|
2109
|
+
const targets = request.targetIndex !== undefined
|
|
2110
|
+
? [request.targetIndex]
|
|
2111
|
+
: request.targetIndexes?.length
|
|
2112
|
+
? request.targetIndexes
|
|
2113
|
+
: runningIndexes.length > 0
|
|
2114
|
+
? runningIndexes
|
|
2115
|
+
: statusPayload.mode === "single" && statusPayload.steps[0]?.status === "pending"
|
|
2116
|
+
? [0]
|
|
2117
|
+
: [];
|
|
1747
2118
|
const now = Date.now();
|
|
1748
|
-
const
|
|
1749
|
-
const rejected: Array<{ index: number; reason: string }> = [];
|
|
1750
|
-
for (const index of targets) {
|
|
2119
|
+
const targetStates = targets.map((index) => {
|
|
1751
2120
|
const step = statusPayload.steps[index];
|
|
1752
|
-
if (!step) {
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2121
|
+
if (!step) return { index, state: "failed" as const, reason: "child index out of range" };
|
|
2122
|
+
if (step.status === "pending") return { index, state: "scheduled" as const };
|
|
2123
|
+
if (step.status !== "running") return { index, state: "failed" as const, reason: `child is ${step.status}` };
|
|
2124
|
+
if (steeringCapabilities.get(index)?.supported === false) return { index, state: "failed" as const, reason: "child Pi session does not support steering" };
|
|
2125
|
+
return { index, state: "routed" as const };
|
|
2126
|
+
});
|
|
2127
|
+
recordSteeringLifecycle(request, targetStates);
|
|
2128
|
+
emitSteeringEvent("subagent.steer.requested", request, undefined, { targets: targetStates });
|
|
2129
|
+
for (const target of targetStates) {
|
|
2130
|
+
if (target.state === "routed") {
|
|
2131
|
+
try {
|
|
2132
|
+
enqueueStepSteer(asyncDir, target.index, request);
|
|
2133
|
+
updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
|
|
2134
|
+
emitSteeringEvent("subagent.steer.routed", request, target.index);
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
markSteeringAttention(target.index);
|
|
2137
|
+
updateSteeringLifecycleTarget(request.id, target.index, "failed", now, { reason: error instanceof Error ? error.message : String(error) });
|
|
2138
|
+
emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: error instanceof Error ? error.message : String(error) });
|
|
2139
|
+
}
|
|
2140
|
+
} else if (target.state === "failed") {
|
|
2141
|
+
markSteeringAttention(target.index);
|
|
2142
|
+
emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: target.reason });
|
|
2143
|
+
} else {
|
|
2144
|
+
emitSteeringEvent("subagent.steer.scheduled", request, target.index);
|
|
1759
2145
|
}
|
|
1760
|
-
enqueueStepSteer(asyncDir, index, request);
|
|
1761
|
-
step.steerCount = (step.steerCount ?? 0) + 1;
|
|
1762
|
-
step.lastSteerAt = now;
|
|
1763
|
-
accepted.push(index);
|
|
1764
2146
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2147
|
+
emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: no requested child remained steerable.`);
|
|
2148
|
+
statusPayload.lastUpdate = now;
|
|
2149
|
+
writeStatusPayload();
|
|
2150
|
+
};
|
|
2151
|
+
const consumeSteerAck = (ack: SteerAck): void => {
|
|
2152
|
+
const lifecycle = steeringStatus(statusPayload);
|
|
2153
|
+
const request = lifecycle.recent.find((candidate) => candidate.id === ack.requestId);
|
|
2154
|
+
if (!request || !request.targets.some((target) => target.index === ack.index)) return;
|
|
2155
|
+
const late = fs.existsSync(steeringMarkerPath(ack.requestId));
|
|
2156
|
+
const now = Date.now();
|
|
2157
|
+
if (ack.state === "delivered") {
|
|
2158
|
+
updateSteeringLifecycleTarget(ack.requestId, ack.index, late ? "late" : "delivered", now, { reason: late ? "acknowledged after recovery commit" : undefined });
|
|
2159
|
+
emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { late, message: ack.message });
|
|
2160
|
+
} else {
|
|
2161
|
+
markSteeringAttention(ack.index);
|
|
2162
|
+
updateSteeringLifecycleTarget(ack.requestId, ack.index, "failed", now, { reason: ack.message });
|
|
2163
|
+
emitSteeringEvent("subagent.steer.failed", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { reason: ack.message });
|
|
1770
2164
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
runId: id,
|
|
1775
|
-
requestId: request.id,
|
|
1776
|
-
message: request.message,
|
|
1777
|
-
...(request.source ? { source: request.source } : {}),
|
|
1778
|
-
...(request.targetIndex !== undefined ? { targetIndex: request.targetIndex } : {}),
|
|
1779
|
-
acceptedIndexes: accepted,
|
|
1780
|
-
...(rejected.length ? { rejected } : {}),
|
|
1781
|
-
}));
|
|
2165
|
+
emitTerminalSteeringNotice(ack.requestId, `Steering failed for run ${id}: ${ack.message}`);
|
|
2166
|
+
statusPayload.lastUpdate = now;
|
|
2167
|
+
writeStatusPayload();
|
|
1782
2168
|
};
|
|
1783
2169
|
const flushPendingStepSteers = (flatIndex: number): void => {
|
|
1784
2170
|
const remaining: SteerRequest[] = [];
|
|
@@ -1797,25 +2183,51 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1797
2183
|
statusPayload.lastUpdate = now;
|
|
1798
2184
|
writeStatusPayload();
|
|
1799
2185
|
};
|
|
1800
|
-
const updateStepTurnBudget = (
|
|
2186
|
+
const updateStepTurnBudget = (
|
|
2187
|
+
flatIndex: number,
|
|
2188
|
+
turnCount: number,
|
|
2189
|
+
now: number,
|
|
2190
|
+
terminalAssistantStop: boolean,
|
|
2191
|
+
toolWorkActiveOrStarting: boolean,
|
|
2192
|
+
): void => {
|
|
1801
2193
|
const budget = config.turnBudget;
|
|
1802
2194
|
const step = statusPayload.steps[flatIndex];
|
|
1803
|
-
if (!budget || !step || timedOut || turnBudgetExceeded || step.turnBudgetExceeded) return;
|
|
2195
|
+
if (!budget || !step || timedOut || stopped || turnBudgetExceeded || step.turnBudgetExceeded) return;
|
|
1804
2196
|
if (turnCount < budget.maxTurns) {
|
|
1805
2197
|
const state: TurnBudgetState = { ...budget, outcome: "within-budget", turnCount };
|
|
1806
2198
|
step.turnBudget = state;
|
|
1807
2199
|
statusPayload.turnBudget = state;
|
|
1808
2200
|
return;
|
|
1809
2201
|
}
|
|
1810
|
-
const state = turnBudgetState(budget, turnCount, false);
|
|
1811
|
-
step.turnBudget = state;
|
|
1812
|
-
statusPayload.turnBudget = state;
|
|
1813
2202
|
if (!step.wrapUpRequested) {
|
|
1814
2203
|
step.wrapUpRequested = true;
|
|
1815
2204
|
statusPayload.wrapUpRequested = true;
|
|
1816
2205
|
appendRecentStepOutput(step, [turnBudgetSoftNote(budget, turnCount)]);
|
|
1817
2206
|
}
|
|
1818
|
-
|
|
2207
|
+
const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting);
|
|
2208
|
+
if (decision === "defer") {
|
|
2209
|
+
const firstDeferredTurn = step.turnBudget?.terminationDeferredAtTurn;
|
|
2210
|
+
const deferredState = turnBudgetDeferredState(budget, turnCount, firstDeferredTurn);
|
|
2211
|
+
step.turnBudget = deferredState;
|
|
2212
|
+
statusPayload.turnBudget = deferredState;
|
|
2213
|
+
if (firstDeferredTurn === undefined) {
|
|
2214
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
2215
|
+
type: "subagent.step.turn_budget_deferred",
|
|
2216
|
+
ts: now,
|
|
2217
|
+
runId: id,
|
|
2218
|
+
stepIndex: flatIndex,
|
|
2219
|
+
agent: step.agent,
|
|
2220
|
+
turnCount,
|
|
2221
|
+
maxTurns: budget.maxTurns,
|
|
2222
|
+
graceTurns: budget.graceTurns,
|
|
2223
|
+
}));
|
|
2224
|
+
}
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const state = turnBudgetState(budget, turnCount, false);
|
|
2228
|
+
step.turnBudget = state;
|
|
2229
|
+
statusPayload.turnBudget = state;
|
|
2230
|
+
if (decision !== "abort") return;
|
|
1819
2231
|
const exceededState = turnBudgetState(budget, turnCount, true);
|
|
1820
2232
|
const message = turnBudgetExceededMessage(budget, turnCount);
|
|
1821
2233
|
step.turnBudget = exceededState;
|
|
@@ -1836,6 +2248,22 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1836
2248
|
if (!step) return;
|
|
1837
2249
|
const now = Date.now();
|
|
1838
2250
|
statusPayload.currentStep = flatIndex;
|
|
2251
|
+
if (isChildWatchdogStatusEvent(event)) {
|
|
2252
|
+
const next = acceptChildWatchdogEvent({
|
|
2253
|
+
current: step.watchdog,
|
|
2254
|
+
event,
|
|
2255
|
+
runId: id,
|
|
2256
|
+
agent: step.agent,
|
|
2257
|
+
childIndex: flatIndex,
|
|
2258
|
+
});
|
|
2259
|
+
if (!next) return;
|
|
2260
|
+
step.watchdog = next;
|
|
2261
|
+
step.lastActivityAt = now;
|
|
2262
|
+
statusPayload.lastActivityAt = now;
|
|
2263
|
+
statusPayload.lastUpdate = now;
|
|
2264
|
+
writeStatusPayload();
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
1839
2267
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
1840
2268
|
const mutates = isMutatingTool(event.toolName, event.args);
|
|
1841
2269
|
const currentPath = resolveCurrentPath(event.toolName, event.args);
|
|
@@ -1925,7 +2353,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1925
2353
|
statusPayload.totalTokens = { input: totalInput + input, output: totalOutput + output, total: totalInput + totalOutput + input + output };
|
|
1926
2354
|
}
|
|
1927
2355
|
statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
|
|
1928
|
-
updateStepTurnBudget(
|
|
2356
|
+
updateStepTurnBudget(
|
|
2357
|
+
flatIndex,
|
|
2358
|
+
step.turnCount,
|
|
2359
|
+
now,
|
|
2360
|
+
isTerminalAssistantStop(event.message),
|
|
2361
|
+
assistantStartsToolCall(event.message) || Boolean(step.currentTool),
|
|
2362
|
+
);
|
|
1929
2363
|
}
|
|
1930
2364
|
syncTopLevelCurrentTool();
|
|
1931
2365
|
step.lastActivityAt = now;
|
|
@@ -1951,6 +2385,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1951
2385
|
config: controlConfig,
|
|
1952
2386
|
startedAt: step.startedAt ?? overallStartTime,
|
|
1953
2387
|
lastActivityAt,
|
|
2388
|
+
currentTool: step.currentTool,
|
|
1954
2389
|
now,
|
|
1955
2390
|
});
|
|
1956
2391
|
if (idleState === "needs_attention") {
|
|
@@ -2026,8 +2461,40 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2026
2461
|
interruptNestedAsyncDescendants();
|
|
2027
2462
|
interruptActiveChildren();
|
|
2028
2463
|
};
|
|
2464
|
+
const stopRunner = () => {
|
|
2465
|
+
if (stopped || timedOut || interrupted || statusPayload.state !== "running") return;
|
|
2466
|
+
stopped = true;
|
|
2467
|
+
const now = Date.now();
|
|
2468
|
+
statusPayload.state = "stopped";
|
|
2469
|
+
statusPayload.stopped = true;
|
|
2470
|
+
statusPayload.error = stopMessage;
|
|
2471
|
+
currentActivityState = undefined;
|
|
2472
|
+
statusPayload.activityState = undefined;
|
|
2473
|
+
statusPayload.lastUpdate = now;
|
|
2474
|
+
for (const step of statusPayload.steps) {
|
|
2475
|
+
if (step.status !== "running" && step.status !== "pending") continue;
|
|
2476
|
+
step.status = "stopped";
|
|
2477
|
+
step.error = stopMessage;
|
|
2478
|
+
step.exitCode = 1;
|
|
2479
|
+
step.stopped = true;
|
|
2480
|
+
step.activityState = undefined;
|
|
2481
|
+
step.endedAt = now;
|
|
2482
|
+
step.durationMs = step.startedAt ? now - step.startedAt : 0;
|
|
2483
|
+
step.lastActivityAt = now;
|
|
2484
|
+
}
|
|
2485
|
+
writeStatusPayload();
|
|
2486
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
2487
|
+
type: "subagent.run.stopped",
|
|
2488
|
+
ts: now,
|
|
2489
|
+
runId: id,
|
|
2490
|
+
message: stopMessage,
|
|
2491
|
+
}));
|
|
2492
|
+
stopAbortController.abort();
|
|
2493
|
+
stopNestedAsyncDescendants();
|
|
2494
|
+
stopActiveChildren();
|
|
2495
|
+
};
|
|
2029
2496
|
const timeoutRunner = () => {
|
|
2030
|
-
if (timedOut || interrupted || statusPayload.state !== "running") return;
|
|
2497
|
+
if (timedOut || stopped || interrupted || statusPayload.state !== "running") return;
|
|
2031
2498
|
timedOut = true;
|
|
2032
2499
|
const now = Date.now();
|
|
2033
2500
|
const message = timeoutMessage ?? "Subagent timed out.";
|
|
@@ -2068,12 +2535,36 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2068
2535
|
const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
|
|
2069
2536
|
onInterrupt: interruptRunner,
|
|
2070
2537
|
onTimeout: timeoutRunner,
|
|
2538
|
+
onStop: stopRunner,
|
|
2071
2539
|
onSteer: (request) => {
|
|
2072
2540
|
const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
|
|
2073
|
-
if (targetStep?.status === "pending")
|
|
2074
|
-
|
|
2075
|
-
|
|
2541
|
+
if (targetStep?.status === "pending") {
|
|
2542
|
+
deliverSteerRequest(request);
|
|
2543
|
+
pendingStepSteers.push(request);
|
|
2544
|
+
} else if (request.targetIndexes !== undefined || request.targetIndex !== undefined || statusPayload.steps.some((step) => step.status === "running")) {
|
|
2545
|
+
deliverSteerRequest(request);
|
|
2546
|
+
} else {
|
|
2547
|
+
deliverSteerRequest(request);
|
|
2548
|
+
pendingStepSteers.push(request);
|
|
2549
|
+
}
|
|
2550
|
+
},
|
|
2551
|
+
onSteerCapability: (capability) => {
|
|
2552
|
+
steeringCapabilities.set(capability.index, capability);
|
|
2553
|
+
if (!capability.supported) {
|
|
2554
|
+
const now = Date.now();
|
|
2555
|
+
const lifecycle = steeringStatus(statusPayload);
|
|
2556
|
+
for (const request of lifecycle.recent) {
|
|
2557
|
+
if (!request.targets.some((target) => target.index === capability.index && (target.state === "routed" || target.state === "scheduled"))) continue;
|
|
2558
|
+
markSteeringAttention(capability.index);
|
|
2559
|
+
updateSteeringLifecycleTarget(request.id, capability.index, "failed", now, { reason: "child Pi session does not support steering" });
|
|
2560
|
+
emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: "child Pi session does not support steering" }, capability.index, { reason: "child Pi session does not support steering" });
|
|
2561
|
+
emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child ${capability.index} does not support steering.`);
|
|
2562
|
+
}
|
|
2563
|
+
statusPayload.lastUpdate = now;
|
|
2564
|
+
writeStatusPayload();
|
|
2565
|
+
}
|
|
2076
2566
|
},
|
|
2567
|
+
onSteerAck: consumeSteerAck,
|
|
2077
2568
|
});
|
|
2078
2569
|
if (config.deadlineAt !== undefined) {
|
|
2079
2570
|
const remainingMs = Math.max(0, config.deadlineAt - Date.now());
|
|
@@ -2097,7 +2588,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2097
2588
|
let stepCursor = 0;
|
|
2098
2589
|
|
|
2099
2590
|
while (true) {
|
|
2100
|
-
if (interrupted || timedOut || turnBudgetExceeded) break;
|
|
2591
|
+
if (interrupted || timedOut || stopped || turnBudgetExceeded) break;
|
|
2101
2592
|
consumePendingAppendRequests();
|
|
2102
2593
|
if (stepCursor >= steps.length) break;
|
|
2103
2594
|
const stepIndex = stepCursor++;
|
|
@@ -2108,6 +2599,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2108
2599
|
let materialized: ReturnType<typeof materializeDynamicParallelStep>;
|
|
2109
2600
|
try {
|
|
2110
2601
|
materialized = materializeDynamicParallelStep(step as Parameters<typeof materializeDynamicParallelStep>[0], outputs, stepIndex, { maxItems: config.dynamicFanoutMaxItems, allowRunnerFields: true });
|
|
2602
|
+
if (materialized.parallel.length > 1 && step.parallel.outputPath && !step.parallel.namespaceOutputPath) {
|
|
2603
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} materialized ${materialized.parallel.length} items that resolve output to the same path: ${step.parallel.outputPath}. Remove the explicit output path or use an inherited relative agent output so each item can be isolated.`);
|
|
2604
|
+
}
|
|
2111
2605
|
if (materialized.collectedOnEmpty) validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
|
|
2112
2606
|
} catch (error) {
|
|
2113
2607
|
const now = Date.now();
|
|
@@ -2131,6 +2625,16 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2131
2625
|
break;
|
|
2132
2626
|
}
|
|
2133
2627
|
|
|
2628
|
+
const effectiveDynamicGroupAcceptance = resolveEffectiveAcceptance({
|
|
2629
|
+
explicit: step.acceptanceInput,
|
|
2630
|
+
agentName: step.parallel.agent,
|
|
2631
|
+
acceptanceRole: step.acceptanceRole,
|
|
2632
|
+
task: materialized.parallel.map((task) => task.task ?? step.parallel.task).join("\n") || step.parallel.task,
|
|
2633
|
+
mode: config.mode,
|
|
2634
|
+
async: true,
|
|
2635
|
+
dynamicGroup: true,
|
|
2636
|
+
});
|
|
2637
|
+
|
|
2134
2638
|
if (materialized.parallel.length === 0) {
|
|
2135
2639
|
const now = Date.now();
|
|
2136
2640
|
const collection = materialized.collectedOnEmpty ?? [];
|
|
@@ -2149,37 +2653,40 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2149
2653
|
placeholder.durationMs = 0;
|
|
2150
2654
|
}
|
|
2151
2655
|
previousOutput = "Dynamic fanout produced 0 results.";
|
|
2152
|
-
const groupAcceptance =
|
|
2656
|
+
const groupAcceptance = effectiveDynamicGroupAcceptance.explicit && !timedOut && !stopped
|
|
2153
2657
|
? await evaluateAcceptance({
|
|
2154
|
-
acceptance:
|
|
2658
|
+
acceptance: effectiveDynamicGroupAcceptance,
|
|
2155
2659
|
output: "",
|
|
2156
2660
|
report: aggregateAcceptanceReport({
|
|
2157
2661
|
results: [],
|
|
2158
2662
|
notes: "Dynamic fanout produced 0 results.",
|
|
2159
2663
|
}),
|
|
2160
2664
|
cwd,
|
|
2161
|
-
signal: timeoutAbortController.signal,
|
|
2162
|
-
abortMessage: timeoutMessage ?? "Subagent timed out.",
|
|
2665
|
+
signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
|
|
2666
|
+
abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
|
|
2163
2667
|
})
|
|
2164
2668
|
: undefined;
|
|
2165
|
-
const
|
|
2166
|
-
const
|
|
2669
|
+
const groupStopped = stopped || stopAbortController.signal.aborted;
|
|
2670
|
+
const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
|
|
2671
|
+
const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
|
|
2167
2672
|
if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
|
|
2168
2673
|
const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
2169
|
-
if (groupTimedOut || groupAcceptanceFailure) {
|
|
2170
|
-
const errorMessage = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
|
|
2171
|
-
statusPayload.state = "failed";
|
|
2674
|
+
if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
|
|
2675
|
+
const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
|
|
2676
|
+
statusPayload.state = groupStopped ? "stopped" : "failed";
|
|
2172
2677
|
statusPayload.error = errorMessage;
|
|
2678
|
+
statusPayload.stopped = groupStopped ? true : statusPayload.stopped;
|
|
2173
2679
|
if (placeholder) {
|
|
2174
|
-
placeholder.status = "failed";
|
|
2680
|
+
placeholder.status = groupStopped ? "stopped" : "failed";
|
|
2175
2681
|
placeholder.error = errorMessage;
|
|
2176
2682
|
placeholder.exitCode = 1;
|
|
2177
2683
|
placeholder.timedOut = groupTimedOut ? true : undefined;
|
|
2684
|
+
placeholder.stopped = groupStopped ? true : undefined;
|
|
2178
2685
|
}
|
|
2179
|
-
markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
|
|
2686
|
+
markDynamicGraphGroup(stepIndex, groupStopped ? "stopped" : "failed", errorMessage, effectiveGroupAcceptance);
|
|
2180
2687
|
statusPayload.lastUpdate = Date.now();
|
|
2181
2688
|
writeStatusPayload();
|
|
2182
|
-
results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, acceptance: effectiveGroupAcceptance });
|
|
2689
|
+
results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, stopped: groupStopped ? true : undefined, acceptance: effectiveGroupAcceptance });
|
|
2183
2690
|
break;
|
|
2184
2691
|
}
|
|
2185
2692
|
flatIndex++;
|
|
@@ -2193,9 +2700,25 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2193
2700
|
const thinkingOverride = step.thinkingOverrides?.[itemIndex];
|
|
2194
2701
|
const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
|
|
2195
2702
|
const thinking = thinkingOverride ? resolveEffectiveThinking(model, thinkingOverride) : undefined;
|
|
2703
|
+
const outputPath = step.parallel.namespaceOutputPath && step.parallel.outputPath
|
|
2704
|
+
? path.join(path.dirname(step.parallel.outputPath), `dynamic-${stepIndex}`, `${itemIndex}-${step.parallel.agent}`, path.basename(step.parallel.outputPath))
|
|
2705
|
+
: step.parallel.outputPath;
|
|
2706
|
+
const taskText = task.task ?? step.parallel.task;
|
|
2707
|
+
const materializedTask = step.parallel.namespaceOutputPath ? injectSingleOutputInstruction(taskText, outputPath, step.parallel) : taskText;
|
|
2196
2708
|
return {
|
|
2197
2709
|
...step.parallel,
|
|
2198
|
-
task:
|
|
2710
|
+
task: materializedTask,
|
|
2711
|
+
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
2712
|
+
explicit: step.parallel.acceptanceInput,
|
|
2713
|
+
agentName: step.parallel.agent,
|
|
2714
|
+
acceptanceRole: step.parallel.acceptanceRole,
|
|
2715
|
+
task: materializedTask,
|
|
2716
|
+
mode: config.mode,
|
|
2717
|
+
async: true,
|
|
2718
|
+
dynamic: step.parallel.acceptanceInput === undefined,
|
|
2719
|
+
}),
|
|
2720
|
+
systemPrompt: step.parallel.namespaceOutputPath ? injectOutputPathSystemPrompt(step.parallel.systemPrompt ?? "", outputPath, step.parallel) : step.parallel.systemPrompt,
|
|
2721
|
+
outputPath,
|
|
2199
2722
|
label: task.label ?? step.parallel.label,
|
|
2200
2723
|
...(step.sessionFiles?.[itemIndex] ? { sessionFile: step.sessionFiles[itemIndex] } : {}),
|
|
2201
2724
|
...(thinkingOverride ? {
|
|
@@ -2276,6 +2799,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2276
2799
|
const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
|
|
2277
2800
|
const fi = groupStartFlatIndex + taskIdx;
|
|
2278
2801
|
if (timedOut) return timedOutStepResult(task.agent);
|
|
2802
|
+
if (stopped) return stoppedStepResult(task.agent);
|
|
2279
2803
|
if (interrupted) return pausedStepResult(task.agent);
|
|
2280
2804
|
if (aborted && failFast) {
|
|
2281
2805
|
const skippedAt = Date.now();
|
|
@@ -2311,6 +2835,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2311
2835
|
flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
2312
2836
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
2313
2837
|
steerInboxDir: stepSteerInboxDir(asyncDir, fi),
|
|
2838
|
+
steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
|
|
2839
|
+
steerAckDir: steerAcksDir(asyncDir, fi),
|
|
2314
2840
|
piPackageRoot: config.piPackageRoot,
|
|
2315
2841
|
piArgv1: config.piArgv1,
|
|
2316
2842
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
@@ -2318,21 +2844,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2318
2844
|
nestedRoute: config.nestedRoute,
|
|
2319
2845
|
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2320
2846
|
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2847
|
+
registerStop: (stop) => registerStepStop(fi, stop),
|
|
2321
2848
|
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
|
|
2322
2849
|
timeoutSignal: timeoutAbortController.signal,
|
|
2850
|
+
stopSignal: stopAbortController.signal,
|
|
2323
2851
|
timeoutMessage,
|
|
2852
|
+
stopMessage,
|
|
2324
2853
|
turnBudget: config.turnBudget,
|
|
2325
2854
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
2326
2855
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2327
|
-
|
|
2856
|
+
onWriterProcess,
|
|
2857
|
+
skipAcceptance: () => timedOut || stopped,
|
|
2328
2858
|
});
|
|
2329
2859
|
const taskEndTime = Date.now();
|
|
2330
2860
|
const childInterrupted = singleResult.interrupted === true;
|
|
2331
|
-
|
|
2861
|
+
const childStopped = singleResult.stopped === true;
|
|
2862
|
+
statusPayload.steps[fi].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2332
2863
|
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
2333
2864
|
statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
|
|
2334
|
-
statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2865
|
+
statusPayload.steps[fi].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2335
2866
|
statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2867
|
+
statusPayload.steps[fi].stopped = stopped || childStopped ? true : undefined;
|
|
2336
2868
|
statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
|
|
2337
2869
|
statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2338
2870
|
statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
|
|
@@ -2348,22 +2880,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2348
2880
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
2349
2881
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
2350
2882
|
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2351
|
-
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2883
|
+
statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2352
2884
|
statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
|
|
2353
2885
|
statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
|
|
2354
2886
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
2355
2887
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2356
2888
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
2357
2889
|
statusPayload.steps[fi].acceptance = singleResult.acceptance;
|
|
2890
|
+
statusPayload.steps[fi].watchdog = singleResult.watchdog;
|
|
2358
2891
|
statusPayload.lastUpdate = taskEndTime;
|
|
2359
2892
|
writeStatusPayload();
|
|
2360
2893
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2361
|
-
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2894
|
+
type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2362
2895
|
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
2363
|
-
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
2896
|
+
exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
2364
2897
|
}));
|
|
2365
2898
|
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
2366
|
-
return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
|
|
2899
|
+
return stopped || childStopped ? { ...singleResult, output: stopMessage, error: stopMessage, exitCode: 1, interrupted: false, timedOut: false, stopped: true, skipped: false } : timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
|
|
2367
2900
|
}, globalSemaphore);
|
|
2368
2901
|
|
|
2369
2902
|
flatIndex += dynamicSteps.length;
|
|
@@ -2372,11 +2905,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2372
2905
|
agent: pr.agent,
|
|
2373
2906
|
output: pr.output,
|
|
2374
2907
|
error: pr.error,
|
|
2375
|
-
|
|
2908
|
+
protocolError: pr.protocolError,
|
|
2909
|
+
success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
|
|
2376
2910
|
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
2377
2911
|
skipped: pr.skipped,
|
|
2378
2912
|
interrupted: pr.interrupted,
|
|
2379
2913
|
timedOut: pr.timedOut,
|
|
2914
|
+
stopped: pr.stopped,
|
|
2380
2915
|
turnBudget: pr.turnBudget,
|
|
2381
2916
|
turnBudgetExceeded: pr.turnBudgetExceeded,
|
|
2382
2917
|
wrapUpRequested: pr.wrapUpRequested,
|
|
@@ -2395,6 +2930,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2395
2930
|
structuredOutputPath: pr.structuredOutputPath,
|
|
2396
2931
|
structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
|
|
2397
2932
|
acceptance: pr.acceptance,
|
|
2933
|
+
watchdog: pr.watchdog,
|
|
2398
2934
|
});
|
|
2399
2935
|
}
|
|
2400
2936
|
const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
|
|
@@ -2409,24 +2945,25 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2409
2945
|
stepIndex,
|
|
2410
2946
|
};
|
|
2411
2947
|
statusPayload.outputs = outputs;
|
|
2412
|
-
const groupAcceptance =
|
|
2948
|
+
const groupAcceptance = !timedOut && !stopped
|
|
2413
2949
|
? await evaluateAcceptance({
|
|
2414
|
-
acceptance:
|
|
2950
|
+
acceptance: effectiveDynamicGroupAcceptance,
|
|
2415
2951
|
output: "",
|
|
2416
2952
|
report: aggregateAcceptanceReport({
|
|
2417
2953
|
results: parallelResults,
|
|
2418
2954
|
notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
|
|
2419
2955
|
}),
|
|
2420
2956
|
cwd,
|
|
2421
|
-
signal: timeoutAbortController.signal,
|
|
2422
|
-
abortMessage: timeoutMessage ?? "Subagent timed out.",
|
|
2957
|
+
signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
|
|
2958
|
+
abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
|
|
2423
2959
|
})
|
|
2424
2960
|
: undefined;
|
|
2425
|
-
const
|
|
2426
|
-
const
|
|
2427
|
-
const
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2961
|
+
const groupStopped = stopped || stopAbortController.signal.aborted;
|
|
2962
|
+
const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
|
|
2963
|
+
const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
|
|
2964
|
+
const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
2965
|
+
const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
|
|
2966
|
+
markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
|
|
2430
2967
|
if (groupError) {
|
|
2431
2968
|
results.push({
|
|
2432
2969
|
agent: step.parallel.agent,
|
|
@@ -2435,10 +2972,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2435
2972
|
success: false,
|
|
2436
2973
|
exitCode: 1,
|
|
2437
2974
|
timedOut: groupTimedOut ? true : undefined,
|
|
2975
|
+
stopped: groupStopped ? true : undefined,
|
|
2438
2976
|
structuredOutput: collection,
|
|
2439
2977
|
acceptance: effectiveGroupAcceptance,
|
|
2440
2978
|
});
|
|
2441
2979
|
statusPayload.error = groupError;
|
|
2980
|
+
statusPayload.stopped = groupStopped ? true : statusPayload.stopped;
|
|
2442
2981
|
}
|
|
2443
2982
|
} catch (error) {
|
|
2444
2983
|
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
@@ -2547,6 +3086,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2547
3086
|
async (task, taskIdx) => {
|
|
2548
3087
|
const fi = groupStartFlatIndex + taskIdx;
|
|
2549
3088
|
if (timedOut) return timedOutStepResult(task.agent);
|
|
3089
|
+
if (stopped) return stoppedStepResult(task.agent);
|
|
2550
3090
|
if (interrupted) return pausedStepResult(task.agent);
|
|
2551
3091
|
if (aborted && failFast) {
|
|
2552
3092
|
const skippedAt = Date.now();
|
|
@@ -2598,6 +3138,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2598
3138
|
flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
2599
3139
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
2600
3140
|
steerInboxDir: stepSteerInboxDir(asyncDir, fi),
|
|
3141
|
+
steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
|
|
3142
|
+
steerAckDir: steerAcksDir(asyncDir, fi),
|
|
2601
3143
|
piPackageRoot: config.piPackageRoot,
|
|
2602
3144
|
piArgv1: config.piArgv1,
|
|
2603
3145
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
@@ -2605,13 +3147,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2605
3147
|
nestedRoute: config.nestedRoute,
|
|
2606
3148
|
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2607
3149
|
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
3150
|
+
registerStop: (stop) => registerStepStop(fi, stop),
|
|
2608
3151
|
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
|
|
2609
3152
|
timeoutSignal: timeoutAbortController.signal,
|
|
3153
|
+
stopSignal: stopAbortController.signal,
|
|
2610
3154
|
timeoutMessage,
|
|
3155
|
+
stopMessage,
|
|
2611
3156
|
turnBudget: config.turnBudget,
|
|
2612
3157
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
2613
3158
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2614
|
-
|
|
3159
|
+
onWriterProcess,
|
|
3160
|
+
skipAcceptance: () => timedOut || stopped,
|
|
2615
3161
|
});
|
|
2616
3162
|
if (task.sessionFile) {
|
|
2617
3163
|
latestSessionFile = task.sessionFile;
|
|
@@ -2620,12 +3166,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2620
3166
|
const taskEndTime = Date.now();
|
|
2621
3167
|
const taskDuration = taskEndTime - taskStartTime;
|
|
2622
3168
|
const childInterrupted = singleResult.interrupted === true;
|
|
3169
|
+
const childStopped = singleResult.stopped === true;
|
|
2623
3170
|
|
|
2624
|
-
statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
3171
|
+
statusPayload.steps[fi].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2625
3172
|
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
2626
3173
|
statusPayload.steps[fi].durationMs = taskDuration;
|
|
2627
|
-
statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
3174
|
+
statusPayload.steps[fi].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2628
3175
|
statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
3176
|
+
statusPayload.steps[fi].stopped = stopped || childStopped ? true : undefined;
|
|
2629
3177
|
statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
|
|
2630
3178
|
statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2631
3179
|
statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
|
|
@@ -2641,20 +3189,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2641
3189
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
2642
3190
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
2643
3191
|
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2644
|
-
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
3192
|
+
statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2645
3193
|
statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
|
|
2646
3194
|
statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
|
|
2647
3195
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
2648
3196
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2649
3197
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
2650
3198
|
statusPayload.steps[fi].acceptance = singleResult.acceptance;
|
|
3199
|
+
statusPayload.steps[fi].watchdog = singleResult.watchdog;
|
|
2651
3200
|
statusPayload.lastUpdate = taskEndTime;
|
|
2652
3201
|
writeStatusPayload();
|
|
2653
3202
|
|
|
2654
3203
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2655
|
-
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
3204
|
+
type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2656
3205
|
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
2657
|
-
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
|
|
3206
|
+
exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
|
|
2658
3207
|
}));
|
|
2659
3208
|
if (singleResult.completionGuardTriggered) {
|
|
2660
3209
|
const event = buildControlEvent({
|
|
@@ -2671,7 +3220,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2671
3220
|
}
|
|
2672
3221
|
|
|
2673
3222
|
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
2674
|
-
return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
|
|
3223
|
+
return stopped || childStopped ? { ...singleResult, output: stopMessage, error: stopMessage, exitCode: 1, interrupted: false, timedOut: false, stopped: true, skipped: false } : timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
|
|
2675
3224
|
},
|
|
2676
3225
|
globalSemaphore,
|
|
2677
3226
|
);
|
|
@@ -2701,11 +3250,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2701
3250
|
agent: pr.agent,
|
|
2702
3251
|
output: pr.output,
|
|
2703
3252
|
error: pr.error,
|
|
2704
|
-
|
|
3253
|
+
protocolError: pr.protocolError,
|
|
3254
|
+
success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
|
|
2705
3255
|
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
2706
3256
|
skipped: pr.skipped,
|
|
2707
3257
|
interrupted: pr.interrupted,
|
|
2708
3258
|
timedOut: pr.timedOut,
|
|
3259
|
+
stopped: pr.stopped,
|
|
2709
3260
|
turnBudget: pr.turnBudget,
|
|
2710
3261
|
turnBudgetExceeded: pr.turnBudgetExceeded,
|
|
2711
3262
|
wrapUpRequested: pr.wrapUpRequested,
|
|
@@ -2720,12 +3271,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2720
3271
|
artifactPaths: pr.artifactPaths,
|
|
2721
3272
|
transcriptPath: pr.transcriptPath,
|
|
2722
3273
|
transcriptError: pr.transcriptError,
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
}
|
|
3274
|
+
structuredOutput: pr.structuredOutput,
|
|
3275
|
+
structuredOutputPath: pr.structuredOutputPath,
|
|
3276
|
+
structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
|
|
3277
|
+
acceptance: pr.acceptance,
|
|
3278
|
+
watchdog: pr.watchdog,
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
2729
3281
|
for (let t = 0; t < group.parallel.length; t++) {
|
|
2730
3282
|
const outputName = group.parallel[t]?.outputName;
|
|
2731
3283
|
if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
|
|
@@ -2738,13 +3290,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2738
3290
|
|
|
2739
3291
|
previousOutput = aggregateParallelOutputs(
|
|
2740
3292
|
parallelResults.map((r) => ({
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
3293
|
+
agent: r.agent,
|
|
3294
|
+
output: r.output,
|
|
3295
|
+
exitCode: r.exitCode,
|
|
3296
|
+
error: r.error,
|
|
3297
|
+
model: r.model,
|
|
3298
|
+
attemptedModels: r.attemptedModels,
|
|
3299
|
+
})),
|
|
2748
3300
|
);
|
|
2749
3301
|
previousOutput = appendParallelWorktreeSummary(previousOutput, worktreeSetup, asyncDir, stepIndex, group);
|
|
2750
3302
|
|
|
@@ -2789,12 +3341,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2789
3341
|
flushPendingStepSteers(flatIndex);
|
|
2790
3342
|
const singleResult = await runSingleStep(seqStep, {
|
|
2791
3343
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
2792
|
-
outputs,
|
|
3344
|
+
outputs: statusPayload.mode === "single" ? undefined : outputs,
|
|
2793
3345
|
sessionDir: config.sessionDir,
|
|
2794
3346
|
artifactsDir, artifactConfig, id,
|
|
2795
3347
|
flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
2796
3348
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
2797
3349
|
steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
|
|
3350
|
+
steerCapabilityPath: steerCapabilityPath(asyncDir, flatIndex),
|
|
3351
|
+
steerAckDir: steerAcksDir(asyncDir, flatIndex),
|
|
2798
3352
|
piPackageRoot: config.piPackageRoot,
|
|
2799
3353
|
piArgv1: config.piArgv1,
|
|
2800
3354
|
childIntercomTarget: config.childIntercomTargets?.[flatIndex],
|
|
@@ -2802,25 +3356,31 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2802
3356
|
nestedRoute: config.nestedRoute,
|
|
2803
3357
|
registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
|
|
2804
3358
|
registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
|
|
3359
|
+
registerStop: (stop) => registerStepStop(flatIndex, stop),
|
|
2805
3360
|
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
|
|
2806
3361
|
timeoutSignal: timeoutAbortController.signal,
|
|
3362
|
+
stopSignal: stopAbortController.signal,
|
|
2807
3363
|
timeoutMessage,
|
|
3364
|
+
stopMessage,
|
|
2808
3365
|
turnBudget: config.turnBudget,
|
|
2809
3366
|
onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
|
|
2810
3367
|
onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
|
|
2811
|
-
|
|
3368
|
+
onWriterProcess,
|
|
3369
|
+
skipAcceptance: () => timedOut || stopped,
|
|
2812
3370
|
});
|
|
2813
3371
|
if (seqStep.sessionFile) {
|
|
2814
3372
|
latestSessionFile = seqStep.sessionFile;
|
|
2815
3373
|
}
|
|
2816
3374
|
|
|
2817
3375
|
previousOutput = singleResult.output;
|
|
3376
|
+
const childStopped = singleResult.stopped === true;
|
|
2818
3377
|
results.push({
|
|
2819
3378
|
agent: singleResult.agent,
|
|
2820
|
-
output: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
|
|
2821
|
-
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
|
|
2822
|
-
|
|
2823
|
-
|
|
3379
|
+
output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
|
|
3380
|
+
error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
|
|
3381
|
+
protocolError: singleResult.protocolError,
|
|
3382
|
+
success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
|
|
3383
|
+
exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
|
|
2824
3384
|
sessionFile: singleResult.sessionFile,
|
|
2825
3385
|
intercomTarget: singleResult.intercomTarget,
|
|
2826
3386
|
model: singleResult.model,
|
|
@@ -2834,8 +3394,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2834
3394
|
structuredOutputPath: singleResult.structuredOutputPath,
|
|
2835
3395
|
structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
|
|
2836
3396
|
acceptance: singleResult.acceptance,
|
|
3397
|
+
watchdog: singleResult.watchdog,
|
|
2837
3398
|
interrupted: singleResult.interrupted,
|
|
2838
3399
|
timedOut: timedOut || singleResult.timedOut ? true : undefined,
|
|
3400
|
+
stopped: stopped || childStopped ? true : undefined,
|
|
2839
3401
|
turnBudget: singleResult.turnBudget,
|
|
2840
3402
|
turnBudgetExceeded: singleResult.turnBudgetExceeded,
|
|
2841
3403
|
wrapUpRequested: singleResult.wrapUpRequested,
|
|
@@ -2874,11 +3436,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2874
3436
|
|
|
2875
3437
|
const stepEndTime = Date.now();
|
|
2876
3438
|
const childInterrupted = singleResult.interrupted === true;
|
|
2877
|
-
statusPayload.steps[flatIndex].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
3439
|
+
statusPayload.steps[flatIndex].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2878
3440
|
statusPayload.steps[flatIndex].endedAt = stepEndTime;
|
|
2879
3441
|
statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
|
|
2880
|
-
statusPayload.steps[flatIndex].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
3442
|
+
statusPayload.steps[flatIndex].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2881
3443
|
statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
3444
|
+
statusPayload.steps[flatIndex].stopped = stopped || childStopped ? true : undefined;
|
|
2882
3445
|
statusPayload.steps[flatIndex].turnBudget = singleResult.turnBudget;
|
|
2883
3446
|
statusPayload.steps[flatIndex].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2884
3447
|
statusPayload.steps[flatIndex].wrapUpRequested = singleResult.wrapUpRequested;
|
|
@@ -2894,13 +3457,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2894
3457
|
statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
|
|
2895
3458
|
statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
|
|
2896
3459
|
statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
|
|
2897
|
-
statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
3460
|
+
statusPayload.steps[flatIndex].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2898
3461
|
statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
|
|
2899
3462
|
statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
|
|
2900
3463
|
statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
|
|
2901
3464
|
statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2902
3465
|
statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
2903
3466
|
statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
|
|
3467
|
+
statusPayload.steps[flatIndex].watchdog = singleResult.watchdog;
|
|
2904
3468
|
if (stepTokens) {
|
|
2905
3469
|
statusPayload.steps[flatIndex].tokens = stepTokens;
|
|
2906
3470
|
statusPayload.totalTokens = { ...previousCumulativeTokens };
|
|
@@ -2909,12 +3473,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2909
3473
|
writeStatusPayload();
|
|
2910
3474
|
|
|
2911
3475
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2912
|
-
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
3476
|
+
type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2913
3477
|
ts: stepEndTime,
|
|
2914
3478
|
runId: id,
|
|
2915
3479
|
stepIndex: flatIndex,
|
|
2916
3480
|
agent: seqStep.agent,
|
|
2917
|
-
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
|
|
3481
|
+
exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
|
|
2918
3482
|
durationMs: stepEndTime - stepStartTime,
|
|
2919
3483
|
tokens: stepTokens,
|
|
2920
3484
|
}));
|
|
@@ -3003,11 +3567,29 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
3003
3567
|
clearTimeout(timeoutTimer);
|
|
3004
3568
|
timeoutTimer = undefined;
|
|
3005
3569
|
}
|
|
3570
|
+
statusPayload.state = stopped ? "stopped" : timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
3571
|
+
closeSteerInbox(asyncDir, statusPayload.state);
|
|
3006
3572
|
disposeControlInbox();
|
|
3573
|
+
for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
|
|
3007
3574
|
const effectiveSessionFile = sessionFile ?? latestSessionFile;
|
|
3575
|
+
const steeringLifecycle = steeringStatus(statusPayload);
|
|
3576
|
+
for (const request of steeringLifecycle.recent) {
|
|
3577
|
+
let changed = false;
|
|
3578
|
+
for (const target of request.targets) {
|
|
3579
|
+
if (target.state !== "scheduled" && target.state !== "routed") continue;
|
|
3580
|
+
changed = true;
|
|
3581
|
+
updateSteeringLifecycleTarget(request.id, target.index, "failed", Date.now(), { reason: "child terminated before steering delivery" });
|
|
3582
|
+
markSteeringAttention(target.index);
|
|
3583
|
+
emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: "child terminated before steering delivery" }, target.index, { reason: "child terminated before steering delivery" });
|
|
3584
|
+
}
|
|
3585
|
+
if (changed) emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child terminated before delivery.`);
|
|
3586
|
+
}
|
|
3008
3587
|
const runEndedAt = Date.now();
|
|
3009
|
-
statusPayload.state = timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
3010
3588
|
statusPayload.activityState = undefined;
|
|
3589
|
+
if (stopped) {
|
|
3590
|
+
statusPayload.stopped = true;
|
|
3591
|
+
statusPayload.error = stopMessage;
|
|
3592
|
+
}
|
|
3011
3593
|
if (timedOut) {
|
|
3012
3594
|
statusPayload.timedOut = true;
|
|
3013
3595
|
statusPayload.error = timeoutMessage ?? "Subagent timed out.";
|
|
@@ -3068,9 +3650,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
3068
3650
|
id,
|
|
3069
3651
|
agent: agentName,
|
|
3070
3652
|
mode: resultMode,
|
|
3071
|
-
success: !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
|
|
3072
|
-
state: timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
3073
|
-
summary: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
|
|
3653
|
+
success: !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
|
|
3654
|
+
state: stopped ? "stopped" : timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
3655
|
+
summary: stopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
|
|
3074
3656
|
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
3075
3657
|
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
3076
3658
|
...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
|
|
@@ -3078,15 +3660,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
3078
3660
|
...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
|
|
3079
3661
|
...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
|
|
3080
3662
|
...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
|
|
3081
|
-
...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
|
|
3663
|
+
...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
|
|
3082
3664
|
results: results.map((r) => ({
|
|
3083
3665
|
agent: r.agent,
|
|
3084
3666
|
output: r.output,
|
|
3085
3667
|
error: r.error,
|
|
3668
|
+
protocolError: r.protocolError,
|
|
3086
3669
|
success: r.success,
|
|
3087
3670
|
skipped: r.skipped || undefined,
|
|
3088
3671
|
interrupted: r.interrupted || undefined,
|
|
3089
3672
|
timedOut: r.timedOut || undefined,
|
|
3673
|
+
stopped: r.stopped || undefined,
|
|
3090
3674
|
turnBudget: r.turnBudget,
|
|
3091
3675
|
turnBudgetExceeded: r.turnBudgetExceeded || undefined,
|
|
3092
3676
|
wrapUpRequested: r.wrapUpRequested || undefined,
|
|
@@ -3106,10 +3690,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
3106
3690
|
structuredOutputPath: r.structuredOutputPath,
|
|
3107
3691
|
structuredOutputSchemaPath: r.structuredOutputSchemaPath,
|
|
3108
3692
|
acceptance: r.acceptance,
|
|
3693
|
+
watchdog: r.watchdog,
|
|
3109
3694
|
})),
|
|
3110
3695
|
outputs,
|
|
3111
3696
|
workflowGraph: statusPayload.workflowGraph,
|
|
3112
|
-
exitCode: timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
3697
|
+
exitCode: stopped || timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
3113
3698
|
timestamp: runEndedAt,
|
|
3114
3699
|
durationMs: runEndedAt - overallStartTime,
|
|
3115
3700
|
totalTokens: statusPayload.totalTokens,
|
|
@@ -3132,6 +3717,89 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
3132
3717
|
}
|
|
3133
3718
|
}
|
|
3134
3719
|
|
|
3720
|
+
async function waitForStartupControl(
|
|
3721
|
+
controlPath: string,
|
|
3722
|
+
token: string,
|
|
3723
|
+
action: "ack" | "proceed",
|
|
3724
|
+
timeoutMs = 30_000,
|
|
3725
|
+
): Promise<void> {
|
|
3726
|
+
const deadline = Date.now() + timeoutMs;
|
|
3727
|
+
while (Date.now() <= deadline) {
|
|
3728
|
+
if (fs.existsSync(controlPath)) {
|
|
3729
|
+
let payload: { action?: unknown; token?: unknown };
|
|
3730
|
+
try {
|
|
3731
|
+
payload = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { action?: unknown; token?: unknown };
|
|
3732
|
+
} catch (error) {
|
|
3733
|
+
throw new Error(`Failed to read runner startup control '${controlPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
3734
|
+
}
|
|
3735
|
+
if (payload.token !== token) throw new Error("Runner startup control token does not match the acquired session lease.");
|
|
3736
|
+
if (payload.action === action) return;
|
|
3737
|
+
if (payload.action !== "ack" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
|
|
3738
|
+
}
|
|
3739
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3740
|
+
}
|
|
3741
|
+
throw new Error(`Timed out after ${timeoutMs}ms waiting for runner startup control '${action}'.`);
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
|
|
3745
|
+
let lease: ReturnType<typeof acquireSessionLease> | undefined;
|
|
3746
|
+
let startupCommitted = config.revivalLease === undefined;
|
|
3747
|
+
const startupPath = path.join(config.asyncDir, "runner-startup.json");
|
|
3748
|
+
const startupAckPath = path.join(config.asyncDir, "runner-startup-ack.json");
|
|
3749
|
+
const startupProceedPath = path.join(config.asyncDir, "runner-startup-proceed.json");
|
|
3750
|
+
const releaseOnExit = (): void => {
|
|
3751
|
+
try {
|
|
3752
|
+
lease?.release();
|
|
3753
|
+
} catch {
|
|
3754
|
+
// Exit cleanup is best effort; a dead-owner lease is reclaimed on the next revival.
|
|
3755
|
+
}
|
|
3756
|
+
};
|
|
3757
|
+
process.once("exit", releaseOnExit);
|
|
3758
|
+
try {
|
|
3759
|
+
if (config.revivalLease) {
|
|
3760
|
+
lease = acquireSessionLease(config.revivalLease);
|
|
3761
|
+
writeAtomicJson(startupPath, { state: "ready", token: lease.owner.token, pid: process.pid, owner: lease.owner });
|
|
3762
|
+
await waitForStartupControl(startupAckPath, lease.owner.token, "ack");
|
|
3763
|
+
writeAtomicJson(startupPath, { state: "acknowledged", token: lease.owner.token, pid: process.pid });
|
|
3764
|
+
await waitForStartupControl(startupProceedPath, lease.owner.token, "proceed");
|
|
3765
|
+
startupCommitted = true;
|
|
3766
|
+
for (const controlPath of [startupAckPath, startupProceedPath]) {
|
|
3767
|
+
try {
|
|
3768
|
+
fs.rmSync(controlPath, { force: true });
|
|
3769
|
+
} catch {
|
|
3770
|
+
// Startup control cleanup is best effort after the parent commits the run.
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
await runSubagent(config, lease ? (writer) => lease!.updateWriter(writer) : undefined);
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
if (config.revivalLease && !startupCommitted) {
|
|
3777
|
+
try {
|
|
3778
|
+
writeAtomicJson(startupPath, { state: "error", pid: process.pid, error: error instanceof Error ? error.message : String(error) });
|
|
3779
|
+
} catch {
|
|
3780
|
+
// The parent will time out and terminate this runner if the handshake cannot be written.
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
throw error;
|
|
3784
|
+
} finally {
|
|
3785
|
+
process.off("exit", releaseOnExit);
|
|
3786
|
+
if (lease) {
|
|
3787
|
+
try {
|
|
3788
|
+
lease.release();
|
|
3789
|
+
} catch (error) {
|
|
3790
|
+
console.error("Failed to release session revival lease:", error);
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
function startConfiguredSubagent(config: SubagentRunConfig): void {
|
|
3797
|
+
runConfiguredSubagent(config).catch((runErr) => {
|
|
3798
|
+
console.error("Subagent runner error:", runErr);
|
|
3799
|
+
process.exit(1);
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3135
3803
|
const configArg = process.argv[2];
|
|
3136
3804
|
if (configArg) {
|
|
3137
3805
|
try {
|
|
@@ -3142,10 +3810,7 @@ if (configArg) {
|
|
|
3142
3810
|
} catch {
|
|
3143
3811
|
// Temp config cleanup is best effort.
|
|
3144
3812
|
}
|
|
3145
|
-
|
|
3146
|
-
console.error("Subagent runner error:", runErr);
|
|
3147
|
-
process.exit(1);
|
|
3148
|
-
});
|
|
3813
|
+
startConfiguredSubagent(config);
|
|
3149
3814
|
} catch (err) {
|
|
3150
3815
|
console.error("Subagent runner error:", err);
|
|
3151
3816
|
process.exit(1);
|
|
@@ -3159,10 +3824,7 @@ if (configArg) {
|
|
|
3159
3824
|
process.stdin.on("end", () => {
|
|
3160
3825
|
try {
|
|
3161
3826
|
const config = JSON.parse(input) as SubagentRunConfig;
|
|
3162
|
-
|
|
3163
|
-
console.error("Subagent runner error:", runErr);
|
|
3164
|
-
process.exit(1);
|
|
3165
|
-
});
|
|
3827
|
+
startConfiguredSubagent(config);
|
|
3166
3828
|
} catch (err) {
|
|
3167
3829
|
console.error("Subagent runner error:", err);
|
|
3168
3830
|
process.exit(1);
|