UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

606 lines • 32.8 kB
import type { Analytics } from './analytics.js'; import type { AttributeChange, ExperimentalSetAttributesResult } from './attributes.js'; import type { BatchEventRequest, CreateEventBatchParams, CreateEventParams, CreateEventRequest, Event, EventBatchResult, EventResult, GetEventParams, ListEventsByCorrelationIdParams, ListEventsParams, RunCreatedEventRequest } from './events.js'; import type { GetHookParams, Hook, ListHooksParams } from './hooks.js'; import type { Queue } from './queue.js'; import type { BulkCancelWorkflowRunsRequest, BulkCancelWorkflowRunsResult, GetWorkflowRunParams, ListWorkflowRunsParams, WaitForTerminalRunStatusParams, WorkflowRun, WorkflowRunWithoutData } from './runs.js'; import type { GetChunksOptions, PaginatedResponse, StreamChunksResponse, StreamInfoResponse } from './shared.js'; import type { GetStepParams, ListWorkflowRunStepsParams, Step, StepWithoutData } from './steps.js'; export interface StreamWriteSession { /** * Write one ordered group from this in-memory writer lifetime. * `chunkSeq` is writer-local and identifies the first chunk in `chunks`. */ write(chunkSeq: number, chunks: (string | Uint8Array)[]): Promise<void>; /** Close this writer lifetime after all prior writes are durable. */ close(): Promise<void>; /** Release transport resources without semantically closing the stream. */ dispose?(): Promise<void> | void; } export interface CreateStreamWriteSessionOptions { /** Stable observational id for this in-memory writer lifetime. */ writerId: `wrtr_${string}`; } export interface Streamer { /** * Number of milliseconds a stream waits for additional chunks to arrive * before flushing to the underlying transport. * * Default `0`: the first chunk dispatches immediately, and chunks * arriving while a request is in flight coalesce into the next group. * Setting this to > 0 trades first-chunk latency for fewer requests. * * The `WORKFLOW_STREAM_FLUSH_INTERVAL_MS` environment variable, when * set, overrides this option. * * Not supported by all worlds. */ streamFlushIntervalMs?: number; streams: { /** * Optionally create a stateful writer session. Core creates at most one * session per in-memory WritableStream and otherwise uses the stateless * write/writeMulti/close methods below unchanged. */ createWriteSession?(runId: string, name: string, options: CreateStreamWriteSessionOptions): StreamWriteSession; write(runId: string, name: string, chunk: string | Uint8Array): Promise<void>; /** * Write multiple chunks to a stream in a single operation. * This is an optional optimization for world implementations that can * batch multiple writes efficiently (e.g., single HTTP request for world-vercel). * * If not implemented, the caller should fall back to sequential write() calls. * * @param runId - The run ID * @param name - The stream name * @param chunks - Array of chunks to write, in order */ writeMulti?(runId: string, name: string, chunks: (string | Uint8Array)[]): Promise<void>; close(runId: string, name: string): Promise<void>; /** * Read from a stream starting at the given chunk index. * Positive values skip that many chunks from the start (0-based). * Negative values start that many chunks before the current end * (e.g. -3 on a 10-chunk stream starts at chunk 7). Clamped to 0. */ get(runId: string, name: string, startIndex?: number): Promise<ReadableStream<Uint8Array>>; list(runId: string): Promise<string[]>; /** * Fetch stream chunks with cursor-based pagination. * * Unlike `get` (which returns a live `ReadableStream` that waits * for new chunks in real-time), `getChunks` returns a snapshot of currently * available chunks in a standard paginated response. * * @param runId - The workflow run ID that owns the stream * @param name - The stream name/ID * @param options - Pagination options (limit defaults to 100, max 1000) * @returns Paginated chunks with a `done` flag indicating stream completion */ getChunks(runId: string, name: string, options?: GetChunksOptions): Promise<StreamChunksResponse>; /** * Retrieve lightweight metadata about a stream. * * Returns the tail index (index of the last known chunk, 0-based) and * whether the stream is complete. This is useful for resolving a negative * `startIndex` into an absolute position before connecting to a stream. * * @param runId - The workflow run ID that owns the stream * @param name - The stream name/ID */ getInfo(runId: string, name: string): Promise<StreamInfoResponse>; }; } /** * Storage interface for workflow data. * * Workflow storage models an append-only event log, so all state changes are handled through `events.create()`. * Run/Step/Hook entities provide materialized views into the current state, but entities can't be modified directly. * * User-originated state changes are also handled via events: * - run_cancelled event for run cancellation * - hook_disposed event for explicit hook disposal (optional) * * When a workflow reaches a terminal state, its Hooks can no longer be resumed. * Worlds normally remove them and release their tokens. A Hook with minimum * retention remains readable and keeps its token unavailable until its retention * ends. A hook_disposed event always removes the Hook and releases its token. */ export interface Storage { runs: { get(id: string, params: GetWorkflowRunParams & { resolveData: 'none'; }): Promise<WorkflowRunWithoutData>; get(id: string, params?: GetWorkflowRunParams & { resolveData?: 'all'; }): Promise<WorkflowRun>; get(id: string, params?: GetWorkflowRunParams): Promise<WorkflowRun | WorkflowRunWithoutData>; /** * Long poll for a run to reach a terminal status (`completed`, `failed`, * or `cancelled`), returning the same entity `get` returns. * * This is how a caller awaiting a run's outcome (`await run.returnValue`) * avoids paying interval-poll quantization for it: instead of asking * "is it done yet?" every second, it asks once and the World answers the * moment the run finishes. * * The contract: * * - **Resolve as soon as the run is terminal**, with the run entity in * the shape `params.resolveData` asks for. * - **Resolve no later than roughly `params.timeoutMs`** with the latest * snapshot, whatever its status. A timeout is a normal return, never an * error: a run that is still running is a legitimate answer. * - **`timeoutMs` is an upper bound, not a lower one.** An * implementation MAY resolve earlier with a non-terminal snapshot. For * example, `@workflow/world-vercel` does when the backend it is talking * to has no long-poll route and it degrades to a plain read. Callers * must therefore pace their own retries rather than assume one call per * `timeoutMs` (the runtime's `Run#pollReturnValue` keeps consecutive * non-terminal observations at least one poll interval apart). * - **Fail exactly like `get`.** A missing run throws * `WorkflowRunNotFoundError`; transport failures surface as they would * on any other read. * * OPTIONAL. Omit it entirely when the World has no way to wait (a * deterministic simulator, a store with no change notification) and the * runtime keeps interval-polling `get` on * `WORKFLOW_RETURN_VALUE_POLL_INTERVAL_MS`. There is nothing to declare * beyond the method's presence, and no behavior degrades when it is * absent: the fast path is strictly additive. * * Implementations are free to satisfy this however their backend allows, * such as a server-side long poll (`world-vercel` holds * `GET /v2/runs/:runId/status` open), a change notification * (`world-postgres` uses `LISTEN`/`NOTIFY`, `world-local` an in-process * emitter), or a tight internal poll, as long as a lost or missing * notification degrades to returning a snapshot rather than hanging past * the budget. */ waitForTerminalStatus?: { (id: string, params: WaitForTerminalRunStatusParams & { resolveData: 'none'; }): Promise<WorkflowRunWithoutData>; (id: string, params?: WaitForTerminalRunStatusParams & { resolveData?: 'all'; }): Promise<WorkflowRun>; (id: string, params?: WaitForTerminalRunStatusParams): Promise<WorkflowRun | WorkflowRunWithoutData>; }; /** * Retrieves several runs as one snapshot. The result preserves the input * order and contains `null` for run IDs that do not exist. */ getMany?: { (ids: readonly string[], params: GetWorkflowRunParams & { resolveData: 'none'; }): Promise<(WorkflowRunWithoutData | null)[]>; (ids: readonly string[], params?: GetWorkflowRunParams & { resolveData?: 'all'; }): Promise<(WorkflowRun | null)[]>; (ids: readonly string[], params?: GetWorkflowRunParams): Promise<(WorkflowRun | WorkflowRunWithoutData | null)[]>; }; /** * Lists canonical workflow storage records. * * @remarks Observability and inspection usage of this method is * deprecated. Use `world.analytics?.runs.list()` for plan-aware * observability queries. This storage API remains available for * operational and payload-bearing callers. */ list(params: ListWorkflowRunsParams & { resolveData: 'none'; }): Promise<PaginatedResponse<WorkflowRunWithoutData>>; list(params?: ListWorkflowRunsParams & { resolveData?: 'all'; }): Promise<PaginatedResponse<WorkflowRun>>; list(params?: ListWorkflowRunsParams): Promise<PaginatedResponse<WorkflowRun | WorkflowRunWithoutData>>; /** * Apply a batch of attribute changes to a run. Merge semantics: * - `value: string` upserts the key * - `value: null` removes the key * - keys not listed in `changes` are untouched * * Returns the post-merge attribute snapshot on the run. * * Pass `options.allowReservedAttributes: true` to permit keys * starting with the reserved `$` prefix. Default behavior rejects * those keys so user code can't accidentally collide with * framework / tooling namespaces; framework callers that own a * sub-namespace flip this on. * * OPTIONAL. World implementations may omit this method; the SDK * helper (`setAttributes` in `@workflow/core`) feature-detects its * absence and no-ops with a one-time warning so third-party / * community worlds keep working without adopting the experimental * API. * * EXPERIMENTAL: this method exists as a stopgap until the * `attr_set` event type lands in a future spec version. When that * happens, `setAttributes` will dispatch through `events.create` * instead, and this method is expected to be removed. See the * `attributes-mvp` changelog entry for the migration shape. */ experimentalSetAttributes?(runId: string, changes: AttributeChange[], options?: { allowReservedAttributes?: boolean; }): Promise<ExperimentalSetAttributesResult>; /** * Cancel many runs in a single operation, returning a per-run outcome * for each requested ID (order preserved) plus an aggregate summary. * * OPTIONAL. The SDK helper `cancelRuns` in `@workflow/core` falls back to * bounded-concurrency single-run cancellation when unavailable. */ cancelMany?(request: BulkCancelWorkflowRunsRequest): Promise<BulkCancelWorkflowRunsResult>; }; steps: { get(runId: string, stepId: string, params: GetStepParams & { resolveData: 'none'; }): Promise<StepWithoutData>; get(runId: string, stepId: string, params?: GetStepParams & { resolveData?: 'all'; }): Promise<Step>; get(runId: string, stepId: string, params?: GetStepParams): Promise<Step | StepWithoutData>; list(params: ListWorkflowRunStepsParams & { resolveData: 'none'; }): Promise<PaginatedResponse<StepWithoutData>>; list(params: ListWorkflowRunStepsParams & { resolveData?: 'all'; }): Promise<PaginatedResponse<Step>>; list(params: ListWorkflowRunStepsParams): Promise<PaginatedResponse<Step | StepWithoutData>>; }; /** * The event log, and the one part of this interface with a requirement the * types cannot express: **the World allocates every event id, and every id * is a slot**: `evnt_` followed by the event's dense, 1-based position in * its run's log, zero-padded to 26 characters. Use `slotToEventId()` to * format one. * * Not a capability to opt into. The runtime reads a position out of every id * it loads (`requireEventSlot`) and fails the run if it cannot, so a World * whose ids are not positions cannot replay anything at all. Two properties * are what the runtime actually relies on: * * - **Density.** A run's slots are contiguous from 1, so the number of * events a reader holds *is* the position of the last one. That is what * makes {@link CreateEventParams.eventCount} a complete statement of the * writer's snapshot in a single integer, and what lets a reader tell a * complete log from a truncated one by its length. * - **Bump and report.** A create never fails because its requested slot is * taken. The World advances to the next free slot, commits there, and * returns the events occupying the slots it skipped over on the success * response (see {@link EventResult.events}). The writer learns its * snapshot was stale without the write being rejected, which is why no * World needs a precondition guard. * * Allocating at the commit is what makes a reader's log a *prefix* of the * run's log rather than a prefix with a hole in it. A World that hands a * position out earlier, and can therefore let an event land behind one a * reader has already passed, breaks the property every replay depends on. */ events: { /** * Create a run_created event to start a new workflow run. * The runId may be provided by the client or left as null for the server to generate. * * @param runId - Client-generated runId, or null for server-generated * @param data - The run_created event data * @param params - Optional parameters for event creation * @returns Promise resolving to the created event and run entity */ create<T extends RunCreatedEventRequest>(runId: string | null, data: T, params?: CreateEventParams): Promise<EventResult<T['eventType']>>; /** * Create an event for an existing workflow run and atomically update the entity. * Returns both the event and the affected entity (run/step/hook). * * @param runId - The workflow run ID (required for all events except run_created) * @param data - The event to create * @param params - Optional parameters for event creation * @returns Promise resolving to the created event and affected entity */ create<T extends CreateEventRequest>(runId: string, data: T, params?: CreateEventParams): Promise<EventResult<T['eventType']>>; /** * OPTIONAL batch write: append an ordered list of events to the run's * log in one durable, atomic-per-attempt write, with a per-event outcome * for each (see {@link BatchEventItemResult}). The events land in request * order at consecutive slots. A concurrent writer may push the whole * batch to slots above the caller's view of the log; no skipped-event * report accompanies the result, so a position-tracking caller compares * the committed slots against its expectation and reloads the log to * observe what landed in between. Its local view stays a strict PREFIX * of the log (never a hole), so replaying it stays correct and the * next reload self-corrects. * * Presence of the method IS the capability declaration: the core runtime * batches only when the World implements it (and the run's spec version * supports slot identity); absent, every write takes the single-event * `create` path unchanged. A World must implement it with real * atomicity per attempt (a lost race must leave nothing behind) or not * implement it at all. * * Size limits are the caller's problem: Worlds enforce their own caps * (world-vercel enforces an event-count cap and a byte budget over frame * meta plus inline-bound payloads) and reject an oversized batch with a * request-level error. The core fold sizes its chunks accordingly. * * Not expressible in a batch (Worlds reject the whole batch with a * request-level error): `run_created`, `run_started`, `run_cancelled`, * `hook_created`, `hook_disposed`, `attr_set`, and more events targeting * one entity than a single write can express (the one legal combination * is `step_created` followed by `step_started` for the same step, which * creates the step born-running: the step's input MUST ride the * `step_created`; a `step_started` carrying a payload rejects the whole * batch). Events outside this list keep their own ordering requirements: * a caller mixing a batch with single writes (hook or attribute events) * owns those barriers itself: the core runtime never batches a * suspension that carries hook or attribute writes. */ createBatch?(runId: string, events: BatchEventRequest[], params?: CreateEventBatchParams): Promise<EventBatchResult>; get(runId: string, eventId: string, params?: GetEventParams): Promise<Event>; list(params: ListEventsParams): Promise<PaginatedResponse<Event>>; listByCorrelationId(params: ListEventsByCorrelationIdParams): Promise<PaginatedResponse<Event>>; }; hooks: { /** * Returns a Hook by ID. A Hook kept by minimum retention remains readable * after its run ends, but cannot be resumed. */ get(hookId: string, params?: GetHookParams): Promise<Hook>; /** * Returns the Hook that owns a token, including a Hook kept by minimum * retention after its run ends. */ getByToken(token: string, params?: GetHookParams): Promise<Hook>; /** * Lists Hooks, including Hooks kept by minimum retention after their runs * end. */ list(params: ListHooksParams): Promise<PaginatedResponse<Hook>>; }; } /** * Optional feature capabilities a World implementation declares so the core * runtime can enable optimizations that depend on backend behavior, instead * of inferring support from environment variables alone. Every capability * defaults to "unsupported" when absent: runtime fast paths that rely on * one must fail closed (keep their conservative behavior) unless the World * explicitly declares it. */ export interface WorldCapabilities { /** * Enables invoke() and request/response processing through createQueueHandler. * Requires at most one active workflow runner per runId across all worker * processes. Different runs may execute concurrently. * * The active runner must process inputs while it awaits step work. A replacement * runner may take over after the previous runner stops, so process identity can * change over the run's lifetime. */ invoke?: boolean; /** * Supports `experimental_minRetention` for Hooks. Missing or inactive means * the runtime rejects retained Hooks before registration. */ hookRetention?: { active: boolean; }; /** * The World's queue supports `maxConcurrency`-limited consumption, in * particular the per-run flow topics consumed with `maxConcurrency: 1` * that `WORKFLOW_SEQUENTIAL_REPLAYS=1` uses to serialize a run's * orchestrator invocations. Worlds whose queue has no concurrency-limit * concept must leave this unset. * * Note this declares queue *support*, not deployed configuration: the * serialization also requires the build-time half (a flow trigger emitted * with `maxConcurrency: 1`), which a runtime process cannot verify today. * The core runtime therefore does not yet take any fast path from this * capability alone: it exists so a future build-verified signal can be * combined with it (and so Worlds document the contract explicitly). */ maxConcurrency?: boolean; /** * The World's `events.create` deduplicates concurrent `hook_received` writes * that carry the same `(runId, resumeId)`, collapsing them onto a single * committed event and returning the canonical one to every caller. Two * writers rely on it: `resumeHook()`'s durable write attaches a `resumeId` + * payload digest so transport-level retries of one write converge on exactly * one event, and legacy `hookInput` queue redeliveries (from older * producers) converge through the same constraint. * * The core runtime fails closed on this: a `resumeId` is attached ONLY when * the World declares `hookResumeDedup === true` (or the live backend attests * it per-lookup, below). A World that accepts a `resumeId` but does not * enforce the `(runId, resumeId)` constraint must leave this unset so the * runtime keeps the plain single-shot write. * * Declaring this also commits the World to ROUND-TRIPPING the key: * `events.list` must return `resumeId` on `hook_received` events it * persisted with one, because the legacy `hookInput` consumer path detects * an already-materialized resume by matching `resumeId` in the loaded log. * * Enabled statically for `world-local` (filesystem sidecar claim keyed on * `(runId, resumeId)`; the adapter and its backend ship together, so a static * capability can never drift from the backend). `world-vercel` deliberately * leaves this UNSET and instead attests support per-lookup via the * server-computed, response-only `Hook.resumeCapabilities.hookResumeDedupVersion` * (see `HookResumeCapabilitiesSchema`), so a server rollback or kill switch * degrades new resumes to plain writes immediately without redeploying * the adapter. `world-postgres` enforces resume identities transactionally * and declares the capability statically. * * The resume gate treats EITHER signal as backend support (see * `resume-hook.ts`): this static capability OR a current * `resumeCapabilities.hookResumeDedupVersion` on the by-token hook. */ hookResumeDedup?: boolean; /** * Deployments are atomic and immutable: a deployment id names one fixed * build for its whole lifetime, so a run pinned to one may only execute * there. Worlds that declare this get the runtime's deployment-affinity * guard, which re-routes a misrouted delivery to the run's own deployment * and ultimately fails the run with `DEPLOYMENT_MISMATCH`. * * Worlds whose deployment id is synthetic or version-tagged (e.g. * `dpl_local@<sdk-version>`, which legitimately differs across SDK versions * within one logical environment) must leave this unset: there a * "mismatch" is not a real cross-deployment delivery, and guarding would * fail ordinary runs after a version bump. */ deploymentAffinity?: boolean; } /** * The "World" interface represents how Workflows are able to communicate with the outside world. */ export interface World extends Queue, Streamer, Storage { /** * Optional analytics read namespace for observability surfaces. * * These APIs return metadata-only rows intended for UI/CLI listing and * trace views. Payload-bearing fields remain on the canonical runtime * storage APIs (`runs`, `steps`, `events`, `hooks`) and their RemoteRef * resolution path. */ analytics?: Analytics; /** * The Workflow protocol spec version this World implements, and the version * stamped on every run it creates. * * Declare `SPEC_VERSION_CURRENT` rather than a literal. The runtime checks * this against `[SPEC_VERSION_CURRENT, SPEC_VERSION_MAX_SUPPORTED]` before it * creates or replays anything, and refuses a World outside that range: below * the floor the World allocates event ids the runtime cannot read positions * out of (see the event log contract above), above the ceiling it speaks a * spec this runtime has not learned. */ specVersion: number; /** * Feature capabilities this World implementation supports. See * {@link WorldCapabilities}. Absent (or absent members) means * "unsupported": runtime optimizations gated on a capability fail closed. */ capabilities?: WorldCapabilities; /** * Absolute wall-clock time when the current function invocation will be * terminated by the hosting platform, if known. Used to optimize runtime behavior. */ getRuntimeDeadline?(): Promise<Date | undefined>; /** * A function that will be called to start any background tasks needed by the World implementation. * For example, in the case of a queue backed World, this would start the queue processing. */ start?(): Promise<void>; /** * Release any resources held by the World implementation (connection pools, listeners, etc.). * After calling `close()`, the World instance should not be used again. * * This is important for CLI commands and short-lived processes that need to exit cleanly * without relying on `process.exit()`. */ close?(): Promise<void>; /** * Resolve the most recent deployment ID for the current deployment's environment. * * Used when `deploymentId: 'latest'` is passed to `start()`. The implementation * determines the latest deployment that shares the same environment (e.g., same * "production" target or same git branch for "preview" deployments) as the * current deployment. * * Not all World implementations support this: it is only implemented by * world-vercel where deployment routing is meaningful. */ resolveLatestDeploymentId?(): Promise<string>; /** * Retrieve the AES-256 encryption key for a specific workflow run. * * The returned key is a ready-to-use 32-byte AES-256 key. The World * implementation handles all key retrieval and derivation internally * (e.g., HKDF from a deployment key). The core encryption module uses * this key directly for AES-GCM encrypt/decrypt operations. * * Two overloads: * * - `getEncryptionKeyForRun(run)`: Preferred. Pass a `WorkflowRun` when * the run entity already exists. The World reads any context it needs * (e.g., `deploymentId`) directly from the run. * * - `getEncryptionKeyForRun(runId, context?)`: Used when the run entity * is not locally available, such as `start()` before run creation or a * forwarded writable stream carrying its owning deployment context. The * `context` parameter carries opaque world-specific data (e.g., * `{ deploymentId }` for world-vercel) needed to resolve the correct key. * When `context` is omitted, the World assumes the current deployment. * * When not implemented, encryption is disabled: data is stored unencrypted. */ getEncryptionKeyForRun?(run: WorkflowRun): Promise<Uint8Array | undefined>; getEncryptionKeyForRun?(runId: string, context?: Record<string, unknown>): Promise<Uint8Array | undefined>; /** * Mint a new workflow run ID. * * Called by `start()` to generate the unique ID for a newly-created run. * The returned value is the "bare" ID (without any `wrun_` prefix); the * core attaches the prefix. * * Implementations are free to embed world-specific metadata in the ID * (e.g., a region identifier) as long as the returned string remains a * valid ULID. When omitted, `start()` falls back to generating a standard * monotonic ULID. * * @param options - The full options bag passed to `start()` (typed as * `Record<string, unknown>` here to avoid a circular dependency with * `@workflow/core`). Worlds should read only the fields they * recognize. For example, `@workflow/world-vercel` reads * `options.region` to embed a region identifier. Unrecognized keys * must be ignored. `start()` always passes an object (an empty one * when it was called with no options), but implementations should * tolerate `undefined` for direct callers. */ createRunId?(options?: Readonly<Record<string, unknown>>): string; /** * The environment this World's writes are attributed to by the backend * (`@workflow/world-vercel`: `'production' | 'preview' | 'development'`). * * Synchronous and side-effect free: implementations derive this from * configuration or environment variables they already hold, never from a * network call. Return `undefined` when the environment can't be determined. * * The value MUST match the attribution the backend will actually apply to * this client's writes: for `world-vercel` that means keeping it in lockstep * with the `x-vercel-environment` header (proxy path) and the OIDC token's * `environment` claim (in-deployment path). A value that merely looks * plausible is worse than `undefined`, because callers use it to detect * cross-tenant mismatches and a wrong answer manufactures a false one. * * `start()` stamps this into the queue message's `runInput` so the consuming * deployment can tell that a message it was handed was created against a * different environment than its own. Not all Worlds have an environment * dimension: local dev and Postgres have exactly one tenant, so they omit * this and the check is skipped. */ getEnvironment?(): string | undefined; /** * World-specific display fields for a run. * * Tooling (e.g. the `workflow inspect` CLI) calls this to enrich a * run's listing row / detail output with fields only the world can * derive: a region decoded from the run ID, placement read off the * run's `executionContext`, a shard, a billing tier, etc. Consumers * render each returned key as an additional column/property; when the * hook is absent, no extra fields appear at all. * * The contract: * - **Cheap and pure.** Called once per displayed run, so avoid I/O. * Prefer deriving fields from the entity you are given. * - **Read only what you recognize.** The argument is the run entity * as the caller has it (a full storage run, or a leaner analytics * row), typed loosely for the same reason as {@link createRunId}. * Tolerate missing fields. * - **Must not throw.** * - A `null` field value means "applicable but undeterminable" and is * preserved as `null` in structured output (vs. the hook being * absent, where the key does not exist at all). Return `null` or an * empty object to add nothing for a given run. */ describeRun?(run: Readonly<Record<string, unknown>>): Record<string, string | null> | null | Promise<Record<string, string | null> | null>; } //# sourceMappingURL=interfaces.d.ts.map