experimental-a2 0.18.1 → 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 +44 -0
- package/dist/actor.js +1 -1
- package/dist/{ai-BMLlI2Ff.d.ts → ai-Cz3hqVhk.d.ts} +33 -16
- package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +1 -1
- package/dist/ai-control-CvLbW10B.js +46 -0
- package/dist/ai-control-CvLbW10B.js.map +1 -0
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +63 -13
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-B2suP2Qh.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
- package/dist/{ai-harness-BuLh_EYi.js → ai-harness-Clbg5ei7.js} +88 -15
- package/dist/ai-harness-Clbg5ei7.js.map +1 -0
- package/dist/{ai-D6mGlO8j.js → ai-mMiWul1T.js} +116 -77
- package/dist/ai-mMiWul1T.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.d.ts.map +1 -1
- package/dist/ai-pi.js +56 -11
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +1 -1
- package/dist/ai-server.d.ts +2 -2
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +123 -53
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +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 +5 -7
- 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 +26 -14
- package/docs/agents/04-harnesses.mdx +51 -5
- package/docs/agents/meta.ts +1 -1
- package/docs/client/meta.ts +1 -1
- package/docs/contract/03-sessions.mdx +2 -2
- 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 +36 -19
- 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 +3 -3
- 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 +19 -5
- package/examples/playground/app/a0/a0.css +244 -5
- package/examples/playground/app/a0/app-view.tsx +2 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/attachments.ts +133 -0
- package/examples/playground/app/a0/chat.test.tsx +595 -0
- package/examples/playground/app/a0/chat.tsx +623 -63
- package/examples/playground/app/a0/context-menu.tsx +38 -11
- package/examples/playground/app/a0/model.test.ts +28 -0
- package/examples/playground/app/a0/model.ts +30 -0
- package/examples/playground/app/a0/server.ts +5 -0
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- package/examples/playground/app/a0/uploads/route.ts +29 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -89
- 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/agent/model.test.ts +12 -3
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
- 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/next.config.ts +7 -4
- package/examples/playground/package.json +4 -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 +9 -4
- package/src/ai-client-state.ts +21 -9
- package/src/ai-context-schema.ts +28 -4
- package/src/ai-context.ts +83 -3
- package/src/ai-control-server.ts +70 -43
- package/src/ai-control-state.ts +3 -4
- package/src/ai-control.ts +59 -10
- package/src/ai-coordinator.ts +7 -6
- package/src/ai-fx.ts +95 -23
- package/src/ai-harness.ts +171 -29
- package/src/ai-message-shape.ts +41 -0
- package/src/ai-pi.ts +88 -32
- package/src/ai-stored-state.ts +1 -3
- package/src/ai.ts +116 -77
- package/src/errors.ts +1 -1
- package/src/internal.ts +2 -1
- package/src/libfx.d.ts +8 -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 +11 -16
- package/src/server.ts +66 -32
- package/src/store-postgres.ts +5 -123
- package/dist/ai-BMLlI2Ff.d.ts.map +0 -1
- package/dist/ai-D6mGlO8j.js.map +0 -1
- package/dist/ai-harness-B2suP2Qh.d.ts.map +0 -1
- package/dist/ai-harness-BuLh_EYi.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/examples/playground/app/a0/no-photon.cjs +0 -4
package/src/ai-pi.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
AssistantMessage,
|
|
15
15
|
AssistantMessageEvent,
|
|
16
16
|
AssistantMessageEventStream,
|
|
17
|
+
ImageContent,
|
|
17
18
|
JsonObject,
|
|
18
19
|
TextContent,
|
|
19
20
|
ThinkingContent,
|
|
@@ -50,6 +51,8 @@ import {
|
|
|
50
51
|
gatewayCredential,
|
|
51
52
|
type Harness,
|
|
52
53
|
type HarnessGeneration,
|
|
54
|
+
harnessImages,
|
|
55
|
+
type HarnessInput,
|
|
53
56
|
type HarnessRead,
|
|
54
57
|
type HarnessSandbox,
|
|
55
58
|
type HarnessStep,
|
|
@@ -130,11 +133,14 @@ type PiRecord =
|
|
|
130
133
|
| { call: 'step'; response: StoredResponse }
|
|
131
134
|
| { call: 'compaction'; response: AssistantMessage }
|
|
132
135
|
|
|
136
|
+
/** A user message as Pi takes it. */
|
|
137
|
+
type PiInput = { text: string; images: ImageContent[] }
|
|
138
|
+
|
|
133
139
|
type RecordedCall = {
|
|
134
140
|
kind: 'step' | 'compaction'
|
|
135
141
|
response: AssistantMessage
|
|
136
142
|
/** Steering to queue while serving this call. */
|
|
137
|
-
steers:
|
|
143
|
+
steers: PiInput[]
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
type Saved = {
|
|
@@ -232,7 +238,8 @@ async function runStep(
|
|
|
232
238
|
fail(state, diverged(`call ${state.next} is not a ${kind}`))
|
|
233
239
|
state.next++
|
|
234
240
|
// Steering queued while a response streams enters after its tools.
|
|
235
|
-
for (const text of recorded.steers)
|
|
241
|
+
for (const { text, images } of recorded.steers)
|
|
242
|
+
await session!.steer(text, images.length === 0 ? undefined : images)
|
|
236
243
|
replay(out, recorded.response)
|
|
237
244
|
return
|
|
238
245
|
}
|
|
@@ -252,6 +259,9 @@ async function runStep(
|
|
|
252
259
|
const upstream = realStream(model, context, {
|
|
253
260
|
...options,
|
|
254
261
|
headers: { ...options?.headers, ...gatewayHeaders(credential) },
|
|
262
|
+
...(step.speed === 'fast'
|
|
263
|
+
? { onPayload: fastTier(options?.onPayload) }
|
|
264
|
+
: {}),
|
|
255
265
|
})
|
|
256
266
|
const live = kind === 'step' ? liveStep() : undefined
|
|
257
267
|
for await (const event of upstream) {
|
|
@@ -547,9 +557,10 @@ async function runStep(
|
|
|
547
557
|
}
|
|
548
558
|
for (const plan of read.turns) {
|
|
549
559
|
if (step.signal.aborted) return
|
|
560
|
+
const prompt = await piInput({ input: plan.prompt, signal: step.signal })
|
|
550
561
|
const state: TurnState = {
|
|
551
562
|
plan,
|
|
552
|
-
calls: recordedTurn(plan),
|
|
563
|
+
calls: await recordedTurn({ turn: plan, signal: step.signal }),
|
|
553
564
|
next: 0,
|
|
554
565
|
liveStep: false,
|
|
555
566
|
over: false,
|
|
@@ -557,8 +568,9 @@ async function runStep(
|
|
|
557
568
|
compactions: [],
|
|
558
569
|
}
|
|
559
570
|
turn = state
|
|
560
|
-
const running = session.prompt(
|
|
571
|
+
const running = session.prompt(prompt.text, {
|
|
561
572
|
expandPromptTemplates: false,
|
|
573
|
+
...(prompt.images.length === 0 ? {} : { images: prompt.images }),
|
|
562
574
|
})
|
|
563
575
|
if (plan.end !== 'live') {
|
|
564
576
|
await running.catch((error: unknown) => {
|
|
@@ -622,17 +634,43 @@ async function runStep(
|
|
|
622
634
|
// ------------------------------------------------------------ the plan
|
|
623
635
|
|
|
624
636
|
/** A turn's recorded model calls, in the order Pi made them. */
|
|
625
|
-
function recordedTurn(
|
|
637
|
+
async function recordedTurn({
|
|
638
|
+
turn,
|
|
639
|
+
signal,
|
|
640
|
+
}: {
|
|
641
|
+
turn: HarnessTurn
|
|
642
|
+
signal: AbortSignal
|
|
643
|
+
}): Promise<RecordedCall[]> {
|
|
626
644
|
const calls: RecordedCall[] = []
|
|
627
|
-
turn.generations.
|
|
645
|
+
for (const [index, generation] of turn.generations.entries()) {
|
|
628
646
|
calls.push(...recordedCalls(generation))
|
|
629
647
|
const steers = turn.steers.get(index)
|
|
630
648
|
const last = calls.findLast((call) => call.kind === 'step')
|
|
631
|
-
if (steers !== undefined && last !== undefined)
|
|
632
|
-
|
|
649
|
+
if (steers !== undefined && last !== undefined)
|
|
650
|
+
for (const input of steers)
|
|
651
|
+
last.steers.push(await piInput({ input, signal }))
|
|
652
|
+
}
|
|
633
653
|
return calls
|
|
634
654
|
}
|
|
635
655
|
|
|
656
|
+
async function piInput({
|
|
657
|
+
input,
|
|
658
|
+
signal,
|
|
659
|
+
}: {
|
|
660
|
+
input: HarnessInput
|
|
661
|
+
signal: AbortSignal
|
|
662
|
+
}): Promise<PiInput> {
|
|
663
|
+
const images = await harnessImages({ harness: 'pi()', input, signal })
|
|
664
|
+
return {
|
|
665
|
+
text: input.text,
|
|
666
|
+
images: images.map((image) => ({
|
|
667
|
+
type: 'image',
|
|
668
|
+
data: image.data,
|
|
669
|
+
mimeType: image.mediaType,
|
|
670
|
+
})),
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
636
674
|
function recordedCalls(generation: RecordedGeneration): RecordedCall[] {
|
|
637
675
|
const texts = new Map<string, string>()
|
|
638
676
|
const inputs = new Map<string, unknown>()
|
|
@@ -937,30 +975,28 @@ async function sessionTools(
|
|
|
937
975
|
) => Promise<AgentToolResult<unknown>>,
|
|
938
976
|
): Promise<ToolDefinition[]> {
|
|
939
977
|
return Promise.all(
|
|
940
|
-
Object.entries(tools)
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
}),
|
|
978
|
+
Object.entries(tools).map(async ([name, tool]): Promise<ToolDefinition> => {
|
|
979
|
+
const builtin = builtinOf(tools, name)
|
|
980
|
+
if (builtin !== undefined)
|
|
981
|
+
// Pi's own definition, for its prompt; A2 already ran the call.
|
|
982
|
+
return Object.assign(builtinDefinition(modules, builtin, cwd), {
|
|
983
|
+
execute: (
|
|
984
|
+
toolCallId: string,
|
|
985
|
+
_params: unknown,
|
|
986
|
+
signal?: AbortSignal,
|
|
987
|
+
) => result(toolCallId, signal, 'pi'),
|
|
988
|
+
})
|
|
989
|
+
return {
|
|
990
|
+
name,
|
|
991
|
+
label: name,
|
|
992
|
+
description:
|
|
993
|
+
typeof tool.description === 'string' ? tool.description : '',
|
|
994
|
+
parameters: (await asSchema(tool.inputSchema)
|
|
995
|
+
.jsonSchema) as ToolDefinition['parameters'],
|
|
996
|
+
execute: (toolCallId, _params, signal) =>
|
|
997
|
+
result(toolCallId, signal, 'tool'),
|
|
998
|
+
}
|
|
999
|
+
}),
|
|
964
1000
|
)
|
|
965
1001
|
}
|
|
966
1002
|
|
|
@@ -1227,6 +1263,26 @@ function thinkingLevel(step: HarnessStep): {
|
|
|
1227
1263
|
return { thinkingLevel: step.reasoning === 'none' ? 'off' : step.reasoning }
|
|
1228
1264
|
}
|
|
1229
1265
|
|
|
1266
|
+
type PayloadHook = NonNullable<
|
|
1267
|
+
NonNullable<Parameters<ModelRuntime['streamSimple']>[2]>['onPayload']
|
|
1268
|
+
>
|
|
1269
|
+
|
|
1270
|
+
/** Asks AI Gateway for the model's fast tier, on the payload Pi's own hook returns. */
|
|
1271
|
+
function fastTier(inner: PayloadHook | undefined): PayloadHook {
|
|
1272
|
+
return async (payload, model) => {
|
|
1273
|
+
const body = ((await inner?.(payload, model)) ?? payload) as {
|
|
1274
|
+
providerOptions?: { gateway?: object }
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
...body,
|
|
1278
|
+
providerOptions: {
|
|
1279
|
+
...body.providerOptions,
|
|
1280
|
+
gateway: { ...body.providerOptions?.gateway, speed: 'fast' },
|
|
1281
|
+
},
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1230
1286
|
function gatewayHeaders(credential: {
|
|
1231
1287
|
token: string
|
|
1232
1288
|
method: string
|
package/src/ai-stored-state.ts
CHANGED
|
@@ -211,9 +211,7 @@ const storeState = <S extends AIStoredView>(state: S): AIStoredState => {
|
|
|
211
211
|
if (
|
|
212
212
|
value === undefined ||
|
|
213
213
|
(typeof value === 'string' && value.length > 80) ||
|
|
214
|
-
(field !== undefined &&
|
|
215
|
-
payloadFields.has(field) &&
|
|
216
|
-
!(field === 'input' && isRecord(value) && 'message' in value)) ||
|
|
214
|
+
(field !== undefined && payloadFields.has(field)) ||
|
|
217
215
|
(isRecord(value) && Object.hasOwn(value, '$a2Value'))
|
|
218
216
|
)
|
|
219
217
|
return intern(value)
|
package/src/ai.ts
CHANGED
|
@@ -23,7 +23,8 @@ import {
|
|
|
23
23
|
} from './ai-client-state.ts'
|
|
24
24
|
export type { PendingQueueCommand } from './ai-client-state.ts'
|
|
25
25
|
import { applyControlChanges } from './ai-control-state.ts'
|
|
26
|
-
import {
|
|
26
|
+
import { STEERED_REASON, unexplainedAdmission } from './ai-control.ts'
|
|
27
|
+
import { isJSONCompatible, isRecord, isUIMessage } from './ai-message-shape.ts'
|
|
27
28
|
import { AI_EVENT_ID_PREFIX } from './ai-id.ts'
|
|
28
29
|
import type {
|
|
29
30
|
ControlCommit,
|
|
@@ -31,6 +32,7 @@ import type {
|
|
|
31
32
|
ControlCommand,
|
|
32
33
|
ControlReceipt,
|
|
33
34
|
InboxItem,
|
|
35
|
+
InputRef,
|
|
34
36
|
ToolExecutionRequest,
|
|
35
37
|
WorkReport,
|
|
36
38
|
} from './ai-control.ts'
|
|
@@ -39,6 +41,7 @@ export type {
|
|
|
39
41
|
ControlCommand,
|
|
40
42
|
ControlReceipt,
|
|
41
43
|
InboxItem,
|
|
44
|
+
InputRef,
|
|
42
45
|
} from './ai-control.ts'
|
|
43
46
|
import { contract as createContract } from './contract.ts'
|
|
44
47
|
import type { Contract, ContractEvent, EventDefs } from './contract.ts'
|
|
@@ -139,10 +142,15 @@ export type TranscriptMessage<M extends UIMessage> = Omit<M, 'parts'> & {
|
|
|
139
142
|
|
|
140
143
|
export type SessionCreatedPayload = { metadata?: JSONValue }
|
|
141
144
|
export type SessionClosedPayload = { reason?: string }
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* A message entering the transcript. Its content is the command's that brought
|
|
147
|
+
* it in, which every reader of the transcript has already folded.
|
|
148
|
+
*/
|
|
149
|
+
export type MessageCreatedPayload = {
|
|
150
|
+
messageId: string
|
|
151
|
+
role: UIMessage['role']
|
|
152
|
+
/** For user messages, false records passive context. Omission schedules. */
|
|
153
|
+
generate?: false
|
|
146
154
|
}
|
|
147
155
|
export type MessageCompletedPayload = { messageId: string }
|
|
148
156
|
type MessageInterruption = {
|
|
@@ -312,7 +320,7 @@ export type AIEventDefs<M extends UIMessage = UIMessage> = {
|
|
|
312
320
|
}>
|
|
313
321
|
'ai.session.created': StandardSchemaV1<SessionCreatedPayload>
|
|
314
322
|
'ai.session.closed': StandardSchemaV1<SessionClosedPayload>
|
|
315
|
-
'ai.message.created': StandardSchemaV1<MessageCreatedPayload
|
|
323
|
+
'ai.message.created': StandardSchemaV1<MessageCreatedPayload>
|
|
316
324
|
'ai.message.completed': StandardSchemaV1<MessageCompletedPayload>
|
|
317
325
|
'ai.message.interrupted': StandardSchemaV1<MessageInterruptedPayload>
|
|
318
326
|
'ai.generation.requested': StandardSchemaV1<GenerationRequestedPayload>
|
|
@@ -402,7 +410,7 @@ export type GenerationUsage = {
|
|
|
402
410
|
export type AIState<M extends UIMessage = UIMessage> = {
|
|
403
411
|
inbox: { paused: boolean; items: InboxItem<M>[] }
|
|
404
412
|
pendingQueueCommands?: PendingQueueCommand<M>[]
|
|
405
|
-
active: ActiveTurn
|
|
413
|
+
active: ActiveTurn | null
|
|
406
414
|
starting: boolean
|
|
407
415
|
receipt: ControlReceipt | null
|
|
408
416
|
pausedGenerationIds?: Record<string, true>
|
|
@@ -433,13 +441,13 @@ export type AIState<M extends UIMessage = UIMessage> = {
|
|
|
433
441
|
error: string | null
|
|
434
442
|
}
|
|
435
443
|
|
|
436
|
-
/** The `UIMessage` type a contract's
|
|
444
|
+
/** The `UIMessage` type a contract's input commands carry. */
|
|
437
445
|
export type AIMessageOf<D> = D extends {
|
|
438
|
-
readonly 'ai.
|
|
446
|
+
readonly 'ai.control.requested': infer S extends StandardSchemaV1
|
|
439
447
|
}
|
|
440
|
-
? StandardSchemaV1.InferOutput<S> extends
|
|
441
|
-
infer M extends UIMessage
|
|
442
|
-
|
|
448
|
+
? Extract<StandardSchemaV1.InferOutput<S>, { action: 'send' }> extends {
|
|
449
|
+
message: infer M extends UIMessage
|
|
450
|
+
}
|
|
443
451
|
? M
|
|
444
452
|
: UIMessage
|
|
445
453
|
: UIMessage
|
|
@@ -529,33 +537,6 @@ const schema = <T>(
|
|
|
529
537
|
},
|
|
530
538
|
})
|
|
531
539
|
|
|
532
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
533
|
-
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
534
|
-
|
|
535
|
-
const isJSONCompatible = (
|
|
536
|
-
value: unknown,
|
|
537
|
-
seen: Set<object> = new Set(),
|
|
538
|
-
): boolean => {
|
|
539
|
-
if (
|
|
540
|
-
value === null ||
|
|
541
|
-
typeof value === 'string' ||
|
|
542
|
-
typeof value === 'boolean'
|
|
543
|
-
) {
|
|
544
|
-
return true
|
|
545
|
-
}
|
|
546
|
-
if (typeof value === 'number') return Number.isFinite(value)
|
|
547
|
-
if (typeof value === 'undefined') return true
|
|
548
|
-
if (typeof value !== 'object') return false
|
|
549
|
-
if (seen.has(value)) return false
|
|
550
|
-
seen.add(value)
|
|
551
|
-
const valid = Array.isArray(value)
|
|
552
|
-
? value.every((item) => isJSONCompatible(item, seen))
|
|
553
|
-
: Object.getPrototypeOf(value) === Object.prototype &&
|
|
554
|
-
Object.values(value).every((item) => isJSONCompatible(item, seen))
|
|
555
|
-
seen.delete(value)
|
|
556
|
-
return valid
|
|
557
|
-
}
|
|
558
|
-
|
|
559
540
|
const jsonObject = <T extends object>(
|
|
560
541
|
label: string,
|
|
561
542
|
check: (value: Record<string, unknown>) => boolean,
|
|
@@ -584,45 +565,39 @@ const optionalStringField = (
|
|
|
584
565
|
key: string,
|
|
585
566
|
): boolean => value[key] === undefined || typeof value[key] === 'string'
|
|
586
567
|
|
|
587
|
-
const messageValue = (value: unknown): value is UIMessage =>
|
|
588
|
-
isRecord(value) &&
|
|
589
|
-
typeof value['id'] === 'string' &&
|
|
590
|
-
(value['role'] === 'user' ||
|
|
591
|
-
value['role'] === 'assistant' ||
|
|
592
|
-
value['role'] === 'system') &&
|
|
593
|
-
Array.isArray(value['parts']) &&
|
|
594
|
-
value['parts'].every(
|
|
595
|
-
(part) => isRecord(part) && typeof part['type'] === 'string',
|
|
596
|
-
) &&
|
|
597
|
-
isJSONCompatible(value)
|
|
598
|
-
|
|
599
568
|
const validateMessage = <M extends UIMessage>(
|
|
600
569
|
value: unknown,
|
|
601
570
|
messageSchema?: StandardSchemaV1<M>,
|
|
602
571
|
): SchemaResult<M> => {
|
|
603
|
-
if (!
|
|
572
|
+
if (!isUIMessage(value)) return issue('invalid AI SDK UIMessage')
|
|
604
573
|
if (!messageSchema) return { value: value as M }
|
|
605
574
|
return validateSync(messageSchema, value, 'the AI message')
|
|
606
575
|
}
|
|
607
576
|
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
577
|
+
const messageCreatedSchema: StandardSchemaV1<MessageCreatedPayload> = schema(
|
|
578
|
+
'ai.message.created',
|
|
579
|
+
(value) => {
|
|
580
|
+
if (
|
|
581
|
+
!isRecord(value) ||
|
|
582
|
+
Object.keys(value).some(
|
|
583
|
+
(key) => key !== 'messageId' && key !== 'role' && key !== 'generate',
|
|
584
|
+
) ||
|
|
585
|
+
!stringField(value, 'messageId') ||
|
|
586
|
+
(value['role'] !== 'user' &&
|
|
587
|
+
value['role'] !== 'assistant' &&
|
|
588
|
+
value['role'] !== 'system') ||
|
|
589
|
+
(value['generate'] !== undefined && value['generate'] !== false)
|
|
590
|
+
)
|
|
591
|
+
return issue('invalid ai.message.created payload')
|
|
619
592
|
return {
|
|
620
593
|
value: {
|
|
621
|
-
|
|
622
|
-
|
|
594
|
+
messageId: value['messageId'] as string,
|
|
595
|
+
role: value['role'],
|
|
596
|
+
...(value['generate'] === false ? { generate: false as const } : {}),
|
|
623
597
|
},
|
|
624
598
|
}
|
|
625
|
-
}
|
|
599
|
+
},
|
|
600
|
+
)
|
|
626
601
|
|
|
627
602
|
const progressPayloadSchema: StandardSchemaV1<GenerationProgressPayload> =
|
|
628
603
|
schema('ai.generation.progress', (value) => {
|
|
@@ -731,6 +706,14 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
|
|
|
731
706
|
'AI command',
|
|
732
707
|
)
|
|
733
708
|
if (payload.issues) return { issues: payload.issues }
|
|
709
|
+
if (
|
|
710
|
+
action === 'interrupt' &&
|
|
711
|
+
isRecord(payload.value) &&
|
|
712
|
+
payload.value['reason'] === STEERED_REASON
|
|
713
|
+
)
|
|
714
|
+
return issue(
|
|
715
|
+
`the interruption reason "${STEERED_REASON}" is reserved for steering`,
|
|
716
|
+
)
|
|
734
717
|
return { value: { action, [key]: payload.value } as ControlCommand<M> }
|
|
735
718
|
}
|
|
736
719
|
if (action === 'send' || action === 'edit' || action === 'steer') {
|
|
@@ -857,7 +840,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
|
|
|
857
840
|
'ai.session.closed': jsonObject('ai.session.closed', (value) =>
|
|
858
841
|
optionalStringField(value, 'reason'),
|
|
859
842
|
),
|
|
860
|
-
'ai.message.created':
|
|
843
|
+
'ai.message.created': messageCreatedSchema,
|
|
861
844
|
'ai.message.completed': jsonObject('ai.message.completed', (value) =>
|
|
862
845
|
stringField(value, 'messageId'),
|
|
863
846
|
),
|
|
@@ -1036,7 +1019,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
|
|
|
1036
1019
|
export const events: AIEventDefs<UIMessage> = createEvents()
|
|
1037
1020
|
|
|
1038
1021
|
const initialState = <M extends UIMessage>(): AIState<M> => ({
|
|
1039
|
-
inbox:
|
|
1022
|
+
inbox: { paused: false, items: [] },
|
|
1040
1023
|
pendingQueueCommands: [],
|
|
1041
1024
|
active: null,
|
|
1042
1025
|
starting: false,
|
|
@@ -1153,6 +1136,22 @@ export function reduceAIState<M extends UIMessage>(
|
|
|
1153
1136
|
)
|
|
1154
1137
|
}
|
|
1155
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* An input's content, while it is queued or about to enter the transcript:
|
|
1141
|
+
* its inbox item's, or its pending send's or steer's, whose command id is the
|
|
1142
|
+
* input's id.
|
|
1143
|
+
*/
|
|
1144
|
+
function inputContent<M extends UIMessage>(state: AIState<M>, id: string): M {
|
|
1145
|
+
const queued = state.inbox.items.find((item) => item.id === id)?.message
|
|
1146
|
+
if (queued !== undefined) return queued
|
|
1147
|
+
const command = state.pendingQueueCommands?.find(
|
|
1148
|
+
(item) => item.commandId === id,
|
|
1149
|
+
)?.command
|
|
1150
|
+
if (command?.action === 'send' || command?.action === 'steer')
|
|
1151
|
+
return command.message
|
|
1152
|
+
throw unexplainedAdmission(id)
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1156
1155
|
function reduceAIEvent<M extends UIMessage>(
|
|
1157
1156
|
state: AIState<M>,
|
|
1158
1157
|
event: { type: string; payload: unknown; index: number; id?: string },
|
|
@@ -1177,12 +1176,35 @@ function reduceAIEvent<M extends UIMessage>(
|
|
|
1177
1176
|
}
|
|
1178
1177
|
case 'ai.control.committed': {
|
|
1179
1178
|
const commit = event.payload as ControlCommit
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1179
|
+
const changes = commit.changes.filter(
|
|
1180
|
+
(change) => change.path[0] === 'inbox' || change.path[0] === 'active',
|
|
1181
|
+
)
|
|
1182
|
+
let { inbox, active } = state
|
|
1183
|
+
if (changes.length > 0) {
|
|
1184
|
+
// A commit holds inputs by reference; their content is the commands'.
|
|
1185
|
+
const visible = applyControlChanges({
|
|
1186
|
+
state: {
|
|
1187
|
+
inbox: {
|
|
1188
|
+
...inbox,
|
|
1189
|
+
items: inbox.items.map(
|
|
1190
|
+
({ message: _, ...input }): InputRef => input,
|
|
1191
|
+
),
|
|
1192
|
+
},
|
|
1193
|
+
active,
|
|
1194
|
+
},
|
|
1195
|
+
changes,
|
|
1196
|
+
})
|
|
1197
|
+
const queued = new Map(inbox.items.map((item) => [item.id, item]))
|
|
1198
|
+
inbox = {
|
|
1199
|
+
...visible.inbox,
|
|
1200
|
+
items: visible.inbox.items.map((input) => ({
|
|
1201
|
+
...input,
|
|
1202
|
+
message:
|
|
1203
|
+
queued.get(input.id)?.message ?? inputContent(state, input.id),
|
|
1204
|
+
})),
|
|
1205
|
+
}
|
|
1206
|
+
active = visible.active
|
|
1207
|
+
}
|
|
1186
1208
|
const { phase } = commit.view
|
|
1187
1209
|
const status: AgentStatus = commit.view.closed
|
|
1188
1210
|
? 'closed'
|
|
@@ -1199,7 +1221,8 @@ function reduceAIEvent<M extends UIMessage>(
|
|
|
1199
1221
|
: 'idle'
|
|
1200
1222
|
return {
|
|
1201
1223
|
...state,
|
|
1202
|
-
|
|
1224
|
+
inbox,
|
|
1225
|
+
active,
|
|
1203
1226
|
status,
|
|
1204
1227
|
activeRequestId: commit.view.requestId,
|
|
1205
1228
|
activeRequestReason: commit.view.requestReason,
|
|
@@ -1214,9 +1237,24 @@ function reduceAIEvent<M extends UIMessage>(
|
|
|
1214
1237
|
const settled = pending.find(
|
|
1215
1238
|
(item) => item.commandId === receipt.commandId,
|
|
1216
1239
|
)
|
|
1240
|
+
// An edit's content takes effect at its receipt.
|
|
1241
|
+
const edited =
|
|
1242
|
+
receipt.outcome === 'applied' && settled?.command.action === 'edit'
|
|
1243
|
+
? settled.command.message
|
|
1244
|
+
: undefined
|
|
1217
1245
|
return {
|
|
1218
1246
|
...state,
|
|
1219
1247
|
receipt,
|
|
1248
|
+
...(edited === undefined
|
|
1249
|
+
? {}
|
|
1250
|
+
: {
|
|
1251
|
+
inbox: {
|
|
1252
|
+
...state.inbox,
|
|
1253
|
+
items: state.inbox.items.map((item) =>
|
|
1254
|
+
item.id === edited.id ? { ...item, message: edited } : item,
|
|
1255
|
+
),
|
|
1256
|
+
},
|
|
1257
|
+
}),
|
|
1220
1258
|
...(settled === undefined
|
|
1221
1259
|
? {}
|
|
1222
1260
|
: {
|
|
@@ -1329,7 +1367,8 @@ function reduceAIEvent<M extends UIMessage>(
|
|
|
1329
1367
|
activeProjection: null,
|
|
1330
1368
|
}
|
|
1331
1369
|
case 'ai.message.created': {
|
|
1332
|
-
const {
|
|
1370
|
+
const { messageId } = event.payload as MessageCreatedPayload
|
|
1371
|
+
const message = inputContent(state, messageId)
|
|
1333
1372
|
return {
|
|
1334
1373
|
...state,
|
|
1335
1374
|
messages: upsertMessage(state.messages, message),
|
|
@@ -2516,7 +2555,7 @@ export function handlerContext<D extends AgentToolEventDefs = AIEventDefs>(
|
|
|
2516
2555
|
}
|
|
2517
2556
|
|
|
2518
2557
|
/** The built-in AI reducer's name, and so its checkpoint identity. */
|
|
2519
|
-
export const AI_STATE_REDUCER = 'a2.ai.state.
|
|
2558
|
+
export const AI_STATE_REDUCER = 'a2.ai.state.v27'
|
|
2520
2559
|
|
|
2521
2560
|
/**
|
|
2522
2561
|
* The built-in AI protocol plus application events, as a core contract whose
|
package/src/errors.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type A2ServerErrorCode =
|
|
|
7
7
|
| 'SESSION_CLEARED' // a read or call reached across the session's latest clear
|
|
8
8
|
| 'CLAIM_EXPIRED' // ctx.signal abort reason: the attempt's lease lapsed without renewal
|
|
9
9
|
| 'STORE_UNAVAILABLE' // store backend failure — retryable, nothing was written
|
|
10
|
-
| 'STORE_NOT_CONFIGURED' //
|
|
10
|
+
| 'STORE_NOT_CONFIGURED' // no `store` in production, a missing peer dependency, or Postgres lacks A2's tables
|
|
11
11
|
| 'UNKNOWN_PRESENCE_FIELD' // presence field not in the contract's `presence` map
|
|
12
12
|
| 'PRESENCE_NOT_SUPPORTED' // contract declares presence but the store lacks the capability
|
|
13
13
|
|
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
|
package/src/libfx.d.ts
CHANGED
|
@@ -14,8 +14,15 @@ declare module 'libfx' {
|
|
|
14
14
|
cancel(): void
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export type FxPromptBlock =
|
|
18
|
+
| { type: 'text'; text: string }
|
|
19
|
+
| { type: 'image'; data: string; mimeType: string }
|
|
20
|
+
|
|
17
21
|
export type FxAgent = {
|
|
18
|
-
prompt(
|
|
22
|
+
prompt(
|
|
23
|
+
input: string | FxPromptBlock[],
|
|
24
|
+
options?: { signal?: AbortSignal },
|
|
25
|
+
): FxTurn
|
|
19
26
|
checkpoint(): Promise<Uint8Array>
|
|
20
27
|
close(): Promise<void>
|
|
21
28
|
}
|
|
@@ -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
|
},
|