eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
221 lines • 10.8 kB
TypeScript
import type { Event, SerializedData, StartedStep, World } from '#compiled/@workflow/world/index.js';
import type { PayloadKey } from '../serialization/encryption.js';
import { ReplayRecoveryReporter } from './replay-recovery-reporter.js';
import { type ResumeTtrTracking } from './resume-latency.js';
import { type StepLatencyTracking } from './step-latency.js';
export declare const DEFAULT_STEP_MAX_RETRIES = 3;
export declare const STEP_STREAM_DRAIN_TIMEOUT_MS = 30000;
export declare function getStepStreamDrainTimeoutMs(): number;
export interface StepExecutorParams {
world: World;
workflowRunId: string;
/** Deployment that owns the workflow run, for forwarded writable streams. */
workflowDeploymentId?: string;
workflowName: string;
workflowStartedAt: number;
/** Request ID of the invocation executing this step, when provided by its queue. */
requestId?: string;
/** Root run id of this run's lineage, carried into the step context. */
rootRunId?: string;
stepId: string;
stepName: string;
encryptionKey?: PayloadKey;
/**
* The workflow run's specVersion, used to gate payload compression.
* Step outputs/errors are only compressed when the run is marked as
* compression-capable (specVersion >= 5).
*/
runSpecVersion?: number;
/**
* Lazy step start: the already-dehydrated step input. When provided, the
* `step_started` event carries this input so the world creates the step on
* the fly (no separate `step_created` round-trip). Set by the owned-inline
* path for the step whose `step_created` the suspension handler deferred.
* The world's atomic create-claim is the exactly-one-owner gate: losing it
* surfaces as `EntityConflictError` → `{ type: 'skipped' }`, so a handler
* that did not win the create never runs the body. Omitted on every other
* path, where the step already has a `step_created` and `step_started`
* carries no payload (the legacy contract).
*/
lazyStepInput?: SerializedData;
/**
* Pre-claimed inline start: the suspension handler committed (or lost) this
* step's `step_created` + `step_started` pair inside its batched fan-out
* write, so the start this executor would otherwise send has already been
* decided. `owned: false` returns `{ type: 'skipped' }` before any write,
* the same outcome as losing the lazy claim's atomic create. `owned: true`
* skips both start paths (no start write at all) and runs the body against
* the claimed step. Mutually exclusive with `lazyStepInput`: the input
* already rode the pair's `step_created`, and the claimed step carries it.
*/
preclaimedStart?: PreclaimedInlineStart;
/**
* Inline step ownership: the queue message ID of the invocation this
* executeStep call runs in (from the queue handler's meta). When set, the
* `step_started` this call sends is stamped with it: on the lazy paths
* (where `lazyStepInput` is present) and on the owned-recovery
* payload-less start (where it is not; the re-stamp keeps a recovered
* step readable as owned by this message, since ownership derives from
* the LATEST start, so a recovery start is never bare). Wake
* replays that observe an actively-owned step suppress the immediate
* requeue and enqueue a delayed backstop instead. Omitted on the
* background-step path, whose bare start intentionally clears ownership
* (the step is queue-owned from that point).
*/
ownerMessageId?: string;
/**
* Inline-delta optimization (opt-in). When provided, the cursor of the
* event log as observed by the caller *before* this step's events were
* written. It is threaded into the step-terminal `events.create` so a
* supporting World can return the delta of events written since (see
* {@link import('@workflow/world').CreateEventParams.sinceCursor}).
* Only set this when `correlationId`-based ownership guarantees this
* handler is the sole inline writer for the run on this iteration.
*/
inlineDeltaSinceCursor?: string;
/**
* Suppress optimistic inline start for this step regardless of
* `WORKFLOW_OPTIMISTIC_INLINE_START` / `forceOptimisticStart`: take the
* await-then-run path so the body only runs after the `step_started` claim
* succeeds. Set by the runtime for guard-enforced batches that are
* stale-sensitive (an open hook means an out-of-band event can make the
* scheduling view stale): the guard's 412 fence can reject a stale claim's
* durable writes, but it cannot un-run a body that optimistic start began
* before the claim settled; awaiting the claim extends the fence to user
* code. Wins over `forceOptimisticStart` and the env flag.
*/
suppressOptimisticStart?: boolean;
/**
* Force optimistic inline start regardless of
* `WORKFLOW_OPTIMISTIC_INLINE_START`. Set by turbo mode on the first delivery
* of the first invocation, where forcing it is safe: there is no concurrent
* peer handler to race the create-claim, so the body runs exactly once. Only
* meaningful together with `lazyStepInput` (a brand-new lazy step).
*/
forceOptimisticStart?: boolean;
/**
* Turbo mode only: a promise that resolves once the backgrounded
* `run_started` has landed. When set, the lazy/optimistic `step_started` is
* chained on it so the step is never created before its run exists. The body
* still runs immediately against locally-synthesized state (only the network
* write waits), so the `run_started` round-trip overlaps the body. `undefined`
* outside turbo, where `run_started` was already awaited up front.
*/
runReadyBarrier?: Promise<unknown>;
/**
* Latency telemetry (TTFS / STSO): eligibility and anchor timestamps decided
* by the orchestrator. When set, this executor computes the final values
* against the wall clock taken immediately before user code runs and
* attaches them to the step's terminal event. Set only for the first step of
* an inline batch, and only on first-attempt executions that qualify; see
* runtime/step-latency.ts.
*/
latencyTracking?: StepLatencyTracking;
/**
* Hook-resume TTR telemetry: the boundaries of a hook resumption whose next
* durable step is the one this call is about to execute. When set, the
* executor closes the measurement against the `step_started` claim and the
* wall clock taken immediately before user code, and attaches the total plus
* its phase breakdown to this step's `step.execute` span. Set by the runtime
* for exactly one step per resumption; see runtime/resume-latency.ts.
*/
resumeTracking?: ResumeTtrTracking;
/**
* Authoritative attempt number for this execution, used to bound retries
* against `maxRetries` BEFORE the body runs. To also catch
* step timeouts (which we can't have catch handlers for), we determine
* the attempt count based as follows:
* - Inline (combined handler): the number of `step_started` events already
* in the event log for this step, plus one for the attempt about to run.
* - Background (queue-dispatched): the queue delivery count
* (`metadata.attempt`), which increments on every redelivery.
*/
authoritativeAttempt?: number;
/** One-shot recovery telemetry activated by the orchestrator replay. */
replayRecoveryReporter?: ReplayRecoveryReporter;
}
/**
* The settled outcome of a `step_created` + `step_started` pair the
* suspension handler folded into its batched fan-out write (see
* `SuspensionHandlerResult.inlineClaims`). Handed to executeStep as
* {@link StepExecutorParams.preclaimedStart} so the executor runs (or skips)
* the body off the batch's verdict instead of sending a start of its own.
*/
export type PreclaimedInlineStart = {
/** The pair committed: this execution owns the step and runs the body. */
owned: true;
/**
* The started step entity from the batch result, with the locally
* dehydrated input attached by the suspension handler (batch responses
* return refs lazily; the body's hydration wants the same bytes the
* pair's `step_created` carried).
*/
step: StartedStep;
/**
* `Date.now()` taken right before the batch POST that carried the pair:
* the claim's "start POST sent" instant, anchoring RSFS exactly like
* the lazy claim's own POST would.
*/
batchPostSentAtMs?: number;
/**
* `Date.now()` taken right after that batch POST returned: the
* claim's completion instant (T6 of the hook-resume TTR window).
*/
claimCompletedAtMs?: number;
} | {
/**
* The pair lost its atomic create-claim (per-event 409): a concurrent
* writer owns the step, so the body must not run here.
*/
owned: false;
};
/**
* Inline-delta returned by a step-terminal write when the caller passed
* {@link StepExecutorParams.inlineDeltaSinceCursor} and the World supports
* the optimization. `events` are the events written strictly after that
* cursor (this step's events plus anything interleaved in-band), `cursor`
* is the position past the last one, and `hasMore` signals a further page
* (the caller then falls back to a full incremental fetch). Absent when
* the World did not return a delta.
*/
export interface InlineEventDelta {
events: Event[];
cursor: string | null;
hasMore: boolean;
}
/**
* Result of a step execution attempt. The caller decides what to do
* based on the result type (e.g., queue workflow continuation, replay inline, etc.).
*
* `inlineDelta` is attached to a `completed` result when the caller
* requested it via {@link StepExecutorParams.inlineDeltaSinceCursor} and
* the World returned one. Step failures are rare and not the inline
* optimization's target, so the `failed` path leaves it to the normal
* incremental fetch.
*/
export type StepExecutionResult = {
type: 'completed';
hasPendingOps?: boolean;
inlineDelta?: InlineEventDelta;
} | {
type: 'failed';
} | {
type: 'retry';
timeoutSeconds: number;
} | {
type: 'skipped';
} | {
type: 'gone';
} | {
type: 'throttled';
timeoutSeconds: number;
};
/**
* Executes a single step: creates step_started event, hydrates input,
* runs the step function, creates step_completed/step_failed/step_retrying events.
*
* Does NOT queue workflow continuation messages; the caller decides what to do next.
* Used by the combined workflow handler for step execution.
*/
export declare function executeStep(params: StepExecutorParams): Promise<StepExecutionResult>;
//# sourceMappingURL=step-executor.d.ts.map