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.
Files changed (78) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/{ai-BMLlI2Ff.d.ts → ai-Cz3hqVhk.d.ts} +33 -16
  3. package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
  4. package/dist/ai-client.d.ts +1 -1
  5. package/dist/ai-client.js +1 -1
  6. package/dist/ai-control-CvLbW10B.js +46 -0
  7. package/dist/ai-control-CvLbW10B.js.map +1 -0
  8. package/dist/ai-fx.d.ts +1 -1
  9. package/dist/ai-fx.d.ts.map +1 -1
  10. package/dist/ai-fx.js +30 -10
  11. package/dist/ai-fx.js.map +1 -1
  12. package/dist/{ai-harness-BuLh_EYi.js → ai-harness-BuuC-gJ3.js} +79 -15
  13. package/dist/ai-harness-BuuC-gJ3.js.map +1 -0
  14. package/dist/{ai-harness-B2suP2Qh.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
  15. package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
  16. package/dist/{ai-D6mGlO8j.js → ai-mMiWul1T.js} +116 -77
  17. package/dist/ai-mMiWul1T.js.map +1 -0
  18. package/dist/ai-pi.d.ts +1 -1
  19. package/dist/ai-pi.d.ts.map +1 -1
  20. package/dist/ai-pi.js +55 -10
  21. package/dist/ai-pi.js.map +1 -1
  22. package/dist/ai-react.d.ts +1 -1
  23. package/dist/ai-react.js +1 -1
  24. package/dist/ai-server.d.ts +2 -2
  25. package/dist/ai-server.d.ts.map +1 -1
  26. package/dist/ai-server.js +118 -50
  27. package/dist/ai-server.js.map +1 -1
  28. package/dist/ai.d.ts +2 -2
  29. package/dist/ai.js +1 -1
  30. package/dist/scheduler-vercel.d.ts.map +1 -1
  31. package/dist/scheduler-vercel.js +1 -4
  32. package/dist/scheduler-vercel.js.map +1 -1
  33. package/docs/agents/03-conversations.mdx +25 -13
  34. package/docs/agents/04-harnesses.mdx +51 -5
  35. package/docs/contract/03-sessions.mdx +1 -1
  36. package/docs/reference/04-ai.mdx +27 -18
  37. package/docs/reference/05-adapters.mdx +2 -2
  38. package/docs/server/02-timers.mdx +1 -1
  39. package/docs/server/05-production.mdx +2 -2
  40. package/examples/playground/DEPLOY.md +9 -0
  41. package/examples/playground/app/a0/a0.css +244 -5
  42. package/examples/playground/app/a0/app-view.tsx +2 -1
  43. package/examples/playground/app/a0/attachments.ts +133 -0
  44. package/examples/playground/app/a0/chat.test.tsx +504 -0
  45. package/examples/playground/app/a0/chat.tsx +623 -63
  46. package/examples/playground/app/a0/context-menu.tsx +38 -11
  47. package/examples/playground/app/a0/model.test.ts +28 -0
  48. package/examples/playground/app/a0/model.ts +30 -0
  49. package/examples/playground/app/a0/server.ts +5 -0
  50. package/examples/playground/app/a0/uploads/route.ts +29 -0
  51. package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
  52. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +1 -0
  53. package/examples/playground/app/agent/model.test.ts +12 -3
  54. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
  55. package/examples/playground/next.config.ts +7 -4
  56. package/examples/playground/package.json +3 -1
  57. package/package.json +1 -1
  58. package/src/ai-agent.ts +4 -0
  59. package/src/ai-client-state.ts +21 -9
  60. package/src/ai-context-schema.ts +28 -4
  61. package/src/ai-context.ts +83 -3
  62. package/src/ai-control-server.ts +70 -43
  63. package/src/ai-control-state.ts +3 -4
  64. package/src/ai-control.ts +59 -10
  65. package/src/ai-coordinator.ts +7 -6
  66. package/src/ai-fx.ts +58 -20
  67. package/src/ai-harness.ts +153 -29
  68. package/src/ai-message-shape.ts +41 -0
  69. package/src/ai-pi.ts +88 -32
  70. package/src/ai-stored-state.ts +1 -3
  71. package/src/ai.ts +116 -77
  72. package/src/libfx.d.ts +8 -1
  73. package/src/scheduler-vercel.ts +1 -12
  74. package/dist/ai-BMLlI2Ff.d.ts.map +0 -1
  75. package/dist/ai-D6mGlO8j.js.map +0 -1
  76. package/dist/ai-harness-B2suP2Qh.d.ts.map +0 -1
  77. package/dist/ai-harness-BuLh_EYi.js.map +0 -1
  78. package/examples/playground/app/a0/no-photon.cjs +0 -4
@@ -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<M>>
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.v5',
85
- initialState: initialControlState<M>(),
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<M, D>(state.coordinator, {
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 (first) state.inbox.items.unshift(candidate)
194
- else state.inbox.items.push(candidate)
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<M, D>(state.coordinator, {
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.message.role !== command.message.role)
281
+ else if (item.role !== command.message.role)
251
282
  reason = 'role-mismatch'
252
- else item.message = command.message
253
- break
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.message.role !== 'user') reason = 'not-user-input'
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
- moveInput({ from: position, to: 0 })
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
- response.failure !== undefined ||
595
- response.calls.every(
596
- (call) => callReadyForModel(call) || approvalBlocksExecution(call),
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: 'steered after step' })
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: { message: item.message, generate: item.generate },
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
- const receipt: ControlReceipt = {
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 ({
@@ -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 = <M extends UIMessage>(options: {
135
- before: AIControlState<M>
136
- after: AIControlState<M>
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
- export type InboxItem<M extends UIMessage = UIMessage> = {
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
- message: M
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<M extends UIMessage = UIMessage> = {
67
+ export type ActiveTurn = {
59
68
  turnId: string
60
- input: InboxItem<M>
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<M extends UIMessage = UIMessage> = {
88
+ export type AIControlState = {
80
89
  pendingStops: Record<string, string>
81
- inbox: { paused: boolean; items: InboxItem<M>[] }
90
+ inbox: { paused: boolean; items: InputRef[] }
82
91
  active:
83
- | (ActiveTurn<M> & {
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
- export const initialControlState = <
111
- M extends UIMessage,
112
- >(): AIControlState<M> => ({
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,
@@ -107,7 +107,7 @@ const updateResponse = (
107
107
  ? state
108
108
  : { ...state, response: update(state.response) }
109
109
 
110
- export const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
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<M>
124
- const message = payload.message
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((item) => item.messageId !== message.id),
128
+ ...state.queued.filter(
129
+ (item) => item.messageId !== payload.messageId,
130
+ ),
130
131
  {
131
- messageId: message.id,
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
- // Over libfx's bound: later steps replay from the previous snapshot.
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
- if ((init.method ?? 'GET').toUpperCase() === 'GET')
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
- let turns = read.turns.map(turnPlan)
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(turn: HarnessTurn): 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, texts: string[] | undefined) => {
403
- if (texts !== undefined) steers.set(Math.max(0, at), texts)
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
- return { prompt: turn.prompt, calls, steers, end: turn.end }
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
- .filter(([, tool]) => tool.type !== 'provider')
769
- .map(async ([toolName, tool]) => ({
770
- name: toolName,
771
- description:
772
- typeof tool.description === 'string' ? tool.description : toolName,
773
- inputSchema: (await asSchema(tool.inputSchema).jsonSchema) as object,
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