UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

88 lines (87 loc) • 6.66 kB
/** * PrepareTask's session acquire+reconcile phase. Acquire the session * and, when resuming, reconcile any interrupted (orphan) tool call before the turn. Both are retried * together on a F2 optimistic-lock conflict: a concurrent writer on another instance can move the * leaf during reconcile's append, and the fix can't be applied to a now-stale in-memory view — so we * drop it and **re-wake**. Bounded; exhaustion → the conflict propagates and the task fails with * `errorCode="conflict"` for the caller to retry. The retry loop's WHOLENESS is the T4 invariant * (resume-at validation + rebranch + reconcile re-run as one unit against the fresh view) — pinned * in `prepare-task-phase-pins`. * * Ownership: on SUCCESS the acquired session's ownership transfers whole * to the caller (the driver's forgetOnThrow / the Runner's finish own the cleanup from then on); * every failure path INSIDE this phase drops its own cached view first (`forget`, never `release` — * audit B-17: release deletes the history on the default store), so a throw here never strands a * cached view and never hands out a half-acquired resource. * * Throws pass through unchanged: `resume.session_not_found` (requireExisting fail-loud mapping), * the `resume_at.*` target-validation refusals, and — after the bounded re-wake budget — the * session conflict itself. A throw after a `resumeAt` branch landed first writes that branch back * (`settleBranchWriteBack`); on success the branch is handed over with the session. */ import { StoredSession } from "../session.js"; import type { AcquiredSession, SessionStore } from "../session.js"; import { type RecoveredOrphan } from "../session-reconcile.js"; import type { TaskSpec, ToolEffect } from "../types.js"; import type { PrepareResume } from "./contracts.js"; import { type ResumeAtBranch } from "./branch-writeback.js"; export interface PrepareAcquireReconcileInput { /** borrowed-mutable (service port) — the deployment's session store. This phase's verbs on it are * the acquire/forget half of the session resource protocol: `acquire` per attempt, `forget` to * drop a stale cached view on a lost CAS and on the throw path (never `release` — B-17). */ sessions: SessionStore; /** borrowed-readonly — the REBOUND spec's four session-addressing fields (窄输入: exactly what the * slice reads; a new read here must widen this Pick). `sessionId`/`requireExistingSession` drive * the acquire, `resumeAt`/`resumeAtMode` the §E18 rebranch validation. */ spec: Pick<TaskSpec, "sessionId" | "requireExistingSession" | "resumeAt" | "resumeAtMode">; /** borrowed-readonly — the durable-resume seats this phase reads: `leafId` (rewind to the * suspension point BEFORE reconcile) and `suspendedBatch` (wake-reconcile SKIPS those calls — * they are resumed, not crash-interrupted). Absent for a fresh/non-durable run. */ resume: Pick<PrepareResume, "leafId" | "suspendedBatch"> | undefined; /** borrowed-readonly here — the safety scan's effect registry (its mutation-owner table lives on * {@link import("./prepare-safety-scan.js").PrepareSafetyScanResult}); reconcile classifies an * orphaned call's retry-safety by it. */ toolEffects: Map<string, ToolEffect>; /** borrowed-readonly — subagent transcript persistence: the trusted spawner's placement * declaration for THIS run's session ({@link RunInternals.sessionPlacement}), forwarded verbatim * into the create-form acquire so the store creates the child's transcript into its subagent * partition. Never combined with `requireExistingSession` semantics: the require-existing form * re-opens and placement is first-write immutable, so it rides only the create-capable arm. */ placement?: import("../session.js").SessionPlacement; /** borrowed-readonly — the durable-park TOPOLOGY gap sentence for this * deployment (`durableParkGapFor`), or `undefined` when the topology is whole / does not apply. * Read ONLY on the fail-loud missing-session path below, where it turns a symptom into a named * absent seat; nothing in the phase's control flow depends on it. Computed by the driver (which * holds `deps`) rather than here, keeping this slice's input the narrow Pick its contract says. */ durableParkGap?: string; /** borrowed-readonly — the incident channel for this phase's own cleanup legs: a `resumeAt` branch it could not write back, a forget that did not settle. */ report?: (err: Error, sessionId: string) => void; } /** The phase's outputs (four-class form). All six are fresh bindings — * the driver destructures them into consts, so a consumer moved ahead of this call is a lexical * error, which is the static guarantee the extraction keeps. */ export interface PrepareAcquireReconcileResult { /** owned (resource handover) — the store's acquired session. Handover happens ONLY on return: * every throw inside the phase already forgot its own cached view. */ acquired: AcquiredSession; /** owned — the conflict-proxy-wrapped view over `acquired`'s storage (engine-internal: prepare * always wraps storage in the concrete built-in — the epoch pin needs its * append/getPromptEpoch). Aliases `acquired.session`'s storage through the proxy. */ session: StoredSession; /** owned cell — flipped to `hit: true` by the conflict-detecting proxy installed inside `session` * whenever a RUN-time write loses the optimistic lock (identity is contract: the proxy closes * over this exact object; the run loop reads it into `errorCode="conflict"`). */ conflictRef: { hit: boolean; }; /** owned — scan-1/A5: what the wake/crash reconcile closed, handed to the run loop so it can mint * the matching stream frames (see `Prepared.wakeRecovered`). Written by the acquire attempt that * SUCCEEDS (a conflict-losing attempt re-wakes and re-derives the whole set from the fresh * branch), and stays empty for a fresh session / a clean one with nothing to reconcile. */ wakeRecovered: RecoveredOrphan[]; /** owned (handed over with the session) — the `resumeAt` branch this phase wrote, absent when it wrote * none; the driver's throw cleanup writes it back on any later prepare refusal. */ branch: ResumeAtBranch | undefined; } /** The B-3 phase body — the acquire+reconcile slice, verbatim (see the module header for the contract). */ export declare function prepareAcquireReconcile(input: PrepareAcquireReconcileInput): Promise<PrepareAcquireReconcileResult>;