experimental-a2 0.15.0 → 0.16.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 +73 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/{actor-BfQSE0KC.d.ts → actor-ohPC-81x.d.ts} +5 -5
- package/dist/{actor-BfQSE0KC.d.ts.map → actor-ohPC-81x.d.ts.map} +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +1 -1
- package/dist/ai-CrLYwNEx.js +2431 -0
- package/dist/ai-CrLYwNEx.js.map +1 -0
- package/dist/{ai-Cai-lCbj.d.ts → ai-HJ9fHfYI.d.ts} +72 -13
- package/dist/ai-HJ9fHfYI.d.ts.map +1 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +755 -290
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1999
- package/dist/client-BYzHjkwU.js.map +1 -1
- package/dist/{client-BAEABRZB.d.ts → client-CzyacQpJ.d.ts} +9 -9
- package/dist/client-CzyacQpJ.d.ts.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/idempotent-id-BRJVeylj.js +13 -0
- package/dist/idempotent-id-BRJVeylj.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -12
- package/dist/otel.d.ts +1 -1
- package/dist/react.d.ts +13 -13
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js.map +1 -1
- package/dist/reducer-DEMjEY_O.js.map +1 -1
- package/dist/{reducer-BcS9VDKC.d.ts → reducer-otzHjuJj.d.ts} +3 -3
- package/dist/{reducer-BcS9VDKC.d.ts.map → reducer-otzHjuJj.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-CjJSGcF7.js → server-BD5ckxZb.js} +39 -26
- package/dist/server-BD5ckxZb.js.map +1 -0
- package/dist/{server-Bp5Nd1pF.d.ts → server-DbD7IJVK.d.ts} +5 -5
- package/dist/{server-Bp5Nd1pF.d.ts.map → server-DbD7IJVK.d.ts.map} +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-D_yhNdPz.d.ts → store-DyZM6fS5.d.ts} +3 -2
- package/dist/{store-D_yhNdPz.d.ts.map → store-DyZM6fS5.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +1 -1
- package/dist/{store-redis-notify-D2EI6gwX.js → store-redis-notify-BVLUfI3j.js} +14 -13
- package/dist/store-redis-notify-BVLUfI3j.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/{telemetry-CpeclqB2.d.ts → telemetry-B5jzpy6y.d.ts} +2 -2
- package/dist/telemetry-B5jzpy6y.d.ts.map +1 -0
- package/docs/concepts/04-state.mdx +9 -4
- package/docs/guides/06-ai-agents.mdx +134 -43
- package/docs/reference/01-api.mdx +104 -27
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +20 -28
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +123 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.tsx +37 -43
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +44 -24
- package/examples/playground/app/agent/compaction-timeline.test.ts +47 -7
- package/examples/playground/app/agent/compaction-timeline.ts +13 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -5
- package/examples/playground/app/chat/[chatId]/session.ts +17 -2
- package/examples/playground/app/chat/model.test.ts +4 -4
- package/examples/playground/app/chat/model.ts +18 -6
- package/examples/playground/app/chat/server.ts +6 -3
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +0 -2
- package/src/ai-context-schema.ts +296 -0
- package/src/ai-context.ts +680 -0
- package/src/ai-control-server.ts +160 -131
- package/src/ai-control-state.ts +6 -0
- package/src/ai-control.ts +6 -7
- package/src/ai-coordinator.ts +92 -38
- package/src/ai-id.ts +6 -0
- package/src/ai-input-tokens.ts +75 -0
- package/src/ai-message-projection.ts +348 -0
- package/src/ai-server.ts +232 -334
- package/src/ai-stored-state.ts +484 -0
- package/src/ai.ts +252 -418
- package/src/client.ts +21 -15
- package/src/react.ts +25 -19
- package/src/reducer.ts +2 -1
- package/src/server.ts +73 -56
- package/src/store-redis-core.ts +15 -17
- package/src/store.ts +1 -0
- package/src/telemetry.ts +2 -1
- package/dist/ai-Cai-lCbj.d.ts.map +0 -1
- package/dist/ai-control-CcD4hh3y.js +0 -119
- package/dist/ai-control-CcD4hh3y.js.map +0 -1
- package/dist/ai.js.map +0 -1
- package/dist/client-BAEABRZB.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server-CjJSGcF7.js.map +0 -1
- package/dist/store-redis-notify-D2EI6gwX.js.map +0 -1
- package/dist/telemetry-CpeclqB2.d.ts.map +0 -1
package/src/ai-control-server.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { AICoordinatorState, CoordinatorCall } from './ai-coordinator.ts'
|
|
|
28
28
|
import { validateSync } from './validate.ts'
|
|
29
29
|
import type { StandardSchemaV1 } from './standard-schema.ts'
|
|
30
30
|
import { A2Error } from './errors.ts'
|
|
31
|
+
import { aiEventId } from './ai-id.ts'
|
|
31
32
|
|
|
32
33
|
export class ControlCancelled extends Error {}
|
|
33
34
|
|
|
@@ -58,7 +59,7 @@ export type AIControlRuntime<
|
|
|
58
59
|
settled(options: {
|
|
59
60
|
ctx: HandlerContext<D>
|
|
60
61
|
events: void | AppendInput<D> | readonly AppendInput<D>[]
|
|
61
|
-
}): AppendInput<D
|
|
62
|
+
}): Promise<AppendInput<D>>
|
|
62
63
|
cancelled(options: { error: unknown; signal: AbortSignal }): boolean
|
|
63
64
|
reducer: Reducer<D, AIControlState<M>>
|
|
64
65
|
coordinator: Reducer<D, AICoordinatorState>
|
|
@@ -72,7 +73,7 @@ export const createControlRuntime = <
|
|
|
72
73
|
}): AIControlRuntime<M, D> => {
|
|
73
74
|
const reducer = options.agent.contract
|
|
74
75
|
.reducer({
|
|
75
|
-
name: 'a2.ai.control.
|
|
76
|
+
name: 'a2.ai.control.v4',
|
|
76
77
|
initialState: initialControlState<M>(),
|
|
77
78
|
})
|
|
78
79
|
.fold((state, event) =>
|
|
@@ -97,16 +98,22 @@ export const createControlRuntime = <
|
|
|
97
98
|
const state = structuredClone(snapshot.state)
|
|
98
99
|
const facts: WorkFact[] = []
|
|
99
100
|
const jobs: AppendInput<D>[] = []
|
|
100
|
-
const emit = ({
|
|
101
|
+
const emit = async ({
|
|
101
102
|
type,
|
|
102
103
|
payload,
|
|
103
|
-
id
|
|
104
|
+
id,
|
|
104
105
|
}: {
|
|
105
106
|
type: string
|
|
106
107
|
payload: unknown
|
|
107
108
|
id?: string
|
|
108
|
-
}): void => {
|
|
109
|
-
const fact = {
|
|
109
|
+
}): Promise<void> => {
|
|
110
|
+
const fact = {
|
|
111
|
+
type,
|
|
112
|
+
payload,
|
|
113
|
+
id:
|
|
114
|
+
id ??
|
|
115
|
+
(await aiEventId('a2.ai.fact', ctx.event.id, String(facts.length))),
|
|
116
|
+
}
|
|
110
117
|
facts.push(fact)
|
|
111
118
|
state.coordinator = foldCoordinator<M, D>(state.coordinator, {
|
|
112
119
|
...ctx.event,
|
|
@@ -114,20 +121,20 @@ export const createControlRuntime = <
|
|
|
114
121
|
} as ContractEvent<D>)
|
|
115
122
|
}
|
|
116
123
|
let reason: ControlReceipt['reason']
|
|
117
|
-
const end = ({
|
|
124
|
+
const end = async ({
|
|
118
125
|
lastSeenIndex,
|
|
119
126
|
reason: interruptionReason,
|
|
120
127
|
}: {
|
|
121
128
|
lastSeenIndex: number | undefined
|
|
122
129
|
reason: string | undefined
|
|
123
|
-
}): void => {
|
|
130
|
+
}): Promise<void> => {
|
|
124
131
|
const response = state.coordinator.response
|
|
125
132
|
if (response === undefined) return
|
|
126
133
|
const owner =
|
|
127
134
|
response.generation === undefined
|
|
128
135
|
? { requestId: response.activeRequestId! }
|
|
129
136
|
: { generationId: response.generation.generationId }
|
|
130
|
-
emit({
|
|
137
|
+
await emit({
|
|
131
138
|
type: 'ai.message.interrupted',
|
|
132
139
|
payload: {
|
|
133
140
|
messageId: response.responseMessageId,
|
|
@@ -140,24 +147,6 @@ export const createControlRuntime = <
|
|
|
140
147
|
})
|
|
141
148
|
state.active = null
|
|
142
149
|
}
|
|
143
|
-
const knownInput = (id: string): boolean =>
|
|
144
|
-
state.inbox.items.some((candidate) => candidate.id === id) ||
|
|
145
|
-
Object.hasOwn(state.reservedIds, id)
|
|
146
|
-
const missingInputReason = (id: string): ControlReceipt['reason'] =>
|
|
147
|
-
state.reservedIds[id] === 'removed' || !knownInput(id)
|
|
148
|
-
? 'not-found'
|
|
149
|
-
: 'already-active'
|
|
150
|
-
const inputTaken = ({
|
|
151
|
-
message,
|
|
152
|
-
generate = true,
|
|
153
|
-
}: {
|
|
154
|
-
message: M
|
|
155
|
-
generate?: boolean
|
|
156
|
-
}): boolean =>
|
|
157
|
-
knownInput(message.id) ||
|
|
158
|
-
(generate &&
|
|
159
|
-
message.role === 'user' &&
|
|
160
|
-
knownInput(`${message.id}:assistant`))
|
|
161
150
|
const insert = ({
|
|
162
151
|
message,
|
|
163
152
|
generate = true,
|
|
@@ -171,18 +160,10 @@ export const createControlRuntime = <
|
|
|
171
160
|
}): void => {
|
|
172
161
|
const candidate = {
|
|
173
162
|
id: message.id,
|
|
174
|
-
revision: 0,
|
|
175
163
|
message,
|
|
176
164
|
generate: generate && message.role === 'user',
|
|
177
165
|
...(afterStepOf === undefined ? {} : { afterStepOf }),
|
|
178
166
|
}
|
|
179
|
-
state.reservedIds = {
|
|
180
|
-
...state.reservedIds,
|
|
181
|
-
[candidate.id]: true,
|
|
182
|
-
...(candidate.generate
|
|
183
|
-
? { [`${candidate.id}:assistant`]: true as const }
|
|
184
|
-
: {}),
|
|
185
|
-
}
|
|
186
167
|
if (first) state.inbox.items.unshift(candidate)
|
|
187
168
|
else state.inbox.items.push(candidate)
|
|
188
169
|
}
|
|
@@ -230,61 +211,41 @@ export const createControlRuntime = <
|
|
|
230
211
|
else
|
|
231
212
|
switch (command.action) {
|
|
232
213
|
case 'send':
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
})
|
|
238
|
-
)
|
|
239
|
-
reason = 'duplicate-input'
|
|
240
|
-
else
|
|
241
|
-
insert({
|
|
242
|
-
message: command.message,
|
|
243
|
-
generate: command.generate !== false,
|
|
244
|
-
})
|
|
214
|
+
insert({
|
|
215
|
+
message: command.message,
|
|
216
|
+
generate: command.generate !== false,
|
|
217
|
+
})
|
|
245
218
|
break
|
|
246
219
|
case 'edit': {
|
|
247
220
|
const item = state.inbox.items.find(
|
|
248
221
|
(candidate) => candidate.id === command.message.id,
|
|
249
222
|
)
|
|
250
|
-
if (item === undefined)
|
|
251
|
-
reason = missingInputReason(command.message.id)
|
|
252
|
-
else if (item.revision !== command.expectedRevision)
|
|
253
|
-
reason = 'revision-conflict'
|
|
223
|
+
if (item === undefined) reason = 'not-pending'
|
|
254
224
|
else if (item.message.role !== command.message.role)
|
|
255
|
-
reason = '
|
|
256
|
-
else
|
|
257
|
-
item.message = command.message
|
|
258
|
-
item.revision += 1
|
|
259
|
-
}
|
|
225
|
+
reason = 'role-mismatch'
|
|
226
|
+
else item.message = command.message
|
|
260
227
|
break
|
|
261
228
|
}
|
|
262
229
|
case 'remove': {
|
|
263
230
|
const position = state.inbox.items.findIndex(
|
|
264
231
|
(item) => item.id === command.inputId,
|
|
265
232
|
)
|
|
266
|
-
if (position < 0) reason =
|
|
267
|
-
else
|
|
268
|
-
state.inbox.items.splice(position, 1)
|
|
269
|
-
state.reservedIds = {
|
|
270
|
-
...state.reservedIds,
|
|
271
|
-
[command.inputId]: 'removed',
|
|
272
|
-
}
|
|
273
|
-
}
|
|
233
|
+
if (position < 0) reason = 'not-pending'
|
|
234
|
+
else state.inbox.items.splice(position, 1)
|
|
274
235
|
break
|
|
275
236
|
}
|
|
276
237
|
case 'move': {
|
|
277
238
|
const index = state.inbox.items.findIndex(
|
|
278
239
|
(candidate) => candidate.id === command.inputId,
|
|
279
240
|
)
|
|
280
|
-
if (index < 0) reason =
|
|
241
|
+
if (index < 0) reason = 'not-pending'
|
|
281
242
|
else if (
|
|
282
243
|
command.beforeId !== null &&
|
|
283
244
|
!state.inbox.items.some(
|
|
284
245
|
(candidate) => candidate.id === command.beforeId,
|
|
285
246
|
)
|
|
286
247
|
)
|
|
287
|
-
reason = 'not-
|
|
248
|
+
reason = 'not-pending'
|
|
288
249
|
else {
|
|
289
250
|
const destination =
|
|
290
251
|
command.beforeId === null
|
|
@@ -304,23 +265,12 @@ export const createControlRuntime = <
|
|
|
304
265
|
(item) => item.id === command.inputId,
|
|
305
266
|
)
|
|
306
267
|
const selected = state.inbox.items[position]
|
|
307
|
-
if (!selected) reason =
|
|
268
|
+
if (!selected) reason = 'not-pending'
|
|
308
269
|
else if ((state.active?.turnId ?? null) !== command.turnId)
|
|
309
270
|
reason = 'stale-turn'
|
|
310
271
|
else if (selected.message.role !== 'user') reason = 'not-user-input'
|
|
311
|
-
else if (
|
|
312
|
-
!selected.generate &&
|
|
313
|
-
knownInput(`${selected.id}:assistant`)
|
|
314
|
-
)
|
|
315
|
-
reason = 'duplicate-input'
|
|
316
272
|
else {
|
|
317
|
-
|
|
318
|
-
state.reservedIds = {
|
|
319
|
-
...state.reservedIds,
|
|
320
|
-
[`${selected.id}:assistant`]: true,
|
|
321
|
-
}
|
|
322
|
-
selected.generate = true
|
|
323
|
-
}
|
|
273
|
+
selected.generate = true
|
|
324
274
|
if (state.active) selected.afterStepOf = state.active.turnId
|
|
325
275
|
else delete selected.afterStepOf
|
|
326
276
|
moveInput({ from: position, to: 0 })
|
|
@@ -330,19 +280,18 @@ export const createControlRuntime = <
|
|
|
330
280
|
case 'stop':
|
|
331
281
|
if (state.active?.turnId !== command.turnId) reason = 'stale-turn'
|
|
332
282
|
else
|
|
333
|
-
end({
|
|
283
|
+
await end({
|
|
284
|
+
lastSeenIndex: command.lastSeenIndex,
|
|
285
|
+
reason: undefined,
|
|
286
|
+
})
|
|
334
287
|
break
|
|
335
288
|
case 'steer':
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
first: true,
|
|
343
|
-
afterStepOf: command.turnId,
|
|
344
|
-
})
|
|
345
|
-
}
|
|
289
|
+
insert({
|
|
290
|
+
message: command.message,
|
|
291
|
+
generate: true,
|
|
292
|
+
first: true,
|
|
293
|
+
afterStepOf: command.turnId,
|
|
294
|
+
})
|
|
346
295
|
break
|
|
347
296
|
case 'interrupt': {
|
|
348
297
|
const response = state.coordinator.response
|
|
@@ -355,7 +304,7 @@ export const createControlRuntime = <
|
|
|
355
304
|
)
|
|
356
305
|
reason = 'stale-turn'
|
|
357
306
|
else
|
|
358
|
-
end({
|
|
307
|
+
await end({
|
|
359
308
|
lastSeenIndex: command.request.lastSeenIndex,
|
|
360
309
|
reason: command.request.reason,
|
|
361
310
|
})
|
|
@@ -373,7 +322,7 @@ export const createControlRuntime = <
|
|
|
373
322
|
active.version += 1
|
|
374
323
|
const generation = state.coordinator.response?.generation
|
|
375
324
|
if (generation)
|
|
376
|
-
emit({
|
|
325
|
+
await emit({
|
|
377
326
|
type: 'ai.generation.paused',
|
|
378
327
|
payload: {
|
|
379
328
|
generationId: generation.generationId,
|
|
@@ -389,6 +338,7 @@ export const createControlRuntime = <
|
|
|
389
338
|
case 'tool-result': {
|
|
390
339
|
const call = state.coordinator.response?.calls.find(
|
|
391
340
|
(candidate) =>
|
|
341
|
+
candidate.call.requestId === command.result.requestId &&
|
|
392
342
|
candidate.call.generationId === command.result.generationId &&
|
|
393
343
|
candidate.call.messageId === command.result.messageId &&
|
|
394
344
|
candidate.call.toolCallId === command.result.toolCallId &&
|
|
@@ -396,15 +346,15 @@ export const createControlRuntime = <
|
|
|
396
346
|
!candidate.terminal,
|
|
397
347
|
)
|
|
398
348
|
if (!call) reason = 'not-found'
|
|
399
|
-
else emit({ type: 'ai.tool.result', payload: command.result })
|
|
349
|
+
else await emit({ type: 'ai.tool.result', payload: command.result })
|
|
400
350
|
break
|
|
401
351
|
}
|
|
402
352
|
case 'approval': {
|
|
403
353
|
const response = state.coordinator.response
|
|
404
354
|
const call = response?.calls.find(
|
|
405
355
|
(candidate) =>
|
|
406
|
-
candidate.approval?.
|
|
407
|
-
|
|
356
|
+
candidate.approval?.requestId === command.response.requestId &&
|
|
357
|
+
candidate.approval.approvalId === command.response.approvalId &&
|
|
408
358
|
candidate.approval.generationId ===
|
|
409
359
|
command.response.generationId &&
|
|
410
360
|
candidate.approval.messageId === command.response.messageId,
|
|
@@ -413,7 +363,10 @@ export const createControlRuntime = <
|
|
|
413
363
|
else if (!call || call.response || call.terminal)
|
|
414
364
|
reason = 'not-found'
|
|
415
365
|
else
|
|
416
|
-
emit({
|
|
366
|
+
await emit({
|
|
367
|
+
type: 'ai.approval.responded',
|
|
368
|
+
payload: command.response,
|
|
369
|
+
})
|
|
417
370
|
break
|
|
418
371
|
}
|
|
419
372
|
case 'request-input':
|
|
@@ -423,7 +376,8 @@ export const createControlRuntime = <
|
|
|
423
376
|
const response = state.coordinator.response
|
|
424
377
|
if (
|
|
425
378
|
response?.failure !== undefined ||
|
|
426
|
-
response?.generation?.
|
|
379
|
+
response?.generation?.requestId !== payload.requestId ||
|
|
380
|
+
response.generation.generationId !== payload.generationId ||
|
|
427
381
|
response.responseMessageId !== payload.messageId ||
|
|
428
382
|
(command.action === 'input' &&
|
|
429
383
|
!response.inputs.some(
|
|
@@ -434,7 +388,7 @@ export const createControlRuntime = <
|
|
|
434
388
|
)
|
|
435
389
|
reason = 'not-found'
|
|
436
390
|
else
|
|
437
|
-
emit({
|
|
391
|
+
await emit({
|
|
438
392
|
type:
|
|
439
393
|
command.action === 'input'
|
|
440
394
|
? 'ai.input.responded'
|
|
@@ -453,14 +407,22 @@ export const createControlRuntime = <
|
|
|
453
407
|
)
|
|
454
408
|
reason = 'stale-turn'
|
|
455
409
|
else if (!state.inbox.paused && !state.active.suspended) {
|
|
456
|
-
emit({
|
|
410
|
+
await emit({
|
|
411
|
+
type: 'ai.retry.requested',
|
|
412
|
+
payload: command.request,
|
|
413
|
+
})
|
|
457
414
|
requestModel({
|
|
458
415
|
request: {
|
|
459
416
|
messageId: response.rootMessageId,
|
|
460
417
|
responseMessageId: response.responseMessageId,
|
|
461
418
|
reason: 'retry',
|
|
462
419
|
},
|
|
463
|
-
id:
|
|
420
|
+
id: await aiEventId(
|
|
421
|
+
'a2.ai.request.retry',
|
|
422
|
+
options.agent.contract.name,
|
|
423
|
+
ctx.event.sessionId,
|
|
424
|
+
command.request.retryId,
|
|
425
|
+
),
|
|
464
426
|
})
|
|
465
427
|
} else reason = 'stale-turn'
|
|
466
428
|
break
|
|
@@ -468,6 +430,15 @@ export const createControlRuntime = <
|
|
|
468
430
|
}
|
|
469
431
|
} else {
|
|
470
432
|
const report = ctx.event.payload as WorkReport
|
|
433
|
+
if (
|
|
434
|
+
report.kind === 'tool' &&
|
|
435
|
+
report.settled &&
|
|
436
|
+
state.coordinator.unsettledTools
|
|
437
|
+
) {
|
|
438
|
+
delete state.coordinator.unsettledTools[report.workId]
|
|
439
|
+
if (Object.keys(state.coordinator.unsettledTools).length === 0)
|
|
440
|
+
delete state.coordinator.unsettledTools
|
|
441
|
+
}
|
|
471
442
|
const active = state.active
|
|
472
443
|
const response = state.coordinator.response
|
|
473
444
|
const call = response?.calls.find(
|
|
@@ -492,11 +463,18 @@ export const createControlRuntime = <
|
|
|
492
463
|
authorized ||
|
|
493
464
|
(report.kind === 'tool' && event.type === 'ai.tool.result')
|
|
494
465
|
) {
|
|
495
|
-
emit({
|
|
466
|
+
await emit({
|
|
467
|
+
type: event.type,
|
|
468
|
+
payload: event.payload,
|
|
469
|
+
id: event.id,
|
|
470
|
+
})
|
|
496
471
|
work.facts = [...(work.facts ?? []), event.id]
|
|
497
472
|
}
|
|
498
473
|
}
|
|
499
|
-
if (report.settled)
|
|
474
|
+
if (report.settled) {
|
|
475
|
+
work.settled = true
|
|
476
|
+
delete work.facts
|
|
477
|
+
}
|
|
500
478
|
if (!authorized) reason = 'stale-work'
|
|
501
479
|
}
|
|
502
480
|
}
|
|
@@ -534,7 +512,7 @@ export const createControlRuntime = <
|
|
|
534
512
|
)
|
|
535
513
|
continue
|
|
536
514
|
if (call.response?.approved === false) {
|
|
537
|
-
emit({
|
|
515
|
+
await emit({
|
|
538
516
|
type: 'ai.tool.result',
|
|
539
517
|
payload: {
|
|
540
518
|
...call.call,
|
|
@@ -544,8 +522,17 @@ export const createControlRuntime = <
|
|
|
544
522
|
})
|
|
545
523
|
continue
|
|
546
524
|
}
|
|
547
|
-
const id =
|
|
525
|
+
const id = await aiEventId(
|
|
526
|
+
'a2.ai.tool.execution',
|
|
527
|
+
ctx.event.id,
|
|
528
|
+
call.call.generationId,
|
|
529
|
+
call.call.toolCallId,
|
|
530
|
+
)
|
|
548
531
|
call.work = { id, version: active.version, settled: false }
|
|
532
|
+
state.coordinator.unsettledTools = {
|
|
533
|
+
...state.coordinator.unsettledTools,
|
|
534
|
+
[id]: true,
|
|
535
|
+
}
|
|
549
536
|
jobs.push({
|
|
550
537
|
type: 'ai.tool.execution.requested',
|
|
551
538
|
id,
|
|
@@ -593,7 +580,7 @@ export const createControlRuntime = <
|
|
|
593
580
|
response.completion !== undefined &&
|
|
594
581
|
response.completion.finishReason !== 'tool-calls'
|
|
595
582
|
if (handoffReady && !naturalCompletion) {
|
|
596
|
-
end({ lastSeenIndex: undefined, reason: 'steered after step' })
|
|
583
|
+
await end({ lastSeenIndex: undefined, reason: 'steered after step' })
|
|
597
584
|
} else if (readyForModel && (!handoff || handoffReady)) {
|
|
598
585
|
const resume =
|
|
599
586
|
active.model !== null && active.model.version !== active.version
|
|
@@ -605,20 +592,29 @@ export const createControlRuntime = <
|
|
|
605
592
|
(response.completion && input))
|
|
606
593
|
) {
|
|
607
594
|
const nextReason = resume ? 'resume' : input ? 'input' : 'tool'
|
|
595
|
+
const sourceGenerationId = (
|
|
596
|
+
response.generation ?? response.source?.generation
|
|
597
|
+
)?.generationId
|
|
608
598
|
const id =
|
|
609
599
|
nextReason === 'tool'
|
|
610
|
-
?
|
|
611
|
-
|
|
600
|
+
? await aiEventId(
|
|
601
|
+
'a2.ai.request.tool',
|
|
602
|
+
response.generation!.generationId,
|
|
603
|
+
)
|
|
604
|
+
: await aiEventId('a2.ai.request', ctx.event.id, nextReason)
|
|
612
605
|
requestModel({
|
|
613
606
|
request: {
|
|
614
607
|
messageId: response.responseMessageId,
|
|
615
608
|
responseMessageId: response.responseMessageId,
|
|
616
609
|
reason: nextReason,
|
|
610
|
+
...(sourceGenerationId === undefined
|
|
611
|
+
? {}
|
|
612
|
+
: { sourceGenerationId }),
|
|
617
613
|
},
|
|
618
614
|
id,
|
|
619
615
|
})
|
|
620
616
|
} else if (naturalCompletion) {
|
|
621
|
-
emit({
|
|
617
|
+
await emit({
|
|
622
618
|
type: 'ai.message.completed',
|
|
623
619
|
payload: {
|
|
624
620
|
messageId: response.responseMessageId,
|
|
@@ -648,10 +644,15 @@ export const createControlRuntime = <
|
|
|
648
644
|
while (state.inbox.items.length > 0) {
|
|
649
645
|
const item = state.inbox.items.shift()!
|
|
650
646
|
delete item.afterStepOf
|
|
651
|
-
emit({
|
|
647
|
+
await emit({
|
|
652
648
|
type: 'ai.message.created',
|
|
653
649
|
payload: { message: item.message, generate: item.generate },
|
|
654
|
-
id:
|
|
650
|
+
id: await aiEventId(
|
|
651
|
+
'a2.ai.admitted',
|
|
652
|
+
options.agent.contract.name,
|
|
653
|
+
ctx.event.sessionId,
|
|
654
|
+
item.id,
|
|
655
|
+
),
|
|
655
656
|
})
|
|
656
657
|
if (!item.generate) continue
|
|
657
658
|
state.active = {
|
|
@@ -662,9 +663,19 @@ export const createControlRuntime = <
|
|
|
662
663
|
suspended: false,
|
|
663
664
|
model: null,
|
|
664
665
|
}
|
|
666
|
+
const requestId = await aiEventId(
|
|
667
|
+
'a2.ai.request.message',
|
|
668
|
+
options.agent.contract.name,
|
|
669
|
+
ctx.event.sessionId,
|
|
670
|
+
item.id,
|
|
671
|
+
)
|
|
665
672
|
requestModel({
|
|
666
|
-
request: {
|
|
667
|
-
|
|
673
|
+
request: {
|
|
674
|
+
messageId: item.id,
|
|
675
|
+
responseMessageId: requestId,
|
|
676
|
+
reason: 'message',
|
|
677
|
+
},
|
|
678
|
+
id: requestId,
|
|
668
679
|
})
|
|
669
680
|
break
|
|
670
681
|
}
|
|
@@ -679,14 +690,19 @@ export const createControlRuntime = <
|
|
|
679
690
|
...(facts as AppendInput<D>[]),
|
|
680
691
|
{
|
|
681
692
|
type: 'ai.control.committed',
|
|
693
|
+
id: await aiEventId('a2.ai.control.committed', ctx.event.id),
|
|
682
694
|
payload: controlCommit({ before, after: state, moves }),
|
|
683
695
|
} as AppendInput<D>,
|
|
684
696
|
...jobs,
|
|
685
|
-
{
|
|
697
|
+
{
|
|
698
|
+
type: 'ai.control.decided',
|
|
699
|
+
id: await aiEventId('a2.ai.control.decided', ctx.event.id),
|
|
700
|
+
payload: receipt,
|
|
701
|
+
} as AppendInput<D>,
|
|
686
702
|
]
|
|
687
703
|
}
|
|
688
704
|
|
|
689
|
-
const report = ({
|
|
705
|
+
const report = async ({
|
|
690
706
|
ctx,
|
|
691
707
|
events,
|
|
692
708
|
settled,
|
|
@@ -696,7 +712,7 @@ export const createControlRuntime = <
|
|
|
696
712
|
events: readonly AppendInput<D>[]
|
|
697
713
|
settled: boolean
|
|
698
714
|
name: string
|
|
699
|
-
}): AppendInput<D
|
|
715
|
+
}): Promise<AppendInput<D>> => {
|
|
700
716
|
const payload = ctx.event.payload as
|
|
701
717
|
GenerationRequestedPayload | { turnId: string; version: number }
|
|
702
718
|
const { turnId, version } = 'turnId' in payload ? payload : payload.control!
|
|
@@ -716,7 +732,12 @@ export const createControlRuntime = <
|
|
|
716
732
|
}
|
|
717
733
|
return {
|
|
718
734
|
type: 'ai.work.reported',
|
|
719
|
-
id:
|
|
735
|
+
id: await aiEventId(
|
|
736
|
+
'a2.ai.report',
|
|
737
|
+
ctx.event.id,
|
|
738
|
+
String(ctx.attempt),
|
|
739
|
+
name,
|
|
740
|
+
),
|
|
720
741
|
payload: {
|
|
721
742
|
workId: ctx.event.id,
|
|
722
743
|
attempt: ctx.attempt,
|
|
@@ -724,19 +745,27 @@ export const createControlRuntime = <
|
|
|
724
745
|
version,
|
|
725
746
|
kind: ctx.event.type === 'ai.generation.requested' ? 'model' : 'tool',
|
|
726
747
|
settled,
|
|
727
|
-
events:
|
|
728
|
-
|
|
729
|
-
(
|
|
730
|
-
event
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
748
|
+
events: await Promise.all(
|
|
749
|
+
events
|
|
750
|
+
.filter(
|
|
751
|
+
(event) =>
|
|
752
|
+
event.type !== 'ai.message.completed' &&
|
|
753
|
+
event.type !== 'ai.message.interrupted',
|
|
754
|
+
)
|
|
755
|
+
.map(async (event, index) => ({
|
|
756
|
+
type: event.type,
|
|
757
|
+
payload: event.payload,
|
|
758
|
+
id:
|
|
759
|
+
event.id ??
|
|
760
|
+
(await aiEventId(
|
|
761
|
+
'a2.ai.work.fact',
|
|
762
|
+
ctx.event.id,
|
|
763
|
+
String(ctx.attempt),
|
|
764
|
+
name,
|
|
765
|
+
String(index),
|
|
766
|
+
)),
|
|
767
|
+
})),
|
|
768
|
+
),
|
|
740
769
|
},
|
|
741
770
|
} as AppendInput<D>
|
|
742
771
|
}
|
|
@@ -757,7 +786,7 @@ export const createControlRuntime = <
|
|
|
757
786
|
(event) => event.type !== 'ai.generation.progress',
|
|
758
787
|
)
|
|
759
788
|
if (lifecycle.length === 0) return ctx.session.append(name, ...progress)
|
|
760
|
-
const input = report({ ctx, events: lifecycle, settled: false, name })
|
|
789
|
+
const input = await report({ ctx, events: lifecycle, settled: false, name })
|
|
761
790
|
const persisted = await ctx.session.append(name, ...progress, input)
|
|
762
791
|
const index = persisted.at(-1)!.index
|
|
763
792
|
const feed = ctx.session.stream({ startAfter: index })
|
|
@@ -803,7 +832,7 @@ export const createControlRuntime = <
|
|
|
803
832
|
}: {
|
|
804
833
|
ctx: HandlerContext<D>
|
|
805
834
|
events: void | AppendInput<D> | readonly AppendInput<D>[]
|
|
806
|
-
}): AppendInput<D
|
|
835
|
+
}): Promise<AppendInput<D>> =>
|
|
807
836
|
report({
|
|
808
837
|
ctx,
|
|
809
838
|
events:
|
package/src/ai-control-state.ts
CHANGED
|
@@ -58,6 +58,10 @@ export const controlChanges = ({
|
|
|
58
58
|
!Array.isArray(left) &&
|
|
59
59
|
!Array.isArray(right)
|
|
60
60
|
) {
|
|
61
|
+
if (Object.keys(right).length === 0) {
|
|
62
|
+
changes.push({ path, value: right })
|
|
63
|
+
return
|
|
64
|
+
}
|
|
61
65
|
for (const key of Object.keys(left))
|
|
62
66
|
if (!Object.hasOwn(right, key))
|
|
63
67
|
changes.push({ path: [...path, key], remove: true })
|
|
@@ -146,6 +150,8 @@ export const controlCommit = <M extends UIMessage>(options: {
|
|
|
146
150
|
paused: options.after.inbox.paused,
|
|
147
151
|
closed: options.after.coordinator.closed,
|
|
148
152
|
requestId: response?.activeRequestId ?? null,
|
|
153
|
+
requestReason: response?.requestReason ?? null,
|
|
154
|
+
sourceGenerationId: response?.sourceGenerationId ?? null,
|
|
149
155
|
responseMessageId: response?.responseMessageId ?? null,
|
|
150
156
|
},
|
|
151
157
|
}
|
package/src/ai-control.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AICoordinatorState } from './ai-coordinator.ts'
|
|
|
3
3
|
import type {
|
|
4
4
|
ApprovalRespondedPayload,
|
|
5
5
|
GenerationStartedPayload,
|
|
6
|
+
GenerationReason,
|
|
6
7
|
InputRequestedPayload,
|
|
7
8
|
InputRespondedPayload,
|
|
8
9
|
MessageInterruptedPayload,
|
|
@@ -13,7 +14,6 @@ import type {
|
|
|
13
14
|
|
|
14
15
|
export type InboxItem<M extends UIMessage = UIMessage> = {
|
|
15
16
|
id: string
|
|
16
|
-
revision: number
|
|
17
17
|
message: M
|
|
18
18
|
generate: boolean
|
|
19
19
|
afterStepOf?: string
|
|
@@ -21,7 +21,7 @@ export type InboxItem<M extends UIMessage = UIMessage> = {
|
|
|
21
21
|
|
|
22
22
|
export type ControlCommand<M extends UIMessage = UIMessage> =
|
|
23
23
|
| { action: 'send'; message: M; generate?: boolean }
|
|
24
|
-
| { action: 'edit'; message: M
|
|
24
|
+
| { action: 'edit'; message: M }
|
|
25
25
|
| { action: 'move'; inputId: string; beforeId: string | null }
|
|
26
26
|
| { action: 'remove'; inputId: string }
|
|
27
27
|
| {
|
|
@@ -46,11 +46,10 @@ export type ControlReceipt = {
|
|
|
46
46
|
work?: true
|
|
47
47
|
outcome: 'applied' | 'rejected'
|
|
48
48
|
reason?:
|
|
49
|
-
| '
|
|
50
|
-
| '
|
|
49
|
+
| 'not-pending'
|
|
50
|
+
| 'role-mismatch'
|
|
51
51
|
| 'stale-turn'
|
|
52
52
|
| 'not-found'
|
|
53
|
-
| 'duplicate-input'
|
|
54
53
|
| 'closed'
|
|
55
54
|
| 'stale-work'
|
|
56
55
|
| 'not-user-input'
|
|
@@ -78,7 +77,6 @@ export type ControlWork = {
|
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
export type AIControlState<M extends UIMessage = UIMessage> = {
|
|
81
|
-
reservedIds: Record<string, true | 'removed'>
|
|
82
80
|
inbox: { paused: boolean; items: InboxItem<M>[] }
|
|
83
81
|
active:
|
|
84
82
|
| (ActiveTurn<M> & {
|
|
@@ -111,7 +109,6 @@ export type ToolExecutionRequest = {
|
|
|
111
109
|
export const initialControlState = <
|
|
112
110
|
M extends UIMessage,
|
|
113
111
|
>(): AIControlState<M> => ({
|
|
114
|
-
reservedIds: {},
|
|
115
112
|
inbox: { paused: false, items: [] },
|
|
116
113
|
active: null,
|
|
117
114
|
coordinator: { closed: false, queued: [] },
|
|
@@ -134,6 +131,8 @@ export type ControlCommit = {
|
|
|
134
131
|
paused: boolean
|
|
135
132
|
closed: boolean
|
|
136
133
|
requestId: string | null
|
|
134
|
+
requestReason: GenerationReason | null
|
|
135
|
+
sourceGenerationId: string | null
|
|
137
136
|
responseMessageId: string | null
|
|
138
137
|
}
|
|
139
138
|
}
|