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
package/src/ai-harness.ts CHANGED
@@ -1,16 +1,20 @@
1
- import type {
2
- Experimental_SandboxSession,
3
- FinishReason,
4
- LanguageModelUsage,
5
- ToolSet,
6
- UIMessage,
7
- UIMessageChunk,
1
+ import {
2
+ createDownload,
3
+ type Experimental_SandboxSession,
4
+ type FileUIPart,
5
+ type FinishReason,
6
+ type LanguageModelUsage,
7
+ type ToolSet,
8
+ type UIMessage,
9
+ type UIMessageChunk,
8
10
  } from 'ai'
9
11
  import type {
10
12
  GenerationCompletedPayload,
13
+ MessageCreatedPayload,
11
14
  ReasoningLevel,
12
15
  ToolResultPayload,
13
16
  } from './ai.ts'
17
+ import { STEERED_REASON } from './ai-control.ts'
14
18
  import { boundedCache, type BoundedCache } from './bounded-cache.ts'
15
19
  import type { ContractEvent, EventDefs } from './contract.ts'
16
20
 
@@ -307,21 +311,21 @@ function savedEnvelope(value: unknown): SavedEnvelope | undefined {
307
311
 
308
312
  // ------------------------------------------------- the log as harness turns
309
313
 
310
- // A2's reason on `ai.message.interrupted` when a steer ends a response.
311
- const STEERED = 'steered after step'
312
-
313
314
  /** A completed generation as the log recorded it. */
314
315
  export type RecordedGeneration = {
315
316
  chunks: UIMessageChunk[]
316
317
  completed: GenerationCompletedPayload
317
318
  }
318
319
 
320
+ /** What user messages hand a harness: their text, and the files they attach. */
321
+ export type HarnessInput = { text: string; files: FileUIPart[] }
322
+
319
323
  /** One harness turn to rebuild: its prompt, completed generations, and end. */
320
324
  export type HarnessTurn = {
321
- prompt: string
325
+ prompt: HarnessInput
322
326
  generations: RecordedGeneration[]
323
327
  /** Steering that entered after the generation at that index. */
324
- steers: Map<number, string[]>
328
+ steers: Map<number, HarnessInput[]>
325
329
  end: 'stop' | 'interrupted' | 'live'
326
330
  }
327
331
 
@@ -342,15 +346,18 @@ export function harnessTurns(
342
346
  }
343
347
  type Response = {
344
348
  id: string
345
- prompt: string[]
349
+ prompt: HarnessInput[]
346
350
  generations: Generation[]
347
351
  end?: 'stop' | 'steered' | 'interrupted'
348
352
  }
349
- // Queued messages are admitted together, possibly before the snapshot.
350
- const userText = new Map<string, string>(
353
+ // Prompts take their content from the transcript: a harness agent never
354
+ // runs A2's compaction, so the step's messages hold every input admitted by
355
+ // its read, before the snapshot or after it. A session another loop
356
+ // compacted can't move to a harness.
357
+ const userInput = new Map<string, HarnessInput>(
351
358
  step.messages
352
359
  .filter((message) => message.role === 'user')
353
- .map((message) => [message.id, textOf(message)]),
360
+ .map((message) => [message.id, inputOf(message)]),
354
361
  )
355
362
  let passive: string[] = []
356
363
  const responses = new Map<string, Response>()
@@ -362,10 +369,9 @@ export function harnessTurns(
362
369
  const payload = event.payload as Record<string, unknown>
363
370
  switch (event.type) {
364
371
  case 'ai.message.created': {
365
- const message = payload['message'] as UIMessage
366
- if (message.role !== 'user') break
367
- userText.set(message.id, textOf(message))
368
- if (payload['generate'] === false) passive.push(message.id)
372
+ const admitted = event.payload as MessageCreatedPayload
373
+ if (admitted.role === 'user' && admitted.generate === false)
374
+ passive.push(admitted.messageId)
369
375
  break
370
376
  }
371
377
  case 'ai.generation.requested': {
@@ -376,7 +382,7 @@ export function harnessTurns(
376
382
  passive = []
377
383
  responses.set(id, {
378
384
  id,
379
- prompt: ids.flatMap((p) => userText.get(p) ?? []),
385
+ prompt: ids.flatMap((p) => userInput.get(p) ?? []),
380
386
  generations: [],
381
387
  })
382
388
  break
@@ -420,7 +426,7 @@ export function harnessTurns(
420
426
  const response = responses.get(payload['messageId'] as string)
421
427
  if (response)
422
428
  response.end =
423
- payload['reason'] === STEERED ? 'steered' : 'interrupted'
429
+ payload['reason'] === STEERED_REASON ? 'steered' : 'interrupted'
424
430
  break
425
431
  }
426
432
  }
@@ -429,13 +435,16 @@ export function harnessTurns(
429
435
  const turns: HarnessTurn[] = []
430
436
  let steered = false
431
437
  for (const response of responses.values()) {
432
- const text = response.prompt.join('\n\n')
438
+ const input: HarnessInput = {
439
+ text: response.prompt.map((prompt) => prompt.text).join('\n\n'),
440
+ files: response.prompt.flatMap((prompt) => prompt.files),
441
+ }
433
442
  let turn = turns.at(-1)
434
443
  if (steered && turn !== undefined) {
435
444
  const at = Math.max(0, turn.generations.length - 1)
436
- turn.steers.set(at, [...(turn.steers.get(at) ?? []), text])
445
+ turn.steers.set(at, [...(turn.steers.get(at) ?? []), input])
437
446
  } else {
438
- turn = { prompt: text, generations: [], steers: new Map(), end: 'live' }
447
+ turn = { prompt: input, generations: [], steers: new Map(), end: 'live' }
439
448
  turns.push(turn)
440
449
  }
441
450
  for (const generation of response.generations)
@@ -455,10 +464,125 @@ export function harnessTurns(
455
464
  return { turns, results }
456
465
  }
457
466
 
458
- function textOf(message: UIMessage): string {
459
- return message.parts
460
- .flatMap((part) => (part.type === 'text' ? [part.text] : []))
461
- .join('\n')
467
+ function inputOf(message: UIMessage): HarnessInput {
468
+ return {
469
+ text: message.parts
470
+ .flatMap((part) => (part.type === 'text' ? [part.text] : []))
471
+ .join('\n'),
472
+ files: message.parts.filter(
473
+ (part): part is FileUIPart => part.type === 'file',
474
+ ),
475
+ }
476
+ }
477
+
478
+ // ------------------------------------------------------------- attachments
479
+
480
+ /** An attached image as an engine takes it: its bytes in base64. */
481
+ export type HarnessImage = { mediaType: string; data: string }
482
+
483
+ // What one message's files hold in a step's memory, at most.
484
+ const MAX_ATTACHMENT_BYTES = 32 * 1024 * 1024
485
+
486
+ type Attachment = { data: string; bytes: number }
487
+
488
+ // Every step of a response replays its turn, and a URL's file doesn't change,
489
+ // so the process keeps what it fetched.
490
+ const attachments = boundedCache<string, Attachment>({
491
+ maxEntries: 256,
492
+ maxBytes: 64 * 1024 * 1024,
493
+ })
494
+
495
+ /**
496
+ * The images an input attaches, as its engine takes them: engines take
497
+ * bytes, not URLs. A file it can't attach fails the step, naming the file,
498
+ * and is never left out: the failure may pass, and the step is retried like
499
+ * any other.
500
+ */
501
+ export async function harnessImages({
502
+ harness,
503
+ input,
504
+ signal,
505
+ }: {
506
+ /** As the error names it: `fx()` or `pi()`. */
507
+ harness: string
508
+ input: HarnessInput
509
+ signal: AbortSignal
510
+ }): Promise<HarnessImage[]> {
511
+ const images: HarnessImage[] = []
512
+ let budget = MAX_ATTACHMENT_BYTES
513
+ // One at a time, so the budget bounds what the step holds.
514
+ for (const file of input.files) {
515
+ try {
516
+ if (!file.mediaType.startsWith('image/'))
517
+ throw new Error(`it takes images only, not ${file.mediaType}`)
518
+ const attached = await attachment({ url: file.url, budget, signal })
519
+ budget -= attached.bytes
520
+ images.push({
521
+ mediaType: file.mediaType,
522
+ data: attached.data,
523
+ })
524
+ } catch (error) {
525
+ if (signal.aborted) throw error
526
+ throw new Error(
527
+ `${harness} couldn't attach ${file.filename ?? 'a file'}: ${
528
+ error instanceof Error ? error.message : String(error)
529
+ }`,
530
+ { cause: error },
531
+ )
532
+ }
533
+ }
534
+ return images
535
+ }
536
+
537
+ async function attachment({
538
+ url,
539
+ budget,
540
+ signal,
541
+ }: {
542
+ url: string
543
+ budget: number
544
+ signal: AbortSignal
545
+ }): Promise<Attachment> {
546
+ const tooLarge = () =>
547
+ new Error(
548
+ `a message's files may hold ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MiB in a step`,
549
+ )
550
+ const cached = attachments.get(url)
551
+ if (cached !== undefined) {
552
+ if (cached.bytes > budget) throw tooLarge()
553
+ return cached
554
+ }
555
+ const { protocol } = new URL(url)
556
+ if (protocol !== 'https:' && protocol !== 'data:')
557
+ throw new Error(`attachments must be https: or data: URLs, not ${protocol}`)
558
+ // AI SDK's download checks an https: URL's address and its redirects,
559
+ // which a message's sender chooses; a data: URL holds its bytes.
560
+ const bytes =
561
+ protocol === 'data:'
562
+ ? dataBytes(url)
563
+ : (
564
+ await createDownload({ maxBytes: budget })({
565
+ url: new URL(url),
566
+ abortSignal: signal,
567
+ })
568
+ ).data
569
+ if (bytes.byteLength > budget) throw tooLarge()
570
+ const attached: Attachment = {
571
+ data: Buffer.from(bytes).toString('base64'),
572
+ bytes: bytes.byteLength,
573
+ }
574
+ if (protocol === 'https:')
575
+ attachments.set(url, attached, attached.data.length)
576
+ return attached
577
+ }
578
+
579
+ function dataBytes(url: string): Uint8Array {
580
+ const comma = url.indexOf(',')
581
+ if (comma === -1) throw new Error('its data: URL has no data')
582
+ const body = url.slice(comma + 1)
583
+ return url.slice(0, comma).endsWith(';base64')
584
+ ? Buffer.from(body, 'base64')
585
+ : Buffer.from(decodeURIComponent(body))
462
586
  }
463
587
 
464
588
  // ------------------------------------------------------------- Gateway
@@ -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,
@@ -130,11 +133,14 @@ type PiRecord =
130
133
  | { call: 'step'; response: StoredResponse }
131
134
  | { call: 'compaction'; response: AssistantMessage }
132
135
 
136
+ /** A user message as Pi takes it. */
137
+ type PiInput = { text: string; images: ImageContent[] }
138
+
133
139
  type RecordedCall = {
134
140
  kind: 'step' | 'compaction'
135
141
  response: AssistantMessage
136
142
  /** Steering to queue while serving this call. */
137
- steers: string[]
143
+ steers: PiInput[]
138
144
  }
139
145
 
140
146
  type Saved = {
@@ -232,7 +238,8 @@ async function runStep(
232
238
  fail(state, diverged(`call ${state.next} is not a ${kind}`))
233
239
  state.next++
234
240
  // Steering queued while a response streams enters after its tools.
235
- 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)
236
243
  replay(out, recorded.response)
237
244
  return
238
245
  }
@@ -252,6 +259,9 @@ async function runStep(
252
259
  const upstream = realStream(model, context, {
253
260
  ...options,
254
261
  headers: { ...options?.headers, ...gatewayHeaders(credential) },
262
+ ...(step.speed === 'fast'
263
+ ? { onPayload: fastTier(options?.onPayload) }
264
+ : {}),
255
265
  })
256
266
  const live = kind === 'step' ? liveStep() : undefined
257
267
  for await (const event of upstream) {
@@ -547,9 +557,10 @@ async function runStep(
547
557
  }
548
558
  for (const plan of read.turns) {
549
559
  if (step.signal.aborted) return
560
+ const prompt = await piInput({ input: plan.prompt, signal: step.signal })
550
561
  const state: TurnState = {
551
562
  plan,
552
- calls: recordedTurn(plan),
563
+ calls: await recordedTurn({ turn: plan, signal: step.signal }),
553
564
  next: 0,
554
565
  liveStep: false,
555
566
  over: false,
@@ -557,8 +568,9 @@ async function runStep(
557
568
  compactions: [],
558
569
  }
559
570
  turn = state
560
- const running = session.prompt(plan.prompt, {
571
+ const running = session.prompt(prompt.text, {
561
572
  expandPromptTemplates: false,
573
+ ...(prompt.images.length === 0 ? {} : { images: prompt.images }),
562
574
  })
563
575
  if (plan.end !== 'live') {
564
576
  await running.catch((error: unknown) => {
@@ -622,17 +634,43 @@ async function runStep(
622
634
  // ------------------------------------------------------------ the plan
623
635
 
624
636
  /** A turn's recorded model calls, in the order Pi made them. */
625
- function recordedTurn(turn: HarnessTurn): RecordedCall[] {
637
+ async function recordedTurn({
638
+ turn,
639
+ signal,
640
+ }: {
641
+ turn: HarnessTurn
642
+ signal: AbortSignal
643
+ }): Promise<RecordedCall[]> {
626
644
  const calls: RecordedCall[] = []
627
- turn.generations.forEach((generation, index) => {
645
+ for (const [index, generation] of turn.generations.entries()) {
628
646
  calls.push(...recordedCalls(generation))
629
647
  const steers = turn.steers.get(index)
630
648
  const last = calls.findLast((call) => call.kind === 'step')
631
- if (steers !== undefined && last !== undefined) last.steers.push(...steers)
632
- })
649
+ if (steers !== undefined && last !== undefined)
650
+ for (const input of steers)
651
+ last.steers.push(await piInput({ input, signal }))
652
+ }
633
653
  return calls
634
654
  }
635
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
+
636
674
  function recordedCalls(generation: RecordedGeneration): RecordedCall[] {
637
675
  const texts = new Map<string, string>()
638
676
  const inputs = new Map<string, unknown>()
@@ -937,30 +975,28 @@ async function sessionTools(
937
975
  ) => Promise<AgentToolResult<unknown>>,
938
976
  ): Promise<ToolDefinition[]> {
939
977
  return Promise.all(
940
- Object.entries(tools)
941
- .filter(([, tool]) => tool.type !== 'provider')
942
- .map(async ([name, tool]): Promise<ToolDefinition> => {
943
- const builtin = builtinOf(tools, name)
944
- if (builtin !== undefined)
945
- // Pi's own definition, for its prompt; A2 already ran the call.
946
- return Object.assign(builtinDefinition(modules, builtin, cwd), {
947
- execute: (
948
- toolCallId: string,
949
- _params: unknown,
950
- signal?: AbortSignal,
951
- ) => result(toolCallId, signal, 'pi'),
952
- })
953
- return {
954
- name,
955
- label: name,
956
- description:
957
- typeof tool.description === 'string' ? tool.description : '',
958
- parameters: (await asSchema(tool.inputSchema)
959
- .jsonSchema) as ToolDefinition['parameters'],
960
- execute: (toolCallId, _params, signal) =>
961
- result(toolCallId, signal, 'tool'),
962
- }
963
- }),
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
+ }),
964
1000
  )
965
1001
  }
966
1002
 
@@ -1227,6 +1263,26 @@ function thinkingLevel(step: HarnessStep): {
1227
1263
  return { thinkingLevel: step.reasoning === 'none' ? 'off' : step.reasoning }
1228
1264
  }
1229
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
+
1230
1286
  function gatewayHeaders(credential: {
1231
1287
  token: string
1232
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)