experimental-a2 0.7.0 → 0.8.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.
- package/CHANGELOG.md +19 -0
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +13 -11
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-286j79Mt.js → server-B2XNevQA.js} +123 -53
- package/dist/server-B2XNevQA.js.map +1 -0
- package/dist/{server-DgXmORIq.d.ts → server-DjPhHnbI.d.ts} +7 -4
- package/dist/server-DjPhHnbI.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-flRz1OWh.d.ts → store-RJO35BMj.d.ts} +25 -8
- package/dist/store-RJO35BMj.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +79 -19
- package/dist/store-memory.js.map +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +230 -101
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-DEYO8Ryv.js → store-redis-core-DT01r4GZ.js} +167 -29
- package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -2
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +103 -19
- package/dist/store-sqlite.js.map +1 -1
- package/docs/concepts/02-handlers.mdx +4 -0
- package/docs/concepts/04-state.mdx +57 -9
- package/docs/guides/06-ai-agents.mdx +2 -1
- package/docs/reference/01-api.mdx +39 -16
- package/package.json +1 -1
- package/src/ai-server.ts +27 -10
- package/src/server.ts +242 -87
- package/src/store-memory.ts +138 -20
- package/src/store-postgres.ts +355 -138
- package/src/store-redis-core.ts +201 -27
- package/src/store-redis-http.ts +1 -1
- package/src/store-redis.ts +1 -1
- package/src/store-sqlite.ts +191 -34
- package/src/store.ts +27 -9
- package/dist/server-286j79Mt.js.map +0 -1
- package/dist/server-DgXmORIq.d.ts.map +0 -1
- package/dist/store-flRz1OWh.d.ts.map +0 -1
- package/dist/store-redis-core-DEYO8Ryv.js.map +0 -1
package/dist/store-postgres.js
CHANGED
|
@@ -71,6 +71,26 @@ create table if not exists a2_snapshots (
|
|
|
71
71
|
primary key (session_id, reducer_name)
|
|
72
72
|
);
|
|
73
73
|
|
|
74
|
+
create table if not exists a2_snapshot_history (
|
|
75
|
+
session_id text not null,
|
|
76
|
+
reducer_name text not null,
|
|
77
|
+
up_to_index bigint not null,
|
|
78
|
+
state jsonb not null,
|
|
79
|
+
updated_at bigint not null,
|
|
80
|
+
primary key (session_id, reducer_name, up_to_index)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
create table if not exists a2_snapshot_pins (
|
|
84
|
+
session_id text not null,
|
|
85
|
+
event_index bigint not null,
|
|
86
|
+
reducer_name text not null,
|
|
87
|
+
up_to_index bigint not null,
|
|
88
|
+
primary key (session_id, event_index, reducer_name, up_to_index)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
create index if not exists a2_snapshot_pins_checkpoint
|
|
92
|
+
on a2_snapshot_pins (session_id, reducer_name, up_to_index);
|
|
93
|
+
|
|
74
94
|
create table if not exists a2_presence (
|
|
75
95
|
ns text not null,
|
|
76
96
|
participant text not null,
|
|
@@ -175,6 +195,90 @@ function postgres(options = {}) {
|
|
|
175
195
|
}
|
|
176
196
|
});
|
|
177
197
|
const client = connection.get;
|
|
198
|
+
const readStateBatch = async (requests, reducerName) => {
|
|
199
|
+
if (requests.length === 0) return [];
|
|
200
|
+
const c = await client();
|
|
201
|
+
const input = requests.map((request, position) => ({
|
|
202
|
+
position,
|
|
203
|
+
sessionId: request.sessionId,
|
|
204
|
+
throughIndex: request.throughIndex ?? null,
|
|
205
|
+
snapshotThroughIndex: request.snapshotThroughIndex ?? request.throughIndex ?? null
|
|
206
|
+
}));
|
|
207
|
+
const { rows } = await c.query(`with request as materialized (
|
|
208
|
+
select * from jsonb_to_recordset($1::jsonb) as value(
|
|
209
|
+
position integer,
|
|
210
|
+
"sessionId" text,
|
|
211
|
+
"throughIndex" bigint,
|
|
212
|
+
"snapshotThroughIndex" bigint
|
|
213
|
+
)
|
|
214
|
+
), selected as materialized (
|
|
215
|
+
select request.position,
|
|
216
|
+
request."sessionId" as session_id,
|
|
217
|
+
request."throughIndex" as through_index,
|
|
218
|
+
head.up_to_index as head_index,
|
|
219
|
+
candidate.up_to_index as snapshot_index,
|
|
220
|
+
candidate.state as snapshot_state
|
|
221
|
+
from request
|
|
222
|
+
left join a2_snapshots head
|
|
223
|
+
on head.session_id = request."sessionId"
|
|
224
|
+
and head.reducer_name = $2
|
|
225
|
+
left join lateral (
|
|
226
|
+
select up_to_index, state from (
|
|
227
|
+
select head.up_to_index, head.state
|
|
228
|
+
where request."snapshotThroughIndex" is null
|
|
229
|
+
or head.up_to_index <= request."snapshotThroughIndex"
|
|
230
|
+
union all
|
|
231
|
+
select history.up_to_index, history.state
|
|
232
|
+
from a2_snapshot_history history
|
|
233
|
+
where history.session_id = request."sessionId"
|
|
234
|
+
and history.reducer_name = $2
|
|
235
|
+
and (
|
|
236
|
+
request."snapshotThroughIndex" is null
|
|
237
|
+
or history.up_to_index <= request."snapshotThroughIndex"
|
|
238
|
+
)
|
|
239
|
+
) candidates
|
|
240
|
+
order by up_to_index desc
|
|
241
|
+
limit 1
|
|
242
|
+
) candidate on true
|
|
243
|
+
)
|
|
244
|
+
select selected.position, 0 as row_order, selected.session_id,
|
|
245
|
+
selected.head_index, selected.snapshot_index,
|
|
246
|
+
selected.snapshot_state,
|
|
247
|
+
null::bigint as idx, null::text as event_type,
|
|
248
|
+
null::jsonb as payload, null::text as event_id,
|
|
249
|
+
null::bigint as created_at
|
|
250
|
+
from selected
|
|
251
|
+
union all
|
|
252
|
+
select selected.position, 1 as row_order, event.session_id,
|
|
253
|
+
selected.head_index, null::bigint as snapshot_index,
|
|
254
|
+
null::jsonb as snapshot_state,
|
|
255
|
+
event.idx, event.event_type, event.payload,
|
|
256
|
+
event.event_id, event.created_at
|
|
257
|
+
from selected
|
|
258
|
+
join a2_events event on event.session_id = selected.session_id
|
|
259
|
+
where event.idx > coalesce(selected.snapshot_index, 0)
|
|
260
|
+
and (
|
|
261
|
+
selected.through_index is null
|
|
262
|
+
or event.idx <= selected.through_index
|
|
263
|
+
)
|
|
264
|
+
order by position, row_order, idx`, [jsonb(input), reducerName]);
|
|
265
|
+
const reads = requests.map(() => ({
|
|
266
|
+
headIndex: null,
|
|
267
|
+
snapshot: null,
|
|
268
|
+
events: []
|
|
269
|
+
}));
|
|
270
|
+
for (const row of rows) {
|
|
271
|
+
const read = reads[asNumber(row["position"])];
|
|
272
|
+
if (row["row_order"] === 0 || row["row_order"] === "0") {
|
|
273
|
+
read.headIndex = row["head_index"] === null ? null : asNumber(row["head_index"]);
|
|
274
|
+
if (row["snapshot_index"] !== null) read.snapshot = {
|
|
275
|
+
index: asNumber(row["snapshot_index"]),
|
|
276
|
+
state: row["snapshot_state"]
|
|
277
|
+
};
|
|
278
|
+
} else read.events.push(toEvent(row));
|
|
279
|
+
}
|
|
280
|
+
return reads;
|
|
281
|
+
};
|
|
178
282
|
let mutex = Promise.resolve();
|
|
179
283
|
const withTx = async (fn) => {
|
|
180
284
|
const c = await client();
|
|
@@ -520,6 +624,16 @@ function postgres(options = {}) {
|
|
|
520
624
|
index,
|
|
521
625
|
jsonb(ids)
|
|
522
626
|
]);
|
|
627
|
+
await query(`delete from a2_snapshot_pins
|
|
628
|
+
where session_id = $1 and event_index = $2`, [sessionId, index]);
|
|
629
|
+
await query(`delete from a2_snapshot_history history
|
|
630
|
+
where history.session_id = $1
|
|
631
|
+
and not exists (
|
|
632
|
+
select 1 from a2_snapshot_pins pin
|
|
633
|
+
where pin.session_id = history.session_id
|
|
634
|
+
and pin.reducer_name = history.reducer_name
|
|
635
|
+
and pin.up_to_index = history.up_to_index
|
|
636
|
+
)`, [sessionId]);
|
|
523
637
|
const parentLane = parent["lane"] ?? null;
|
|
524
638
|
if (parentLane !== null) await query(`update a2_events set lane_ready = true
|
|
525
639
|
where session_id = $1
|
|
@@ -535,8 +649,8 @@ function postgres(options = {}) {
|
|
|
535
649
|
}));
|
|
536
650
|
},
|
|
537
651
|
async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
|
|
538
|
-
return wrap(async () => {
|
|
539
|
-
const { rows } = await
|
|
652
|
+
return wrap(() => withTx(async (query) => {
|
|
653
|
+
const { rows } = await query(`with current as materialized (
|
|
540
654
|
select e.* from a2_events e
|
|
541
655
|
where e.session_id = $1 and e.idx = $2
|
|
542
656
|
for update of e
|
|
@@ -593,116 +707,131 @@ function postgres(options = {}) {
|
|
|
593
707
|
]);
|
|
594
708
|
const row = rows[0];
|
|
595
709
|
if (!row || row["outcome"] === "missing") throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
710
|
+
if (row["outcome"] === "dead_lettered") {
|
|
711
|
+
await query(`delete from a2_snapshot_pins
|
|
712
|
+
where session_id = $1 and event_index = $2`, [sessionId, index]);
|
|
713
|
+
await query(`delete from a2_snapshot_history history
|
|
714
|
+
where history.session_id = $1
|
|
715
|
+
and not exists (
|
|
716
|
+
select 1 from a2_snapshot_pins pin
|
|
717
|
+
where pin.session_id = history.session_id
|
|
718
|
+
and pin.reducer_name = history.reducer_name
|
|
719
|
+
and pin.up_to_index = history.up_to_index
|
|
720
|
+
)`, [sessionId]);
|
|
721
|
+
}
|
|
596
722
|
return {
|
|
597
723
|
outcome: row["outcome"],
|
|
598
724
|
failureCount: asNumber(row["failure_count"])
|
|
599
725
|
};
|
|
600
|
-
});
|
|
726
|
+
}));
|
|
601
727
|
},
|
|
602
|
-
async readState(sessionId, reducerName) {
|
|
603
|
-
return wrap(async () => {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
)
|
|
609
|
-
select 0 as row_order, 'snapshot' as row_kind,
|
|
610
|
-
snapshot.up_to_index as snapshot_index,
|
|
611
|
-
snapshot.state as snapshot_state,
|
|
612
|
-
null::text as session_id, null::bigint as idx,
|
|
613
|
-
null::text as event_type, null::jsonb as payload,
|
|
614
|
-
null::text as event_id, null::bigint as created_at
|
|
615
|
-
from snapshot
|
|
616
|
-
union all
|
|
617
|
-
select 1 as row_order, 'event' as row_kind,
|
|
618
|
-
null::bigint as snapshot_index,
|
|
619
|
-
null::jsonb as snapshot_state,
|
|
620
|
-
event.session_id, event.idx, event.event_type,
|
|
621
|
-
event.payload, event.event_id, event.created_at
|
|
622
|
-
from a2_events event
|
|
623
|
-
where event.session_id = $1
|
|
624
|
-
and event.idx > coalesce((select up_to_index from snapshot), 0)
|
|
625
|
-
order by row_order, idx`, [sessionId, reducerName]);
|
|
626
|
-
const snapshot = rows.find((row) => row["row_kind"] === "snapshot");
|
|
627
|
-
return {
|
|
628
|
-
snapshot: snapshot ? {
|
|
629
|
-
index: asNumber(snapshot["snapshot_index"]),
|
|
630
|
-
state: snapshot["snapshot_state"]
|
|
631
|
-
} : null,
|
|
632
|
-
events: rows.filter((row) => row["row_kind"] === "event").map(toEvent)
|
|
633
|
-
};
|
|
634
|
-
});
|
|
728
|
+
async readState(sessionId, reducerName, stateOptions) {
|
|
729
|
+
return wrap(async () => (await readStateBatch([{
|
|
730
|
+
sessionId,
|
|
731
|
+
...stateOptions?.throughIndex === void 0 ? {} : { throughIndex: stateOptions.throughIndex },
|
|
732
|
+
...stateOptions?.snapshotThroughIndex === void 0 ? {} : { snapshotThroughIndex: stateOptions.snapshotThroughIndex }
|
|
733
|
+
}], reducerName))[0]);
|
|
635
734
|
},
|
|
636
|
-
async readStates(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
let read = bySession.get(sessionId);
|
|
667
|
-
if (!read) {
|
|
668
|
-
read = {
|
|
669
|
-
snapshot: null,
|
|
670
|
-
events: []
|
|
671
|
-
};
|
|
672
|
-
bySession.set(sessionId, read);
|
|
673
|
-
}
|
|
674
|
-
return read;
|
|
675
|
-
};
|
|
676
|
-
for (const row of rows) {
|
|
677
|
-
const read = readOf(row["session_id"]);
|
|
678
|
-
if (row["row_kind"] === "snapshot") read.snapshot = {
|
|
679
|
-
index: asNumber(row["snapshot_index"]),
|
|
680
|
-
state: row["snapshot_state"]
|
|
681
|
-
};
|
|
682
|
-
else read.events.push(toEvent(row));
|
|
735
|
+
async readStates(requests, reducerName) {
|
|
736
|
+
return wrap(() => readStateBatch(requests, reducerName));
|
|
737
|
+
},
|
|
738
|
+
async putSnapshots(sessionId, reducerName, writes) {
|
|
739
|
+
await wrap(() => withTx(async (query) => {
|
|
740
|
+
const normalized = [...new Map(writes.map((write) => [write.index, write])).values()].toSorted((a, b) => a.index - b.index);
|
|
741
|
+
if (normalized.length === 0) return;
|
|
742
|
+
await query("select pg_advisory_xact_lock(hashtext($1), hashtext($2))", [sessionId, reducerName]);
|
|
743
|
+
const requestedPins = normalized.flatMap((write) => (write.pinEventIndexes ?? []).map((eventIndex) => ({
|
|
744
|
+
eventIndex,
|
|
745
|
+
snapshotIndex: write.index
|
|
746
|
+
})));
|
|
747
|
+
if (requestedPins.length > 0) {
|
|
748
|
+
const { rows: events } = await query(`select idx, processed_at, failed_at from a2_events
|
|
749
|
+
where session_id = $1 and idx = any($2::bigint[])
|
|
750
|
+
for update`, [sessionId, [...new Set(requestedPins.map((pin) => pin.eventIndex))]]);
|
|
751
|
+
const unfinished = new Set(events.filter((row) => row["processed_at"] === null && row["failed_at"] === null).map((row) => asNumber(row["idx"])));
|
|
752
|
+
const accepted = requestedPins.filter((pin) => unfinished.has(pin.eventIndex));
|
|
753
|
+
if (accepted.length > 0) await query(`insert into a2_snapshot_pins
|
|
754
|
+
(session_id, event_index, reducer_name, up_to_index)
|
|
755
|
+
select $1, pin."eventIndex", $2, pin."snapshotIndex"
|
|
756
|
+
from jsonb_to_recordset($3::jsonb) as pin(
|
|
757
|
+
"eventIndex" bigint,
|
|
758
|
+
"snapshotIndex" bigint
|
|
759
|
+
)
|
|
760
|
+
on conflict do nothing`, [
|
|
761
|
+
sessionId,
|
|
762
|
+
reducerName,
|
|
763
|
+
jsonb(accepted)
|
|
764
|
+
]);
|
|
683
765
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
766
|
+
const { rows: headRows } = await query(`select up_to_index, state, updated_at from a2_snapshots
|
|
767
|
+
where session_id = $1 and reducer_name = $2
|
|
768
|
+
for update`, [sessionId, reducerName]);
|
|
769
|
+
const currentHead = headRows[0];
|
|
770
|
+
const latestWrite = normalized.at(-1);
|
|
771
|
+
const writesHead = !currentHead || latestWrite.index >= asNumber(currentHead["up_to_index"]);
|
|
772
|
+
const advancesHead = !currentHead || latestWrite.index > asNumber(currentHead["up_to_index"]);
|
|
773
|
+
const finalHeadIndex = advancesHead ? latestWrite.index : asNumber(currentHead["up_to_index"]);
|
|
774
|
+
const { rows: pinRows } = await query(`select distinct up_to_index from a2_snapshot_pins
|
|
775
|
+
where session_id = $1 and reducer_name = $2
|
|
776
|
+
and up_to_index = any($3::bigint[])`, [
|
|
777
|
+
sessionId,
|
|
778
|
+
reducerName,
|
|
779
|
+
[.../* @__PURE__ */ new Set([...normalized.map((write) => write.index), ...currentHead ? [asNumber(currentHead["up_to_index"])] : []])]
|
|
780
|
+
]);
|
|
781
|
+
const pinned = new Set(pinRows.map((row) => asNumber(row["up_to_index"])));
|
|
782
|
+
const historical = /* @__PURE__ */ new Map();
|
|
783
|
+
if (advancesHead && currentHead && pinned.has(asNumber(currentHead["up_to_index"]))) {
|
|
784
|
+
const index = asNumber(currentHead["up_to_index"]);
|
|
785
|
+
historical.set(index, {
|
|
786
|
+
index,
|
|
787
|
+
state: currentHead["state"],
|
|
788
|
+
updatedAt: asNumber(currentHead["updated_at"])
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
const updatedAt = clock.now().getTime();
|
|
792
|
+
for (const write of normalized) if (write.index < finalHeadIndex && pinned.has(write.index)) historical.set(write.index, {
|
|
793
|
+
index: write.index,
|
|
794
|
+
state: write.state,
|
|
795
|
+
updatedAt
|
|
687
796
|
});
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
797
|
+
if (historical.size > 0) await query(`insert into a2_snapshot_history
|
|
798
|
+
(session_id, reducer_name, up_to_index, state, updated_at)
|
|
799
|
+
select $1, $2, snapshot.index, snapshot.state, snapshot."updatedAt"
|
|
800
|
+
from jsonb_to_recordset($3::jsonb) as snapshot(
|
|
801
|
+
index bigint,
|
|
802
|
+
state jsonb,
|
|
803
|
+
"updatedAt" bigint
|
|
804
|
+
)
|
|
805
|
+
on conflict (session_id, reducer_name, up_to_index) do update set
|
|
806
|
+
state = excluded.state,
|
|
807
|
+
updated_at = excluded.updated_at`, [
|
|
699
808
|
sessionId,
|
|
700
809
|
reducerName,
|
|
701
|
-
|
|
702
|
-
jsonb(state),
|
|
703
|
-
clock.now().getTime()
|
|
810
|
+
jsonb([...historical.values()])
|
|
704
811
|
]);
|
|
705
|
-
|
|
812
|
+
if (writesHead) {
|
|
813
|
+
await query(`insert into a2_snapshots
|
|
814
|
+
(session_id, reducer_name, up_to_index, state, updated_at)
|
|
815
|
+
values ($1, $2, $3, $4::jsonb, $5)
|
|
816
|
+
on conflict (session_id, reducer_name) do update set
|
|
817
|
+
up_to_index = excluded.up_to_index,
|
|
818
|
+
state = excluded.state,
|
|
819
|
+
updated_at = excluded.updated_at
|
|
820
|
+
where excluded.up_to_index >= a2_snapshots.up_to_index`, [
|
|
821
|
+
sessionId,
|
|
822
|
+
reducerName,
|
|
823
|
+
latestWrite.index,
|
|
824
|
+
jsonb(latestWrite.state),
|
|
825
|
+
clock.now().getTime()
|
|
826
|
+
]);
|
|
827
|
+
await query(`delete from a2_snapshot_history
|
|
828
|
+
where session_id = $1 and reducer_name = $2 and up_to_index = $3`, [
|
|
829
|
+
sessionId,
|
|
830
|
+
reducerName,
|
|
831
|
+
latestWrite.index
|
|
832
|
+
]);
|
|
833
|
+
}
|
|
834
|
+
}));
|
|
706
835
|
},
|
|
707
836
|
presence: {
|
|
708
837
|
async set(ns, participant, values, meta) {
|