UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

631 lines (630 loc) • 38.1 kB
import { deliverEngineNotice } from "../types.js"; import { withDeploymentAllowedTools } from "../allow-entry.js"; import { uuidv7 } from "../../internal/harness.js"; import { snapshotActorAssertion } from "../../internal/llm.js"; import { CheckpointError } from "../checkpoint-store.js"; import { createRapidRefillState } from "../auto-compaction.js"; import { boundInputHashOf } from "../canonical-json.js"; import { emitTrace } from "../trace.js"; import { emitTaskOutcome } from "../task-outcome.js"; import { authCarrierFingerprint, displayUrlOf, routeKeyOf } from "../../brain/route-adjudicator.js"; import { runWithReasoningWireFacts } from "../../brain/status-sink.js"; import { expandTiers } from "../roles.js"; import { screenSwappableDeps } from "../swappable-deps.js"; import { runSideQuery } from "../side-query.js"; import { PushQueue } from "../push-queue.js"; import { TtlSessionStore } from "../session-store.js"; import { errorCodeOf } from "./assemble-result.js"; import { effectiveDelegationFacts, prepareTask } from "./prepare-task.js"; import { SessionReadFileStates } from "./session-read-file-states.js"; import { SessionModeSeats } from "./session-mode-seats.js"; import { RunnerSharedToolResultStore } from "../tool-result-store.js"; import { PAUSE_REGISTRY } from "../pause-registry.js"; import { wrapToolPolicy } from "../tool-policy.js"; import { PendingSessionNotifications } from "../task-notification.js"; import { ToolDetachHub } from "../tool-detach.js"; import { assertContextSettingsValue, assertContextSettingsWords, contextOverlayFor } from "../context-settings.js"; import { createRunState } from "./initial-run-state.js"; import { reconciledToolEndBody } from "./tool-end-body.js"; import { answerFaceForRedeemedCall } from "./decide-continuation.js"; import { createTurnBoundary } from "./run-turn-boundary.js"; import { createHarnessHandlers } from "./run-harness-handlers.js"; import { resumeAdmission } from "./resume-admission.js"; import { resumeReviewOutcome } from "./resume-review-outcome.js"; import { resumePolicyOutcome } from "./resume-policy-outcome.js"; import { resumeCheckpointScreen } from "./resume-checkpoint-screen.js"; import { resumeInternalsAndConfig } from "./resume-internals-and-config.js"; import { resumeApply } from "./resume-apply.js"; import { resumePreflight } from "./resume-preflight.js"; import { resumeClaim } from "./resume-claim.js"; import { streamSettleBackstop } from "./stream-settle-backstop.js"; import { streamReap } from "./stream-reap.js"; import { streamSteerVerb } from "./stream-steer-verb.js"; import { streamLifecycleVerbs } from "./stream-lifecycle-verbs.js"; import { streamHaltVerbs } from "./stream-halt-verbs.js"; import { streamModeVerb } from "./stream-mode-verb.js"; import { runNotificationLane } from "./run-notification-lane.js"; import { assertNoRetiredRunnerDeps } from "./retired-runner-deps.js"; import { runLeg } from "./run-leg.js"; import { runTerminalAdoption } from "./run-terminal-adoption.js"; import { runSettleAndTeardown } from "./run-settle-and-teardown.js"; import { runIdentityWiring } from "./run-identity-wiring.js"; import { runTelemetryAndBudgetSeats } from "./run-telemetry-and-budget-seats.js"; import { runAttachmentSeats } from "./run-attachment-seats.js"; import { runToolMountFacts } from "./run-tool-mount-facts.js"; import { runReasoningSeat } from "./run-reasoning-seat.js"; import { runClockAndContent } from "./run-clock-and-content.js"; import { createToolProgressLane } from "./tool-progress-lane.js"; import { createForwardSinkScope } from "./forward-sink.js"; import { runBrainSinks } from "./run-brain-sinks.js"; import { runCompactionMachinery } from "./run-compaction-machinery.js"; import { runStopAndFinalVerify } from "./run-stop-and-final-verify.js"; import { runRecoveryLanes } from "./run-recovery-lanes.js"; import { runGitLane } from "./run-git-lane.js"; import { thrownText } from "../thrown-value.js"; export class Runner { deps; sessions; sessionLocks = new Map(); sessionReadStates = new SessionReadFileStates(); sessionModeSeats = new SessionModeSeats(); pendingSessionNotifications = new PendingSessionNotifications(); parentConstraintRegistry = new Map(); suspendedEnvReaps = new Map(); locallyClaimedTokens = new Set(); static PARENT_CONSTRAINT_REGISTRY_CAP = 1024; depsSeat; constructor(deps) { assertNoRetiredRunnerDeps(deps); const runner = this; this.depsSeat = { get deps() { return runner.deps; } }; this.deps = withDeploymentAllowedTools(deps); if (this.deps.tiers) { this.deps = { ...this.deps, models: expandTiers(this.deps.models, this.deps.tiers) }; } if (this.deps.contextSettings !== undefined) { this.deps = { ...this.deps, contextSettings: assertContextSettingsWords(assertContextSettingsValue(this.deps.contextSettings, "RunnerDeps"), this.deps.models, "RunnerDeps") }; } if (this.deps.onError) { const rawOnError = this.deps.onError; const reportSinkFailure = (sinkErr, context) => { try { emitTrace(this.deps.tracer, () => ({ kind: "task.onerror_sink_failed", version: 1, sessionId: context.sessionId, phase: context.phase, reason: (thrownText(sinkErr)).slice(0, 256), ts: Date.now(), })); } catch { } }; this.deps = { ...this.deps, onError: (err, context) => { try { const ret = rawOnError(err, context); if (ret !== null && ret !== undefined && typeof ret.then === "function") { Promise.resolve(ret).then(undefined, (sinkErr) => reportSinkFailure(sinkErr, context)); } } catch (sinkErr) { reportSinkFailure(sinkErr, context); } }, }; } if (!this.deps.toolResultStore) { this.deps = { ...this.deps, toolResultStore: new RunnerSharedToolResultStore({ maxTotalChars: 64_000_000 }) }; } this.sessions = deps.sessionStore ?? new TtlSessionStore(); } emitTaskOutcome(outcome) { emitTaskOutcome(this.deps, outcome); } get agentCatalog() { return { ...(this.deps.agents ? { agents: [...this.deps.agents] } : {}), ...(this.deps.builtinAgents !== undefined ? { builtinAgents: this.deps.builtinAgents } : {}), ...(this.deps.models ? { models: { ...this.deps.models } } : {}), ...(this.deps.roles ? { roles: { ...this.deps.roles } } : {}), }; } get contextSettings() { return this.deps.contextSettings; } contextOverlayOf(ref) { return contextOverlayFor(this.deps.contextSettings, ref, this.deps); } get gateBaseline() { const deps = this.deps; const h = deps.hooks; const hooks = h ? { ...(typeof h.preToolUse === "function" ? { preToolUse: (t, i, c) => h.preToolUse(t, i, c) } : {}), ...(typeof h.preToolUse === "function" && h.preToolUseObservational === true ? { preToolUseObservational: true } : {}), ...(typeof h.postToolUse === "function" ? { postToolUse: (t, i, o, c) => h.postToolUse(t, i, o, c) } : {}), ...(typeof h.userPromptSubmit === "function" ? { userPromptSubmit: (p, c) => h.userPromptSubmit(p, c) } : {}), ...(typeof h.stop === "function" ? { stop: (c) => h.stop(c) } : {}), ...(typeof h.postToolUseFailure === "function" ? { postToolUseFailure: (t, i, f, c) => h.postToolUseFailure(t, i, f, c) } : {}), ...(typeof h.postToolBatch === "function" ? { postToolBatch: (b, m, c) => h.postToolBatch(b, m, c) } : {}), ...(typeof h.preCompact === "function" ? { preCompact: (c) => h.preCompact(c) } : {}), ...(typeof h.postCompact === "function" ? { postCompact: (c) => h.postCompact(c) } : {}), ...(typeof h.stopFailure === "function" ? { stopFailure: (c) => h.stopFailure(c) } : {}), ...(typeof h.permissionDenied === "function" ? { permissionDenied: (p) => h.permissionDenied(p) } : {}), ...(h.timeoutMs !== undefined ? { timeoutMs: h.timeoutMs } : {}), } : undefined; const MIRRORED_HOOK_KEYS = [ "preToolUse", "preToolUseObservational", "postToolUse", "userPromptSubmit", "stop", "postToolUseFailure", "postToolBatch", "preCompact", "postCompact", "stopFailure", "permissionDenied", "timeoutMs", ]; const _mirrorIsComplete = true; void _mirrorIsComplete; return { ...(deps.toolPolicy !== undefined ? { toolPolicy: wrapToolPolicy(deps.toolPolicy, (req, signal) => deps.toolPolicy.check(req, signal)), } : {}), ...(hooks !== undefined ? { hooks } : {}), }; } async acquireSessionLock(sessionId) { const prev = this.sessionLocks.get(sessionId) ?? Promise.resolve(); let release; const mine = new Promise((r) => { release = r; }); const chain = prev.then(() => mine); this.sessionLocks.set(sessionId, chain); await prev.catch(() => { }); return () => { release(); void chain.then(() => { if (this.sessionLocks.get(sessionId) === chain) { this.sessionLocks.delete(sessionId); } }); }; } swapDeps(next) { const screened = screenSwappableDeps(next); const { seats, candidate } = screened; const catalogSwap = seats.includes("models"); let expanded; let tiers; const movedEntries = []; if (catalogSwap) { const models = candidate.models; tiers = Object.hasOwn(candidate, "tiers") ? candidate.tiers : this.deps.tiers; expanded = tiers && Object.keys(tiers).length > 0 ? expandTiers({ ...models }, tiers) : { ...models }; for (const [name, nextModel] of Object.entries(expanded ?? {})) { const prior = this.deps.models?.[name]; if (!prior) continue; if (routeKeyOf(prior.baseUrl) !== routeKeyOf(nextModel.baseUrl) && authCarrierFingerprint(prior.headers) === authCarrierFingerprint(nextModel.headers)) { movedEntries.push({ modelId: name, from: displayUrlOf(prior.baseUrl), to: displayUrlOf(nextModel.baseUrl) }); } } } const faceSeats = ["readFace", "writeFace"].filter((seat) => seats.includes(seat)); const priorFaces = { readFace: this.deps.readFace, writeFace: this.deps.writeFace, }; const written = { ...this.deps, ...(expanded !== undefined ? { models: expanded } : {}), ...(catalogSwap && tiers !== undefined ? { tiers } : {}), }; if (catalogSwap && tiers === undefined) delete written.tiers; const writtenFaceSeats = written; for (const seat of faceSeats) { if (candidate[seat] !== undefined) writtenFaceSeats[seat] = candidate[seat]; else delete writtenFaceSeats[seat]; } const priorContextSettings = this.deps.contextSettings; if (seats.includes("contextSettings")) { if (candidate.contextSettings !== undefined) written.contextSettings = candidate.contextSettings; else delete written.contextSettings; } assertContextSettingsWords(written.contextSettings, written.models, "swapDeps"); this.deps = written; if (catalogSwap) { const modelCount = Object.keys(candidate.models).length; deliverEngineNotice(this.deps.onNotice, { code: "config.models_swapped", message: `model catalog swapped: ${modelCount} model(s), ${tiers ? Object.keys(tiers).length : 0} tier binding(s); in-flight tasks finish on their resolved models, new tasks resolve against the new catalog`, detail: { models: modelCount, tiers: tiers ? Object.keys(tiers).length : 0, occurrenceId: uuidv7() }, }); if (movedEntries.length > 0) { const RENDER_CAP = 8; const rendered = movedEntries.slice(0, RENDER_CAP); deliverEngineNotice(this.deps.onNotice, { code: "route.base_url_changed_key_unchanged", message: `model catalog swap moved ${movedEntries.length} entry/entries to a new baseUrl while the Model-visible credential half stayed unchanged: ` + rendered.map((e) => `"${e.modelId}" ${e.from || "(config root)"} → ${e.to || "(config root)"}`).join("; ") + (movedEntries.length > rendered.length ? `; +${movedEntries.length - rendered.length} more` : "") + " — if the provider changed (not just its domain), update the credential reference in the same step", detail: { entries: rendered, total: movedEntries.length, occurrenceId: uuidv7() }, }); } } if (seats.includes("contextSettings")) { const to = candidate.contextSettings; const words = to !== undefined ? Object.keys(to) : []; deliverEngineNotice(this.deps.onNotice, { code: "config.context_settings_swapped", message: `deployment context-window settings swapped: ${words.length} model word(s) declared` + `${priorContextSettings !== undefined ? ` (was ${Object.keys(priorContextSettings).length})` : ""}` + "; unlike every other swappable seat this one is read LIVE — running legs measure their next boundary against the new windows, while their routing stays on the generation they prepared under", detail: { words: words.slice(0, 8), declared: words.length, previousDeclared: priorContextSettings !== undefined ? Object.keys(priorContextSettings).length : 0, occurrenceId: uuidv7() }, }); } for (const seat of faceSeats) { const to = candidate[seat] ?? null; const from = priorFaces[seat] ?? null; const message = `deployment ${seat === "readFace" ? "read" : "write"} face swapped: ${from ?? "(no declaration)"} → ${to ?? "(no declaration)"}; in-flight legs keep the face they prepared under, new legs resolve against the new declaration`; const detail = { from, to, occurrenceId: uuidv7() }; if (seat === "readFace") deliverEngineNotice(this.deps.onNotice, { code: "config.read_face_swapped", message, detail }); else deliverEngineNotice(this.deps.onNotice, { code: "config.write_face_swapped", message, detail }); } } sideQuery(spec) { return runWithReasoningWireFacts(() => { }, () => runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles })); } runTaskStream(spec, resume, internals) { if (resume !== undefined && (typeof resume !== "object" || resume.outcome === undefined)) { throw new TypeError("runTaskStream: `resume` must be a ResumeRun carrying `outcome` — got a value without one. " + "(Note the parameter order: runTaskStream(spec, resume?, internals?) — internals is the THIRD parameter.)"); } const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor); const entryTracer = { tracer: spec.tracer ?? this.deps.tracer }; const queue = new PushQueue(); const detachHub = new ToolDetachHub(); let resultValue; let suggestionsDone = Promise.resolve([]); let handle; let publishReady; const ready = new Promise((res) => { publishReady = res; }); const READY_TIMEOUT_MS = 30_000; const orTimeout = (p) => { let t; const timeout = new Promise((res2) => { t = setTimeout(() => res2(undefined), READY_TIMEOUT_MS); }); return Promise.race([p, timeout]).finally(() => clearTimeout(t)); }; let reapHandle; const acceptedSteerInputs = new Map(); const notifyRef = {}; const captureOptOutRef = {}; const mcpRedialRef = {}; const manualCompactRef = { requested: false, waiters: [] }; const drainManualCompactWaiters = (outcome) => { manualCompactRef.requested = false; for (const w of manualCompactRef.waiters.splice(0)) w.resolve(outcome); }; const runStartedAt = Date.now(); const taskIdRef = { current: spec.taskId, sessionId: spec.sessionId }; let held; let releaseLock; const holdSessionLock = (sessionId) => { if (held === undefined) held = { sessionId, taken: this.acquireSessionLock(sessionId).then((release) => void (releaseLock = release)) }; else if (held.sessionId !== sessionId) return Promise.reject(Object.assign(new Error(`internal invariant: a leg holds one session lock — it holds "${held.sessionId}" and was asked for "${sessionId}"`), { code: "internal.session_lock_second_id" })); return held.taken; }; const namedSessionId = spec.sessionId; const run = (async () => { if (namedSessionId) await holdSessionLock(namedSessionId); try { await this.runLocked({ spec, queue, holdSessionLock: namedSessionId ? undefined : holdSessionLock, setResult: (r) => { resultValue = r; const pcs = internals?.inheritedGate?.parentConstraints; if (resume !== undefined) this.locallyClaimedTokens.delete(resume.cp.token); const rToken = r.terminal.kind === "paused" ? r.terminal.token : undefined; if (resume !== undefined && rToken !== resume.cp.token) { this.parentConstraintRegistry.delete(resume.cp.token); this.suspendedEnvReaps.delete(resume.cp.token); } if (r.terminal.kind === "paused" && PAUSE_REGISTRY[r.terminal.gate.kind].taskStatus === "suspended" && pcs !== undefined && pcs.length > 0) { if (this.parentConstraintRegistry.size >= Runner.PARENT_CONSTRAINT_REGISTRY_CAP) { const oldest = this.parentConstraintRegistry.keys().next().value; if (oldest !== undefined) this.parentConstraintRegistry.delete(oldest); } this.parentConstraintRegistry.set(r.terminal.token, pcs); } }, onSuggestions: (p) => { suggestionsDone = p; }, onReady: (h) => { handle = h; publishReady(h); }, onSuspend: (s) => { reapHandle = s; this.suspendedEnvReaps.set(s.token, s); }, manualCompactRef, taskIdRef, resume, internals: { ...(internals ?? {}), detachHub, forwardScope: createForwardSinkScope(this.deps.onNotice) }, notifyRef, captureOptOutRef, mcpRedialRef, entryActor, entryTracer, }); } finally { releaseLock?.(); publishReady(handle); manualCompactRef.closed = true; if (manualCompactRef.waiters.length > 0) manualCompactRef.emitMooted?.("task_ending"); manualCompactRef.emitMooted = undefined; drainManualCompactWaiters("mooted"); } })(); const live = { get resultValue() { return resultValue; }, set resultValue(v) { resultValue = v; }, get handle() { return handle; }, get reapHandle() { return reapHandle; }, }; const settled = run.catch(streamSettleBackstop({ spec, resume, queue, taskIdRef, entryTracer, runStartedAt, live, runner: this.depsSeat, sessions: this.sessions, locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry, suspendedEnvReaps: this.suspendedEnvReaps, }).onRunRejected); void settled.then(() => acceptedSteerInputs.clear(), () => acceptedSteerInputs.clear()); const steeringError = (msg, code = "steering.not_running") => { const e = new Error(`cannot steer: ${msg}`); e.code = code; return e; }; const { reapSuspended } = streamReap({ live, runner: this.depsSeat, locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry, suspendedEnvReaps: this.suspendedEnvReaps, }); const { steer } = streamSteerVerb({ spec, internals, queue, acceptedSteerInputs, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, steeringError, runner: this.depsSeat }); const { notify, optOutMemoryCapture, reconnectMcpServer, compact, detach, interrupt } = streamLifecycleVerbs({ live, ready, orTimeout, steeringError, notifyRef, captureOptOutRef, mcpRedialRef, manualCompactRef, detachHub }); const { halt, destroy } = streamHaltVerbs({ spec, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, run, reapSuspended, runner: this.depsSeat }); const { setPermissionMode } = streamModeVerb({ live, ready, orTimeout, steeringError }); return { [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator](), result: async () => { await settled; return resultValue; }, suggestions: async () => { await run.catch(() => { }); return suggestionsDone.catch(() => []); }, steer, notify, optOutMemoryCapture, setPermissionMode, reconnectMcpServer, compact, detach, interrupt, halt, destroy, }; } runLocked(input) { const { spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, holdSessionLock, resume, internals, notifyRef, captureOptOutRef, mcpRedialRef, entryActor, entryTracer } = input; return runNotificationLane({ spec, queue, resume, internals, taskIdRef, holdSessionLock, runner: this.depsSeat, self: this, sessions: this.sessions, sessionReadStates: this.sessionReadStates, sessionModeSeats: this.sessionModeSeats, pendingSessionNotifications: this.pendingSessionNotifications, prepareTask, next: ({ prepared, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, modelCatalog, notificationLane, unsubscribeTaskNotifications }) => { const { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, internalsNotifier, undrainedUserAtEnd, userInputLostAtEnd } = runIdentityWiring({ spec, queue, prepared, internals, taskIdRef, onReady, manualCompactRef, notifyRef, captureOptOutRef, mcpRedialRef, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, notificationLane, pendingSessionNotifications: this.pendingSessionNotifications, runner: this.depsSeat, }); const rs = createRunState(); const { noteUnevaluablePriceTable } = runTelemetryAndBudgetSeats({ spec, prepared, resume, stats, taskIdRef, entryTracer, modelCatalog, runSourceTaskId, rs, runner: this.depsSeat, sessions: this.sessions }); return runAttachmentSeats({ spec, prepared, resume, rs, runner: this.depsSeat, next: () => { const { todoToolMounted, taskToolsMounted, writeFamilyOf, toolStartAt, startedToolCallIds } = runToolMountFacts({ spec, prepared, rs }); const { observeReasoningWireFacts, reasoningResolution } = runReasoningSeat({ prepared, rs, taskIdRef }); const { effectiveTimeoutMs, walltimeMonotonicDeadline, timeout, pushContent, ownCommittedTailRef, emitCommitted } = runClockAndContent({ spec, prepared, queue, internals, rs, ident, parentToolCallId, startedToolCallIds }); const toolProgress = createToolProgressLane(pushContent, ident); prepared.toolProgressRef.current = toolProgress; const { subagentName, withBrainSinks } = runBrainSinks({ queue, internals, rs, ident, parentToolCallId, observeReasoningWireFacts }); const toolLabels = new Map(prepared.tools.flatMap((t) => (t.label !== undefined && t.label !== t.name ? [[t.name, t.label]] : []))); for (const orphan of prepared.wakeRecovered) { queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan, prepared.structuredProjector), ...ident() }); emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId); } const postToolBatchHook = (spec.hooks ?? this.deps.hooks)?.postToolBatch; const batchArgs = postToolBatchHook ? new Map() : undefined; rs.turn.toolBatch = []; const { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, announceWorkspaceMove } = createHarnessHandlers({ prepared, stats, rs, deps: { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs, internalsNotifier, runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info), onNotice: this.deps.onNotice }, }, }); const { compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions } = runCompactionMachinery({ spec, prepared, rs, stats, noteUnevaluablePriceTable, runner: this.depsSeat }); runStopAndFinalVerify({ spec, prepared, queue, rs, stats, ident, walltimeMonotonicDeadline, runner: this.depsSeat }); runRecoveryLanes({ spec, prepared, queue, rs, ident, compactionBrain, compactionBreaker, windowSafetyOptions, runner: this.depsSeat }); const rapidRefill = createRapidRefillState(); const drainManualCompact = (outcome) => { manualCompactRef.requested = false; for (const w of manualCompactRef.waiters.splice(0)) w.resolve(outcome); }; const onTurnBoundary = createTurnBoundary({ prepared, stats, rs, deps: { spec, queue, loopLatch, manualCompactRef, todoToolMounted, taskToolsMounted, walltimeMonotonicDeadline, timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact, runnerHooks: { onError: this.deps.onError }, runner: this.depsSeat, }, }); const { flushGitMirror, unsubGitRetry, unsubBoundary, unsub } = runGitLane({ prepared, queue, rs, ident, onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, onTurnBoundary, runner: this.depsSeat }); return runLeg({ spec, queue, prepared, resume, rs, isDelegatedChild: internals?.isDelegatedChild === true, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner: this.depsSeat, next: ({ final, threw, abortedLive, userInterruptedLive, strandedHumanAnswers }) => runTerminalAdoption({ spec, queue, prepared, rs, stats, ident, emitCommitted, startedToolCallIds, toolLabels, timeout, compactionBrain, compactionBreaker, windowSafetyOptions, undrainedUserAtEnd, userInputLostAtEnd, final, threw, abortedLive, userInterruptedLive, runner: this.depsSeat, sessions: this.sessions, next: ({ threw, comp, answerSettled, owedUserInput }) => runSettleAndTeardown({ spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, compactionBreaker, owedUserInput, setResult, onSuggestions, onSuspend, runner: this.depsSeat, sessions: this.sessions, }), }), }); }, }); }, }); } async runTask(spec, internals) { const stream = this.runTaskStream(spec, undefined, internals); for await (const _ of stream) { void _; } return stream.result(); } async resume(token, outcome, taskConfig, internals) { const stream = await this.resumeStream(token, outcome, taskConfig, internals); for await (const _ of stream) { void _; } return stream.result(); } async resumeStream(token, outcome, taskConfig, internals) { return await resumeAdmission({ token, outcome, taskConfig, internals, runner: this.depsSeat, parentConstraintRegistry: this.parentConstraintRegistry, next: (admitted) => this.resumeLadder(token, outcome, admitted), }); } async resumeLadder(token, outcome, admitted) { const { config, internals, store, cp, outcomeGate, resumeSignal, wakeMessage } = admitted; const { plainReviewOutcome, plainParkOutcome } = resumeReviewOutcome({ cp, outcome, outcomeGate, wakeMessage }); const { plainPolicyOutcome: capturedPolicyOutcome, redeemedAnswer } = resumePolicyOutcome({ cp, outcome, config, runner: this.depsSeat }); const { resumeInternals } = resumeCheckpointScreen({ cp, resumeSignal, internals, runner: this.depsSeat }); const { recheckGovernanceWindow, windowEntrance } = resumeInternalsAndConfig({ cp, config, internals, outcomeGate, runner: this.depsSeat }); if (windowEntrance !== undefined) await windowEntrance; return await resumeApply({ cp, config, internals, outcomeGate, wakeMessage, resumeSignal, recheckGovernanceWindow, plainPolicyOutcome: capturedPolicyOutcome, plainReviewOutcome, plainParkOutcome, runner: this.depsSeat, effectiveDelegationFacts, next: ({ wakeHookContext, plainPolicyOutcome, outcomeForStore }) => resumePreflight({ token, cp, resumeSignal, recheckGovernanceWindow, store, runner: this.depsSeat, locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry, suspendedEnvReaps: this.suspendedEnvReaps, next: () => resumeClaim({ token, cp, store, outcomeForStore, locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry, next: () => this.resumeHandoff({ token, cp, store, config, internals, resumeInternals, plainPolicyOutcome, redeemedAnswer, outcomeForStore, wakeMessage, wakeHookContext, resumeSignal }), }), }), }); } async resumeHandoff(claimed) { const { token, cp, store, config, internals, resumeInternals, plainPolicyOutcome, redeemedAnswer, outcomeForStore, wakeMessage, wakeHookContext, resumeSignal } = claimed; let consumeFlipDone = false; if (internals?.afterCheckpointResolve !== undefined) { await internals.afterCheckpointResolve(); consumeFlipDone = true; } const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined ? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, config.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions)) : undefined; const spec = { ...config, ...((config.principal || undefined) === undefined && (cp.principal || undefined) !== undefined ? { principal: cp.principal } : {}), objective: "", sessionId: cp.sessionId, requireExistingSession: true, preemptSignal: config.preemptSignal?.aborted ? undefined : config.preemptSignal, ...(answerFace !== undefined ? { onQuestion: answerFace } : {}), }; const reopenFn = store.reopen?.bind(store); const recordExecutionOutcome = (gate) => store.recordExecutionOutcome(token, cp.scope, gate); const onEnvRestoreFailed = reopenFn ? async (reason) => { const reopened = await reopenFn(token, cp.scope, reason); if (!reopened) { throw new CheckpointError("checkpoint.reopen_failed", "checkpoint could not be reopened for a retry (missing, expired, or changed concurrently) — the resume failure is terminal"); } } : undefined; if (resumeSignal?.aborted === true && consumeFlipDone) { throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted after its row was already claimed — the checkpoint stays consumed and the caller's rollback settles the outcome-unknown terminal (reopening here would strand a pending approval no redemption path can reach)"); } if (resumeSignal?.aborted === true) { let reopenOutcome = "refused"; if (onEnvRestoreFailed !== undefined) { try { await onEnvRestoreFailed("env_failed"); reopenOutcome = "reopened"; } catch (err) { reopenOutcome = errorCodeOf(err) === "checkpoint.reopen_failed" ? "refused" : "unknown"; } } throw reopenOutcome === "reopened" ? new CheckpointError("checkpoint.resume_aborted", "the resume was aborted before the approved action could run — the checkpoint was reopened, so the same approval can be resumed with a live signal") : reopenOutcome === "refused" ? new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run AND the store refused to reopen the checkpoint — the approval is terminally consumed and the suspended work was not executed; a retry needs a fresh approval") : new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run and the reopen attempt FAILED IN FLIGHT — the checkpoint's state is unprovable from here: it may already be pending again. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead (the approved action did NOT run either way)"); } return this.runTaskStream(spec, { cp, outcome: outcomeForStore, onEnvRestoreFailed, recordExecutionOutcome, ...(wakeMessage !== undefined ? { wakeMessage } : {}), ...(wakeHookContext !== undefined ? { wakeMessageHookContext: wakeHookContext } : {}) }, resumeInternals); } } export function runTask(spec, deps) { return new Runner(deps).runTask(spec); }