Core
Every entry point, and the isomorphic experimental-a2 core: contracts, reducers, IDs, and the event shape.
Entry points
| Entry point | Ships | Peer dependency |
|---|---|---|
experimental-a2 | contract, A2Error: isomorphic core | none |
experimental-a2/server | createServer: implement a contract | none |
experimental-a2/client | createClient: framework-agnostic session client | none |
experimental-a2/react | createReact | react |
experimental-a2/ai | agentContract, handlerContext, inputs, and the AI reducer: isomorphic | ai |
experimental-a2/ai/server | agent: an agent's loop as an A2 server | ai |
experimental-a2/ai/fx | fx: the FX harness for agent() | ai, libfx |
experimental-a2/ai/pi | pi: the Pi harness for agent() | ai, @earendil-works/pi-coding-agent, @earendil-works/pi-ai |
experimental-a2/ai/client | createAgentClient: contract-free agent client | ai |
experimental-a2/ai/react | useAgent | ai, react |
experimental-a2/store-postgres | postgres: Postgres store backend | pg (or inject a client) |
experimental-a2/store-redis | redis: Redis Streams store backend, push-native streaming | ioredis (or inject a client) |
experimental-a2/store-redis-http | redisHttp: the same Redis store over provider REST APIs (Upstash) | none |
experimental-a2/store-sqlite | SQLite store backend | none |
experimental-a2/store-memory | in-memory store backend | none |
experimental-a2/scheduler-vercel | vercelQueues: recovery and timers | @vercel/queue |
experimental-a2/scheduler-qstash | qstash: signed recovery and timers | @upstash/qstash |
experimental-a2/cache-indexeddb | indexedDb browser cache | none |
experimental-a2/otel | otel telemetry adapter | @opentelemetry/api |
experimental-a2 imports none of the backends, and a browser-bundle test in CI
enforces it. Importing a backend's entry point, such as
experimental-a2/store-postgres, is what pulls that backend in; core never puts
one in your client bundle.
experimental-a2
a2.contract(options)
a2.contract(options: {
name: string
events: Record<string, StandardSchemaV1>
presence?: Record<string, StandardSchemaV1>
reducer?: { name: string; initialState: S; stateSchema?: StandardSchemaV1; fold: (state: S, event: Event) => S }
}): ContractDefines one kind of lifecycle. The result is a plain, importable, isomorphic value: the server implements it, reducers derive from it, and the browser types its pushes off it. See Events.
| Option | Meaning |
|---|---|
name | Unique per app; identifies the contract in storage and queue messages. May not contain the control character U+001F. |
events | Event type to payload schema. Types may not start with a2., which A2 reserves for its own events, such as a2.cleared. |
presence | Optional. Field to schema for one participant's ephemeral state. Replicated last-writer-wins, never stored in the log, invisible to reducers. See Presence. |
reducer | Optional. The one reducer the contract publishes to clients; an inline definition includes fold. See reducer(options). |
Schemas are Standard Schema: Zod, Valibot, ArkType, anything that implements it. Validators must be synchronous; async ones are rejected at definition time. The validated output is what's stored.
contract.batch(...events)
Returns the same events as a readonly tuple, preserving each literal type in a heterogeneous handler-returned array. A2 validates them when the handler completes. See Handlers.
reducer(options)
reducer(options: {
contract: Contract
name: string // identity; keys cached snapshots
initialState: S // the seed (schema input, when stateSchema is set)
stateSchema?: StandardSchemaV1 // declares S, checks initialState, guards cached folds
}): { fold(fn: (state: S, event: Event) => S): Reducer<S> }Builds a standalone reducer for server reads or for the contract's reducer
field. See State.
- The first call fixes the state and event types, so
.fold()receives concrete ones. Literal unions in your state survive with zero annotations. namekeys cached snapshots, so two reducers never fight over a cache entry. Rename it when the fold's logic changes: a rename (or astateSchemamismatch, when present) invalidates, and the next read refolds from raw events.
A2Error
The shared base for A2 failures, discriminated by code. The session client
exposes its A2ClientError subclass. See Errors.
idempotentId(...parts)
idempotentId(...parts: string[]): Promise<string> derives a stable UUIDv8
from an ordered tuple of strings. It performs no I/O and runs on the server and
in the browser.
import { idempotentId } from 'experimental-a2'
export const id = await idempotentId('orders', 'shop-7', 'request-42')- Pass the result as an event's
idwhen retries should converge on the same event. - The same tuple produces the same ID across runtimes and A2 versions.
- String boundaries, order, and case matter; strings are not normalized. Empty strings and an empty tuple are valid.
- Every argument has the same role; a prefix such as
'orders'is a namespace by convention. Include enough context to distinguish operations across contracts and sessions: event IDs are globally unique within a store. - A2 preserves explicit IDs and does not change its automatic ID generation. Changing an existing recipe changes retry identity, so retries of already committed requests must keep their original IDs.
SessionCleared
An Event whose type is 'a2.cleared' and whose payload is null: the
marker a clear leaves in
session.stream(). It never reaches handlers or history(). Comparing
event.type with that string narrows a stream item to SessionCleared. The
session client handles the marker itself, so it matters only when you read the
server stream yourself.
Event
type Event = {
id: string
type: string
payload: unknown // typed by the contract's schema for `type`
index: number // position in the session's log, from 1
sessionId: string
createdAt: Date
}