/** * Durable agent submission lifecycle storage. * * One contract for every backend: the in-memory reference store, the Node * SQLite store, and the Postgres store (also used by Databricks Lakebase) * implement this interface with identical observable behavior. The * per-method invariants below are written in terms of observable behavior, * not storage primitives, so a non-SQL backend implements them natively. * Where a method is described as atomic, concurrent callers must never both * observe success; whether that is achieved with transactions, conditional * updates, or unique indexes is the implementation's choice. Verify an * implementation with `defineSubmissionStoreContractTests` from * `submission-store-contract.ts`. */ import type { DeliveredMessage } from './delivered-message.js'; import type { DispatchInput } from './dispatch.js'; import type { FabricActor, JsonValue } from './types.js'; /** Default maximum total attempts before terminalization. */ export declare const DURABILITY_DEFAULT_MAX_ATTEMPTS = 10; /** Default submission timeout in milliseconds (one hour). */ export declare const DURABILITY_DEFAULT_TIMEOUT_MS = 3600000; /** Default lease duration for submission ownership in milliseconds (30 seconds). */ export declare const LEASE_DURATION_MS = 30000; /** * One admitted agent submission — the persisted operational payload for both * transports. `kind` records how the submission arrived (`'dispatch'` via * `dispatch()`, `'direct'` via the agent HTTP route); a dispatch's * `submissionId` is the public `dispatchId` from its receipt. */ export interface AgentSubmissionInput { readonly kind: 'dispatch' | 'direct'; readonly submissionId: string; /** Persistent agent name. */ readonly agent: string; /** Instance id of the target persistent agent. */ readonly id: string; /** Named session within the instance. */ readonly session: string; /** The normalized message delivered to the session. */ readonly message: DeliveredMessage; /** Immutable instance creation data, when supplied on first contact. */ readonly initialData?: JsonValue; /** Persistent instance incarnation precondition captured at admission. */ readonly uid?: string | null; /** Resolved instance generation stamped by trusted admission. */ readonly instanceUid?: string; /** False for transports whose response envelope must remain request-isolated. */ readonly joinWhileBusy?: boolean; /** Admission timestamp in epoch milliseconds. */ readonly acceptedAt: number; readonly tenantId?: string; readonly actor?: FabricActor; /** Admission-resolved static agent durability, persisted for crash recovery. */ readonly durability?: AgentSubmissionDurability; } /** * The harness identity string (`agent:::`) targeted by a * submission input. This is the {@link persistentStoreSessionId} of the * addressed instance session and the per-session FIFO key of the store. */ export declare function submissionStoreSessionId(input: Pick): string; /** Map a {@link DispatchInput} onto the persisted submission input shape. */ export declare function createDispatchAgentSubmissionInput(dispatch: DispatchInput): AgentSubmissionInput; /** Mint a direct-prompt submission input with a fresh submission id. */ export declare function createDirectAgentSubmissionInput(options: { agent: string; id: string; session?: string; message: DeliveredMessage; initialData?: JsonValue; uid?: string | null; joinWhileBusy?: boolean; tenantId?: string; actor?: FabricActor; durability?: AgentSubmissionDurability; }): AgentSubmissionInput; /** * Minimal canonical settlement record for a direct submission. The * conversation-stream phase reuses this shape as the durable terminal record * a reconnecting waiter observes. */ export interface SubmissionSettledRecord { readonly type: 'submission_settled'; readonly submissionId: string; readonly outcome: 'completed' | 'failed' | 'aborted'; readonly resultText?: string; readonly error?: string; /** Host submission whose coalesced response answered this joined delivery. */ readonly answeredBySubmissionId?: string; } export type AgentSubmissionStatus = 'queued' | 'running' | 'joining' | 'joined' | 'terminalizing' | 'settled'; export interface AgentSubmission { readonly sequence: number; readonly submissionId: string; /** Harness identity string `agent:::` — the per-session FIFO key. */ readonly storeSessionId: string; readonly kind: 'dispatch' | 'direct'; readonly input: AgentSubmissionInput; readonly status: AgentSubmissionStatus; readonly acceptedAt: number; readonly canonicalReadyAt: number | null; readonly attemptId?: string; readonly inputAppliedAt?: number; readonly recoveryRequestedAt?: number; /** * When set, abort was requested for this submission. This is a durable * abort+recovery *signal*, NOT a terminal classification: the aborted * outcome is read only from the settlement (a direct `submission_settled` * record with `outcome: 'aborted'`). A submission that completes or fails * while this is set still settles completed/failed — the flag merely tells * the owning attempt to stop and tells recovery to settle aborted rather * than retry. May be present while `queued` (an abort arrived before the * submission was ever claimed). */ readonly abortRequestedAt?: number; readonly startedAt?: number; /** Running host whose live response absorbed this delivery. */ readonly joinedInto?: string; readonly error?: string; readonly attemptCount: number; readonly maxRetry: number; readonly timeoutAt: number; readonly ownerId?: string; readonly leaseExpiresAt: number; } export interface SubmissionSettlementObligation { readonly submissionId: string; readonly storeSessionId: string; readonly attemptId: string; readonly recordId: string; readonly record: SubmissionSettledRecord; } export interface SubmissionAttemptRef { readonly submissionId: string; readonly attemptId: string; } export interface SubmissionClaimRef extends SubmissionAttemptRef { readonly ownerId: string; readonly leaseExpiresAt: number; } export interface AgentSubmissionDurability { readonly maxRetry: number; readonly timeoutAt: number; } /** * Harness-owned durable evidence that a submission attempt was started and * has not yet settled. A coordinator inserts a marker immediately before * starting an attempt and deletes it when the attempt settles; * reconciliation treats a fresh marker as proof that the attempt may still * be running and must not be reconciled as interrupted. */ export interface AgentAttemptMarker { readonly submissionId: string; readonly attemptId: string; readonly createdAt: number; } export interface AgentDispatchReceipt { readonly submissionId: string; readonly acceptedAt: number; } export type AgentDispatchAdmission = { readonly kind: 'submission'; readonly submission: AgentSubmission; } | { readonly kind: 'retained_receipt'; readonly receipt: AgentDispatchReceipt; } | { readonly kind: 'conflict'; }; /** * Durable submission lifecycle storage. * * Stability: the lease method group mirrors the durable-execution engine and * is subject to change until 1.0. This applies to every backend equally. */ export interface AgentSubmissionStore { /** Return the submission, or `null` when the id is unknown. */ getSubmission(submissionId: string): Promise; /** True while any submission is queued, running, or terminalizing. */ hasUnsettledSubmissions(): Promise; /** * Queued submissions that are each the oldest unsettled submission of * their session, in admission order. At most one runnable head exists * per session; later queued work in the same session is excluded until * everything admitted before it has settled. */ listRunnableSubmissions(): Promise; /** All queued submissions without canonical readiness, in admission order. */ listUnreadySubmissions(): Promise; /** All running submissions, in admission order. */ listRunningSubmissions(): Promise; /** Direct settlement obligations reserved but not yet finalized. */ listPendingSubmissionSettlements(): Promise; /** * Recovery handoff: atomically move a running submission from `attempt` * to `nextAttemptId`, increment `attemptCount`, clear any pending recovery * request, and (when given) install the new lease. Returns the updated * submission, or `null` — without writing — when the submission is not * running under `attempt`. */ replaceSubmissionAttempt(attempt: SubmissionAttemptRef, nextAttemptId: string, lease?: { ownerId: string; leaseExpiresAt: number; }): Promise; /** * Idempotent admission keyed by dispatch id. An exact replay (same id, * same payload) returns the already-admitted submission; the same id * with a different payload returns `conflict`. */ admitDispatch(input: DispatchInput): Promise; /** * Admit a direct prompt (`input.kind === 'direct'`) as a queued submission. * Idempotent for an exact replay of the same submission id and payload. */ admitDirect(input: AgentSubmissionInput): Promise; /** * Mark a newly admitted queued submission's canonical conversation as materialized. * Idempotent while queued; returns `null` when the submission is missing or no longer queued. */ markSubmissionCanonicalReady(submissionId: string): Promise; /** * Atomic compare-and-set. Transition the submission from queued to * running ONLY when it is currently queued and is the runnable head of * its session (no earlier unsettled submission in the same session), * recording the attempt id, owner, lease expiry, and start time, * incrementing `attemptCount`, resetting `maxRetry` to the system * default, and initializing `timeoutAt` when still unset (a previously * initialized timeout is preserved across requeue/reclaim). Returns the * claimed submission, or `null` when any condition fails. Two concurrent * claims for the same submission must never both succeed. */ claimSubmission(claim: SubmissionClaimRef): Promise; /** * Record once that the submission's input was canonically applied, * installing the supplied durability (or defaults) on first application. * Gated on a running submission owned by `attempt`; otherwise `false`. */ markSubmissionInputApplied(attempt: SubmissionAttemptRef, durability?: AgentSubmissionDurability): Promise; /** * Stamp `recoveryRequestedAt` once. Gated on a running submission owned * by `attempt`; otherwise `false`. */ requestSubmissionRecovery(attempt: SubmissionAttemptRef): Promise; /** * Record an abort request for every unsettled submission in a session. * Atomically stamps `abortRequestedAt` (COALESCE — first request wins) on * each `queued` or `running` submission with the given `storeSessionId` and * returns their submission ids. It does NOT settle anything and does NOT * change `status`: terminal settlement always happens through an * attempt-based path (the pre-execution abort check when a queued submission * is claimed, the in-flight abort settle, or the recovery abort branch) so a * durable canonical terminal record always exists. `terminalizing` and * `settled` submissions are left untouched (a committed outcome must not be * overridden). Idempotent; returns an empty array when nothing is unsettled. */ requestSessionAbort(storeSessionId: string): Promise; /** * Return a running submission to queued — clearing its attempt, owner, * and lease — ONLY while input has not been applied and `attempt` owns * the submission; otherwise `false`. */ requeueSubmissionBeforeInputApplied(attempt: SubmissionAttemptRef): Promise; /** * Atomically reserve the exact canonical settlement record as an obligation. * Only a running direct submission owned by `attempt` may transition to * terminalizing. Exact retries return the existing obligation; conflicting * record identities or payloads return `null`. */ reserveSubmissionSettlement(attempt: SubmissionAttemptRef, settlement: { recordId: string; record: SubmissionSettledRecord; }): Promise; /** Finalize an owned terminalizing submission after its canonical record exists. */ finalizeSubmissionSettlement(attempt: SubmissionAttemptRef, recordId: string): Promise; /** * Settle the submission successfully. Gated on a running submission * owned by `attempt`: a stale attempt or an already-settled submission * returns `false` and preserves the first terminal state. */ completeSubmission(attempt: SubmissionAttemptRef): Promise; /** * Settle the submission with an error message. Same gating as * {@link completeSubmission}: the first terminal state wins. */ failSubmission(attempt: SubmissionAttemptRef, error: unknown): Promise; /** Claim the canonical-ready queued prefix behind a live host attempt. */ claimJoinableSubmissions(host: SubmissionAttemptRef, agentName: string): Promise; /** Confirm that a claimed delivery's canonical input is durable. */ finalizeJoinedSubmission(host: SubmissionAttemptRef, submissionId: string): Promise; /** Return an unconfirmed claimed delivery to the queue. */ revertJoiningSubmission(host: SubmissionAttemptRef, submissionId: string): Promise; /** Unsettled deliveries attached to a host, in admission order. */ listJoinedSubmissions(hostSubmissionId: string): Promise; /** Settle one confirmed join with the host's outcome. */ settleJoinedSubmission(host: SubmissionAttemptRef, submissionId: string, outcome: 'completed' | 'failed' | 'aborted', error?: unknown): Promise; /** * Durably record that the attempt was started. Idempotent: re-inserting * the same (submissionId, attemptId) keeps the original `createdAt`. */ insertAttemptMarker(attempt: SubmissionAttemptRef): Promise; /** Delete the marker matching both ids exactly; a no-op when absent. */ deleteAttemptMarker(attempt: SubmissionAttemptRef): Promise; /** All attempt markers. */ listAttemptMarkers(): Promise; /** * Delete every settled submission and associated lifecycle record for one * persistent store session. Implementations must reject deletion while any * submission for the session is unsettled. */ deleteSessionSubmissions?(storeSessionId: string): Promise; /** * Extend the lease expiry (now + `LEASE_DURATION_MS`) for each listed * submission that is running AND owned by `ownerId`. Submissions owned * by another coordinator, settled, or unknown are silently skipped. */ renewLeases(ownerId: string, submissionIds: string[]): Promise; /** * Running submissions whose lease has expired (a positive * `leaseExpiresAt` in the past). Queued and settled submissions are * never returned. */ listExpiredSubmissions(): Promise; } /** * Context needed for submission payload validation. * * Implementations extract these fields from their storage-specific * row/document type before calling {@link isSubmissionPayload}. */ export interface SubmissionPayloadContext { readonly kind: string; readonly submissionId: string; readonly storeSessionId: string; readonly acceptedAt: number; } /** * Validate that a parsed JSON payload matches the expected submission shape. * * Used after deserializing a persisted payload to verify the object is a * well-formed {@link AgentSubmissionInput} that is consistent with the stored * submission metadata. Both dispatch and direct payloads carry the same * `message: DeliveredMessage` field — validated identically here regardless * of transport `kind`. */ export declare function isSubmissionPayload(input: unknown, ctx: SubmissionPayloadContext): input is AgentSubmissionInput; /** The queued row that {@link admitSubmissionWithBackend} writes on first admission. */ export interface SubmissionInsertRow { readonly submissionId: string; readonly storeSessionId: string; readonly kind: 'dispatch' | 'direct'; /** The serialized {@link AgentSubmissionInput}. */ readonly payload: string; readonly acceptedAt: number; } /** * The minimal shape {@link admitSubmissionWithBackend} needs from a persisted * submission row: the transport `kind` and persisted `payload` it compares * against the incoming admission. `payload` may be the serialized JSON string * or an already-deserialized object (e.g. a Postgres JSONB column). */ export interface SubmissionAdmissionRow { readonly kind?: unknown; readonly payload?: unknown; } /** * Storage callbacks for {@link admitSubmissionWithBackend}. * * Every callback runs inside the transaction the caller has already opened * (or the backend's equivalent atomicity scope). Callbacks may return plain * values (synchronous backends) or native `Promise`s — non-native thenables * are not supported. */ export interface SubmissionAdmissionBackend { /** Look up a retained dispatch receipt. Only consulted for `kind: 'dispatch'`. */ getDispatchReceipt(submissionId: string): AgentDispatchReceipt | null | Promise; /** Insert the queued submission row, ignoring a duplicate `submissionId`. */ insertIfAbsent(row: SubmissionInsertRow): void | Promise; /** Read back the submission row for `submissionId`, if present. */ getExisting(submissionId: string): Row | undefined | Promise; /** Parse a persisted row into an {@link AgentSubmission}. */ parseSubmission(row: Row): AgentSubmission; } /** * Shared submission admission algorithm for row-oriented backends: * dispatch-receipt check → insert-or-ignore → read-back → payload compare * (idempotent replay vs. conflict). The message payload is stored as JSON * verbatim; payload identity is deep JSON equality, so a backend that * normalizes stored JSON (e.g. Postgres JSONB key ordering) still recognizes * an exact replay. * * The caller owns transaction scoping — invoke this inside one transaction * and pass callbacks bound to it. When every callback is synchronous the * result is returned synchronously, so the algorithm also fits synchronous * backends. */ export declare function admitSubmissionWithBackend(input: AgentSubmissionInput, backend: SubmissionAdmissionBackend): AgentDispatchAdmission | Promise; /** Structural equality over JSON values (objects compared key-order-insensitively). */ export declare function jsonDeepEqual(a: unknown, b: unknown): boolean; //# sourceMappingURL=submission-store.d.ts.map