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.
Files changed (152) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/{actor-Bjce9_5U.d.ts → actor-C7b-1arn.d.ts} +3 -3
  3. package/dist/{actor-Bjce9_5U.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-react.d.ts +1 -1
  6. package/dist/actor-react.js +1 -1
  7. package/dist/actor.d.ts +1 -1
  8. package/dist/actor.js +2 -2
  9. package/dist/{ai-DrUaMyRt.d.ts → ai-Cz3hqVhk.d.ts} +34 -17
  10. package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
  11. package/dist/ai-client.d.ts +1 -1
  12. package/dist/ai-client.js +2 -2
  13. package/dist/ai-control-CvLbW10B.js +46 -0
  14. package/dist/ai-control-CvLbW10B.js.map +1 -0
  15. package/dist/ai-fx.d.ts +1 -1
  16. package/dist/ai-fx.d.ts.map +1 -1
  17. package/dist/ai-fx.js +122 -44
  18. package/dist/ai-fx.js.map +1 -1
  19. package/dist/{ai-harness-BuLh_EYi.js → ai-harness-BuuC-gJ3.js} +79 -15
  20. package/dist/ai-harness-BuuC-gJ3.js.map +1 -0
  21. package/dist/{ai-harness-DvsPeJeB.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
  22. package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
  23. package/dist/{ai-BpOGm-zB.js → ai-mMiWul1T.js} +117 -78
  24. package/dist/ai-mMiWul1T.js.map +1 -0
  25. package/dist/ai-pi.d.ts +1 -1
  26. package/dist/ai-pi.d.ts.map +1 -1
  27. package/dist/ai-pi.js +114 -23
  28. package/dist/ai-pi.js.map +1 -1
  29. package/dist/ai-react.d.ts +1 -1
  30. package/dist/ai-react.js +2 -2
  31. package/dist/ai-server.d.ts +3 -3
  32. package/dist/ai-server.d.ts.map +1 -1
  33. package/dist/ai-server.js +127 -55
  34. package/dist/ai-server.js.map +1 -1
  35. package/dist/ai-tool-input-DmSqMUQi.js +40 -0
  36. package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +1 -1
  39. package/dist/{client-BReKqwZC.js → client-BhHLoNKF.js} +3 -3
  40. package/dist/{client-BReKqwZC.js.map → client-BhHLoNKF.js.map} +1 -1
  41. package/dist/client.js +1 -1
  42. package/dist/index.d.ts +2 -2
  43. package/dist/{internal-B2T3U4Rq.js → internal-CscSY7K_.js} +4 -2
  44. package/dist/{internal-B2T3U4Rq.js.map → internal-CscSY7K_.js.map} +1 -1
  45. package/dist/react.js +1 -1
  46. package/dist/scheduler-qstash.d.ts +2 -2
  47. package/dist/scheduler-qstash.js +3 -3
  48. package/dist/scheduler-qstash.js.map +1 -1
  49. package/dist/scheduler-vercel.d.ts +7 -7
  50. package/dist/scheduler-vercel.d.ts.map +1 -1
  51. package/dist/scheduler-vercel.js +10 -14
  52. package/dist/scheduler-vercel.js.map +1 -1
  53. package/dist/{server-Dw_k_7kO.js → server-D-Y4pqwu.js} +37 -11
  54. package/dist/server-D-Y4pqwu.js.map +1 -0
  55. package/dist/{server-En7cbi3v.d.ts → server-DYAATFuT.d.ts} +11 -5
  56. package/dist/server-DYAATFuT.d.ts.map +1 -0
  57. package/dist/server.d.ts +2 -2
  58. package/dist/server.js +1 -1
  59. package/dist/{session-clear-Bo-XTFEy.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
  60. package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
  61. package/dist/{store-Djdv0bnZ.d.ts → store-CyLUy86v.d.ts} +23 -4
  62. package/dist/store-CyLUy86v.d.ts.map +1 -0
  63. package/dist/store-N8PXxDAS.js.map +1 -1
  64. package/dist/store-memory.d.ts +1 -1
  65. package/dist/store-memory.js +25 -6
  66. package/dist/store-memory.js.map +1 -1
  67. package/dist/{store-polling-B5FcwJfo.js → store-polling-7WJV0YWP.js} +2 -2
  68. package/dist/{store-polling-B5FcwJfo.js.map → store-polling-7WJV0YWP.js.map} +1 -1
  69. package/dist/store-postgres.d.ts +1 -1
  70. package/dist/store-postgres.d.ts.map +1 -1
  71. package/dist/store-postgres.js +55 -25
  72. package/dist/store-postgres.js.map +1 -1
  73. package/dist/{store-presence-polling-BAwS8RWn.js → store-presence-polling-B1ixeqIS.js} +3 -3
  74. package/dist/{store-presence-polling-BAwS8RWn.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
  75. package/dist/store-redis-http.d.ts +1 -1
  76. package/dist/store-redis-http.js +3 -3
  77. package/dist/{store-redis-notify-BRITti_o.js → store-redis-notify-CM0AfLY0.js} +88 -37
  78. package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
  79. package/dist/store-redis.d.ts +1 -1
  80. package/dist/store-redis.js +1 -1
  81. package/dist/store-sqlite.d.ts +1 -1
  82. package/dist/store-sqlite.js +35 -20
  83. package/dist/store-sqlite.js.map +1 -1
  84. package/dist/{wire-rSUglYo8.js → wire-BKxH1vbP.js} +2 -2
  85. package/dist/{wire-rSUglYo8.js.map → wire-BKxH1vbP.js.map} +1 -1
  86. package/docs/agents/03-conversations.mdx +27 -15
  87. package/docs/agents/04-harnesses.mdx +51 -5
  88. package/docs/contract/03-sessions.mdx +1 -1
  89. package/docs/index.mdx +3 -3
  90. package/docs/reference/02-server.mdx +12 -7
  91. package/docs/reference/04-ai.mdx +41 -23
  92. package/docs/reference/05-adapters.mdx +6 -6
  93. package/docs/server/01-handlers.mdx +1 -1
  94. package/docs/server/02-timers.mdx +1 -1
  95. package/docs/server/03-cancellation.mdx +3 -2
  96. package/docs/server/05-production.mdx +4 -3
  97. package/docs/server/06-guarantees.mdx +20 -11
  98. package/examples/playground/DEPLOY.md +11 -4
  99. package/examples/playground/app/a0/[appId]/page.tsx +2 -8
  100. package/examples/playground/app/a0/a0.css +244 -5
  101. package/examples/playground/app/a0/app-view.tsx +2 -1
  102. package/examples/playground/app/a0/attachments.ts +133 -0
  103. package/examples/playground/app/a0/chat.test.tsx +504 -0
  104. package/examples/playground/app/a0/chat.tsx +623 -63
  105. package/examples/playground/app/a0/context-menu.tsx +38 -11
  106. package/examples/playground/app/a0/devtools.tsx +2 -8
  107. package/examples/playground/app/a0/model.test.ts +28 -0
  108. package/examples/playground/app/a0/model.ts +30 -0
  109. package/examples/playground/app/a0/server.ts +13 -19
  110. package/examples/playground/app/a0/uploads/route.ts +29 -0
  111. package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
  112. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +1 -0
  113. package/examples/playground/app/agent/model.test.ts +12 -3
  114. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
  115. package/examples/playground/next.config.ts +7 -4
  116. package/examples/playground/package.json +3 -1
  117. package/package.json +1 -1
  118. package/src/ai-agent.ts +14 -3
  119. package/src/ai-client-state.ts +21 -9
  120. package/src/ai-context-schema.ts +28 -4
  121. package/src/ai-context.ts +83 -3
  122. package/src/ai-control-server.ts +70 -43
  123. package/src/ai-control-state.ts +3 -4
  124. package/src/ai-control.ts +59 -10
  125. package/src/ai-coordinator.ts +7 -6
  126. package/src/ai-fx.ts +184 -67
  127. package/src/ai-harness.ts +153 -29
  128. package/src/ai-message-shape.ts +41 -0
  129. package/src/ai-pi.ts +161 -46
  130. package/src/ai-stored-state.ts +1 -3
  131. package/src/ai-tool-input.ts +45 -0
  132. package/src/ai.ts +116 -77
  133. package/src/internal.ts +6 -0
  134. package/src/libfx.d.ts +8 -1
  135. package/src/scheduler-qstash.ts +5 -5
  136. package/src/scheduler-vercel.ts +15 -27
  137. package/src/server.ts +58 -12
  138. package/src/store-memory.ts +50 -9
  139. package/src/store-postgres.ts +79 -28
  140. package/src/store-redis-core.ts +112 -31
  141. package/src/store-sqlite.ts +61 -28
  142. package/src/store.ts +22 -3
  143. package/dist/ai-BpOGm-zB.js.map +0 -1
  144. package/dist/ai-DrUaMyRt.d.ts.map +0 -1
  145. package/dist/ai-harness-BuLh_EYi.js.map +0 -1
  146. package/dist/ai-harness-DvsPeJeB.d.ts.map +0 -1
  147. package/dist/server-Dw_k_7kO.js.map +0 -1
  148. package/dist/server-En7cbi3v.d.ts.map +0 -1
  149. package/dist/session-clear-Bo-XTFEy.d.ts.map +0 -1
  150. package/dist/store-Djdv0bnZ.d.ts.map +0 -1
  151. package/dist/store-redis-notify-BRITti_o.js.map +0 -1
  152. package/examples/playground/app/a0/no-photon.cjs +0 -4
@@ -0,0 +1,41 @@
1
+ import type { UIMessage } from 'ai'
2
+
3
+ export const isRecord = (value: unknown): value is Record<string, unknown> =>
4
+ typeof value === 'object' && value !== null && !Array.isArray(value)
5
+
6
+ export const isJSONCompatible = (
7
+ value: unknown,
8
+ seen: Set<object> = new Set(),
9
+ ): boolean => {
10
+ if (
11
+ value === null ||
12
+ typeof value === 'string' ||
13
+ typeof value === 'boolean'
14
+ ) {
15
+ return true
16
+ }
17
+ if (typeof value === 'number') return Number.isFinite(value)
18
+ if (typeof value === 'undefined') return true
19
+ if (typeof value !== 'object') return false
20
+ if (seen.has(value)) return false
21
+ seen.add(value)
22
+ const valid = Array.isArray(value)
23
+ ? value.every((item) => isJSONCompatible(item, seen))
24
+ : Object.getPrototypeOf(value) === Object.prototype &&
25
+ Object.values(value).every((item) => isJSONCompatible(item, seen))
26
+ seen.delete(value)
27
+ return valid
28
+ }
29
+
30
+ /** Whether a value has an AI SDK `UIMessage`'s shape. */
31
+ export const isUIMessage = (value: unknown): value is UIMessage =>
32
+ isRecord(value) &&
33
+ typeof value['id'] === 'string' &&
34
+ (value['role'] === 'user' ||
35
+ value['role'] === 'assistant' ||
36
+ value['role'] === 'system') &&
37
+ Array.isArray(value['parts']) &&
38
+ value['parts'].every(
39
+ (part) => isRecord(part) && typeof part['type'] === 'string',
40
+ ) &&
41
+ isJSONCompatible(value)
package/src/ai-pi.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
  AssistantMessage,
15
15
  AssistantMessageEvent,
16
16
  AssistantMessageEventStream,
17
+ ImageContent,
17
18
  JsonObject,
18
19
  TextContent,
19
20
  ThinkingContent,
@@ -50,6 +51,8 @@ import {
50
51
  gatewayCredential,
51
52
  type Harness,
52
53
  type HarnessGeneration,
54
+ harnessImages,
55
+ type HarnessInput,
53
56
  type HarnessRead,
54
57
  type HarnessSandbox,
55
58
  type HarnessStep,
@@ -59,6 +62,7 @@ import {
59
62
  type RecordedGeneration,
60
63
  saveHarness,
61
64
  } from './ai-harness.ts'
65
+ import { type SchemaInput, schemaInput } from './ai-tool-input.ts'
62
66
  import type { CompactionPartData, ToolResultPayload } from './ai.ts'
63
67
  import { boundedCache } from './bounded-cache.ts'
64
68
  import { ambientToolScopeStorage } from './internal.ts'
@@ -110,11 +114,14 @@ type PiModules = {
110
114
  ai: typeof import('@earendil-works/pi-ai')
111
115
  }
112
116
 
113
- /** A content block without the text the transcript already holds. */
117
+ /**
118
+ * A content block without the text the transcript already holds. A tool call
119
+ * keeps its arguments only where the transcript's input is not the model's.
120
+ */
114
121
  type StoredBlock =
115
122
  | (Omit<TextContent, 'text'> & { ref: string })
116
123
  | (Omit<ThinkingContent, 'thinking'> & { ref: string })
117
- | Omit<ToolCall, 'arguments'>
124
+ | (Omit<ToolCall, 'arguments'> & Partial<Pick<ToolCall, 'arguments'>>)
118
125
 
119
126
  /** What a step's transcript parts do not hold of Pi's response. */
120
127
  type StoredResponse = Omit<AssistantMessage, 'content'> & {
@@ -126,11 +133,14 @@ type PiRecord =
126
133
  | { call: 'step'; response: StoredResponse }
127
134
  | { call: 'compaction'; response: AssistantMessage }
128
135
 
136
+ /** A user message as Pi takes it. */
137
+ type PiInput = { text: string; images: ImageContent[] }
138
+
129
139
  type RecordedCall = {
130
140
  kind: 'step' | 'compaction'
131
141
  response: AssistantMessage
132
142
  /** Steering to queue while serving this call. */
133
- steers: string[]
143
+ steers: PiInput[]
134
144
  }
135
145
 
136
146
  type Saved = {
@@ -228,7 +238,8 @@ async function runStep(
228
238
  fail(state, diverged(`call ${state.next} is not a ${kind}`))
229
239
  state.next++
230
240
  // Steering queued while a response streams enters after its tools.
231
- for (const text of recorded.steers) await session!.steer(text)
241
+ for (const { text, images } of recorded.steers)
242
+ await session!.steer(text, images.length === 0 ? undefined : images)
232
243
  replay(out, recorded.response)
233
244
  return
234
245
  }
@@ -248,10 +259,13 @@ async function runStep(
248
259
  const upstream = realStream(model, context, {
249
260
  ...options,
250
261
  headers: { ...options?.headers, ...gatewayHeaders(credential) },
262
+ ...(step.speed === 'fast'
263
+ ? { onPayload: fastTier(options?.onPayload) }
264
+ : {}),
251
265
  })
252
266
  const live = kind === 'step' ? liveStep() : undefined
253
267
  for await (const event of upstream) {
254
- live?.(event)
268
+ await live?.(event)
255
269
  if (event.type === 'done' || event.type === 'error') {
256
270
  const response = event.type === 'done' ? event.message : event.error
257
271
  if (kind === 'compaction') state.compactions.push(response)
@@ -267,11 +281,39 @@ async function runStep(
267
281
  }
268
282
  }
269
283
 
284
+ /**
285
+ * A live call's input as Pi's loop, then the AI SDK step, read it. Pi
286
+ * answers a call to no tool, or whose arguments it cannot prepare or
287
+ * validate, itself. The agent's tool reads the arguments Pi accepts through
288
+ * its own schema; A2 runs Pi's own tools from the model's arguments, as
289
+ * Pi's loop does.
290
+ */
291
+ const callInput = async (block: ToolCall): Promise<SchemaInput> => {
292
+ const definition = tools.find((tool) => tool.name === block.name)
293
+ if (definition === undefined)
294
+ return { success: false, errorText: `Tool ${block.name} not found` }
295
+ let args: unknown
296
+ try {
297
+ const prepared = definition.prepareArguments?.(block.arguments)
298
+ args = modules.ai.validateToolArguments(
299
+ definition,
300
+ prepared === undefined
301
+ ? block
302
+ : { ...block, arguments: prepared as JsonObject },
303
+ )
304
+ } catch (error) {
305
+ return { success: false, errorText: asError(error).message }
306
+ }
307
+ if (builtinOf(step.tools, block.name) !== undefined)
308
+ return { success: true, value: block.arguments }
309
+ return schemaInput({ tools: step.tools, toolName: block.name, value: args })
310
+ }
311
+
270
312
  /** Streams one live step response as UI chunks and records it. */
271
313
  const liveStep = () => {
272
314
  const ref = (index: number) => `pi:${step.requestIndex}:${index}`
273
315
  const started = new Set<string>()
274
- return (event: AssistantMessageEvent): void => {
316
+ return async (event: AssistantMessageEvent): Promise<void> => {
275
317
  switch (event.type) {
276
318
  case 'start':
277
319
  emit({ type: 'start-step' })
@@ -325,6 +367,8 @@ async function runStep(
325
367
  case 'done':
326
368
  case 'error': {
327
369
  const response = event.type === 'done' ? event.message : event.error
370
+ // Calls whose transcript input is not the model's arguments.
371
+ const changed = new Set<string>()
328
372
  if (event.type === 'done') {
329
373
  for (const block of response.content) {
330
374
  if (block.type !== 'toolCall') continue
@@ -335,26 +379,41 @@ async function runStep(
335
379
  toolName: block.name,
336
380
  })
337
381
  // Pi does not run the calls of a cut-off response.
338
- emit(
382
+ const input: SchemaInput =
339
383
  response.stopReason === 'length'
340
384
  ? {
341
- type: 'tool-input-error',
342
- toolCallId: block.id,
343
- toolName: block.name,
344
- input: block.arguments,
385
+ success: false,
345
386
  errorText:
346
387
  'The response hit the output token limit before this call was complete.',
347
388
  }
348
- : {
389
+ : await callInput(block)
390
+ if (
391
+ input.success &&
392
+ JSON.stringify(input.value) !== JSON.stringify(block.arguments)
393
+ )
394
+ changed.add(block.id)
395
+ emit(
396
+ input.success
397
+ ? {
349
398
  type: 'tool-input-available',
350
399
  toolCallId: block.id,
351
400
  toolName: block.name,
401
+ input: input.value,
402
+ }
403
+ : {
404
+ type: 'tool-input-error',
405
+ toolCallId: block.id,
406
+ toolName: block.name,
352
407
  input: block.arguments,
408
+ errorText: input.errorText,
353
409
  },
354
410
  )
355
411
  }
356
412
  }
357
- record({ call: 'step', response: storedResponse(response, ref) })
413
+ record({
414
+ call: 'step',
415
+ response: storedResponse(response, ref, changed),
416
+ })
358
417
  completion = {
359
418
  finishReason: finishReason(response.stopReason),
360
419
  usage: modelUsage(response.usage),
@@ -498,9 +557,10 @@ async function runStep(
498
557
  }
499
558
  for (const plan of read.turns) {
500
559
  if (step.signal.aborted) return
560
+ const prompt = await piInput({ input: plan.prompt, signal: step.signal })
501
561
  const state: TurnState = {
502
562
  plan,
503
- calls: recordedTurn(plan),
563
+ calls: await recordedTurn({ turn: plan, signal: step.signal }),
504
564
  next: 0,
505
565
  liveStep: false,
506
566
  over: false,
@@ -508,8 +568,9 @@ async function runStep(
508
568
  compactions: [],
509
569
  }
510
570
  turn = state
511
- const running = session.prompt(plan.prompt, {
571
+ const running = session.prompt(prompt.text, {
512
572
  expandPromptTemplates: false,
573
+ ...(prompt.images.length === 0 ? {} : { images: prompt.images }),
513
574
  })
514
575
  if (plan.end !== 'live') {
515
576
  await running.catch((error: unknown) => {
@@ -573,17 +634,43 @@ async function runStep(
573
634
  // ------------------------------------------------------------ the plan
574
635
 
575
636
  /** A turn's recorded model calls, in the order Pi made them. */
576
- function recordedTurn(turn: HarnessTurn): RecordedCall[] {
637
+ async function recordedTurn({
638
+ turn,
639
+ signal,
640
+ }: {
641
+ turn: HarnessTurn
642
+ signal: AbortSignal
643
+ }): Promise<RecordedCall[]> {
577
644
  const calls: RecordedCall[] = []
578
- turn.generations.forEach((generation, index) => {
645
+ for (const [index, generation] of turn.generations.entries()) {
579
646
  calls.push(...recordedCalls(generation))
580
647
  const steers = turn.steers.get(index)
581
648
  const last = calls.findLast((call) => call.kind === 'step')
582
- if (steers !== undefined && last !== undefined) last.steers.push(...steers)
583
- })
649
+ if (steers !== undefined && last !== undefined)
650
+ for (const input of steers)
651
+ last.steers.push(await piInput({ input, signal }))
652
+ }
584
653
  return calls
585
654
  }
586
655
 
656
+ async function piInput({
657
+ input,
658
+ signal,
659
+ }: {
660
+ input: HarnessInput
661
+ signal: AbortSignal
662
+ }): Promise<PiInput> {
663
+ const images = await harnessImages({ harness: 'pi()', input, signal })
664
+ return {
665
+ text: input.text,
666
+ images: images.map((image) => ({
667
+ type: 'image',
668
+ data: image.data,
669
+ mimeType: image.mediaType,
670
+ })),
671
+ }
672
+ }
673
+
587
674
  function recordedCalls(generation: RecordedGeneration): RecordedCall[] {
588
675
  const texts = new Map<string, string>()
589
676
  const inputs = new Map<string, unknown>()
@@ -620,6 +707,7 @@ function recordedCalls(generation: RecordedGeneration): RecordedCall[] {
620
707
  function storedResponse(
621
708
  response: AssistantMessage,
622
709
  ref: (index: number) => string,
710
+ changed: ReadonlySet<string>,
623
711
  ): StoredResponse {
624
712
  return {
625
713
  ...response,
@@ -634,6 +722,7 @@ function storedResponse(
634
722
  return { ...rest, ref: ref(index) }
635
723
  }
636
724
  case 'toolCall': {
725
+ if (changed.has(block.id)) return block
637
726
  const { arguments: _, ...rest } = block
638
727
  return rest
639
728
  }
@@ -653,7 +742,8 @@ function restoredResponse(
653
742
  if (block.type === 'toolCall')
654
743
  return {
655
744
  ...block,
656
- arguments: (inputs.get(block.id) ?? {}) as JsonObject,
745
+ arguments:
746
+ block.arguments ?? ((inputs.get(block.id) ?? {}) as JsonObject),
657
747
  }
658
748
  const { ref, ...rest } = block
659
749
  const text = texts.get(ref) ?? ''
@@ -826,6 +916,15 @@ async function recordedResult(
826
916
  return { ...kept, details: { ...details, truncation } }
827
917
  }
828
918
 
919
+ /** The Pi tool a name in the agent's tool set runs, unless the agent replaced it. */
920
+ const builtinOf = (tools: ToolSet, name: string): Builtin | undefined =>
921
+ BUILTINS.find(
922
+ (candidate) =>
923
+ candidate === name &&
924
+ Object.hasOwn(tools, name) &&
925
+ tools[name] === builtinTools[name],
926
+ )
927
+
829
928
  // Each definition is typed by its parameters; a session takes any of them.
830
929
  const untyped = (definition: object) => definition as ToolDefinition
831
930
 
@@ -876,32 +975,28 @@ async function sessionTools(
876
975
  ) => Promise<AgentToolResult<unknown>>,
877
976
  ): Promise<ToolDefinition[]> {
878
977
  return Promise.all(
879
- Object.entries(tools)
880
- .filter(([, tool]) => tool.type !== 'provider')
881
- .map(async ([name, tool]): Promise<ToolDefinition> => {
882
- const builtin = BUILTINS.find(
883
- (candidate) => candidate === name && builtinTools[name] === tool,
884
- )
885
- if (builtin !== undefined)
886
- // Pi's own definition, for its prompt; A2 already ran the call.
887
- return Object.assign(builtinDefinition(modules, builtin, cwd), {
888
- execute: (
889
- toolCallId: string,
890
- _params: unknown,
891
- signal?: AbortSignal,
892
- ) => result(toolCallId, signal, 'pi'),
893
- })
894
- return {
895
- name,
896
- label: name,
897
- description:
898
- typeof tool.description === 'string' ? tool.description : '',
899
- parameters: (await asSchema(tool.inputSchema)
900
- .jsonSchema) as ToolDefinition['parameters'],
901
- execute: (toolCallId, _params, signal) =>
902
- result(toolCallId, signal, 'tool'),
903
- }
904
- }),
978
+ Object.entries(tools).map(async ([name, tool]): Promise<ToolDefinition> => {
979
+ const builtin = builtinOf(tools, name)
980
+ if (builtin !== undefined)
981
+ // Pi's own definition, for its prompt; A2 already ran the call.
982
+ return Object.assign(builtinDefinition(modules, builtin, cwd), {
983
+ execute: (
984
+ toolCallId: string,
985
+ _params: unknown,
986
+ signal?: AbortSignal,
987
+ ) => result(toolCallId, signal, 'pi'),
988
+ })
989
+ return {
990
+ name,
991
+ label: name,
992
+ description:
993
+ typeof tool.description === 'string' ? tool.description : '',
994
+ parameters: (await asSchema(tool.inputSchema)
995
+ .jsonSchema) as ToolDefinition['parameters'],
996
+ execute: (toolCallId, _params, signal) =>
997
+ result(toolCallId, signal, 'tool'),
998
+ }
999
+ }),
905
1000
  )
906
1001
  }
907
1002
 
@@ -1168,6 +1263,26 @@ function thinkingLevel(step: HarnessStep): {
1168
1263
  return { thinkingLevel: step.reasoning === 'none' ? 'off' : step.reasoning }
1169
1264
  }
1170
1265
 
1266
+ type PayloadHook = NonNullable<
1267
+ NonNullable<Parameters<ModelRuntime['streamSimple']>[2]>['onPayload']
1268
+ >
1269
+
1270
+ /** Asks AI Gateway for the model's fast tier, on the payload Pi's own hook returns. */
1271
+ function fastTier(inner: PayloadHook | undefined): PayloadHook {
1272
+ return async (payload, model) => {
1273
+ const body = ((await inner?.(payload, model)) ?? payload) as {
1274
+ providerOptions?: { gateway?: object }
1275
+ }
1276
+ return {
1277
+ ...body,
1278
+ providerOptions: {
1279
+ ...body.providerOptions,
1280
+ gateway: { ...body.providerOptions?.gateway, speed: 'fast' },
1281
+ },
1282
+ }
1283
+ }
1284
+ }
1285
+
1171
1286
  function gatewayHeaders(credential: {
1172
1287
  token: string
1173
1288
  method: string
@@ -211,9 +211,7 @@ const storeState = <S extends AIStoredView>(state: S): AIStoredState => {
211
211
  if (
212
212
  value === undefined ||
213
213
  (typeof value === 'string' && value.length > 80) ||
214
- (field !== undefined &&
215
- payloadFields.has(field) &&
216
- !(field === 'input' && isRecord(value) && 'message' in value)) ||
214
+ (field !== undefined && payloadFields.has(field)) ||
217
215
  (isRecord(value) && Object.hasOwn(value, '$a2Value'))
218
216
  )
219
217
  return intern(value)
@@ -0,0 +1,45 @@
1
+ import {
2
+ asSchema,
3
+ InvalidToolInputError,
4
+ type ToolSet,
5
+ TypeValidationError,
6
+ } from 'ai'
7
+
8
+ export type SchemaInput =
9
+ { success: true; value: unknown } | { success: false; errorText: string }
10
+
11
+ /**
12
+ * A call's input through its tool's schema, as the AI SDK step reads it: the
13
+ * schema's output with its defaults, or the AI SDK's error text. A name the
14
+ * tool set does not own has no schema.
15
+ */
16
+ export async function schemaInput({
17
+ tools,
18
+ toolName,
19
+ value,
20
+ }: {
21
+ tools: ToolSet
22
+ toolName: string
23
+ value: unknown
24
+ }): Promise<SchemaInput> {
25
+ const schema = Object.hasOwn(tools, toolName)
26
+ ? asSchema(tools[toolName]!.inputSchema)
27
+ : undefined
28
+ if (schema?.validate === undefined) return { success: true, value }
29
+ let cause: unknown
30
+ try {
31
+ const result = await schema.validate(value)
32
+ if (result.success) return { success: true, value: result.value }
33
+ cause = result.error
34
+ } catch (error) {
35
+ cause = error
36
+ }
37
+ return {
38
+ success: false,
39
+ errorText: new InvalidToolInputError({
40
+ toolName,
41
+ toolInput: JSON.stringify(value),
42
+ cause: TypeValidationError.wrap({ value, cause }),
43
+ }).message,
44
+ }
45
+ }