experimental-a2 0.16.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 +8 -0
- package/dist/{ai-HJ9fHfYI.d.ts → ai-CrEf6p_W.d.ts} +2 -2
- package/dist/ai-CrEf6p_W.d.ts.map +1 -0
- package/dist/{ai-CrLYwNEx.js → ai-DgOBltJ_.js} +323 -303
- package/dist/ai-DgOBltJ_.js.map +1 -0
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +35 -8
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/docs/guides/06-ai-agents.mdx +15 -5
- package/docs/reference/01-api.mdx +8 -3
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/ai-client-state.ts +36 -2
- package/src/ai-control-server.ts +24 -5
- package/src/ai-control.ts +2 -0
- package/src/ai-message-projection.ts +127 -1
- package/src/ai-server.ts +15 -2
- package/src/ai.ts +6 -123
- package/dist/ai-CrLYwNEx.js.map +0 -1
- package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
package/dist/ai.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as reduceAIState, A as MessageInterruptedPayload, B as SessionCreatedPayload, C as GenerationUsage, D as LanguageModelUsage, E as JSONValue, F as PendingApproval, G as UIMessageChunk, H as ToolCalledPayload, I as PendingInput, J as createReducer, K as agent, L as RetryGenerationOptions, M as ModelMetadataRequestedPayload, N as ModelMetadataResolvedPayload, O as MessageCompletedPayload, P as ModelMetadataState, Q as inputs, R as RetryRequestedPayload, S as GenerationStartedPayload, T as InputRespondedPayload, U as ToolResultPayload, V as ToolActivity, W as UIMessage, X as events, Y as deriveUIMessages, Z as handlerContext, _ as GenerationCompletedPayload, a as ActiveGenerationProjection, at as ControlReceipt, b as GenerationReason, c as AgentStatus, d as ApprovalRequestedPayload, et as AIStoredReducer, f as ApprovalRespondedPayload, g as FinishReason, h as CompactionState, i as ActiveGeneration, it as ControlCommand, j as ModelLimits, k as MessageCreatedPayload, l as AgentToolContext, m as CompactionRequestedPayload, n as AIInputs, nt as PendingQueueCommand, o as AgentDefinition, ot as InboxItem, p as CompactionCompletedPayload, q as createEvents, r as AIState, rt as ActiveTurn, s as AgentOptions, t as AIEventDefs, tt as AIStoredState, u as AgentToolEventDefs, v as GenerationFailedPayload, w as InputRequestedPayload, x as GenerationRequestedPayload, y as GenerationProgressPayload, z as SessionClosedPayload } from "./ai-
|
|
1
|
+
import { $ as reduceAIState, A as MessageInterruptedPayload, B as SessionCreatedPayload, C as GenerationUsage, D as LanguageModelUsage, E as JSONValue, F as PendingApproval, G as UIMessageChunk, H as ToolCalledPayload, I as PendingInput, J as createReducer, K as agent, L as RetryGenerationOptions, M as ModelMetadataRequestedPayload, N as ModelMetadataResolvedPayload, O as MessageCompletedPayload, P as ModelMetadataState, Q as inputs, R as RetryRequestedPayload, S as GenerationStartedPayload, T as InputRespondedPayload, U as ToolResultPayload, V as ToolActivity, W as UIMessage, X as events, Y as deriveUIMessages, Z as handlerContext, _ as GenerationCompletedPayload, a as ActiveGenerationProjection, at as ControlReceipt, b as GenerationReason, c as AgentStatus, d as ApprovalRequestedPayload, et as AIStoredReducer, f as ApprovalRespondedPayload, g as FinishReason, h as CompactionState, i as ActiveGeneration, it as ControlCommand, j as ModelLimits, k as MessageCreatedPayload, l as AgentToolContext, m as CompactionRequestedPayload, n as AIInputs, nt as PendingQueueCommand, o as AgentDefinition, ot as InboxItem, p as CompactionCompletedPayload, q as createEvents, r as AIState, rt as ActiveTurn, s as AgentOptions, t as AIEventDefs, tt as AIStoredState, u as AgentToolEventDefs, v as GenerationFailedPayload, w as InputRequestedPayload, x as GenerationRequestedPayload, y as GenerationProgressPayload, z as SessionClosedPayload } from "./ai-CrEf6p_W.js";
|
|
2
2
|
export { AIEventDefs, AIInputs, AIState, type AIStoredReducer, type AIStoredState, ActiveGeneration, ActiveGenerationProjection, type ActiveTurn, AgentDefinition, AgentOptions, AgentStatus, AgentToolContext, AgentToolEventDefs, ApprovalRequestedPayload, ApprovalRespondedPayload, CompactionCompletedPayload, CompactionRequestedPayload, CompactionState, type ControlCommand, type ControlReceipt, type FinishReason, GenerationCompletedPayload, GenerationFailedPayload, GenerationProgressPayload, GenerationReason, GenerationRequestedPayload, GenerationStartedPayload, GenerationUsage, type InboxItem, InputRequestedPayload, InputRespondedPayload, type JSONValue, type LanguageModelUsage, MessageCompletedPayload, MessageCreatedPayload, MessageInterruptedPayload, ModelLimits, ModelMetadataRequestedPayload, ModelMetadataResolvedPayload, ModelMetadataState, PendingApproval, PendingInput, type PendingQueueCommand, RetryGenerationOptions, RetryRequestedPayload, SessionClosedPayload, SessionCreatedPayload, ToolActivity, ToolCalledPayload, ToolResultPayload, type UIMessage, type UIMessageChunk, agent, createEvents, createReducer, deriveUIMessages, events, handlerContext, inputs, reduceAIState };
|
package/dist/ai.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as events, c as reduceAIState, i as deriveUIMessages, n as createEvents, o as handlerContext, r as createReducer, s as inputs, t as agent } from "./ai-
|
|
1
|
+
import { a as events, c as reduceAIState, i as deriveUIMessages, n as createEvents, o as handlerContext, r as createReducer, s as inputs, t as agent } from "./ai-DgOBltJ_.js";
|
|
2
2
|
export { agent, createEvents, createReducer, deriveUIMessages, events, handlerContext, inputs, reduceAIState };
|
|
@@ -728,8 +728,10 @@ export function StopButton() {
|
|
|
728
728
|
}
|
|
729
729
|
```
|
|
730
730
|
|
|
731
|
-
The
|
|
732
|
-
user clicked.
|
|
731
|
+
The client immediately previews Stop for the observed turn. `lastSeenIndex` is the exact confirmed log frontier visible when the
|
|
732
|
+
user clicked. While that generation's indexed projection is retained, the client freezes the response at that
|
|
733
|
+
frontier through the request echo and controller decision. A rejected push or decision removes the preview.
|
|
734
|
+
The reducer rewinds the indexed generation projection to that
|
|
733
735
|
frontier and keeps the partial text the user actually saw. Tool parts whose
|
|
734
736
|
arguments are still streaming are drafts: interruption removes them from the
|
|
735
737
|
conversation and subsequent model prompts, even if their current JSON parses.
|
|
@@ -740,6 +742,13 @@ input, and compaction events remain in raw history but cannot reactivate it or
|
|
|
740
742
|
alter the projection.
|
|
741
743
|
Completed tool results at or before the visible frontier remain completed.
|
|
742
744
|
|
|
745
|
+
Once Stop is persisted, A2's existing cancellation stream aborts running model
|
|
746
|
+
and tool work for that turn without waiting for the control handler. Startup
|
|
747
|
+
checks also skip matching jobs created while Stop is pending. Cancellation is
|
|
748
|
+
cooperative: tools must honor their abort signal. The control handler remains
|
|
749
|
+
responsible for recording the interruption and advancing the inbox; a Stop for
|
|
750
|
+
an older turn cannot cancel its successor.
|
|
751
|
+
|
|
743
752
|
For the lower-level `inputs.interrupt()` builder, between
|
|
744
753
|
`ai.generation.requested` and `ai.generation.started`, copy
|
|
745
754
|
`activeRequestId` as `requestId` and `activeResponseMessageId` as `messageId`.
|
|
@@ -1219,8 +1228,9 @@ With `agent().reducer` or `createReducer()`, the client state is already optimis
|
|
|
1219
1228
|
An idle, unpaused send appears in `state.messages` immediately, with
|
|
1220
1229
|
`state.starting: true`. Another send waits in `state.inbox.items` while that
|
|
1221
1230
|
optimistic turn or a confirmed turn is active. Paused sessions keep new sends in
|
|
1222
|
-
the inbox. Render the conversation and queue from `state
|
|
1223
|
-
|
|
1231
|
+
the inbox. Render the conversation and queue from `state`. A pending Stop clears
|
|
1232
|
+
the displayed `state.active` turn immediately; rejection restores it. New active
|
|
1233
|
+
turn identities still come from accepted control decisions.
|
|
1224
1234
|
Steering appears immediately in `state.messages`, after the current response,
|
|
1225
1235
|
and stays there while that step finishes. Send now has the same display when
|
|
1226
1236
|
an active turn is targeted. These inputs are omitted from the client inbox,
|
|
@@ -1267,7 +1277,7 @@ A custom reducer can keep normalized AI state by nesting `agent.reducer.fold()`
|
|
|
1267
1277
|
and derive its view with `agent.reducer.view()`. It does not inherit the built-in
|
|
1268
1278
|
optimistic client display. `reduceAIState()` remains a pure transition over the
|
|
1269
1279
|
confirmed `AIState` view for applications that project raw event histories.
|
|
1270
|
-
`state.pendingQueueCommands` tracks unresolved queue commands incrementally, so
|
|
1280
|
+
`state.pendingQueueCommands` tracks unresolved queue commands and Stops incrementally, so
|
|
1271
1281
|
the client does not scan raw history. Pending edits apply in order, so the latest
|
|
1272
1282
|
local edit stays visible while earlier commands are confirmed. Editing and Send
|
|
1273
1283
|
now can remain available while an edit awaits its receipt. Close an editor as
|
|
@@ -1234,7 +1234,7 @@ message type. Both are isomorphic.
|
|
|
1234
1234
|
|
|
1235
1235
|
Clients using `agent().reducer` or `createReducer()` expose `state.messages`,
|
|
1236
1236
|
`state.inbox`, and `state.starting` with pending send, edit, move, remove,
|
|
1237
|
-
Send now, steer, pause, and
|
|
1237
|
+
Send now, steer, pause, resume, and Stop commands applied for display. Idle, unpaused
|
|
1238
1238
|
sends appear in the conversation immediately. A generating input occupies the
|
|
1239
1239
|
optimistic turn, so subsequent sends queue. Busy or paused sessions queue new
|
|
1240
1240
|
sends. `starting` indicates a speculative turn awaiting acceptance; it does not
|
|
@@ -1245,7 +1245,12 @@ inbox. This display survives receipt and hydration, including while paused.
|
|
|
1245
1245
|
They remain pending server inputs until admission and do not enter the model
|
|
1246
1246
|
prompt early. Multiple steering inputs follow inbox admission order.
|
|
1247
1247
|
|
|
1248
|
-
|
|
1248
|
+
New active turn identities come from accepted control decisions. A pending Stop
|
|
1249
|
+
for that turn clears the displayed `state.active`, generation, and pending
|
|
1250
|
+
approvals/inputs, and previews an idle status (or paused when the inbox is paused). With
|
|
1251
|
+
`lastSeenIndex`, its retained generation projection freezes at the observed
|
|
1252
|
+
frontier. The preview lasts through the request echo until the controller's
|
|
1253
|
+
decision; rejected requests restore the confirmed active turn and output.
|
|
1249
1254
|
Server reads and reducer folds retain confirmed messages and inbox entries;
|
|
1250
1255
|
`state.starting` is false there. Custom reducers that call `reduceAIState()` or
|
|
1251
1256
|
nest the AI fold do not inherit the client display behavior. Repeat editing and
|
|
@@ -1313,7 +1318,7 @@ Checkpoint size and serialization cost still grow with retained progress.
|
|
|
1313
1318
|
Tool-input deltas update `input-streaming` parts with best-effort partial JSON
|
|
1314
1319
|
for display. The state stays `input-streaming` until a completed input arrives;
|
|
1315
1320
|
partial parsing does not authorize execution or change interruption handling.
|
|
1316
|
-
Extension events are ignored. The default reducer name is `a2.ai.state.
|
|
1321
|
+
Extension events are ignored. The default reducer name is `a2.ai.state.v22:records-v2`.
|
|
1317
1322
|
|
|
1318
1323
|
### `deriveUIMessages(history)` and `reduceAIState(state, event)`
|
|
1319
1324
|
|
package/package.json
CHANGED
package/src/ai-client-state.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { UIMessage } from 'ai'
|
|
2
2
|
import type { AIState } from './ai.ts'
|
|
3
3
|
import type { ControlCommand, InboxItem } from './ai-control.ts'
|
|
4
|
+
import { interruptGenerationProjection } from './ai-message-projection.ts'
|
|
4
5
|
|
|
5
6
|
export type QueueCommand<M extends UIMessage> = Extract<
|
|
6
7
|
ControlCommand<M>,
|
|
@@ -14,6 +15,7 @@ export type QueueCommand<M extends UIMessage> = Extract<
|
|
|
14
15
|
| 'steer'
|
|
15
16
|
| 'pause'
|
|
16
17
|
| 'resume'
|
|
18
|
+
| 'stop'
|
|
17
19
|
}
|
|
18
20
|
>
|
|
19
21
|
export type PendingQueueCommand<M extends UIMessage> = {
|
|
@@ -31,7 +33,8 @@ export const isQueueCommand = <M extends UIMessage>(
|
|
|
31
33
|
command.action === 'send-now' ||
|
|
32
34
|
command.action === 'steer' ||
|
|
33
35
|
command.action === 'pause' ||
|
|
34
|
-
command.action === 'resume'
|
|
36
|
+
command.action === 'resume' ||
|
|
37
|
+
command.action === 'stop'
|
|
35
38
|
|
|
36
39
|
const moveItem = <M extends UIMessage>({
|
|
37
40
|
items,
|
|
@@ -166,6 +169,8 @@ export function projectAIClientState<M extends UIMessage>(
|
|
|
166
169
|
paused = false
|
|
167
170
|
suspended = false
|
|
168
171
|
break
|
|
172
|
+
case 'stop':
|
|
173
|
+
continue
|
|
169
174
|
}
|
|
170
175
|
admit()
|
|
171
176
|
}
|
|
@@ -174,10 +179,39 @@ export function projectAIClientState<M extends UIMessage>(
|
|
|
174
179
|
show(item.message)
|
|
175
180
|
return false
|
|
176
181
|
})
|
|
177
|
-
|
|
182
|
+
const displayed = {
|
|
178
183
|
...state,
|
|
179
184
|
messages,
|
|
180
185
|
inbox: { items, paused },
|
|
181
186
|
starting: starting && !suspended,
|
|
182
187
|
}
|
|
188
|
+
const stop = pending.find(
|
|
189
|
+
({ command }) =>
|
|
190
|
+
command.action === 'stop' && command.turnId === state.active?.turnId,
|
|
191
|
+
)?.command
|
|
192
|
+
if (stop?.action !== 'stop') return displayed
|
|
193
|
+
const projection = state.activeProjection
|
|
194
|
+
const stopped =
|
|
195
|
+
projection === null
|
|
196
|
+
? displayed
|
|
197
|
+
: (interruptGenerationProjection(
|
|
198
|
+
displayed,
|
|
199
|
+
projection.generationId,
|
|
200
|
+
projection.responseMessageId,
|
|
201
|
+
stop.lastSeenIndex ?? Number.POSITIVE_INFINITY,
|
|
202
|
+
) ?? displayed)
|
|
203
|
+
return {
|
|
204
|
+
...stopped,
|
|
205
|
+
active: null,
|
|
206
|
+
status: paused ? 'paused' : 'idle',
|
|
207
|
+
starting: false,
|
|
208
|
+
activeGeneration: null,
|
|
209
|
+
activeRequestId: null,
|
|
210
|
+
activeRequestReason: null,
|
|
211
|
+
activeRequestSourceGenerationId: null,
|
|
212
|
+
activeResponseMessageId: null,
|
|
213
|
+
pendingApprovals: [],
|
|
214
|
+
pendingInputs: [],
|
|
215
|
+
error: null,
|
|
216
|
+
}
|
|
183
217
|
}
|
package/src/ai-control-server.ts
CHANGED
|
@@ -73,17 +73,36 @@ export const createControlRuntime = <
|
|
|
73
73
|
}): AIControlRuntime<M, D> => {
|
|
74
74
|
const reducer = options.agent.contract
|
|
75
75
|
.reducer({
|
|
76
|
-
name: 'a2.ai.control.
|
|
76
|
+
name: 'a2.ai.control.v5',
|
|
77
77
|
initialState: initialControlState<M>(),
|
|
78
78
|
})
|
|
79
|
-
.fold((state, event) =>
|
|
80
|
-
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'
|
|
81
100
|
? applyControlChanges({
|
|
82
101
|
state,
|
|
83
102
|
changes: (event.payload as ControlCommit).changes,
|
|
84
103
|
})
|
|
85
|
-
: state
|
|
86
|
-
)
|
|
104
|
+
: state
|
|
105
|
+
})
|
|
87
106
|
const coordinator = aiCoordinatorReducer(options.agent.contract)
|
|
88
107
|
|
|
89
108
|
const handler = async (
|
package/src/ai-control.ts
CHANGED
|
@@ -77,6 +77,7 @@ export type ControlWork = {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export type AIControlState<M extends UIMessage = UIMessage> = {
|
|
80
|
+
pendingStops: Record<string, string>
|
|
80
81
|
inbox: { paused: boolean; items: InboxItem<M>[] }
|
|
81
82
|
active:
|
|
82
83
|
| (ActiveTurn<M> & {
|
|
@@ -109,6 +110,7 @@ export type ToolExecutionRequest = {
|
|
|
109
110
|
export const initialControlState = <
|
|
110
111
|
M extends UIMessage,
|
|
111
112
|
>(): AIControlState<M> => ({
|
|
113
|
+
pendingStops: {},
|
|
112
114
|
inbox: { paused: false, items: [] },
|
|
113
115
|
active: null,
|
|
114
116
|
coordinator: { closed: false, queued: [] },
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { UIMessage, UIMessageChunk } from 'ai'
|
|
2
2
|
import type {
|
|
3
3
|
ActiveGenerationProjection,
|
|
4
|
+
AIState,
|
|
4
5
|
ApprovalRequestedPayload,
|
|
5
6
|
ApprovalRespondedPayload,
|
|
6
7
|
ToolCalledPayload,
|
|
7
8
|
ToolResultPayload,
|
|
8
9
|
ToolActivity,
|
|
9
10
|
} from './ai.ts'
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type UIMessageCursor,
|
|
13
|
+
projectUIMessageStream,
|
|
14
|
+
interruptUIMessage,
|
|
15
|
+
} from './ai-projector.ts'
|
|
11
16
|
import {
|
|
12
17
|
flattenProgressBatches,
|
|
13
18
|
appendProgressBatch,
|
|
@@ -346,3 +351,124 @@ export const advanceProjection = <M extends UIMessage>({
|
|
|
346
351
|
},
|
|
347
352
|
}
|
|
348
353
|
}
|
|
354
|
+
|
|
355
|
+
export const projectedTools = <M extends UIMessage>(
|
|
356
|
+
projection: ActiveGenerationProjection<M>,
|
|
357
|
+
): ToolActivity[] =>
|
|
358
|
+
projection.toolEvents.reduce(
|
|
359
|
+
(tools, event) => reduceToolActivity(tools, event),
|
|
360
|
+
projection.baseTools,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
export const restoreGenerationBase = <M extends UIMessage>(
|
|
364
|
+
state: AIState<M>,
|
|
365
|
+
generationId: string,
|
|
366
|
+
responseMessageId: string,
|
|
367
|
+
reason: 'interrupted' | 'superseded' | 'failed',
|
|
368
|
+
): AIState<M> => {
|
|
369
|
+
const projection =
|
|
370
|
+
state.activeProjection?.generationId === generationId
|
|
371
|
+
? state.activeProjection
|
|
372
|
+
: null
|
|
373
|
+
return {
|
|
374
|
+
...state,
|
|
375
|
+
messages:
|
|
376
|
+
projection?.baseMessage === undefined
|
|
377
|
+
? state.messages.filter((message) => message.id !== responseMessageId)
|
|
378
|
+
: upsertMessage(state.messages, projection.baseMessage),
|
|
379
|
+
tools:
|
|
380
|
+
projection === null
|
|
381
|
+
? state.tools.filter(
|
|
382
|
+
(activity) => activity.generationId !== generationId,
|
|
383
|
+
)
|
|
384
|
+
: projection.baseTools,
|
|
385
|
+
pendingApprovals: state.pendingApprovals.filter(
|
|
386
|
+
(approval) => approval.generationId !== generationId,
|
|
387
|
+
),
|
|
388
|
+
pendingInputs: state.pendingInputs.filter(
|
|
389
|
+
(input) => input.generationId !== generationId,
|
|
390
|
+
),
|
|
391
|
+
activeGeneration: null,
|
|
392
|
+
activeProjection: null,
|
|
393
|
+
terminalGenerations: {
|
|
394
|
+
...state.terminalGenerations,
|
|
395
|
+
[generationId]: reason,
|
|
396
|
+
},
|
|
397
|
+
compaction:
|
|
398
|
+
projection === null
|
|
399
|
+
? state.compaction?.generationId === generationId
|
|
400
|
+
? null
|
|
401
|
+
: state.compaction
|
|
402
|
+
: (projection.baseCompaction ?? null),
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export const stopGenerationTools = (
|
|
407
|
+
tools: ToolActivity[],
|
|
408
|
+
generationId: string | undefined,
|
|
409
|
+
responseMessageId: string,
|
|
410
|
+
error: string,
|
|
411
|
+
): ToolActivity[] =>
|
|
412
|
+
tools.map((activity) =>
|
|
413
|
+
activity.status === 'running' &&
|
|
414
|
+
(generationId === undefined
|
|
415
|
+
? activity.messageId === responseMessageId
|
|
416
|
+
: activity.generationId === generationId)
|
|
417
|
+
? { ...activity, status: 'failed', error, preliminary: false }
|
|
418
|
+
: activity,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
export const interruptGenerationProjection = <M extends UIMessage>(
|
|
422
|
+
state: AIState<M>,
|
|
423
|
+
generationId: string,
|
|
424
|
+
responseMessageId: string,
|
|
425
|
+
cutoff: number,
|
|
426
|
+
): AIState<M> | null => {
|
|
427
|
+
const projection =
|
|
428
|
+
state.activeProjection?.generationId === generationId
|
|
429
|
+
? state.activeProjection
|
|
430
|
+
: null
|
|
431
|
+
if (projection === null) return null
|
|
432
|
+
const retained: ActiveGenerationProjection<M> = {
|
|
433
|
+
...projection,
|
|
434
|
+
batches: progressBatches(
|
|
435
|
+
flattenProgressBatches(projection.batches).filter(
|
|
436
|
+
(batch) => batch.index <= cutoff,
|
|
437
|
+
),
|
|
438
|
+
),
|
|
439
|
+
toolEvents: projection.toolEvents.filter(
|
|
440
|
+
(toolEvent) => toolEvent.index <= cutoff,
|
|
441
|
+
),
|
|
442
|
+
approvalEvents: projection.approvalEvents.filter(
|
|
443
|
+
(approvalEvent) => approvalEvent.index <= cutoff,
|
|
444
|
+
),
|
|
445
|
+
}
|
|
446
|
+
const shouldProject =
|
|
447
|
+
retained.baseMessage !== undefined || retained.batches.length > 0
|
|
448
|
+
const restored = restoreGenerationBase(
|
|
449
|
+
state,
|
|
450
|
+
generationId,
|
|
451
|
+
responseMessageId,
|
|
452
|
+
'interrupted',
|
|
453
|
+
)
|
|
454
|
+
return {
|
|
455
|
+
...restored,
|
|
456
|
+
messages: shouldProject
|
|
457
|
+
? upsertMessage(
|
|
458
|
+
state.messages,
|
|
459
|
+
interruptUIMessage(projectedMessage(retained)),
|
|
460
|
+
)
|
|
461
|
+
: restored.messages,
|
|
462
|
+
tools: stopGenerationTools(
|
|
463
|
+
projectedTools(retained),
|
|
464
|
+
generationId,
|
|
465
|
+
responseMessageId,
|
|
466
|
+
'Tool execution was interrupted.',
|
|
467
|
+
),
|
|
468
|
+
usage:
|
|
469
|
+
projection.completionIndex === undefined ||
|
|
470
|
+
projection.completionIndex <= cutoff
|
|
471
|
+
? restored.usage
|
|
472
|
+
: restored.usage.filter((entry) => entry.generationId !== generationId),
|
|
473
|
+
}
|
|
474
|
+
}
|
package/src/ai-server.ts
CHANGED
|
@@ -1219,6 +1219,7 @@ export function createHandlers<
|
|
|
1219
1219
|
)
|
|
1220
1220
|
if (
|
|
1221
1221
|
state.active?.turnId !== request.turnId ||
|
|
1222
|
+
Object.values(state.pendingStops).includes(request.turnId) ||
|
|
1222
1223
|
state.active.suspended ||
|
|
1223
1224
|
state.active.version !== request.version ||
|
|
1224
1225
|
state.coordinator.response?.failure !== undefined ||
|
|
@@ -1256,9 +1257,15 @@ export function createHandlers<
|
|
|
1256
1257
|
snapshot.state.authority?.phase === 'pausing'
|
|
1257
1258
|
)
|
|
1258
1259
|
return
|
|
1259
|
-
const
|
|
1260
|
-
await ctx.session.state(
|
|
1260
|
+
const controlState = (
|
|
1261
|
+
await ctx.session.state(control.reducer, { through: snapshot.index })
|
|
1261
1262
|
).state
|
|
1263
|
+
if (
|
|
1264
|
+
request.control !== undefined &&
|
|
1265
|
+
Object.values(controlState.pendingStops).includes(request.control.turnId)
|
|
1266
|
+
)
|
|
1267
|
+
return
|
|
1268
|
+
const coordinatorState = controlState.coordinator
|
|
1262
1269
|
if (
|
|
1263
1270
|
coordinatorState.closed ||
|
|
1264
1271
|
coordinatorState.response?.activeRequestId !== requestId
|
|
@@ -1876,6 +1883,9 @@ export function createHandlers<
|
|
|
1876
1883
|
'ai.generation.requested': {
|
|
1877
1884
|
lane: 'a2.ai.model',
|
|
1878
1885
|
abortOn: {
|
|
1886
|
+
'ai.control.requested': (event, trigger) =>
|
|
1887
|
+
event.payload.action === 'stop' &&
|
|
1888
|
+
event.payload.turnId === trigger.payload.control?.turnId,
|
|
1879
1889
|
'ai.control.committed': (event, trigger) => {
|
|
1880
1890
|
const active = (event.payload as ControlCommit).view
|
|
1881
1891
|
return (
|
|
@@ -1897,6 +1907,9 @@ export function createHandlers<
|
|
|
1897
1907
|
},
|
|
1898
1908
|
'ai.tool.execution.requested': {
|
|
1899
1909
|
abortOn: {
|
|
1910
|
+
'ai.control.requested': (event, trigger) =>
|
|
1911
|
+
event.payload.action === 'stop' &&
|
|
1912
|
+
event.payload.turnId === trigger.payload.turnId,
|
|
1900
1913
|
'ai.generation.failed': (event, trigger) =>
|
|
1901
1914
|
event.payload.generationId === trigger.payload.call.generationId,
|
|
1902
1915
|
'ai.control.committed': (event, trigger) => {
|
package/src/ai.ts
CHANGED
|
@@ -53,6 +53,10 @@ import {
|
|
|
53
53
|
approvalRequestedChunk,
|
|
54
54
|
toolResultChunk,
|
|
55
55
|
advanceProjection,
|
|
56
|
+
projectedTools,
|
|
57
|
+
restoreGenerationBase,
|
|
58
|
+
stopGenerationTools,
|
|
59
|
+
interruptGenerationProjection,
|
|
56
60
|
projectedMessage,
|
|
57
61
|
reduceToolActivity,
|
|
58
62
|
} from './ai-message-projection.ts'
|
|
@@ -1027,127 +1031,6 @@ const projectedResponse = <M extends UIMessage>(options: {
|
|
|
1027
1031
|
: message
|
|
1028
1032
|
}
|
|
1029
1033
|
|
|
1030
|
-
const projectedTools = <M extends UIMessage>(
|
|
1031
|
-
projection: ActiveGenerationProjection<M>,
|
|
1032
|
-
): ToolActivity[] =>
|
|
1033
|
-
projection.toolEvents.reduce(
|
|
1034
|
-
(tools, event) => reduceToolActivity(tools, event),
|
|
1035
|
-
projection.baseTools,
|
|
1036
|
-
)
|
|
1037
|
-
|
|
1038
|
-
const restoreGenerationBase = <M extends UIMessage>(
|
|
1039
|
-
state: AIState<M>,
|
|
1040
|
-
generationId: string,
|
|
1041
|
-
responseMessageId: string,
|
|
1042
|
-
reason: 'interrupted' | 'superseded' | 'failed',
|
|
1043
|
-
): AIState<M> => {
|
|
1044
|
-
const projection =
|
|
1045
|
-
state.activeProjection?.generationId === generationId
|
|
1046
|
-
? state.activeProjection
|
|
1047
|
-
: null
|
|
1048
|
-
return {
|
|
1049
|
-
...state,
|
|
1050
|
-
messages:
|
|
1051
|
-
projection?.baseMessage === undefined
|
|
1052
|
-
? state.messages.filter((message) => message.id !== responseMessageId)
|
|
1053
|
-
: upsertMessage(state.messages, projection.baseMessage),
|
|
1054
|
-
tools:
|
|
1055
|
-
projection === null
|
|
1056
|
-
? state.tools.filter(
|
|
1057
|
-
(activity) => activity.generationId !== generationId,
|
|
1058
|
-
)
|
|
1059
|
-
: projection.baseTools,
|
|
1060
|
-
pendingApprovals: state.pendingApprovals.filter(
|
|
1061
|
-
(approval) => approval.generationId !== generationId,
|
|
1062
|
-
),
|
|
1063
|
-
pendingInputs: state.pendingInputs.filter(
|
|
1064
|
-
(input) => input.generationId !== generationId,
|
|
1065
|
-
),
|
|
1066
|
-
activeGeneration: null,
|
|
1067
|
-
activeProjection: null,
|
|
1068
|
-
terminalGenerations: {
|
|
1069
|
-
...state.terminalGenerations,
|
|
1070
|
-
[generationId]: reason,
|
|
1071
|
-
},
|
|
1072
|
-
compaction:
|
|
1073
|
-
projection === null
|
|
1074
|
-
? state.compaction?.generationId === generationId
|
|
1075
|
-
? null
|
|
1076
|
-
: state.compaction
|
|
1077
|
-
: (projection.baseCompaction ?? null),
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
const stopGenerationTools = (
|
|
1082
|
-
tools: ToolActivity[],
|
|
1083
|
-
generationId: string | undefined,
|
|
1084
|
-
responseMessageId: string,
|
|
1085
|
-
error: string,
|
|
1086
|
-
): ToolActivity[] =>
|
|
1087
|
-
tools.map((activity) =>
|
|
1088
|
-
activity.status === 'running' &&
|
|
1089
|
-
(generationId === undefined
|
|
1090
|
-
? activity.messageId === responseMessageId
|
|
1091
|
-
: activity.generationId === generationId)
|
|
1092
|
-
? { ...activity, status: 'failed', error, preliminary: false }
|
|
1093
|
-
: activity,
|
|
1094
|
-
)
|
|
1095
|
-
|
|
1096
|
-
const interruptGenerationProjection = <M extends UIMessage>(
|
|
1097
|
-
state: AIState<M>,
|
|
1098
|
-
generationId: string,
|
|
1099
|
-
responseMessageId: string,
|
|
1100
|
-
cutoff: number,
|
|
1101
|
-
): AIState<M> | null => {
|
|
1102
|
-
const projection =
|
|
1103
|
-
state.activeProjection?.generationId === generationId
|
|
1104
|
-
? state.activeProjection
|
|
1105
|
-
: null
|
|
1106
|
-
if (projection === null) return null
|
|
1107
|
-
const retained: ActiveGenerationProjection<M> = {
|
|
1108
|
-
...projection,
|
|
1109
|
-
batches: progressBatches(
|
|
1110
|
-
flattenProgressBatches(projection.batches).filter(
|
|
1111
|
-
(batch) => batch.index <= cutoff,
|
|
1112
|
-
),
|
|
1113
|
-
),
|
|
1114
|
-
toolEvents: projection.toolEvents.filter(
|
|
1115
|
-
(toolEvent) => toolEvent.index <= cutoff,
|
|
1116
|
-
),
|
|
1117
|
-
approvalEvents: projection.approvalEvents.filter(
|
|
1118
|
-
(approvalEvent) => approvalEvent.index <= cutoff,
|
|
1119
|
-
),
|
|
1120
|
-
}
|
|
1121
|
-
const shouldProject =
|
|
1122
|
-
retained.baseMessage !== undefined || retained.batches.length > 0
|
|
1123
|
-
const restored = restoreGenerationBase(
|
|
1124
|
-
state,
|
|
1125
|
-
generationId,
|
|
1126
|
-
responseMessageId,
|
|
1127
|
-
'interrupted',
|
|
1128
|
-
)
|
|
1129
|
-
return {
|
|
1130
|
-
...restored,
|
|
1131
|
-
messages: shouldProject
|
|
1132
|
-
? upsertMessage(
|
|
1133
|
-
state.messages,
|
|
1134
|
-
interruptUIMessage(projectedMessage(retained)),
|
|
1135
|
-
)
|
|
1136
|
-
: restored.messages,
|
|
1137
|
-
tools: stopGenerationTools(
|
|
1138
|
-
projectedTools(retained),
|
|
1139
|
-
generationId,
|
|
1140
|
-
responseMessageId,
|
|
1141
|
-
'Tool execution was interrupted.',
|
|
1142
|
-
),
|
|
1143
|
-
usage:
|
|
1144
|
-
projection.completionIndex === undefined ||
|
|
1145
|
-
projection.completionIndex <= cutoff
|
|
1146
|
-
? restored.usage
|
|
1147
|
-
: restored.usage.filter((entry) => entry.generationId !== generationId),
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
1034
|
const terminalGenerationReason = <M extends UIMessage>(
|
|
1152
1035
|
state: AIState<M>,
|
|
1153
1036
|
generationId: string,
|
|
@@ -2306,7 +2189,7 @@ export function createReducer<D extends EventDefs>(options: {
|
|
|
2306
2189
|
type M = AIMessageFrom<D>
|
|
2307
2190
|
const reducer = options.contract
|
|
2308
2191
|
.reducer({
|
|
2309
|
-
name: options.name ?? 'a2.ai.state.
|
|
2192
|
+
name: options.name ?? 'a2.ai.state.v22',
|
|
2310
2193
|
initialState: initialState<M>(),
|
|
2311
2194
|
...(options.stateSchema === undefined
|
|
2312
2195
|
? {}
|
|
@@ -2505,7 +2388,7 @@ export function agent<
|
|
|
2505
2388
|
})
|
|
2506
2389
|
const reducer = agentContract
|
|
2507
2390
|
.reducer({
|
|
2508
|
-
name: options.reducerName ?? 'a2.ai.state.
|
|
2391
|
+
name: options.reducerName ?? 'a2.ai.state.v22',
|
|
2509
2392
|
initialState: initialState<M>(),
|
|
2510
2393
|
})
|
|
2511
2394
|
.fold((state, event) => reduceAIState(state, event))
|