---
title: Core
description: "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`](/stores/postgres) | `postgres`: Postgres store backend | `pg` (or inject a client) |
| [`experimental-a2/store-redis`](/stores/redis) | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
| [`experimental-a2/store-redis-http`](/stores/redis-http) | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
| [`experimental-a2/store-sqlite`](/stores/sqlite) | SQLite store backend | none |
| [`experimental-a2/store-memory`](/stores/memory) | in-memory store backend | none |
| [`experimental-a2/scheduler-vercel`](/schedulers/vercel-queues) | `vercelQueues`: recovery and timers | `@vercel/queue` |
| [`experimental-a2/scheduler-qstash`](/schedulers/qstash) | `qstash`: signed recovery and timers | `@upstash/qstash` |
| `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
| [`experimental-a2/otel`](/reference/telemetry) | `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)`

```ts
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 }
}): Contract
```

Defines 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](/contract/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](/client/presence). |
| `reducer` | Optional. The one reducer the contract publishes to clients; an inline definition includes `fold`. See [`reducer(options)`](#reduceroptions). |

Schemas are [Standard Schema](https://standardschema.dev): 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](/server/handlers#return-what-happens-after-success).

### `reducer(options)`

```ts
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](/contract/state#reducers).

- The first call fixes the state and event types, so `.fold()` receives
  concrete ones. Literal unions in your state survive with zero annotations.
- `name` keys cached snapshots, so two reducers never fight over a cache
  entry. Rename it when the fold's logic changes: a rename (or a `stateSchema`
  mismatch, 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](/reference/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.

```ts shared/request-id.ts
import { idempotentId } from 'experimental-a2'

export const id = await idempotentId('orders', 'shop-7', 'request-42')
```

- Pass the result as an event's `id` when 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](/reference/server#sessionclearoptions) 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

```ts
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
}
```
