@sema-agent/core
Version:
Stateless, task-oriented AI agent core
631 lines (630 loc) • 38.1 kB
JavaScript
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);
}