experimental-a2 0.15.0 → 0.16.1
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 +81 -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-Cai-lCbj.d.ts → ai-CrEf6p_W.d.ts} +73 -14
- package/dist/ai-CrEf6p_W.d.ts.map +1 -0
- package/dist/ai-DgOBltJ_.js +2451 -0
- package/dist/ai-DgOBltJ_.js.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 +788 -296
- 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 +149 -48
- package/docs/reference/01-api.mdx +110 -28
- 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 +36 -4
- package/src/ai-context-schema.ts +296 -0
- package/src/ai-context.ts +680 -0
- package/src/ai-control-server.ts +183 -135
- package/src/ai-control-state.ts +6 -0
- package/src/ai-control.ts +8 -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 +474 -0
- package/src/ai-server.ts +247 -336
- package/src/ai-stored-state.ts +484 -0
- package/src/ai.ts +255 -538
- 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,17 +73,36 @@ 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.v5',
|
|
76
77
|
initialState: initialControlState<M>(),
|
|
77
78
|
})
|
|
78
|
-
.fold((state, event) =>
|
|
79
|
-
event.type === 'ai.control.
|
|
79
|
+
.fold((state, event) => {
|
|
80
|
+
if (event.type === 'ai.control.requested') {
|
|
81
|
+
const command = event.payload as ControlCommand<M>
|
|
82
|
+
if (command.action === 'stop')
|
|
83
|
+
return {
|
|
84
|
+
...state,
|
|
85
|
+
pendingStops: {
|
|
86
|
+
...state.pendingStops,
|
|
87
|
+
[event.id]: command.turnId,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (event.type === 'ai.control.decided') {
|
|
92
|
+
const receipt = event.payload as ControlReceipt
|
|
93
|
+
if (Object.hasOwn(state.pendingStops, receipt.commandId)) {
|
|
94
|
+
const pendingStops = { ...state.pendingStops }
|
|
95
|
+
delete pendingStops[receipt.commandId]
|
|
96
|
+
return { ...state, pendingStops }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return event.type === 'ai.control.committed'
|
|
80
100
|
? applyControlChanges({
|
|
81
101
|
state,
|
|
82
102
|
changes: (event.payload as ControlCommit).changes,
|
|
83
103
|
})
|
|
84
|
-
: state
|
|
85
|
-
)
|
|
104
|
+
: state
|
|
105
|
+
})
|
|
86
106
|
const coordinator = aiCoordinatorReducer(options.agent.contract)
|
|
87
107
|
|
|
88
108
|
const handler = async (
|
|
@@ -97,16 +117,22 @@ export const createControlRuntime = <
|
|
|
97
117
|
const state = structuredClone(snapshot.state)
|
|
98
118
|
const facts: WorkFact[] = []
|
|
99
119
|
const jobs: AppendInput<D>[] = []
|
|
100
|
-
const emit = ({
|
|
120
|
+
const emit = async ({
|
|
101
121
|
type,
|
|
102
122
|
payload,
|
|
103
|
-
id
|
|
123
|
+
id,
|
|
104
124
|
}: {
|
|
105
125
|
type: string
|
|
106
126
|
payload: unknown
|
|
107
127
|
id?: string
|
|
108
|
-
}): void => {
|
|
109
|
-
const fact = {
|
|
128
|
+
}): Promise<void> => {
|
|
129
|
+
const fact = {
|
|
130
|
+
type,
|
|
131
|
+
payload,
|
|
132
|
+
id:
|
|
133
|
+
id ??
|
|
134
|
+
(await aiEventId('a2.ai.fact', ctx.event.id, String(facts.length))),
|
|
135
|
+
}
|
|
110
136
|
facts.push(fact)
|
|
111
137
|
state.coordinator = foldCoordinator<M, D>(state.coordinator, {
|
|
112
138
|
...ctx.event,
|
|
@@ -114,20 +140,20 @@ export const createControlRuntime = <
|
|
|
114
140
|
} as ContractEvent<D>)
|
|
115
141
|
}
|
|
116
142
|
let reason: ControlReceipt['reason']
|
|
117
|
-
const end = ({
|
|
143
|
+
const end = async ({
|
|
118
144
|
lastSeenIndex,
|
|
119
145
|
reason: interruptionReason,
|
|
120
146
|
}: {
|
|
121
147
|
lastSeenIndex: number | undefined
|
|
122
148
|
reason: string | undefined
|
|
123
|
-
}): void => {
|
|
149
|
+
}): Promise<void> => {
|
|
124
150
|
const response = state.coordinator.response
|
|
125
151
|
if (response === undefined) return
|
|
126
152
|
const owner =
|
|
127
153
|
response.generation === undefined
|
|
128
154
|
? { requestId: response.activeRequestId! }
|
|
129
155
|
: { generationId: response.generation.generationId }
|
|
130
|
-
emit({
|
|
156
|
+
await emit({
|
|
131
157
|
type: 'ai.message.interrupted',
|
|
132
158
|
payload: {
|
|
133
159
|
messageId: response.responseMessageId,
|
|
@@ -140,24 +166,6 @@ export const createControlRuntime = <
|
|
|
140
166
|
})
|
|
141
167
|
state.active = null
|
|
142
168
|
}
|
|
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
169
|
const insert = ({
|
|
162
170
|
message,
|
|
163
171
|
generate = true,
|
|
@@ -171,18 +179,10 @@ export const createControlRuntime = <
|
|
|
171
179
|
}): void => {
|
|
172
180
|
const candidate = {
|
|
173
181
|
id: message.id,
|
|
174
|
-
revision: 0,
|
|
175
182
|
message,
|
|
176
183
|
generate: generate && message.role === 'user',
|
|
177
184
|
...(afterStepOf === undefined ? {} : { afterStepOf }),
|
|
178
185
|
}
|
|
179
|
-
state.reservedIds = {
|
|
180
|
-
...state.reservedIds,
|
|
181
|
-
[candidate.id]: true,
|
|
182
|
-
...(candidate.generate
|
|
183
|
-
? { [`${candidate.id}:assistant`]: true as const }
|
|
184
|
-
: {}),
|
|
185
|
-
}
|
|
186
186
|
if (first) state.inbox.items.unshift(candidate)
|
|
187
187
|
else state.inbox.items.push(candidate)
|
|
188
188
|
}
|
|
@@ -230,61 +230,41 @@ export const createControlRuntime = <
|
|
|
230
230
|
else
|
|
231
231
|
switch (command.action) {
|
|
232
232
|
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
|
-
})
|
|
233
|
+
insert({
|
|
234
|
+
message: command.message,
|
|
235
|
+
generate: command.generate !== false,
|
|
236
|
+
})
|
|
245
237
|
break
|
|
246
238
|
case 'edit': {
|
|
247
239
|
const item = state.inbox.items.find(
|
|
248
240
|
(candidate) => candidate.id === command.message.id,
|
|
249
241
|
)
|
|
250
|
-
if (item === undefined)
|
|
251
|
-
reason = missingInputReason(command.message.id)
|
|
252
|
-
else if (item.revision !== command.expectedRevision)
|
|
253
|
-
reason = 'revision-conflict'
|
|
242
|
+
if (item === undefined) reason = 'not-pending'
|
|
254
243
|
else if (item.message.role !== command.message.role)
|
|
255
|
-
reason = '
|
|
256
|
-
else
|
|
257
|
-
item.message = command.message
|
|
258
|
-
item.revision += 1
|
|
259
|
-
}
|
|
244
|
+
reason = 'role-mismatch'
|
|
245
|
+
else item.message = command.message
|
|
260
246
|
break
|
|
261
247
|
}
|
|
262
248
|
case 'remove': {
|
|
263
249
|
const position = state.inbox.items.findIndex(
|
|
264
250
|
(item) => item.id === command.inputId,
|
|
265
251
|
)
|
|
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
|
-
}
|
|
252
|
+
if (position < 0) reason = 'not-pending'
|
|
253
|
+
else state.inbox.items.splice(position, 1)
|
|
274
254
|
break
|
|
275
255
|
}
|
|
276
256
|
case 'move': {
|
|
277
257
|
const index = state.inbox.items.findIndex(
|
|
278
258
|
(candidate) => candidate.id === command.inputId,
|
|
279
259
|
)
|
|
280
|
-
if (index < 0) reason =
|
|
260
|
+
if (index < 0) reason = 'not-pending'
|
|
281
261
|
else if (
|
|
282
262
|
command.beforeId !== null &&
|
|
283
263
|
!state.inbox.items.some(
|
|
284
264
|
(candidate) => candidate.id === command.beforeId,
|
|
285
265
|
)
|
|
286
266
|
)
|
|
287
|
-
reason = 'not-
|
|
267
|
+
reason = 'not-pending'
|
|
288
268
|
else {
|
|
289
269
|
const destination =
|
|
290
270
|
command.beforeId === null
|
|
@@ -304,23 +284,12 @@ export const createControlRuntime = <
|
|
|
304
284
|
(item) => item.id === command.inputId,
|
|
305
285
|
)
|
|
306
286
|
const selected = state.inbox.items[position]
|
|
307
|
-
if (!selected) reason =
|
|
287
|
+
if (!selected) reason = 'not-pending'
|
|
308
288
|
else if ((state.active?.turnId ?? null) !== command.turnId)
|
|
309
289
|
reason = 'stale-turn'
|
|
310
290
|
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
291
|
else {
|
|
317
|
-
|
|
318
|
-
state.reservedIds = {
|
|
319
|
-
...state.reservedIds,
|
|
320
|
-
[`${selected.id}:assistant`]: true,
|
|
321
|
-
}
|
|
322
|
-
selected.generate = true
|
|
323
|
-
}
|
|
292
|
+
selected.generate = true
|
|
324
293
|
if (state.active) selected.afterStepOf = state.active.turnId
|
|
325
294
|
else delete selected.afterStepOf
|
|
326
295
|
moveInput({ from: position, to: 0 })
|
|
@@ -330,19 +299,18 @@ export const createControlRuntime = <
|
|
|
330
299
|
case 'stop':
|
|
331
300
|
if (state.active?.turnId !== command.turnId) reason = 'stale-turn'
|
|
332
301
|
else
|
|
333
|
-
end({
|
|
302
|
+
await end({
|
|
303
|
+
lastSeenIndex: command.lastSeenIndex,
|
|
304
|
+
reason: undefined,
|
|
305
|
+
})
|
|
334
306
|
break
|
|
335
307
|
case 'steer':
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
first: true,
|
|
343
|
-
afterStepOf: command.turnId,
|
|
344
|
-
})
|
|
345
|
-
}
|
|
308
|
+
insert({
|
|
309
|
+
message: command.message,
|
|
310
|
+
generate: true,
|
|
311
|
+
first: true,
|
|
312
|
+
afterStepOf: command.turnId,
|
|
313
|
+
})
|
|
346
314
|
break
|
|
347
315
|
case 'interrupt': {
|
|
348
316
|
const response = state.coordinator.response
|
|
@@ -355,7 +323,7 @@ export const createControlRuntime = <
|
|
|
355
323
|
)
|
|
356
324
|
reason = 'stale-turn'
|
|
357
325
|
else
|
|
358
|
-
end({
|
|
326
|
+
await end({
|
|
359
327
|
lastSeenIndex: command.request.lastSeenIndex,
|
|
360
328
|
reason: command.request.reason,
|
|
361
329
|
})
|
|
@@ -373,7 +341,7 @@ export const createControlRuntime = <
|
|
|
373
341
|
active.version += 1
|
|
374
342
|
const generation = state.coordinator.response?.generation
|
|
375
343
|
if (generation)
|
|
376
|
-
emit({
|
|
344
|
+
await emit({
|
|
377
345
|
type: 'ai.generation.paused',
|
|
378
346
|
payload: {
|
|
379
347
|
generationId: generation.generationId,
|
|
@@ -389,6 +357,7 @@ export const createControlRuntime = <
|
|
|
389
357
|
case 'tool-result': {
|
|
390
358
|
const call = state.coordinator.response?.calls.find(
|
|
391
359
|
(candidate) =>
|
|
360
|
+
candidate.call.requestId === command.result.requestId &&
|
|
392
361
|
candidate.call.generationId === command.result.generationId &&
|
|
393
362
|
candidate.call.messageId === command.result.messageId &&
|
|
394
363
|
candidate.call.toolCallId === command.result.toolCallId &&
|
|
@@ -396,15 +365,15 @@ export const createControlRuntime = <
|
|
|
396
365
|
!candidate.terminal,
|
|
397
366
|
)
|
|
398
367
|
if (!call) reason = 'not-found'
|
|
399
|
-
else emit({ type: 'ai.tool.result', payload: command.result })
|
|
368
|
+
else await emit({ type: 'ai.tool.result', payload: command.result })
|
|
400
369
|
break
|
|
401
370
|
}
|
|
402
371
|
case 'approval': {
|
|
403
372
|
const response = state.coordinator.response
|
|
404
373
|
const call = response?.calls.find(
|
|
405
374
|
(candidate) =>
|
|
406
|
-
candidate.approval?.
|
|
407
|
-
|
|
375
|
+
candidate.approval?.requestId === command.response.requestId &&
|
|
376
|
+
candidate.approval.approvalId === command.response.approvalId &&
|
|
408
377
|
candidate.approval.generationId ===
|
|
409
378
|
command.response.generationId &&
|
|
410
379
|
candidate.approval.messageId === command.response.messageId,
|
|
@@ -413,7 +382,10 @@ export const createControlRuntime = <
|
|
|
413
382
|
else if (!call || call.response || call.terminal)
|
|
414
383
|
reason = 'not-found'
|
|
415
384
|
else
|
|
416
|
-
emit({
|
|
385
|
+
await emit({
|
|
386
|
+
type: 'ai.approval.responded',
|
|
387
|
+
payload: command.response,
|
|
388
|
+
})
|
|
417
389
|
break
|
|
418
390
|
}
|
|
419
391
|
case 'request-input':
|
|
@@ -423,7 +395,8 @@ export const createControlRuntime = <
|
|
|
423
395
|
const response = state.coordinator.response
|
|
424
396
|
if (
|
|
425
397
|
response?.failure !== undefined ||
|
|
426
|
-
response?.generation?.
|
|
398
|
+
response?.generation?.requestId !== payload.requestId ||
|
|
399
|
+
response.generation.generationId !== payload.generationId ||
|
|
427
400
|
response.responseMessageId !== payload.messageId ||
|
|
428
401
|
(command.action === 'input' &&
|
|
429
402
|
!response.inputs.some(
|
|
@@ -434,7 +407,7 @@ export const createControlRuntime = <
|
|
|
434
407
|
)
|
|
435
408
|
reason = 'not-found'
|
|
436
409
|
else
|
|
437
|
-
emit({
|
|
410
|
+
await emit({
|
|
438
411
|
type:
|
|
439
412
|
command.action === 'input'
|
|
440
413
|
? 'ai.input.responded'
|
|
@@ -453,14 +426,22 @@ export const createControlRuntime = <
|
|
|
453
426
|
)
|
|
454
427
|
reason = 'stale-turn'
|
|
455
428
|
else if (!state.inbox.paused && !state.active.suspended) {
|
|
456
|
-
emit({
|
|
429
|
+
await emit({
|
|
430
|
+
type: 'ai.retry.requested',
|
|
431
|
+
payload: command.request,
|
|
432
|
+
})
|
|
457
433
|
requestModel({
|
|
458
434
|
request: {
|
|
459
435
|
messageId: response.rootMessageId,
|
|
460
436
|
responseMessageId: response.responseMessageId,
|
|
461
437
|
reason: 'retry',
|
|
462
438
|
},
|
|
463
|
-
id:
|
|
439
|
+
id: await aiEventId(
|
|
440
|
+
'a2.ai.request.retry',
|
|
441
|
+
options.agent.contract.name,
|
|
442
|
+
ctx.event.sessionId,
|
|
443
|
+
command.request.retryId,
|
|
444
|
+
),
|
|
464
445
|
})
|
|
465
446
|
} else reason = 'stale-turn'
|
|
466
447
|
break
|
|
@@ -468,6 +449,15 @@ export const createControlRuntime = <
|
|
|
468
449
|
}
|
|
469
450
|
} else {
|
|
470
451
|
const report = ctx.event.payload as WorkReport
|
|
452
|
+
if (
|
|
453
|
+
report.kind === 'tool' &&
|
|
454
|
+
report.settled &&
|
|
455
|
+
state.coordinator.unsettledTools
|
|
456
|
+
) {
|
|
457
|
+
delete state.coordinator.unsettledTools[report.workId]
|
|
458
|
+
if (Object.keys(state.coordinator.unsettledTools).length === 0)
|
|
459
|
+
delete state.coordinator.unsettledTools
|
|
460
|
+
}
|
|
471
461
|
const active = state.active
|
|
472
462
|
const response = state.coordinator.response
|
|
473
463
|
const call = response?.calls.find(
|
|
@@ -492,11 +482,18 @@ export const createControlRuntime = <
|
|
|
492
482
|
authorized ||
|
|
493
483
|
(report.kind === 'tool' && event.type === 'ai.tool.result')
|
|
494
484
|
) {
|
|
495
|
-
emit({
|
|
485
|
+
await emit({
|
|
486
|
+
type: event.type,
|
|
487
|
+
payload: event.payload,
|
|
488
|
+
id: event.id,
|
|
489
|
+
})
|
|
496
490
|
work.facts = [...(work.facts ?? []), event.id]
|
|
497
491
|
}
|
|
498
492
|
}
|
|
499
|
-
if (report.settled)
|
|
493
|
+
if (report.settled) {
|
|
494
|
+
work.settled = true
|
|
495
|
+
delete work.facts
|
|
496
|
+
}
|
|
500
497
|
if (!authorized) reason = 'stale-work'
|
|
501
498
|
}
|
|
502
499
|
}
|
|
@@ -534,7 +531,7 @@ export const createControlRuntime = <
|
|
|
534
531
|
)
|
|
535
532
|
continue
|
|
536
533
|
if (call.response?.approved === false) {
|
|
537
|
-
emit({
|
|
534
|
+
await emit({
|
|
538
535
|
type: 'ai.tool.result',
|
|
539
536
|
payload: {
|
|
540
537
|
...call.call,
|
|
@@ -544,8 +541,17 @@ export const createControlRuntime = <
|
|
|
544
541
|
})
|
|
545
542
|
continue
|
|
546
543
|
}
|
|
547
|
-
const id =
|
|
544
|
+
const id = await aiEventId(
|
|
545
|
+
'a2.ai.tool.execution',
|
|
546
|
+
ctx.event.id,
|
|
547
|
+
call.call.generationId,
|
|
548
|
+
call.call.toolCallId,
|
|
549
|
+
)
|
|
548
550
|
call.work = { id, version: active.version, settled: false }
|
|
551
|
+
state.coordinator.unsettledTools = {
|
|
552
|
+
...state.coordinator.unsettledTools,
|
|
553
|
+
[id]: true,
|
|
554
|
+
}
|
|
549
555
|
jobs.push({
|
|
550
556
|
type: 'ai.tool.execution.requested',
|
|
551
557
|
id,
|
|
@@ -593,7 +599,7 @@ export const createControlRuntime = <
|
|
|
593
599
|
response.completion !== undefined &&
|
|
594
600
|
response.completion.finishReason !== 'tool-calls'
|
|
595
601
|
if (handoffReady && !naturalCompletion) {
|
|
596
|
-
end({ lastSeenIndex: undefined, reason: 'steered after step' })
|
|
602
|
+
await end({ lastSeenIndex: undefined, reason: 'steered after step' })
|
|
597
603
|
} else if (readyForModel && (!handoff || handoffReady)) {
|
|
598
604
|
const resume =
|
|
599
605
|
active.model !== null && active.model.version !== active.version
|
|
@@ -605,20 +611,29 @@ export const createControlRuntime = <
|
|
|
605
611
|
(response.completion && input))
|
|
606
612
|
) {
|
|
607
613
|
const nextReason = resume ? 'resume' : input ? 'input' : 'tool'
|
|
614
|
+
const sourceGenerationId = (
|
|
615
|
+
response.generation ?? response.source?.generation
|
|
616
|
+
)?.generationId
|
|
608
617
|
const id =
|
|
609
618
|
nextReason === 'tool'
|
|
610
|
-
?
|
|
611
|
-
|
|
619
|
+
? await aiEventId(
|
|
620
|
+
'a2.ai.request.tool',
|
|
621
|
+
response.generation!.generationId,
|
|
622
|
+
)
|
|
623
|
+
: await aiEventId('a2.ai.request', ctx.event.id, nextReason)
|
|
612
624
|
requestModel({
|
|
613
625
|
request: {
|
|
614
626
|
messageId: response.responseMessageId,
|
|
615
627
|
responseMessageId: response.responseMessageId,
|
|
616
628
|
reason: nextReason,
|
|
629
|
+
...(sourceGenerationId === undefined
|
|
630
|
+
? {}
|
|
631
|
+
: { sourceGenerationId }),
|
|
617
632
|
},
|
|
618
633
|
id,
|
|
619
634
|
})
|
|
620
635
|
} else if (naturalCompletion) {
|
|
621
|
-
emit({
|
|
636
|
+
await emit({
|
|
622
637
|
type: 'ai.message.completed',
|
|
623
638
|
payload: {
|
|
624
639
|
messageId: response.responseMessageId,
|
|
@@ -648,10 +663,15 @@ export const createControlRuntime = <
|
|
|
648
663
|
while (state.inbox.items.length > 0) {
|
|
649
664
|
const item = state.inbox.items.shift()!
|
|
650
665
|
delete item.afterStepOf
|
|
651
|
-
emit({
|
|
666
|
+
await emit({
|
|
652
667
|
type: 'ai.message.created',
|
|
653
668
|
payload: { message: item.message, generate: item.generate },
|
|
654
|
-
id:
|
|
669
|
+
id: await aiEventId(
|
|
670
|
+
'a2.ai.admitted',
|
|
671
|
+
options.agent.contract.name,
|
|
672
|
+
ctx.event.sessionId,
|
|
673
|
+
item.id,
|
|
674
|
+
),
|
|
655
675
|
})
|
|
656
676
|
if (!item.generate) continue
|
|
657
677
|
state.active = {
|
|
@@ -662,9 +682,19 @@ export const createControlRuntime = <
|
|
|
662
682
|
suspended: false,
|
|
663
683
|
model: null,
|
|
664
684
|
}
|
|
685
|
+
const requestId = await aiEventId(
|
|
686
|
+
'a2.ai.request.message',
|
|
687
|
+
options.agent.contract.name,
|
|
688
|
+
ctx.event.sessionId,
|
|
689
|
+
item.id,
|
|
690
|
+
)
|
|
665
691
|
requestModel({
|
|
666
|
-
request: {
|
|
667
|
-
|
|
692
|
+
request: {
|
|
693
|
+
messageId: item.id,
|
|
694
|
+
responseMessageId: requestId,
|
|
695
|
+
reason: 'message',
|
|
696
|
+
},
|
|
697
|
+
id: requestId,
|
|
668
698
|
})
|
|
669
699
|
break
|
|
670
700
|
}
|
|
@@ -679,14 +709,19 @@ export const createControlRuntime = <
|
|
|
679
709
|
...(facts as AppendInput<D>[]),
|
|
680
710
|
{
|
|
681
711
|
type: 'ai.control.committed',
|
|
712
|
+
id: await aiEventId('a2.ai.control.committed', ctx.event.id),
|
|
682
713
|
payload: controlCommit({ before, after: state, moves }),
|
|
683
714
|
} as AppendInput<D>,
|
|
684
715
|
...jobs,
|
|
685
|
-
{
|
|
716
|
+
{
|
|
717
|
+
type: 'ai.control.decided',
|
|
718
|
+
id: await aiEventId('a2.ai.control.decided', ctx.event.id),
|
|
719
|
+
payload: receipt,
|
|
720
|
+
} as AppendInput<D>,
|
|
686
721
|
]
|
|
687
722
|
}
|
|
688
723
|
|
|
689
|
-
const report = ({
|
|
724
|
+
const report = async ({
|
|
690
725
|
ctx,
|
|
691
726
|
events,
|
|
692
727
|
settled,
|
|
@@ -696,7 +731,7 @@ export const createControlRuntime = <
|
|
|
696
731
|
events: readonly AppendInput<D>[]
|
|
697
732
|
settled: boolean
|
|
698
733
|
name: string
|
|
699
|
-
}): AppendInput<D
|
|
734
|
+
}): Promise<AppendInput<D>> => {
|
|
700
735
|
const payload = ctx.event.payload as
|
|
701
736
|
GenerationRequestedPayload | { turnId: string; version: number }
|
|
702
737
|
const { turnId, version } = 'turnId' in payload ? payload : payload.control!
|
|
@@ -716,7 +751,12 @@ export const createControlRuntime = <
|
|
|
716
751
|
}
|
|
717
752
|
return {
|
|
718
753
|
type: 'ai.work.reported',
|
|
719
|
-
id:
|
|
754
|
+
id: await aiEventId(
|
|
755
|
+
'a2.ai.report',
|
|
756
|
+
ctx.event.id,
|
|
757
|
+
String(ctx.attempt),
|
|
758
|
+
name,
|
|
759
|
+
),
|
|
720
760
|
payload: {
|
|
721
761
|
workId: ctx.event.id,
|
|
722
762
|
attempt: ctx.attempt,
|
|
@@ -724,19 +764,27 @@ export const createControlRuntime = <
|
|
|
724
764
|
version,
|
|
725
765
|
kind: ctx.event.type === 'ai.generation.requested' ? 'model' : 'tool',
|
|
726
766
|
settled,
|
|
727
|
-
events:
|
|
728
|
-
|
|
729
|
-
(
|
|
730
|
-
event
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
767
|
+
events: await Promise.all(
|
|
768
|
+
events
|
|
769
|
+
.filter(
|
|
770
|
+
(event) =>
|
|
771
|
+
event.type !== 'ai.message.completed' &&
|
|
772
|
+
event.type !== 'ai.message.interrupted',
|
|
773
|
+
)
|
|
774
|
+
.map(async (event, index) => ({
|
|
775
|
+
type: event.type,
|
|
776
|
+
payload: event.payload,
|
|
777
|
+
id:
|
|
778
|
+
event.id ??
|
|
779
|
+
(await aiEventId(
|
|
780
|
+
'a2.ai.work.fact',
|
|
781
|
+
ctx.event.id,
|
|
782
|
+
String(ctx.attempt),
|
|
783
|
+
name,
|
|
784
|
+
String(index),
|
|
785
|
+
)),
|
|
786
|
+
})),
|
|
787
|
+
),
|
|
740
788
|
},
|
|
741
789
|
} as AppendInput<D>
|
|
742
790
|
}
|
|
@@ -757,7 +805,7 @@ export const createControlRuntime = <
|
|
|
757
805
|
(event) => event.type !== 'ai.generation.progress',
|
|
758
806
|
)
|
|
759
807
|
if (lifecycle.length === 0) return ctx.session.append(name, ...progress)
|
|
760
|
-
const input = report({ ctx, events: lifecycle, settled: false, name })
|
|
808
|
+
const input = await report({ ctx, events: lifecycle, settled: false, name })
|
|
761
809
|
const persisted = await ctx.session.append(name, ...progress, input)
|
|
762
810
|
const index = persisted.at(-1)!.index
|
|
763
811
|
const feed = ctx.session.stream({ startAfter: index })
|
|
@@ -803,7 +851,7 @@ export const createControlRuntime = <
|
|
|
803
851
|
}: {
|
|
804
852
|
ctx: HandlerContext<D>
|
|
805
853
|
events: void | AppendInput<D> | readonly AppendInput<D>[]
|
|
806
|
-
}): AppendInput<D
|
|
854
|
+
}): Promise<AppendInput<D>> =>
|
|
807
855
|
report({
|
|
808
856
|
ctx,
|
|
809
857
|
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
|
}
|