experimental-a2 0.19.0 → 0.20.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 +16 -0
- package/dist/actor.js +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +34 -4
- package/dist/ai-fx.js.map +1 -1
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -1
- package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-Clbg5ei7.js} +11 -2
- package/dist/ai-harness-Clbg5ei7.js.map +1 -0
- package/dist/ai-pi.js +2 -2
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +6 -4
- package/dist/ai-server.js.map +1 -1
- package/dist/internal-CscSY7K_.js.map +1 -1
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +5 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +4 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/server-DYAATFuT.d.ts.map +1 -1
- package/dist/{server-D-Y4pqwu.js → server-qyb5aJJG.js} +39 -25
- package/dist/server-qyb5aJJG.js.map +1 -0
- package/dist/server.js +1 -1
- package/dist/session-clear-DGI4jTQ5.js.map +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +38 -136
- package/dist/store-postgres.js.map +1 -1
- package/docs/01-quickstart.mdx +7 -4
- package/docs/actors/01-introduction.mdx +1 -1
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/02-tools.mdx +1 -1
- package/docs/agents/03-conversations.mdx +1 -1
- package/docs/agents/meta.ts +1 -1
- package/docs/client/meta.ts +1 -1
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/index.mdx +2 -2
- package/docs/reference/01-core.mdx +8 -8
- package/docs/reference/02-server.mdx +3 -3
- package/docs/reference/04-ai.mdx +9 -1
- package/docs/reference/05-telemetry.mdx +132 -0
- package/docs/reference/06-errors.mdx +1 -1
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +112 -0
- package/docs/schedulers/02-vercel-queues.mdx +87 -0
- package/docs/schedulers/03-qstash.mdx +123 -0
- package/docs/schedulers/meta.ts +1 -0
- package/docs/server/01-handlers.mdx +29 -2
- package/docs/server/02-timers.mdx +2 -2
- package/docs/server/06-guarantees.mdx +6 -3
- package/docs/stores/01-overview.mdx +37 -0
- package/docs/stores/02-redis.mdx +49 -0
- package/docs/stores/03-redis-http.mdx +56 -0
- package/docs/stores/04-postgres.mdx +121 -0
- package/docs/stores/05-sqlite.mdx +34 -0
- package/docs/stores/06-memory.mdx +22 -0
- package/docs/stores/meta.ts +1 -0
- package/examples/playground/DEPLOY.md +12 -7
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/chat.test.tsx +421 -330
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
- package/examples/playground/app/agent/ingress.test.ts +41 -17
- package/examples/playground/app/agent/model-input.test.tsx +92 -43
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
- package/examples/playground/app/chat/server.test.ts +38 -65
- package/examples/playground/app/components/activity-history.test.tsx +60 -18
- package/examples/playground/app/components/fast-link.test.tsx +3 -5
- package/examples/playground/app/initial-state.test.tsx +86 -45
- package/examples/playground/package.json +2 -1
- package/examples/playground/test/agent.ts +35 -0
- package/examples/playground/test/model.ts +149 -0
- package/examples/playground/test/serve.ts +184 -0
- package/examples/playground/test/server-only.ts +1 -0
- package/examples/playground/vitest.config.ts +27 -0
- package/package.json +3 -1
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +137 -0
- package/src/ai-agent.ts +5 -4
- package/src/ai-fx.ts +37 -3
- package/src/ai-harness.ts +18 -0
- package/src/errors.ts +1 -1
- package/src/internal.ts +2 -1
- package/src/postgres-notifications.ts +0 -22
- package/src/postgres-resources.ts +6 -9
- package/src/postgres-schema.ts +80 -0
- package/src/scheduler-qstash.ts +12 -5
- package/src/scheduler-vercel.ts +10 -4
- package/src/server.ts +66 -32
- package/src/store-postgres.ts +5 -123
- package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
- package/dist/server-D-Y4pqwu.js.map +0 -1
- package/docs/reference/05-adapters.mdx +0 -351
- package/docs/server/05-production.mdx +0 -323
package/src/internal.ts
CHANGED
|
@@ -7,7 +7,8 @@ import type { ScheduledEvent } from './scheduler-task.ts'
|
|
|
7
7
|
import type { DrainableServer } from './server.ts'
|
|
8
8
|
import type { Event } from './store.ts'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** `failed`: a scheduler drain threw after arming its successor. */
|
|
11
|
+
export type DrainOutcome = 'busy' | 'stalled' | 'settled' | 'failed'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* A null-prototype record for maps keyed by caller-controlled strings
|
|
@@ -220,25 +220,3 @@ export function postgresNotifications(
|
|
|
220
220
|
}
|
|
221
221
|
return manager
|
|
222
222
|
}
|
|
223
|
-
|
|
224
|
-
export const POSTGRES_NOTIFICATIONS_SQL = `
|
|
225
|
-
create or replace function a2_notify_events() returns trigger language plpgsql as $$
|
|
226
|
-
begin
|
|
227
|
-
perform pg_notify('a2_events_' || md5(new.session_id), '');
|
|
228
|
-
return null;
|
|
229
|
-
end $$;
|
|
230
|
-
create or replace function a2_notify_presence() returns trigger language plpgsql as $$
|
|
231
|
-
begin
|
|
232
|
-
perform pg_notify('a2_presence_' || md5(case when TG_OP = 'DELETE' then old.ns else new.ns end),
|
|
233
|
-
coalesce(current_setting('a2.presence_source', true), ''));
|
|
234
|
-
return null;
|
|
235
|
-
end $$;
|
|
236
|
-
do $$ begin
|
|
237
|
-
if not exists (select 1 from pg_trigger where tgname = 'a2_notify_events' and tgrelid = 'a2_events'::regclass) then
|
|
238
|
-
create trigger a2_notify_events after insert on a2_events for each row execute function a2_notify_events();
|
|
239
|
-
end if;
|
|
240
|
-
if not exists (select 1 from pg_trigger where tgname = 'a2_notify_presence' and tgrelid = 'a2_presence'::regclass) then
|
|
241
|
-
create trigger a2_notify_presence after insert or update or delete on a2_presence for each row execute function a2_notify_presence();
|
|
242
|
-
end if;
|
|
243
|
-
end $$;
|
|
244
|
-
`
|
|
@@ -15,10 +15,7 @@ export type PostgresQueries = {
|
|
|
15
15
|
client: PostgresClient
|
|
16
16
|
raw: PostgresClient
|
|
17
17
|
exclusive<T>(fn: () => Promise<T>): Promise<T>
|
|
18
|
-
initialize(options: {
|
|
19
|
-
schema: string
|
|
20
|
-
run: () => Promise<void>
|
|
21
|
-
}): Promise<void>
|
|
18
|
+
initialize(options: { key: string; run: () => Promise<void> }): Promise<void>
|
|
22
19
|
close(): Promise<void>
|
|
23
20
|
}
|
|
24
21
|
|
|
@@ -128,7 +125,7 @@ export function postgresConnectionKey({
|
|
|
128
125
|
|
|
129
126
|
const queries = (raw: PostgresClient): PostgresQueries => {
|
|
130
127
|
let pending: Promise<unknown> = Promise.resolve()
|
|
131
|
-
const
|
|
128
|
+
const initialized = new Map<string, Promise<void>>()
|
|
132
129
|
const exclusive = <T>(fn: () => Promise<T>): Promise<T> => {
|
|
133
130
|
const result = pending.then(fn)
|
|
134
131
|
pending = result.catch(() => {})
|
|
@@ -140,13 +137,13 @@ const queries = (raw: PostgresClient): PostgresQueries => {
|
|
|
140
137
|
? raw
|
|
141
138
|
: { query: (text, values) => exclusive(() => raw.query(text, values)) },
|
|
142
139
|
exclusive,
|
|
143
|
-
initialize({
|
|
144
|
-
const existing =
|
|
140
|
+
initialize({ key, run }) {
|
|
141
|
+
const existing = initialized.get(key)
|
|
145
142
|
if (existing) return existing
|
|
146
143
|
const attempt = run()
|
|
147
|
-
|
|
144
|
+
initialized.set(key, attempt)
|
|
148
145
|
void attempt.catch(() => {
|
|
149
|
-
if (
|
|
146
|
+
if (initialized.get(key) === attempt) initialized.delete(key)
|
|
150
147
|
})
|
|
151
148
|
return attempt
|
|
152
149
|
},
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Postgres schema contract. A2 never runs DDL in a Postgres database:
|
|
3
|
+
* `sql/postgres/schema.sql` (and `notifications.sql` for
|
|
4
|
+
* `listenConnectionString`) create the tables, and a store checks the
|
|
5
|
+
* recorded version before its first query.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { A2Error } from './errors.ts'
|
|
9
|
+
import type { PostgresClient } from './store-postgres.ts'
|
|
10
|
+
|
|
11
|
+
/** The `a2_schema` version this release needs; `schema.sql` records it. */
|
|
12
|
+
export const POSTGRES_SCHEMA_VERSION = 1
|
|
13
|
+
|
|
14
|
+
const PACKAGE = 'node_modules/experimental-a2'
|
|
15
|
+
|
|
16
|
+
const apply = (file: string): string =>
|
|
17
|
+
`apply ${PACKAGE}/sql/postgres/${file} to this database`
|
|
18
|
+
|
|
19
|
+
const SEE_DOCS = `See ${PACKAGE}/docs/stores/04-postgres.mdx.`
|
|
20
|
+
|
|
21
|
+
const VERSION = 'select version from a2_schema'
|
|
22
|
+
const VERSION_AND_TRIGGERS = `select version, (select count(*) from pg_trigger
|
|
23
|
+
where (tgname = 'a2_notify_events' and tgrelid = to_regclass('a2_events'))
|
|
24
|
+
or (tgname = 'a2_notify_presence' and tgrelid = to_regclass('a2_presence')))
|
|
25
|
+
as notify_triggers from a2_schema`
|
|
26
|
+
|
|
27
|
+
/** Explains a failed version read, or returns undefined when the schema is not the cause. */
|
|
28
|
+
const diagnose = async (
|
|
29
|
+
client: PostgresClient,
|
|
30
|
+
): Promise<A2Error | undefined> => {
|
|
31
|
+
const [found] = (
|
|
32
|
+
await client.query(`select
|
|
33
|
+
to_regclass('a2_schema') is not null as marked,
|
|
34
|
+
to_regclass('a2_events') is not null as installed,
|
|
35
|
+
has_table_privilege(to_regclass('a2_schema'), 'select') as readable`)
|
|
36
|
+
).rows
|
|
37
|
+
if (!found) return undefined
|
|
38
|
+
if (!found['marked'])
|
|
39
|
+
return new A2Error(
|
|
40
|
+
'STORE_NOT_CONFIGURED',
|
|
41
|
+
found['installed']
|
|
42
|
+
? `This Postgres database has A2's tables but no a2_schema version. That is expected once after upgrading from a release that created its tables at runtime, and your events are untouched. A2 doesn't create or change tables at runtime: ${apply('schema.sql')}. ${SEE_DOCS}`
|
|
43
|
+
: `A2's tables aren't in this Postgres database, and A2 doesn't create tables at runtime: ${apply('schema.sql')}. ${SEE_DOCS}`,
|
|
44
|
+
)
|
|
45
|
+
if (found['readable'] === false)
|
|
46
|
+
return new A2Error(
|
|
47
|
+
'STORE_NOT_CONFIGURED',
|
|
48
|
+
`The role this store connects as can't read a2_schema. Apply schema.sql as that role, or grant it select on a2_schema and select, insert, update, and delete on A2's other tables. ${SEE_DOCS}`,
|
|
49
|
+
)
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reads the schema version (and, with a listener, the triggers) in one query that takes no lock a writer holds. */
|
|
54
|
+
export const checkPostgresSchema = async ({
|
|
55
|
+
client,
|
|
56
|
+
listen,
|
|
57
|
+
}: {
|
|
58
|
+
client: PostgresClient
|
|
59
|
+
listen: boolean
|
|
60
|
+
}): Promise<void> => {
|
|
61
|
+
let row: Record<string, unknown> | undefined
|
|
62
|
+
try {
|
|
63
|
+
row = (await client.query(listen ? VERSION_AND_TRIGGERS : VERSION)).rows[0]
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw (await diagnose(client).catch(() => undefined)) ?? err
|
|
66
|
+
}
|
|
67
|
+
const version = Number(row?.['version'] ?? 0)
|
|
68
|
+
if (version < POSTGRES_SCHEMA_VERSION) {
|
|
69
|
+
throw new A2Error(
|
|
70
|
+
'STORE_NOT_CONFIGURED',
|
|
71
|
+
`This Postgres database has A2 schema version ${version}, and this release of experimental-a2 needs version ${POSTGRES_SCHEMA_VERSION}: ${apply('schema.sql')}. ${SEE_DOCS}`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
if (listen && Number(row?.['notify_triggers']) < 2) {
|
|
75
|
+
throw new A2Error(
|
|
76
|
+
'STORE_NOT_CONFIGURED',
|
|
77
|
+
`listenConnectionString needs A2's notification triggers, which aren't in this Postgres database. After schema.sql, ${apply('notifications.sql')}. ${SEE_DOCS}`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/scheduler-qstash.ts
CHANGED
|
@@ -497,8 +497,11 @@ const readBoundedBody = async (
|
|
|
497
497
|
return new TextDecoder().decode(bytes)
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
|
|
501
|
-
|
|
500
|
+
// A store that isn't set up yet (Postgres before schema.sql, a missing driver)
|
|
501
|
+
// is fixed without changing the task, so its work waits like any outage.
|
|
502
|
+
const storeOutage = (error: unknown): error is A2Error =>
|
|
503
|
+
error instanceof A2Error &&
|
|
504
|
+
(error.code === 'STORE_UNAVAILABLE' || error.code === 'STORE_NOT_CONFIGURED')
|
|
502
505
|
|
|
503
506
|
const response = {
|
|
504
507
|
ok: (): Response => new Response('ok'),
|
|
@@ -888,7 +891,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
888
891
|
await deliverSchedulerAppend(server, task)
|
|
889
892
|
else await deliverSchedulerClear(server, task)
|
|
890
893
|
} catch (error) {
|
|
891
|
-
if (!
|
|
894
|
+
if (!storeOutage(error)) throw error
|
|
892
895
|
await sendStoreOutageSuccessor(task, outage)
|
|
893
896
|
}
|
|
894
897
|
return response.ok()
|
|
@@ -906,13 +909,17 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
906
909
|
outcome: 'unknown' as const,
|
|
907
910
|
}
|
|
908
911
|
} catch (error) {
|
|
909
|
-
if (!
|
|
912
|
+
if (!storeOutage(error)) throw error
|
|
910
913
|
await sendStoreOutageSuccessor(task, outage)
|
|
911
914
|
return response.ok()
|
|
912
915
|
}
|
|
913
916
|
if (result.settled) return response.ok()
|
|
914
917
|
|
|
915
|
-
if (
|
|
918
|
+
if (
|
|
919
|
+
result.outcome === 'busy' ||
|
|
920
|
+
result.outcome === 'stalled' ||
|
|
921
|
+
result.outcome === 'failed'
|
|
922
|
+
) {
|
|
916
923
|
if (result.schedulerArm) await result.schedulerArm
|
|
917
924
|
else {
|
|
918
925
|
await sendTask({
|
package/src/scheduler-vercel.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* the configured deployment scope for sending and receiving.
|
|
15
15
|
* - `handler(...servers)` is the delivery route: look up the server for
|
|
16
16
|
* the contract named in the message and drain it. Settled drains ack;
|
|
17
|
-
* contention
|
|
17
|
+
* contention, handler failure, and a drain that throws after arming its
|
|
18
|
+
* successor all ack once a later watchdog is durable.
|
|
18
19
|
*
|
|
19
20
|
* `@vercel/queue` is an optional peer dependency, imported lazily. The
|
|
20
21
|
* `transport` option is the injection seam — tests run a real little
|
|
@@ -342,9 +343,14 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
|
|
|
342
343
|
}
|
|
343
344
|
if (result.settled) return
|
|
344
345
|
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
346
|
+
if (
|
|
347
|
+
result.outcome === 'busy' ||
|
|
348
|
+
result.outcome === 'stalled' ||
|
|
349
|
+
result.outcome === 'failed'
|
|
350
|
+
) {
|
|
351
|
+
// The live owner arms its own claim windows, the store holds
|
|
352
|
+
// a failed event until its retry time, and a drain that failed
|
|
353
|
+
// after claiming armed its successor. Ensure a later slot here
|
|
348
354
|
// as well before relinquishing this durable delivery. If the queue is down, the
|
|
349
355
|
// throw leaves the current message unacked for visibility-timeout
|
|
350
356
|
// redelivery.
|
package/src/server.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
} from './stream-buffer.ts'
|
|
51
51
|
import type {
|
|
52
52
|
A2Store,
|
|
53
|
+
CompleteAttemptResult,
|
|
53
54
|
AppendEvent,
|
|
54
55
|
Event,
|
|
55
56
|
EventCause,
|
|
@@ -1004,7 +1005,7 @@ export function createServer<
|
|
|
1004
1005
|
const signal = createDrainSignal()
|
|
1005
1006
|
const p = new Promise<void>((resolve) => {
|
|
1006
1007
|
queueMicrotask(() => {
|
|
1007
|
-
void drainSession(sessionId, watchdogDueAt, signal)
|
|
1008
|
+
void drainSession(sessionId, { watchdogDueAt, signal })
|
|
1008
1009
|
.then(() => resolve())
|
|
1009
1010
|
.catch(() => resolve())
|
|
1010
1011
|
})
|
|
@@ -2931,8 +2932,16 @@ export function createServer<
|
|
|
2931
2932
|
|
|
2932
2933
|
const drainSession = (
|
|
2933
2934
|
sessionId: string,
|
|
2934
|
-
|
|
2935
|
-
|
|
2935
|
+
{
|
|
2936
|
+
watchdogDueAt,
|
|
2937
|
+
signal,
|
|
2938
|
+
handOffFailure = false,
|
|
2939
|
+
}: {
|
|
2940
|
+
watchdogDueAt?: number | undefined
|
|
2941
|
+
signal?: DrainSignal | undefined
|
|
2942
|
+
/** Return a failure after arming as `failed` instead of throwing it. */
|
|
2943
|
+
handOffFailure?: boolean
|
|
2944
|
+
} = {},
|
|
2936
2945
|
): Promise<DrainResult> =>
|
|
2937
2946
|
telemetry.span(
|
|
2938
2947
|
'a2.drain',
|
|
@@ -3131,7 +3140,8 @@ export function createServer<
|
|
|
3131
3140
|
}
|
|
3132
3141
|
| undefined
|
|
3133
3142
|
let handlerSignal = supersedeSignal
|
|
3134
|
-
let
|
|
3143
|
+
let completion: CompleteAttemptResult
|
|
3144
|
+
let completing = false
|
|
3135
3145
|
try {
|
|
3136
3146
|
if (registration?.abortOn) {
|
|
3137
3147
|
subscription = await subscribeAbort(
|
|
@@ -3158,7 +3168,32 @@ export function createServer<
|
|
|
3158
3168
|
await subscription.close()
|
|
3159
3169
|
subscription = undefined
|
|
3160
3170
|
}
|
|
3161
|
-
returned = await returnedEvents(sessionId, event, result)
|
|
3171
|
+
const returned = await returnedEvents(sessionId, event, result)
|
|
3172
|
+
await renewal
|
|
3173
|
+
completing = true
|
|
3174
|
+
completion = await telemetry.span(
|
|
3175
|
+
'a2.commit',
|
|
3176
|
+
{
|
|
3177
|
+
'a2.contract': name,
|
|
3178
|
+
'a2.session_id': sessionId,
|
|
3179
|
+
'a2.commit.operation': 'complete',
|
|
3180
|
+
'a2.commit.index': event.index,
|
|
3181
|
+
'a2.commit.attempt': event.attemptCount,
|
|
3182
|
+
},
|
|
3183
|
+
async (commitSpan) => {
|
|
3184
|
+
const committed = await store.completeAttempt({
|
|
3185
|
+
sessionId: ns,
|
|
3186
|
+
index: event.index,
|
|
3187
|
+
attempt: event.attemptCount,
|
|
3188
|
+
events: returned,
|
|
3189
|
+
})
|
|
3190
|
+
commitSpan.setAttribute(
|
|
3191
|
+
'a2.commit.outcome',
|
|
3192
|
+
committed.outcome,
|
|
3193
|
+
)
|
|
3194
|
+
return committed
|
|
3195
|
+
},
|
|
3196
|
+
)
|
|
3162
3197
|
} catch (err) {
|
|
3163
3198
|
if (supersedeSignal.aborted && err === supersedeSignal.reason) {
|
|
3164
3199
|
// The handler surrendered to its own abort — identity, not
|
|
@@ -3172,6 +3207,14 @@ export function createServer<
|
|
|
3172
3207
|
appendsEvents: false,
|
|
3173
3208
|
}
|
|
3174
3209
|
}
|
|
3210
|
+
const refused =
|
|
3211
|
+
completing &&
|
|
3212
|
+
err instanceof A2Error &&
|
|
3213
|
+
err.code === 'PARTIAL_DUPLICATE_BATCH'
|
|
3214
|
+
// A store that cannot record the completion is not the
|
|
3215
|
+
// handler failing: the claim lapses, and the claim that takes
|
|
3216
|
+
// it over counts it. A returned batch it refuses is.
|
|
3217
|
+
if (completing && !refused) throw err
|
|
3175
3218
|
// Everything else — including a store fence rejection — is
|
|
3176
3219
|
// adjudicated by failAttempt, which is itself fenced: a
|
|
3177
3220
|
// genuinely superseded attempt gets 'superseded' back with
|
|
@@ -3192,11 +3235,14 @@ export function createServer<
|
|
|
3192
3235
|
index: event.index,
|
|
3193
3236
|
attempt: event.attemptCount,
|
|
3194
3237
|
error: describeError(err),
|
|
3195
|
-
// A NonRetriableError is terminal on its first throw
|
|
3196
|
-
//
|
|
3197
|
-
//
|
|
3238
|
+
// A NonRetriableError is terminal on its first throw,
|
|
3239
|
+
// and so is a refused batch, whose retry returns the
|
|
3240
|
+
// same ids: deterministic failures should not burn the
|
|
3241
|
+
// retry budget before dead-lettering.
|
|
3198
3242
|
maxFailures:
|
|
3199
|
-
err instanceof NonRetriableError
|
|
3243
|
+
err instanceof NonRetriableError || refused
|
|
3244
|
+
? 1
|
|
3245
|
+
: MAX_FAILURES,
|
|
3200
3246
|
retryAfterMs: failureRetryDelayMs(event.failureCount + 1),
|
|
3201
3247
|
})
|
|
3202
3248
|
commitSpan.setAttribute('a2.commit.outcome', result.outcome)
|
|
@@ -3212,27 +3258,6 @@ export function createServer<
|
|
|
3212
3258
|
} finally {
|
|
3213
3259
|
await subscription?.close()
|
|
3214
3260
|
}
|
|
3215
|
-
await renewal
|
|
3216
|
-
const completion = await telemetry.span(
|
|
3217
|
-
'a2.commit',
|
|
3218
|
-
{
|
|
3219
|
-
'a2.contract': name,
|
|
3220
|
-
'a2.session_id': sessionId,
|
|
3221
|
-
'a2.commit.operation': 'complete',
|
|
3222
|
-
'a2.commit.index': event.index,
|
|
3223
|
-
'a2.commit.attempt': event.attemptCount,
|
|
3224
|
-
},
|
|
3225
|
-
async (commitSpan) => {
|
|
3226
|
-
const result = await store.completeAttempt({
|
|
3227
|
-
sessionId: ns,
|
|
3228
|
-
index: event.index,
|
|
3229
|
-
attempt: event.attemptCount,
|
|
3230
|
-
events: returned,
|
|
3231
|
-
})
|
|
3232
|
-
commitSpan.setAttribute('a2.commit.outcome', result.outcome)
|
|
3233
|
-
return result
|
|
3234
|
-
},
|
|
3235
|
-
)
|
|
3236
3261
|
eventSpan.setAttribute(
|
|
3237
3262
|
'a2.event.outcome',
|
|
3238
3263
|
completion.outcome === 'completed'
|
|
@@ -3252,7 +3277,7 @@ export function createServer<
|
|
|
3252
3277
|
return {
|
|
3253
3278
|
status: 'processed',
|
|
3254
3279
|
opensLane: event.lane !== null,
|
|
3255
|
-
appendsEvents:
|
|
3280
|
+
appendsEvents: completion.events.length > 0,
|
|
3256
3281
|
}
|
|
3257
3282
|
},
|
|
3258
3283
|
)
|
|
@@ -3424,6 +3449,12 @@ export function createServer<
|
|
|
3424
3449
|
if (signal) signal.closed = true
|
|
3425
3450
|
break
|
|
3426
3451
|
}
|
|
3452
|
+
} catch (error) {
|
|
3453
|
+
// The successor armed for this drain's claims drains the session
|
|
3454
|
+
// again, so the scheduler delivery need not stay as a second retry.
|
|
3455
|
+
if (!handOffFailure || schedulerArm === undefined) throw error
|
|
3456
|
+
span.recordError(error)
|
|
3457
|
+
outcome = 'failed'
|
|
3427
3458
|
} finally {
|
|
3428
3459
|
if (signal) signal.closed = true
|
|
3429
3460
|
stopRenewing = true
|
|
@@ -3525,7 +3556,10 @@ export function createServer<
|
|
|
3525
3556
|
},
|
|
3526
3557
|
schedulerDrain: (sessionId, opts) => {
|
|
3527
3558
|
assertSessionId(sessionId)
|
|
3528
|
-
return drainSession(sessionId,
|
|
3559
|
+
return drainSession(sessionId, {
|
|
3560
|
+
watchdogDueAt: opts?.watchdogDueAt,
|
|
3561
|
+
handOffFailure: true,
|
|
3562
|
+
})
|
|
3529
3563
|
},
|
|
3530
3564
|
schedulerAppend: async (sessionId, events, originIndex) => {
|
|
3531
3565
|
if (!scheduler) {
|
package/src/store-postgres.ts
CHANGED
|
@@ -27,9 +27,9 @@ import { pollPresence } from './store-presence-polling.ts'
|
|
|
27
27
|
import {
|
|
28
28
|
postgresNotifications,
|
|
29
29
|
postgresNotificationChannel,
|
|
30
|
-
POSTGRES_NOTIFICATIONS_SQL,
|
|
31
30
|
type PostgresListenerConnection,
|
|
32
31
|
} from './postgres-notifications.ts'
|
|
32
|
+
import { checkPostgresSchema } from './postgres-schema.ts'
|
|
33
33
|
import { retryableLazy } from './retryable-lazy.ts'
|
|
34
34
|
import { postgresNotificationScope } from './postgres-notification-scope.ts'
|
|
35
35
|
import { postgresPoolClient } from './postgres-pool.ts'
|
|
@@ -92,91 +92,6 @@ export type PostgresStore = A2Store & {
|
|
|
92
92
|
close(): Promise<void>
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const SCHEMA = `
|
|
96
|
-
create table if not exists a2_events (
|
|
97
|
-
session_id text not null,
|
|
98
|
-
idx bigint not null,
|
|
99
|
-
event_type text not null,
|
|
100
|
-
payload jsonb not null,
|
|
101
|
-
event_id text not null,
|
|
102
|
-
created_at bigint not null,
|
|
103
|
-
cause jsonb,
|
|
104
|
-
lane text,
|
|
105
|
-
lane_ready boolean not null default false,
|
|
106
|
-
processed_at bigint,
|
|
107
|
-
processed_by_attempt integer,
|
|
108
|
-
returned_event_ids jsonb,
|
|
109
|
-
first_claimed_at bigint,
|
|
110
|
-
last_claimed_at bigint,
|
|
111
|
-
attempt_count integer not null default 0,
|
|
112
|
-
failure_count integer not null default 0,
|
|
113
|
-
last_failed_at bigint,
|
|
114
|
-
last_failed_attempt integer,
|
|
115
|
-
last_error text,
|
|
116
|
-
failed_at bigint,
|
|
117
|
-
claim_holder text,
|
|
118
|
-
claim_expires_at bigint,
|
|
119
|
-
primary key (session_id, idx)
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
create unique index if not exists a2_events_event_id on a2_events (event_id);
|
|
123
|
-
create index if not exists a2_events_unprocessed
|
|
124
|
-
on a2_events (session_id, idx) where processed_at is null;
|
|
125
|
-
create index if not exists a2_events_lane_pending
|
|
126
|
-
on a2_events (session_id, lane, idx) where processed_at is null;
|
|
127
|
-
create index if not exists a2_events_dispatch_ready
|
|
128
|
-
on a2_events (session_id, idx)
|
|
129
|
-
where lane_ready and processed_at is null and failed_at is null;
|
|
130
|
-
create index if not exists a2_events_claim_expiry
|
|
131
|
-
on a2_events (session_id, claim_expires_at)
|
|
132
|
-
where lane_ready and processed_at is null and failed_at is null;
|
|
133
|
-
|
|
134
|
-
create table if not exists a2_snapshots (
|
|
135
|
-
session_id text not null,
|
|
136
|
-
reducer_name text not null,
|
|
137
|
-
up_to_index bigint not null,
|
|
138
|
-
state jsonb not null,
|
|
139
|
-
updated_at bigint not null,
|
|
140
|
-
primary key (session_id, reducer_name)
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
create table if not exists a2_snapshot_history (
|
|
144
|
-
session_id text not null,
|
|
145
|
-
reducer_name text not null,
|
|
146
|
-
up_to_index bigint not null,
|
|
147
|
-
state jsonb not null,
|
|
148
|
-
updated_at bigint not null,
|
|
149
|
-
primary key (session_id, reducer_name, up_to_index)
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
create table if not exists a2_snapshot_pins (
|
|
153
|
-
session_id text not null,
|
|
154
|
-
event_index bigint not null,
|
|
155
|
-
reducer_name text not null,
|
|
156
|
-
up_to_index bigint not null,
|
|
157
|
-
primary key (session_id, event_index, reducer_name, up_to_index)
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
create index if not exists a2_snapshot_pins_checkpoint
|
|
161
|
-
on a2_snapshot_pins (session_id, reducer_name, up_to_index);
|
|
162
|
-
|
|
163
|
-
create table if not exists a2_clears (
|
|
164
|
-
session_id text primary key,
|
|
165
|
-
clear_index bigint not null
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
create table if not exists a2_presence (
|
|
169
|
-
ns text not null,
|
|
170
|
-
participant text not null,
|
|
171
|
-
field text not null,
|
|
172
|
-
value jsonb not null,
|
|
173
|
-
seen bigint not null,
|
|
174
|
-
at bigint not null,
|
|
175
|
-
expires_at bigint not null,
|
|
176
|
-
primary key (ns, participant, field)
|
|
177
|
-
);
|
|
178
|
-
`
|
|
179
|
-
|
|
180
95
|
const asNumber = (value: unknown): number => Number(value) // bigint columns arrive as strings
|
|
181
96
|
|
|
182
97
|
/**
|
|
@@ -281,37 +196,6 @@ const wrap = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
|
281
196
|
|
|
282
197
|
const jsonb = (value: unknown): string => JSON.stringify(value) ?? 'null'
|
|
283
198
|
|
|
284
|
-
const initializeSchema = async ({
|
|
285
|
-
client,
|
|
286
|
-
listen,
|
|
287
|
-
}: {
|
|
288
|
-
client: PostgresClient
|
|
289
|
-
listen: boolean
|
|
290
|
-
}): Promise<void> => {
|
|
291
|
-
const schema = SCHEMA + (listen ? POSTGRES_NOTIFICATIONS_SQL : '')
|
|
292
|
-
if (!client.connect) {
|
|
293
|
-
await client.query(schema)
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const conn = await client.connect()
|
|
298
|
-
try {
|
|
299
|
-
await conn.query('begin')
|
|
300
|
-
// `if not exists` does not serialize concurrent catalog writes. Every
|
|
301
|
-
// serverless isolate takes the same transaction-scoped lock before DDL.
|
|
302
|
-
await conn.query(
|
|
303
|
-
"select pg_advisory_xact_lock(hashtext('a2:postgres:schema'))",
|
|
304
|
-
)
|
|
305
|
-
await conn.query(schema)
|
|
306
|
-
await conn.query('commit')
|
|
307
|
-
} catch (err) {
|
|
308
|
-
await conn.query('rollback').catch(() => {})
|
|
309
|
-
throw err
|
|
310
|
-
} finally {
|
|
311
|
-
conn.release()
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
199
|
const loadPg = async () =>
|
|
316
200
|
import('pg').catch(() => {
|
|
317
201
|
throw new A2Error(
|
|
@@ -420,12 +304,10 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
420
304
|
try {
|
|
421
305
|
const listen = Boolean(options.listenConnectionString)
|
|
422
306
|
const resource = lease.value
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
run: () => initializeSchema({ client: resource.client, listen }),
|
|
428
|
-
})
|
|
307
|
+
await resource.initialize({
|
|
308
|
+
key: listen ? 'schema+triggers' : 'schema',
|
|
309
|
+
run: () => checkPostgresSchema({ client: resource.client, listen }),
|
|
310
|
+
})
|
|
429
311
|
return lease
|
|
430
312
|
} catch (error) {
|
|
431
313
|
if (!injected) await lease.release().catch(() => {})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ai-harness-BuuC-gJ3.js","names":[],"sources":["../src/ai-harness.ts"],"sourcesContent":["import {\n createDownload,\n type Experimental_SandboxSession,\n type FileUIPart,\n type FinishReason,\n type LanguageModelUsage,\n type ToolSet,\n type UIMessage,\n type UIMessageChunk,\n} from 'ai'\nimport type {\n GenerationCompletedPayload,\n MessageCreatedPayload,\n ReasoningLevel,\n ToolResultPayload,\n} from './ai.ts'\nimport { STEERED_REASON } from './ai-control.ts'\nimport { boundedCache, type BoundedCache } from './bounded-cache.ts'\nimport type { ContractEvent, EventDefs } from './contract.ts'\n\ndeclare const harnessBrand: unique symbol\n\n/**\n * A coding agent's own loop for `agent({ harness })`, from `fx()` or `pi()`.\n * `Tools` names the tools the harness brings, which `toolRecovery` may name.\n */\nexport type Harness<\n Type extends 'fx' | 'pi' = 'fx' | 'pi',\n Tools extends string = string,\n> = {\n readonly type: Type\n readonly [harnessBrand]: { readonly tools: Tools }\n}\n\n/** What a harness's sandbox resolver receives for one step or tool execution. */\nexport type HarnessSandboxContext = {\n session: { readonly id: string }\n signal: AbortSignal\n}\n\n/** Resolves the AI SDK sandbox session for an agent session, the same one every time. */\nexport type HarnessSandbox = (\n context: HarnessSandboxContext,\n) => Experimental_SandboxSession | Promise<Experimental_SandboxSession>\n\n/** What a harness step streams, as the default generator does. */\nexport type HarnessGeneration = {\n stream: ReadableStream<UIMessageChunk>\n completion: () => { finishReason?: FinishReason; usage?: LanguageModelUsage }\n}\n\n/** One stored snapshot and the log after it, read in one store operation. */\nexport type HarnessLogRead = {\n snapshot: { index: number; state: unknown } | null\n events: ContractEvent<EventDefs>[]\n}\n\n/** The session log as a harness step reads it, plus the state saved in it. */\nexport type HarnessLog = {\n /** The latest saved state under `name` and the log after it; `fresh` reads it all. */\n read(options: {\n name: string\n throughIndex: number\n fresh?: boolean\n }): Promise<HarnessLogRead>\n write(options: { name: string; index: number; state: unknown }): Promise<void>\n}\n\n/** Everything one durable generation hands its harness. */\nexport type HarnessStep = {\n agentName: string\n sessionId: string\n requestIndex: number\n /** This attempt at the generation. */\n generationId: string\n responseMessageId: string\n /** The log index this step's context was read through. */\n throughIndex: number\n /** The transcript through that index. */\n messages: readonly UIMessage[]\n model: string\n reasoning?: ReasoningLevel\n speed?: 'fast' | 'standard'\n instructions: string[]\n tools: ToolSet\n log: HarnessLog\n signal: AbortSignal\n}\n\n/** The internal side of a harness value. */\nexport type HarnessDriver = {\n /** The session's sandbox; every local tool execution receives it. */\n readonly sandbox?: HarnessSandbox\n /** Tools the harness brings; A2 executes them like the agent's own. */\n readonly tools: ToolSet\n step(step: HarnessStep): Promise<HarnessGeneration>\n}\n\n// A global symbol, so harness values work across separately bundled copies.\nconst DRIVER = Symbol.for('experimental-a2.ai.harness')\n\nexport function defineHarness<\n Type extends 'fx' | 'pi',\n Tools extends string = never,\n>(type: Type, driver: HarnessDriver): Harness<Type, Tools> {\n const harness = { type }\n Object.defineProperty(harness, DRIVER, { value: driver })\n return Object.freeze(harness) as unknown as Harness<Type, Tools>\n}\n\nexport function harnessDriver(harness: unknown): HarnessDriver {\n const driver =\n typeof harness === 'object' && harness !== null\n ? (harness as { [DRIVER]?: HarnessDriver })[DRIVER]\n : undefined\n if (driver === undefined)\n throw new TypeError(\n \"harness must be { type: 'ai-sdk' }, or come from fx() or pi()\",\n )\n return driver\n}\n\n// ------------------------------------------------------- saved state\n\n/**\n * What an agent's steps saved in this process at the end of a response, per\n * session and harness. A cache: a cold process rebuilds from the whole log.\n */\nexport type HarnessStates = BoundedCache<\n string,\n { index: number; state: unknown }\n>\n\nexport const harnessStates = (): HarnessStates =>\n boundedCache({ maxEntries: 1_000, maxBytes: 64 * 1024 * 1024 })\n\nconst savedKey = (sessionId: string, name: string): string =>\n `${sessionId}\\u0000${name}`\n\n// Binary state counts its bytes; everything else its JSON text.\nconst sizeOf = (value: unknown): number => {\n let binary = 0\n const text = JSON.stringify(value, (_key, item: unknown) => {\n if (!(item instanceof Uint8Array)) return item\n binary += item.byteLength\n return null\n })\n return binary + (text?.length ?? 0)\n}\n\n/**\n * The session log as a harness step reads it: the state this process saved\n * at the end of the last response, when it has one, and the log after it.\n * Saved states are copied in and out, so a step cannot change one in place.\n */\nexport function harnessLog(options: {\n states: HarnessStates\n session: {\n readonly id: string\n history(bounds: {\n gte?: number\n lte?: number\n }): Promise<ContractEvent<EventDefs>[]>\n }\n}): HarnessLog {\n const { states, session } = options\n return {\n read: async ({ name, throughIndex, fresh }) => {\n const entry = fresh ? undefined : states.get(savedKey(session.id, name))\n const snapshot =\n entry === undefined || entry.index > throughIndex\n ? null\n : { index: entry.index, state: structuredClone(entry.state) }\n const events = await session.history(\n snapshot === null\n ? { lte: throughIndex }\n : { gte: snapshot.index + 1, lte: throughIndex },\n )\n return { snapshot, events }\n },\n write: async ({ name, index, state }) => {\n const copy = structuredClone(state)\n states.set(\n savedKey(session.id, name),\n { index, state: copy },\n sizeOf(copy),\n )\n },\n }\n}\n\nconst agentStates = new WeakMap<object, HarnessStates>()\n\n/** Registers an agent server's saved harness states, for `savedHarnessState`. */\nexport function registerHarnessStates(\n server: object,\n states: HarnessStates,\n): void {\n agentStates.set(server, states)\n}\n\n/** What an agent's process saved for one session's harness; tests read it. */\nexport function savedHarnessState(\n server: object,\n options: { sessionId: string; name: string },\n): { index: number; state: unknown } | undefined {\n return agentStates.get(server)?.get(savedKey(options.sessionId, options.name))\n}\n\n/** A harness's state at the end of a response, from the attempt that ran it. */\ntype SavedEnvelope = {\n generationId: string\n responseMessageId: string\n state: unknown\n}\n\nexport type HarnessRead<S> = {\n /** The state saved at the end of the last response, when it is usable. */\n saved: S | undefined\n turns: HarnessTurn[]\n results: Map<string, ToolResultPayload>\n}\n\n/**\n * Reads a harness's saved state and the log since, as turns to replay. A\n * snapshot counts only when the attempt that wrote it committed: a stopped,\n * superseded, or still-running attempt can write one the log never kept.\n * `replayAll` reads the whole log instead, for a state that fails to restore.\n */\nexport async function readHarness<S>({\n step,\n name,\n parse,\n}: {\n step: HarnessStep\n name: string\n parse: (state: unknown) => S | undefined\n}): Promise<HarnessRead<S> & { replayAll: () => Promise<HarnessRead<S>> }> {\n const replayAll = async (): Promise<HarnessRead<S>> => {\n const read = await step.log.read({\n name,\n throughIndex: step.throughIndex,\n fresh: true,\n })\n return { saved: undefined, ...harnessTurns(step, read.events, undefined) }\n }\n const read = await step.log.read({ name, throughIndex: step.throughIndex })\n if (read.snapshot === null)\n return {\n saved: undefined,\n ...harnessTurns(step, read.events, undefined),\n replayAll,\n }\n const envelope = savedEnvelope(read.snapshot.state)\n const saved = envelope === undefined ? undefined : parse(envelope.state)\n const committed =\n envelope !== undefined &&\n read.events.some(\n (event) =>\n event.type === 'ai.generation.completed' &&\n (event.payload as { generationId?: unknown }).generationId ===\n envelope.generationId,\n )\n // A snapshot of this very response belongs to an earlier attempt at it.\n if (\n saved === undefined ||\n !committed ||\n envelope.responseMessageId === step.responseMessageId\n )\n return { ...(await replayAll()), replayAll }\n return {\n saved,\n ...harnessTurns(step, read.events, envelope.responseMessageId),\n replayAll,\n }\n}\n\n/** Saves a harness's state at the end of a response; losing it costs a longer replay. */\nexport async function saveHarness({\n step,\n name,\n state,\n}: {\n step: HarnessStep\n name: string\n state: unknown\n}): Promise<void> {\n const envelope: SavedEnvelope = {\n generationId: step.generationId,\n responseMessageId: step.responseMessageId,\n state,\n }\n try {\n await step.log.write({ name, index: step.requestIndex, state: envelope })\n } catch {\n // The snapshot is a cache.\n }\n}\n\nfunction savedEnvelope(value: unknown): SavedEnvelope | undefined {\n if (typeof value !== 'object' || value === null) return undefined\n const { generationId, responseMessageId, state } = value as Record<\n string,\n unknown\n >\n return typeof generationId === 'string' &&\n typeof responseMessageId === 'string'\n ? { generationId, responseMessageId, state }\n : undefined\n}\n\n// ------------------------------------------------- the log as harness turns\n\n/** A completed generation as the log recorded it. */\nexport type RecordedGeneration = {\n chunks: UIMessageChunk[]\n completed: GenerationCompletedPayload\n}\n\n/** What user messages hand a harness: their text, and the files they attach. */\nexport type HarnessInput = { text: string; files: FileUIPart[] }\n\n/** One harness turn to rebuild: its prompt, completed generations, and end. */\nexport type HarnessTurn = {\n prompt: HarnessInput\n generations: RecordedGeneration[]\n /** Steering that entered after the generation at that index. */\n steers: Map<number, HarnessInput[]>\n end: 'stop' | 'interrupted' | 'live'\n}\n\n/**\n * Groups the log since a harness snapshot into harness turns. A response\n * steered after a step continues its turn, with the next response's prompt as\n * steering. `covered` is the response the snapshot already holds.\n */\nexport function harnessTurns(\n step: HarnessStep,\n events: readonly ContractEvent<EventDefs>[],\n covered: string | undefined,\n): { turns: HarnessTurn[]; results: Map<string, ToolResultPayload> } {\n type Generation = {\n id: string\n chunks: UIMessageChunk[]\n completed?: GenerationCompletedPayload\n }\n type Response = {\n id: string\n prompt: HarnessInput[]\n generations: Generation[]\n end?: 'stop' | 'steered' | 'interrupted'\n }\n // Prompts take their content from the transcript: a harness agent never\n // runs A2's compaction, so the step's messages hold every input admitted by\n // its read, before the snapshot or after it. A session another loop\n // compacted can't move to a harness.\n const userInput = new Map<string, HarnessInput>(\n step.messages\n .filter((message) => message.role === 'user')\n .map((message) => [message.id, inputOf(message)]),\n )\n let passive: string[] = []\n const responses = new Map<string, Response>()\n const byStart = new Map<number, Generation>()\n const byId = new Map<string, Generation>()\n const results = new Map<string, ToolResultPayload>()\n\n for (const event of events) {\n const payload = event.payload as Record<string, unknown>\n switch (event.type) {\n case 'ai.message.created': {\n const admitted = event.payload as MessageCreatedPayload\n if (admitted.role === 'user' && admitted.generate === false)\n passive.push(admitted.messageId)\n break\n }\n case 'ai.generation.requested': {\n const id = payload['responseMessageId'] as string\n if (id === covered || responses.has(id)) break\n const messageId = payload['messageId'] as string\n const ids = [...passive.filter((p) => p !== messageId), messageId]\n passive = []\n responses.set(id, {\n id,\n prompt: ids.flatMap((p) => userInput.get(p) ?? []),\n generations: [],\n })\n break\n }\n case 'ai.generation.started': {\n const response = responses.get(payload['responseMessageId'] as string)\n if (response === undefined) break\n const generation: Generation = {\n id: payload['generationId'] as string,\n chunks: [],\n }\n response.generations.push(generation)\n byStart.set(event.index, generation)\n byId.set(generation.id, generation)\n break\n }\n case 'ai.generation.progress':\n byStart\n .get(payload['startIndex'] as number)\n ?.chunks.push(...(payload['chunks'] as UIMessageChunk[]))\n break\n case 'ai.generation.completed': {\n const generation = byId.get(payload['generationId'] as string)\n if (generation)\n generation.completed = event.payload as GenerationCompletedPayload\n break\n }\n case 'ai.tool.result':\n if (payload['preliminary'] !== true)\n results.set(\n payload['toolCallId'] as string,\n event.payload as ToolResultPayload,\n )\n break\n case 'ai.message.completed': {\n const response = responses.get(payload['messageId'] as string)\n if (response) response.end = 'stop'\n break\n }\n case 'ai.message.interrupted': {\n const response = responses.get(payload['messageId'] as string)\n if (response)\n response.end =\n payload['reason'] === STEERED_REASON ? 'steered' : 'interrupted'\n break\n }\n }\n }\n\n const turns: HarnessTurn[] = []\n let steered = false\n for (const response of responses.values()) {\n const input: HarnessInput = {\n text: response.prompt.map((prompt) => prompt.text).join('\\n\\n'),\n files: response.prompt.flatMap((prompt) => prompt.files),\n }\n let turn = turns.at(-1)\n if (steered && turn !== undefined) {\n const at = Math.max(0, turn.generations.length - 1)\n turn.steers.set(at, [...(turn.steers.get(at) ?? []), input])\n } else {\n turn = { prompt: input, generations: [], steers: new Map(), end: 'live' }\n turns.push(turn)\n }\n for (const generation of response.generations)\n if (generation.completed !== undefined)\n turn.generations.push({\n chunks: generation.chunks,\n completed: generation.completed,\n })\n turn.end =\n response.id === step.responseMessageId\n ? 'live'\n : response.end === 'stop'\n ? 'stop'\n : 'interrupted'\n steered = response.end === 'steered'\n }\n return { turns, results }\n}\n\nfunction inputOf(message: UIMessage): HarnessInput {\n return {\n text: message.parts\n .flatMap((part) => (part.type === 'text' ? [part.text] : []))\n .join('\\n'),\n files: message.parts.filter(\n (part): part is FileUIPart => part.type === 'file',\n ),\n }\n}\n\n// ------------------------------------------------------------- attachments\n\n/** An attached image as an engine takes it: its bytes in base64. */\nexport type HarnessImage = { mediaType: string; data: string }\n\n// What one message's files hold in a step's memory, at most.\nconst MAX_ATTACHMENT_BYTES = 32 * 1024 * 1024\n\ntype Attachment = { data: string; bytes: number }\n\n// Every step of a response replays its turn, and a URL's file doesn't change,\n// so the process keeps what it fetched.\nconst attachments = boundedCache<string, Attachment>({\n maxEntries: 256,\n maxBytes: 64 * 1024 * 1024,\n})\n\n/**\n * The images an input attaches, as its engine takes them: engines take\n * bytes, not URLs. A file it can't attach fails the step, naming the file,\n * and is never left out: the failure may pass, and the step is retried like\n * any other.\n */\nexport async function harnessImages({\n harness,\n input,\n signal,\n}: {\n /** As the error names it: `fx()` or `pi()`. */\n harness: string\n input: HarnessInput\n signal: AbortSignal\n}): Promise<HarnessImage[]> {\n const images: HarnessImage[] = []\n let budget = MAX_ATTACHMENT_BYTES\n // One at a time, so the budget bounds what the step holds.\n for (const file of input.files) {\n try {\n if (!file.mediaType.startsWith('image/'))\n throw new Error(`it takes images only, not ${file.mediaType}`)\n const attached = await attachment({ url: file.url, budget, signal })\n budget -= attached.bytes\n images.push({\n mediaType: file.mediaType,\n data: attached.data,\n })\n } catch (error) {\n if (signal.aborted) throw error\n throw new Error(\n `${harness} couldn't attach ${file.filename ?? 'a file'}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n { cause: error },\n )\n }\n }\n return images\n}\n\nasync function attachment({\n url,\n budget,\n signal,\n}: {\n url: string\n budget: number\n signal: AbortSignal\n}): Promise<Attachment> {\n const tooLarge = () =>\n new Error(\n `a message's files may hold ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MiB in a step`,\n )\n const cached = attachments.get(url)\n if (cached !== undefined) {\n if (cached.bytes > budget) throw tooLarge()\n return cached\n }\n const { protocol } = new URL(url)\n if (protocol !== 'https:' && protocol !== 'data:')\n throw new Error(`attachments must be https: or data: URLs, not ${protocol}`)\n // AI SDK's download checks an https: URL's address and its redirects,\n // which a message's sender chooses; a data: URL holds its bytes.\n const bytes =\n protocol === 'data:'\n ? dataBytes(url)\n : (\n await createDownload({ maxBytes: budget })({\n url: new URL(url),\n abortSignal: signal,\n })\n ).data\n if (bytes.byteLength > budget) throw tooLarge()\n const attached: Attachment = {\n data: Buffer.from(bytes).toString('base64'),\n bytes: bytes.byteLength,\n }\n if (protocol === 'https:')\n attachments.set(url, attached, attached.data.length)\n return attached\n}\n\nfunction dataBytes(url: string): Uint8Array {\n const comma = url.indexOf(',')\n if (comma === -1) throw new Error('its data: URL has no data')\n const body = url.slice(comma + 1)\n return url.slice(0, comma).endsWith(';base64')\n ? Buffer.from(body, 'base64')\n : Buffer.from(decodeURIComponent(body))\n}\n\n// ------------------------------------------------------------- Gateway\n\nexport type GatewayCredential = { token: string; method: 'api-key' | 'oidc' }\n\n/** Read per step: on Vercel the OIDC token rides on each function request. */\nexport function gatewayCredential(harness: string): GatewayCredential {\n const key = process.env['AI_GATEWAY_API_KEY']\n if (key) return { token: key, method: 'api-key' }\n const oidc = requestOidcToken() ?? process.env['VERCEL_OIDC_TOKEN']\n if (oidc) return { token: oidc, method: 'oidc' }\n throw new Error(`${harness}() needs AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN`)\n}\n\n// Where Vercel Functions keep the current request, as @vercel/oidc reads it.\nconst REQUEST_CONTEXT = Symbol.for('@vercel/request-context')\n\nfunction requestOidcToken(): string | undefined {\n const context = (\n globalThis as {\n [REQUEST_CONTEXT]?: { get?: () => { headers?: Record<string, unknown> } }\n }\n )[REQUEST_CONTEXT]\n const token = context?.get?.()?.headers?.['x-vercel-oidc-token']\n return typeof token === 'string' && token !== '' ? token : undefined\n}\n\n// ------------------------------------------------------------- helpers\n\nexport class Divergence extends Error {\n constructor(harness: string, message: string) {\n super(`${harness} replay diverged from the log: ${message}`)\n }\n}\n\nexport type Deferred = { promise: Promise<void>; resolve: () => void }\n\nexport function deferred(): Deferred {\n let resolve!: () => void\n const promise = new Promise<void>((done) => {\n resolve = done\n })\n return { promise, resolve }\n}\n\n/** A result that never settles; the harness is stopped instead. */\nexport function pending(\n signal: AbortSignal | null | undefined,\n): Promise<never> {\n return new Promise((_, reject) => {\n if (signal?.aborted) reject(signal.reason)\n signal?.addEventListener('abort', () => reject(signal.reason), {\n once: true,\n })\n })\n}\n"],"mappings":"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"}
|