experimental-a2 0.3.0 → 0.5.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 +207 -0
- package/dist/ai-CFNeCrRl.d.ts +334 -0
- package/dist/ai-CFNeCrRl.d.ts.map +1 -0
- package/dist/ai-server.browser.js +2 -0
- package/dist/ai-server.browser.js.map +1 -0
- package/dist/ai-server.d.ts +49 -9
- package/dist/ai-server.d.ts.map +1 -0
- package/dist/ai-server.js +123 -49
- package/dist/ai-server.js.map +1 -0
- package/dist/ai.d.ts +2 -303
- package/dist/ai.js +233 -86
- package/dist/ai.js.map +1 -0
- package/dist/cli-B3VuxoDe.js +599 -0
- package/dist/cli-B3VuxoDe.js.map +1 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +7 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/client-BKlyLiOU.js +1008 -0
- package/dist/client-BKlyLiOU.js.map +1 -0
- package/dist/client-D7mvIXrF.d.ts +191 -0
- package/dist/client-D7mvIXrF.d.ts.map +1 -0
- package/dist/client.d.ts +2 -90
- package/dist/client.js +1 -409
- package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +12 -2
- package/dist/contract-48bUMgcL.js.map +1 -0
- package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +63 -8
- package/dist/contract-jIfaR085.d.ts.map +1 -0
- package/dist/devtools-J_jZ2vQf.d.ts +152 -0
- package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
- package/dist/devtools-kJJaORn-.js +340 -0
- package/dist/devtools-kJJaORn-.js.map +1 -0
- package/dist/devtools-server.browser.js +3 -1
- package/dist/devtools-server.browser.js.map +1 -0
- package/dist/devtools-server.d.ts +4 -3
- package/dist/devtools-server.d.ts.map +1 -0
- package/dist/devtools-server.js +226 -43
- package/dist/devtools-server.js.map +1 -0
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +2 -0
- package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +6 -4
- package/dist/errors-BQuJpe82.js.map +1 -0
- package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +3 -2
- package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
- package/dist/http.d.ts +135 -28
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +647 -61
- package/dist/http.js.map +1 -0
- package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +4 -2
- package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +4 -1
- package/dist/inspection-DaxB5jM2.js.map +1 -0
- package/dist/internal-DstsI6Re.js +211 -0
- package/dist/internal-DstsI6Re.js.map +1 -0
- package/dist/otel.d.ts +3 -2
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +2 -0
- package/dist/otel.js.map +1 -0
- package/dist/platform-B4TnJtWu.js +34 -0
- package/dist/platform-B4TnJtWu.js.map +1 -0
- package/dist/react.d.ts +47 -15
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +30 -15
- package/dist/react.js.map +1 -0
- package/dist/retryable-lazy-DZWmHpii.js +2 -0
- package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +79 -0
- package/dist/scheduler-qstash.d.ts.map +1 -0
- package/dist/scheduler-qstash.js +501 -0
- package/dist/scheduler-qstash.js.map +1 -0
- package/dist/scheduler-task-BpzhPnRS.js +56 -0
- package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
- package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +18 -24
- package/dist/scheduler-vercel.d.ts.map +1 -0
- package/dist/scheduler-vercel.js +228 -0
- package/dist/scheduler-vercel.js.map +1 -0
- package/dist/server-C72KOw51.js +1457 -0
- package/dist/server-C72KOw51.js.map +1 -0
- package/dist/server-DUF9pjsx.d.ts +261 -0
- package/dist/server-DUF9pjsx.d.ts.map +1 -0
- package/dist/server.browser.js +2 -0
- package/dist/server.browser.js.map +1 -0
- package/dist/server.d.ts +4 -155
- package/dist/server.js +2 -2
- package/dist/{log-ldf5g8Cx.d.ts → store-DysUkTH3.d.ts} +120 -35
- package/dist/store-DysUkTH3.d.ts.map +1 -0
- package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +3 -1
- package/dist/store-N8PXxDAS.js.map +1 -0
- package/dist/store-codec-DTG0Ftek.js +10 -0
- package/dist/store-codec-DTG0Ftek.js.map +1 -0
- package/dist/store-memory.d.ts +12 -0
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/{log-memory.js → store-memory.js} +145 -34
- package/dist/store-memory.js.map +1 -0
- package/dist/{log-polling-DZ1MiKLg.js → store-polling-dSeLxzfb.js} +7 -5
- package/dist/store-polling-dSeLxzfb.js.map +1 -0
- package/dist/{log-postgres.d.ts → store-postgres.d.ts} +7 -6
- package/dist/store-postgres.d.ts.map +1 -0
- package/dist/{log-postgres.js → store-postgres.js} +214 -24
- package/dist/store-postgres.js.map +1 -0
- package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-BFLwz0Wj.js} +271 -66
- package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
- package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +8 -7
- package/dist/store-redis-http.d.ts.map +1 -0
- package/dist/{log-redis-http.js → store-redis-http.js} +22 -12
- package/dist/store-redis-http.js.map +1 -0
- package/dist/{log-redis.d.ts → store-redis.d.ts} +7 -6
- package/dist/store-redis.d.ts.map +1 -0
- package/dist/{log-redis.js → store-redis.js} +101 -39
- package/dist/store-redis.js.map +1 -0
- package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +7 -6
- package/dist/store-sqlite.d.ts.map +1 -0
- package/dist/{log-sqlite.js → store-sqlite.js} +118 -22
- package/dist/store-sqlite.js.map +1 -0
- package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +3 -2
- package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
- package/dist/testing.browser.d.ts +1 -0
- package/dist/testing.browser.js +6 -0
- package/dist/testing.browser.js.map +1 -0
- package/dist/testing.d.ts +32 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +103 -0
- package/dist/testing.js.map +1 -0
- package/dist/validate-XKT4FSNn.js +2 -0
- package/dist/validate-XKT4FSNn.js.map +1 -0
- package/dist/wire-BFQmSJ-9.js +284 -0
- package/dist/wire-BFQmSJ-9.js.map +1 -0
- package/docs/01-quickstart.mdx +3 -3
- package/docs/concepts/01-contracts.mdx +18 -13
- package/docs/concepts/02-handlers.mdx +7 -7
- package/docs/concepts/03-durability.mdx +21 -21
- package/docs/concepts/04-state.mdx +18 -21
- package/docs/guides/01-timers.mdx +154 -49
- package/docs/guides/02-cancellation.mdx +30 -4
- package/docs/guides/03-react.mdx +76 -57
- package/docs/guides/04-local-first.mdx +1 -1
- package/docs/guides/05-production.mdx +310 -61
- package/docs/guides/06-ai-agents.mdx +252 -74
- package/docs/guides/07-devtools.mdx +137 -12
- package/docs/guides/08-application-data.mdx +1 -1
- package/docs/guides/09-presence.mdx +263 -0
- package/docs/guides/10-transports.mdx +140 -0
- package/docs/index.mdx +18 -14
- package/docs/reference/01-api.mdx +839 -110
- package/docs/reference/02-errors.mdx +13 -6
- package/package.json +24 -7
- package/src/ai-coordinator.ts +358 -0
- package/src/ai-projector.ts +524 -0
- package/src/ai-sdk-step.ts +261 -0
- package/src/ai-server.browser.ts +5 -0
- package/src/ai-server.ts +1719 -0
- package/src/ai.ts +2155 -0
- package/src/cache-indexeddb.ts +10 -0
- package/src/cli-bin.ts +5 -0
- package/src/cli.ts +1046 -0
- package/src/client.ts +1826 -0
- package/src/contract.ts +206 -0
- package/src/deterministic-id.ts +72 -0
- package/src/devtools-app.ts +989 -0
- package/src/devtools-server.browser.ts +5 -0
- package/src/devtools-server.ts +604 -0
- package/src/devtools.ts +716 -0
- package/src/errors.ts +50 -0
- package/src/http.ts +394 -0
- package/src/idempotent-replay.ts +53 -0
- package/src/index.ts +37 -0
- package/src/inspection.ts +39 -0
- package/src/internal.ts +426 -0
- package/src/otel.ts +59 -0
- package/src/platform.ts +60 -0
- package/src/push-envelope.ts +137 -0
- package/src/react.ts +284 -0
- package/src/reducer.ts +108 -0
- package/src/retryable-lazy.ts +27 -0
- package/src/scheduler-qstash.ts +915 -0
- package/src/scheduler-task.ts +106 -0
- package/src/scheduler-vercel.ts +437 -0
- package/src/server.browser.ts +12 -0
- package/src/server.ts +2700 -0
- package/src/session-socket.ts +548 -0
- package/src/sse.ts +141 -0
- package/src/standard-schema.ts +77 -0
- package/src/store-codec.ts +10 -0
- package/src/store-memory.ts +788 -0
- package/src/store-polling.ts +102 -0
- package/src/store-postgres.ts +1212 -0
- package/src/store-redis-core.ts +1494 -0
- package/src/store-redis-http.ts +116 -0
- package/src/store-redis.ts +458 -0
- package/src/store-sqlite.ts +1108 -0
- package/src/store.ts +385 -0
- package/src/telemetry.ts +47 -0
- package/src/testing.browser.ts +5 -0
- package/src/testing.ts +185 -0
- package/src/validate.ts +39 -0
- package/src/wire.ts +454 -0
- package/dist/internal-gCd5qMry.js +0 -44
- package/dist/log-memory.d.ts +0 -11
- package/dist/recovery-vercel.js +0 -119
- package/dist/server-BcLa4RFL.js +0 -877
- package/dist/wire-BVsgR8o9.js +0 -62
package/src/ai.ts
ADDED
|
@@ -0,0 +1,2155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* experimental-a2/ai — an isomorphic, extensible agent definition.
|
|
3
|
+
*
|
|
4
|
+
* `agent()` assembles a normal A2 contract, a reducer over the built-in
|
|
5
|
+
* AI protocol, and pure builders for the events applications append.
|
|
6
|
+
* Model calls and tool implementations stay in experimental-a2/ai/server.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
FinishReason,
|
|
11
|
+
JSONValue,
|
|
12
|
+
LanguageModelUsage,
|
|
13
|
+
UIMessage,
|
|
14
|
+
UIMessageChunk,
|
|
15
|
+
} from 'ai'
|
|
16
|
+
import { contract as createContract } from './contract.ts'
|
|
17
|
+
import type { AppendInput, Contract, EventDefs } from './contract.ts'
|
|
18
|
+
import { interruptUIMessage, projectUIMessage } from './ai-projector.ts'
|
|
19
|
+
import { ambientToolScopeStorage } from './internal.ts'
|
|
20
|
+
import type { Reducer } from './reducer.ts'
|
|
21
|
+
import type { HandlerContext } from './server.ts'
|
|
22
|
+
import type { StandardSchemaV1 } from './standard-schema.ts'
|
|
23
|
+
import { assertSyncSchema, validateSync } from './validate.ts'
|
|
24
|
+
|
|
25
|
+
export type AgentStatus =
|
|
26
|
+
'idle' | 'generating' | 'waiting' | 'failed' | 'closed'
|
|
27
|
+
|
|
28
|
+
export type GenerationReason = 'message' | 'tool' | 'input' | 'retry'
|
|
29
|
+
|
|
30
|
+
export type SessionCreatedPayload = { metadata?: JSONValue }
|
|
31
|
+
export type SessionClosedPayload = { reason?: string }
|
|
32
|
+
export type MessageCreatedPayload<M extends UIMessage> = {
|
|
33
|
+
message: M
|
|
34
|
+
/** For user messages, false records passive context. Omission or true schedules. */
|
|
35
|
+
generate?: boolean
|
|
36
|
+
}
|
|
37
|
+
export type MessageCompletedPayload = { messageId: string }
|
|
38
|
+
type MessageInterruption = {
|
|
39
|
+
messageId: string
|
|
40
|
+
reason?: string
|
|
41
|
+
/** Last confirmed event-log index visible to the interrupter. */
|
|
42
|
+
lastSeenIndex?: number
|
|
43
|
+
}
|
|
44
|
+
export type MessageInterruptedPayload = MessageInterruption &
|
|
45
|
+
(
|
|
46
|
+
| { generationId: string; requestId?: never }
|
|
47
|
+
| { requestId: string; generationId?: never }
|
|
48
|
+
)
|
|
49
|
+
export type GenerationRequestedPayload = {
|
|
50
|
+
messageId: string
|
|
51
|
+
reason: GenerationReason
|
|
52
|
+
responseMessageId?: string
|
|
53
|
+
}
|
|
54
|
+
export type RetryGenerationOptions = {
|
|
55
|
+
messageId: string
|
|
56
|
+
responseMessageId: string
|
|
57
|
+
retryId: string
|
|
58
|
+
}
|
|
59
|
+
export type RetryRequestedPayload = RetryGenerationOptions
|
|
60
|
+
export type GenerationStartedPayload = {
|
|
61
|
+
requestId: string
|
|
62
|
+
messageId: string
|
|
63
|
+
generationId: string
|
|
64
|
+
responseMessageId: string
|
|
65
|
+
attempt: number
|
|
66
|
+
model: string
|
|
67
|
+
}
|
|
68
|
+
export type GenerationProgressPayload = {
|
|
69
|
+
requestId: string
|
|
70
|
+
messageId: string
|
|
71
|
+
generationId: string
|
|
72
|
+
responseMessageId: string
|
|
73
|
+
sequence: number
|
|
74
|
+
chunks: UIMessageChunk[]
|
|
75
|
+
}
|
|
76
|
+
export type GenerationCompletedPayload = {
|
|
77
|
+
requestId: string
|
|
78
|
+
messageId: string
|
|
79
|
+
generationId: string
|
|
80
|
+
responseMessageId: string
|
|
81
|
+
finishReason?: FinishReason
|
|
82
|
+
usage?: LanguageModelUsage
|
|
83
|
+
}
|
|
84
|
+
export type GenerationFailedPayload = {
|
|
85
|
+
requestId: string
|
|
86
|
+
messageId: string
|
|
87
|
+
generationId: string
|
|
88
|
+
responseMessageId: string
|
|
89
|
+
error: string
|
|
90
|
+
/** True when a later attempt replaced an incomplete attempt. */
|
|
91
|
+
superseded?: boolean
|
|
92
|
+
stepLimit?: boolean
|
|
93
|
+
}
|
|
94
|
+
export type ToolCalledPayload = {
|
|
95
|
+
requestId: string
|
|
96
|
+
messageId: string
|
|
97
|
+
generationId: string
|
|
98
|
+
toolCallId: string
|
|
99
|
+
toolName: string
|
|
100
|
+
input: unknown
|
|
101
|
+
dynamic?: boolean
|
|
102
|
+
providerExecuted?: boolean
|
|
103
|
+
supportsDeferredResults?: boolean
|
|
104
|
+
providerMetadata?: unknown
|
|
105
|
+
toolMetadata?: unknown
|
|
106
|
+
title?: string
|
|
107
|
+
}
|
|
108
|
+
export type ToolResultPayload = {
|
|
109
|
+
requestId: string
|
|
110
|
+
messageId: string
|
|
111
|
+
generationId: string
|
|
112
|
+
toolCallId: string
|
|
113
|
+
toolName?: string
|
|
114
|
+
input?: unknown
|
|
115
|
+
rawInput?: unknown
|
|
116
|
+
output?: unknown
|
|
117
|
+
error?: string
|
|
118
|
+
denied?: boolean
|
|
119
|
+
preliminary?: boolean
|
|
120
|
+
phase?: 'input' | 'execution'
|
|
121
|
+
dynamic?: boolean
|
|
122
|
+
providerExecuted?: boolean
|
|
123
|
+
providerMetadata?: unknown
|
|
124
|
+
toolMetadata?: unknown
|
|
125
|
+
}
|
|
126
|
+
export type ApprovalRequestedPayload = {
|
|
127
|
+
messageId: string
|
|
128
|
+
generationId: string
|
|
129
|
+
approvalId: string
|
|
130
|
+
toolCallId: string
|
|
131
|
+
isAutomatic?: boolean
|
|
132
|
+
signature?: string
|
|
133
|
+
}
|
|
134
|
+
export type ApprovalRespondedPayload = {
|
|
135
|
+
messageId: string
|
|
136
|
+
generationId: string
|
|
137
|
+
approvalId: string
|
|
138
|
+
approved: boolean
|
|
139
|
+
reason?: string
|
|
140
|
+
}
|
|
141
|
+
export type InputRequestedPayload = {
|
|
142
|
+
messageId: string
|
|
143
|
+
generationId: string
|
|
144
|
+
inputId: string
|
|
145
|
+
name: string
|
|
146
|
+
question?: JSONValue
|
|
147
|
+
}
|
|
148
|
+
export type InputRespondedPayload = {
|
|
149
|
+
messageId: string
|
|
150
|
+
generationId: string
|
|
151
|
+
inputId: string
|
|
152
|
+
name: string
|
|
153
|
+
value: JSONValue
|
|
154
|
+
}
|
|
155
|
+
export type CompactionRequestedPayload = {
|
|
156
|
+
generationId: string
|
|
157
|
+
throughMessageId: string
|
|
158
|
+
}
|
|
159
|
+
export type CompactionCompletedPayload<M extends UIMessage> = {
|
|
160
|
+
generationId: string
|
|
161
|
+
throughMessageId: string
|
|
162
|
+
messages: M[]
|
|
163
|
+
retainedMessageIds?: string[]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type AIEventDefs<M extends UIMessage = UIMessage> = {
|
|
167
|
+
'ai.session.created': StandardSchemaV1<SessionCreatedPayload>
|
|
168
|
+
'ai.session.closed': StandardSchemaV1<SessionClosedPayload>
|
|
169
|
+
'ai.message.created': StandardSchemaV1<MessageCreatedPayload<M>>
|
|
170
|
+
'ai.message.completed': StandardSchemaV1<MessageCompletedPayload>
|
|
171
|
+
'ai.message.interrupted': StandardSchemaV1<MessageInterruptedPayload>
|
|
172
|
+
'ai.generation.requested': StandardSchemaV1<GenerationRequestedPayload>
|
|
173
|
+
'ai.retry.requested': StandardSchemaV1<RetryRequestedPayload>
|
|
174
|
+
'ai.generation.started': StandardSchemaV1<GenerationStartedPayload>
|
|
175
|
+
'ai.generation.progress': StandardSchemaV1<GenerationProgressPayload>
|
|
176
|
+
'ai.generation.completed': StandardSchemaV1<GenerationCompletedPayload>
|
|
177
|
+
'ai.generation.failed': StandardSchemaV1<GenerationFailedPayload>
|
|
178
|
+
'ai.tool.called': StandardSchemaV1<ToolCalledPayload>
|
|
179
|
+
'ai.tool.result': StandardSchemaV1<ToolResultPayload>
|
|
180
|
+
'ai.approval.requested': StandardSchemaV1<ApprovalRequestedPayload>
|
|
181
|
+
'ai.approval.responded': StandardSchemaV1<ApprovalRespondedPayload>
|
|
182
|
+
'ai.input.requested': StandardSchemaV1<InputRequestedPayload>
|
|
183
|
+
'ai.input.responded': StandardSchemaV1<InputRespondedPayload>
|
|
184
|
+
'ai.compaction.requested': StandardSchemaV1<CompactionRequestedPayload>
|
|
185
|
+
'ai.compaction.completed': StandardSchemaV1<CompactionCompletedPayload<M>>
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type ActiveGeneration = GenerationStartedPayload
|
|
189
|
+
|
|
190
|
+
export type ActiveGenerationProjection<M extends UIMessage = UIMessage> = {
|
|
191
|
+
generationId: string
|
|
192
|
+
responseMessageId: string
|
|
193
|
+
/** Log index of the completed model step while its response remains interruptible. */
|
|
194
|
+
completionIndex?: number
|
|
195
|
+
baseMessage?: M
|
|
196
|
+
baseCompaction?: CompactionState<M> | null
|
|
197
|
+
batches: Array<{ index: number; chunks: UIMessageChunk[] }>
|
|
198
|
+
baseTools: ToolActivity[]
|
|
199
|
+
toolEvents: Array<
|
|
200
|
+
| { index: number; type: 'ai.tool.called'; payload: ToolCalledPayload }
|
|
201
|
+
| { index: number; type: 'ai.tool.result'; payload: ToolResultPayload }
|
|
202
|
+
>
|
|
203
|
+
approvalEvents: Array<
|
|
204
|
+
| {
|
|
205
|
+
index: number
|
|
206
|
+
type: 'ai.approval.requested'
|
|
207
|
+
payload: ApprovalRequestedPayload
|
|
208
|
+
}
|
|
209
|
+
| {
|
|
210
|
+
index: number
|
|
211
|
+
type: 'ai.approval.responded'
|
|
212
|
+
payload: ApprovalRespondedPayload
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export type PendingApproval = ApprovalRequestedPayload
|
|
218
|
+
export type PendingInput = InputRequestedPayload
|
|
219
|
+
|
|
220
|
+
export type ToolActivity = {
|
|
221
|
+
requestId: string
|
|
222
|
+
messageId: string
|
|
223
|
+
generationId: string
|
|
224
|
+
toolCallId: string
|
|
225
|
+
toolName?: string
|
|
226
|
+
input?: unknown
|
|
227
|
+
rawInput?: unknown
|
|
228
|
+
output?: unknown
|
|
229
|
+
error?: string
|
|
230
|
+
preliminary?: boolean
|
|
231
|
+
phase?: 'input' | 'execution'
|
|
232
|
+
dynamic?: boolean
|
|
233
|
+
providerExecuted?: boolean
|
|
234
|
+
callProviderMetadata?: unknown
|
|
235
|
+
resultProviderMetadata?: unknown
|
|
236
|
+
toolMetadata?: unknown
|
|
237
|
+
title?: string
|
|
238
|
+
status: 'running' | 'completed' | 'failed' | 'denied'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export type CompactionState<M extends UIMessage> = {
|
|
242
|
+
status: 'running' | 'completed'
|
|
243
|
+
generationId: string
|
|
244
|
+
throughMessageId: string
|
|
245
|
+
messages?: M[]
|
|
246
|
+
retainedMessageIds?: string[]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export type GenerationUsage = {
|
|
250
|
+
generationId: string
|
|
251
|
+
usage: LanguageModelUsage
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type AIState<M extends UIMessage = UIMessage> = {
|
|
255
|
+
session: { created: boolean; metadata?: JSONValue }
|
|
256
|
+
messages: M[]
|
|
257
|
+
status: AgentStatus
|
|
258
|
+
activeGeneration: ActiveGeneration | null
|
|
259
|
+
activeRequestId: string | null
|
|
260
|
+
activeResponseMessageId: string | null
|
|
261
|
+
activeProjection: ActiveGenerationProjection<M> | null
|
|
262
|
+
responseGenerationIds: Record<string, string>
|
|
263
|
+
terminalRequestIds?: Record<string, true>
|
|
264
|
+
terminalGenerations?: Record<
|
|
265
|
+
string,
|
|
266
|
+
'completed' | 'failed' | 'interrupted' | 'superseded'
|
|
267
|
+
>
|
|
268
|
+
pendingApprovals: PendingApproval[]
|
|
269
|
+
pendingInputs: PendingInput[]
|
|
270
|
+
tools: ToolActivity[]
|
|
271
|
+
compaction: CompactionState<M> | null
|
|
272
|
+
usage: GenerationUsage[]
|
|
273
|
+
error: string | null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export type AgentDefinition<
|
|
277
|
+
M extends UIMessage = UIMessage,
|
|
278
|
+
D extends EventDefs = AIEventDefs<M>,
|
|
279
|
+
> = {
|
|
280
|
+
readonly contract: Contract<D>
|
|
281
|
+
readonly reducer: Reducer<D, AIState<M>>
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export type AgentToolEventDefs = EventDefs &
|
|
285
|
+
Pick<AIEventDefs, 'ai.tool.called' | 'ai.approval.responded'>
|
|
286
|
+
|
|
287
|
+
/** The durable handler context supplied when A2 executes a local AI tool. */
|
|
288
|
+
export type AgentToolContext<D extends AgentToolEventDefs> =
|
|
289
|
+
| HandlerContext<D, 'ai.tool.called'>
|
|
290
|
+
| HandlerContext<D, 'ai.approval.responded'>
|
|
291
|
+
|
|
292
|
+
export type AgentOptions<M extends UIMessage, E extends EventDefs> = {
|
|
293
|
+
name: string
|
|
294
|
+
events?: E
|
|
295
|
+
/** Optional stricter schema for custom UIMessage metadata/data/tool types. */
|
|
296
|
+
messageSchema?: StandardSchemaV1<M>
|
|
297
|
+
/** Snapshot identity. Bump when application-level projection semantics change. */
|
|
298
|
+
reducerName?: string
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
type AIMessageFrom<D extends EventDefs> =
|
|
302
|
+
D extends Record<'ai.message.created', infer S extends StandardSchemaV1>
|
|
303
|
+
? StandardSchemaV1.InferOutput<S> extends MessageCreatedPayload<
|
|
304
|
+
infer M extends UIMessage
|
|
305
|
+
>
|
|
306
|
+
? M
|
|
307
|
+
: UIMessage
|
|
308
|
+
: UIMessage
|
|
309
|
+
|
|
310
|
+
type SchemaResult<T> = StandardSchemaV1.Result<T>
|
|
311
|
+
|
|
312
|
+
const issue = <T>(message: string): SchemaResult<T> => ({
|
|
313
|
+
issues: [{ message }],
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const schema = <T>(
|
|
317
|
+
label: string,
|
|
318
|
+
parse: (value: unknown) => SchemaResult<T>,
|
|
319
|
+
): StandardSchemaV1<T> => ({
|
|
320
|
+
'~standard': {
|
|
321
|
+
version: 1,
|
|
322
|
+
vendor: 'a2',
|
|
323
|
+
validate(value) {
|
|
324
|
+
try {
|
|
325
|
+
return parse(value)
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return issue(
|
|
328
|
+
`${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
336
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
337
|
+
|
|
338
|
+
const isJSONCompatible = (
|
|
339
|
+
value: unknown,
|
|
340
|
+
seen: Set<object> = new Set(),
|
|
341
|
+
): boolean => {
|
|
342
|
+
if (
|
|
343
|
+
value === null ||
|
|
344
|
+
typeof value === 'string' ||
|
|
345
|
+
typeof value === 'boolean'
|
|
346
|
+
) {
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
if (typeof value === 'number') return Number.isFinite(value)
|
|
350
|
+
if (typeof value === 'undefined') return true
|
|
351
|
+
if (typeof value !== 'object') return false
|
|
352
|
+
if (seen.has(value)) return false
|
|
353
|
+
seen.add(value)
|
|
354
|
+
const valid = Array.isArray(value)
|
|
355
|
+
? value.every((item) => isJSONCompatible(item, seen))
|
|
356
|
+
: Object.getPrototypeOf(value) === Object.prototype &&
|
|
357
|
+
Object.values(value).every((item) => isJSONCompatible(item, seen))
|
|
358
|
+
seen.delete(value)
|
|
359
|
+
return valid
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const jsonObject = <T extends object>(
|
|
363
|
+
label: string,
|
|
364
|
+
check: (value: Record<string, unknown>) => boolean,
|
|
365
|
+
): StandardSchemaV1<T> =>
|
|
366
|
+
schema(label, (value) => {
|
|
367
|
+
if (!isRecord(value) || !isJSONCompatible(value) || !check(value)) {
|
|
368
|
+
return issue(`invalid ${label}`)
|
|
369
|
+
}
|
|
370
|
+
return { value: value as T }
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const stringField = (value: Record<string, unknown>, key: string): boolean =>
|
|
374
|
+
typeof value[key] === 'string' && value[key].length > 0
|
|
375
|
+
|
|
376
|
+
const optionalStringField = (
|
|
377
|
+
value: Record<string, unknown>,
|
|
378
|
+
key: string,
|
|
379
|
+
): boolean => value[key] === undefined || typeof value[key] === 'string'
|
|
380
|
+
|
|
381
|
+
const messageValue = (value: unknown): value is UIMessage =>
|
|
382
|
+
isRecord(value) &&
|
|
383
|
+
typeof value['id'] === 'string' &&
|
|
384
|
+
(value['role'] === 'user' ||
|
|
385
|
+
value['role'] === 'assistant' ||
|
|
386
|
+
value['role'] === 'system') &&
|
|
387
|
+
Array.isArray(value['parts']) &&
|
|
388
|
+
value['parts'].every(
|
|
389
|
+
(part) => isRecord(part) && typeof part['type'] === 'string',
|
|
390
|
+
) &&
|
|
391
|
+
isJSONCompatible(value)
|
|
392
|
+
|
|
393
|
+
const validateMessage = <M extends UIMessage>(
|
|
394
|
+
value: unknown,
|
|
395
|
+
messageSchema?: StandardSchemaV1<M>,
|
|
396
|
+
): SchemaResult<M> => {
|
|
397
|
+
if (!messageValue(value)) return issue('invalid AI SDK UIMessage')
|
|
398
|
+
if (!messageSchema) return { value: value as M }
|
|
399
|
+
return validateSync(messageSchema, value, 'the AI message')
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const messagePayloadSchema = <M extends UIMessage>(
|
|
403
|
+
messageSchema?: StandardSchemaV1<M>,
|
|
404
|
+
): StandardSchemaV1<MessageCreatedPayload<M>> =>
|
|
405
|
+
schema('ai.message.created', (value) => {
|
|
406
|
+
if (!isRecord(value)) return issue('invalid ai.message.created payload')
|
|
407
|
+
const message = validateMessage(value['message'], messageSchema)
|
|
408
|
+
if (message.issues) return { issues: message.issues }
|
|
409
|
+
const generate = value['generate']
|
|
410
|
+
if (generate !== undefined && typeof generate !== 'boolean') {
|
|
411
|
+
return issue('invalid ai.message.created generate flag')
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
value: {
|
|
415
|
+
message: message.value,
|
|
416
|
+
...(generate === false ? { generate: false } : {}),
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
const progressPayloadSchema: StandardSchemaV1<GenerationProgressPayload> =
|
|
422
|
+
schema('ai.generation.progress', (value) => {
|
|
423
|
+
if (
|
|
424
|
+
!isRecord(value) ||
|
|
425
|
+
!isJSONCompatible(value) ||
|
|
426
|
+
!stringField(value, 'requestId') ||
|
|
427
|
+
!stringField(value, 'messageId') ||
|
|
428
|
+
!stringField(value, 'generationId') ||
|
|
429
|
+
!stringField(value, 'responseMessageId') ||
|
|
430
|
+
typeof value['sequence'] !== 'number' ||
|
|
431
|
+
!Number.isInteger(value['sequence']) ||
|
|
432
|
+
value['sequence'] < 0 ||
|
|
433
|
+
!Array.isArray(value['chunks']) ||
|
|
434
|
+
!value['chunks'].every(
|
|
435
|
+
(chunk) => isRecord(chunk) && typeof chunk['type'] === 'string',
|
|
436
|
+
)
|
|
437
|
+
) {
|
|
438
|
+
return issue('invalid ai.generation.progress payload')
|
|
439
|
+
}
|
|
440
|
+
return { value: value as GenerationProgressPayload }
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const completedPayloadSchema: StandardSchemaV1<GenerationCompletedPayload> =
|
|
444
|
+
schema('ai.generation.completed', (value) => {
|
|
445
|
+
if (
|
|
446
|
+
!isRecord(value) ||
|
|
447
|
+
!isJSONCompatible(value) ||
|
|
448
|
+
!stringField(value, 'requestId') ||
|
|
449
|
+
!stringField(value, 'messageId') ||
|
|
450
|
+
!stringField(value, 'generationId') ||
|
|
451
|
+
!stringField(value, 'responseMessageId') ||
|
|
452
|
+
!optionalStringField(value, 'finishReason')
|
|
453
|
+
) {
|
|
454
|
+
return issue('invalid ai.generation.completed payload')
|
|
455
|
+
}
|
|
456
|
+
return { value: value as GenerationCompletedPayload }
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
const compactionPayloadSchema = <M extends UIMessage>(
|
|
460
|
+
messageSchema?: StandardSchemaV1<M>,
|
|
461
|
+
): StandardSchemaV1<CompactionCompletedPayload<M>> =>
|
|
462
|
+
schema('ai.compaction.completed', (value) => {
|
|
463
|
+
if (
|
|
464
|
+
!isRecord(value) ||
|
|
465
|
+
!isJSONCompatible(value) ||
|
|
466
|
+
!stringField(value, 'generationId') ||
|
|
467
|
+
!stringField(value, 'throughMessageId') ||
|
|
468
|
+
!Array.isArray(value['messages']) ||
|
|
469
|
+
(value['retainedMessageIds'] !== undefined &&
|
|
470
|
+
(!Array.isArray(value['retainedMessageIds']) ||
|
|
471
|
+
!value['retainedMessageIds'].every(
|
|
472
|
+
(candidate) => typeof candidate === 'string',
|
|
473
|
+
)))
|
|
474
|
+
) {
|
|
475
|
+
return issue('invalid ai.compaction.completed payload')
|
|
476
|
+
}
|
|
477
|
+
const messages: M[] = []
|
|
478
|
+
for (const candidate of value['messages']) {
|
|
479
|
+
const result = validateMessage(candidate, messageSchema)
|
|
480
|
+
if (result.issues) return { issues: result.issues }
|
|
481
|
+
messages.push(result.value)
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
value: {
|
|
485
|
+
generationId: value['generationId'] as string,
|
|
486
|
+
throughMessageId: value['throughMessageId'] as string,
|
|
487
|
+
messages,
|
|
488
|
+
...(value['retainedMessageIds'] === undefined
|
|
489
|
+
? {}
|
|
490
|
+
: { retainedMessageIds: value['retainedMessageIds'] as string[] }),
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
export function createEvents<M extends UIMessage = UIMessage>(options?: {
|
|
496
|
+
messageSchema?: StandardSchemaV1<M>
|
|
497
|
+
}): AIEventDefs<M> {
|
|
498
|
+
const messageSchema = options?.messageSchema
|
|
499
|
+
if (messageSchema) assertSyncSchema(messageSchema, 'the AI message schema')
|
|
500
|
+
const definition: AIEventDefs<M> = {
|
|
501
|
+
'ai.session.created': jsonObject('ai.session.created', () => true),
|
|
502
|
+
'ai.session.closed': jsonObject('ai.session.closed', (value) =>
|
|
503
|
+
optionalStringField(value, 'reason'),
|
|
504
|
+
),
|
|
505
|
+
'ai.message.created': messagePayloadSchema(messageSchema),
|
|
506
|
+
'ai.message.completed': jsonObject('ai.message.completed', (value) =>
|
|
507
|
+
stringField(value, 'messageId'),
|
|
508
|
+
),
|
|
509
|
+
'ai.message.interrupted': jsonObject(
|
|
510
|
+
'ai.message.interrupted',
|
|
511
|
+
(value) =>
|
|
512
|
+
stringField(value, 'messageId') &&
|
|
513
|
+
((stringField(value, 'generationId') &&
|
|
514
|
+
value['requestId'] === undefined) ||
|
|
515
|
+
(stringField(value, 'requestId') &&
|
|
516
|
+
value['generationId'] === undefined)) &&
|
|
517
|
+
optionalStringField(value, 'reason') &&
|
|
518
|
+
(value['lastSeenIndex'] === undefined ||
|
|
519
|
+
(typeof value['lastSeenIndex'] === 'number' &&
|
|
520
|
+
Number.isInteger(value['lastSeenIndex']) &&
|
|
521
|
+
value['lastSeenIndex'] >= 0)),
|
|
522
|
+
),
|
|
523
|
+
'ai.generation.requested': jsonObject(
|
|
524
|
+
'ai.generation.requested',
|
|
525
|
+
(value) =>
|
|
526
|
+
stringField(value, 'messageId') &&
|
|
527
|
+
optionalStringField(value, 'responseMessageId') &&
|
|
528
|
+
(value['reason'] === 'message' ||
|
|
529
|
+
value['reason'] === 'tool' ||
|
|
530
|
+
value['reason'] === 'input' ||
|
|
531
|
+
value['reason'] === 'retry'),
|
|
532
|
+
),
|
|
533
|
+
'ai.retry.requested': jsonObject(
|
|
534
|
+
'ai.retry.requested',
|
|
535
|
+
(value) =>
|
|
536
|
+
stringField(value, 'messageId') &&
|
|
537
|
+
stringField(value, 'responseMessageId') &&
|
|
538
|
+
stringField(value, 'retryId'),
|
|
539
|
+
),
|
|
540
|
+
'ai.generation.started': jsonObject(
|
|
541
|
+
'ai.generation.started',
|
|
542
|
+
(value) =>
|
|
543
|
+
stringField(value, 'requestId') &&
|
|
544
|
+
stringField(value, 'messageId') &&
|
|
545
|
+
stringField(value, 'generationId') &&
|
|
546
|
+
stringField(value, 'responseMessageId') &&
|
|
547
|
+
typeof value['attempt'] === 'number' &&
|
|
548
|
+
Number.isInteger(value['attempt']) &&
|
|
549
|
+
value['attempt'] > 0 &&
|
|
550
|
+
stringField(value, 'model'),
|
|
551
|
+
),
|
|
552
|
+
'ai.generation.progress': progressPayloadSchema,
|
|
553
|
+
'ai.generation.completed': completedPayloadSchema,
|
|
554
|
+
'ai.generation.failed': jsonObject(
|
|
555
|
+
'ai.generation.failed',
|
|
556
|
+
(value) =>
|
|
557
|
+
stringField(value, 'requestId') &&
|
|
558
|
+
stringField(value, 'messageId') &&
|
|
559
|
+
stringField(value, 'generationId') &&
|
|
560
|
+
stringField(value, 'responseMessageId') &&
|
|
561
|
+
stringField(value, 'error') &&
|
|
562
|
+
(value['superseded'] === undefined ||
|
|
563
|
+
typeof value['superseded'] === 'boolean') &&
|
|
564
|
+
(value['stepLimit'] === undefined ||
|
|
565
|
+
typeof value['stepLimit'] === 'boolean'),
|
|
566
|
+
),
|
|
567
|
+
'ai.tool.called': jsonObject(
|
|
568
|
+
'ai.tool.called',
|
|
569
|
+
(value) =>
|
|
570
|
+
stringField(value, 'requestId') &&
|
|
571
|
+
stringField(value, 'messageId') &&
|
|
572
|
+
stringField(value, 'generationId') &&
|
|
573
|
+
stringField(value, 'toolCallId') &&
|
|
574
|
+
stringField(value, 'toolName') &&
|
|
575
|
+
(value['dynamic'] === undefined ||
|
|
576
|
+
typeof value['dynamic'] === 'boolean') &&
|
|
577
|
+
(value['providerExecuted'] === undefined ||
|
|
578
|
+
typeof value['providerExecuted'] === 'boolean') &&
|
|
579
|
+
(value['supportsDeferredResults'] === undefined ||
|
|
580
|
+
typeof value['supportsDeferredResults'] === 'boolean') &&
|
|
581
|
+
optionalStringField(value, 'title'),
|
|
582
|
+
),
|
|
583
|
+
'ai.tool.result': jsonObject(
|
|
584
|
+
'ai.tool.result',
|
|
585
|
+
(value) =>
|
|
586
|
+
stringField(value, 'requestId') &&
|
|
587
|
+
stringField(value, 'messageId') &&
|
|
588
|
+
stringField(value, 'generationId') &&
|
|
589
|
+
stringField(value, 'toolCallId') &&
|
|
590
|
+
optionalStringField(value, 'toolName') &&
|
|
591
|
+
optionalStringField(value, 'error') &&
|
|
592
|
+
(value['denied'] === undefined ||
|
|
593
|
+
typeof value['denied'] === 'boolean') &&
|
|
594
|
+
(value['preliminary'] === undefined ||
|
|
595
|
+
typeof value['preliminary'] === 'boolean') &&
|
|
596
|
+
(value['phase'] === undefined ||
|
|
597
|
+
value['phase'] === 'input' ||
|
|
598
|
+
value['phase'] === 'execution') &&
|
|
599
|
+
(value['dynamic'] === undefined ||
|
|
600
|
+
typeof value['dynamic'] === 'boolean') &&
|
|
601
|
+
(value['providerExecuted'] === undefined ||
|
|
602
|
+
typeof value['providerExecuted'] === 'boolean'),
|
|
603
|
+
),
|
|
604
|
+
'ai.approval.requested': jsonObject(
|
|
605
|
+
'ai.approval.requested',
|
|
606
|
+
(value) =>
|
|
607
|
+
stringField(value, 'messageId') &&
|
|
608
|
+
stringField(value, 'generationId') &&
|
|
609
|
+
stringField(value, 'approvalId') &&
|
|
610
|
+
stringField(value, 'toolCallId') &&
|
|
611
|
+
(value['isAutomatic'] === undefined ||
|
|
612
|
+
typeof value['isAutomatic'] === 'boolean') &&
|
|
613
|
+
optionalStringField(value, 'signature'),
|
|
614
|
+
),
|
|
615
|
+
'ai.approval.responded': jsonObject(
|
|
616
|
+
'ai.approval.responded',
|
|
617
|
+
(value) =>
|
|
618
|
+
stringField(value, 'messageId') &&
|
|
619
|
+
stringField(value, 'generationId') &&
|
|
620
|
+
stringField(value, 'approvalId') &&
|
|
621
|
+
typeof value['approved'] === 'boolean' &&
|
|
622
|
+
optionalStringField(value, 'reason'),
|
|
623
|
+
),
|
|
624
|
+
'ai.input.requested': jsonObject(
|
|
625
|
+
'ai.input.requested',
|
|
626
|
+
(value) =>
|
|
627
|
+
stringField(value, 'messageId') &&
|
|
628
|
+
stringField(value, 'generationId') &&
|
|
629
|
+
stringField(value, 'inputId') &&
|
|
630
|
+
stringField(value, 'name'),
|
|
631
|
+
),
|
|
632
|
+
'ai.input.responded': jsonObject(
|
|
633
|
+
'ai.input.responded',
|
|
634
|
+
(value) =>
|
|
635
|
+
stringField(value, 'messageId') &&
|
|
636
|
+
stringField(value, 'generationId') &&
|
|
637
|
+
stringField(value, 'inputId') &&
|
|
638
|
+
stringField(value, 'name'),
|
|
639
|
+
),
|
|
640
|
+
'ai.compaction.requested': jsonObject(
|
|
641
|
+
'ai.compaction.requested',
|
|
642
|
+
(value) =>
|
|
643
|
+
stringField(value, 'generationId') &&
|
|
644
|
+
stringField(value, 'throughMessageId'),
|
|
645
|
+
),
|
|
646
|
+
'ai.compaction.completed': compactionPayloadSchema(messageSchema),
|
|
647
|
+
}
|
|
648
|
+
return definition
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export const events: AIEventDefs<UIMessage> = createEvents()
|
|
652
|
+
|
|
653
|
+
const initialState = <M extends UIMessage>(): AIState<M> => ({
|
|
654
|
+
session: { created: false },
|
|
655
|
+
messages: [],
|
|
656
|
+
status: 'idle',
|
|
657
|
+
activeGeneration: null,
|
|
658
|
+
activeRequestId: null,
|
|
659
|
+
activeResponseMessageId: null,
|
|
660
|
+
activeProjection: null,
|
|
661
|
+
responseGenerationIds: {},
|
|
662
|
+
pendingApprovals: [],
|
|
663
|
+
pendingInputs: [],
|
|
664
|
+
tools: [],
|
|
665
|
+
compaction: null,
|
|
666
|
+
usage: [],
|
|
667
|
+
error: null,
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
const upsertMessage = <M extends UIMessage>(messages: M[], message: M): M[] => {
|
|
671
|
+
const index = messages.findIndex((candidate) => candidate.id === message.id)
|
|
672
|
+
if (index === -1) return [...messages, message]
|
|
673
|
+
const next = [...messages]
|
|
674
|
+
next[index] = message
|
|
675
|
+
return next
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const answerApproval = <M extends UIMessage>(
|
|
679
|
+
messages: M[],
|
|
680
|
+
response: ApprovalRespondedPayload,
|
|
681
|
+
): M[] =>
|
|
682
|
+
messages.map((message) => {
|
|
683
|
+
if (message.id !== response.messageId) return message
|
|
684
|
+
let changed = false
|
|
685
|
+
const parts = message.parts.map((part) => {
|
|
686
|
+
const candidate = part as unknown
|
|
687
|
+
if (
|
|
688
|
+
!isRecord(candidate) ||
|
|
689
|
+
candidate['state'] !== 'approval-requested' ||
|
|
690
|
+
!isRecord(candidate['approval']) ||
|
|
691
|
+
candidate['approval']['id'] !== response.approvalId
|
|
692
|
+
) {
|
|
693
|
+
return part
|
|
694
|
+
}
|
|
695
|
+
changed = true
|
|
696
|
+
return {
|
|
697
|
+
...part,
|
|
698
|
+
state: 'approval-responded',
|
|
699
|
+
approval: {
|
|
700
|
+
...candidate['approval'],
|
|
701
|
+
approved: response.approved,
|
|
702
|
+
...(response.reason === undefined ? {} : { reason: response.reason }),
|
|
703
|
+
},
|
|
704
|
+
} as typeof part
|
|
705
|
+
})
|
|
706
|
+
return changed ? ({ ...message, parts } as M) : message
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
const projectMessageChunks = <M extends UIMessage>(
|
|
710
|
+
messages: M[],
|
|
711
|
+
messageId: string,
|
|
712
|
+
chunks: UIMessageChunk[],
|
|
713
|
+
): M[] =>
|
|
714
|
+
messages.map((message) =>
|
|
715
|
+
message.id === messageId
|
|
716
|
+
? projectUIMessage({ messageId, base: message, chunks })
|
|
717
|
+
: message,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
const approvalRequestedChunk = (
|
|
721
|
+
payload: ApprovalRequestedPayload,
|
|
722
|
+
): UIMessageChunk => ({
|
|
723
|
+
type: 'tool-approval-request',
|
|
724
|
+
approvalId: payload.approvalId,
|
|
725
|
+
toolCallId: payload.toolCallId,
|
|
726
|
+
...(payload.isAutomatic === undefined
|
|
727
|
+
? {}
|
|
728
|
+
: { isAutomatic: payload.isAutomatic }),
|
|
729
|
+
...(payload.signature === undefined ? {} : { signature: payload.signature }),
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
const approvalRespondedChunk = (
|
|
733
|
+
payload: ApprovalRespondedPayload,
|
|
734
|
+
): UIMessageChunk => ({
|
|
735
|
+
type: 'tool-approval-response',
|
|
736
|
+
approvalId: payload.approvalId,
|
|
737
|
+
approved: payload.approved,
|
|
738
|
+
...(payload.reason === undefined ? {} : { reason: payload.reason }),
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
const toolResultChunk = (payload: ToolResultPayload): UIMessageChunk => {
|
|
742
|
+
if (payload.phase === 'input') {
|
|
743
|
+
return {
|
|
744
|
+
type: 'tool-input-error',
|
|
745
|
+
toolCallId: payload.toolCallId,
|
|
746
|
+
toolName: payload.toolName ?? 'unknown',
|
|
747
|
+
input: payload.rawInput ?? payload.input,
|
|
748
|
+
errorText: payload.error ?? 'Invalid tool input',
|
|
749
|
+
...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
|
|
750
|
+
...(payload.providerExecuted === undefined
|
|
751
|
+
? {}
|
|
752
|
+
: { providerExecuted: payload.providerExecuted }),
|
|
753
|
+
...(payload.providerMetadata === undefined
|
|
754
|
+
? {}
|
|
755
|
+
: { providerMetadata: payload.providerMetadata }),
|
|
756
|
+
...(payload.toolMetadata === undefined
|
|
757
|
+
? {}
|
|
758
|
+
: { toolMetadata: payload.toolMetadata }),
|
|
759
|
+
} as UIMessageChunk
|
|
760
|
+
}
|
|
761
|
+
if (payload.denied === true) {
|
|
762
|
+
return {
|
|
763
|
+
type: 'tool-output-denied',
|
|
764
|
+
toolCallId: payload.toolCallId,
|
|
765
|
+
} as UIMessageChunk
|
|
766
|
+
}
|
|
767
|
+
if (payload.error !== undefined) {
|
|
768
|
+
return {
|
|
769
|
+
type: 'tool-output-error',
|
|
770
|
+
toolCallId: payload.toolCallId,
|
|
771
|
+
errorText: payload.error,
|
|
772
|
+
...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
|
|
773
|
+
...(payload.providerExecuted === undefined
|
|
774
|
+
? {}
|
|
775
|
+
: { providerExecuted: payload.providerExecuted }),
|
|
776
|
+
...(payload.providerMetadata === undefined
|
|
777
|
+
? {}
|
|
778
|
+
: { providerMetadata: payload.providerMetadata }),
|
|
779
|
+
...(payload.toolMetadata === undefined
|
|
780
|
+
? {}
|
|
781
|
+
: { toolMetadata: payload.toolMetadata }),
|
|
782
|
+
} as UIMessageChunk
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
type: 'tool-output-available',
|
|
786
|
+
toolCallId: payload.toolCallId,
|
|
787
|
+
output: payload.output,
|
|
788
|
+
...(payload.preliminary === undefined
|
|
789
|
+
? {}
|
|
790
|
+
: { preliminary: payload.preliminary }),
|
|
791
|
+
...(payload.dynamic === undefined ? {} : { dynamic: payload.dynamic }),
|
|
792
|
+
...(payload.providerExecuted === undefined
|
|
793
|
+
? {}
|
|
794
|
+
: { providerExecuted: payload.providerExecuted }),
|
|
795
|
+
...(payload.providerMetadata === undefined
|
|
796
|
+
? {}
|
|
797
|
+
: { providerMetadata: payload.providerMetadata }),
|
|
798
|
+
...(payload.toolMetadata === undefined
|
|
799
|
+
? {}
|
|
800
|
+
: { toolMetadata: payload.toolMetadata }),
|
|
801
|
+
} as UIMessageChunk
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const waitingStatus = <M extends UIMessage>(state: AIState<M>): AgentStatus => {
|
|
805
|
+
if (state.pendingApprovals.length > 0 || state.pendingInputs.length > 0) {
|
|
806
|
+
return 'waiting'
|
|
807
|
+
}
|
|
808
|
+
return state.activeGeneration !== null ||
|
|
809
|
+
state.tools.some((activity) => activity.status === 'running')
|
|
810
|
+
? 'generating'
|
|
811
|
+
: 'idle'
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const lifecycleStatus = <M extends UIMessage>(
|
|
815
|
+
previous: AIState<M>,
|
|
816
|
+
next: AIState<M>,
|
|
817
|
+
): AgentStatus =>
|
|
818
|
+
previous.status === 'failed' ? 'failed' : waitingStatus(next)
|
|
819
|
+
|
|
820
|
+
const isStaleGeneration = <M extends UIMessage>(
|
|
821
|
+
state: AIState<M>,
|
|
822
|
+
messageId: string,
|
|
823
|
+
generationId: string,
|
|
824
|
+
): boolean => {
|
|
825
|
+
const current = state.responseGenerationIds[messageId]
|
|
826
|
+
return current !== undefined && current !== generationId
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const projectedMessage = <M extends UIMessage>(
|
|
830
|
+
projection: ActiveGenerationProjection<M>,
|
|
831
|
+
): M => {
|
|
832
|
+
const projectionEvents: Array<{ index: number; chunks: UIMessageChunk[] }> = [
|
|
833
|
+
...projection.batches,
|
|
834
|
+
]
|
|
835
|
+
for (const event of projection.toolEvents) {
|
|
836
|
+
if (event.type === 'ai.tool.result') {
|
|
837
|
+
projectionEvents.push({
|
|
838
|
+
index: event.index,
|
|
839
|
+
chunks: [toolResultChunk(event.payload)],
|
|
840
|
+
})
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
for (const event of projection.approvalEvents) {
|
|
844
|
+
projectionEvents.push({
|
|
845
|
+
index: event.index,
|
|
846
|
+
chunks: [
|
|
847
|
+
event.type === 'ai.approval.requested'
|
|
848
|
+
? approvalRequestedChunk(event.payload)
|
|
849
|
+
: approvalRespondedChunk(event.payload),
|
|
850
|
+
],
|
|
851
|
+
})
|
|
852
|
+
}
|
|
853
|
+
return projectUIMessage({
|
|
854
|
+
messageId: projection.responseMessageId,
|
|
855
|
+
...(projection.baseMessage === undefined
|
|
856
|
+
? {}
|
|
857
|
+
: { base: projection.baseMessage }),
|
|
858
|
+
chunks: projectionEvents
|
|
859
|
+
.toSorted((left, right) => left.index - right.index)
|
|
860
|
+
.flatMap((event) => event.chunks),
|
|
861
|
+
})
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const reduceToolActivity = (
|
|
865
|
+
tools: ToolActivity[],
|
|
866
|
+
event:
|
|
867
|
+
| { type: 'ai.tool.called'; payload: ToolCalledPayload }
|
|
868
|
+
| { type: 'ai.tool.result'; payload: ToolResultPayload },
|
|
869
|
+
): ToolActivity[] => {
|
|
870
|
+
if (event.type === 'ai.tool.called') {
|
|
871
|
+
const payload = event.payload
|
|
872
|
+
const { providerMetadata, ...called } = payload
|
|
873
|
+
const activity: ToolActivity = {
|
|
874
|
+
...called,
|
|
875
|
+
...(providerMetadata === undefined
|
|
876
|
+
? {}
|
|
877
|
+
: { callProviderMetadata: providerMetadata }),
|
|
878
|
+
status: 'running',
|
|
879
|
+
}
|
|
880
|
+
return [
|
|
881
|
+
...tools.filter(
|
|
882
|
+
(candidate) =>
|
|
883
|
+
candidate.generationId !== payload.generationId ||
|
|
884
|
+
candidate.toolCallId !== payload.toolCallId,
|
|
885
|
+
),
|
|
886
|
+
activity,
|
|
887
|
+
]
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const payload = event.payload
|
|
891
|
+
const existing = tools.find(
|
|
892
|
+
(candidate) =>
|
|
893
|
+
candidate.generationId === payload.generationId &&
|
|
894
|
+
candidate.toolCallId === payload.toolCallId,
|
|
895
|
+
)
|
|
896
|
+
const toolName = payload.toolName ?? existing?.toolName
|
|
897
|
+
const input = payload.input === undefined ? existing?.input : payload.input
|
|
898
|
+
const dynamic = payload.dynamic ?? existing?.dynamic
|
|
899
|
+
const providerExecuted =
|
|
900
|
+
payload.providerExecuted ?? existing?.providerExecuted
|
|
901
|
+
const toolMetadata = payload.toolMetadata ?? existing?.toolMetadata
|
|
902
|
+
const resultProviderMetadata =
|
|
903
|
+
payload.providerMetadata ?? existing?.resultProviderMetadata
|
|
904
|
+
const activity: ToolActivity = {
|
|
905
|
+
requestId: payload.requestId,
|
|
906
|
+
messageId: payload.messageId,
|
|
907
|
+
generationId: payload.generationId,
|
|
908
|
+
toolCallId: payload.toolCallId,
|
|
909
|
+
...(toolName === undefined ? {} : { toolName }),
|
|
910
|
+
...(input === undefined ? {} : { input }),
|
|
911
|
+
...(payload.rawInput === undefined ? {} : { rawInput: payload.rawInput }),
|
|
912
|
+
...(payload.output === undefined ? {} : { output: payload.output }),
|
|
913
|
+
...(payload.error === undefined ? {} : { error: payload.error }),
|
|
914
|
+
...(payload.preliminary === undefined
|
|
915
|
+
? {}
|
|
916
|
+
: { preliminary: payload.preliminary }),
|
|
917
|
+
...(payload.phase === undefined ? {} : { phase: payload.phase }),
|
|
918
|
+
...(dynamic === undefined ? {} : { dynamic }),
|
|
919
|
+
...(providerExecuted === undefined ? {} : { providerExecuted }),
|
|
920
|
+
...(existing?.callProviderMetadata === undefined
|
|
921
|
+
? {}
|
|
922
|
+
: { callProviderMetadata: existing.callProviderMetadata }),
|
|
923
|
+
...(resultProviderMetadata === undefined ? {} : { resultProviderMetadata }),
|
|
924
|
+
...(toolMetadata === undefined ? {} : { toolMetadata }),
|
|
925
|
+
...(existing?.title === undefined ? {} : { title: existing.title }),
|
|
926
|
+
status: payload.denied
|
|
927
|
+
? 'denied'
|
|
928
|
+
: payload.error
|
|
929
|
+
? 'failed'
|
|
930
|
+
: payload.preliminary
|
|
931
|
+
? 'running'
|
|
932
|
+
: 'completed',
|
|
933
|
+
}
|
|
934
|
+
return [
|
|
935
|
+
...tools.filter(
|
|
936
|
+
(candidate) =>
|
|
937
|
+
candidate.generationId !== payload.generationId ||
|
|
938
|
+
candidate.toolCallId !== payload.toolCallId,
|
|
939
|
+
),
|
|
940
|
+
activity,
|
|
941
|
+
]
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const projectedTools = <M extends UIMessage>(
|
|
945
|
+
projection: ActiveGenerationProjection<M>,
|
|
946
|
+
): ToolActivity[] =>
|
|
947
|
+
projection.toolEvents.reduce(
|
|
948
|
+
(tools, event) => reduceToolActivity(tools, event),
|
|
949
|
+
projection.baseTools,
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
const restoreGenerationBase = <M extends UIMessage>(
|
|
953
|
+
state: AIState<M>,
|
|
954
|
+
generationId: string,
|
|
955
|
+
responseMessageId: string,
|
|
956
|
+
reason: 'interrupted' | 'superseded',
|
|
957
|
+
): AIState<M> => {
|
|
958
|
+
const projection =
|
|
959
|
+
state.activeProjection?.generationId === generationId
|
|
960
|
+
? state.activeProjection
|
|
961
|
+
: null
|
|
962
|
+
return {
|
|
963
|
+
...state,
|
|
964
|
+
messages:
|
|
965
|
+
projection?.baseMessage === undefined
|
|
966
|
+
? state.messages.filter((message) => message.id !== responseMessageId)
|
|
967
|
+
: upsertMessage(state.messages, projection.baseMessage),
|
|
968
|
+
tools:
|
|
969
|
+
projection === null
|
|
970
|
+
? state.tools.filter(
|
|
971
|
+
(activity) => activity.generationId !== generationId,
|
|
972
|
+
)
|
|
973
|
+
: projection.baseTools,
|
|
974
|
+
pendingApprovals: state.pendingApprovals.filter(
|
|
975
|
+
(approval) => approval.generationId !== generationId,
|
|
976
|
+
),
|
|
977
|
+
pendingInputs: state.pendingInputs.filter(
|
|
978
|
+
(input) => input.generationId !== generationId,
|
|
979
|
+
),
|
|
980
|
+
activeGeneration: null,
|
|
981
|
+
activeProjection: null,
|
|
982
|
+
terminalGenerations: {
|
|
983
|
+
...state.terminalGenerations,
|
|
984
|
+
[generationId]: reason,
|
|
985
|
+
},
|
|
986
|
+
compaction:
|
|
987
|
+
projection === null
|
|
988
|
+
? state.compaction?.generationId === generationId
|
|
989
|
+
? null
|
|
990
|
+
: state.compaction
|
|
991
|
+
: (projection.baseCompaction ?? null),
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const stopGenerationTools = (
|
|
996
|
+
tools: ToolActivity[],
|
|
997
|
+
generationId: string | undefined,
|
|
998
|
+
responseMessageId: string,
|
|
999
|
+
error: string,
|
|
1000
|
+
): ToolActivity[] =>
|
|
1001
|
+
tools.map((activity) =>
|
|
1002
|
+
activity.status === 'running' &&
|
|
1003
|
+
(generationId === undefined
|
|
1004
|
+
? activity.messageId === responseMessageId
|
|
1005
|
+
: activity.generationId === generationId)
|
|
1006
|
+
? { ...activity, status: 'failed', error, preliminary: false }
|
|
1007
|
+
: activity,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
const interruptGenerationProjection = <M extends UIMessage>(
|
|
1011
|
+
state: AIState<M>,
|
|
1012
|
+
generationId: string,
|
|
1013
|
+
responseMessageId: string,
|
|
1014
|
+
cutoff: number,
|
|
1015
|
+
): AIState<M> | null => {
|
|
1016
|
+
const projection =
|
|
1017
|
+
state.activeProjection?.generationId === generationId
|
|
1018
|
+
? state.activeProjection
|
|
1019
|
+
: null
|
|
1020
|
+
if (projection === null) return null
|
|
1021
|
+
const retained: ActiveGenerationProjection<M> = {
|
|
1022
|
+
...projection,
|
|
1023
|
+
batches: projection.batches.filter((batch) => batch.index <= cutoff),
|
|
1024
|
+
toolEvents: projection.toolEvents.filter(
|
|
1025
|
+
(toolEvent) => toolEvent.index <= cutoff,
|
|
1026
|
+
),
|
|
1027
|
+
approvalEvents: projection.approvalEvents.filter(
|
|
1028
|
+
(approvalEvent) => approvalEvent.index <= cutoff,
|
|
1029
|
+
),
|
|
1030
|
+
}
|
|
1031
|
+
const shouldProject =
|
|
1032
|
+
retained.baseMessage !== undefined || retained.batches.length > 0
|
|
1033
|
+
const restored = restoreGenerationBase(
|
|
1034
|
+
state,
|
|
1035
|
+
generationId,
|
|
1036
|
+
responseMessageId,
|
|
1037
|
+
'interrupted',
|
|
1038
|
+
)
|
|
1039
|
+
return {
|
|
1040
|
+
...restored,
|
|
1041
|
+
messages: shouldProject
|
|
1042
|
+
? upsertMessage(
|
|
1043
|
+
restored.messages,
|
|
1044
|
+
interruptUIMessage(projectedMessage(retained)),
|
|
1045
|
+
)
|
|
1046
|
+
: restored.messages,
|
|
1047
|
+
tools: stopGenerationTools(
|
|
1048
|
+
projectedTools(retained),
|
|
1049
|
+
generationId,
|
|
1050
|
+
responseMessageId,
|
|
1051
|
+
'Tool execution was interrupted.',
|
|
1052
|
+
),
|
|
1053
|
+
usage:
|
|
1054
|
+
projection.completionIndex === undefined ||
|
|
1055
|
+
projection.completionIndex <= cutoff
|
|
1056
|
+
? restored.usage
|
|
1057
|
+
: restored.usage.filter((entry) => entry.generationId !== generationId),
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const generationRequestId = (generationId: string): string | undefined => {
|
|
1062
|
+
const marker = ':generation:'
|
|
1063
|
+
const markerIndex = generationId.lastIndexOf(marker)
|
|
1064
|
+
return markerIndex === -1 ? undefined : generationId.slice(0, markerIndex)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const terminalGenerationReason = <M extends UIMessage>(
|
|
1068
|
+
state: AIState<M>,
|
|
1069
|
+
generationId: string,
|
|
1070
|
+
): 'completed' | 'failed' | 'interrupted' | 'superseded' | undefined => {
|
|
1071
|
+
const terminal = state.terminalGenerations?.[generationId]
|
|
1072
|
+
if (terminal !== undefined) return terminal
|
|
1073
|
+
const requestId = generationRequestId(generationId)
|
|
1074
|
+
return requestId !== undefined &&
|
|
1075
|
+
state.terminalRequestIds?.[requestId] === true
|
|
1076
|
+
? 'interrupted'
|
|
1077
|
+
: undefined
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Pure projection of the built-in AI protocol. Unknown extension events are ignored. */
|
|
1081
|
+
export function reduceAIState<M extends UIMessage>(
|
|
1082
|
+
state: AIState<M>,
|
|
1083
|
+
event: { type: string; payload: unknown; index: number; id?: string },
|
|
1084
|
+
): AIState<M> {
|
|
1085
|
+
if (state.status === 'closed') return state
|
|
1086
|
+
switch (event.type) {
|
|
1087
|
+
case 'ai.session.created': {
|
|
1088
|
+
const payload = event.payload as SessionCreatedPayload
|
|
1089
|
+
return {
|
|
1090
|
+
...state,
|
|
1091
|
+
session: {
|
|
1092
|
+
created: true,
|
|
1093
|
+
...(payload.metadata === undefined
|
|
1094
|
+
? state.session.metadata === undefined
|
|
1095
|
+
? {}
|
|
1096
|
+
: { metadata: state.session.metadata }
|
|
1097
|
+
: { metadata: payload.metadata }),
|
|
1098
|
+
},
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
case 'ai.session.closed':
|
|
1102
|
+
return {
|
|
1103
|
+
...state,
|
|
1104
|
+
status: 'closed',
|
|
1105
|
+
activeGeneration: null,
|
|
1106
|
+
activeRequestId: null,
|
|
1107
|
+
activeResponseMessageId: null,
|
|
1108
|
+
activeProjection: null,
|
|
1109
|
+
}
|
|
1110
|
+
case 'ai.message.created': {
|
|
1111
|
+
const { message } = event.payload as MessageCreatedPayload<M>
|
|
1112
|
+
return { ...state, messages: upsertMessage(state.messages, message) }
|
|
1113
|
+
}
|
|
1114
|
+
case 'ai.message.completed': {
|
|
1115
|
+
const { messageId } = event.payload as MessageCompletedPayload
|
|
1116
|
+
const generationId = state.responseGenerationIds[messageId]
|
|
1117
|
+
if (
|
|
1118
|
+
generationId === undefined ||
|
|
1119
|
+
terminalGenerationReason(state, generationId) !== undefined
|
|
1120
|
+
) {
|
|
1121
|
+
return state
|
|
1122
|
+
}
|
|
1123
|
+
const requestId = generationRequestId(generationId)
|
|
1124
|
+
return {
|
|
1125
|
+
...state,
|
|
1126
|
+
activeRequestId: null,
|
|
1127
|
+
activeResponseMessageId: null,
|
|
1128
|
+
activeProjection:
|
|
1129
|
+
state.activeProjection?.generationId === generationId
|
|
1130
|
+
? null
|
|
1131
|
+
: state.activeProjection,
|
|
1132
|
+
terminalGenerations: {
|
|
1133
|
+
...state.terminalGenerations,
|
|
1134
|
+
[generationId]: 'completed',
|
|
1135
|
+
},
|
|
1136
|
+
...(requestId === undefined
|
|
1137
|
+
? {}
|
|
1138
|
+
: {
|
|
1139
|
+
terminalRequestIds: {
|
|
1140
|
+
...state.terminalRequestIds,
|
|
1141
|
+
[requestId]: true,
|
|
1142
|
+
},
|
|
1143
|
+
}),
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
case 'ai.generation.requested': {
|
|
1147
|
+
const payload = event.payload as GenerationRequestedPayload
|
|
1148
|
+
const schedulesToolContinuation = payload.reason === 'tool'
|
|
1149
|
+
const responseMessageId =
|
|
1150
|
+
payload.responseMessageId ??
|
|
1151
|
+
(payload.reason === 'message'
|
|
1152
|
+
? `${payload.messageId}:assistant`
|
|
1153
|
+
: payload.messageId)
|
|
1154
|
+
const ownerGenerationId =
|
|
1155
|
+
state.responseGenerationIds[responseMessageId] ??
|
|
1156
|
+
state.activeGeneration?.generationId
|
|
1157
|
+
const ownerRequestId =
|
|
1158
|
+
ownerGenerationId === undefined
|
|
1159
|
+
? undefined
|
|
1160
|
+
: generationRequestId(ownerGenerationId)
|
|
1161
|
+
const requestId = (event as { id?: string }).id
|
|
1162
|
+
if (
|
|
1163
|
+
requestId !== undefined &&
|
|
1164
|
+
state.terminalRequestIds?.[requestId] === true
|
|
1165
|
+
) {
|
|
1166
|
+
return state
|
|
1167
|
+
}
|
|
1168
|
+
const acceptsRequest =
|
|
1169
|
+
payload.reason === 'message' ||
|
|
1170
|
+
(payload.reason === 'tool'
|
|
1171
|
+
? ownerGenerationId !== undefined &&
|
|
1172
|
+
requestId === `ai.generate:tools:${ownerGenerationId}` &&
|
|
1173
|
+
state.activeRequestId === ownerRequestId &&
|
|
1174
|
+
state.status !== 'failed'
|
|
1175
|
+
: payload.reason === 'retry'
|
|
1176
|
+
? state.status === 'failed'
|
|
1177
|
+
: ownerGenerationId !== undefined &&
|
|
1178
|
+
requestId?.startsWith(
|
|
1179
|
+
`ai.generate:input:${encodeURIComponent(responseMessageId)}:${encodeURIComponent(ownerGenerationId)}:`,
|
|
1180
|
+
) === true &&
|
|
1181
|
+
state.activeRequestId === ownerRequestId &&
|
|
1182
|
+
state.status !== 'failed')
|
|
1183
|
+
if (!acceptsRequest) return state
|
|
1184
|
+
return {
|
|
1185
|
+
...state,
|
|
1186
|
+
messages:
|
|
1187
|
+
payload.reason === 'retry' && payload.responseMessageId !== undefined
|
|
1188
|
+
? state.messages.filter(
|
|
1189
|
+
(message) => message.id !== payload.responseMessageId,
|
|
1190
|
+
)
|
|
1191
|
+
: state.messages,
|
|
1192
|
+
activeProjection:
|
|
1193
|
+
payload.reason === 'retry' ? null : state.activeProjection,
|
|
1194
|
+
status: schedulesToolContinuation ? state.status : 'generating',
|
|
1195
|
+
activeRequestId: requestId ?? null,
|
|
1196
|
+
activeResponseMessageId: responseMessageId,
|
|
1197
|
+
error: schedulesToolContinuation ? state.error : null,
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
case 'ai.generation.started': {
|
|
1201
|
+
const payload = event.payload as GenerationStartedPayload
|
|
1202
|
+
if (
|
|
1203
|
+
state.terminalRequestIds?.[payload.requestId] === true ||
|
|
1204
|
+
terminalGenerationReason(state, payload.generationId) !== undefined
|
|
1205
|
+
) {
|
|
1206
|
+
return state
|
|
1207
|
+
}
|
|
1208
|
+
if (
|
|
1209
|
+
state.activeRequestId !== null &&
|
|
1210
|
+
state.activeRequestId !== payload.requestId
|
|
1211
|
+
) {
|
|
1212
|
+
return state
|
|
1213
|
+
}
|
|
1214
|
+
const currentOwner =
|
|
1215
|
+
state.responseGenerationIds[payload.responseMessageId]
|
|
1216
|
+
const currentTerminal =
|
|
1217
|
+
currentOwner === undefined
|
|
1218
|
+
? undefined
|
|
1219
|
+
: terminalGenerationReason(state, currentOwner)
|
|
1220
|
+
if (
|
|
1221
|
+
currentTerminal === 'completed' ||
|
|
1222
|
+
currentTerminal === 'interrupted'
|
|
1223
|
+
) {
|
|
1224
|
+
return state
|
|
1225
|
+
}
|
|
1226
|
+
const marker = ':generation:'
|
|
1227
|
+
const currentAttempt = currentOwner?.startsWith(
|
|
1228
|
+
`${payload.requestId}${marker}`,
|
|
1229
|
+
)
|
|
1230
|
+
? Number(currentOwner.slice(`${payload.requestId}${marker}`.length))
|
|
1231
|
+
: undefined
|
|
1232
|
+
if (
|
|
1233
|
+
currentAttempt !== undefined &&
|
|
1234
|
+
Number.isFinite(currentAttempt) &&
|
|
1235
|
+
currentAttempt >= payload.attempt
|
|
1236
|
+
) {
|
|
1237
|
+
return state
|
|
1238
|
+
}
|
|
1239
|
+
const activeGeneration = state.activeGeneration
|
|
1240
|
+
const replacesActiveAttempt =
|
|
1241
|
+
activeGeneration !== null &&
|
|
1242
|
+
activeGeneration.requestId === payload.requestId &&
|
|
1243
|
+
activeGeneration.responseMessageId === payload.responseMessageId &&
|
|
1244
|
+
activeGeneration.attempt < payload.attempt &&
|
|
1245
|
+
state.activeProjection?.generationId === activeGeneration.generationId
|
|
1246
|
+
const baseState = replacesActiveAttempt
|
|
1247
|
+
? restoreGenerationBase(
|
|
1248
|
+
state,
|
|
1249
|
+
activeGeneration.generationId,
|
|
1250
|
+
activeGeneration.responseMessageId,
|
|
1251
|
+
'superseded',
|
|
1252
|
+
)
|
|
1253
|
+
: state
|
|
1254
|
+
const baseMessage = baseState.messages.find(
|
|
1255
|
+
(message) => message.id === payload.responseMessageId,
|
|
1256
|
+
)
|
|
1257
|
+
return {
|
|
1258
|
+
...baseState,
|
|
1259
|
+
status: 'generating',
|
|
1260
|
+
activeGeneration: payload,
|
|
1261
|
+
activeResponseMessageId: null,
|
|
1262
|
+
responseGenerationIds: {
|
|
1263
|
+
...baseState.responseGenerationIds,
|
|
1264
|
+
[payload.responseMessageId]: payload.generationId,
|
|
1265
|
+
},
|
|
1266
|
+
activeProjection: {
|
|
1267
|
+
generationId: payload.generationId,
|
|
1268
|
+
responseMessageId: payload.responseMessageId,
|
|
1269
|
+
...(baseMessage === undefined ? {} : { baseMessage }),
|
|
1270
|
+
baseCompaction: baseState.compaction,
|
|
1271
|
+
batches: [],
|
|
1272
|
+
baseTools: baseState.tools,
|
|
1273
|
+
toolEvents: [],
|
|
1274
|
+
approvalEvents: [],
|
|
1275
|
+
},
|
|
1276
|
+
error: null,
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
case 'ai.generation.progress': {
|
|
1280
|
+
const payload = event.payload as GenerationProgressPayload
|
|
1281
|
+
if (terminalGenerationReason(state, payload.generationId) !== undefined) {
|
|
1282
|
+
return state
|
|
1283
|
+
}
|
|
1284
|
+
const activeProjection = state.activeProjection
|
|
1285
|
+
if (
|
|
1286
|
+
state.activeGeneration?.generationId !== payload.generationId ||
|
|
1287
|
+
activeProjection?.generationId !== payload.generationId
|
|
1288
|
+
) {
|
|
1289
|
+
return state
|
|
1290
|
+
}
|
|
1291
|
+
const projection: ActiveGenerationProjection<M> = {
|
|
1292
|
+
...activeProjection,
|
|
1293
|
+
batches: [
|
|
1294
|
+
...activeProjection.batches.filter(
|
|
1295
|
+
(batch) => batch.index !== event.index,
|
|
1296
|
+
),
|
|
1297
|
+
{ index: event.index, chunks: payload.chunks },
|
|
1298
|
+
].toSorted((left, right) => left.index - right.index),
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
...state,
|
|
1302
|
+
messages: upsertMessage(state.messages, projectedMessage(projection)),
|
|
1303
|
+
activeProjection: projection,
|
|
1304
|
+
status: 'generating',
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
case 'ai.generation.completed': {
|
|
1308
|
+
const payload = event.payload as GenerationCompletedPayload
|
|
1309
|
+
if (terminalGenerationReason(state, payload.generationId) !== undefined) {
|
|
1310
|
+
return state
|
|
1311
|
+
}
|
|
1312
|
+
if (state.activeGeneration?.generationId !== payload.generationId) {
|
|
1313
|
+
return state
|
|
1314
|
+
}
|
|
1315
|
+
const usage = payload.usage
|
|
1316
|
+
? [
|
|
1317
|
+
...state.usage.filter(
|
|
1318
|
+
(entry) => entry.generationId !== payload.generationId,
|
|
1319
|
+
),
|
|
1320
|
+
{ generationId: payload.generationId, usage: payload.usage },
|
|
1321
|
+
]
|
|
1322
|
+
: state.usage
|
|
1323
|
+
const next = {
|
|
1324
|
+
...state,
|
|
1325
|
+
activeGeneration: null,
|
|
1326
|
+
activeProjection:
|
|
1327
|
+
state.activeProjection?.generationId === payload.generationId
|
|
1328
|
+
? { ...state.activeProjection, completionIndex: event.index }
|
|
1329
|
+
: state.activeProjection,
|
|
1330
|
+
usage,
|
|
1331
|
+
}
|
|
1332
|
+
return { ...next, status: waitingStatus(next) }
|
|
1333
|
+
}
|
|
1334
|
+
case 'ai.generation.failed': {
|
|
1335
|
+
const payload = event.payload as GenerationFailedPayload
|
|
1336
|
+
if (terminalGenerationReason(state, payload.generationId) !== undefined) {
|
|
1337
|
+
return state
|
|
1338
|
+
}
|
|
1339
|
+
if (
|
|
1340
|
+
state.activeGeneration?.generationId !== payload.generationId &&
|
|
1341
|
+
!(
|
|
1342
|
+
payload.stepLimit === true &&
|
|
1343
|
+
state.responseGenerationIds[payload.responseMessageId] ===
|
|
1344
|
+
payload.generationId &&
|
|
1345
|
+
state.activeRequestId ===
|
|
1346
|
+
`ai.generate:tools:${payload.generationId}` &&
|
|
1347
|
+
state.activeResponseMessageId === payload.responseMessageId
|
|
1348
|
+
)
|
|
1349
|
+
) {
|
|
1350
|
+
return state
|
|
1351
|
+
}
|
|
1352
|
+
if (payload.superseded === true) {
|
|
1353
|
+
return restoreGenerationBase(
|
|
1354
|
+
state,
|
|
1355
|
+
payload.generationId,
|
|
1356
|
+
payload.responseMessageId,
|
|
1357
|
+
'superseded',
|
|
1358
|
+
)
|
|
1359
|
+
}
|
|
1360
|
+
const projection =
|
|
1361
|
+
state.activeProjection?.generationId === payload.generationId
|
|
1362
|
+
? state.activeProjection
|
|
1363
|
+
: null
|
|
1364
|
+
const shouldProject =
|
|
1365
|
+
projection !== null &&
|
|
1366
|
+
(projection.baseMessage !== undefined || projection.batches.length > 0)
|
|
1367
|
+
return {
|
|
1368
|
+
...state,
|
|
1369
|
+
messages: shouldProject
|
|
1370
|
+
? upsertMessage(
|
|
1371
|
+
state.messages,
|
|
1372
|
+
interruptUIMessage(
|
|
1373
|
+
projectedMessage(projection),
|
|
1374
|
+
`Generation failed before tool completion: ${payload.error}`,
|
|
1375
|
+
),
|
|
1376
|
+
)
|
|
1377
|
+
: state.messages,
|
|
1378
|
+
status: 'failed',
|
|
1379
|
+
activeGeneration: null,
|
|
1380
|
+
activeResponseMessageId: null,
|
|
1381
|
+
activeProjection: projection,
|
|
1382
|
+
terminalGenerations: {
|
|
1383
|
+
...state.terminalGenerations,
|
|
1384
|
+
[payload.generationId]: 'failed',
|
|
1385
|
+
},
|
|
1386
|
+
terminalRequestIds: {
|
|
1387
|
+
...state.terminalRequestIds,
|
|
1388
|
+
[payload.requestId]: true,
|
|
1389
|
+
...(state.activeRequestId === null
|
|
1390
|
+
? {}
|
|
1391
|
+
: { [state.activeRequestId]: true as const }),
|
|
1392
|
+
},
|
|
1393
|
+
compaction:
|
|
1394
|
+
projection?.generationId === payload.generationId
|
|
1395
|
+
? (projection.baseCompaction ?? null)
|
|
1396
|
+
: state.compaction?.generationId === payload.generationId
|
|
1397
|
+
? null
|
|
1398
|
+
: state.compaction,
|
|
1399
|
+
pendingApprovals: state.pendingApprovals.filter(
|
|
1400
|
+
(approval) => approval.generationId !== payload.generationId,
|
|
1401
|
+
),
|
|
1402
|
+
pendingInputs: state.pendingInputs.filter(
|
|
1403
|
+
(input) => input.generationId !== payload.generationId,
|
|
1404
|
+
),
|
|
1405
|
+
tools: stopGenerationTools(
|
|
1406
|
+
state.tools,
|
|
1407
|
+
payload.generationId,
|
|
1408
|
+
payload.responseMessageId,
|
|
1409
|
+
payload.error,
|
|
1410
|
+
),
|
|
1411
|
+
error: payload.error,
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
case 'ai.message.interrupted': {
|
|
1415
|
+
const payload = event.payload as MessageInterruptedPayload
|
|
1416
|
+
const currentGenerationId = state.responseGenerationIds[payload.messageId]
|
|
1417
|
+
const targetRequestId =
|
|
1418
|
+
payload.requestId ?? generationRequestId(payload.generationId)
|
|
1419
|
+
const ownsRequestPhase =
|
|
1420
|
+
payload.requestId !== undefined &&
|
|
1421
|
+
state.activeGeneration === null &&
|
|
1422
|
+
state.activeRequestId === payload.requestId &&
|
|
1423
|
+
state.activeResponseMessageId === payload.messageId
|
|
1424
|
+
if (
|
|
1425
|
+
currentGenerationId !== undefined &&
|
|
1426
|
+
!ownsRequestPhase &&
|
|
1427
|
+
(payload.generationId !== undefined
|
|
1428
|
+
? currentGenerationId !== payload.generationId
|
|
1429
|
+
: generationRequestId(currentGenerationId) !== payload.requestId)
|
|
1430
|
+
) {
|
|
1431
|
+
return state
|
|
1432
|
+
}
|
|
1433
|
+
if (
|
|
1434
|
+
state.activeGeneration === null &&
|
|
1435
|
+
state.activeRequestId !== null &&
|
|
1436
|
+
state.activeResponseMessageId === payload.messageId &&
|
|
1437
|
+
targetRequestId !== state.activeRequestId
|
|
1438
|
+
) {
|
|
1439
|
+
return state
|
|
1440
|
+
}
|
|
1441
|
+
const currentTerminal =
|
|
1442
|
+
currentGenerationId === undefined
|
|
1443
|
+
? undefined
|
|
1444
|
+
: terminalGenerationReason(state, currentGenerationId)
|
|
1445
|
+
if (
|
|
1446
|
+
currentTerminal === 'completed' ||
|
|
1447
|
+
currentTerminal === 'interrupted' ||
|
|
1448
|
+
currentTerminal === 'superseded'
|
|
1449
|
+
) {
|
|
1450
|
+
return state
|
|
1451
|
+
}
|
|
1452
|
+
const active = state.activeGeneration
|
|
1453
|
+
if (active === null) {
|
|
1454
|
+
if (
|
|
1455
|
+
payload.requestId !== undefined &&
|
|
1456
|
+
state.activeRequestId !== null &&
|
|
1457
|
+
state.activeRequestId === payload.requestId &&
|
|
1458
|
+
state.activeResponseMessageId === payload.messageId
|
|
1459
|
+
) {
|
|
1460
|
+
const projectedState =
|
|
1461
|
+
currentGenerationId === undefined
|
|
1462
|
+
? null
|
|
1463
|
+
: interruptGenerationProjection(
|
|
1464
|
+
state,
|
|
1465
|
+
currentGenerationId,
|
|
1466
|
+
payload.messageId,
|
|
1467
|
+
payload.lastSeenIndex ?? Number.POSITIVE_INFINITY,
|
|
1468
|
+
)
|
|
1469
|
+
const baseState = projectedState ?? state
|
|
1470
|
+
return {
|
|
1471
|
+
...baseState,
|
|
1472
|
+
status: 'idle',
|
|
1473
|
+
activeRequestId: null,
|
|
1474
|
+
activeResponseMessageId: null,
|
|
1475
|
+
activeProjection: null,
|
|
1476
|
+
terminalRequestIds: {
|
|
1477
|
+
...baseState.terminalRequestIds,
|
|
1478
|
+
[payload.requestId]: true,
|
|
1479
|
+
},
|
|
1480
|
+
...(currentGenerationId === undefined
|
|
1481
|
+
? {}
|
|
1482
|
+
: {
|
|
1483
|
+
terminalGenerations: {
|
|
1484
|
+
...baseState.terminalGenerations,
|
|
1485
|
+
[currentGenerationId]: 'interrupted' as const,
|
|
1486
|
+
},
|
|
1487
|
+
}),
|
|
1488
|
+
error: null,
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const targetGenerationId =
|
|
1492
|
+
payload.generationId ??
|
|
1493
|
+
(currentGenerationId !== undefined &&
|
|
1494
|
+
generationRequestId(currentGenerationId) === payload.requestId
|
|
1495
|
+
? currentGenerationId
|
|
1496
|
+
: undefined)
|
|
1497
|
+
if (targetGenerationId === undefined) return state
|
|
1498
|
+
if (currentGenerationId !== targetGenerationId) return state
|
|
1499
|
+
const matchesGeneration = (generationId: string): boolean =>
|
|
1500
|
+
targetGenerationId === generationId
|
|
1501
|
+
const hasMessageWork =
|
|
1502
|
+
state.pendingApprovals.some(
|
|
1503
|
+
(approval) =>
|
|
1504
|
+
approval.messageId === payload.messageId &&
|
|
1505
|
+
matchesGeneration(approval.generationId),
|
|
1506
|
+
) ||
|
|
1507
|
+
state.pendingInputs.some(
|
|
1508
|
+
(input) =>
|
|
1509
|
+
input.messageId === payload.messageId &&
|
|
1510
|
+
matchesGeneration(input.generationId),
|
|
1511
|
+
) ||
|
|
1512
|
+
state.tools.some(
|
|
1513
|
+
(activity) =>
|
|
1514
|
+
activity.messageId === payload.messageId &&
|
|
1515
|
+
activity.status === 'running' &&
|
|
1516
|
+
matchesGeneration(activity.generationId),
|
|
1517
|
+
)
|
|
1518
|
+
const resolvesFailedGeneration =
|
|
1519
|
+
state.status === 'failed' &&
|
|
1520
|
+
currentGenerationId === targetGenerationId &&
|
|
1521
|
+
(state.activeRequestId === null ||
|
|
1522
|
+
state.activeRequestId === targetRequestId ||
|
|
1523
|
+
state.activeRequestId === `ai.generate:tools:${targetGenerationId}`)
|
|
1524
|
+
const resolvesCurrentGeneration =
|
|
1525
|
+
currentGenerationId === targetGenerationId &&
|
|
1526
|
+
currentTerminal === undefined
|
|
1527
|
+
if (
|
|
1528
|
+
!hasMessageWork &&
|
|
1529
|
+
!resolvesFailedGeneration &&
|
|
1530
|
+
!resolvesCurrentGeneration
|
|
1531
|
+
) {
|
|
1532
|
+
return state
|
|
1533
|
+
}
|
|
1534
|
+
const pendingRequestId = state.activeRequestId
|
|
1535
|
+
const projectedState = interruptGenerationProjection(
|
|
1536
|
+
state,
|
|
1537
|
+
targetGenerationId,
|
|
1538
|
+
payload.messageId,
|
|
1539
|
+
payload.lastSeenIndex ?? Number.POSITIVE_INFINITY,
|
|
1540
|
+
)
|
|
1541
|
+
const baseState = projectedState ?? state
|
|
1542
|
+
const next = {
|
|
1543
|
+
...baseState,
|
|
1544
|
+
activeRequestId: null,
|
|
1545
|
+
activeResponseMessageId: null,
|
|
1546
|
+
activeProjection: null,
|
|
1547
|
+
messages:
|
|
1548
|
+
projectedState === null
|
|
1549
|
+
? state.messages.map((message) =>
|
|
1550
|
+
message.id === payload.messageId
|
|
1551
|
+
? interruptUIMessage(message)
|
|
1552
|
+
: message,
|
|
1553
|
+
)
|
|
1554
|
+
: baseState.messages,
|
|
1555
|
+
pendingApprovals: baseState.pendingApprovals.filter(
|
|
1556
|
+
(approval) =>
|
|
1557
|
+
approval.messageId !== payload.messageId ||
|
|
1558
|
+
!matchesGeneration(approval.generationId),
|
|
1559
|
+
),
|
|
1560
|
+
pendingInputs: baseState.pendingInputs.filter(
|
|
1561
|
+
(input) =>
|
|
1562
|
+
input.messageId !== payload.messageId ||
|
|
1563
|
+
!matchesGeneration(input.generationId),
|
|
1564
|
+
),
|
|
1565
|
+
tools:
|
|
1566
|
+
projectedState === null
|
|
1567
|
+
? stopGenerationTools(
|
|
1568
|
+
state.tools,
|
|
1569
|
+
targetGenerationId,
|
|
1570
|
+
payload.messageId,
|
|
1571
|
+
'Tool execution was interrupted.',
|
|
1572
|
+
)
|
|
1573
|
+
: baseState.tools,
|
|
1574
|
+
terminalGenerations: {
|
|
1575
|
+
...baseState.terminalGenerations,
|
|
1576
|
+
[targetGenerationId]: 'interrupted' as const,
|
|
1577
|
+
},
|
|
1578
|
+
...(pendingRequestId === null
|
|
1579
|
+
? {}
|
|
1580
|
+
: {
|
|
1581
|
+
terminalRequestIds: {
|
|
1582
|
+
...baseState.terminalRequestIds,
|
|
1583
|
+
[pendingRequestId]: true as const,
|
|
1584
|
+
},
|
|
1585
|
+
}),
|
|
1586
|
+
error: resolvesFailedGeneration ? null : state.error,
|
|
1587
|
+
}
|
|
1588
|
+
return {
|
|
1589
|
+
...next,
|
|
1590
|
+
status: waitingStatus(next),
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (
|
|
1594
|
+
payload.messageId !== active.responseMessageId ||
|
|
1595
|
+
(payload.generationId !== active.generationId &&
|
|
1596
|
+
payload.requestId !== active.requestId)
|
|
1597
|
+
) {
|
|
1598
|
+
return state
|
|
1599
|
+
}
|
|
1600
|
+
const cutoff = payload.lastSeenIndex ?? Number.POSITIVE_INFINITY
|
|
1601
|
+
const projectedState = interruptGenerationProjection(
|
|
1602
|
+
state,
|
|
1603
|
+
active.generationId,
|
|
1604
|
+
active.responseMessageId,
|
|
1605
|
+
cutoff,
|
|
1606
|
+
)
|
|
1607
|
+
const baseState =
|
|
1608
|
+
projectedState ??
|
|
1609
|
+
restoreGenerationBase(
|
|
1610
|
+
state,
|
|
1611
|
+
active.generationId,
|
|
1612
|
+
active.responseMessageId,
|
|
1613
|
+
'interrupted',
|
|
1614
|
+
)
|
|
1615
|
+
const next = {
|
|
1616
|
+
...baseState,
|
|
1617
|
+
activeRequestId: null,
|
|
1618
|
+
activeResponseMessageId: null,
|
|
1619
|
+
terminalRequestIds: {
|
|
1620
|
+
...baseState.terminalRequestIds,
|
|
1621
|
+
[active.requestId]: true as const,
|
|
1622
|
+
},
|
|
1623
|
+
tools:
|
|
1624
|
+
projectedState === null
|
|
1625
|
+
? stopGenerationTools(
|
|
1626
|
+
baseState.tools,
|
|
1627
|
+
active.generationId,
|
|
1628
|
+
active.responseMessageId,
|
|
1629
|
+
'Tool execution was interrupted.',
|
|
1630
|
+
)
|
|
1631
|
+
: baseState.tools,
|
|
1632
|
+
}
|
|
1633
|
+
return { ...next, status: lifecycleStatus(state, next) }
|
|
1634
|
+
}
|
|
1635
|
+
case 'ai.tool.called': {
|
|
1636
|
+
const payload = event.payload as ToolCalledPayload
|
|
1637
|
+
const projection = state.activeProjection
|
|
1638
|
+
if (
|
|
1639
|
+
isStaleGeneration(state, payload.messageId, payload.generationId) ||
|
|
1640
|
+
terminalGenerationReason(state, payload.generationId) !== undefined
|
|
1641
|
+
) {
|
|
1642
|
+
return state
|
|
1643
|
+
}
|
|
1644
|
+
if (
|
|
1645
|
+
state.tools.some(
|
|
1646
|
+
(activity) =>
|
|
1647
|
+
activity.generationId === payload.generationId &&
|
|
1648
|
+
activity.toolCallId === payload.toolCallId &&
|
|
1649
|
+
activity.status === 'failed' &&
|
|
1650
|
+
state.activeGeneration?.generationId !== payload.generationId,
|
|
1651
|
+
)
|
|
1652
|
+
) {
|
|
1653
|
+
return state
|
|
1654
|
+
}
|
|
1655
|
+
if (projection?.generationId !== payload.generationId) {
|
|
1656
|
+
const next = {
|
|
1657
|
+
...state,
|
|
1658
|
+
tools: reduceToolActivity(state.tools, {
|
|
1659
|
+
type: 'ai.tool.called',
|
|
1660
|
+
payload,
|
|
1661
|
+
}),
|
|
1662
|
+
}
|
|
1663
|
+
return { ...next, status: lifecycleStatus(state, next) }
|
|
1664
|
+
}
|
|
1665
|
+
const activeProjection: ActiveGenerationProjection<M> = {
|
|
1666
|
+
...projection,
|
|
1667
|
+
toolEvents: [
|
|
1668
|
+
...projection.toolEvents,
|
|
1669
|
+
{ index: event.index, type: 'ai.tool.called', payload },
|
|
1670
|
+
],
|
|
1671
|
+
}
|
|
1672
|
+
const next = {
|
|
1673
|
+
...state,
|
|
1674
|
+
activeProjection,
|
|
1675
|
+
tools: projectedTools(activeProjection),
|
|
1676
|
+
}
|
|
1677
|
+
return { ...next, status: lifecycleStatus(state, next) }
|
|
1678
|
+
}
|
|
1679
|
+
case 'ai.tool.result': {
|
|
1680
|
+
const payload = event.payload as ToolResultPayload
|
|
1681
|
+
const projection = state.activeProjection
|
|
1682
|
+
const terminalReason = terminalGenerationReason(
|
|
1683
|
+
state,
|
|
1684
|
+
payload.generationId,
|
|
1685
|
+
)
|
|
1686
|
+
const completesFailedTool =
|
|
1687
|
+
terminalReason === 'failed' &&
|
|
1688
|
+
state.status === 'failed' &&
|
|
1689
|
+
payload.preliminary !== true &&
|
|
1690
|
+
projection?.generationId === payload.generationId &&
|
|
1691
|
+
projectedTools(projection).some(
|
|
1692
|
+
(activity) =>
|
|
1693
|
+
activity.generationId === payload.generationId &&
|
|
1694
|
+
activity.toolCallId === payload.toolCallId &&
|
|
1695
|
+
activity.status === 'running',
|
|
1696
|
+
)
|
|
1697
|
+
if (
|
|
1698
|
+
isStaleGeneration(state, payload.messageId, payload.generationId) ||
|
|
1699
|
+
(terminalReason !== undefined && !completesFailedTool)
|
|
1700
|
+
) {
|
|
1701
|
+
return state
|
|
1702
|
+
}
|
|
1703
|
+
if (
|
|
1704
|
+
state.tools.some(
|
|
1705
|
+
(activity) =>
|
|
1706
|
+
activity.generationId === payload.generationId &&
|
|
1707
|
+
activity.toolCallId === payload.toolCallId &&
|
|
1708
|
+
activity.status === 'failed' &&
|
|
1709
|
+
state.activeGeneration?.generationId !== payload.generationId &&
|
|
1710
|
+
!completesFailedTool,
|
|
1711
|
+
)
|
|
1712
|
+
) {
|
|
1713
|
+
return state
|
|
1714
|
+
}
|
|
1715
|
+
if (projection?.generationId !== payload.generationId) {
|
|
1716
|
+
const next = {
|
|
1717
|
+
...state,
|
|
1718
|
+
messages: projectMessageChunks(state.messages, payload.messageId, [
|
|
1719
|
+
toolResultChunk(payload),
|
|
1720
|
+
]),
|
|
1721
|
+
tools: reduceToolActivity(state.tools, {
|
|
1722
|
+
type: 'ai.tool.result',
|
|
1723
|
+
payload,
|
|
1724
|
+
}),
|
|
1725
|
+
}
|
|
1726
|
+
return { ...next, status: lifecycleStatus(state, next) }
|
|
1727
|
+
}
|
|
1728
|
+
const activeProjection: ActiveGenerationProjection<M> = {
|
|
1729
|
+
...projection,
|
|
1730
|
+
toolEvents: [
|
|
1731
|
+
...projection.toolEvents,
|
|
1732
|
+
{ index: event.index, type: 'ai.tool.result', payload },
|
|
1733
|
+
],
|
|
1734
|
+
}
|
|
1735
|
+
const projected = projectedMessage(activeProjection)
|
|
1736
|
+
const tools = projectedTools(activeProjection)
|
|
1737
|
+
const failureError = state.error ?? 'Generation failed.'
|
|
1738
|
+
const next = {
|
|
1739
|
+
...state,
|
|
1740
|
+
messages: upsertMessage(
|
|
1741
|
+
state.messages,
|
|
1742
|
+
terminalReason === 'failed'
|
|
1743
|
+
? interruptUIMessage(
|
|
1744
|
+
projected,
|
|
1745
|
+
`Generation failed before tool completion: ${failureError}`,
|
|
1746
|
+
)
|
|
1747
|
+
: projected,
|
|
1748
|
+
),
|
|
1749
|
+
activeProjection,
|
|
1750
|
+
tools:
|
|
1751
|
+
terminalReason === 'failed'
|
|
1752
|
+
? stopGenerationTools(
|
|
1753
|
+
tools,
|
|
1754
|
+
payload.generationId,
|
|
1755
|
+
payload.messageId,
|
|
1756
|
+
failureError,
|
|
1757
|
+
)
|
|
1758
|
+
: tools,
|
|
1759
|
+
}
|
|
1760
|
+
return { ...next, status: lifecycleStatus(state, next) }
|
|
1761
|
+
}
|
|
1762
|
+
case 'ai.approval.requested': {
|
|
1763
|
+
const payload = event.payload as ApprovalRequestedPayload
|
|
1764
|
+
const projection = state.activeProjection
|
|
1765
|
+
if (
|
|
1766
|
+
isStaleGeneration(state, payload.messageId, payload.generationId) ||
|
|
1767
|
+
terminalGenerationReason(state, payload.generationId) !== undefined
|
|
1768
|
+
) {
|
|
1769
|
+
return state
|
|
1770
|
+
}
|
|
1771
|
+
if (
|
|
1772
|
+
state.tools.some(
|
|
1773
|
+
(activity) =>
|
|
1774
|
+
activity.generationId === payload.generationId &&
|
|
1775
|
+
activity.toolCallId === payload.toolCallId &&
|
|
1776
|
+
activity.status === 'failed' &&
|
|
1777
|
+
state.activeGeneration?.generationId !== payload.generationId,
|
|
1778
|
+
)
|
|
1779
|
+
) {
|
|
1780
|
+
return state
|
|
1781
|
+
}
|
|
1782
|
+
const activeProjection =
|
|
1783
|
+
projection?.generationId === payload.generationId
|
|
1784
|
+
? {
|
|
1785
|
+
...projection,
|
|
1786
|
+
approvalEvents: [
|
|
1787
|
+
...projection.approvalEvents,
|
|
1788
|
+
{
|
|
1789
|
+
index: event.index,
|
|
1790
|
+
type: 'ai.approval.requested' as const,
|
|
1791
|
+
payload,
|
|
1792
|
+
},
|
|
1793
|
+
],
|
|
1794
|
+
}
|
|
1795
|
+
: projection
|
|
1796
|
+
const next = {
|
|
1797
|
+
...state,
|
|
1798
|
+
messages:
|
|
1799
|
+
activeProjection === null || activeProjection === projection
|
|
1800
|
+
? projectMessageChunks(state.messages, payload.messageId, [
|
|
1801
|
+
approvalRequestedChunk(payload),
|
|
1802
|
+
])
|
|
1803
|
+
: upsertMessage(state.messages, projectedMessage(activeProjection)),
|
|
1804
|
+
activeProjection,
|
|
1805
|
+
pendingApprovals:
|
|
1806
|
+
payload.isAutomatic === true
|
|
1807
|
+
? state.pendingApprovals
|
|
1808
|
+
: [
|
|
1809
|
+
...state.pendingApprovals.filter(
|
|
1810
|
+
(approval) =>
|
|
1811
|
+
approval.approvalId !== payload.approvalId ||
|
|
1812
|
+
approval.messageId !== payload.messageId,
|
|
1813
|
+
),
|
|
1814
|
+
payload,
|
|
1815
|
+
],
|
|
1816
|
+
}
|
|
1817
|
+
return {
|
|
1818
|
+
...next,
|
|
1819
|
+
status: lifecycleStatus(state, next),
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
case 'ai.approval.responded': {
|
|
1823
|
+
const payload = event.payload as ApprovalRespondedPayload
|
|
1824
|
+
if (
|
|
1825
|
+
isStaleGeneration(state, payload.messageId, payload.generationId) ||
|
|
1826
|
+
terminalGenerationReason(state, payload.generationId) !== undefined
|
|
1827
|
+
) {
|
|
1828
|
+
return state
|
|
1829
|
+
}
|
|
1830
|
+
const projection = state.activeProjection
|
|
1831
|
+
const pendingRequest = state.pendingApprovals.find(
|
|
1832
|
+
(candidate) =>
|
|
1833
|
+
candidate.approvalId === payload.approvalId &&
|
|
1834
|
+
candidate.messageId === payload.messageId &&
|
|
1835
|
+
candidate.generationId === payload.generationId,
|
|
1836
|
+
)
|
|
1837
|
+
const activeRequest = projection?.approvalEvents.findLast(
|
|
1838
|
+
(candidate) =>
|
|
1839
|
+
candidate.type === 'ai.approval.requested' &&
|
|
1840
|
+
candidate.payload.approvalId === payload.approvalId &&
|
|
1841
|
+
candidate.payload.messageId === payload.messageId &&
|
|
1842
|
+
candidate.payload.generationId === payload.generationId,
|
|
1843
|
+
)
|
|
1844
|
+
if (
|
|
1845
|
+
state.status === 'failed' &&
|
|
1846
|
+
pendingRequest === undefined &&
|
|
1847
|
+
activeRequest === undefined
|
|
1848
|
+
) {
|
|
1849
|
+
return state
|
|
1850
|
+
}
|
|
1851
|
+
const activeProjection =
|
|
1852
|
+
projection !== null && activeRequest !== undefined
|
|
1853
|
+
? {
|
|
1854
|
+
...projection,
|
|
1855
|
+
approvalEvents: [
|
|
1856
|
+
...projection.approvalEvents,
|
|
1857
|
+
{
|
|
1858
|
+
index: event.index,
|
|
1859
|
+
type: 'ai.approval.responded' as const,
|
|
1860
|
+
payload,
|
|
1861
|
+
},
|
|
1862
|
+
],
|
|
1863
|
+
}
|
|
1864
|
+
: projection
|
|
1865
|
+
const next = {
|
|
1866
|
+
...state,
|
|
1867
|
+
messages:
|
|
1868
|
+
activeProjection === null || activeProjection === projection
|
|
1869
|
+
? answerApproval(state.messages, payload)
|
|
1870
|
+
: upsertMessage(state.messages, projectedMessage(activeProjection)),
|
|
1871
|
+
activeProjection,
|
|
1872
|
+
pendingApprovals: state.pendingApprovals.filter(
|
|
1873
|
+
(approval) =>
|
|
1874
|
+
approval.approvalId !== payload.approvalId ||
|
|
1875
|
+
approval.messageId !== payload.messageId ||
|
|
1876
|
+
approval.generationId !== payload.generationId,
|
|
1877
|
+
),
|
|
1878
|
+
}
|
|
1879
|
+
return {
|
|
1880
|
+
...next,
|
|
1881
|
+
status: lifecycleStatus(state, next),
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
case 'ai.input.requested': {
|
|
1885
|
+
const payload = event.payload as InputRequestedPayload
|
|
1886
|
+
if (
|
|
1887
|
+
isStaleGeneration(state, payload.messageId, payload.generationId) ||
|
|
1888
|
+
terminalGenerationReason(state, payload.generationId) !== undefined
|
|
1889
|
+
) {
|
|
1890
|
+
return state
|
|
1891
|
+
}
|
|
1892
|
+
const next = {
|
|
1893
|
+
...state,
|
|
1894
|
+
pendingInputs: [
|
|
1895
|
+
...state.pendingInputs.filter(
|
|
1896
|
+
(input) =>
|
|
1897
|
+
input.messageId !== payload.messageId ||
|
|
1898
|
+
input.generationId !== payload.generationId ||
|
|
1899
|
+
input.inputId !== payload.inputId,
|
|
1900
|
+
),
|
|
1901
|
+
payload,
|
|
1902
|
+
],
|
|
1903
|
+
}
|
|
1904
|
+
return { ...next, status: lifecycleStatus(state, next) }
|
|
1905
|
+
}
|
|
1906
|
+
case 'ai.input.responded': {
|
|
1907
|
+
const payload = event.payload as InputRespondedPayload
|
|
1908
|
+
if (
|
|
1909
|
+
isStaleGeneration(state, payload.messageId, payload.generationId) ||
|
|
1910
|
+
terminalGenerationReason(state, payload.generationId) !== undefined
|
|
1911
|
+
) {
|
|
1912
|
+
return state
|
|
1913
|
+
}
|
|
1914
|
+
const next = {
|
|
1915
|
+
...state,
|
|
1916
|
+
pendingInputs: state.pendingInputs.filter(
|
|
1917
|
+
(input) =>
|
|
1918
|
+
input.inputId !== payload.inputId ||
|
|
1919
|
+
input.messageId !== payload.messageId ||
|
|
1920
|
+
input.generationId !== payload.generationId,
|
|
1921
|
+
),
|
|
1922
|
+
}
|
|
1923
|
+
return { ...next, status: lifecycleStatus(state, next) }
|
|
1924
|
+
}
|
|
1925
|
+
case 'ai.compaction.requested': {
|
|
1926
|
+
const payload = event.payload as CompactionRequestedPayload
|
|
1927
|
+
if (state.activeGeneration?.generationId !== payload.generationId) {
|
|
1928
|
+
return state
|
|
1929
|
+
}
|
|
1930
|
+
return {
|
|
1931
|
+
...state,
|
|
1932
|
+
compaction: { status: 'running', ...payload },
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
case 'ai.compaction.completed': {
|
|
1936
|
+
const payload = event.payload as CompactionCompletedPayload<M>
|
|
1937
|
+
if (
|
|
1938
|
+
state.activeGeneration?.generationId !== payload.generationId ||
|
|
1939
|
+
state.compaction?.status !== 'running' ||
|
|
1940
|
+
state.compaction.generationId !== payload.generationId
|
|
1941
|
+
) {
|
|
1942
|
+
return state
|
|
1943
|
+
}
|
|
1944
|
+
return {
|
|
1945
|
+
...state,
|
|
1946
|
+
compaction: { status: 'completed', ...payload },
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
default:
|
|
1950
|
+
return state
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/** Derive AI SDK UI messages directly from an A2 event history. */
|
|
1955
|
+
export function deriveUIMessages<M extends UIMessage = UIMessage>(
|
|
1956
|
+
history: readonly { type: string; payload: unknown; index: number }[],
|
|
1957
|
+
): M[] {
|
|
1958
|
+
let state = initialState<M>()
|
|
1959
|
+
for (const event of history) state = reduceAIState(state, event)
|
|
1960
|
+
return state.messages
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/** Build the standard AI state projection for any contract containing the protocol. */
|
|
1964
|
+
export function createReducer<D extends EventDefs>(options: {
|
|
1965
|
+
contract: Contract<D>
|
|
1966
|
+
name?: string
|
|
1967
|
+
}): Reducer<D, AIState<AIMessageFrom<D>>> {
|
|
1968
|
+
type M = AIMessageFrom<D>
|
|
1969
|
+
return options.contract
|
|
1970
|
+
.reducer({
|
|
1971
|
+
name: options.name ?? 'a2.ai.state.v8',
|
|
1972
|
+
initialState: initialState<M>(),
|
|
1973
|
+
})
|
|
1974
|
+
.fold((state, event) => reduceAIState(state, event))
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
type ProtocolInput<
|
|
1978
|
+
M extends UIMessage,
|
|
1979
|
+
K extends keyof AIEventDefs<M> & string,
|
|
1980
|
+
> = AppendInput<Pick<AIEventDefs<M>, K>>
|
|
1981
|
+
|
|
1982
|
+
export type AIInputs = {
|
|
1983
|
+
/** Record a message. User messages schedule unless generate is false. */
|
|
1984
|
+
message<M extends UIMessage>(
|
|
1985
|
+
message: M,
|
|
1986
|
+
options?: { generate?: boolean },
|
|
1987
|
+
): ProtocolInput<M, 'ai.message.created'>[]
|
|
1988
|
+
/** Record a message without requesting a generation. */
|
|
1989
|
+
seed<M extends UIMessage>(
|
|
1990
|
+
message: M,
|
|
1991
|
+
): ProtocolInput<M, 'ai.message.created'>[]
|
|
1992
|
+
/** Answer an AI SDK tool approval. The tool barrier owns continuation. */
|
|
1993
|
+
approval(
|
|
1994
|
+
response: ApprovalRespondedPayload,
|
|
1995
|
+
): ProtocolInput<UIMessage, 'ai.approval.responded'>[]
|
|
1996
|
+
/** Answer an application-defined input request. */
|
|
1997
|
+
input(
|
|
1998
|
+
response: InputRespondedPayload,
|
|
1999
|
+
): ProtocolInput<UIMessage, 'ai.input.responded'>[]
|
|
2000
|
+
/** Record an application-defined request for input. */
|
|
2001
|
+
requestInput(
|
|
2002
|
+
request: InputRequestedPayload,
|
|
2003
|
+
): ProtocolInput<UIMessage, 'ai.input.requested'>[]
|
|
2004
|
+
/** Request a fresh attempt for a failed response. */
|
|
2005
|
+
retry(
|
|
2006
|
+
options: RetryGenerationOptions,
|
|
2007
|
+
): ProtocolInput<UIMessage, 'ai.retry.requested'>[]
|
|
2008
|
+
/** Interrupt the active response for a message. */
|
|
2009
|
+
interrupt(
|
|
2010
|
+
interruption: MessageInterruptedPayload,
|
|
2011
|
+
): ProtocolInput<UIMessage, 'ai.message.interrupted'>[]
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
const messageInput = <M extends UIMessage>(
|
|
2015
|
+
message: M,
|
|
2016
|
+
generate?: boolean,
|
|
2017
|
+
): ProtocolInput<M, 'ai.message.created'>[] => [
|
|
2018
|
+
{
|
|
2019
|
+
type: 'ai.message.created',
|
|
2020
|
+
id: `ai.message:${message.id}`,
|
|
2021
|
+
payload: {
|
|
2022
|
+
message,
|
|
2023
|
+
...(generate === false ? { generate: false } : {}),
|
|
2024
|
+
},
|
|
2025
|
+
},
|
|
2026
|
+
]
|
|
2027
|
+
|
|
2028
|
+
/** Pure append inputs for the built-in AI protocol. */
|
|
2029
|
+
export const inputs: AIInputs = {
|
|
2030
|
+
message<M extends UIMessage>(message: M, options?: { generate?: boolean }) {
|
|
2031
|
+
return messageInput(message, options?.generate)
|
|
2032
|
+
},
|
|
2033
|
+
seed<M extends UIMessage>(message: M) {
|
|
2034
|
+
return messageInput(message, false)
|
|
2035
|
+
},
|
|
2036
|
+
approval(response) {
|
|
2037
|
+
return [
|
|
2038
|
+
{
|
|
2039
|
+
type: 'ai.approval.responded',
|
|
2040
|
+
id: `ai.approval:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.approvalId)}:response`,
|
|
2041
|
+
payload: response,
|
|
2042
|
+
},
|
|
2043
|
+
]
|
|
2044
|
+
},
|
|
2045
|
+
input(response) {
|
|
2046
|
+
return [
|
|
2047
|
+
{
|
|
2048
|
+
type: 'ai.input.responded',
|
|
2049
|
+
id: `ai.input:${encodeURIComponent(response.messageId)}:${encodeURIComponent(response.generationId)}:${encodeURIComponent(response.inputId)}:response`,
|
|
2050
|
+
payload: response,
|
|
2051
|
+
},
|
|
2052
|
+
]
|
|
2053
|
+
},
|
|
2054
|
+
requestInput(request) {
|
|
2055
|
+
return [
|
|
2056
|
+
{
|
|
2057
|
+
type: 'ai.input.requested',
|
|
2058
|
+
id: `ai.input:${encodeURIComponent(request.messageId)}:${encodeURIComponent(request.generationId)}:${encodeURIComponent(request.inputId)}:request`,
|
|
2059
|
+
payload: request,
|
|
2060
|
+
},
|
|
2061
|
+
]
|
|
2062
|
+
},
|
|
2063
|
+
retry(retryOptions) {
|
|
2064
|
+
return [
|
|
2065
|
+
{
|
|
2066
|
+
type: 'ai.retry.requested',
|
|
2067
|
+
id: `ai.retry:${retryOptions.retryId}`,
|
|
2068
|
+
payload: retryOptions,
|
|
2069
|
+
},
|
|
2070
|
+
]
|
|
2071
|
+
},
|
|
2072
|
+
interrupt(interruption) {
|
|
2073
|
+
return [
|
|
2074
|
+
{
|
|
2075
|
+
type: 'ai.message.interrupted',
|
|
2076
|
+
id: `ai.interrupt:${interruption.generationId ?? interruption.requestId}`,
|
|
2077
|
+
payload: interruption,
|
|
2078
|
+
},
|
|
2079
|
+
]
|
|
2080
|
+
},
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
/**
|
|
2084
|
+
* Read the durable handler context ambient to the current AI tool
|
|
2085
|
+
* execution. A2 runs each local tool's `execute` inside this scope, so
|
|
2086
|
+
* plain AI SDK `tool()` definitions reach their `event`, `attempt`,
|
|
2087
|
+
* `session`, and `signal` without an A2-specific wrapper. The agent (or
|
|
2088
|
+
* contract) argument carries the event types and is verified against
|
|
2089
|
+
* the executing server's contract.
|
|
2090
|
+
*/
|
|
2091
|
+
export function handlerContext<D extends AgentToolEventDefs>(
|
|
2092
|
+
agentOrContract: Contract<D> | { readonly contract: Contract<D> },
|
|
2093
|
+
): AgentToolContext<D> {
|
|
2094
|
+
const contract =
|
|
2095
|
+
'contract' in agentOrContract ? agentOrContract.contract : agentOrContract
|
|
2096
|
+
const scope = ambientToolScopeStorage()?.getStore()
|
|
2097
|
+
if (scope === undefined) {
|
|
2098
|
+
throw new TypeError(
|
|
2099
|
+
`handlerContext('${contract.name}') requires an ambient A2 tool execution: call it inside a local tool's execute, or wrap direct tool tests with runWithHandlerContext`,
|
|
2100
|
+
)
|
|
2101
|
+
}
|
|
2102
|
+
if (scope.contract !== undefined && scope.contract.name !== contract.name) {
|
|
2103
|
+
throw new TypeError(
|
|
2104
|
+
`handlerContext('${contract.name}') was called during a tool execution for agent '${scope.contract.name}'`,
|
|
2105
|
+
)
|
|
2106
|
+
}
|
|
2107
|
+
return scope.context as AgentToolContext<D>
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
/**
|
|
2111
|
+
* Define an agent: built-in AI protocol + application events and one state
|
|
2112
|
+
* reducer. The result is isomorphic.
|
|
2113
|
+
*/
|
|
2114
|
+
export function agent<
|
|
2115
|
+
M extends UIMessage = UIMessage,
|
|
2116
|
+
E extends EventDefs = Record<never, never>,
|
|
2117
|
+
>(options: AgentOptions<M, E>): AgentDefinition<M, AIEventDefs<M> & E> {
|
|
2118
|
+
const builtIns = createEvents(
|
|
2119
|
+
options.messageSchema === undefined
|
|
2120
|
+
? undefined
|
|
2121
|
+
: { messageSchema: options.messageSchema },
|
|
2122
|
+
)
|
|
2123
|
+
const extensions = options.events ?? ({} as E)
|
|
2124
|
+
for (const key of Object.keys(extensions)) {
|
|
2125
|
+
if (Object.hasOwn(builtIns, key)) {
|
|
2126
|
+
throw new TypeError(
|
|
2127
|
+
`agent event '${key}' conflicts with a built-in event`,
|
|
2128
|
+
)
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const agentContract = createContract({
|
|
2132
|
+
name: options.name,
|
|
2133
|
+
events: { ...builtIns, ...extensions },
|
|
2134
|
+
})
|
|
2135
|
+
const reducer = agentContract
|
|
2136
|
+
.reducer({
|
|
2137
|
+
name: options.reducerName ?? 'a2.ai.state.v8',
|
|
2138
|
+
initialState: initialState<M>(),
|
|
2139
|
+
})
|
|
2140
|
+
.fold((state, event) => reduceAIState(state, event))
|
|
2141
|
+
|
|
2142
|
+
const definition = {
|
|
2143
|
+
contract: agentContract,
|
|
2144
|
+
reducer,
|
|
2145
|
+
}
|
|
2146
|
+
return definition as unknown as AgentDefinition<M, AIEventDefs<M> & E>
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
export type {
|
|
2150
|
+
FinishReason,
|
|
2151
|
+
JSONValue,
|
|
2152
|
+
LanguageModelUsage,
|
|
2153
|
+
UIMessage,
|
|
2154
|
+
UIMessageChunk,
|
|
2155
|
+
}
|