experimental-a2 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/ai-server.browser.js +2 -2
  3. package/dist/ai-server.d.ts +19 -7
  4. package/dist/ai-server.js +730 -96
  5. package/dist/ai.d.ts +32 -11
  6. package/dist/ai.js +253 -75
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.js +4 -4
  9. package/dist/{contract-B0kAXoaL.js → contract-CG_adnu_.js} +2 -1
  10. package/dist/{contract-DL8btVd9.d.ts → contract-C_3dIIEU.d.ts} +4 -1
  11. package/dist/devtools-server.browser.js +2 -2
  12. package/dist/devtools-server.js +1 -1
  13. package/dist/http.d.ts +1 -1
  14. package/dist/http.js +4 -3
  15. package/dist/idempotent-replay-BMyHrP0L.js +19 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/{internal-Dm8Ejnud.js → internal-D6wNxTck.js} +3 -3
  19. package/dist/{log-Dg1I8NRr.d.ts → log-ldf5g8Cx.d.ts} +74 -56
  20. package/dist/log-memory.d.ts +1 -1
  21. package/dist/log-memory.js +173 -96
  22. package/dist/{log-polling-RO7kclzR.js → log-polling-6COoN60V.js} +1 -1
  23. package/dist/log-postgres.d.ts +1 -1
  24. package/dist/log-postgres.js +235 -192
  25. package/dist/log-redis.d.ts +1 -1
  26. package/dist/log-redis.js +453 -263
  27. package/dist/log-sqlite.d.ts +1 -1
  28. package/dist/log-sqlite.js +216 -127
  29. package/dist/otel.d.ts +1 -1
  30. package/dist/otel.js +1 -1
  31. package/dist/react.d.ts +1 -1
  32. package/dist/react.js +1 -1
  33. package/dist/recovery-vercel.d.ts +2 -2
  34. package/dist/recovery-vercel.js +9 -10
  35. package/dist/server-DJgD2YWP.js +877 -0
  36. package/dist/server.browser.js +4 -4
  37. package/dist/server.d.ts +46 -27
  38. package/dist/server.js +1 -1
  39. package/dist/{telemetry-C78al20p.d.ts → telemetry-Cso0qyHQ.d.ts} +1 -1
  40. package/dist/{wire-2QpU1EtJ.js → wire-BVsgR8o9.js} +1 -1
  41. package/docs/01-quickstart.mdx +7 -7
  42. package/docs/concepts/01-contracts.mdx +22 -22
  43. package/docs/concepts/02-handlers.mdx +223 -89
  44. package/docs/concepts/03-durability.mdx +199 -112
  45. package/docs/concepts/04-state.mdx +27 -1
  46. package/docs/guides/01-timers.mdx +4 -4
  47. package/docs/guides/02-cancellation.mdx +32 -4
  48. package/docs/guides/05-production.mdx +61 -27
  49. package/docs/guides/06-ai-agents.mdx +151 -70
  50. package/docs/guides/07-devtools.mdx +6 -3
  51. package/docs/guides/08-application-data.mdx +5 -6
  52. package/docs/index.mdx +30 -14
  53. package/docs/reference/01-api.mdx +305 -70
  54. package/package.json +31 -31
  55. package/dist/server-DYsnKTTy.js +0 -780
@@ -1,4 +1,4 @@
1
1
  //#region src/devtools-server.browser.ts
2
- /** a2/devtools/server owns log access and is server-only. */
3
- throw new Error("a2/devtools/server is server-only — mount its handler in a server route and open that URL in the browser.");
2
+ /** experimental-a2/devtools/server owns log access and is server-only. */
3
+ throw new Error("experimental-a2/devtools/server is server-only — mount its handler in a server route and open that URL in the browser.");
4
4
  //#endregion
@@ -819,7 +819,7 @@ if (bootstrap) {
819
819
  //#endregion
820
820
  //#region src/devtools-server.ts
821
821
  /**
822
- * a2/devtools/server — a read-only dashboard over A2's durable data.
822
+ * experimental-a2/devtools/server — a read-only dashboard over A2's durable data.
823
823
  * One Request handler serves the application, its private browser bundle,
824
824
  * JSON inspection endpoints, and live SSE invalidations.
825
825
  */
package/dist/http.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Event } from "./log-Dg1I8NRr.js";
1
+ import { a as Event } from "./log-ldf5g8Cx.js";
2
2
  import { PushedEvent } from "./server.js";
3
3
  import { t as A2Error } from "./errors-xL_JTXsY.js";
4
4
  //#region src/http.d.ts
package/dist/http.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as A2Error } from "./errors-BJRMd-h6.js";
2
- import { r as STREAM_TIMINGS } from "./internal-Dm8Ejnud.js";
3
- import { a as eventToWire, n as errorStatus, r as errorToWire, t as errorFromWire } from "./wire-2QpU1EtJ.js";
2
+ import { r as STREAM_TIMINGS } from "./internal-D6wNxTck.js";
3
+ import { a as eventToWire, n as errorStatus, r as errorToWire, t as errorFromWire } from "./wire-BVsgR8o9.js";
4
4
  //#region src/http.ts
5
5
  /**
6
- * a2/http — route-side transport helpers (specs/a2-api.md §9–10).
6
+ * experimental-a2/http — route-side transport helpers (specs/a2-api.md §9–10).
7
7
  *
8
8
  * `sseResponse` pipes a `session.stream()` iterable into a server-sent
9
9
  * events Response; `parsePushBody` validates the push envelope;
@@ -39,6 +39,7 @@ async function parsePushBody(req) {
39
39
  payload
40
40
  };
41
41
  if (id !== void 0) out.id = id;
42
+ Object.defineProperty(out, "~a2.pushed", { value: true });
42
43
  return out;
43
44
  })
44
45
  };
@@ -0,0 +1,19 @@
1
+ import { t as A2Error } from "./errors-BJRMd-h6.js";
2
+ //#region src/idempotent-replay.ts
3
+ const canonicalJson = (value) => JSON.stringify(value, (_key, candidate) => {
4
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) return candidate;
5
+ return Object.fromEntries(Object.entries(candidate).toSorted(([left], [right]) => left.localeCompare(right)));
6
+ }) ?? "null";
7
+ const idempotentReplay = (events, existing) => {
8
+ const byId = new Map(existing.map((event) => [event.id, event]));
9
+ const ordered = events.map((event) => event.id === void 0 ? void 0 : byId.get(event.id));
10
+ const firstIndex = ordered[0]?.index;
11
+ const batchSize = events[0]?.cause?.batchSize;
12
+ if (firstIndex === void 0 || batchSize !== void 0 && batchSize !== events.length || ordered.some((stored, index) => {
13
+ const event = events[index];
14
+ return stored === void 0 || stored.index !== firstIndex + index || stored.type !== event.type || stored.lane !== (event.lane ?? null) || stored.cause?.batchSize !== event.cause?.batchSize || canonicalJson(stored.payload) !== canonicalJson(event.payload);
15
+ })) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "already-appended events do not match the retried batch");
16
+ return ordered;
17
+ };
18
+ //#endregion
19
+ export { idempotentReplay as t };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as ReducerOptions, c as ReducerBuilder, i as EventDefs, l as StandardSchemaV1, n as Contract, o as contract, r as ContractEvent, s as Reducer, t as AppendInput } from "./contract-DL8btVd9.js";
2
- import { a as Event } from "./log-Dg1I8NRr.js";
3
- import { i as A2Telemetry, n as A2SpanHandle, r as A2SpanName, t as A2AttributeValue } from "./telemetry-C78al20p.js";
1
+ import { a as EventDefs, c as Reducer, i as EventBatchFactory, l as ReducerBuilder, n as Contract, o as ReducerOptions, r as ContractEvent, s as contract, t as AppendInput, u as StandardSchemaV1 } from "./contract-C_3dIIEU.js";
2
+ import { a as Event } from "./log-ldf5g8Cx.js";
3
+ import { i as A2Telemetry, n as A2SpanHandle, r as A2SpanName, t as A2AttributeValue } from "./telemetry-Cso0qyHQ.js";
4
4
  import { n as A2ErrorCode, t as A2Error } from "./errors-xL_JTXsY.js";
5
- export { type A2AttributeValue, A2Error, type A2ErrorCode, type A2SpanHandle, type A2SpanName, type A2Telemetry, type AppendInput, type Contract, type ContractEvent, type Event, type EventDefs, type Reducer, type ReducerBuilder, type ReducerOptions, type StandardSchemaV1, contract };
5
+ export { type A2AttributeValue, A2Error, type A2ErrorCode, type A2SpanHandle, type A2SpanName, type A2Telemetry, type AppendInput, type Contract, type ContractEvent, type Event, type EventBatchFactory, type EventDefs, type Reducer, type ReducerBuilder, type ReducerOptions, type StandardSchemaV1, contract };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as contract } from "./contract-B0kAXoaL.js";
1
+ import { t as contract } from "./contract-CG_adnu_.js";
2
2
  import { t as A2Error } from "./errors-BJRMd-h6.js";
3
3
  export { A2Error, contract };
@@ -1,13 +1,13 @@
1
1
  //#region src/internal.ts
2
2
  const serverInternals = /* @__PURE__ */ new WeakMap();
3
3
  /**
4
- * Drain timing knobs. Mutable only as a white-box test seam — lease
4
+ * Drain timing knobs. Mutable only as a white-box test seam. Claim
5
5
  * heartbeats run on real timers, so tests shrink these to keep suites
6
6
  * fast. Production always uses the defaults.
7
7
  */
8
8
  const DRAIN_TIMINGS = {
9
- leaseTtlMs: 5e3,
10
- leaseHeartbeatMs: 2e3,
9
+ claimTtlMs: 5e3,
10
+ claimHeartbeatMs: 2e3,
11
11
  recoveryGraceMs: 1e3,
12
12
  recoveryArmTimeoutMs: 2e3
13
13
  };
@@ -3,8 +3,8 @@
3
3
  * The A2Log interface — the storage contract every log backend
4
4
  * implements. See specs/a2-implementation.md §2–3.
5
5
  *
6
- * This is the whole storage contract: append, read, the drain markers,
7
- * the failure markers, leases, snapshots, and the live stream. Recovery
6
+ * This is the whole storage contract: append, read, dispatch claims,
7
+ * failure markers, snapshots, and the live stream. Recovery
8
8
  * needs nothing extra — the armed queue message is its own state, and
9
9
  * the log is the only thing it consults.
10
10
  */
@@ -22,25 +22,35 @@ type Event = {
22
22
  type EventCause = {
23
23
  index: number;
24
24
  attempt: number;
25
+ /** Size of one named handler append, used to reject truncated retries. */
26
+ batchSize?: number;
25
27
  };
26
28
  /**
27
29
  * What the log stores: immutable event history, including its causal edge,
28
- * plus derived drain and failure bookkeeping. The bookkeeping is disposable;
30
+ * plus derived dispatch and failure bookkeeping. The bookkeeping is disposable;
29
31
  * the event and its cause are not.
30
32
  */
31
33
  type StoredEvent = Event & {
32
- /** Same-session handler dispatch that appended this event; null means root or legacy unknown. */
34
+ /** Same-session handler dispatch that appended this event; null means root. */
33
35
  cause: EventCause | null;
34
- /** Adapter clock time recorded by completion or manual skip; null while pending. */
36
+ /** Session-scoped serial execution key resolved when the event is appended. */
37
+ lane: string | null;
38
+ /** Adapter clock time recorded by append settlement, completion, or manual skip; null while pending. */
35
39
  processedAt: Date | null;
36
- /** Dispatch attempt that completed this event; null for pending or administrative completion. */
40
+ /** Dispatch attempt that completed this event; null without dispatch or while pending. */
37
41
  processedByAttempt: number | null;
42
+ /** Exact ordered child ids atomically returned by the completing attempt. */
43
+ returnedEventIds: string[] | null;
38
44
  /** Adapter clock time recorded by the first durable dispatch claim. */
39
45
  firstClaimedAt: Date | null;
40
46
  /** Adapter clock time recorded by the most recent durable dispatch claim. */
41
47
  lastClaimedAt: Date | null;
42
48
  /** Durable dispatch claims, including claims abandoned by hard kills. */
43
49
  attemptCount: number;
50
+ /** Current dispatch holder; null when the event is not claimed. */
51
+ claimHolder: string | null;
52
+ /** Adapter clock expiry for the current dispatch claim. */
53
+ claimExpiresAt: Date | null;
44
54
  /** Caught handler failures. This alone drives dead-lettering. */
45
55
  failureCount: number;
46
56
  /** Adapter clock time recorded by the most recent caught handler failure. */
@@ -89,7 +99,7 @@ type StoredSessionPage = {
89
99
  /**
90
100
  * Optional read-only administration implemented by A2's shipped logs.
91
101
  * It is separate from the correctness contract so custom logs do not
92
- * need to expose storage-wide discovery merely to store and drain events.
102
+ * need to expose storage-wide discovery merely to process events.
93
103
  */
94
104
  type A2LogInspection = {
95
105
  listSessions(options: {
@@ -100,17 +110,21 @@ type A2LogInspection = {
100
110
  }): Promise<StoredSessionPage>;
101
111
  listSnapshots(sessionId: string): Promise<StoredSnapshot[]>;
102
112
  };
103
- /** The result of atomically claiming the next event in a drain. */
104
- type LogClaimResult = {
113
+ /** The result of atomically claiming every currently eligible event. */
114
+ type LogClaimAvailableResult = {
105
115
  outcome: "claimed";
106
- event: StoredEvent;
116
+ events: StoredEvent[];
107
117
  } | {
108
118
  outcome: "busy";
119
+ retryAt: Date;
109
120
  } | {
110
121
  outcome: "settled";
111
122
  };
112
123
  /** A completion may lose to a newer claim or an earlier completion. */
113
- type LogHandoffResult = LogClaimResult | {
124
+ type CompleteAttemptResult = {
125
+ outcome: "completed";
126
+ events: StoredEvent[];
127
+ } | {
114
128
  outcome: "superseded";
115
129
  };
116
130
  /** The result of atomically recording a caught handler failure. */
@@ -124,11 +138,25 @@ type AppendEvent = {
124
138
  payload: unknown;
125
139
  /** Caller-supplied idempotency key; generated when absent. */
126
140
  id?: string;
127
- /** Internal causal edge supplied atomically by `ctx.append`. */
141
+ /** Internal causal edge supplied atomically by handler `session.append`. */
128
142
  cause?: EventCause;
143
+ /** Session-scoped serial execution key resolved before persistence. */
144
+ lane?: string;
145
+ /** Internal hint: settle this event in the append transaction; no handler is registered. */
146
+ settled?: true;
147
+ };
148
+ /** A handler-returned event with the deterministic id recovery requires. */
149
+ type ReturnedEvent = AppendEvent & {
150
+ id: string;
151
+ };
152
+ /** The rows written by an append and the session's pending state. */
153
+ type LogAppendResult = {
154
+ events: StoredEvent[];
155
+ /** Whether the session contains an event without a completion marker. */
156
+ hasPending: boolean;
129
157
  };
130
158
  /**
131
- * An injectable clock. Adapters take one so tests can drive lease
159
+ * An injectable clock. Adapters take one so tests can drive claim
132
160
  * expiry, failure timestamps, and (later) stuck-session detection
133
161
  * deterministically — against real storage, no mocking.
134
162
  */
@@ -146,43 +174,53 @@ interface A2Log {
146
174
  * lost — return the existing rows as success. If only *some* ids
147
175
  * exist, the caller mixed an already-sent batch with fresh events —
148
176
  * always a caller bug — so throw `A2Error('PARTIAL_DUPLICATE_BATCH')`.
177
+ * Events carrying `settled: true` get `processedAt` in this same atomic
178
+ * operation, with no dispatch claim or `processedByAttempt`. The result's
179
+ * `hasPending` reflects the whole session in the same atomic operation,
180
+ * including older events and idempotent retries.
149
181
  */
150
- append(sessionId: string, events: AppendEvent[]): Promise<StoredEvent[]>;
182
+ append(sessionId: string, events: AppendEvent[]): Promise<LogAppendResult>;
151
183
  /** Events for one session, oldest first. */
152
184
  read(sessionId: string, opts?: {
153
185
  afterIndex?: number;
154
- unprocessedOnly?: boolean;
155
186
  }): Promise<StoredEvent[]>;
156
187
  /**
157
- * Atomically selects the first pending event, checks its scope, acquires the
158
- * session lease, increments its durable dispatch count, and records the
159
- * operation's clock time. A claimed event's `attemptCount` is the 1-based
160
- * attempt passed to its handler.
188
+ * Atomically claims every eligible pending event. Unlaned events are all
189
+ * independently eligible. Within a lane, only the lowest-index unfinished
190
+ * event is eligible. A live claim produces `busy` only when it is the sole
191
+ * remaining obstacle to actionable work. Claimed events are returned in log
192
+ * order. Excluded rows remain lane barriers.
161
193
  */
162
- claimNext(options: {
194
+ claimAvailable(options: {
163
195
  sessionId: string;
164
196
  holder: string;
165
197
  ttlMs: number;
166
198
  expiresAtMs?: number;
167
- /** Inclusive causal-tree frontier. Omit for a full-session drain. */
168
- maxIndex?: number;
169
- }): Promise<LogClaimResult>;
199
+ excludeIndexes?: readonly number[];
200
+ }): Promise<LogClaimAvailableResult>;
170
201
  /**
171
- * Atomically marks one event processed only if its attempt is still current
172
- * and it has no completion marker. While this holder still owns a live lease,
173
- * claims the next pending event. Completion and any claimed successor record
174
- * the same adapter clock value.
202
+ * Renews the listed live claims still owned by `holder`. Missing, expired,
203
+ * completed, failed, or superseded claims are omitted from the result. The
204
+ * renewed indexes are returned in log order.
175
205
  */
176
- completeAndClaimNext(options: {
206
+ renewClaims(options: {
177
207
  sessionId: string;
178
208
  holder: string;
179
- completedIndex: number;
209
+ indexes: number[];
210
+ ttlMs: number;
211
+ expiresAtMs?: number;
212
+ }): Promise<number[]>;
213
+ /**
214
+ * Atomically completes one current attempt and appends its returned events.
215
+ * Retrying a committed completion with the same attempt and deterministic
216
+ * child ids returns the existing children. A stale attempt never appends.
217
+ */
218
+ completeAttempt(options: {
219
+ sessionId: string;
220
+ index: number;
180
221
  attempt: number;
181
- /** Inclusive causal-tree frontier. Omit for a full-session drain. */
182
- maxIndex?: number;
183
- }): Promise<LogHandoffResult>;
184
- /** The drain-completion marker and drain idempotency check. */
185
- markProcessed(sessionId: string, index: number): Promise<void>;
222
+ events: ReturnedEvent[];
223
+ }): Promise<CompleteAttemptResult>;
186
224
  /**
187
225
  * Atomically records a caught failure for one claimed attempt. A stale
188
226
  * attempt cannot poison a processed event or a newer dispatch. Accepted
@@ -195,8 +233,6 @@ interface A2Log {
195
233
  error: string;
196
234
  maxFailures: number;
197
235
  }): Promise<FailAttemptResult>;
198
- /** Dead-letters the event. */
199
- markFailed(sessionId: string, index: number): Promise<void>;
200
236
  /**
201
237
  * Reads a reducer snapshot and its event tail as one consistent adapter
202
238
  * operation. On a cache miss, `snapshot` is null and `events` is the full
@@ -210,26 +246,8 @@ interface A2Log {
210
246
  * (`where up_to_index < excluded.up_to_index`).
211
247
  */
212
248
  putSnapshot(sessionId: string, reducerName: string, index: number, state: unknown): Promise<void>;
213
- /** Read-only operational data used by `a2/devtools/server`. */
249
+ /** Read-only operational data used by `experimental-a2/devtools/server`. */
214
250
  inspect?: A2LogInspection;
215
- /**
216
- * One lease per session — serializes processing order for a session,
217
- * not individual events. TTL-bounded, never held indefinitely.
218
- * Re-acquiring with the same holder renews the TTL.
219
- */
220
- lease: {
221
- acquire(options: {
222
- sessionId: string;
223
- holder: string;
224
- ttlMs: number;
225
- /** Optional absolute expiry used for platform-deadline lease windows. */
226
- expiresAtMs?: number;
227
- }): Promise<boolean>;
228
- release(options: {
229
- sessionId: string;
230
- holder: string;
231
- }): Promise<void>;
232
- };
233
251
  /**
234
252
  * A live feed of one session's events, starting after `startAt`
235
253
  * (exclusive). Transport is the backend's choice — in-process pub/sub,
@@ -242,4 +260,4 @@ interface A2Log {
242
260
  }): AsyncIterable<Event>;
243
261
  }
244
262
  //#endregion
245
- export { Event as a, IdSource as c, LogStateRead as d, StoredEvent as f, StoredSnapshot as h, Clock as i, LogClaimResult as l, StoredSessionSummary as m, A2LogInspection as n, EventCause as o, StoredSessionPage as p, AppendEvent as r, FailAttemptResult as s, A2Log as t, LogHandoffResult as u };
263
+ export { Event as a, IdSource as c, LogStateRead as d, StoredEvent as f, StoredSnapshot as h, Clock as i, LogAppendResult as l, StoredSessionSummary as m, A2LogInspection as n, EventCause as o, StoredSessionPage as p, AppendEvent as r, FailAttemptResult as s, A2Log as t, LogClaimAvailableResult as u };
@@ -1,4 +1,4 @@
1
- import { c as IdSource, i as Clock, t as A2Log } from "./log-Dg1I8NRr.js";
1
+ import { c as IdSource, i as Clock, t as A2Log } from "./log-ldf5g8Cx.js";
2
2
  //#region src/log-memory.d.ts
3
3
  type MemoryLogOptions = {
4
4
  /** Injectable clock — every stored timestamp comes from here. */