experimental-a2 0.18.0 → 0.19.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 +39 -0
- package/dist/{actor-Bjce9_5U.d.ts → actor-C7b-1arn.d.ts} +3 -3
- package/dist/{actor-Bjce9_5U.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-DrUaMyRt.d.ts → ai-Cz3hqVhk.d.ts} +34 -17
- package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +2 -2
- package/dist/ai-control-CvLbW10B.js +46 -0
- package/dist/ai-control-CvLbW10B.js.map +1 -0
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +122 -44
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-BuLh_EYi.js → ai-harness-BuuC-gJ3.js} +79 -15
- package/dist/ai-harness-BuuC-gJ3.js.map +1 -0
- package/dist/{ai-harness-DvsPeJeB.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
- package/dist/{ai-BpOGm-zB.js → ai-mMiWul1T.js} +117 -78
- package/dist/ai-mMiWul1T.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.d.ts.map +1 -1
- package/dist/ai-pi.js +114 -23
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +127 -55
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1
- package/dist/{client-BReKqwZC.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-BReKqwZC.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-B2T3U4Rq.js → internal-CscSY7K_.js} +4 -2
- package/dist/{internal-B2T3U4Rq.js.map → internal-CscSY7K_.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +10 -14
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Dw_k_7kO.js → server-D-Y4pqwu.js} +37 -11
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-En7cbi3v.d.ts → server-DYAATFuT.d.ts} +11 -5
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bo-XTFEy.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Djdv0bnZ.d.ts → store-CyLUy86v.d.ts} +23 -4
- package/dist/store-CyLUy86v.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +25 -6
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-B5FcwJfo.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-B5FcwJfo.js.map → store-polling-7WJV0YWP.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +55 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-BAwS8RWn.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-BAwS8RWn.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-BRITti_o.js → store-redis-notify-CM0AfLY0.js} +88 -37
- package/dist/store-redis-notify-CM0AfLY0.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/store-sqlite.js +35 -20
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-rSUglYo8.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-rSUglYo8.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/agents/03-conversations.mdx +27 -15
- package/docs/agents/04-harnesses.mdx +51 -5
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/index.mdx +3 -3
- package/docs/reference/02-server.mdx +12 -7
- package/docs/reference/04-ai.mdx +41 -23
- package/docs/reference/05-adapters.mdx +6 -6
- package/docs/server/01-handlers.mdx +1 -1
- package/docs/server/02-timers.mdx +1 -1
- package/docs/server/03-cancellation.mdx +3 -2
- package/docs/server/05-production.mdx +4 -3
- package/docs/server/06-guarantees.mdx +20 -11
- package/examples/playground/DEPLOY.md +11 -4
- package/examples/playground/app/a0/[appId]/page.tsx +2 -8
- package/examples/playground/app/a0/a0.css +244 -5
- package/examples/playground/app/a0/app-view.tsx +2 -1
- package/examples/playground/app/a0/attachments.ts +133 -0
- package/examples/playground/app/a0/chat.test.tsx +504 -0
- package/examples/playground/app/a0/chat.tsx +623 -63
- package/examples/playground/app/a0/context-menu.tsx +38 -11
- package/examples/playground/app/a0/devtools.tsx +2 -8
- package/examples/playground/app/a0/model.test.ts +28 -0
- package/examples/playground/app/a0/model.ts +30 -0
- package/examples/playground/app/a0/server.ts +13 -19
- package/examples/playground/app/a0/uploads/route.ts +29 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +1 -0
- package/examples/playground/app/agent/model.test.ts +12 -3
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
- package/examples/playground/next.config.ts +7 -4
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-agent.ts +14 -3
- package/src/ai-client-state.ts +21 -9
- package/src/ai-context-schema.ts +28 -4
- package/src/ai-context.ts +83 -3
- package/src/ai-control-server.ts +70 -43
- package/src/ai-control-state.ts +3 -4
- package/src/ai-control.ts +59 -10
- package/src/ai-coordinator.ts +7 -6
- package/src/ai-fx.ts +184 -67
- package/src/ai-harness.ts +153 -29
- package/src/ai-message-shape.ts +41 -0
- package/src/ai-pi.ts +161 -46
- package/src/ai-stored-state.ts +1 -3
- package/src/ai-tool-input.ts +45 -0
- package/src/ai.ts +116 -77
- package/src/internal.ts +6 -0
- package/src/libfx.d.ts +8 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +15 -27
- package/src/server.ts +58 -12
- package/src/store-memory.ts +50 -9
- package/src/store-postgres.ts +79 -28
- package/src/store-redis-core.ts +112 -31
- package/src/store-sqlite.ts +61 -28
- package/src/store.ts +22 -3
- package/dist/ai-BpOGm-zB.js.map +0 -1
- package/dist/ai-DrUaMyRt.d.ts.map +0 -1
- package/dist/ai-harness-BuLh_EYi.js.map +0 -1
- package/dist/ai-harness-DvsPeJeB.d.ts.map +0 -1
- package/dist/server-Dw_k_7kO.js.map +0 -1
- package/dist/server-En7cbi3v.d.ts.map +0 -1
- package/dist/session-clear-Bo-XTFEy.d.ts.map +0 -1
- package/dist/store-Djdv0bnZ.d.ts.map +0 -1
- package/dist/store-redis-notify-BRITti_o.js.map +0 -1
- package/examples/playground/app/a0/no-photon.cjs +0 -4
package/src/ai-context.ts
CHANGED
|
@@ -19,7 +19,12 @@ import type {
|
|
|
19
19
|
ToolCalledPayload,
|
|
20
20
|
ToolResultPayload,
|
|
21
21
|
} from './ai.ts'
|
|
22
|
-
import
|
|
22
|
+
import {
|
|
23
|
+
type ControlCommand,
|
|
24
|
+
type ControlCommit,
|
|
25
|
+
type ControlReceipt,
|
|
26
|
+
unexplainedAdmission,
|
|
27
|
+
} from './ai-control.ts'
|
|
23
28
|
import { contextSchema } from './ai-context-schema.ts'
|
|
24
29
|
import type { Contract, ContractEvent, EventDefs } from './contract.ts'
|
|
25
30
|
import {
|
|
@@ -49,8 +54,22 @@ export type AIModelContext<M extends UIMessage = UIMessage> = {
|
|
|
49
54
|
modelMetadata: Record<string, ModelMetadataState>
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Inputs' content between their command and their admission: each input's
|
|
59
|
+
* content is stored only in the command that brought it in. Lists, not
|
|
60
|
+
* records: clients choose the ids, and a record would read an id such as
|
|
61
|
+
* `constructor` or `__proto__` from its prototype.
|
|
62
|
+
*/
|
|
63
|
+
export type AIContextInputs<M extends UIMessage> = {
|
|
64
|
+
/** Undecided input commands; a remove holds no message. */
|
|
65
|
+
commands: { commandId: string; inputId: string; message?: M }[]
|
|
66
|
+
/** Queued inputs' current content. */
|
|
67
|
+
waiting: { inputId: string; message: M }[]
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
export type AIContextState<M extends UIMessage> = AIModelContext<M> & {
|
|
53
71
|
closed: boolean
|
|
72
|
+
inputs: AIContextInputs<M>
|
|
54
73
|
authority: Pick<
|
|
55
74
|
ControlCommit['view'],
|
|
56
75
|
| 'requestId'
|
|
@@ -211,6 +230,7 @@ export const createContextReducer = <M extends UIMessage, D extends EventDefs>({
|
|
|
211
230
|
compaction: null,
|
|
212
231
|
modelMetadata: {},
|
|
213
232
|
closed: false,
|
|
233
|
+
inputs: { commands: [], waiting: [] },
|
|
214
234
|
authority: null,
|
|
215
235
|
generation: null,
|
|
216
236
|
activeProjection: null,
|
|
@@ -222,7 +242,7 @@ export const createContextReducer = <M extends UIMessage, D extends EventDefs>({
|
|
|
222
242
|
// The application state rides in this checkpoint so every generation
|
|
223
243
|
// reads it without another state read.
|
|
224
244
|
name:
|
|
225
|
-
app === undefined ? 'a2.ai.context.
|
|
245
|
+
app === undefined ? 'a2.ai.context.v7' : `${app.name}+a2.ai.context.v7`,
|
|
226
246
|
initialState,
|
|
227
247
|
stateSchema: contextSchema<M>(app),
|
|
228
248
|
}).fold((state, event) => {
|
|
@@ -265,19 +285,79 @@ const reduceContext = <M extends UIMessage, D extends EventDefs>(
|
|
|
265
285
|
}
|
|
266
286
|
return view.closed ? endGeneration(next) : next
|
|
267
287
|
}
|
|
288
|
+
case 'ai.control.requested': {
|
|
289
|
+
const command = event.payload as ControlCommand<M>
|
|
290
|
+
const held =
|
|
291
|
+
command.action === 'send' ||
|
|
292
|
+
command.action === 'steer' ||
|
|
293
|
+
command.action === 'edit'
|
|
294
|
+
? {
|
|
295
|
+
commandId: event.id,
|
|
296
|
+
inputId: command.message.id,
|
|
297
|
+
message: command.message,
|
|
298
|
+
}
|
|
299
|
+
: command.action === 'remove'
|
|
300
|
+
? { commandId: event.id, inputId: command.inputId }
|
|
301
|
+
: undefined
|
|
302
|
+
if (held === undefined) return state
|
|
303
|
+
return {
|
|
304
|
+
...state,
|
|
305
|
+
inputs: {
|
|
306
|
+
...state.inputs,
|
|
307
|
+
commands: [...state.inputs.commands, held],
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
case 'ai.control.decided': {
|
|
312
|
+
const receipt = event.payload as ControlReceipt
|
|
313
|
+
const held = receipt.work
|
|
314
|
+
? undefined
|
|
315
|
+
: state.inputs.commands.find(
|
|
316
|
+
(command) => command.commandId === receipt.commandId,
|
|
317
|
+
)
|
|
318
|
+
if (held === undefined) return state
|
|
319
|
+
const commands = state.inputs.commands.filter(
|
|
320
|
+
(command) => command !== held,
|
|
321
|
+
)
|
|
322
|
+
if (receipt.outcome !== 'applied')
|
|
323
|
+
return { ...state, inputs: { ...state.inputs, commands } }
|
|
324
|
+
const waiting = state.inputs.waiting.filter(
|
|
325
|
+
(input) => input.inputId !== held.inputId,
|
|
326
|
+
)
|
|
327
|
+
if (held.message !== undefined)
|
|
328
|
+
waiting.push({ inputId: held.inputId, message: held.message })
|
|
329
|
+
return { ...state, inputs: { commands, waiting } }
|
|
330
|
+
}
|
|
268
331
|
case 'ai.session.closed':
|
|
269
332
|
return endGeneration({
|
|
270
333
|
...state,
|
|
271
334
|
closed: true,
|
|
335
|
+
inputs: { commands: [], waiting: [] },
|
|
272
336
|
compaction:
|
|
273
337
|
state.compaction?.status === 'running'
|
|
274
338
|
? (state.activeProjection?.baseCompaction ?? null)
|
|
275
339
|
: state.compaction,
|
|
276
340
|
})
|
|
277
341
|
case 'ai.message.created': {
|
|
278
|
-
const {
|
|
342
|
+
const { messageId } = event.payload as MessageCreatedPayload
|
|
343
|
+
// A send or steer admitted with its own receipt is still undecided
|
|
344
|
+
// here; its command id is the input's.
|
|
345
|
+
const message =
|
|
346
|
+
state.inputs.waiting.find((input) => input.inputId === messageId)
|
|
347
|
+
?.message ??
|
|
348
|
+
state.inputs.commands.find((command) => command.commandId === messageId)
|
|
349
|
+
?.message
|
|
350
|
+
if (message === undefined) throw unexplainedAdmission(messageId)
|
|
279
351
|
return {
|
|
280
352
|
...state,
|
|
353
|
+
inputs: {
|
|
354
|
+
commands: state.inputs.commands.filter(
|
|
355
|
+
(command) => command.commandId !== messageId,
|
|
356
|
+
),
|
|
357
|
+
waiting: state.inputs.waiting.filter(
|
|
358
|
+
(input) => input.inputId !== messageId,
|
|
359
|
+
),
|
|
360
|
+
},
|
|
281
361
|
messages: upsertMessage(state.messages, message),
|
|
282
362
|
activeProjection:
|
|
283
363
|
state.activeProjection?.responseMessageId === message.id
|
package/src/ai-control-server.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { UIMessage } from 'ai'
|
|
|
6
6
|
import type {
|
|
7
7
|
AIEventDefs,
|
|
8
8
|
GenerationRequestedPayload,
|
|
9
|
+
MessageCreatedPayload,
|
|
9
10
|
MessageInterruptedPayload,
|
|
10
11
|
} from './ai.ts'
|
|
11
12
|
import {
|
|
@@ -15,6 +16,9 @@ import {
|
|
|
15
16
|
} from './ai-coordinator.ts'
|
|
16
17
|
import {
|
|
17
18
|
initialControlState,
|
|
19
|
+
STEERED_REASON,
|
|
20
|
+
steeringEnd,
|
|
21
|
+
steersOn,
|
|
18
22
|
type AIControlState,
|
|
19
23
|
type ControlCommand,
|
|
20
24
|
type ControlReceipt,
|
|
@@ -67,7 +71,7 @@ export type AIControlRuntime<
|
|
|
67
71
|
events: void | AppendInput<D> | readonly AppendInput<D>[]
|
|
68
72
|
}): Promise<AppendInput<D>>
|
|
69
73
|
cancelled(options: { error: unknown; signal: AbortSignal }): boolean
|
|
70
|
-
reducer: Reducer<D, AIControlState
|
|
74
|
+
reducer: Reducer<D, AIControlState>
|
|
71
75
|
coordinator: Reducer<D, AICoordinatorState>
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -81,8 +85,8 @@ export const createControlRuntime = <
|
|
|
81
85
|
const telemetry = options.telemetry ?? CONSOLE_TELEMETRY
|
|
82
86
|
const reducer = makeReducer({
|
|
83
87
|
contract: options.contract,
|
|
84
|
-
name: 'a2.ai.control.
|
|
85
|
-
initialState: initialControlState
|
|
88
|
+
name: 'a2.ai.control.v6',
|
|
89
|
+
initialState: initialControlState(),
|
|
86
90
|
}).fold((state, event) => {
|
|
87
91
|
if (event.type === 'ai.control.requested') {
|
|
88
92
|
const command = event.payload as ControlCommand<M>
|
|
@@ -141,7 +145,7 @@ export const createControlRuntime = <
|
|
|
141
145
|
(await aiEventId('a2.ai.fact', ctx.event.id, String(facts.length))),
|
|
142
146
|
}
|
|
143
147
|
facts.push(fact)
|
|
144
|
-
state.coordinator = foldCoordinator<
|
|
148
|
+
state.coordinator = foldCoordinator<D>(state.coordinator, {
|
|
145
149
|
...ctx.event,
|
|
146
150
|
...fact,
|
|
147
151
|
} as ContractEvent<D>)
|
|
@@ -173,25 +177,52 @@ export const createControlRuntime = <
|
|
|
173
177
|
})
|
|
174
178
|
state.active = null
|
|
175
179
|
}
|
|
180
|
+
const decide = async (): Promise<AppendInput<D>[]> => {
|
|
181
|
+
const receipt: ControlReceipt = {
|
|
182
|
+
commandId: ctx.event.id,
|
|
183
|
+
...(ctx.event.type === 'ai.work.reported'
|
|
184
|
+
? { work: true as const }
|
|
185
|
+
: {}),
|
|
186
|
+
outcome: reason === undefined ? 'applied' : 'rejected',
|
|
187
|
+
...(reason === undefined ? {} : { reason }),
|
|
188
|
+
}
|
|
189
|
+
return [
|
|
190
|
+
...(facts as AppendInput<D>[]),
|
|
191
|
+
{
|
|
192
|
+
type: 'ai.control.committed',
|
|
193
|
+
id: await aiEventId('a2.ai.control.committed', ctx.event.id),
|
|
194
|
+
payload: controlCommit({ before, after: state, moves }),
|
|
195
|
+
} as AppendInput<D>,
|
|
196
|
+
...jobs,
|
|
197
|
+
{
|
|
198
|
+
type: 'ai.control.decided',
|
|
199
|
+
id: await aiEventId('a2.ai.control.decided', ctx.event.id),
|
|
200
|
+
payload: receipt,
|
|
201
|
+
} as AppendInput<D>,
|
|
202
|
+
]
|
|
203
|
+
}
|
|
176
204
|
const insert = ({
|
|
177
205
|
message,
|
|
178
206
|
generate = true,
|
|
179
|
-
first = false,
|
|
180
207
|
afterStepOf,
|
|
181
208
|
}: {
|
|
182
209
|
message: M
|
|
183
210
|
generate?: boolean
|
|
184
|
-
first?: boolean
|
|
185
211
|
afterStepOf?: string
|
|
186
212
|
}): void => {
|
|
187
213
|
const candidate = {
|
|
188
214
|
id: message.id,
|
|
189
|
-
message,
|
|
215
|
+
role: message.role,
|
|
190
216
|
generate: generate && message.role === 'user',
|
|
191
217
|
...(afterStepOf === undefined ? {} : { afterStepOf }),
|
|
192
218
|
}
|
|
193
|
-
if (
|
|
194
|
-
else
|
|
219
|
+
if (afterStepOf === undefined) state.inbox.items.push(candidate)
|
|
220
|
+
else
|
|
221
|
+
state.inbox.items.splice(
|
|
222
|
+
steeringEnd(state.inbox.items, afterStepOf),
|
|
223
|
+
0,
|
|
224
|
+
candidate,
|
|
225
|
+
)
|
|
195
226
|
}
|
|
196
227
|
const moveInput = ({ from, to }: { from: number; to: number }): void => {
|
|
197
228
|
if (from === to) return
|
|
@@ -213,7 +244,7 @@ export const createControlRuntime = <
|
|
|
213
244
|
...request,
|
|
214
245
|
control: { turnId: active.turnId, version: active.version },
|
|
215
246
|
}
|
|
216
|
-
state.coordinator = foldCoordinator<
|
|
247
|
+
state.coordinator = foldCoordinator<D>(state.coordinator, {
|
|
217
248
|
...ctx.event,
|
|
218
249
|
type: 'ai.generation.requested',
|
|
219
250
|
id,
|
|
@@ -247,10 +278,11 @@ export const createControlRuntime = <
|
|
|
247
278
|
(candidate) => candidate.id === command.message.id,
|
|
248
279
|
)
|
|
249
280
|
if (item === undefined) reason = 'not-pending'
|
|
250
|
-
else if (item.
|
|
281
|
+
else if (item.role !== command.message.role)
|
|
251
282
|
reason = 'role-mismatch'
|
|
252
|
-
|
|
253
|
-
|
|
283
|
+
// Its content takes effect at its receipt. An edit never runs
|
|
284
|
+
// admission, so no input's admission shares a batch with it.
|
|
285
|
+
return decide()
|
|
254
286
|
}
|
|
255
287
|
case 'remove': {
|
|
256
288
|
const position = state.inbox.items.findIndex(
|
|
@@ -294,12 +326,18 @@ export const createControlRuntime = <
|
|
|
294
326
|
if (!selected) reason = 'not-pending'
|
|
295
327
|
else if ((state.active?.turnId ?? null) !== command.turnId)
|
|
296
328
|
reason = 'stale-turn'
|
|
297
|
-
else if (selected.
|
|
329
|
+
else if (selected.role !== 'user') reason = 'not-user-input'
|
|
298
330
|
else {
|
|
299
331
|
selected.generate = true
|
|
300
332
|
if (state.active) selected.afterStepOf = state.active.turnId
|
|
301
333
|
else delete selected.afterStepOf
|
|
302
|
-
|
|
334
|
+
const others = state.inbox.items.filter(
|
|
335
|
+
(item) => item !== selected,
|
|
336
|
+
)
|
|
337
|
+
moveInput({
|
|
338
|
+
from: position,
|
|
339
|
+
to: state.active ? steeringEnd(others, state.active.turnId) : 0,
|
|
340
|
+
})
|
|
303
341
|
}
|
|
304
342
|
break
|
|
305
343
|
}
|
|
@@ -315,7 +353,6 @@ export const createControlRuntime = <
|
|
|
315
353
|
insert({
|
|
316
354
|
message: command.message,
|
|
317
355
|
generate: true,
|
|
318
|
-
first: true,
|
|
319
356
|
afterStepOf: command.turnId,
|
|
320
357
|
})
|
|
321
358
|
break
|
|
@@ -586,15 +623,19 @@ export const createControlRuntime = <
|
|
|
586
623
|
!call.terminal &&
|
|
587
624
|
!approvalBlocksExecution(call),
|
|
588
625
|
)
|
|
626
|
+
// A paused turn keeps its steering until Resume, so every steer sent
|
|
627
|
+
// while it's paused targets it and they enter together.
|
|
589
628
|
const handoffReady =
|
|
590
629
|
handoff &&
|
|
591
630
|
workSettled &&
|
|
592
631
|
!externalPending &&
|
|
593
|
-
(active.suspended
|
|
594
|
-
|
|
595
|
-
response.
|
|
596
|
-
(
|
|
597
|
-
|
|
632
|
+
(active.suspended
|
|
633
|
+
? !state.inbox.paused
|
|
634
|
+
: response.failure !== undefined ||
|
|
635
|
+
response.calls.every(
|
|
636
|
+
(call) =>
|
|
637
|
+
callReadyForModel(call) || approvalBlocksExecution(call),
|
|
638
|
+
))
|
|
598
639
|
const readyForModel =
|
|
599
640
|
!active.suspended &&
|
|
600
641
|
response.failure === undefined &&
|
|
@@ -606,7 +647,7 @@ export const createControlRuntime = <
|
|
|
606
647
|
response.completion !== undefined &&
|
|
607
648
|
response.completion.finishReason !== 'tool-calls'
|
|
608
649
|
if (handoffReady && !naturalCompletion) {
|
|
609
|
-
await end({ lastSeenIndex: undefined, reason:
|
|
650
|
+
await end({ lastSeenIndex: undefined, reason: STEERED_REASON })
|
|
610
651
|
} else if (readyForModel && (!handoff || handoffReady)) {
|
|
611
652
|
const resume =
|
|
612
653
|
active.model !== null && active.model.version !== active.version
|
|
@@ -669,10 +710,15 @@ export const createControlRuntime = <
|
|
|
669
710
|
) {
|
|
670
711
|
while (state.inbox.items.length > 0) {
|
|
671
712
|
const item = state.inbox.items.shift()!
|
|
713
|
+
if (steersOn(item, state.inbox.items)) item.generate = false
|
|
672
714
|
delete item.afterStepOf
|
|
673
715
|
await emit({
|
|
674
716
|
type: 'ai.message.created',
|
|
675
|
-
payload: {
|
|
717
|
+
payload: {
|
|
718
|
+
messageId: item.id,
|
|
719
|
+
role: item.role,
|
|
720
|
+
...(item.generate ? {} : { generate: false as const }),
|
|
721
|
+
} satisfies MessageCreatedPayload,
|
|
676
722
|
id: await aiEventId(
|
|
677
723
|
'a2.ai.admitted',
|
|
678
724
|
options.contract.name,
|
|
@@ -706,26 +752,7 @@ export const createControlRuntime = <
|
|
|
706
752
|
break
|
|
707
753
|
}
|
|
708
754
|
}
|
|
709
|
-
|
|
710
|
-
commandId: ctx.event.id,
|
|
711
|
-
...(ctx.event.type === 'ai.work.reported' ? { work: true as const } : {}),
|
|
712
|
-
outcome: reason === undefined ? 'applied' : 'rejected',
|
|
713
|
-
...(reason === undefined ? {} : { reason }),
|
|
714
|
-
}
|
|
715
|
-
return [
|
|
716
|
-
...(facts as AppendInput<D>[]),
|
|
717
|
-
{
|
|
718
|
-
type: 'ai.control.committed',
|
|
719
|
-
id: await aiEventId('a2.ai.control.committed', ctx.event.id),
|
|
720
|
-
payload: controlCommit({ before, after: state, moves }),
|
|
721
|
-
} as AppendInput<D>,
|
|
722
|
-
...jobs,
|
|
723
|
-
{
|
|
724
|
-
type: 'ai.control.decided',
|
|
725
|
-
id: await aiEventId('a2.ai.control.decided', ctx.event.id),
|
|
726
|
-
payload: receipt,
|
|
727
|
-
} as AppendInput<D>,
|
|
728
|
-
]
|
|
755
|
+
return decide()
|
|
729
756
|
}
|
|
730
757
|
|
|
731
758
|
const report = async ({
|
package/src/ai-control-state.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type {
|
|
|
3
3
|
ControlCommit,
|
|
4
4
|
ControlChange,
|
|
5
5
|
} from './ai-control.ts'
|
|
6
|
-
import type { UIMessage } from 'ai'
|
|
7
6
|
|
|
8
7
|
type RecordValue = Record<string, unknown> | unknown[]
|
|
9
8
|
const container = (value: unknown): value is RecordValue =>
|
|
@@ -131,9 +130,9 @@ export const applyControlChanges = <T>({
|
|
|
131
130
|
return next
|
|
132
131
|
}
|
|
133
132
|
|
|
134
|
-
export const controlCommit =
|
|
135
|
-
before: AIControlState
|
|
136
|
-
after: AIControlState
|
|
133
|
+
export const controlCommit = (options: {
|
|
134
|
+
before: AIControlState
|
|
135
|
+
after: AIControlState
|
|
137
136
|
moves: ControlChange[]
|
|
138
137
|
}): ControlCommit => {
|
|
139
138
|
const active = options.after.active
|
package/src/ai-control.ts
CHANGED
|
@@ -12,13 +12,22 @@ import type {
|
|
|
12
12
|
ToolResultPayload,
|
|
13
13
|
} from './ai.ts'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* An input in the queue or the active turn. Its content stays in the
|
|
17
|
+
* `ai.control.requested` command that brought it, and in no other event.
|
|
18
|
+
*/
|
|
19
|
+
export type InputRef = {
|
|
16
20
|
id: string
|
|
17
|
-
|
|
21
|
+
role: UIMessage['role']
|
|
18
22
|
generate: boolean
|
|
19
23
|
afterStepOf?: string
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
/** A queued input with its content, as `AIState` shows it. */
|
|
27
|
+
export type InboxItem<M extends UIMessage = UIMessage> = InputRef & {
|
|
28
|
+
message: M
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export type ControlCommand<M extends UIMessage = UIMessage> =
|
|
23
32
|
| { action: 'send'; message: M; generate?: boolean }
|
|
24
33
|
| { action: 'edit'; message: M }
|
|
@@ -55,9 +64,9 @@ export type ControlReceipt = {
|
|
|
55
64
|
| 'not-user-input'
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
export type ActiveTurn
|
|
67
|
+
export type ActiveTurn = {
|
|
59
68
|
turnId: string
|
|
60
|
-
input:
|
|
69
|
+
input: InputRef
|
|
61
70
|
phase:
|
|
62
71
|
| 'starting'
|
|
63
72
|
| 'generating'
|
|
@@ -76,11 +85,11 @@ export type ControlWork = {
|
|
|
76
85
|
facts?: string[]
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
export type AIControlState
|
|
88
|
+
export type AIControlState = {
|
|
80
89
|
pendingStops: Record<string, string>
|
|
81
|
-
inbox: { paused: boolean; items:
|
|
90
|
+
inbox: { paused: boolean; items: InputRef[] }
|
|
82
91
|
active:
|
|
83
|
-
| (ActiveTurn
|
|
92
|
+
| (ActiveTurn & {
|
|
84
93
|
version: number
|
|
85
94
|
suspended: boolean
|
|
86
95
|
model: ControlWork | null
|
|
@@ -107,9 +116,49 @@ export type ToolExecutionRequest = {
|
|
|
107
116
|
generation: GenerationStartedPayload
|
|
108
117
|
}
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
/**
|
|
120
|
+
* The `ai.message.interrupted` reason A2 writes when steering ends a response
|
|
121
|
+
* after a step; a harness's replay continues the turn on it. Reserved, so a
|
|
122
|
+
* client's interruption can't carry it.
|
|
123
|
+
*/
|
|
124
|
+
export const STEERED_REASON = 'steered after step'
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A fold met an admission no command explains. Only A2's control lane writes
|
|
128
|
+
* `ai.message.created`, after the command that brought its input in.
|
|
129
|
+
*/
|
|
130
|
+
export const unexplainedAdmission = (messageId: string): TypeError =>
|
|
131
|
+
new TypeError(
|
|
132
|
+
`ai.message.created for ${messageId} follows no command that brought it in; only A2's control lane writes it`,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Where new steering for a turn goes in the inbox: right after the turn's
|
|
137
|
+
* earlier steering, or first, so it hands off in the order it was sent.
|
|
138
|
+
*/
|
|
139
|
+
export const steeringEnd = (
|
|
140
|
+
items: readonly InputRef[],
|
|
141
|
+
turnId: string,
|
|
142
|
+
): number => items.findLastIndex((item) => item.afterStepOf === turnId) + 1
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether an input steering a turn enters as context for later steering: the
|
|
146
|
+
* run of inputs steering one turn is admitted together, and only its last
|
|
147
|
+
* generating input starts the next turn.
|
|
148
|
+
*/
|
|
149
|
+
export const steersOn = (
|
|
150
|
+
item: InputRef,
|
|
151
|
+
rest: readonly InputRef[],
|
|
152
|
+
): boolean => {
|
|
153
|
+
if (item.afterStepOf === undefined) return false
|
|
154
|
+
for (const next of rest) {
|
|
155
|
+
if (next.afterStepOf !== item.afterStepOf) return false
|
|
156
|
+
if (next.generate) return true
|
|
157
|
+
}
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const initialControlState = (): AIControlState => ({
|
|
113
162
|
pendingStops: {},
|
|
114
163
|
inbox: { paused: false, items: [] },
|
|
115
164
|
active: null,
|
package/src/ai-coordinator.ts
CHANGED
|
@@ -107,7 +107,7 @@ const updateResponse = (
|
|
|
107
107
|
? state
|
|
108
108
|
: { ...state, response: update(state.response) }
|
|
109
109
|
|
|
110
|
-
export const foldCoordinator = <
|
|
110
|
+
export const foldCoordinator = <D extends EventDefs>(
|
|
111
111
|
state: AICoordinatorState,
|
|
112
112
|
event: ContractEvent<D>,
|
|
113
113
|
): AICoordinatorState => {
|
|
@@ -120,15 +120,16 @@ export const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
|
|
|
120
120
|
return rest
|
|
121
121
|
}
|
|
122
122
|
case 'ai.message.created': {
|
|
123
|
-
const payload = event.payload as MessageCreatedPayload
|
|
124
|
-
|
|
125
|
-
if (message.role !== 'user') return state
|
|
123
|
+
const payload = event.payload as MessageCreatedPayload
|
|
124
|
+
if (payload.role !== 'user') return state
|
|
126
125
|
return {
|
|
127
126
|
...state,
|
|
128
127
|
queued: [
|
|
129
|
-
...state.queued.filter(
|
|
128
|
+
...state.queued.filter(
|
|
129
|
+
(item) => item.messageId !== payload.messageId,
|
|
130
|
+
),
|
|
130
131
|
{
|
|
131
|
-
messageId:
|
|
132
|
+
messageId: payload.messageId,
|
|
132
133
|
generate: payload.generate !== false,
|
|
133
134
|
},
|
|
134
135
|
],
|