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-server.ts
ADDED
|
@@ -0,0 +1,1719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* experimental-a2/ai/server — the server-only implementation of an experimental-a2/ai definition.
|
|
3
|
+
*
|
|
4
|
+
* `createHandlers()` exposes the ordinary A2 handler table;
|
|
5
|
+
* `createAgentServer()` is its batteries-included assembly with createServer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// oxlint-disable no-await-in-loop -- stream chunks and durable appends are
|
|
9
|
+
// ordered; parallel consumption would corrupt progress sequence and state.
|
|
10
|
+
|
|
11
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
12
|
+
import { convertToModelMessages } from 'ai'
|
|
13
|
+
import type {
|
|
14
|
+
FinishReason,
|
|
15
|
+
Instructions,
|
|
16
|
+
LanguageModel,
|
|
17
|
+
LanguageModelUsage,
|
|
18
|
+
ModelMessage,
|
|
19
|
+
TextStreamPart,
|
|
20
|
+
ToolSet,
|
|
21
|
+
UIMessage,
|
|
22
|
+
UIMessageChunk,
|
|
23
|
+
} from 'ai'
|
|
24
|
+
import { generateAISDKStep, type AISDKStepSettings } from './ai-sdk-step.ts'
|
|
25
|
+
import type {
|
|
26
|
+
AIEventDefs,
|
|
27
|
+
AIState,
|
|
28
|
+
AgentDefinition,
|
|
29
|
+
AgentToolContext,
|
|
30
|
+
AgentToolEventDefs,
|
|
31
|
+
ApprovalRequestedPayload,
|
|
32
|
+
CompactionCompletedPayload,
|
|
33
|
+
GenerationCompletedPayload,
|
|
34
|
+
GenerationFailedPayload,
|
|
35
|
+
GenerationProgressPayload,
|
|
36
|
+
GenerationRequestedPayload,
|
|
37
|
+
GenerationStartedPayload,
|
|
38
|
+
MessageCompletedPayload,
|
|
39
|
+
MessageCreatedPayload,
|
|
40
|
+
MessageInterruptedPayload,
|
|
41
|
+
ToolCalledPayload,
|
|
42
|
+
ToolResultPayload,
|
|
43
|
+
} from './ai.ts'
|
|
44
|
+
import {
|
|
45
|
+
aiCoordinatorReducer,
|
|
46
|
+
continuationReady,
|
|
47
|
+
type AICoordinatorState,
|
|
48
|
+
} from './ai-coordinator.ts'
|
|
49
|
+
import type { AppendInput, ContractEvent, EventDefs } from './contract.ts'
|
|
50
|
+
import { A2Error } from './errors.ts'
|
|
51
|
+
import {
|
|
52
|
+
consumeSchedulerSendFailure,
|
|
53
|
+
installAmbientToolScopeStorage,
|
|
54
|
+
type AmbientToolScope,
|
|
55
|
+
} from './internal.ts'
|
|
56
|
+
import {
|
|
57
|
+
createServer,
|
|
58
|
+
type A2Server,
|
|
59
|
+
type HandlerContext,
|
|
60
|
+
type PushValidationContext,
|
|
61
|
+
type ServerOptions,
|
|
62
|
+
} from './server.ts'
|
|
63
|
+
|
|
64
|
+
export function validateAgentPush(context: PushValidationContext): void {
|
|
65
|
+
const rejected = context.events.find((event) => {
|
|
66
|
+
if (event.type !== 'ai.message.created') {
|
|
67
|
+
return !(
|
|
68
|
+
event.type === 'ai.approval.responded' ||
|
|
69
|
+
event.type === 'ai.input.responded' ||
|
|
70
|
+
event.type === 'ai.message.interrupted' ||
|
|
71
|
+
event.type === 'ai.retry.requested'
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
const payload = event.payload
|
|
75
|
+
return (
|
|
76
|
+
typeof payload !== 'object' ||
|
|
77
|
+
payload === null ||
|
|
78
|
+
!('message' in payload) ||
|
|
79
|
+
typeof payload.message !== 'object' ||
|
|
80
|
+
payload.message === null ||
|
|
81
|
+
!('role' in payload.message) ||
|
|
82
|
+
payload.message.role !== 'user'
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
if (rejected !== undefined) {
|
|
86
|
+
throw new A2Error(
|
|
87
|
+
'INVALID_PAYLOAD',
|
|
88
|
+
`event '${rejected.type}' is server-only for an AI agent`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type AgentGenerationProgress = {
|
|
94
|
+
type: 'progress'
|
|
95
|
+
chunks: UIMessageChunk[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type AgentGenerationFinish = {
|
|
99
|
+
type: 'finish'
|
|
100
|
+
finishReason?: FinishReason
|
|
101
|
+
usage?: LanguageModelUsage
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type AgentGenerationUpdate = AgentGenerationProgress | AgentGenerationFinish
|
|
105
|
+
|
|
106
|
+
export type AgentGenerateContext<
|
|
107
|
+
M extends UIMessage,
|
|
108
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
109
|
+
T extends ToolSet,
|
|
110
|
+
> = {
|
|
111
|
+
request: ContractEvent<D, 'ai.generation.requested'>
|
|
112
|
+
requestId: string
|
|
113
|
+
generationId: string
|
|
114
|
+
responseMessageId: string
|
|
115
|
+
messages: M[]
|
|
116
|
+
state: AIState<M>
|
|
117
|
+
history: ContractEvent<D>[]
|
|
118
|
+
signal: AbortSignal
|
|
119
|
+
model: LanguageModel
|
|
120
|
+
tools: T
|
|
121
|
+
instructions?: Instructions
|
|
122
|
+
generation: AgentGenerationSettings<T>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type AgentGenerate<
|
|
126
|
+
M extends UIMessage,
|
|
127
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
128
|
+
T extends ToolSet,
|
|
129
|
+
> = (
|
|
130
|
+
context: AgentGenerateContext<M, D, T>,
|
|
131
|
+
) => ReadableStream<UIMessageChunk> | Promise<ReadableStream<UIMessageChunk>>
|
|
132
|
+
|
|
133
|
+
type UIMessageMetadata<M extends UIMessage> =
|
|
134
|
+
M extends UIMessage<infer Metadata> ? Metadata : unknown
|
|
135
|
+
|
|
136
|
+
export type AgentMessageMetadata<
|
|
137
|
+
M extends UIMessage,
|
|
138
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
139
|
+
T extends ToolSet,
|
|
140
|
+
> = (
|
|
141
|
+
context: AgentGenerateContext<M, D, T> & { part: TextStreamPart<T> },
|
|
142
|
+
) => UIMessageMetadata<M> | undefined
|
|
143
|
+
|
|
144
|
+
export type AgentResolverContext<
|
|
145
|
+
M extends UIMessage,
|
|
146
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
147
|
+
> = {
|
|
148
|
+
event: ContractEvent<D, 'ai.generation.requested'>
|
|
149
|
+
state: AIState<M>
|
|
150
|
+
history: ContractEvent<D>[]
|
|
151
|
+
signal: AbortSignal
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type Resolvable<T, C> = T | ((context: C) => T | Promise<T>)
|
|
155
|
+
|
|
156
|
+
export type CompactionPolicy<
|
|
157
|
+
M extends UIMessage,
|
|
158
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
159
|
+
> = {
|
|
160
|
+
shouldCompact(
|
|
161
|
+
context: AgentResolverContext<M, D> & { messages: M[] },
|
|
162
|
+
): boolean | Promise<boolean>
|
|
163
|
+
compact(
|
|
164
|
+
context: AgentResolverContext<M, D> & { messages: M[] },
|
|
165
|
+
): M[] | Promise<M[]>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type AgentGenerationSettings<T extends ToolSet> = AISDKStepSettings<T>
|
|
169
|
+
|
|
170
|
+
export type CreateHandlersOptions<
|
|
171
|
+
M extends UIMessage,
|
|
172
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
173
|
+
T extends ToolSet = ToolSet,
|
|
174
|
+
> = {
|
|
175
|
+
agent: AgentDefinition<M, D>
|
|
176
|
+
/** AI SDK model string or provider model, optionally resolved per generation. */
|
|
177
|
+
model: Resolvable<LanguageModel, AgentResolverContext<M, D>>
|
|
178
|
+
tools?: T
|
|
179
|
+
instructions?: Resolvable<
|
|
180
|
+
Instructions | undefined,
|
|
181
|
+
AgentResolverContext<M, D>
|
|
182
|
+
>
|
|
183
|
+
/** Settings for each one-step AI SDK streamText call. */
|
|
184
|
+
generation?: AgentGenerationSettings<T>
|
|
185
|
+
/**
|
|
186
|
+
* Maximum model steps in one assistant response. Defaults to Infinity.
|
|
187
|
+
*/
|
|
188
|
+
maxSteps?: number
|
|
189
|
+
/** Map default AI SDK stream parts to typed assistant message metadata. */
|
|
190
|
+
messageMetadata?: AgentMessageMetadata<M, D, T>
|
|
191
|
+
/**
|
|
192
|
+
* Replace model generation with an AI SDK UI message chunk stream.
|
|
193
|
+
* The custom stream owns its metadata chunks.
|
|
194
|
+
*/
|
|
195
|
+
generate?: AgentGenerate<M, D, T>
|
|
196
|
+
compaction?: CompactionPolicy<M, D>
|
|
197
|
+
/** Progress is durably flushed at either limit, whichever is reached first. */
|
|
198
|
+
progress?: { maxChunks?: number; maxDelayMs?: number }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type CreateAgentServerOptions<
|
|
202
|
+
M extends UIMessage,
|
|
203
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
204
|
+
T extends ToolSet = ToolSet,
|
|
205
|
+
> = CreateHandlersOptions<M, D, T> &
|
|
206
|
+
Omit<ServerOptions<D>, 'contract' | 'handlers' | 'validatePush'> & {
|
|
207
|
+
handlers?: ServerOptions<D>['handlers']
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const resolve = async <T, C>(
|
|
211
|
+
value: Resolvable<T, C>,
|
|
212
|
+
context: C,
|
|
213
|
+
): Promise<T> =>
|
|
214
|
+
typeof value === 'function'
|
|
215
|
+
? await (value as (context: C) => T | Promise<T>)(context)
|
|
216
|
+
: value
|
|
217
|
+
|
|
218
|
+
const modelName = (model: LanguageModel): string => {
|
|
219
|
+
if (typeof model === 'string') return model
|
|
220
|
+
if ('modelId' in model && typeof model.modelId === 'string') {
|
|
221
|
+
return model.modelId
|
|
222
|
+
}
|
|
223
|
+
return 'custom'
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const errorMessage = (error: unknown): string =>
|
|
227
|
+
error instanceof Error ? error.message : String(error)
|
|
228
|
+
|
|
229
|
+
const toolScopeStorage = installAmbientToolScopeStorage(
|
|
230
|
+
() => new AsyncLocalStorage<AmbientToolScope>(),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
/** Run `fn` with `ctx` as the ambient tool handler context, for direct tool tests. */
|
|
234
|
+
export function runWithHandlerContext<D extends AgentToolEventDefs, R>(
|
|
235
|
+
ctx: AgentToolContext<D>,
|
|
236
|
+
fn: () => R,
|
|
237
|
+
): R {
|
|
238
|
+
return toolScopeStorage.run({ context: ctx }, fn)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const promptCacheKey = (sessionId: string, generationId: string): string =>
|
|
242
|
+
`${sessionId}\u001f${generationId}`
|
|
243
|
+
|
|
244
|
+
const asyncIterator = (value: unknown): AsyncIterator<unknown> | undefined => {
|
|
245
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
246
|
+
const factory = Reflect.get(value, Symbol.asyncIterator)
|
|
247
|
+
if (typeof factory !== 'function') return undefined
|
|
248
|
+
return (factory as (this: object) => AsyncIterator<unknown>).call(value)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const isBoundaryChunk = (chunk: UIMessageChunk | undefined): boolean =>
|
|
252
|
+
chunk !== undefined &&
|
|
253
|
+
(chunk.type === 'text-end' ||
|
|
254
|
+
chunk.type === 'reasoning-end' ||
|
|
255
|
+
chunk.type === 'tool-input-available' ||
|
|
256
|
+
chunk.type === 'tool-input-error' ||
|
|
257
|
+
chunk.type === 'tool-output-available' ||
|
|
258
|
+
chunk.type === 'tool-output-error' ||
|
|
259
|
+
chunk.type === 'tool-output-denied' ||
|
|
260
|
+
chunk.type === 'tool-approval-request' ||
|
|
261
|
+
chunk.type === 'finish' ||
|
|
262
|
+
chunk.type === 'abort' ||
|
|
263
|
+
chunk.type === 'error')
|
|
264
|
+
|
|
265
|
+
const nextOrTimer = async <T>(
|
|
266
|
+
next: Promise<IteratorResult<T>>,
|
|
267
|
+
delayMs: number,
|
|
268
|
+
): Promise<{ type: 'next'; result: IteratorResult<T> } | { type: 'timer' }> => {
|
|
269
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
270
|
+
try {
|
|
271
|
+
return await Promise.race([
|
|
272
|
+
next.then((result) => ({ type: 'next' as const, result })),
|
|
273
|
+
new Promise<{ type: 'timer' }>((finishTimer) => {
|
|
274
|
+
timer = setTimeout(() => finishTimer({ type: 'timer' }), delayMs)
|
|
275
|
+
;(timer as { unref?: () => void }).unref?.()
|
|
276
|
+
}),
|
|
277
|
+
])
|
|
278
|
+
} finally {
|
|
279
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
type GenerationCompletion = {
|
|
284
|
+
finishReason?: FinishReason
|
|
285
|
+
usage?: LanguageModelUsage
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
type GenerationSource = {
|
|
289
|
+
stream: ReadableStream<UIMessageChunk>
|
|
290
|
+
completion?: () => GenerationCompletion
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function generateWithAISDK<
|
|
294
|
+
M extends UIMessage,
|
|
295
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
296
|
+
T extends ToolSet,
|
|
297
|
+
>(
|
|
298
|
+
context: AgentGenerateContext<M, D, T>,
|
|
299
|
+
messageMetadata: AgentMessageMetadata<M, D, T> | undefined,
|
|
300
|
+
): Promise<GenerationSource> {
|
|
301
|
+
const step = await generateAISDKStep<M, T>({
|
|
302
|
+
model: context.model,
|
|
303
|
+
tools: context.tools,
|
|
304
|
+
messages: context.messages,
|
|
305
|
+
responseMessageId: context.responseMessageId,
|
|
306
|
+
abortSignal: context.signal,
|
|
307
|
+
...(context.instructions === undefined
|
|
308
|
+
? {}
|
|
309
|
+
: { instructions: context.instructions }),
|
|
310
|
+
settings: context.generation,
|
|
311
|
+
...(messageMetadata === undefined
|
|
312
|
+
? {}
|
|
313
|
+
: {
|
|
314
|
+
messageMetadata: ({ part }) => messageMetadata({ ...context, part }),
|
|
315
|
+
}),
|
|
316
|
+
})
|
|
317
|
+
return {
|
|
318
|
+
stream: step.stream as ReadableStream<UIMessageChunk>,
|
|
319
|
+
completion: step.completion,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function* consumeGeneration(options: {
|
|
324
|
+
source: GenerationSource
|
|
325
|
+
progress?: { maxChunks?: number; maxDelayMs?: number }
|
|
326
|
+
}): AsyncGenerator<AgentGenerationUpdate> {
|
|
327
|
+
const pending: UIMessageChunk[] = []
|
|
328
|
+
let streamedFinishReason: FinishReason | undefined
|
|
329
|
+
let streamedError: string | undefined
|
|
330
|
+
const maxChunks = options.progress?.maxChunks ?? 16
|
|
331
|
+
const maxDelayMs = options.progress?.maxDelayMs ?? 30
|
|
332
|
+
let lastFlush = Date.now()
|
|
333
|
+
const chunks = options.source.stream[Symbol.asyncIterator]()
|
|
334
|
+
let pendingChunk = chunks.next()
|
|
335
|
+
try {
|
|
336
|
+
for (;;) {
|
|
337
|
+
let result: IteratorResult<UIMessageChunk>
|
|
338
|
+
if (pending.length > 0) {
|
|
339
|
+
const remaining = Math.max(0, maxDelayMs - (Date.now() - lastFlush))
|
|
340
|
+
const outcome = await nextOrTimer(pendingChunk, remaining)
|
|
341
|
+
if (outcome.type === 'timer') {
|
|
342
|
+
yield { type: 'progress', chunks: pending.splice(0) }
|
|
343
|
+
lastFlush = Date.now()
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
result = outcome.result
|
|
347
|
+
} else {
|
|
348
|
+
result = await pendingChunk
|
|
349
|
+
}
|
|
350
|
+
if (result.done) break
|
|
351
|
+
const chunk = result.value
|
|
352
|
+
pending.push(chunk)
|
|
353
|
+
if (chunk.type === 'finish') streamedFinishReason = chunk.finishReason
|
|
354
|
+
if (chunk.type === 'error') streamedError = chunk.errorText
|
|
355
|
+
pendingChunk = chunks.next()
|
|
356
|
+
|
|
357
|
+
if (pending.length >= maxChunks || isBoundaryChunk(pending.at(-1))) {
|
|
358
|
+
yield { type: 'progress', chunks: pending.splice(0) }
|
|
359
|
+
lastFlush = Date.now()
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (pending.length > 0)
|
|
364
|
+
yield { type: 'progress', chunks: pending.splice(0) }
|
|
365
|
+
throw error
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let completion: GenerationCompletion | undefined
|
|
369
|
+
try {
|
|
370
|
+
completion = options.source.completion?.()
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (pending.length > 0) {
|
|
373
|
+
yield { type: 'progress', chunks: pending.splice(0) }
|
|
374
|
+
}
|
|
375
|
+
throw error
|
|
376
|
+
}
|
|
377
|
+
if (pending.length > 0) {
|
|
378
|
+
yield { type: 'progress', chunks: pending.splice(0) }
|
|
379
|
+
}
|
|
380
|
+
if (streamedError !== undefined) throw new Error(streamedError)
|
|
381
|
+
const finishReason = completion?.finishReason ?? streamedFinishReason
|
|
382
|
+
yield {
|
|
383
|
+
type: 'finish',
|
|
384
|
+
...(finishReason === undefined ? {} : { finishReason }),
|
|
385
|
+
...(completion?.usage === undefined ? {} : { usage: completion.usage }),
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const replay = <M extends UIMessage, D extends AIEventDefs<M> & EventDefs>(
|
|
390
|
+
definition: AgentDefinition<M, D>,
|
|
391
|
+
history: ContractEvent<D>[],
|
|
392
|
+
): AIState<M> => {
|
|
393
|
+
let state = definition.reducer.initialState
|
|
394
|
+
for (const event of history) state = definition.reducer.fold(state, event)
|
|
395
|
+
return state
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const contextMessages = <M extends UIMessage>(state: AIState<M>): M[] => {
|
|
399
|
+
const compaction = state.compaction
|
|
400
|
+
if (compaction?.status !== 'completed' || !compaction.messages) {
|
|
401
|
+
return state.messages
|
|
402
|
+
}
|
|
403
|
+
const boundary = state.messages.findIndex(
|
|
404
|
+
(message) => message.id === compaction.throughMessageId,
|
|
405
|
+
)
|
|
406
|
+
const retained = new Set(compaction.retainedMessageIds ?? [])
|
|
407
|
+
return boundary === -1
|
|
408
|
+
? state.messages
|
|
409
|
+
: [
|
|
410
|
+
...compaction.messages,
|
|
411
|
+
...state.messages
|
|
412
|
+
.slice(0, boundary + 1)
|
|
413
|
+
.filter((message) => retained.has(message.id)),
|
|
414
|
+
...state.messages.slice(boundary + 1),
|
|
415
|
+
]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const activeContextMessages = <M extends UIMessage>(
|
|
419
|
+
state: AIState<M>,
|
|
420
|
+
coordinator: AICoordinatorState,
|
|
421
|
+
): M[] => {
|
|
422
|
+
const queued = new Set(coordinator.queued.map((item) => item.messageId))
|
|
423
|
+
return contextMessages(state).filter((message) => !queued.has(message.id))
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const generationRequestId = (generationId: string): string | undefined => {
|
|
427
|
+
const markerIndex = generationId.lastIndexOf(':generation:')
|
|
428
|
+
return markerIndex === -1 ? undefined : generationId.slice(0, markerIndex)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
type ToolCallClassification = 'automatic' | 'approval' | 'unknown'
|
|
432
|
+
|
|
433
|
+
type PendingToolCall = {
|
|
434
|
+
call: ToolCalledPayload
|
|
435
|
+
classification: ToolCallClassification
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const toolCalledEvent = <D extends EventDefs>(
|
|
439
|
+
payload: ToolCalledPayload,
|
|
440
|
+
): AppendInput<D> =>
|
|
441
|
+
({
|
|
442
|
+
type: 'ai.tool.called',
|
|
443
|
+
id: `${payload.generationId}:tool:${payload.toolCallId}:called`,
|
|
444
|
+
payload,
|
|
445
|
+
}) as AppendInput<D>
|
|
446
|
+
|
|
447
|
+
const approvalClassification = <T extends ToolSet>(
|
|
448
|
+
tools: T,
|
|
449
|
+
generation: AgentGenerationSettings<T>,
|
|
450
|
+
toolName: string,
|
|
451
|
+
): ToolCallClassification => {
|
|
452
|
+
const configured = generation.toolApproval
|
|
453
|
+
if (typeof configured === 'function') return 'unknown'
|
|
454
|
+
const policy = configured?.[toolName]
|
|
455
|
+
if (typeof policy === 'function') return 'unknown'
|
|
456
|
+
if (
|
|
457
|
+
policy === 'user-approval' ||
|
|
458
|
+
(typeof policy === 'object' && policy?.type === 'user-approval')
|
|
459
|
+
) {
|
|
460
|
+
return 'approval'
|
|
461
|
+
}
|
|
462
|
+
if (
|
|
463
|
+
policy === 'denied' ||
|
|
464
|
+
(typeof policy === 'object' && policy?.type === 'denied')
|
|
465
|
+
) {
|
|
466
|
+
return 'approval'
|
|
467
|
+
}
|
|
468
|
+
const tool = tools[toolName] as { needsApproval?: unknown } | undefined
|
|
469
|
+
if (typeof tool?.needsApproval === 'function') return 'unknown'
|
|
470
|
+
return tool?.needsApproval === true ? 'approval' : 'automatic'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const lifecycleEvents = <D extends EventDefs, T extends ToolSet>(options: {
|
|
474
|
+
requestId: string
|
|
475
|
+
messageId: string
|
|
476
|
+
generationId: string
|
|
477
|
+
sequence: number
|
|
478
|
+
chunks: UIMessageChunk[]
|
|
479
|
+
pending: PendingToolCall[]
|
|
480
|
+
tools: T
|
|
481
|
+
generation: AgentGenerationSettings<T>
|
|
482
|
+
custom: boolean
|
|
483
|
+
}): { events: AppendInput<D>[]; pending: PendingToolCall[] } => {
|
|
484
|
+
const result: AppendInput<D>[] = []
|
|
485
|
+
const pending = [...options.pending]
|
|
486
|
+
const take = (toolCallId: string): PendingToolCall | undefined => {
|
|
487
|
+
const index = pending.findIndex(
|
|
488
|
+
(candidate) => candidate.call.toolCallId === toolCallId,
|
|
489
|
+
)
|
|
490
|
+
if (index === -1) return undefined
|
|
491
|
+
return pending.splice(index, 1)[0]
|
|
492
|
+
}
|
|
493
|
+
const flush = (toolCallId: string): void => {
|
|
494
|
+
const pendingCall = take(toolCallId)
|
|
495
|
+
if (pendingCall) result.push(toolCalledEvent<D>(pendingCall.call))
|
|
496
|
+
}
|
|
497
|
+
for (const [chunkIndex, chunk] of options.chunks.entries()) {
|
|
498
|
+
const resultId = `${options.generationId}:tool-result:${options.sequence}:${chunkIndex}`
|
|
499
|
+
if (chunk.type === 'tool-input-available') {
|
|
500
|
+
const payload: ToolCalledPayload = {
|
|
501
|
+
requestId: options.requestId,
|
|
502
|
+
messageId: options.messageId,
|
|
503
|
+
generationId: options.generationId,
|
|
504
|
+
toolCallId: chunk.toolCallId,
|
|
505
|
+
toolName: chunk.toolName,
|
|
506
|
+
input: chunk.input,
|
|
507
|
+
...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
|
|
508
|
+
...(chunk.providerExecuted === undefined
|
|
509
|
+
? {}
|
|
510
|
+
: { providerExecuted: chunk.providerExecuted }),
|
|
511
|
+
...((
|
|
512
|
+
options.tools[chunk.toolName] as
|
|
513
|
+
{ supportsDeferredResults?: boolean } | undefined
|
|
514
|
+
)?.supportsDeferredResults === true
|
|
515
|
+
? { supportsDeferredResults: true }
|
|
516
|
+
: {}),
|
|
517
|
+
...(chunk.providerMetadata === undefined
|
|
518
|
+
? {}
|
|
519
|
+
: { providerMetadata: chunk.providerMetadata }),
|
|
520
|
+
...(chunk.toolMetadata === undefined
|
|
521
|
+
? {}
|
|
522
|
+
: { toolMetadata: chunk.toolMetadata }),
|
|
523
|
+
...(chunk.title === undefined ? {} : { title: chunk.title }),
|
|
524
|
+
}
|
|
525
|
+
take(payload.toolCallId)
|
|
526
|
+
const classification = approvalClassification(
|
|
527
|
+
options.tools,
|
|
528
|
+
options.generation,
|
|
529
|
+
chunk.toolName,
|
|
530
|
+
)
|
|
531
|
+
if (classification === 'automatic') {
|
|
532
|
+
result.push(toolCalledEvent<D>(payload))
|
|
533
|
+
} else {
|
|
534
|
+
pending.push({ call: payload, classification })
|
|
535
|
+
}
|
|
536
|
+
} else if (chunk.type === 'tool-input-error') {
|
|
537
|
+
const payload: ToolResultPayload = {
|
|
538
|
+
requestId: options.requestId,
|
|
539
|
+
messageId: options.messageId,
|
|
540
|
+
generationId: options.generationId,
|
|
541
|
+
toolCallId: chunk.toolCallId,
|
|
542
|
+
toolName: chunk.toolName,
|
|
543
|
+
...(chunk.dynamic ? { input: chunk.input } : { rawInput: chunk.input }),
|
|
544
|
+
error: chunk.errorText,
|
|
545
|
+
phase: 'input',
|
|
546
|
+
...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
|
|
547
|
+
...(chunk.providerExecuted === undefined
|
|
548
|
+
? {}
|
|
549
|
+
: { providerExecuted: chunk.providerExecuted }),
|
|
550
|
+
...(chunk.providerMetadata === undefined
|
|
551
|
+
? {}
|
|
552
|
+
: { providerMetadata: chunk.providerMetadata }),
|
|
553
|
+
...(chunk.toolMetadata === undefined
|
|
554
|
+
? {}
|
|
555
|
+
: { toolMetadata: chunk.toolMetadata }),
|
|
556
|
+
}
|
|
557
|
+
const called: ToolCalledPayload = {
|
|
558
|
+
requestId: options.requestId,
|
|
559
|
+
messageId: options.messageId,
|
|
560
|
+
generationId: options.generationId,
|
|
561
|
+
toolCallId: chunk.toolCallId,
|
|
562
|
+
toolName: chunk.toolName,
|
|
563
|
+
input: chunk.input,
|
|
564
|
+
}
|
|
565
|
+
take(chunk.toolCallId)
|
|
566
|
+
result.push(toolCalledEvent<D>(called), {
|
|
567
|
+
type: 'ai.tool.result',
|
|
568
|
+
id: resultId,
|
|
569
|
+
payload,
|
|
570
|
+
} as AppendInput<D>)
|
|
571
|
+
} else if (chunk.type === 'tool-output-available') {
|
|
572
|
+
flush(chunk.toolCallId)
|
|
573
|
+
const payload: ToolResultPayload = {
|
|
574
|
+
requestId: options.requestId,
|
|
575
|
+
messageId: options.messageId,
|
|
576
|
+
generationId: options.generationId,
|
|
577
|
+
toolCallId: chunk.toolCallId,
|
|
578
|
+
output: chunk.output,
|
|
579
|
+
phase: 'execution',
|
|
580
|
+
...(chunk.preliminary === undefined
|
|
581
|
+
? {}
|
|
582
|
+
: { preliminary: chunk.preliminary }),
|
|
583
|
+
...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
|
|
584
|
+
...(chunk.providerExecuted === undefined
|
|
585
|
+
? {}
|
|
586
|
+
: { providerExecuted: chunk.providerExecuted }),
|
|
587
|
+
...(chunk.providerMetadata === undefined
|
|
588
|
+
? {}
|
|
589
|
+
: { providerMetadata: chunk.providerMetadata }),
|
|
590
|
+
...(chunk.toolMetadata === undefined
|
|
591
|
+
? {}
|
|
592
|
+
: { toolMetadata: chunk.toolMetadata }),
|
|
593
|
+
}
|
|
594
|
+
result.push({
|
|
595
|
+
type: 'ai.tool.result',
|
|
596
|
+
id: resultId,
|
|
597
|
+
payload,
|
|
598
|
+
} as AppendInput<D>)
|
|
599
|
+
} else if (chunk.type === 'tool-output-error') {
|
|
600
|
+
flush(chunk.toolCallId)
|
|
601
|
+
const payload: ToolResultPayload = {
|
|
602
|
+
requestId: options.requestId,
|
|
603
|
+
messageId: options.messageId,
|
|
604
|
+
generationId: options.generationId,
|
|
605
|
+
toolCallId: chunk.toolCallId,
|
|
606
|
+
error: chunk.errorText,
|
|
607
|
+
phase: 'execution',
|
|
608
|
+
...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
|
|
609
|
+
...(chunk.providerExecuted === undefined
|
|
610
|
+
? {}
|
|
611
|
+
: { providerExecuted: chunk.providerExecuted }),
|
|
612
|
+
...(chunk.providerMetadata === undefined
|
|
613
|
+
? {}
|
|
614
|
+
: { providerMetadata: chunk.providerMetadata }),
|
|
615
|
+
...(chunk.toolMetadata === undefined
|
|
616
|
+
? {}
|
|
617
|
+
: { toolMetadata: chunk.toolMetadata }),
|
|
618
|
+
}
|
|
619
|
+
result.push({
|
|
620
|
+
type: 'ai.tool.result',
|
|
621
|
+
id: resultId,
|
|
622
|
+
payload,
|
|
623
|
+
} as AppendInput<D>)
|
|
624
|
+
} else if (chunk.type === 'tool-output-denied') {
|
|
625
|
+
flush(chunk.toolCallId)
|
|
626
|
+
const payload: ToolResultPayload = {
|
|
627
|
+
requestId: options.requestId,
|
|
628
|
+
messageId: options.messageId,
|
|
629
|
+
generationId: options.generationId,
|
|
630
|
+
toolCallId: chunk.toolCallId,
|
|
631
|
+
denied: true,
|
|
632
|
+
phase: 'execution',
|
|
633
|
+
}
|
|
634
|
+
result.push({
|
|
635
|
+
type: 'ai.tool.result',
|
|
636
|
+
id: resultId,
|
|
637
|
+
payload,
|
|
638
|
+
} as AppendInput<D>)
|
|
639
|
+
} else if (chunk.type === 'tool-approval-request') {
|
|
640
|
+
flush(chunk.toolCallId)
|
|
641
|
+
const payload: ApprovalRequestedPayload = {
|
|
642
|
+
messageId: options.messageId,
|
|
643
|
+
generationId: options.generationId,
|
|
644
|
+
approvalId: chunk.approvalId,
|
|
645
|
+
toolCallId: chunk.toolCallId,
|
|
646
|
+
...(chunk.isAutomatic === undefined
|
|
647
|
+
? {}
|
|
648
|
+
: { isAutomatic: chunk.isAutomatic }),
|
|
649
|
+
...(chunk.signature === undefined
|
|
650
|
+
? {}
|
|
651
|
+
: { signature: chunk.signature }),
|
|
652
|
+
}
|
|
653
|
+
result.push({
|
|
654
|
+
type: 'ai.approval.requested',
|
|
655
|
+
id: `${options.generationId}:approval:${chunk.approvalId}`,
|
|
656
|
+
payload,
|
|
657
|
+
} as AppendInput<D>)
|
|
658
|
+
} else if (chunk.type === 'tool-approval-response') {
|
|
659
|
+
result.push({
|
|
660
|
+
type: 'ai.approval.responded',
|
|
661
|
+
id: `${options.generationId}:approval:${chunk.approvalId}:automatic-response`,
|
|
662
|
+
payload: {
|
|
663
|
+
messageId: options.messageId,
|
|
664
|
+
generationId: options.generationId,
|
|
665
|
+
approvalId: chunk.approvalId,
|
|
666
|
+
approved: chunk.approved,
|
|
667
|
+
...(chunk.reason === undefined ? {} : { reason: chunk.reason }),
|
|
668
|
+
},
|
|
669
|
+
} as AppendInput<D>)
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return { events: result, pending }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function queuedMessagesAt<D extends EventDefs>(
|
|
676
|
+
history: ContractEvent<D>[],
|
|
677
|
+
): AICoordinatorState['queued'] {
|
|
678
|
+
const queued: AICoordinatorState['queued'] = []
|
|
679
|
+
for (const event of history) {
|
|
680
|
+
if (event.type === 'ai.message.created') {
|
|
681
|
+
const payload = event.payload as MessageCreatedPayload<UIMessage>
|
|
682
|
+
if (payload.message.role !== 'user') continue
|
|
683
|
+
const duplicate = queued.findIndex(
|
|
684
|
+
(item) => item.messageId === payload.message.id,
|
|
685
|
+
)
|
|
686
|
+
if (duplicate !== -1) queued.splice(duplicate, 1)
|
|
687
|
+
queued.push({
|
|
688
|
+
index: event.index,
|
|
689
|
+
messageId: payload.message.id,
|
|
690
|
+
generate: payload.generate !== false,
|
|
691
|
+
})
|
|
692
|
+
continue
|
|
693
|
+
}
|
|
694
|
+
if (event.type !== 'ai.generation.requested') continue
|
|
695
|
+
const request = event.payload as GenerationRequestedPayload
|
|
696
|
+
if (request.reason !== 'message') continue
|
|
697
|
+
const requested = queued.findIndex(
|
|
698
|
+
(item) => item.messageId === request.messageId,
|
|
699
|
+
)
|
|
700
|
+
if (requested !== -1) queued.splice(0, requested + 1)
|
|
701
|
+
}
|
|
702
|
+
return queued
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function withoutGenerationLifecycle<D extends EventDefs>(
|
|
706
|
+
history: ContractEvent<D>[],
|
|
707
|
+
generationIds: ReadonlySet<string>,
|
|
708
|
+
): ContractEvent<D>[] {
|
|
709
|
+
if (generationIds.size === 0) return history
|
|
710
|
+
const approvalKeys = new Set<string>()
|
|
711
|
+
const inputKeys = new Set<string>()
|
|
712
|
+
for (const event of history) {
|
|
713
|
+
const payload = event.payload as {
|
|
714
|
+
generationId?: unknown
|
|
715
|
+
messageId?: unknown
|
|
716
|
+
approvalId?: unknown
|
|
717
|
+
inputId?: unknown
|
|
718
|
+
}
|
|
719
|
+
if (
|
|
720
|
+
typeof payload.generationId !== 'string' ||
|
|
721
|
+
!generationIds.has(payload.generationId) ||
|
|
722
|
+
typeof payload.messageId !== 'string'
|
|
723
|
+
) {
|
|
724
|
+
continue
|
|
725
|
+
}
|
|
726
|
+
if (
|
|
727
|
+
event.type === 'ai.approval.requested' &&
|
|
728
|
+
typeof payload.approvalId === 'string'
|
|
729
|
+
) {
|
|
730
|
+
approvalKeys.add(`${payload.messageId}\u001f${payload.approvalId}`)
|
|
731
|
+
}
|
|
732
|
+
if (
|
|
733
|
+
event.type === 'ai.input.requested' &&
|
|
734
|
+
typeof payload.inputId === 'string'
|
|
735
|
+
) {
|
|
736
|
+
inputKeys.add(`${payload.messageId}\u001f${payload.inputId}`)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return history.filter((event) => {
|
|
740
|
+
const payload = event.payload as {
|
|
741
|
+
generationId?: unknown
|
|
742
|
+
messageId?: unknown
|
|
743
|
+
approvalId?: unknown
|
|
744
|
+
inputId?: unknown
|
|
745
|
+
}
|
|
746
|
+
if (
|
|
747
|
+
typeof payload.generationId === 'string' &&
|
|
748
|
+
generationIds.has(payload.generationId)
|
|
749
|
+
) {
|
|
750
|
+
return false
|
|
751
|
+
}
|
|
752
|
+
if (
|
|
753
|
+
event.type === 'ai.approval.responded' &&
|
|
754
|
+
typeof payload.messageId === 'string' &&
|
|
755
|
+
typeof payload.approvalId === 'string'
|
|
756
|
+
) {
|
|
757
|
+
return !approvalKeys.has(
|
|
758
|
+
`${payload.messageId}\u001f${payload.approvalId}`,
|
|
759
|
+
)
|
|
760
|
+
}
|
|
761
|
+
if (
|
|
762
|
+
event.type === 'ai.input.responded' &&
|
|
763
|
+
typeof payload.messageId === 'string' &&
|
|
764
|
+
typeof payload.inputId === 'string'
|
|
765
|
+
) {
|
|
766
|
+
return !inputKeys.has(`${payload.messageId}\u001f${payload.inputId}`)
|
|
767
|
+
}
|
|
768
|
+
return true
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Build the ordinary A2 handler table for the built-in agent protocol.
|
|
774
|
+
* Application handlers can be spread beside this table.
|
|
775
|
+
*/
|
|
776
|
+
export function createHandlers<
|
|
777
|
+
M extends UIMessage,
|
|
778
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
779
|
+
T extends ToolSet = ToolSet,
|
|
780
|
+
>(
|
|
781
|
+
options: CreateHandlersOptions<M, D, T>,
|
|
782
|
+
): NonNullable<ServerOptions<D>['handlers']> {
|
|
783
|
+
if (options.model === undefined) {
|
|
784
|
+
throw new TypeError('createHandlers requires model')
|
|
785
|
+
}
|
|
786
|
+
if (
|
|
787
|
+
options.progress?.maxChunks !== undefined &&
|
|
788
|
+
(!Number.isInteger(options.progress.maxChunks) ||
|
|
789
|
+
options.progress.maxChunks < 1)
|
|
790
|
+
) {
|
|
791
|
+
throw new TypeError('progress.maxChunks must be a positive integer')
|
|
792
|
+
}
|
|
793
|
+
if (
|
|
794
|
+
options.progress?.maxDelayMs !== undefined &&
|
|
795
|
+
(!Number.isFinite(options.progress.maxDelayMs) ||
|
|
796
|
+
options.progress.maxDelayMs < 0)
|
|
797
|
+
) {
|
|
798
|
+
throw new TypeError('progress.maxDelayMs must be a non-negative number')
|
|
799
|
+
}
|
|
800
|
+
if (
|
|
801
|
+
options.maxSteps !== undefined &&
|
|
802
|
+
options.maxSteps !== Number.POSITIVE_INFINITY &&
|
|
803
|
+
(!Number.isInteger(options.maxSteps) || options.maxSteps < 1)
|
|
804
|
+
) {
|
|
805
|
+
throw new TypeError('maxSteps must be a positive integer or Infinity')
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const tools = options.tools ?? ({} as T)
|
|
809
|
+
const generation = options.generation ?? {}
|
|
810
|
+
const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
|
|
811
|
+
const coordinator = aiCoordinatorReducer(options.agent.contract)
|
|
812
|
+
const promptCache = new Map<string, Promise<ModelMessage[]>>()
|
|
813
|
+
|
|
814
|
+
const coordinatorStateAt = (
|
|
815
|
+
history: ContractEvent<D>[],
|
|
816
|
+
frontier = Number.POSITIVE_INFINITY,
|
|
817
|
+
): AICoordinatorState => {
|
|
818
|
+
let state = coordinator.initialState
|
|
819
|
+
for (const event of history) {
|
|
820
|
+
if (event.index >= frontier) break
|
|
821
|
+
state = coordinator.fold(state, event)
|
|
822
|
+
}
|
|
823
|
+
return state
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const requestForNextMessage = (
|
|
827
|
+
state: AICoordinatorState,
|
|
828
|
+
): AppendInput<D> | undefined => {
|
|
829
|
+
if (state.closed || state.response !== undefined) return undefined
|
|
830
|
+
const next = state.queued.find((item) => item.generate !== false)
|
|
831
|
+
if (next === undefined) return undefined
|
|
832
|
+
return {
|
|
833
|
+
type: 'ai.generation.requested',
|
|
834
|
+
id: `ai.generate:message:${next.messageId}`,
|
|
835
|
+
payload: { messageId: next.messageId, reason: 'message' },
|
|
836
|
+
} as AppendInput<D>
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const scheduleNext = async (
|
|
840
|
+
ctx: Pick<HandlerContext<D>, 'session'>,
|
|
841
|
+
): Promise<AppendInput<D> | void> =>
|
|
842
|
+
requestForNextMessage((await ctx.session.state(coordinator)).state)
|
|
843
|
+
|
|
844
|
+
const continueIfReady = async (
|
|
845
|
+
ctx: Pick<HandlerContext<D>, 'session'>,
|
|
846
|
+
generationId: string,
|
|
847
|
+
): Promise<void> => {
|
|
848
|
+
const state = (await ctx.session.state(coordinator)).state
|
|
849
|
+
const response = state.response
|
|
850
|
+
if (response?.generation?.generationId !== generationId) {
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
const input = response.inputResponse
|
|
854
|
+
const inputReady =
|
|
855
|
+
response.completion?.generationId === generationId &&
|
|
856
|
+
response.failure === undefined &&
|
|
857
|
+
input !== undefined &&
|
|
858
|
+
response.inputs.length === 0 &&
|
|
859
|
+
response.calls.every(
|
|
860
|
+
(call) =>
|
|
861
|
+
call.terminal ||
|
|
862
|
+
(call.call.providerExecuted === true &&
|
|
863
|
+
call.call.supportsDeferredResults !== true &&
|
|
864
|
+
call.approval !== undefined &&
|
|
865
|
+
call.response !== undefined),
|
|
866
|
+
)
|
|
867
|
+
if (inputReady) {
|
|
868
|
+
await ctx.session.append('continue-after-input', {
|
|
869
|
+
type: 'ai.generation.requested',
|
|
870
|
+
id: `ai.generate:input:${encodeURIComponent(response.responseMessageId)}:${encodeURIComponent(input.generationId)}:${encodeURIComponent(input.inputId)}`,
|
|
871
|
+
payload: {
|
|
872
|
+
messageId: response.responseMessageId,
|
|
873
|
+
responseMessageId: response.responseMessageId,
|
|
874
|
+
reason: 'input',
|
|
875
|
+
},
|
|
876
|
+
} as AppendInput<D>)
|
|
877
|
+
return
|
|
878
|
+
}
|
|
879
|
+
if (!continuationReady(state)) return
|
|
880
|
+
await ctx.session.append('continue-after-tools', {
|
|
881
|
+
type: 'ai.generation.requested',
|
|
882
|
+
id: `ai.generate:tools:${generationId}`,
|
|
883
|
+
payload: {
|
|
884
|
+
messageId: response.responseMessageId,
|
|
885
|
+
responseMessageId: response.responseMessageId,
|
|
886
|
+
reason: 'tool',
|
|
887
|
+
},
|
|
888
|
+
} as AppendInput<D>)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
type RuntimeTool = {
|
|
892
|
+
execute?: (
|
|
893
|
+
input: unknown,
|
|
894
|
+
options: {
|
|
895
|
+
toolCallId: string
|
|
896
|
+
messages: ModelMessage[]
|
|
897
|
+
abortSignal?: AbortSignal
|
|
898
|
+
},
|
|
899
|
+
) => unknown
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
type ToolHandlerContext =
|
|
903
|
+
| HandlerContext<D, 'ai.tool.called'>
|
|
904
|
+
| HandlerContext<D, 'ai.approval.responded'>
|
|
905
|
+
|
|
906
|
+
const resultEvent = (
|
|
907
|
+
call: ToolCalledPayload,
|
|
908
|
+
suffix: string,
|
|
909
|
+
result: {
|
|
910
|
+
output?: unknown
|
|
911
|
+
error?: string
|
|
912
|
+
denied?: boolean
|
|
913
|
+
preliminary?: boolean
|
|
914
|
+
},
|
|
915
|
+
): AppendInput<D> =>
|
|
916
|
+
({
|
|
917
|
+
type: 'ai.tool.result',
|
|
918
|
+
id: `${call.generationId}:tool:${call.toolCallId}:${suffix}`,
|
|
919
|
+
payload: {
|
|
920
|
+
requestId: call.requestId,
|
|
921
|
+
messageId: call.messageId,
|
|
922
|
+
generationId: call.generationId,
|
|
923
|
+
toolCallId: call.toolCallId,
|
|
924
|
+
toolName: call.toolName,
|
|
925
|
+
input: call.input,
|
|
926
|
+
phase: 'execution',
|
|
927
|
+
...(result.output === undefined ? {} : { output: result.output }),
|
|
928
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
929
|
+
...(result.denied === undefined ? {} : { denied: result.denied }),
|
|
930
|
+
...(result.preliminary === undefined
|
|
931
|
+
? {}
|
|
932
|
+
: { preliminary: result.preliminary }),
|
|
933
|
+
...(call.dynamic === undefined ? {} : { dynamic: call.dynamic }),
|
|
934
|
+
...(call.providerExecuted === undefined
|
|
935
|
+
? {}
|
|
936
|
+
: { providerExecuted: call.providerExecuted }),
|
|
937
|
+
...(call.toolMetadata === undefined
|
|
938
|
+
? {}
|
|
939
|
+
: { toolMetadata: call.toolMetadata }),
|
|
940
|
+
},
|
|
941
|
+
}) as AppendInput<D>
|
|
942
|
+
|
|
943
|
+
const promptMessages = (
|
|
944
|
+
sessionId: string,
|
|
945
|
+
call: ToolCalledPayload,
|
|
946
|
+
readHistory: () => Promise<ContractEvent<D>[]>,
|
|
947
|
+
): Promise<ModelMessage[]> => {
|
|
948
|
+
const key = promptCacheKey(sessionId, call.generationId)
|
|
949
|
+
const cached = promptCache.get(key)
|
|
950
|
+
if (cached) return cached
|
|
951
|
+
const computation = (async () => {
|
|
952
|
+
const history = await readHistory()
|
|
953
|
+
const coordinatorState = coordinatorStateAt(history)
|
|
954
|
+
const compaction = history.find(
|
|
955
|
+
(event) =>
|
|
956
|
+
event.type === 'ai.compaction.completed' &&
|
|
957
|
+
(event.payload as CompactionCompletedPayload<M>).generationId ===
|
|
958
|
+
call.generationId,
|
|
959
|
+
)
|
|
960
|
+
if (compaction !== undefined) {
|
|
961
|
+
const messages = (
|
|
962
|
+
compaction.payload as CompactionCompletedPayload<M>
|
|
963
|
+
).messages.filter(
|
|
964
|
+
(message) =>
|
|
965
|
+
!coordinatorState.queued.some(
|
|
966
|
+
(queued) => queued.messageId === message.id,
|
|
967
|
+
),
|
|
968
|
+
)
|
|
969
|
+
return convertToModelMessages(messages, { tools })
|
|
970
|
+
}
|
|
971
|
+
const started = history.find(
|
|
972
|
+
(event) =>
|
|
973
|
+
event.type === 'ai.generation.started' &&
|
|
974
|
+
(event.payload as GenerationStartedPayload).generationId ===
|
|
975
|
+
call.generationId,
|
|
976
|
+
)
|
|
977
|
+
const frontier = started?.index ?? Number.POSITIVE_INFINITY
|
|
978
|
+
const state = replay(
|
|
979
|
+
options.agent,
|
|
980
|
+
history.filter((event) => event.index < frontier),
|
|
981
|
+
)
|
|
982
|
+
return convertToModelMessages(
|
|
983
|
+
activeContextMessages(state, coordinatorState),
|
|
984
|
+
{ tools },
|
|
985
|
+
)
|
|
986
|
+
})()
|
|
987
|
+
promptCache.set(key, computation)
|
|
988
|
+
void computation.catch(() => {
|
|
989
|
+
if (promptCache.get(key) === computation) promptCache.delete(key)
|
|
990
|
+
})
|
|
991
|
+
return computation
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const toolExecutionFailure = (
|
|
995
|
+
ctx: ToolHandlerContext,
|
|
996
|
+
call: ToolCalledPayload,
|
|
997
|
+
error: unknown,
|
|
998
|
+
): AppendInput<D> | void => {
|
|
999
|
+
const schedulerFailure = consumeSchedulerSendFailure(error)
|
|
1000
|
+
if (ctx.signal.aborted) return
|
|
1001
|
+
if (schedulerFailure === 'retryable') throw error
|
|
1002
|
+
return resultEvent(call, 'execution:error', {
|
|
1003
|
+
error: errorMessage(error),
|
|
1004
|
+
})
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const runToolExecution = async (
|
|
1008
|
+
ctx: ToolHandlerContext,
|
|
1009
|
+
call: ToolCalledPayload,
|
|
1010
|
+
execute: NonNullable<RuntimeTool['execute']>,
|
|
1011
|
+
messages: ModelMessage[],
|
|
1012
|
+
): Promise<AppendInput<D> | void> => {
|
|
1013
|
+
let output: unknown
|
|
1014
|
+
try {
|
|
1015
|
+
output = await execute(call.input, {
|
|
1016
|
+
toolCallId: call.toolCallId,
|
|
1017
|
+
messages,
|
|
1018
|
+
abortSignal: ctx.signal,
|
|
1019
|
+
})
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
return toolExecutionFailure(ctx, call, error)
|
|
1022
|
+
}
|
|
1023
|
+
let iterator: AsyncIterator<unknown> | undefined
|
|
1024
|
+
try {
|
|
1025
|
+
iterator = asyncIterator(output)
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
return toolExecutionFailure(ctx, call, error)
|
|
1028
|
+
}
|
|
1029
|
+
if (iterator === undefined) {
|
|
1030
|
+
return resultEvent(call, 'execution:0', { output })
|
|
1031
|
+
}
|
|
1032
|
+
let last: unknown
|
|
1033
|
+
let sequence = 0
|
|
1034
|
+
for (;;) {
|
|
1035
|
+
let result: IteratorResult<unknown>
|
|
1036
|
+
try {
|
|
1037
|
+
result = await iterator.next()
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
return toolExecutionFailure(ctx, call, error)
|
|
1040
|
+
}
|
|
1041
|
+
if (result.done) break
|
|
1042
|
+
if (ctx.signal.aborted) {
|
|
1043
|
+
try {
|
|
1044
|
+
await iterator.return?.()
|
|
1045
|
+
} catch {
|
|
1046
|
+
// The aborted handler cannot record an iterator cleanup failure.
|
|
1047
|
+
}
|
|
1048
|
+
return
|
|
1049
|
+
}
|
|
1050
|
+
last = result.value
|
|
1051
|
+
try {
|
|
1052
|
+
await ctx.session.append(
|
|
1053
|
+
`tool:${call.toolCallId}:preliminary:${sequence}`,
|
|
1054
|
+
resultEvent(call, `execution:${sequence}:preliminary`, {
|
|
1055
|
+
output: result.value,
|
|
1056
|
+
preliminary: true,
|
|
1057
|
+
}),
|
|
1058
|
+
)
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
try {
|
|
1061
|
+
await iterator.return?.()
|
|
1062
|
+
} catch {
|
|
1063
|
+
// Preserve the append failure that makes the durable handler retry.
|
|
1064
|
+
}
|
|
1065
|
+
throw error
|
|
1066
|
+
}
|
|
1067
|
+
sequence += 1
|
|
1068
|
+
}
|
|
1069
|
+
return resultEvent(
|
|
1070
|
+
call,
|
|
1071
|
+
`execution:${sequence}:final`,
|
|
1072
|
+
sequence === 0 ? {} : { output: last },
|
|
1073
|
+
)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const executeTool = async (
|
|
1077
|
+
ctx: ToolHandlerContext,
|
|
1078
|
+
call: ToolCalledPayload,
|
|
1079
|
+
): Promise<AppendInput<D> | void> => {
|
|
1080
|
+
const tool = tools[call.toolName] as RuntimeTool | undefined
|
|
1081
|
+
const execute = tool?.execute
|
|
1082
|
+
if (execute === undefined) {
|
|
1083
|
+
return resultEvent(call, 'execution:error', {
|
|
1084
|
+
error: `Tool '${call.toolName}' has no server executor`,
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
const messages = await promptMessages(
|
|
1088
|
+
ctx.event.sessionId,
|
|
1089
|
+
call,
|
|
1090
|
+
ctx.session.history,
|
|
1091
|
+
)
|
|
1092
|
+
const scope: AmbientToolScope = {
|
|
1093
|
+
contract: options.agent.contract,
|
|
1094
|
+
context: ctx,
|
|
1095
|
+
}
|
|
1096
|
+
// Async iteration resumes in the caller's context, so the whole
|
|
1097
|
+
// execution, not just the execute call, runs inside the scope.
|
|
1098
|
+
return toolScopeStorage.run(scope, () =>
|
|
1099
|
+
runToolExecution(ctx, call, execute, messages),
|
|
1100
|
+
)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const handleToolCall = async (
|
|
1104
|
+
ctx: HandlerContext<D, 'ai.tool.called'>,
|
|
1105
|
+
): Promise<AppendInput<D> | void> => {
|
|
1106
|
+
const state = (await ctx.session.state(coordinator)).state
|
|
1107
|
+
const response = state.response
|
|
1108
|
+
const current = response?.calls.find(
|
|
1109
|
+
(candidate) => candidate.index === ctx.event.index,
|
|
1110
|
+
)
|
|
1111
|
+
if (
|
|
1112
|
+
current === undefined ||
|
|
1113
|
+
response?.generation?.generationId !== ctx.event.payload.generationId ||
|
|
1114
|
+
response.failure !== undefined ||
|
|
1115
|
+
current.terminal ||
|
|
1116
|
+
current.approval !== undefined
|
|
1117
|
+
) {
|
|
1118
|
+
return
|
|
1119
|
+
}
|
|
1120
|
+
if (current.call.providerExecuted === true) {
|
|
1121
|
+
await continueIfReady(ctx, current.call.generationId)
|
|
1122
|
+
return
|
|
1123
|
+
}
|
|
1124
|
+
return executeTool(ctx, current.call)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const handleApproval = async (
|
|
1128
|
+
ctx: HandlerContext<D, 'ai.approval.responded'>,
|
|
1129
|
+
): Promise<AppendInput<D> | void> => {
|
|
1130
|
+
const state = (await ctx.session.state(coordinator)).state
|
|
1131
|
+
const response = state.response
|
|
1132
|
+
const current = response?.calls.find(
|
|
1133
|
+
(candidate) =>
|
|
1134
|
+
candidate.approval?.approvalId === ctx.event.payload.approvalId &&
|
|
1135
|
+
candidate.approval.messageId === ctx.event.payload.messageId &&
|
|
1136
|
+
candidate.approval.generationId === ctx.event.payload.generationId &&
|
|
1137
|
+
candidate.responseIndex === ctx.event.index,
|
|
1138
|
+
)
|
|
1139
|
+
if (
|
|
1140
|
+
current === undefined ||
|
|
1141
|
+
response?.generation?.generationId !== current.call.generationId ||
|
|
1142
|
+
response.failure !== undefined ||
|
|
1143
|
+
current.terminal
|
|
1144
|
+
) {
|
|
1145
|
+
return
|
|
1146
|
+
}
|
|
1147
|
+
if (current.call.providerExecuted === true) {
|
|
1148
|
+
await continueIfReady(ctx, current.call.generationId)
|
|
1149
|
+
return
|
|
1150
|
+
}
|
|
1151
|
+
if (!ctx.event.payload.approved) {
|
|
1152
|
+
return resultEvent(current.call, 'execution:denied', { denied: true })
|
|
1153
|
+
}
|
|
1154
|
+
return executeTool(ctx, current.call)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const generationHandler = async (
|
|
1158
|
+
ctx: HandlerContext<D, 'ai.generation.requested'>,
|
|
1159
|
+
): Promise<void | AppendInput<D> | readonly AppendInput<D>[]> => {
|
|
1160
|
+
const history = await ctx.session.history()
|
|
1161
|
+
const requestId = ctx.event.id
|
|
1162
|
+
const request = ctx.event.payload
|
|
1163
|
+
const coordinatorState = (await ctx.session.state(coordinator)).state
|
|
1164
|
+
if (
|
|
1165
|
+
coordinatorState.closed ||
|
|
1166
|
+
coordinatorState.response?.activeRequestId !== requestId
|
|
1167
|
+
) {
|
|
1168
|
+
return
|
|
1169
|
+
}
|
|
1170
|
+
const terminal = history.some((event) => {
|
|
1171
|
+
if (event.type === 'ai.generation.completed') {
|
|
1172
|
+
return (
|
|
1173
|
+
(event.payload as GenerationCompletedPayload).requestId === requestId
|
|
1174
|
+
)
|
|
1175
|
+
}
|
|
1176
|
+
if (event.type !== 'ai.generation.failed') return false
|
|
1177
|
+
const payload = event.payload as GenerationFailedPayload
|
|
1178
|
+
return payload.requestId === requestId && payload.superseded !== true
|
|
1179
|
+
})
|
|
1180
|
+
if (terminal) return
|
|
1181
|
+
|
|
1182
|
+
const previousStarts = history.filter(
|
|
1183
|
+
(event) =>
|
|
1184
|
+
event.type === 'ai.generation.started' &&
|
|
1185
|
+
(event.payload as GenerationStartedPayload).requestId === requestId,
|
|
1186
|
+
)
|
|
1187
|
+
const priorAttempts = previousStarts.map(
|
|
1188
|
+
(event) => (event.payload as GenerationStartedPayload).attempt,
|
|
1189
|
+
)
|
|
1190
|
+
if (priorAttempts.some((priorAttempt) => priorAttempt >= ctx.attempt)) {
|
|
1191
|
+
return
|
|
1192
|
+
}
|
|
1193
|
+
const attempt = ctx.attempt
|
|
1194
|
+
const generationId = `${requestId}:generation:${attempt}`
|
|
1195
|
+
const responseMessageId =
|
|
1196
|
+
request.responseMessageId ??
|
|
1197
|
+
(request.reason === 'message'
|
|
1198
|
+
? `${request.messageId}:assistant`
|
|
1199
|
+
: request.messageId)
|
|
1200
|
+
|
|
1201
|
+
const responseStepCount = history.filter(
|
|
1202
|
+
(event) =>
|
|
1203
|
+
event.type === 'ai.generation.completed' &&
|
|
1204
|
+
(event.payload as GenerationCompletedPayload).responseMessageId ===
|
|
1205
|
+
responseMessageId,
|
|
1206
|
+
).length
|
|
1207
|
+
|
|
1208
|
+
const sourceCoordinatorState = coordinatorStateAt(history, ctx.event.index)
|
|
1209
|
+
if (request.reason === 'tool') {
|
|
1210
|
+
const prefix = 'ai.generate:tools:'
|
|
1211
|
+
if (!requestId.startsWith(prefix)) return
|
|
1212
|
+
const sourceGenerationId = requestId.slice(prefix.length)
|
|
1213
|
+
if (
|
|
1214
|
+
!continuationReady(sourceCoordinatorState) ||
|
|
1215
|
+
sourceCoordinatorState.response?.generation?.generationId !==
|
|
1216
|
+
sourceGenerationId ||
|
|
1217
|
+
sourceCoordinatorState.response.responseMessageId !==
|
|
1218
|
+
request.messageId ||
|
|
1219
|
+
sourceCoordinatorState.response.responseMessageId !==
|
|
1220
|
+
request.responseMessageId
|
|
1221
|
+
) {
|
|
1222
|
+
return
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const previous = previousStarts
|
|
1227
|
+
.filter(
|
|
1228
|
+
(event) =>
|
|
1229
|
+
(event.payload as GenerationStartedPayload).attempt < attempt,
|
|
1230
|
+
)
|
|
1231
|
+
.toSorted(
|
|
1232
|
+
(left, right) =>
|
|
1233
|
+
(right.payload as GenerationStartedPayload).attempt -
|
|
1234
|
+
(left.payload as GenerationStartedPayload).attempt,
|
|
1235
|
+
)[0]
|
|
1236
|
+
|
|
1237
|
+
const incompleteId = previous
|
|
1238
|
+
? (previous.payload as GenerationStartedPayload).generationId
|
|
1239
|
+
: undefined
|
|
1240
|
+
const replacedGenerationIds = new Set<string>()
|
|
1241
|
+
if (incompleteId !== undefined) replacedGenerationIds.add(incompleteId)
|
|
1242
|
+
if (
|
|
1243
|
+
request.reason === 'retry' &&
|
|
1244
|
+
sourceCoordinatorState.response?.failure?.generationId !== undefined
|
|
1245
|
+
) {
|
|
1246
|
+
replacedGenerationIds.add(
|
|
1247
|
+
sourceCoordinatorState.response.failure.generationId,
|
|
1248
|
+
)
|
|
1249
|
+
}
|
|
1250
|
+
const promptHistory = withoutGenerationLifecycle(
|
|
1251
|
+
history,
|
|
1252
|
+
replacedGenerationIds,
|
|
1253
|
+
)
|
|
1254
|
+
let state = replay(options.agent, promptHistory)
|
|
1255
|
+
const resolverContext: AgentResolverContext<M, D> = {
|
|
1256
|
+
event: ctx.event,
|
|
1257
|
+
state,
|
|
1258
|
+
history,
|
|
1259
|
+
signal: ctx.signal,
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const resolvedModel = await resolve(options.model, resolverContext)
|
|
1263
|
+
if (resolvedModel === undefined) {
|
|
1264
|
+
throw new TypeError('the model resolver returned undefined')
|
|
1265
|
+
}
|
|
1266
|
+
if (ctx.signal.aborted) return
|
|
1267
|
+
|
|
1268
|
+
if (request.reason === 'tool' && responseStepCount >= maxSteps) {
|
|
1269
|
+
const source = sourceCoordinatorState.response?.generation
|
|
1270
|
+
if (source === undefined) return
|
|
1271
|
+
return {
|
|
1272
|
+
type: 'ai.generation.failed',
|
|
1273
|
+
id: `${requestId}:step-limit`,
|
|
1274
|
+
payload: {
|
|
1275
|
+
requestId: source.requestId,
|
|
1276
|
+
messageId: source.messageId,
|
|
1277
|
+
generationId: source.generationId,
|
|
1278
|
+
responseMessageId,
|
|
1279
|
+
error: `agent exceeded the ${maxSteps}-step limit`,
|
|
1280
|
+
stepLimit: true,
|
|
1281
|
+
},
|
|
1282
|
+
} as AppendInput<D>
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const started: GenerationStartedPayload = {
|
|
1286
|
+
requestId,
|
|
1287
|
+
messageId: request.messageId,
|
|
1288
|
+
generationId,
|
|
1289
|
+
responseMessageId,
|
|
1290
|
+
attempt,
|
|
1291
|
+
model: modelName(resolvedModel),
|
|
1292
|
+
}
|
|
1293
|
+
const startEvents: AppendInput<D>[] = []
|
|
1294
|
+
if (previous) {
|
|
1295
|
+
const payload = previous.payload as GenerationStartedPayload
|
|
1296
|
+
const superseded: GenerationFailedPayload = {
|
|
1297
|
+
requestId,
|
|
1298
|
+
messageId: payload.messageId,
|
|
1299
|
+
generationId: payload.generationId,
|
|
1300
|
+
responseMessageId: payload.responseMessageId,
|
|
1301
|
+
error: 'generation attempt was superseded after an incomplete run',
|
|
1302
|
+
superseded: true,
|
|
1303
|
+
}
|
|
1304
|
+
startEvents.push({
|
|
1305
|
+
type: 'ai.generation.failed',
|
|
1306
|
+
id: `${payload.generationId}:superseded`,
|
|
1307
|
+
payload: superseded,
|
|
1308
|
+
} as AppendInput<D>)
|
|
1309
|
+
}
|
|
1310
|
+
startEvents.push({
|
|
1311
|
+
type: 'ai.generation.started',
|
|
1312
|
+
id: generationId,
|
|
1313
|
+
payload: started,
|
|
1314
|
+
} as AppendInput<D>)
|
|
1315
|
+
await ctx.session.append('generation-start', ...startEvents)
|
|
1316
|
+
|
|
1317
|
+
const startedCoordinatorState = (await ctx.session.state(coordinator)).state
|
|
1318
|
+
if (
|
|
1319
|
+
startedCoordinatorState.response?.activeRequestId !== requestId ||
|
|
1320
|
+
startedCoordinatorState.response.generation?.generationId !== generationId
|
|
1321
|
+
) {
|
|
1322
|
+
return
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const promptCoordinatorState = {
|
|
1326
|
+
...coordinatorStateAt(history),
|
|
1327
|
+
queued: queuedMessagesAt(history),
|
|
1328
|
+
}
|
|
1329
|
+
let messages = activeContextMessages(state, promptCoordinatorState)
|
|
1330
|
+
if (options.compaction) {
|
|
1331
|
+
const compactionContext = { ...resolverContext, messages }
|
|
1332
|
+
if (await options.compaction.shouldCompact(compactionContext)) {
|
|
1333
|
+
const throughMessageId = messages.at(-1)?.id ?? request.messageId
|
|
1334
|
+
await ctx.session.append('compaction-requested', {
|
|
1335
|
+
type: 'ai.compaction.requested',
|
|
1336
|
+
id: `${generationId}:compaction:requested`,
|
|
1337
|
+
payload: { generationId, throughMessageId },
|
|
1338
|
+
})
|
|
1339
|
+
messages = await options.compaction.compact(compactionContext)
|
|
1340
|
+
const completed: CompactionCompletedPayload<M> = {
|
|
1341
|
+
generationId,
|
|
1342
|
+
throughMessageId,
|
|
1343
|
+
messages,
|
|
1344
|
+
...(promptCoordinatorState.queued.length === 0
|
|
1345
|
+
? {}
|
|
1346
|
+
: {
|
|
1347
|
+
retainedMessageIds: promptCoordinatorState.queued.map(
|
|
1348
|
+
(item) => item.messageId,
|
|
1349
|
+
),
|
|
1350
|
+
}),
|
|
1351
|
+
}
|
|
1352
|
+
await ctx.session.append('compaction-completed', {
|
|
1353
|
+
type: 'ai.compaction.completed',
|
|
1354
|
+
id: `${generationId}:compaction:completed`,
|
|
1355
|
+
payload: completed,
|
|
1356
|
+
})
|
|
1357
|
+
state = {
|
|
1358
|
+
...state,
|
|
1359
|
+
compaction: { status: 'completed', ...completed },
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const currentState = (await ctx.session.state(options.agent.reducer)).state
|
|
1365
|
+
const currentCoordinatorState = (await ctx.session.state(coordinator)).state
|
|
1366
|
+
const generationMessages = activeContextMessages(
|
|
1367
|
+
currentState,
|
|
1368
|
+
currentCoordinatorState,
|
|
1369
|
+
)
|
|
1370
|
+
promptCache.set(
|
|
1371
|
+
promptCacheKey(ctx.event.sessionId, generationId),
|
|
1372
|
+
convertToModelMessages(generationMessages, { tools }),
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
let pendingToolCalls: PendingToolCall[] = []
|
|
1376
|
+
try {
|
|
1377
|
+
const resolvedInstructions =
|
|
1378
|
+
options.instructions === undefined
|
|
1379
|
+
? undefined
|
|
1380
|
+
: await resolve(options.instructions, resolverContext)
|
|
1381
|
+
const generateContext: AgentGenerateContext<M, D, T> = {
|
|
1382
|
+
request: ctx.event,
|
|
1383
|
+
requestId,
|
|
1384
|
+
generationId,
|
|
1385
|
+
responseMessageId,
|
|
1386
|
+
messages: generationMessages,
|
|
1387
|
+
state: currentState,
|
|
1388
|
+
history,
|
|
1389
|
+
signal: ctx.signal,
|
|
1390
|
+
model: resolvedModel,
|
|
1391
|
+
tools,
|
|
1392
|
+
...(resolvedInstructions === undefined
|
|
1393
|
+
? {}
|
|
1394
|
+
: { instructions: resolvedInstructions }),
|
|
1395
|
+
generation: options.generation ?? {},
|
|
1396
|
+
}
|
|
1397
|
+
const custom = options.generate !== undefined
|
|
1398
|
+
const source =
|
|
1399
|
+
options.generate === undefined
|
|
1400
|
+
? await generateWithAISDK(generateContext, options.messageMetadata)
|
|
1401
|
+
: { stream: await options.generate(generateContext) }
|
|
1402
|
+
const updates = consumeGeneration({
|
|
1403
|
+
source,
|
|
1404
|
+
...(options.progress === undefined
|
|
1405
|
+
? {}
|
|
1406
|
+
: { progress: options.progress }),
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
let sequence = 0
|
|
1410
|
+
let finish: AgentGenerationFinish | undefined
|
|
1411
|
+
for await (const update of updates) {
|
|
1412
|
+
ctx.signal.throwIfAborted()
|
|
1413
|
+
if (update.type === 'finish') {
|
|
1414
|
+
finish = update
|
|
1415
|
+
continue
|
|
1416
|
+
}
|
|
1417
|
+
const progress: GenerationProgressPayload = {
|
|
1418
|
+
requestId,
|
|
1419
|
+
messageId: request.messageId,
|
|
1420
|
+
generationId,
|
|
1421
|
+
responseMessageId,
|
|
1422
|
+
sequence,
|
|
1423
|
+
chunks: update.chunks,
|
|
1424
|
+
}
|
|
1425
|
+
const lifecycle = lifecycleEvents<D, T>({
|
|
1426
|
+
requestId,
|
|
1427
|
+
messageId: responseMessageId,
|
|
1428
|
+
generationId,
|
|
1429
|
+
sequence,
|
|
1430
|
+
chunks: update.chunks,
|
|
1431
|
+
pending: pendingToolCalls,
|
|
1432
|
+
tools,
|
|
1433
|
+
generation,
|
|
1434
|
+
custom,
|
|
1435
|
+
})
|
|
1436
|
+
pendingToolCalls = lifecycle.pending
|
|
1437
|
+
await ctx.session.append(
|
|
1438
|
+
`generation-progress:${sequence}`,
|
|
1439
|
+
{
|
|
1440
|
+
type: 'ai.generation.progress',
|
|
1441
|
+
id: `${generationId}:progress:${sequence}`,
|
|
1442
|
+
payload: progress,
|
|
1443
|
+
},
|
|
1444
|
+
...lifecycle.events,
|
|
1445
|
+
)
|
|
1446
|
+
sequence += 1
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (!finish) throw new Error('agent generation finished without output')
|
|
1450
|
+
const completed: GenerationCompletedPayload = {
|
|
1451
|
+
requestId,
|
|
1452
|
+
messageId: request.messageId,
|
|
1453
|
+
generationId,
|
|
1454
|
+
responseMessageId,
|
|
1455
|
+
...(finish.finishReason === undefined
|
|
1456
|
+
? {}
|
|
1457
|
+
: { finishReason: finish.finishReason }),
|
|
1458
|
+
...(finish.usage === undefined ? {} : { usage: finish.usage }),
|
|
1459
|
+
}
|
|
1460
|
+
const completionEvent = {
|
|
1461
|
+
type: 'ai.generation.completed',
|
|
1462
|
+
id: `${generationId}:completed`,
|
|
1463
|
+
payload: completed,
|
|
1464
|
+
} as AppendInput<D>
|
|
1465
|
+
const unresolved = options.generate
|
|
1466
|
+
? pendingToolCalls.filter(
|
|
1467
|
+
(pending) => pending.classification !== 'automatic',
|
|
1468
|
+
)
|
|
1469
|
+
: []
|
|
1470
|
+
if (unresolved.length > 0) {
|
|
1471
|
+
throw new Error(
|
|
1472
|
+
`generation ended with unresolved tool authorization for '${unresolved[0]!.call.toolName}'`,
|
|
1473
|
+
)
|
|
1474
|
+
}
|
|
1475
|
+
const pendingEvents = pendingToolCalls
|
|
1476
|
+
.filter(
|
|
1477
|
+
(pending) =>
|
|
1478
|
+
pending.classification === 'automatic' ||
|
|
1479
|
+
(options.generate === undefined &&
|
|
1480
|
+
pending.classification === 'unknown'),
|
|
1481
|
+
)
|
|
1482
|
+
.map((pending) => toolCalledEvent<D>(pending.call))
|
|
1483
|
+
if (finish.finishReason === 'tool-calls') {
|
|
1484
|
+
return [...pendingEvents, completionEvent]
|
|
1485
|
+
}
|
|
1486
|
+
return [
|
|
1487
|
+
...pendingEvents,
|
|
1488
|
+
completionEvent,
|
|
1489
|
+
{
|
|
1490
|
+
type: 'ai.message.completed',
|
|
1491
|
+
id: `${generationId}:message:completed`,
|
|
1492
|
+
payload: {
|
|
1493
|
+
messageId: responseMessageId,
|
|
1494
|
+
} satisfies MessageCompletedPayload,
|
|
1495
|
+
} as AppendInput<D>,
|
|
1496
|
+
]
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
if (error instanceof A2Error) throw error
|
|
1499
|
+
if (ctx.signal.aborted) {
|
|
1500
|
+
const interrupted: MessageInterruptedPayload = {
|
|
1501
|
+
messageId: responseMessageId,
|
|
1502
|
+
generationId,
|
|
1503
|
+
reason: 'aborted',
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
type: 'ai.message.interrupted',
|
|
1507
|
+
id: `${generationId}:interrupted`,
|
|
1508
|
+
payload: interrupted,
|
|
1509
|
+
} as AppendInput<D>
|
|
1510
|
+
}
|
|
1511
|
+
const failed: GenerationFailedPayload = {
|
|
1512
|
+
requestId,
|
|
1513
|
+
messageId: request.messageId,
|
|
1514
|
+
generationId,
|
|
1515
|
+
responseMessageId,
|
|
1516
|
+
error: errorMessage(error),
|
|
1517
|
+
}
|
|
1518
|
+
return {
|
|
1519
|
+
type: 'ai.generation.failed',
|
|
1520
|
+
id: `${generationId}:failed`,
|
|
1521
|
+
payload: failed,
|
|
1522
|
+
} as AppendInput<D>
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const handleMessageCreated = async (
|
|
1527
|
+
ctx: HandlerContext<D, 'ai.message.created'>,
|
|
1528
|
+
): Promise<AppendInput<D> | void> => {
|
|
1529
|
+
if (
|
|
1530
|
+
ctx.event.payload.message.role !== 'user' ||
|
|
1531
|
+
ctx.event.payload.generate === false
|
|
1532
|
+
) {
|
|
1533
|
+
return
|
|
1534
|
+
}
|
|
1535
|
+
return scheduleNext(ctx)
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const handleRetry = async (
|
|
1539
|
+
ctx: HandlerContext<D, 'ai.retry.requested'>,
|
|
1540
|
+
): Promise<AppendInput<D> | void> => {
|
|
1541
|
+
const state = (await ctx.session.state(coordinator)).state
|
|
1542
|
+
const response = state.response
|
|
1543
|
+
if (
|
|
1544
|
+
response?.status !== 'failed' ||
|
|
1545
|
+
response.rootMessageId !== ctx.event.payload.messageId ||
|
|
1546
|
+
response.responseMessageId !== ctx.event.payload.responseMessageId
|
|
1547
|
+
) {
|
|
1548
|
+
return
|
|
1549
|
+
}
|
|
1550
|
+
return {
|
|
1551
|
+
type: 'ai.generation.requested',
|
|
1552
|
+
id: `ai.generate:retry:${ctx.event.payload.retryId}`,
|
|
1553
|
+
payload: {
|
|
1554
|
+
messageId: response.rootMessageId,
|
|
1555
|
+
responseMessageId: response.responseMessageId,
|
|
1556
|
+
reason: 'retry',
|
|
1557
|
+
},
|
|
1558
|
+
} as AppendInput<D>
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const handleInputResponse = async (
|
|
1562
|
+
ctx: HandlerContext<D, 'ai.input.responded'>,
|
|
1563
|
+
): Promise<void> => {
|
|
1564
|
+
const state = (await ctx.session.state(coordinator)).state
|
|
1565
|
+
const response = state.response
|
|
1566
|
+
if (
|
|
1567
|
+
response?.responseMessageId !== ctx.event.payload.messageId ||
|
|
1568
|
+
response.inputResponse?.index !== ctx.event.index ||
|
|
1569
|
+
response.inputResponse.generationId !== ctx.event.payload.generationId ||
|
|
1570
|
+
response.inputResponse.inputId !== ctx.event.payload.inputId
|
|
1571
|
+
) {
|
|
1572
|
+
return
|
|
1573
|
+
}
|
|
1574
|
+
await continueIfReady(ctx, ctx.event.payload.generationId)
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const clearPromptCache = (sessionId: string): void => {
|
|
1578
|
+
const prefix = `${sessionId}\u001f`
|
|
1579
|
+
for (const key of promptCache.keys()) {
|
|
1580
|
+
if (key.startsWith(prefix)) promptCache.delete(key)
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const handleResponseEnded = async (
|
|
1585
|
+
ctx: HandlerContext<D, 'ai.message.completed' | 'ai.message.interrupted'>,
|
|
1586
|
+
): Promise<AppendInput<D> | void> => {
|
|
1587
|
+
clearPromptCache(ctx.event.sessionId)
|
|
1588
|
+
return scheduleNext(ctx)
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
return {
|
|
1592
|
+
'ai.message.created': { lane: 'a2.ai.turn', handler: handleMessageCreated },
|
|
1593
|
+
'ai.retry.requested': { lane: 'a2.ai.turn', handler: handleRetry },
|
|
1594
|
+
'ai.input.responded': {
|
|
1595
|
+
lane: 'a2.ai.turn',
|
|
1596
|
+
handler: handleInputResponse,
|
|
1597
|
+
},
|
|
1598
|
+
'ai.message.completed': {
|
|
1599
|
+
lane: 'a2.ai.turn',
|
|
1600
|
+
handler: handleResponseEnded,
|
|
1601
|
+
},
|
|
1602
|
+
'ai.message.interrupted': {
|
|
1603
|
+
lane: 'a2.ai.turn',
|
|
1604
|
+
handler: handleResponseEnded,
|
|
1605
|
+
},
|
|
1606
|
+
'ai.session.closed': {
|
|
1607
|
+
handler: (ctx) => {
|
|
1608
|
+
clearPromptCache(ctx.event.sessionId)
|
|
1609
|
+
return Promise.resolve()
|
|
1610
|
+
},
|
|
1611
|
+
},
|
|
1612
|
+
'ai.generation.failed': {
|
|
1613
|
+
handler: (ctx) => {
|
|
1614
|
+
promptCache.delete(
|
|
1615
|
+
promptCacheKey(ctx.event.sessionId, ctx.event.payload.generationId),
|
|
1616
|
+
)
|
|
1617
|
+
return Promise.resolve()
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
'ai.generation.requested': {
|
|
1621
|
+
lane: 'a2.ai.turn',
|
|
1622
|
+
abortOn: {
|
|
1623
|
+
'ai.message.interrupted': (event, trigger, context) => {
|
|
1624
|
+
const responseMessageId =
|
|
1625
|
+
trigger.payload.responseMessageId ??
|
|
1626
|
+
(trigger.payload.reason === 'message'
|
|
1627
|
+
? `${trigger.payload.messageId}:assistant`
|
|
1628
|
+
: trigger.payload.messageId)
|
|
1629
|
+
return (
|
|
1630
|
+
event.payload.messageId === responseMessageId &&
|
|
1631
|
+
(event.payload.requestId === trigger.id ||
|
|
1632
|
+
event.payload.generationId ===
|
|
1633
|
+
`${trigger.id}:generation:${context.attempt}`)
|
|
1634
|
+
)
|
|
1635
|
+
},
|
|
1636
|
+
'ai.session.closed': true,
|
|
1637
|
+
},
|
|
1638
|
+
handler: generationHandler,
|
|
1639
|
+
},
|
|
1640
|
+
'ai.generation.completed': {
|
|
1641
|
+
handler: async (ctx) =>
|
|
1642
|
+
continueIfReady(ctx, ctx.event.payload.generationId),
|
|
1643
|
+
},
|
|
1644
|
+
'ai.tool.called': {
|
|
1645
|
+
abortOn: {
|
|
1646
|
+
'ai.generation.failed': (event, trigger) =>
|
|
1647
|
+
event.payload.generationId === trigger.payload.generationId,
|
|
1648
|
+
'ai.message.interrupted': (event, trigger) =>
|
|
1649
|
+
event.payload.messageId === trigger.payload.messageId &&
|
|
1650
|
+
(event.payload.requestId === trigger.payload.requestId ||
|
|
1651
|
+
event.payload.generationId === trigger.payload.generationId),
|
|
1652
|
+
'ai.session.closed': true,
|
|
1653
|
+
},
|
|
1654
|
+
handler: handleToolCall,
|
|
1655
|
+
},
|
|
1656
|
+
'ai.approval.responded': {
|
|
1657
|
+
abortOn: {
|
|
1658
|
+
'ai.generation.failed': (event, trigger) =>
|
|
1659
|
+
event.payload.responseMessageId === trigger.payload.messageId &&
|
|
1660
|
+
event.payload.generationId === trigger.payload.generationId,
|
|
1661
|
+
'ai.message.interrupted': (event, trigger) =>
|
|
1662
|
+
event.payload.messageId === trigger.payload.messageId &&
|
|
1663
|
+
(event.payload.requestId ===
|
|
1664
|
+
generationRequestId(trigger.payload.generationId) ||
|
|
1665
|
+
event.payload.generationId === trigger.payload.generationId),
|
|
1666
|
+
'ai.session.closed': true,
|
|
1667
|
+
},
|
|
1668
|
+
handler: handleApproval,
|
|
1669
|
+
},
|
|
1670
|
+
'ai.tool.result': {
|
|
1671
|
+
handler: async (ctx) => {
|
|
1672
|
+
if (ctx.event.payload.preliminary === true) return
|
|
1673
|
+
await continueIfReady(ctx, ctx.event.payload.generationId)
|
|
1674
|
+
},
|
|
1675
|
+
},
|
|
1676
|
+
} as NonNullable<ServerOptions<D>['handlers']>
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/** Assemble an A2 server with the built-in agent handlers and app extensions. */
|
|
1680
|
+
export function createAgentServer<
|
|
1681
|
+
M extends UIMessage,
|
|
1682
|
+
D extends AIEventDefs<M> & EventDefs,
|
|
1683
|
+
T extends ToolSet = ToolSet,
|
|
1684
|
+
>(options: CreateAgentServerOptions<M, D, T>): A2Server<D> {
|
|
1685
|
+
const {
|
|
1686
|
+
agent: definition,
|
|
1687
|
+
model,
|
|
1688
|
+
tools,
|
|
1689
|
+
instructions,
|
|
1690
|
+
generation,
|
|
1691
|
+
maxSteps,
|
|
1692
|
+
messageMetadata,
|
|
1693
|
+
generate,
|
|
1694
|
+
compaction,
|
|
1695
|
+
progress,
|
|
1696
|
+
handlers,
|
|
1697
|
+
...serverOptions
|
|
1698
|
+
} = options
|
|
1699
|
+
const builtIns = createHandlers({
|
|
1700
|
+
agent: definition,
|
|
1701
|
+
model,
|
|
1702
|
+
...(tools === undefined ? {} : { tools }),
|
|
1703
|
+
...(instructions === undefined ? {} : { instructions }),
|
|
1704
|
+
...(generation === undefined ? {} : { generation }),
|
|
1705
|
+
...(maxSteps === undefined ? {} : { maxSteps }),
|
|
1706
|
+
...(messageMetadata === undefined ? {} : { messageMetadata }),
|
|
1707
|
+
...(generate === undefined ? {} : { generate }),
|
|
1708
|
+
...(compaction === undefined ? {} : { compaction }),
|
|
1709
|
+
...(progress === undefined ? {} : { progress }),
|
|
1710
|
+
})
|
|
1711
|
+
return createServer({
|
|
1712
|
+
...serverOptions,
|
|
1713
|
+
contract: definition.contract,
|
|
1714
|
+
handlers: { ...builtIns, ...handlers },
|
|
1715
|
+
validatePush: validateAgentPush,
|
|
1716
|
+
})
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
export type { Instructions, LanguageModel, ToolSet }
|