experimental-a2 0.18.1 → 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 +28 -0
- package/dist/{ai-BMLlI2Ff.d.ts → ai-Cz3hqVhk.d.ts} +33 -16
- package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +1 -1
- 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 +30 -10
- 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-B2suP2Qh.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
- package/dist/{ai-D6mGlO8j.js → ai-mMiWul1T.js} +116 -77
- 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 +55 -10
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +1 -1
- package/dist/ai-server.d.ts +2 -2
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +118 -50
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +1 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/docs/agents/03-conversations.mdx +25 -13
- package/docs/agents/04-harnesses.mdx +51 -5
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/reference/04-ai.mdx +27 -18
- package/docs/reference/05-adapters.mdx +2 -2
- package/docs/server/02-timers.mdx +1 -1
- package/docs/server/05-production.mdx +2 -2
- package/examples/playground/DEPLOY.md +9 -0
- 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/model.test.ts +28 -0
- package/examples/playground/app/a0/model.ts +30 -0
- package/examples/playground/app/a0/server.ts +5 -0
- 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 +4 -0
- 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 +58 -20
- package/src/ai-harness.ts +153 -29
- package/src/ai-message-shape.ts +41 -0
- package/src/ai-pi.ts +88 -32
- package/src/ai-stored-state.ts +1 -3
- package/src/ai.ts +116 -77
- package/src/libfx.d.ts +8 -1
- package/src/scheduler-vercel.ts +1 -12
- package/dist/ai-BMLlI2Ff.d.ts.map +0 -1
- package/dist/ai-D6mGlO8j.js.map +0 -1
- package/dist/ai-harness-B2suP2Qh.d.ts.map +0 -1
- package/dist/ai-harness-BuLh_EYi.js.map +0 -1
- package/examples/playground/app/a0/no-photon.cjs +0 -4
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
|
],
|
package/src/ai-fx.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type ToolSet,
|
|
13
13
|
type UIMessageChunk,
|
|
14
14
|
} from 'ai'
|
|
15
|
-
import type { FxAgent, FxHostTool, FxTurn } from 'libfx'
|
|
15
|
+
import type { FxAgent, FxHostTool, FxPromptBlock, FxTurn } from 'libfx'
|
|
16
16
|
import {
|
|
17
17
|
type Deferred,
|
|
18
18
|
defineHarness,
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
type GatewayCredential,
|
|
23
23
|
type Harness,
|
|
24
24
|
type HarnessGeneration,
|
|
25
|
+
harnessImages,
|
|
26
|
+
type HarnessInput,
|
|
25
27
|
type HarnessRead,
|
|
26
28
|
type HarnessStep,
|
|
27
29
|
type HarnessTurn,
|
|
@@ -48,6 +50,8 @@ export function fx(): Harness<'fx', never> {
|
|
|
48
50
|
|
|
49
51
|
const SNAPSHOT_VERSION = 1
|
|
50
52
|
const CATALOG_TTL_MS = 60 * 60 * 1000
|
|
53
|
+
/** The Gateway model catalog libfx reads for each new kernel. */
|
|
54
|
+
const CATALOG_PATH = '/coding-agent/v1/models'
|
|
51
55
|
|
|
52
56
|
type StreamPart = { type: string } & Record<string, unknown>
|
|
53
57
|
/** A recorded tool call, with the input the kernel reads from its text. */
|
|
@@ -62,7 +66,7 @@ type RecordedCall = {
|
|
|
62
66
|
|
|
63
67
|
/** One FX turn to replay: its prompt, recorded calls, and how it ended. */
|
|
64
68
|
type TurnPlan = {
|
|
65
|
-
prompt: string
|
|
69
|
+
prompt: string | FxPromptBlock[]
|
|
66
70
|
calls: RecordedCall[]
|
|
67
71
|
/** Steering to queue while serving the call at that index. */
|
|
68
72
|
steers: Map<number, string[]>
|
|
@@ -188,7 +192,7 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
188
192
|
try {
|
|
189
193
|
checkpoint = await agent!.checkpoint()
|
|
190
194
|
} catch {
|
|
191
|
-
//
|
|
195
|
+
// Later steps replay from the previous snapshot.
|
|
192
196
|
return
|
|
193
197
|
}
|
|
194
198
|
await saveHarness({
|
|
@@ -206,8 +210,10 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
206
210
|
init: RequestInit = {},
|
|
207
211
|
): Promise<Response> => {
|
|
208
212
|
const url = input instanceof Request ? input.url : String(input)
|
|
209
|
-
|
|
213
|
+
const method = (init.method ?? 'GET').toUpperCase()
|
|
214
|
+
if (method === 'GET' && new URL(url).pathname === CATALOG_PATH)
|
|
210
215
|
return catalog(url, credential, withAuth(init.headers, credential))
|
|
216
|
+
if (method === 'GET') return fetch(input, init)
|
|
211
217
|
const state = turn!
|
|
212
218
|
if (state.failure) throw state.failure
|
|
213
219
|
const body = decodeBody(init.body)
|
|
@@ -328,7 +334,8 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
328
334
|
return fail(state, diverged(`no recorded result for ${toolName}`))
|
|
329
335
|
})
|
|
330
336
|
|
|
331
|
-
|
|
337
|
+
const plans = (turns: HarnessTurn[]): Promise<TurnPlan[]> =>
|
|
338
|
+
Promise.all(turns.map((recorded) => turnPlan(recorded, step.signal)))
|
|
332
339
|
let results = read.results
|
|
333
340
|
let controller: ReadableStreamDefaultController<UIMessageChunk> | undefined
|
|
334
341
|
let closed = false
|
|
@@ -364,11 +371,10 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
364
371
|
if (read.saved === undefined) throw error
|
|
365
372
|
// An unusable checkpoint: replay the whole log instead.
|
|
366
373
|
read = await replayAll()
|
|
367
|
-
turns = read.turns.map(turnPlan)
|
|
368
374
|
results = read.results
|
|
369
375
|
agent = await create(undefined)
|
|
370
376
|
}
|
|
371
|
-
await run(streamController, turns)
|
|
377
|
+
await run(streamController, await plans(read.turns))
|
|
372
378
|
closed = true
|
|
373
379
|
streamController.close()
|
|
374
380
|
} catch (error) {
|
|
@@ -396,11 +402,14 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
|
|
|
396
402
|
|
|
397
403
|
// ------------------------------------------------------------ the plan
|
|
398
404
|
|
|
399
|
-
function turnPlan(
|
|
405
|
+
async function turnPlan(
|
|
406
|
+
turn: HarnessTurn,
|
|
407
|
+
signal: AbortSignal,
|
|
408
|
+
): Promise<TurnPlan> {
|
|
400
409
|
const calls: RecordedCall[] = []
|
|
401
410
|
const steers = new Map<number, string[]>()
|
|
402
|
-
const steer = (at: number,
|
|
403
|
-
if (
|
|
411
|
+
const steer = (at: number, inputs: HarnessInput[] | undefined) => {
|
|
412
|
+
if (inputs !== undefined) steers.set(Math.max(0, at), inputs.map(steerText))
|
|
404
413
|
}
|
|
405
414
|
steer(0, turn.generations.length === 0 ? turn.steers.get(0) : undefined)
|
|
406
415
|
turn.generations.forEach((generation, index) => {
|
|
@@ -408,7 +417,38 @@ function turnPlan(turn: HarnessTurn): TurnPlan {
|
|
|
408
417
|
// A generation's step is its last call.
|
|
409
418
|
steer(calls.length - 1, turn.steers.get(index))
|
|
410
419
|
})
|
|
411
|
-
|
|
420
|
+
const images = await harnessImages({
|
|
421
|
+
harness: 'fx()',
|
|
422
|
+
input: turn.prompt,
|
|
423
|
+
signal,
|
|
424
|
+
})
|
|
425
|
+
return {
|
|
426
|
+
prompt:
|
|
427
|
+
images.length === 0
|
|
428
|
+
? turn.prompt.text
|
|
429
|
+
: [
|
|
430
|
+
...images.map((image): FxPromptBlock => ({
|
|
431
|
+
type: 'image',
|
|
432
|
+
data: image.data,
|
|
433
|
+
mimeType: image.mediaType,
|
|
434
|
+
})),
|
|
435
|
+
...(turn.prompt.text === ''
|
|
436
|
+
? []
|
|
437
|
+
: [{ type: 'text' as const, text: turn.prompt.text }]),
|
|
438
|
+
],
|
|
439
|
+
calls,
|
|
440
|
+
steers,
|
|
441
|
+
end: turn.end,
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** libfx steers with text alone, so a steer's files fail the step. */
|
|
446
|
+
function steerText(input: HarnessInput): string {
|
|
447
|
+
if (input.files.length > 0)
|
|
448
|
+
throw new Error(
|
|
449
|
+
'fx() steers with text only: send files in a message of their own',
|
|
450
|
+
)
|
|
451
|
+
return input.text
|
|
412
452
|
}
|
|
413
453
|
|
|
414
454
|
/** A completed generation's model calls: its compactions, then its step. */
|
|
@@ -764,15 +804,13 @@ async function kernelTools(
|
|
|
764
804
|
execute: (toolName: string, input: unknown) => unknown,
|
|
765
805
|
): Promise<FxHostTool[]> {
|
|
766
806
|
return Promise.all(
|
|
767
|
-
Object.entries(tools)
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
execute: (input: unknown) => execute(toolName, input),
|
|
775
|
-
})),
|
|
807
|
+
Object.entries(tools).map(async ([toolName, tool]) => ({
|
|
808
|
+
name: toolName,
|
|
809
|
+
description:
|
|
810
|
+
typeof tool.description === 'string' ? tool.description : toolName,
|
|
811
|
+
inputSchema: (await asSchema(tool.inputSchema).jsonSchema) as object,
|
|
812
|
+
execute: (input: unknown) => execute(toolName, input),
|
|
813
|
+
})),
|
|
776
814
|
)
|
|
777
815
|
}
|
|
778
816
|
|