pi-subagents 0.65.1 → 0.67.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 +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import type { Message } from "@earendil-works/pi-ai";
|
|
8
|
+
import { arbitrateCompletionGuardRescue, createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
|
|
8
9
|
|
|
9
10
|
// Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
|
|
10
11
|
function ensureProxyAwareHttpDispatcher(): void {
|
|
@@ -20,7 +21,8 @@ function ensureProxyAwareHttpDispatcher(): void {
|
|
|
20
21
|
console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
const isRunnerEntrypoint = Boolean(process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href);
|
|
25
|
+
if (isRunnerEntrypoint) ensureProxyAwareHttpDispatcher();
|
|
24
26
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
25
27
|
import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
|
|
26
28
|
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
@@ -45,10 +47,10 @@ import {
|
|
|
45
47
|
type AsyncStatus,
|
|
46
48
|
type ChainOutputMap,
|
|
47
49
|
type CostSummary,
|
|
48
|
-
type
|
|
49
|
-
type
|
|
50
|
+
type LaunchResolvedChildExtensions,
|
|
51
|
+
type RuntimeAcknowledgedChildExtensions,
|
|
50
52
|
type ModelAttempt,
|
|
51
|
-
type
|
|
53
|
+
type PiWriterProcessInstanceExit,
|
|
52
54
|
type NestedRouteInfo,
|
|
53
55
|
type NestedRunSummary,
|
|
54
56
|
type ResolvedControlConfig,
|
|
@@ -90,10 +92,14 @@ import {
|
|
|
90
92
|
DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
|
|
91
93
|
Semaphore,
|
|
92
94
|
} from "../shared/parallel-utils.ts";
|
|
93
|
-
import { applyThinkingSuffix,
|
|
94
|
-
import {
|
|
95
|
-
import
|
|
96
|
-
import {
|
|
95
|
+
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
|
|
96
|
+
import type { InheritedChildRuntime } from "../shared/child-launch.ts";
|
|
97
|
+
import { buildRunnerChildLaunch } from "./runner-child-launch.ts";
|
|
98
|
+
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
99
|
+
import type { ChildSessionFactory, DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
|
|
100
|
+
import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type SteerDelivery, type StepSteerHandler } from "./run-child-session.ts";
|
|
101
|
+
import { planReadonlyModelContinuation, READONLY_CONTINUATION_PROMPT, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
|
|
102
|
+
import { getReadonlySessionEvidence } from "../shared/readonly-session-evidence.ts";
|
|
97
103
|
import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
|
|
98
104
|
import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
|
|
99
105
|
import { deriveChildSessionName } from "../../shared/child-session-name.ts";
|
|
@@ -107,8 +113,8 @@ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelSt
|
|
|
107
113
|
import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
|
|
108
114
|
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
109
115
|
import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailureAttempt, recordRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
110
|
-
import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
|
|
111
|
-
import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
|
|
116
|
+
import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
|
|
117
|
+
import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.ts";
|
|
112
118
|
import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
|
|
113
119
|
import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
|
|
114
120
|
import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
|
|
@@ -129,6 +135,9 @@ import type { TokenUsage } from "../../shared/types.ts";
|
|
|
129
135
|
import {
|
|
130
136
|
cleanupWorktrees,
|
|
131
137
|
createWorktrees,
|
|
138
|
+
withWorktreeTransaction,
|
|
139
|
+
WorktreeSetupError,
|
|
140
|
+
type WorktreeSetupProgress,
|
|
132
141
|
diffWorktrees,
|
|
133
142
|
findWorktreeTaskCwdConflict,
|
|
134
143
|
formatWorktreeDiffSummary,
|
|
@@ -136,21 +145,20 @@ import {
|
|
|
136
145
|
WORKTREE_AGENT_CWD_PLACEHOLDER,
|
|
137
146
|
type WorktreeSetup,
|
|
138
147
|
} from "../shared/worktree.ts";
|
|
139
|
-
import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
148
|
+
import { findModelInfo, resolveEffectiveThinking, splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
140
149
|
import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
|
|
141
|
-
import {
|
|
150
|
+
import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
|
|
142
151
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
143
152
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
144
153
|
import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
145
|
-
import { attachContractProjections,
|
|
154
|
+
import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
|
|
146
155
|
import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
147
|
-
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
148
156
|
import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
|
|
149
157
|
import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
|
|
150
158
|
import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
|
|
151
159
|
import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
|
|
152
160
|
import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
|
|
153
|
-
import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup,
|
|
161
|
+
import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writeWorktreeSetupHandoff } from "../shared/parallel-handoff.ts";
|
|
154
162
|
import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
|
|
155
163
|
import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
|
|
156
164
|
import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
|
|
@@ -176,7 +184,7 @@ const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task
|
|
|
176
184
|
// child host.
|
|
177
185
|
process.env[SUBAGENT_CHILD_ENV] = "1";
|
|
178
186
|
|
|
179
|
-
interface SubagentRunConfig {
|
|
187
|
+
export interface SubagentRunConfig {
|
|
180
188
|
id: string;
|
|
181
189
|
steps: RunnerStep[];
|
|
182
190
|
resultPath: string;
|
|
@@ -224,9 +232,11 @@ interface SubagentRunConfig {
|
|
|
224
232
|
globalConcurrencyLimit?: number;
|
|
225
233
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
226
234
|
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
235
|
+
/** Builtin tool names the host runtime provides; used to intersect agent-declared tools. */
|
|
236
|
+
hostAvailableBuiltins?: readonly string[];
|
|
227
237
|
launchContractDigest?: string;
|
|
228
|
-
launchResolvedExtensions?:
|
|
229
|
-
runtimeAcknowledgedExtensions?:
|
|
238
|
+
launchResolvedExtensions?: LaunchResolvedChildExtensions;
|
|
239
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
230
240
|
runnerProcessInstanceId?: string;
|
|
231
241
|
launchBarrierToken?: string;
|
|
232
242
|
parentWorkflowRunId?: string;
|
|
@@ -241,8 +251,8 @@ interface StepResult {
|
|
|
241
251
|
context?: "fresh" | "fork";
|
|
242
252
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
243
253
|
capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
|
|
244
|
-
launchResolvedExtensions?:
|
|
245
|
-
runtimeAcknowledgedExtensions?:
|
|
254
|
+
launchResolvedExtensions?: LaunchResolvedChildExtensions;
|
|
255
|
+
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
246
256
|
output: string;
|
|
247
257
|
outputState?: SubagentOutputState;
|
|
248
258
|
error?: string;
|
|
@@ -674,6 +684,8 @@ interface SingleStepContext {
|
|
|
674
684
|
registerStop?: (stop: (() => void) | undefined) => void;
|
|
675
685
|
/** Receives the live child's steer handler while its session runs. */
|
|
676
686
|
registerSteer?: (steer: StepSteerHandler | undefined) => void;
|
|
687
|
+
/** Reports a live child's later steer consumption or unconsumed settlement. */
|
|
688
|
+
onSteerOutcome?: (request: SteerRequest, delivery: SteerDelivery) => void;
|
|
677
689
|
timeoutSignal?: AbortSignal;
|
|
678
690
|
stopSignal?: AbortSignal;
|
|
679
691
|
timeoutMessage?: string;
|
|
@@ -687,19 +699,23 @@ interface SingleStepContext {
|
|
|
687
699
|
nestedRoute?: NestedRouteInfo;
|
|
688
700
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
689
701
|
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
702
|
+
hostAvailableBuiltins?: readonly string[];
|
|
690
703
|
onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
|
|
691
704
|
onChildEvent?: (event: ChildEvent) => void;
|
|
692
705
|
onExternalProcess?: (process: ExternalProcessStatus) => void;
|
|
693
706
|
onExternalJob?: (status: ExternalJobStatus) => void;
|
|
694
707
|
skipAcceptance?: () => boolean;
|
|
695
|
-
|
|
708
|
+
/** Authoritative owner decision after event delivery; undefined includes incomplete run-wide usage. */
|
|
709
|
+
usageBudgetExhausted?: () => boolean | undefined;
|
|
710
|
+
/** Existing run-owned budget configuration; cost allowance is not settled by the live token ledger. */
|
|
711
|
+
usageBudget?: UsageBudgetConfig;
|
|
696
712
|
/** False when sibling work in the same Git worktree could have caused the tracked diff. */
|
|
697
713
|
trackedMutationEvidenceForCompletionGuard?: boolean;
|
|
698
714
|
orcaProgressTab?: OrcaProgressTab;
|
|
699
715
|
}
|
|
700
716
|
|
|
701
717
|
/** Run a single pi agent step, returning output and metadata */
|
|
702
|
-
async function runSingleStepInner(
|
|
718
|
+
export async function runSingleStepInner(
|
|
703
719
|
step: SubagentStep,
|
|
704
720
|
ctx: SingleStepContext,
|
|
705
721
|
): Promise<StepResult & { completionGuardTriggered?: boolean }> {
|
|
@@ -800,6 +816,7 @@ async function runSingleStepInner(
|
|
|
800
816
|
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
801
817
|
inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
|
|
802
818
|
permissionRules: step.permissionRules,
|
|
819
|
+
hostAvailableBuiltins: ctx.hostAvailableBuiltins,
|
|
803
820
|
}));
|
|
804
821
|
const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
|
|
805
822
|
const contractError = validateImplementationToolContract({
|
|
@@ -827,8 +844,8 @@ async function runSingleStepInner(
|
|
|
827
844
|
// Derive from the pre-acceptance task so internal acceptance/recovery
|
|
828
845
|
// instructions never leak into the display name.
|
|
829
846
|
const childSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task, label: step.label });
|
|
830
|
-
if (step.effectiveAcceptance) {
|
|
831
|
-
const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional:
|
|
847
|
+
if (step.effectiveAcceptance && (step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
|
|
848
|
+
const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContract(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
|
|
832
849
|
if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
|
|
833
850
|
}
|
|
834
851
|
const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
|
|
@@ -1035,11 +1052,31 @@ async function runSingleStepInner(
|
|
|
1035
1052
|
let modelIndex = 0;
|
|
1036
1053
|
let contextOverflow = false;
|
|
1037
1054
|
let launchWarningsEmitted = false;
|
|
1038
|
-
let
|
|
1055
|
+
let recoveryState: LogicalRecoveryState = "unused";
|
|
1056
|
+
let readonlyContinuation: RunChildSessionInput["readonlyContinuation"];
|
|
1057
|
+
const continuationBudget = () => {
|
|
1058
|
+
if (step.toolBudget) return "tool-budget-configured" as const;
|
|
1059
|
+
// Refresh the authoritative run ledger, already fed synchronously by onChildEvent.
|
|
1060
|
+
const exhausted = ctx.usageBudgetExhausted?.();
|
|
1061
|
+
if (exhausted === true) return "exhausted" as const;
|
|
1062
|
+
if (!ctx.usageBudget) return "unconfigured" as const;
|
|
1063
|
+
if (ctx.usageBudget.costUsd || !ctx.onChildEvent || exhausted !== false) return "unknown" as const;
|
|
1064
|
+
return "available" as const;
|
|
1065
|
+
};
|
|
1066
|
+
const lifecycleAllowsContinuation = () => !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted
|
|
1067
|
+
&& !ctx.skipAcceptance?.() && (ctx.deadlineAt === undefined || Date.now() < ctx.deadlineAt);
|
|
1068
|
+
const canContinue = () => lifecycleAllowsContinuation() && ["available", "unconfigured"].includes(continuationBudget());
|
|
1069
|
+
const failContinuationLaunch = (candidate: string | undefined, error: unknown) => {
|
|
1070
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1071
|
+
modelAttempts.push({ model: candidate ?? "default", success: false, exitCode: 1, error: message });
|
|
1072
|
+
if (candidate) attemptedModels.push(candidate);
|
|
1073
|
+
if (finalResult) finalResult = { ...finalResult, exitCode: 1, error: message };
|
|
1074
|
+
};
|
|
1039
1075
|
let nextAttemptTask = task;
|
|
1040
1076
|
modelAttemptsLoop: while (modelIndex < candidates.length) {
|
|
1041
1077
|
if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1042
|
-
|
|
1078
|
+
if (readonlyContinuation && !canContinue()) break;
|
|
1079
|
+
const recoveringAbort = recoveryState === "abort-recovery";
|
|
1043
1080
|
const attemptTask = nextAttemptTask;
|
|
1044
1081
|
const candidate = candidates[modelIndex];
|
|
1045
1082
|
const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
|
|
@@ -1047,6 +1084,10 @@ async function runSingleStepInner(
|
|
|
1047
1084
|
assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
|
|
1048
1085
|
} catch (error) {
|
|
1049
1086
|
const message = error instanceof Error ? error.message : String(error);
|
|
1087
|
+
if (readonlyContinuation) {
|
|
1088
|
+
failContinuationLaunch(candidate, error);
|
|
1089
|
+
break modelAttemptsLoop;
|
|
1090
|
+
}
|
|
1050
1091
|
return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
|
|
1051
1092
|
}
|
|
1052
1093
|
ctx.onAttemptStart?.(omitUndefinedProperties({
|
|
@@ -1072,56 +1113,22 @@ async function runSingleStepInner(
|
|
|
1072
1113
|
})
|
|
1073
1114
|
: undefined;
|
|
1074
1115
|
let watchdogSink: ((event: ChildWatchdogStatusEvent) => void) | undefined;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
fast: step.fast,
|
|
1092
|
-
modelCandidates: step.modelCandidates,
|
|
1093
|
-
systemPrompt: step.systemPrompt ?? "",
|
|
1094
|
-
systemPromptMode: step.systemPromptMode,
|
|
1095
|
-
mcpDirectTools: step.mcpDirectTools,
|
|
1096
|
-
extensionBindings,
|
|
1097
|
-
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
1098
|
-
cwd: step.cwd ?? ctx.cwd,
|
|
1099
|
-
intercomSessionName: ctx.childIntercomTarget,
|
|
1100
|
-
sessionName: childSessionName,
|
|
1101
|
-
orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
|
|
1102
|
-
runId: ctx.id,
|
|
1103
|
-
childAgentName: step.agent,
|
|
1104
|
-
childIndex: ctx.flatIndex,
|
|
1105
|
-
nestedRoute: ctx.nestedRoute,
|
|
1106
|
-
runFanoutBudget: ctx.runFanoutBudget ? {
|
|
1107
|
-
...ctx.runFanoutBudget,
|
|
1108
|
-
...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
|
|
1109
|
-
} : undefined,
|
|
1110
|
-
structuredOutput: effectiveStructuredOutput,
|
|
1111
|
-
toolBudget: step.toolBudget,
|
|
1112
|
-
permissionRules: step.permissionRules,
|
|
1113
|
-
permissionAuditPath: step.permissionRules && ctx.artifactsDir
|
|
1114
|
-
? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
|
|
1115
|
-
: undefined,
|
|
1116
|
-
childWatchdog,
|
|
1117
|
-
watchdogStatus: (event) => watchdogSink?.(event),
|
|
1118
|
-
waitToolEnabled: step.waitToolEnabled,
|
|
1119
|
-
waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
|
|
1120
|
-
thinkingCeiling: step.thinkingCeiling,
|
|
1121
|
-
maxSubagentDepth: step.maxSubagentDepth,
|
|
1122
|
-
inherited: ctx.inheritedChildRuntime,
|
|
1123
|
-
host: "runner",
|
|
1124
|
-
}));
|
|
1116
|
+
let launch: ReturnType<typeof buildRunnerChildLaunch>;
|
|
1117
|
+
try {
|
|
1118
|
+
launch = buildRunnerChildLaunch(step, ctx, {
|
|
1119
|
+
sessionEnabled,
|
|
1120
|
+
sessionDir,
|
|
1121
|
+
model: candidate,
|
|
1122
|
+
sessionName: childSessionName,
|
|
1123
|
+
structuredOutput: effectiveStructuredOutput,
|
|
1124
|
+
childWatchdog,
|
|
1125
|
+
watchdogStatus: (event) => watchdogSink?.(event),
|
|
1126
|
+
});
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
if (!readonlyContinuation) throw error;
|
|
1129
|
+
failContinuationLaunch(candidate, error);
|
|
1130
|
+
break modelAttemptsLoop;
|
|
1131
|
+
}
|
|
1125
1132
|
if (effectiveStructuredOutput && launch.config.structuredOutput) {
|
|
1126
1133
|
// The runner reads the value back from the runtime's files after the run.
|
|
1127
1134
|
launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
|
|
@@ -1148,31 +1155,27 @@ async function runSingleStepInner(
|
|
|
1148
1155
|
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
1149
1156
|
inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
|
|
1150
1157
|
permissionRules: step.permissionRules,
|
|
1158
|
+
hostAvailableBuiltins: ctx.hostAvailableBuiltins,
|
|
1151
1159
|
}));
|
|
1152
1160
|
launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
|
|
1153
|
-
actualLaunchContractDigest =
|
|
1161
|
+
actualLaunchContractDigest = resolveLaunchBinding({
|
|
1154
1162
|
definitionDigest: step.definitionDigest,
|
|
1155
|
-
task: step.launchBindingTask ?? task,
|
|
1156
|
-
...(candidate ? { model: candidate } : {}),
|
|
1157
|
-
modelCandidates: candidates as string[],
|
|
1158
|
-
...(step.fast !== undefined ? { fast: step.fast } : {}),
|
|
1159
|
-
...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
|
|
1160
|
-
...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
|
|
1161
|
-
systemPrompt: step.systemPrompt ?? "",
|
|
1162
1163
|
systemPromptMode: step.systemPromptMode,
|
|
1163
1164
|
inheritProjectContext: step.inheritProjectContext,
|
|
1164
1165
|
inheritGlobalContext: step.inheritGlobalContext,
|
|
1165
1166
|
inheritSkills: step.inheritSkills,
|
|
1167
|
+
task: step.launchBindingTask ?? task,
|
|
1168
|
+
modelCandidates: candidates as string[],
|
|
1169
|
+
fast: step.fast,
|
|
1170
|
+
thinking: resolveEffectiveThinking(candidate, step.thinking),
|
|
1171
|
+
systemPrompt: step.systemPrompt ?? "",
|
|
1166
1172
|
skills: step.skills,
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
...(step.structuredOutputSchema ? { structuredOutputSchema: step.structuredOutputSchema } : {}),
|
|
1174
|
-
...(extensionBindings ? { extensionBindings } : {}),
|
|
1175
|
-
}));
|
|
1173
|
+
toolPlan,
|
|
1174
|
+
outputPath: step.outputPath,
|
|
1175
|
+
outputMode: step.outputMode,
|
|
1176
|
+
structuredOutputSchema: step.structuredOutputSchema,
|
|
1177
|
+
extensionBindings,
|
|
1178
|
+
}).launchContractDigest;
|
|
1176
1179
|
}
|
|
1177
1180
|
capabilityAudit = attemptCapabilityAudit;
|
|
1178
1181
|
// Each attempt rewrites the step output log; synchronous appends keep a
|
|
@@ -1181,6 +1184,9 @@ async function runSingleStepInner(
|
|
|
1181
1184
|
const run = await runChildSession(omitUndefinedProperties({
|
|
1182
1185
|
factory: ctx.childSessions,
|
|
1183
1186
|
launch,
|
|
1187
|
+
collectReadonlyEvidence: true,
|
|
1188
|
+
readonlyContinuation,
|
|
1189
|
+
canContinue,
|
|
1184
1190
|
prompt: `Task: ${attemptTask}`,
|
|
1185
1191
|
childWatchdog,
|
|
1186
1192
|
childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
@@ -1197,6 +1203,7 @@ async function runSingleStepInner(
|
|
|
1197
1203
|
registerTimeout: ctx.registerTimeout,
|
|
1198
1204
|
registerStop: ctx.registerStop,
|
|
1199
1205
|
registerSteer: ctx.registerSteer,
|
|
1206
|
+
onSteerOutcome: ctx.onSteerOutcome,
|
|
1200
1207
|
registerWatchdogStatus: (sink) => { watchdogSink = sink; },
|
|
1201
1208
|
timeoutMessage: ctx.timeoutMessage,
|
|
1202
1209
|
stopMessage: ctx.stopMessage,
|
|
@@ -1209,6 +1216,9 @@ async function runSingleStepInner(
|
|
|
1209
1216
|
modelResponseAliases: step.modelResponseAliases,
|
|
1210
1217
|
mutationTools: step.mutationTools,
|
|
1211
1218
|
}));
|
|
1219
|
+
// A parked run still owes completion evidence when it actually finishes.
|
|
1220
|
+
// Stopped/timedOut runs already have terminal failures; checking completion evidence there is meaningless.
|
|
1221
|
+
const completionDiagnosticsEligible = !run.interrupted && !run.stopped && !run.timedOut;
|
|
1212
1222
|
const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
|
|
1213
1223
|
const toolAvailabilityError = toolDiagnostic ? formatChildToolDiagnostic(toolDiagnostic) : undefined;
|
|
1214
1224
|
const runtimeAcknowledgedExtensions = launch.capture.runtimeAcknowledgedExtensions();
|
|
@@ -1224,7 +1234,7 @@ async function runSingleStepInner(
|
|
|
1224
1234
|
let structuredOutput: unknown;
|
|
1225
1235
|
let structuredError: string | undefined;
|
|
1226
1236
|
let validatedStructuredOutput = false;
|
|
1227
|
-
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !midToolExitError) {
|
|
1237
|
+
if (completionDiagnosticsEligible && effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !midToolExitError) {
|
|
1228
1238
|
if (!run.structuredOutputToolInvoked) {
|
|
1229
1239
|
structuredError = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
|
|
1230
1240
|
} else {
|
|
@@ -1246,13 +1256,13 @@ async function runSingleStepInner(
|
|
|
1246
1256
|
const errorMessages = validatedStructuredOutput
|
|
1247
1257
|
? run.messages.slice(run.structuredOutputMessageStartIndex ?? run.messages.length)
|
|
1248
1258
|
: run.messages;
|
|
1249
|
-
const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
|
|
1259
|
+
const hiddenError = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
|
|
1250
1260
|
? detectSubagentError(errorMessages)
|
|
1251
1261
|
: null;
|
|
1252
1262
|
const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
|
|
1253
1263
|
&& hasEmptyTerminalAssistantResponse(run.messages)
|
|
1254
1264
|
&& (run.toolCount > 0 || Boolean(run.finalOutput.trim()));
|
|
1255
|
-
const emptyOutputError = run.exitCode === 0
|
|
1265
|
+
const emptyOutputError = completionDiagnosticsEligible && run.exitCode === 0
|
|
1256
1266
|
&& !run.error
|
|
1257
1267
|
&& !toolAvailabilityError
|
|
1258
1268
|
&& !structuredError
|
|
@@ -1261,13 +1271,13 @@ async function runSingleStepInner(
|
|
|
1261
1271
|
&& (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
|
|
1262
1272
|
? formatEmptyTerminalAssistantResponseError(run.messages)
|
|
1263
1273
|
: undefined;
|
|
1264
|
-
const completionGuardEnabled =
|
|
1274
|
+
const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
|
|
1265
1275
|
const completionToolPlan = resolvedTaskToolPlan;
|
|
1266
1276
|
const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
|
|
1267
1277
|
const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
|
|
1268
1278
|
finalMutationEvidence = mutationEvidence;
|
|
1269
1279
|
const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
|
|
1270
|
-
const completionGuard = run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
|
|
1280
|
+
const completionGuard = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
|
|
1271
1281
|
? evaluateCompletionMutationGuard(omitUndefinedProperties({
|
|
1272
1282
|
agent: step.agent,
|
|
1273
1283
|
task: taskForCompletionGuard,
|
|
@@ -1280,14 +1290,27 @@ async function runSingleStepInner(
|
|
|
1280
1290
|
}))
|
|
1281
1291
|
: undefined;
|
|
1282
1292
|
const mutationAttemptObserved = run.observedMutationAttempt === true || completionMutationEvidence?.attemptedMutation === true;
|
|
1293
|
+
let arbitration = { triggered: completionGuard?.triggered === true && !mutationAttemptObserved, rescued: false };
|
|
1294
|
+
if (arbitration.triggered) {
|
|
1295
|
+
const modelContext = launch.capture.completionIntentContext?.();
|
|
1296
|
+
arbitration = await arbitrateCompletionGuardRescue({
|
|
1297
|
+
guardTriggered: true,
|
|
1298
|
+
task: taskForCompletionGuard,
|
|
1299
|
+
// Construct lazily too: the shared gate refuses overlength tasks before
|
|
1300
|
+
// registry/auth/model work. The child has already shut down normally.
|
|
1301
|
+
arbiter: modelContext ? async (task) => createTaskMutationArbiter(modelContext)?.(task) ?? "unavailable" : undefined,
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1283
1304
|
const completionEvidence = planCompletionEvidence({
|
|
1284
1305
|
guard: completionGuard,
|
|
1306
|
+
guardTriggered: arbitration.triggered,
|
|
1307
|
+
arbiterRescued: arbitration.rescued,
|
|
1285
1308
|
completionGuardEnabled,
|
|
1286
1309
|
mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
|
|
1287
1310
|
implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
|
|
1288
1311
|
mutationAttemptObserved,
|
|
1289
1312
|
mutationEvidence: completionMutationEvidence,
|
|
1290
|
-
|
|
1313
|
+
agentContractEnabled: isAgentContract(step.agentContract),
|
|
1291
1314
|
});
|
|
1292
1315
|
const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
|
|
1293
1316
|
const requiredOutput = step.outputMode === "file-only" && step.outputPath
|
|
@@ -1296,7 +1319,7 @@ async function runSingleStepInner(
|
|
|
1296
1319
|
? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
|
|
1297
1320
|
: undefined;
|
|
1298
1321
|
finalRequiredOutputMissing = requiredOutput?.missing;
|
|
1299
|
-
const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
|
|
1322
|
+
const missingRequiredOutputError = completionDiagnosticsEligible ? formatRequiredOutputError(requiredOutput) : undefined;
|
|
1300
1323
|
const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
|
|
1301
1324
|
const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
|
|
1302
1325
|
? 1
|
|
@@ -1347,7 +1370,7 @@ async function runSingleStepInner(
|
|
|
1347
1370
|
messages: run.messages,
|
|
1348
1371
|
error,
|
|
1349
1372
|
sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
|
|
1350
|
-
alreadyResumed:
|
|
1373
|
+
alreadyResumed: recoveryState !== "unused",
|
|
1351
1374
|
stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
|
|
1352
1375
|
interrupted: run.interrupted,
|
|
1353
1376
|
timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
|
|
@@ -1367,15 +1390,59 @@ async function runSingleStepInner(
|
|
|
1367
1390
|
if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
|
|
1368
1391
|
if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
|
|
1369
1392
|
if (attempt.success) break modelAttemptsLoop;
|
|
1393
|
+
// The shared token is consumed before sibling creation; no third dispatch of any kind.
|
|
1394
|
+
if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
|
|
1370
1395
|
if (recoveringAbort) break modelAttemptsLoop;
|
|
1371
1396
|
if (abortRecovery?.action === "resume") {
|
|
1372
|
-
|
|
1397
|
+
recoveryState = "abort-recovery";
|
|
1373
1398
|
nextAttemptTask = abortRecovery.prompt;
|
|
1374
1399
|
attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
|
|
1375
1400
|
continue;
|
|
1376
1401
|
}
|
|
1377
1402
|
if (completionEvidence.guardTriggered) break modelAttemptsLoop;
|
|
1378
1403
|
|
|
1404
|
+
const source = getSettledReadonlyChild(run);
|
|
1405
|
+
if (source) {
|
|
1406
|
+
const captured = launch.capture.completionIntentContext?.();
|
|
1407
|
+
const sourceModel = captured?.model;
|
|
1408
|
+
const retained = getReadonlySessionEvidence(source)!;
|
|
1409
|
+
// Modality/capacity assessment, not another history/provenance validator.
|
|
1410
|
+
// Images and unknown content have no certified token bound in this host slice.
|
|
1411
|
+
const textOnly = (JSON.parse(retained.contextJson) as Message[]).every((message) => typeof message.content === "string"
|
|
1412
|
+
|| (Array.isArray(message.content) && message.content.every((block) => ["text", "thinking", "toolCall"].includes(block.type))));
|
|
1413
|
+
const retainedBytes = Buffer.byteLength(retained.contextJson, "utf8");
|
|
1414
|
+
const resolvedCandidates = candidates.map((reference, index) => {
|
|
1415
|
+
// Only exact registry identities qualify; aliases/default guesses remain ineligible.
|
|
1416
|
+
const base = reference ? splitKnownThinkingSuffix(reference).baseModel : "";
|
|
1417
|
+
const slash = base.indexOf("/");
|
|
1418
|
+
const model = slash > 0 ? captured?.modelRegistry.find(base.slice(0, slash), base.slice(slash + 1)) : undefined;
|
|
1419
|
+
// Reserve the source window for runtime/system/tool overhead, plus a conservative
|
|
1420
|
+
// byte bound for the complete retained text and new prompt; never assume a fit
|
|
1421
|
+
// merely because a 429 request was sent. Equal/smaller windows remain denied.
|
|
1422
|
+
const compatible = textOnly && model && sourceModel && model.input?.includes("text")
|
|
1423
|
+
&& Number.isFinite(sourceModel.contextWindow) && sourceModel.contextWindow > 0
|
|
1424
|
+
&& model.contextWindow >= sourceModel.contextWindow + retainedBytes + Buffer.byteLength(READONLY_CONTINUATION_PROMPT, "utf8")
|
|
1425
|
+
&& model.maxTokens > 0 && model.maxTokens <= sourceModel.maxTokens;
|
|
1426
|
+
return { resolved: model ? { provider: model.provider, model: model.id, api: model.api } : undefined,
|
|
1427
|
+
tried: index <= modelIndex, compatibility: compatible ? "compatible" as const : "unknown" as const };
|
|
1428
|
+
});
|
|
1429
|
+
const plan = planReadonlyModelContinuation({ source, recoveryState, candidates: resolvedCandidates, currentIndex: modelIndex,
|
|
1430
|
+
lifecycleAllowsContinuation: lifecycleAllowsContinuation() && !run.interrupted && !run.stopped && !run.timedOut,
|
|
1431
|
+
effectsAllowContinuation: !run.currentTool && !run.observedMutationAttempt && !mutationEvidence.attemptedMutation
|
|
1432
|
+
&& !structuredError && !effectiveStructuredOutput && !missingRequiredOutputError && !toolAvailabilityError
|
|
1433
|
+
&& !completionEvidence.guardTriggered && !midToolExitError && !hiddenError?.hasError,
|
|
1434
|
+
budget: continuationBudget(), knownContextOverflow: isContextOverflow(error) });
|
|
1435
|
+
if (plan.kind === "continue" && canContinue()) {
|
|
1436
|
+
recoveryState = plan.recoveryState;
|
|
1437
|
+
const selected = resolvedCandidates[plan.candidateIndex]!.resolved!;
|
|
1438
|
+
readonlyContinuation = { source, expected: plan.expected, modelId: `${selected.provider}/${selected.model}` };
|
|
1439
|
+
nextAttemptTask = plan.prompt;
|
|
1440
|
+
modelIndex = plan.candidateIndex;
|
|
1441
|
+
attemptNotes.push(`[readonly-continuation] ${attempt.model} returned HTTP 429 after read-only progress; continuing the retained session once with ${candidates[modelIndex]}.`);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1379
1446
|
const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
|
|
1380
1447
|
if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
|
|
1381
1448
|
if (isContextOverflow(error)) {
|
|
@@ -1457,7 +1524,7 @@ async function runSingleStepInner(
|
|
|
1457
1524
|
cwd: step.cwd ?? ctx.cwd,
|
|
1458
1525
|
signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
|
|
1459
1526
|
abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
|
|
1460
|
-
reportOptional:
|
|
1527
|
+
reportOptional: isAgentContract(step.agentContract),
|
|
1461
1528
|
artifactsDir: ctx.artifactsDir,
|
|
1462
1529
|
runId: ctx.id,
|
|
1463
1530
|
watchdog: finalResult?.watchdog,
|
|
@@ -1473,7 +1540,7 @@ async function runSingleStepInner(
|
|
|
1473
1540
|
: acceptance
|
|
1474
1541
|
: undefined;
|
|
1475
1542
|
const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
|
|
1476
|
-
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !
|
|
1543
|
+
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContract(step.agentContract);
|
|
1477
1544
|
const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
1478
1545
|
const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
|
|
1479
1546
|
const baseFinalError = stoppedAfterAcceptance
|
|
@@ -1515,7 +1582,7 @@ async function runSingleStepInner(
|
|
|
1515
1582
|
...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
|
|
1516
1583
|
launchContractDigest: actualLaunchContractDigest,
|
|
1517
1584
|
launchResolvedExtensions,
|
|
1518
|
-
...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?:
|
|
1585
|
+
...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
|
|
1519
1586
|
...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
|
|
1520
1587
|
transcriptError: transcriptWriter?.getError(),
|
|
1521
1588
|
skills: step.skills,
|
|
@@ -1544,6 +1611,7 @@ async function runSingleStepInner(
|
|
|
1544
1611
|
totalCost: costSummaryFromAttempts(modelAttempts),
|
|
1545
1612
|
usage,
|
|
1546
1613
|
artifactPaths,
|
|
1614
|
+
savedOutputPath: finalizedOutput.savedPath,
|
|
1547
1615
|
outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
|
|
1548
1616
|
metadataSaveError: artifactErrors.metadataSaveError,
|
|
1549
1617
|
transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
|
|
@@ -1563,9 +1631,9 @@ async function runSingleStepInner(
|
|
|
1563
1631
|
watchdog: finalResult?.watchdog,
|
|
1564
1632
|
...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
|
|
1565
1633
|
launchResolvedExtensions,
|
|
1566
|
-
...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?:
|
|
1634
|
+
...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
|
|
1567
1635
|
});
|
|
1568
|
-
return
|
|
1636
|
+
return isAgentContract(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
|
|
1569
1637
|
}
|
|
1570
1638
|
|
|
1571
1639
|
async function runSingleStep(
|
|
@@ -1656,12 +1724,15 @@ function markParallelGroupSetupFailure(input: {
|
|
|
1656
1724
|
const statusStep = requiredStatusStep(input.statusPayload, flatTaskIndex);
|
|
1657
1725
|
const task = input.group.parallel[taskIndex];
|
|
1658
1726
|
if (!task) throw new Error(`Missing parallel task at index ${taskIndex}`);
|
|
1659
|
-
statusStep.
|
|
1727
|
+
const stopped = statusStep.stopped || statusStep.stopRequested || input.statusPayload.stopped;
|
|
1728
|
+
const paused = !stopped && input.statusPayload.state === "paused";
|
|
1729
|
+
statusStep.status = stopped ? "stopped" : paused ? "paused" : "failed";
|
|
1660
1730
|
statusStep.startedAt = input.failedAt;
|
|
1661
1731
|
statusStep.endedAt = input.failedAt;
|
|
1662
1732
|
statusStep.durationMs = 0;
|
|
1663
|
-
statusStep.exitCode = 1;
|
|
1664
|
-
|
|
1733
|
+
statusStep.exitCode = paused ? 0 : 1;
|
|
1734
|
+
statusStep.error ??= input.setupError;
|
|
1735
|
+
input.results.push(omitUndefinedProperties({ agent: task.agent, context: task.context, output: input.setupError, error: input.setupError, success: false, exitCode: paused ? 0 : 1, sessionFile: task.sessionFile, stopped: stopped || undefined, interrupted: paused || undefined, timedOut: input.statusPayload.timedOut || undefined }));
|
|
1665
1736
|
}
|
|
1666
1737
|
input.statusPayload.currentStep = input.groupStartFlatIndex;
|
|
1667
1738
|
input.statusPayload.lastUpdate = input.failedAt;
|
|
@@ -1818,7 +1889,10 @@ async function runSingleStepWithTimeout(
|
|
|
1818
1889
|
ctx: SingleStepContext,
|
|
1819
1890
|
parentDeadlineAt?: number,
|
|
1820
1891
|
): Promise<SingleStepResult> {
|
|
1821
|
-
if (step.timeoutMs === undefined) return runSingleStep(step, ctx
|
|
1892
|
+
if (step.timeoutMs === undefined) return runSingleStep(step, parentDeadlineAt === undefined ? ctx : {
|
|
1893
|
+
...ctx,
|
|
1894
|
+
deadlineAt: ctx.deadlineAt === undefined ? parentDeadlineAt : Math.min(ctx.deadlineAt, parentDeadlineAt),
|
|
1895
|
+
});
|
|
1822
1896
|
|
|
1823
1897
|
const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
|
|
1824
1898
|
const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
|
|
@@ -1855,7 +1929,7 @@ async function runSingleStepWithTimeout(
|
|
|
1855
1929
|
}
|
|
1856
1930
|
}
|
|
1857
1931
|
|
|
1858
|
-
async function runSubagent(
|
|
1932
|
+
export async function runSubagent(
|
|
1859
1933
|
config: SubagentRunConfig,
|
|
1860
1934
|
childSessions: ChildSessionFactory,
|
|
1861
1935
|
): Promise<void> {
|
|
@@ -1893,6 +1967,8 @@ async function runSubagent(
|
|
|
1893
1967
|
const stopMessage = "Subagent stopped by user.";
|
|
1894
1968
|
const timeoutAbortController = new AbortController();
|
|
1895
1969
|
const stopAbortController = new AbortController();
|
|
1970
|
+
const setupInterruptController = new AbortController();
|
|
1971
|
+
const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
|
|
1896
1972
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
1897
1973
|
let latestSessionFile: string | undefined;
|
|
1898
1974
|
|
|
@@ -2059,20 +2135,29 @@ async function runSubagent(
|
|
|
2059
2135
|
},
|
|
2060
2136
|
onError: (error, filePath) => console.error(`Failed to update async run index '${filePath}':`, error),
|
|
2061
2137
|
});
|
|
2062
|
-
const queueActiveRunIndex = (): void => {
|
|
2063
|
-
const state =
|
|
2138
|
+
const queueActiveRunIndex = (status: AsyncStatus): void => {
|
|
2139
|
+
const state = status.state;
|
|
2064
2140
|
if (state === lastIndexedStatusState && indexPersistence.pendingCount() === 0) return;
|
|
2065
|
-
indexPersistence.write(asyncDir, { state, toolCallId:
|
|
2141
|
+
indexPersistence.write(asyncDir, { state, toolCallId: status.toolCallId }, (_filePath, payload) => {
|
|
2066
2142
|
const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
|
|
2067
2143
|
updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
|
|
2068
2144
|
});
|
|
2069
2145
|
};
|
|
2146
|
+
let finalResultCommitted = false;
|
|
2147
|
+
let finalResultPublication: { resolve(): void; reject(error: unknown): void } | undefined;
|
|
2070
2148
|
const runPersistence = createCapacityResilientJsonWriter({
|
|
2071
2149
|
keepAlive: true,
|
|
2072
|
-
onSuccess: (filePath) => {
|
|
2073
|
-
if (filePath === statusPath) queueActiveRunIndex();
|
|
2150
|
+
onSuccess: (filePath, payload) => {
|
|
2151
|
+
if (filePath === statusPath) queueActiveRunIndex(payload as AsyncStatus);
|
|
2152
|
+
if (filePath === resultPath && finalResultPublication) {
|
|
2153
|
+
finalResultCommitted = true;
|
|
2154
|
+
finalResultPublication.resolve();
|
|
2155
|
+
}
|
|
2156
|
+
},
|
|
2157
|
+
onError: (error, filePath) => {
|
|
2158
|
+
console.error(`Failed to persist async run state '${filePath}':`, error);
|
|
2159
|
+
if (filePath === resultPath) finalResultPublication?.reject(error);
|
|
2074
2160
|
},
|
|
2075
|
-
onError: (error, filePath) => console.error(`Failed to persist async run state '${filePath}':`, error),
|
|
2076
2161
|
});
|
|
2077
2162
|
try {
|
|
2078
2163
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
@@ -2080,7 +2165,7 @@ async function runSubagent(
|
|
|
2080
2165
|
if (!isStorageCapacityError(error)) throw error;
|
|
2081
2166
|
console.error(`Failed to prepare async run storage '${asyncDir}' while storage is full:`, error);
|
|
2082
2167
|
}
|
|
2083
|
-
runPersistence.write(statusPath, statusPayload);
|
|
2168
|
+
runPersistence.write(statusPath, { ...statusPayload });
|
|
2084
2169
|
|
|
2085
2170
|
let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
2086
2171
|
const currentUsageTotals = (): CostSummary => {
|
|
@@ -2099,6 +2184,13 @@ async function runSubagent(
|
|
|
2099
2184
|
setOptionalProperty(statusPayload, "usageBudget", usageBudgetState(config.usageBudget, currentUsageTotals()));
|
|
2100
2185
|
return statusPayload.usageBudget;
|
|
2101
2186
|
};
|
|
2187
|
+
// Continuation admission only: the existing ledger has no in-flight cost or
|
|
2188
|
+
// external/import usage coverage. Never change ordinary budget enforcement.
|
|
2189
|
+
let continuationUsageUncertain = config.steps.some(isDynamicRunnerGroup) || flatSteps.some((step) => Boolean(step.runner || step.importAsyncRoot));
|
|
2190
|
+
const continuationUsageBudgetExhausted = (): boolean | undefined => {
|
|
2191
|
+
const exhausted = refreshUsageBudget()?.exhausted === true;
|
|
2192
|
+
return exhausted ? true : config.usageBudget && continuationUsageUncertain ? undefined : false;
|
|
2193
|
+
};
|
|
2102
2194
|
const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
|
|
2103
2195
|
if (!config.nestedRoute || !config.nestedSelf) return;
|
|
2104
2196
|
try {
|
|
@@ -2153,7 +2245,6 @@ async function runSubagent(
|
|
|
2153
2245
|
for (const node of graph.nodes) updateNode(node);
|
|
2154
2246
|
statusPayload.workflowGraph = graph;
|
|
2155
2247
|
};
|
|
2156
|
-
let finalResultCommitted = false;
|
|
2157
2248
|
const statusResultState = (): AsyncStatus["state"] | undefined => {
|
|
2158
2249
|
if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
|
|
2159
2250
|
return statusPayload.state;
|
|
@@ -2215,14 +2306,15 @@ async function runSubagent(
|
|
|
2215
2306
|
}), (filePath, payload) => writePendingAsyncResultFile(filePath, payload as Record<string, unknown>));
|
|
2216
2307
|
};
|
|
2217
2308
|
const writeStatusPayloadNow = (): void => {
|
|
2309
|
+
if (finalResultPublication) return;
|
|
2218
2310
|
refreshWorkflowGraph();
|
|
2219
2311
|
writeRecoverableStatusResult();
|
|
2220
|
-
runPersistence.write(statusPath, statusPayload);
|
|
2312
|
+
runPersistence.write(statusPath, { ...statusPayload });
|
|
2221
2313
|
emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
|
|
2222
2314
|
};
|
|
2223
2315
|
const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
|
|
2224
2316
|
const writeStatusPayload = (immediate = true): void => {
|
|
2225
|
-
if (immediate || statusPayload.state !== "running"
|
|
2317
|
+
if (immediate || statusPayload.state !== "running") {
|
|
2226
2318
|
if (!statusWriteCoalescer.flush(statusPath)) writeStatusPayloadNow();
|
|
2227
2319
|
return;
|
|
2228
2320
|
}
|
|
@@ -2825,7 +2917,10 @@ async function runSubagent(
|
|
|
2825
2917
|
const applySteerDelivery = (requestId: string, index: number, delivery: { state: "delivered" | "queued" | "failed"; message: string }): void => {
|
|
2826
2918
|
const lifecycle = steeringStatus(statusPayload);
|
|
2827
2919
|
const request = lifecycle.recent.find((candidate) => candidate.id === requestId);
|
|
2828
|
-
|
|
2920
|
+
const target = request?.targets.find((candidate) => candidate.index === index);
|
|
2921
|
+
if (!request || !target) return;
|
|
2922
|
+
if (target.state === "delivered" || target.state === "late" || target.state === "failed") return;
|
|
2923
|
+
if (target.state === delivery.state) return;
|
|
2829
2924
|
const late = fs.existsSync(steeringMarkerPath(requestId));
|
|
2830
2925
|
const now = Date.now();
|
|
2831
2926
|
if (delivery.state === "delivered") {
|
|
@@ -2878,6 +2973,7 @@ async function runSubagent(
|
|
|
2878
2973
|
const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
|
|
2879
2974
|
const step = statusPayload.steps[flatIndex];
|
|
2880
2975
|
if (!step) return;
|
|
2976
|
+
const previousActivityState = step.activityState;
|
|
2881
2977
|
const now = Date.now();
|
|
2882
2978
|
statusPayload.currentStep = flatIndex;
|
|
2883
2979
|
if (isChildWatchdogStatusEvent(event)) {
|
|
@@ -3012,6 +3108,10 @@ async function runSubagent(
|
|
|
3012
3108
|
appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
|
|
3013
3109
|
step.turnCount = (step.turnCount ?? 0) + 1;
|
|
3014
3110
|
const usage = event.message.usage;
|
|
3111
|
+
if (config.usageBudget) {
|
|
3112
|
+
const known = (value: unknown) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
3113
|
+
if (!known(usage?.input ?? usage?.inputTokens) || !known(usage?.output ?? usage?.outputTokens)) continuationUsageUncertain = true;
|
|
3114
|
+
}
|
|
3015
3115
|
if (usage) {
|
|
3016
3116
|
const input = usage.input ?? usage.inputTokens ?? 0;
|
|
3017
3117
|
const output = usage.output ?? usage.outputTokens ?? 0;
|
|
@@ -3031,7 +3131,8 @@ async function runSubagent(
|
|
|
3031
3131
|
statusPayload.lastActivityAt = now;
|
|
3032
3132
|
statusPayload.lastUpdate = now;
|
|
3033
3133
|
maybeEmitActiveLongRunning(flatIndex, now);
|
|
3034
|
-
|
|
3134
|
+
// A sibling may keep aggregate attention unchanged; publish this step's transition.
|
|
3135
|
+
writeStatusPayload(step.activityState !== previousActivityState);
|
|
3035
3136
|
};
|
|
3036
3137
|
const updateRunnerActivityState = (now: number): boolean => {
|
|
3037
3138
|
if (!controlConfig.enabled) return false;
|
|
@@ -3103,10 +3204,55 @@ async function runSubagent(
|
|
|
3103
3204
|
activityTimer.unref?.();
|
|
3104
3205
|
}
|
|
3105
3206
|
|
|
3207
|
+
const publishSetupUnknown = (progress: WorktreeSetupProgress): void => {
|
|
3208
|
+
if (!progress.unknown) return;
|
|
3209
|
+
const proof = {
|
|
3210
|
+
version: 1 as const, state: "unknown" as const, runId: id,
|
|
3211
|
+
runnerProcessInstanceId: config.runnerProcessInstanceId ?? "unknown",
|
|
3212
|
+
reason: "process-tree-unverified" as const,
|
|
3213
|
+
diagnostic: `Worktree setup settlement unknown: ${progress.unknown}; manual reconciliation required; ${parallelHandoffPath(asyncDir)}`,
|
|
3214
|
+
};
|
|
3215
|
+
statusPayload.processTerminal = proof;
|
|
3216
|
+
// Publish the sticky proof independently of in-process child writer counts.
|
|
3217
|
+
writeAtomicJson(processTerminalPath(asyncDir), proof);
|
|
3218
|
+
};
|
|
3219
|
+
const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
|
|
3220
|
+
let admitted = false;
|
|
3221
|
+
try { await withWorktreeTransaction(() => {
|
|
3222
|
+
if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
|
|
3223
|
+
admitted = true;
|
|
3224
|
+
action();
|
|
3225
|
+
}); }
|
|
3226
|
+
catch (error) {
|
|
3227
|
+
if (admitted) throw error;
|
|
3228
|
+
const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
|
|
3229
|
+
statusPayload.parallelHandoff = writeParallelHandoffGroup({
|
|
3230
|
+
manifestPath: parallelHandoffPath(asyncDir), runId: id,
|
|
3231
|
+
mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
|
|
3232
|
+
source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [],
|
|
3233
|
+
cleanup: { state: "partial", pruned: false, errors: [reason], tasks: setup.worktrees.map((worktree) => ({
|
|
3234
|
+
index: worktree.index, path: worktree.path, branch: worktree.branch, provider: worktree.provider, naming: worktree.naming,
|
|
3235
|
+
worktreeRemoved: false, branchRemoved: false, preserved: true, reason,
|
|
3236
|
+
})) },
|
|
3237
|
+
});
|
|
3238
|
+
previousOutput = [previousOutput, reason, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
|
|
3239
|
+
writeStatusPayload();
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
const cleanupRemainingWorktree = (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number) => finalizeWorktree(setup, stepIndex, flatStartIndex, () => {
|
|
3243
|
+
const cleanup = cleanupWorktrees(setup);
|
|
3244
|
+
statusPayload.parallelHandoff = writeParallelHandoffGroup({
|
|
3245
|
+
manifestPath: parallelHandoffPath(asyncDir), runId: id,
|
|
3246
|
+
mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
|
|
3247
|
+
source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [], cleanup,
|
|
3248
|
+
});
|
|
3249
|
+
writeStatusPayload();
|
|
3250
|
+
}, config.deadlineAt);
|
|
3106
3251
|
const interruptRunner = () => {
|
|
3107
3252
|
consumeInterruptRequest(asyncDir);
|
|
3108
3253
|
if (interrupted || statusPayload.state !== "running") return;
|
|
3109
3254
|
interrupted = true;
|
|
3255
|
+
setupInterruptController.abort();
|
|
3110
3256
|
const now = Date.now();
|
|
3111
3257
|
statusPayload.state = "paused";
|
|
3112
3258
|
currentActivityState = undefined;
|
|
@@ -3340,14 +3486,14 @@ async function runSubagent(
|
|
|
3340
3486
|
cwd,
|
|
3341
3487
|
signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
|
|
3342
3488
|
abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
|
|
3343
|
-
reportOptional:
|
|
3489
|
+
reportOptional: isAgentContract(step.agentContract),
|
|
3344
3490
|
}))
|
|
3345
3491
|
: undefined;
|
|
3346
3492
|
const groupStopped = stopped || stopAbortController.signal.aborted;
|
|
3347
3493
|
const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
|
|
3348
3494
|
const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
|
|
3349
3495
|
if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
|
|
3350
|
-
const groupAcceptanceFailure = effectiveGroupAcceptance && (!
|
|
3496
|
+
const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
3351
3497
|
if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
|
|
3352
3498
|
const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
|
|
3353
3499
|
statusPayload.state = groupStopped ? "stopped" : "failed";
|
|
@@ -3549,10 +3695,12 @@ async function runSubagent(
|
|
|
3549
3695
|
nestedRoute: config.nestedRoute,
|
|
3550
3696
|
capabilityCeiling: config.capabilityCeiling,
|
|
3551
3697
|
runFanoutBudget: config.runFanoutBudget,
|
|
3698
|
+
hostAvailableBuiltins: config.hostAvailableBuiltins,
|
|
3552
3699
|
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
3553
3700
|
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
3554
3701
|
registerStop: (stop) => registerStepStop(fi, stop),
|
|
3555
3702
|
registerSteer: (steer) => registerStepSteer(fi, steer),
|
|
3703
|
+
onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, fi, delivery),
|
|
3556
3704
|
timeoutSignal: timeoutAbortController.signal,
|
|
3557
3705
|
stopSignal: stopAbortController.signal,
|
|
3558
3706
|
trackedMutationEvidenceForCompletionGuard: false,
|
|
@@ -3564,7 +3712,8 @@ async function runSubagent(
|
|
|
3564
3712
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3565
3713
|
onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
|
|
3566
3714
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
|
|
3567
|
-
usageBudgetExhausted:
|
|
3715
|
+
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
3716
|
+
usageBudget: config.usageBudget,
|
|
3568
3717
|
orcaProgressTab,
|
|
3569
3718
|
}), config.deadlineAt);
|
|
3570
3719
|
const taskEndTime = Date.now();
|
|
@@ -3680,7 +3829,7 @@ async function runSubagent(
|
|
|
3680
3829
|
const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
|
|
3681
3830
|
const acceptanceFailures = parallelResults
|
|
3682
3831
|
.map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
|
|
3683
|
-
.filter(({ result, task }) =>
|
|
3832
|
+
.filter(({ result, task }) => isAgentContract(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
|
|
3684
3833
|
if (acceptanceFailures.length > 0) {
|
|
3685
3834
|
const message = acceptanceFailures
|
|
3686
3835
|
.map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
|
|
@@ -3710,13 +3859,13 @@ async function runSubagent(
|
|
|
3710
3859
|
cwd,
|
|
3711
3860
|
signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
|
|
3712
3861
|
abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
|
|
3713
|
-
reportOptional:
|
|
3862
|
+
reportOptional: isAgentContract(step.agentContract),
|
|
3714
3863
|
}))
|
|
3715
3864
|
: undefined;
|
|
3716
3865
|
const groupStopped = stopped || stopAbortController.signal.aborted;
|
|
3717
3866
|
const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
|
|
3718
3867
|
const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
|
|
3719
|
-
const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!
|
|
3868
|
+
const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
3720
3869
|
const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
|
|
3721
3870
|
markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
|
|
3722
3871
|
if (groupError) {
|
|
@@ -3795,7 +3944,9 @@ async function runSubagent(
|
|
|
3795
3944
|
break;
|
|
3796
3945
|
}
|
|
3797
3946
|
try {
|
|
3798
|
-
worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
|
|
3947
|
+
worktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
|
|
3948
|
+
signal: setupSignal,
|
|
3949
|
+
deadlineAt: config.deadlineAt,
|
|
3799
3950
|
agents: group.parallel.map((task) => task.agent),
|
|
3800
3951
|
labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
|
|
3801
3952
|
tasks: group.parallel.map((task) => task.task),
|
|
@@ -3806,9 +3957,10 @@ async function runSubagent(
|
|
|
3806
3957
|
? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
|
|
3807
3958
|
: undefined,
|
|
3808
3959
|
baseDir: config.worktreeBaseDir,
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
const
|
|
3960
|
+
onProgress: (progress) => {
|
|
3961
|
+
publishSetupUnknown(progress);
|
|
3962
|
+
for (const worktree of progress.setup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
|
|
3963
|
+
const pendingHandoff = writeWorktreeSetupHandoff({
|
|
3812
3964
|
manifestPath: parallelHandoffPath(asyncDir),
|
|
3813
3965
|
runId: id,
|
|
3814
3966
|
mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
|
|
@@ -3816,16 +3968,25 @@ async function runSubagent(
|
|
|
3816
3968
|
cwd,
|
|
3817
3969
|
stepIndex,
|
|
3818
3970
|
flatStartIndex: groupStartFlatIndex,
|
|
3819
|
-
|
|
3971
|
+
progress,
|
|
3820
3972
|
laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
|
|
3821
3973
|
});
|
|
3974
|
+
if (!pendingHandoff) return;
|
|
3822
3975
|
statusPayload.parallelHandoff = pendingHandoff;
|
|
3823
3976
|
statusPayload.lastUpdate = Date.now();
|
|
3824
3977
|
writeStatusPayload();
|
|
3825
3978
|
},
|
|
3826
3979
|
}));
|
|
3980
|
+
if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
|
|
3981
|
+
if (setupSignal.aborted) throw new Error(stopped ? stopMessage : timedOut ? timeoutMessage ?? "Subagent timed out." : "Subagent paused during worktree setup.");
|
|
3827
3982
|
} catch (error) {
|
|
3983
|
+
if (worktreeSetup) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
|
|
3984
|
+
if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
|
|
3828
3985
|
const setupError = error instanceof Error ? error.message : String(error);
|
|
3986
|
+
if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
|
|
3987
|
+
for (let index = groupStartFlatIndex; index < groupStartFlatIndex + group.parallel.length; index++) {
|
|
3988
|
+
if (childStopRequests.has(index)) markChildStopped(index);
|
|
3989
|
+
}
|
|
3829
3990
|
const failedAt = Date.now();
|
|
3830
3991
|
markParallelGroupSetupFailure({
|
|
3831
3992
|
statusPayload,
|
|
@@ -3946,10 +4107,12 @@ async function runSubagent(
|
|
|
3946
4107
|
nestedRoute: config.nestedRoute,
|
|
3947
4108
|
capabilityCeiling: config.capabilityCeiling,
|
|
3948
4109
|
runFanoutBudget: config.runFanoutBudget,
|
|
4110
|
+
hostAvailableBuiltins: config.hostAvailableBuiltins,
|
|
3949
4111
|
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
3950
4112
|
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
3951
4113
|
registerStop: (stop) => registerStepStop(fi, stop),
|
|
3952
4114
|
registerSteer: (steer) => registerStepSteer(fi, steer),
|
|
4115
|
+
onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, fi, delivery),
|
|
3953
4116
|
timeoutSignal: timeoutAbortController.signal,
|
|
3954
4117
|
stopSignal: stopAbortController.signal,
|
|
3955
4118
|
trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
|
|
@@ -3961,7 +4124,8 @@ async function runSubagent(
|
|
|
3961
4124
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3962
4125
|
onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
|
|
3963
4126
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
|
|
3964
|
-
usageBudgetExhausted:
|
|
4127
|
+
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
4128
|
+
usageBudget: config.usageBudget,
|
|
3965
4129
|
orcaProgressTab,
|
|
3966
4130
|
}), config.deadlineAt);
|
|
3967
4131
|
if (task.sessionFile) {
|
|
@@ -4139,47 +4303,50 @@ async function runSubagent(
|
|
|
4139
4303
|
})),
|
|
4140
4304
|
);
|
|
4141
4305
|
if (worktreeSetup) {
|
|
4142
|
-
const
|
|
4143
|
-
if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
|
|
4306
|
+
const setup = worktreeSetup;
|
|
4144
4307
|
worktreeFinalized = true;
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4308
|
+
await finalizeWorktree(setup, stepIndex, groupStartFlatIndex, () => {
|
|
4309
|
+
const captured = captureParallelWorktreeDiffs(setup, asyncDir, stepIndex, group);
|
|
4310
|
+
if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
|
|
4311
|
+
const manifestPath = parallelHandoffPath(asyncDir);
|
|
4312
|
+
const handoff = {
|
|
4313
|
+
manifestPath,
|
|
4314
|
+
runId: id,
|
|
4315
|
+
mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
|
|
4316
|
+
source: "async" as const,
|
|
4317
|
+
cwd,
|
|
4318
|
+
stepIndex,
|
|
4319
|
+
flatStartIndex: groupStartFlatIndex,
|
|
4320
|
+
setup,
|
|
4321
|
+
diffs: captured.diffs,
|
|
4322
|
+
results: parallelResults.map((result) => ({
|
|
4323
|
+
agent: result.agent,
|
|
4324
|
+
...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
|
|
4325
|
+
...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
|
|
4326
|
+
...(config.lane ? { lane: config.lane } : {}),
|
|
4327
|
+
status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
|
|
4328
|
+
processSignal: result.processSignal,
|
|
4329
|
+
interrupted: result.interrupted,
|
|
4330
|
+
timedOut: result.timedOut,
|
|
4331
|
+
stopped: result.stopped,
|
|
4332
|
+
}))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
|
|
4333
|
+
summary: result.output || result.error || "(no output)",
|
|
4334
|
+
...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
|
|
4335
|
+
...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
|
|
4336
|
+
...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
|
|
4337
|
+
...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
|
|
4338
|
+
})),
|
|
4339
|
+
};
|
|
4340
|
+
try {
|
|
4341
|
+
writeParallelHandoffGroup(handoff);
|
|
4342
|
+
const cleanup = cleanupWorktrees(setup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
|
|
4343
|
+
statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
|
|
4344
|
+
previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
|
|
4345
|
+
} catch (error) {
|
|
4346
|
+
previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
|
|
4347
|
+
}
|
|
4348
|
+
writeStatusPayload();
|
|
4349
|
+
});
|
|
4183
4350
|
}
|
|
4184
4351
|
|
|
4185
4352
|
appendJsonl(eventsPath, JSON.stringify({
|
|
@@ -4188,12 +4355,12 @@ async function runSubagent(
|
|
|
4188
4355
|
runId: id,
|
|
4189
4356
|
stepIndex,
|
|
4190
4357
|
success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1)
|
|
4191
|
-
&& parallelResults.every((result, index) => !(
|
|
4358
|
+
&& parallelResults.every((result, index) => !(isAgentContract(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
|
|
4192
4359
|
}));
|
|
4193
4360
|
|
|
4194
4361
|
const acceptanceGateFailure = parallelResults
|
|
4195
4362
|
.map((result, index) => ({ result, index, task: group.parallel[index] }))
|
|
4196
|
-
.find(({ result, task }) =>
|
|
4363
|
+
.find(({ result, task }) => isAgentContract(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
|
|
4197
4364
|
if (acceptanceGateFailure) {
|
|
4198
4365
|
statusPayload.error = (acceptanceGateFailure.result.acceptance ? acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) : undefined) ?? "Parallel acceptance gate rejected the step.";
|
|
4199
4366
|
writeStatusPayload();
|
|
@@ -4203,7 +4370,7 @@ async function runSubagent(
|
|
|
4203
4370
|
break;
|
|
4204
4371
|
}
|
|
4205
4372
|
} finally {
|
|
4206
|
-
if (worktreeSetup && !worktreeFinalized)
|
|
4373
|
+
if (worktreeSetup && !worktreeFinalized) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
|
|
4207
4374
|
}
|
|
4208
4375
|
} else {
|
|
4209
4376
|
const seqStep = step as SubagentStep;
|
|
@@ -4230,7 +4397,9 @@ async function runSubagent(
|
|
|
4230
4397
|
let singleWorktreeSetup: WorktreeSetup | undefined;
|
|
4231
4398
|
if (seqStep.worktree) {
|
|
4232
4399
|
try {
|
|
4233
|
-
singleWorktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
|
|
4400
|
+
singleWorktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
|
|
4401
|
+
signal: setupSignal,
|
|
4402
|
+
deadlineAt: config.deadlineAt,
|
|
4234
4403
|
agents: [seqStep.agent],
|
|
4235
4404
|
labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
|
|
4236
4405
|
tasks: [seqStep.task],
|
|
@@ -4241,12 +4410,13 @@ async function runSubagent(
|
|
|
4241
4410
|
? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
|
|
4242
4411
|
: undefined,
|
|
4243
4412
|
baseDir: config.worktreeBaseDir,
|
|
4244
|
-
|
|
4245
|
-
|
|
4413
|
+
onProgress: (progress) => {
|
|
4414
|
+
publishSetupUnknown(progress);
|
|
4415
|
+
const worktree = progress.setup.worktrees[0];
|
|
4246
4416
|
if (worktree) {
|
|
4247
4417
|
setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
|
|
4248
4418
|
}
|
|
4249
|
-
const pendingHandoff =
|
|
4419
|
+
const pendingHandoff = writeWorktreeSetupHandoff({
|
|
4250
4420
|
manifestPath: parallelHandoffPath(asyncDir),
|
|
4251
4421
|
runId: id,
|
|
4252
4422
|
mode: "single",
|
|
@@ -4254,19 +4424,47 @@ async function runSubagent(
|
|
|
4254
4424
|
cwd,
|
|
4255
4425
|
stepIndex,
|
|
4256
4426
|
flatStartIndex: flatIndex,
|
|
4257
|
-
|
|
4427
|
+
progress,
|
|
4258
4428
|
laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
|
|
4259
4429
|
});
|
|
4430
|
+
if (!pendingHandoff) return;
|
|
4260
4431
|
statusPayload.parallelHandoff = pendingHandoff;
|
|
4261
4432
|
statusPayload.lastUpdate = Date.now();
|
|
4262
4433
|
writeStatusPayload();
|
|
4263
4434
|
},
|
|
4264
4435
|
}));
|
|
4265
4436
|
} catch (error) {
|
|
4266
|
-
if (
|
|
4267
|
-
|
|
4437
|
+
if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
|
|
4438
|
+
if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
|
|
4439
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4440
|
+
if (childStopRequests.has(flatIndex)) markChildStopped(flatIndex);
|
|
4441
|
+
const statusStep = requiredStatusStep(statusPayload, flatIndex);
|
|
4442
|
+
statusStep.status = stopped || childStopRequests.has(flatIndex) ? "stopped" : interrupted ? "paused" : "failed";
|
|
4443
|
+
statusStep.error ??= message;
|
|
4444
|
+
statusStep.exitCode = interrupted ? 0 : 1;
|
|
4445
|
+
statusStep.endedAt = Date.now();
|
|
4446
|
+
results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4447
|
+
: childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
|
|
4448
|
+
: timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4449
|
+
: interrupted ? pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4450
|
+
: { agent: seqStep.agent, output: message, error: message, exitCode: 1, success: false });
|
|
4451
|
+
statusPayload.error ??= message;
|
|
4452
|
+
writeStatusPayload();
|
|
4453
|
+
flatIndex++;
|
|
4454
|
+
break;
|
|
4268
4455
|
}
|
|
4269
4456
|
}
|
|
4457
|
+
if (singleWorktreeSetup && config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
|
|
4458
|
+
if (singleWorktreeSetup && (timedOut || stopped || interrupted || childStopRequests.has(flatIndex))) {
|
|
4459
|
+
await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
|
|
4460
|
+
results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4461
|
+
: childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
|
|
4462
|
+
: timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
|
|
4463
|
+
: pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
|
|
4464
|
+
if (interrupted) requiredStatusStep(statusPayload, flatIndex).status = "paused";
|
|
4465
|
+
flatIndex++;
|
|
4466
|
+
continue;
|
|
4467
|
+
}
|
|
4270
4468
|
const singleCwd = singleWorktreeSetup?.worktrees[0]?.agentCwd ?? cwd;
|
|
4271
4469
|
const stepStartTime = Date.now();
|
|
4272
4470
|
statusPayload.currentStep = flatIndex;
|
|
@@ -4311,10 +4509,12 @@ async function runSubagent(
|
|
|
4311
4509
|
nestedRoute: config.nestedRoute,
|
|
4312
4510
|
capabilityCeiling: config.capabilityCeiling,
|
|
4313
4511
|
runFanoutBudget: config.runFanoutBudget,
|
|
4512
|
+
hostAvailableBuiltins: config.hostAvailableBuiltins,
|
|
4314
4513
|
registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
|
|
4315
4514
|
registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
|
|
4316
4515
|
registerStop: (stop) => registerStepStop(flatIndex, stop),
|
|
4317
4516
|
registerSteer: (steer) => registerStepSteer(flatIndex, steer),
|
|
4517
|
+
onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, flatIndex, delivery),
|
|
4318
4518
|
timeoutSignal: timeoutAbortController.signal,
|
|
4319
4519
|
stopSignal: stopAbortController.signal,
|
|
4320
4520
|
timeoutMessage,
|
|
@@ -4325,11 +4525,12 @@ async function runSubagent(
|
|
|
4325
4525
|
onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
|
|
4326
4526
|
onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
|
|
4327
4527
|
skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
|
|
4328
|
-
usageBudgetExhausted:
|
|
4528
|
+
usageBudgetExhausted: continuationUsageBudgetExhausted,
|
|
4529
|
+
usageBudget: config.usageBudget,
|
|
4329
4530
|
orcaProgressTab,
|
|
4330
4531
|
}), config.deadlineAt);
|
|
4331
4532
|
} catch (error) {
|
|
4332
|
-
if (singleWorktreeSetup)
|
|
4533
|
+
if (singleWorktreeSetup) await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
|
|
4333
4534
|
throw error;
|
|
4334
4535
|
}
|
|
4335
4536
|
if (seqStep.sessionFile) {
|
|
@@ -4361,6 +4562,7 @@ async function runSubagent(
|
|
|
4361
4562
|
totalCost: singleResult.totalCost,
|
|
4362
4563
|
usage: singleResult.usage,
|
|
4363
4564
|
artifactPaths: singleResult.artifactPaths,
|
|
4565
|
+
savedOutputPath: singleResult.savedOutputPath,
|
|
4364
4566
|
transcriptPath: singleResult.transcriptPath,
|
|
4365
4567
|
transcriptError: singleResult.transcriptError,
|
|
4366
4568
|
effects: singleResult.effects,
|
|
@@ -4481,48 +4683,51 @@ async function runSubagent(
|
|
|
4481
4683
|
}));
|
|
4482
4684
|
if (stopped || childStopped) appendTerminalChildStatusEvent(flatIndex, stepEndTime);
|
|
4483
4685
|
if (singleWorktreeSetup && !singleResult.detached) {
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
manifestPath
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4686
|
+
const setup = singleWorktreeSetup;
|
|
4687
|
+
await finalizeWorktree(setup, stepIndex, flatIndex, () => {
|
|
4688
|
+
const diffs = diffWorktrees(setup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
|
|
4689
|
+
const diffSummary = formatWorktreeDiffSummary(diffs);
|
|
4690
|
+
const manifestPath = parallelHandoffPath(asyncDir);
|
|
4691
|
+
const handoff = {
|
|
4692
|
+
manifestPath,
|
|
4693
|
+
runId: id,
|
|
4694
|
+
mode: "single" as const,
|
|
4695
|
+
source: "async" as const,
|
|
4696
|
+
cwd,
|
|
4697
|
+
stepIndex,
|
|
4698
|
+
flatStartIndex: flatIndex,
|
|
4699
|
+
setup,
|
|
4700
|
+
diffs,
|
|
4701
|
+
results: [{
|
|
4702
|
+
agent: singleResult.agent,
|
|
4703
|
+
...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
|
|
4704
|
+
...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
|
|
4705
|
+
...(config.lane ? { lane: config.lane } : {}),
|
|
4706
|
+
status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
|
|
4707
|
+
summary: singleResult.output || singleResult.error || "(no output)",
|
|
4708
|
+
...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
|
|
4709
|
+
...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
|
|
4710
|
+
...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
|
|
4711
|
+
...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
|
|
4712
|
+
}],
|
|
4713
|
+
};
|
|
4714
|
+
try {
|
|
4715
|
+
writeParallelHandoffGroup(handoff);
|
|
4716
|
+
const cleanup = cleanupWorktrees(setup, {
|
|
4717
|
+
kind: "preserve",
|
|
4718
|
+
capturedDiffs: diffs,
|
|
4719
|
+
handoffManifestPath: manifestPath,
|
|
4720
|
+
...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
|
|
4721
|
+
? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
|
|
4722
|
+
: {}),
|
|
4723
|
+
});
|
|
4724
|
+
statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
|
|
4725
|
+
previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
|
|
4726
|
+
} catch (error) {
|
|
4727
|
+
previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
|
|
4728
|
+
}
|
|
4729
|
+
writeStatusPayload();
|
|
4730
|
+
});
|
|
4526
4731
|
}
|
|
4527
4732
|
|
|
4528
4733
|
if (singleResult.completionGuardTriggered) {
|
|
@@ -4540,7 +4745,7 @@ async function runSubagent(
|
|
|
4540
4745
|
}
|
|
4541
4746
|
|
|
4542
4747
|
flatIndex++;
|
|
4543
|
-
if (
|
|
4748
|
+
if (isAgentContract(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
|
|
4544
4749
|
statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
|
|
4545
4750
|
writeStatusPayload();
|
|
4546
4751
|
break;
|
|
@@ -4626,6 +4831,11 @@ async function runSubagent(
|
|
|
4626
4831
|
stopped: result.stopped,
|
|
4627
4832
|
})));
|
|
4628
4833
|
const partialWithEvidence = !stopped && !signalTerminated && !timedOut && !usageBudgetExceeded && !interrupted && results.some(partialEvidenceResult) && !results.some(concreteFailureResult);
|
|
4834
|
+
// Flush while still nonterminal; deferred status retries retain that state snapshot.
|
|
4835
|
+
statusWriteCoalescer.flush(statusPath);
|
|
4836
|
+
const publication = new Promise<void>((resolve, reject) => {
|
|
4837
|
+
finalResultPublication = { resolve, reject };
|
|
4838
|
+
});
|
|
4629
4839
|
statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
|
|
4630
4840
|
closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
|
|
4631
4841
|
disposeControlInbox();
|
|
@@ -4637,7 +4847,7 @@ async function runSubagent(
|
|
|
4637
4847
|
for (const target of request.targets) {
|
|
4638
4848
|
if (target.state !== "scheduled" && target.state !== "routed" && target.state !== "queued") continue;
|
|
4639
4849
|
changed = true;
|
|
4640
|
-
const reason = target.state === "queued" ?
|
|
4850
|
+
const reason = target.state === "queued" ? unconsumedSteerReason() : "child terminated before steering delivery";
|
|
4641
4851
|
updateSteeringLifecycleTarget(request.id, target.index, "failed", Date.now(), { reason });
|
|
4642
4852
|
markSteeringAttention(target.index);
|
|
4643
4853
|
emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: reason }, target.index, { reason });
|
|
@@ -4684,6 +4894,9 @@ async function runSubagent(
|
|
|
4684
4894
|
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
4685
4895
|
}
|
|
4686
4896
|
}
|
|
4897
|
+
let childSessionDisposal: Promise<void> | undefined;
|
|
4898
|
+
const disposeChildSessions = (): Promise<void> => childSessionDisposal ??= childSessions.dispose()
|
|
4899
|
+
.catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
|
|
4687
4900
|
try {
|
|
4688
4901
|
runPersistence.write(resultPath, {
|
|
4689
4902
|
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -4724,6 +4937,7 @@ async function runSubagent(
|
|
|
4724
4937
|
totalCost: r.totalCost,
|
|
4725
4938
|
usage: r.usage,
|
|
4726
4939
|
artifactPaths: r.artifactPaths,
|
|
4940
|
+
savedOutputPath: r.savedOutputPath,
|
|
4727
4941
|
outputSaveError: r.outputSaveError,
|
|
4728
4942
|
metadataSaveError: r.metadataSaveError,
|
|
4729
4943
|
truncated: r.truncated,
|
|
@@ -4778,13 +4992,16 @@ async function runSubagent(
|
|
|
4778
4992
|
...(taskIndex !== undefined && { taskIndex }),
|
|
4779
4993
|
...(totalTasks !== undefined && { totalTasks }),
|
|
4780
4994
|
}, (filePath, payload) => { writeAsyncResultFile(filePath, payload as Record<string, unknown>); });
|
|
4781
|
-
|
|
4995
|
+
// Only capacity deferral releases settled sessions before terminal publication.
|
|
4996
|
+
if (!finalResultCommitted) await Promise.all([publication, disposeChildSessions()]);
|
|
4782
4997
|
} catch (err) {
|
|
4783
4998
|
const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
|
|
4784
4999
|
console.error(message, err);
|
|
4785
5000
|
statusPayload.state = "failed";
|
|
4786
5001
|
statusPayload.error = message;
|
|
4787
5002
|
statusPayload.lastUpdate = Date.now();
|
|
5003
|
+
} finally {
|
|
5004
|
+
finalResultPublication = undefined;
|
|
4788
5005
|
}
|
|
4789
5006
|
writeStatusPayload();
|
|
4790
5007
|
await orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
|
|
@@ -4822,9 +5039,8 @@ async function runSubagent(
|
|
|
4822
5039
|
}), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
|
|
4823
5040
|
fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
|
|
4824
5041
|
}));
|
|
4825
|
-
//
|
|
4826
|
-
|
|
4827
|
-
await childSessions.dispose().catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
|
|
5042
|
+
// Preserve normal-success disposal ordering, then drain remaining persistence retries.
|
|
5043
|
+
await disposeChildSessions();
|
|
4828
5044
|
while (runPersistence.pendingCount() + indexPersistence.pendingCount() > 0) {
|
|
4829
5045
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
4830
5046
|
}
|
|
@@ -4832,7 +5048,7 @@ async function runSubagent(
|
|
|
4832
5048
|
indexPersistence.dispose();
|
|
4833
5049
|
if (config.runnerProcessInstanceId) {
|
|
4834
5050
|
// Children run inside this process, so no step has writer processes to prove terminal.
|
|
4835
|
-
const writers: Record<string,
|
|
5051
|
+
const writers: Record<string, PiWriterProcessInstanceExit[]> = {};
|
|
4836
5052
|
const expectedWriters: Record<string, number> = {};
|
|
4837
5053
|
for (const index of results.keys()) {
|
|
4838
5054
|
writers[String(index)] = [];
|
|
@@ -4879,7 +5095,8 @@ async function waitForStartupControl(
|
|
|
4879
5095
|
throw new Error(`Timed out after ${timeoutMs}ms waiting for runner startup control '${action}'.`);
|
|
4880
5096
|
}
|
|
4881
5097
|
|
|
4882
|
-
|
|
5098
|
+
// Both hosts enter here: startup authorization, revival leases and disposal stay shared.
|
|
5099
|
+
export async function runConfiguredSubagent(config: SubagentRunConfig, options?: DefaultChildSessionFactoryOptions): Promise<void> {
|
|
4883
5100
|
let lease: ReturnType<typeof acquireSessionLease> | undefined;
|
|
4884
5101
|
let startupCommitted = config.revivalLease === undefined && config.launchBarrierToken === undefined;
|
|
4885
5102
|
const startupPath = path.join(config.asyncDir, "runner-startup.json");
|
|
@@ -4918,7 +5135,7 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
4918
5135
|
}
|
|
4919
5136
|
}
|
|
4920
5137
|
}
|
|
4921
|
-
const childSessions = await loadRunnerChildSessionFactory(config);
|
|
5138
|
+
const childSessions = await loadRunnerChildSessionFactory(config, options);
|
|
4922
5139
|
try {
|
|
4923
5140
|
await runSubagent(config, childSessions);
|
|
4924
5141
|
} finally {
|
|
@@ -4968,6 +5185,7 @@ function startConfiguredSubagent(config: SubagentRunConfig): void {
|
|
|
4968
5185
|
);
|
|
4969
5186
|
}
|
|
4970
5187
|
|
|
5188
|
+
if (isRunnerEntrypoint) {
|
|
4971
5189
|
const configArg = process.argv[2];
|
|
4972
5190
|
if (configArg) {
|
|
4973
5191
|
try {
|
|
@@ -4999,3 +5217,4 @@ if (configArg) {
|
|
|
4999
5217
|
}
|
|
5000
5218
|
});
|
|
5001
5219
|
}
|
|
5220
|
+
}
|