@tanstack/ai 0.59.0 → 0.63.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 (123) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/adapter.d.ts +9 -0
  3. package/dist/esm/activities/chat/adapter.js +1 -0
  4. package/dist/esm/activities/chat/adapter.js.map +1 -1
  5. package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
  6. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +8 -5
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  10. package/dist/esm/activities/chat/index.js +289 -78
  11. package/dist/esm/activities/chat/index.js.map +1 -1
  12. package/dist/esm/activities/chat/messages.js +35 -19
  13. package/dist/esm/activities/chat/messages.js.map +1 -1
  14. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  15. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  17. package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
  18. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  19. package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
  20. package/dist/esm/activities/chat/stream/processor.js +51 -28
  21. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
  23. package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
  24. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  25. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  26. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  27. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  28. package/dist/esm/activities/embed/adapter.d.ts +7 -0
  29. package/dist/esm/activities/embed/adapter.js +1 -0
  30. package/dist/esm/activities/embed/adapter.js.map +1 -1
  31. package/dist/esm/activities/embed/index.js +2 -0
  32. package/dist/esm/activities/embed/index.js.map +1 -1
  33. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  34. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  35. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  36. package/dist/esm/activities/evaluate/index.js +3 -1
  37. package/dist/esm/activities/evaluate/index.js.map +1 -1
  38. package/dist/esm/activities/files/adapter.d.ts +97 -0
  39. package/dist/esm/activities/files/adapter.js +45 -0
  40. package/dist/esm/activities/files/adapter.js.map +1 -0
  41. package/dist/esm/activities/files/index.d.ts +66 -0
  42. package/dist/esm/activities/files/index.js +78 -0
  43. package/dist/esm/activities/files/index.js.map +1 -0
  44. package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
  45. package/dist/esm/activities/generateImage/adapter.js +1 -0
  46. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  47. package/dist/esm/activities/generateImage/index.js +2 -0
  48. package/dist/esm/activities/generateImage/index.js.map +1 -1
  49. package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
  50. package/dist/esm/activities/generateVideo/adapter.js +1 -0
  51. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  52. package/dist/esm/activities/generateVideo/index.js +3 -0
  53. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  54. package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
  55. package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
  56. package/dist/esm/activities/generateWorld/index.d.ts +4 -3
  57. package/dist/esm/activities/generateWorld/index.js +5 -4
  58. package/dist/esm/activities/generateWorld/index.js.map +1 -1
  59. package/dist/esm/activities/index.d.ts +6 -3
  60. package/dist/esm/activities/index.js +13 -11
  61. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
  62. package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
  63. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  64. package/dist/esm/client.d.ts +3 -1
  65. package/dist/esm/client.js +2 -1
  66. package/dist/esm/client.js.map +1 -1
  67. package/dist/esm/index.d.ts +4 -3
  68. package/dist/esm/index.js +5 -3
  69. package/dist/esm/interrupt-resume.js +29 -4
  70. package/dist/esm/interrupt-resume.js.map +1 -1
  71. package/dist/esm/middlewares/otel.d.ts +5 -2
  72. package/dist/esm/middlewares/otel.js +114 -0
  73. package/dist/esm/middlewares/otel.js.map +1 -1
  74. package/dist/esm/types.d.ts +114 -14
  75. package/dist/esm/utilities/ag-ui-wire.js +39 -16
  76. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  77. package/dist/esm/utilities/content-source.d.ts +60 -0
  78. package/dist/esm/utilities/content-source.js +85 -0
  79. package/dist/esm/utilities/content-source.js.map +1 -0
  80. package/dist/esm/utilities/provider-executed.d.ts +7 -0
  81. package/dist/esm/utilities/provider-executed.js +10 -1
  82. package/dist/esm/utilities/provider-executed.js.map +1 -1
  83. package/dist/esm/utilities/tool-result.d.ts +14 -3
  84. package/dist/esm/utilities/tool-result.js +26 -3
  85. package/dist/esm/utilities/tool-result.js.map +1 -1
  86. package/package.json +4 -4
  87. package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
  88. package/skills/ai-core/chat-experience/SKILL.md +134 -0
  89. package/skills/ai-core/media-generation/SKILL.md +8 -0
  90. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  91. package/src/activities/chat/adapter.ts +10 -0
  92. package/src/activities/chat/agents/define-agent.ts +20 -3
  93. package/src/activities/chat/agents/spawn.ts +20 -12
  94. package/src/activities/chat/index.ts +458 -99
  95. package/src/activities/chat/messages.ts +52 -6
  96. package/src/activities/chat/middleware/types.ts +1 -0
  97. package/src/activities/chat/stream/message-updaters.ts +27 -2
  98. package/src/activities/chat/stream/processor.ts +81 -49
  99. package/src/activities/chat/tools/tool-calls.ts +104 -9
  100. package/src/activities/chat/tools/tool-definition.ts +8 -0
  101. package/src/activities/embed/adapter.ts +7 -0
  102. package/src/activities/embed/index.ts +5 -0
  103. package/src/activities/evaluate/adapter.ts +4 -0
  104. package/src/activities/evaluate/index.ts +6 -0
  105. package/src/activities/files/adapter.ts +120 -0
  106. package/src/activities/files/index.ts +113 -0
  107. package/src/activities/generateImage/adapter.ts +8 -0
  108. package/src/activities/generateImage/index.ts +4 -0
  109. package/src/activities/generateVideo/adapter.ts +8 -0
  110. package/src/activities/generateVideo/index.ts +7 -0
  111. package/src/activities/generateWorld/adapter.ts +4 -2
  112. package/src/activities/generateWorld/index.ts +7 -6
  113. package/src/activities/index.ts +25 -1
  114. package/src/activities/summarize/chat-stream-summarize.ts +22 -12
  115. package/src/client.ts +8 -0
  116. package/src/index.ts +17 -0
  117. package/src/interrupt-resume.ts +55 -4
  118. package/src/middlewares/otel.ts +161 -3
  119. package/src/types.ts +114 -14
  120. package/src/utilities/ag-ui-wire.ts +72 -17
  121. package/src/utilities/content-source.ts +138 -0
  122. package/src/utilities/provider-executed.ts +13 -0
  123. package/src/utilities/tool-result.ts +45 -3
@@ -1,7 +1,13 @@
1
1
  import { normalizeToolResult } from '../../../utilities/tool-result'
2
2
  import { tanstackMetadata } from '../../../utilities/merge-metadata'
3
+ import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
3
4
  import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
4
- import { isStandardSchema, parseWithStandardSchema } from './schema-converter'
5
+ import {
6
+ StandardSchemaValidationError,
7
+ isStandardSchema,
8
+ parseWithStandardSchema,
9
+ validateWithStandardSchema,
10
+ } from './schema-converter'
5
11
  import type { ToolApprovalResolution } from '../../../interrupts'
6
12
  import type {
7
13
  AnyTool,
@@ -18,6 +24,8 @@ import type {
18
24
  ToolCallEndEvent,
19
25
  ToolCallStartEvent,
20
26
  ToolExecutionContext,
27
+ ToolInputResponse,
28
+ ToolResultOutcome,
21
29
  ToolOutputState,
22
30
  } from '../../../types'
23
31
  import type {
@@ -461,6 +469,8 @@ export interface ToolResult {
461
469
  toolName: string
462
470
  result: any
463
471
  state?: 'output-available' | 'output-error'
472
+ /** Set when the user or middleware cancelled or denied the tool call; state is output-error. */
473
+ outcome?: ToolResultOutcome
464
474
  /** Duration of tool execution in milliseconds (only for server-executed tools) */
465
475
  duration?: number
466
476
  /**
@@ -489,9 +499,38 @@ export interface ClientToolRequest {
489
499
  input: any
490
500
  }
491
501
 
502
+ /** Form or sampling input that paused a server tool. */
503
+ export interface McpInputRequest {
504
+ toolCallId: string
505
+ toolName: string
506
+ kind: 'form' | 'sampling'
507
+ request: unknown
508
+ }
509
+
510
+ interface McpInputRequiredThrow {
511
+ name: 'MCPInputRequiredError'
512
+ kind: 'form' | 'sampling'
513
+ request: unknown
514
+ }
515
+
516
+ function isMcpInputRequired(value: unknown): value is McpInputRequiredThrow {
517
+ if (typeof value !== 'object' || value === null) return false
518
+ if (!('name' in value) || value.name !== 'MCPInputRequiredError') {
519
+ return false
520
+ }
521
+ if (!('kind' in value)) return false
522
+ const kindIsFormOrSampling =
523
+ value.kind === 'form' || value.kind === 'sampling'
524
+ if (!kindIsFormOrSampling) return false
525
+ return 'request' in value
526
+ }
527
+
492
528
  export interface ToolResumeExecutionState {
529
+ clientToolErrors?: ReadonlyMap<string, string>
493
530
  deniedToolResults?: ReadonlyMap<string, unknown>
494
531
  cancelledToolCallIds?: ReadonlySet<string>
532
+ /** Answers to `mcp_input` interrupts, by tool call id. */
533
+ inputResponses?: ReadonlyMap<string, ToolInputResponse>
495
534
  }
496
535
 
497
536
  function approvalResolution(
@@ -526,6 +565,8 @@ interface ExecuteToolCallsResult {
526
565
  needsApproval: Array<ApprovalRequest>
527
566
  /** Tools that need client-side execution */
528
567
  needsClientExecution: Array<ClientToolRequest>
568
+ /** Server tools that paused for MCP form or sampling input */
569
+ inputRequired: Array<McpInputRequest>
529
570
  /** Interrupts raised by subagents that run as tools */
530
571
  subagentInterrupts: Array<Interrupt>
531
572
  }
@@ -638,6 +679,7 @@ export async function* executeServerTool<TContext = unknown>(
638
679
  pendingEvents: Array<CustomEvent | StreamChunk>,
639
680
  results: Array<ToolResult>,
640
681
  middlewareHooks?: ToolExecutionMiddlewareHooks,
682
+ inputRequired?: Array<McpInputRequest>,
641
683
  subagentInterrupts?: Array<Interrupt>,
642
684
  ): AsyncGenerator<CustomEvent | StreamChunk, void, void> {
643
685
  const startTime = Date.now()
@@ -743,6 +785,18 @@ export async function* executeServerTool<TContext = unknown>(
743
785
  throw error
744
786
  }
745
787
 
788
+ // Same shape as MCPInputRequiredError. Pause instead of a tool error.
789
+ if (isMcpInputRequired(error)) {
790
+ if (!inputRequired) throw error
791
+ inputRequired.push({
792
+ toolCallId: toolCall.id,
793
+ toolName,
794
+ kind: error.kind,
795
+ request: error.request,
796
+ })
797
+ return
798
+ }
799
+
746
800
  const message = error instanceof Error ? error.message : 'Unknown error'
747
801
  results.push({
748
802
  toolCallId: toolCall.id,
@@ -767,17 +821,35 @@ export async function* executeServerTool<TContext = unknown>(
767
821
  }
768
822
  }
769
823
 
770
- function buildClientToolResult(
824
+ async function buildClientToolResult(
771
825
  toolCallId: string,
772
826
  toolName: string,
773
827
  tool: AnyTool,
774
828
  rawResult: unknown,
775
829
  input?: unknown,
776
- ): ToolResult {
830
+ errorText?: string,
831
+ ): Promise<ToolResult> {
832
+ if (errorText !== undefined) {
833
+ return {
834
+ toolCallId,
835
+ toolName,
836
+ result: { error: errorText },
837
+ input,
838
+ state: 'output-error',
839
+ }
840
+ }
841
+
777
842
  try {
778
843
  let result = rawResult
779
844
  if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
780
- result = parseWithStandardSchema(tool.outputSchema, result)
845
+ const validation = await validateWithStandardSchema<unknown>(
846
+ tool.outputSchema,
847
+ result,
848
+ )
849
+ if (!validation.success) {
850
+ throw new StandardSchemaValidationError(validation.issues)
851
+ }
852
+ result = validation.data
781
853
  }
782
854
 
783
855
  const parsed =
@@ -834,6 +906,7 @@ export async function* executeToolCalls<TContext = unknown>(
834
906
  const results: Array<ToolResult> = []
835
907
  const needsApproval: Array<ApprovalRequest> = []
836
908
  const needsClientExecution: Array<ClientToolRequest> = []
909
+ const inputRequired: Array<McpInputRequest> = []
837
910
  const subagentInterrupts: Array<Interrupt> = []
838
911
 
839
912
  // Create tool lookup map
@@ -854,6 +927,11 @@ export async function* executeToolCalls<TContext = unknown>(
854
927
  })
855
928
 
856
929
  for (const toolCall of toolCalls) {
930
+ // Provider-executed tools (Anthropic web_search / web_fetch) already ran
931
+ // inside the provider response and carry their result on the call's
932
+ // metadata. They have no execute() and must not become client requests.
933
+ if (isProviderExecutedToolCall(toolCall)) continue
934
+
857
935
  const tool = toolMap.get(toolCall.function.name)
858
936
  const toolName = toolCall.function.name
859
937
 
@@ -885,6 +963,7 @@ export async function* executeToolCalls<TContext = unknown>(
885
963
  toolName,
886
964
  result: { error: 'Tool execution cancelled' },
887
965
  state: 'output-error',
966
+ outcome: 'cancelled',
888
967
  })
889
968
  continue
890
969
  }
@@ -936,10 +1015,12 @@ export async function* executeToolCalls<TContext = unknown>(
936
1015
 
937
1016
  // Create a ToolExecutionContext for this tool call with event emission
938
1017
  const pendingEvents: Array<CustomEvent | StreamChunk> = []
1018
+ const inputResponse = resumeState?.inputResponses?.get(toolCall.id)
939
1019
  const context = {
940
1020
  toolCallId: toolCall.id,
941
1021
  context: userContext,
942
1022
  abortSignal,
1023
+ ...(inputResponse !== undefined ? { inputResponse } : {}),
943
1024
  emitCustomEvent: (
944
1025
  eventName: string,
945
1026
  value: Record<string, any>,
@@ -974,14 +1055,16 @@ export async function* executeToolCalls<TContext = unknown>(
974
1055
  if (approved) {
975
1056
  input = editedApprovalArgs(resolution) ?? input
976
1057
  // Approved - check if client has executed
977
- if (clientResults.has(toolCall.id)) {
1058
+ const clientError = resumeState?.clientToolErrors?.get(toolCall.id)
1059
+ if (clientResults.has(toolCall.id) || clientError !== undefined) {
978
1060
  results.push(
979
- buildClientToolResult(
1061
+ await buildClientToolResult(
980
1062
  toolCall.id,
981
1063
  toolName,
982
1064
  tool,
983
1065
  clientResults.get(toolCall.id),
984
1066
  input,
1067
+ clientError,
985
1068
  ),
986
1069
  )
987
1070
  } else {
@@ -1002,6 +1085,7 @@ export async function* executeToolCalls<TContext = unknown>(
1002
1085
  deniedApprovalResult(resolution),
1003
1086
  input,
1004
1087
  state: 'output-error',
1088
+ outcome: 'denied',
1005
1089
  })
1006
1090
  }
1007
1091
  } else {
@@ -1015,14 +1099,16 @@ export async function* executeToolCalls<TContext = unknown>(
1015
1099
  }
1016
1100
  } else {
1017
1101
  // No approval needed - check if client has executed
1018
- if (clientResults.has(toolCall.id)) {
1102
+ const clientError = resumeState?.clientToolErrors?.get(toolCall.id)
1103
+ if (clientResults.has(toolCall.id) || clientError !== undefined) {
1019
1104
  results.push(
1020
- buildClientToolResult(
1105
+ await buildClientToolResult(
1021
1106
  toolCall.id,
1022
1107
  toolName,
1023
1108
  tool,
1024
1109
  clientResults.get(toolCall.id),
1025
1110
  input,
1111
+ clientError,
1026
1112
  ),
1027
1113
  )
1028
1114
  } else {
@@ -1071,6 +1157,7 @@ export async function* executeToolCalls<TContext = unknown>(
1071
1157
  pendingEvents,
1072
1158
  results,
1073
1159
  middlewareHooks,
1160
+ inputRequired,
1074
1161
  subagentInterrupts,
1075
1162
  )
1076
1163
  } else {
@@ -1083,6 +1170,7 @@ export async function* executeToolCalls<TContext = unknown>(
1083
1170
  deniedApprovalResult(resolution),
1084
1171
  input,
1085
1172
  state: 'output-error',
1173
+ outcome: 'denied',
1086
1174
  })
1087
1175
  }
1088
1176
  } else {
@@ -1120,9 +1208,16 @@ export async function* executeToolCalls<TContext = unknown>(
1120
1208
  pendingEvents,
1121
1209
  results,
1122
1210
  middlewareHooks,
1211
+ inputRequired,
1123
1212
  subagentInterrupts,
1124
1213
  )
1125
1214
  }
1126
1215
 
1127
- return { results, needsApproval, needsClientExecution, subagentInterrupts }
1216
+ return {
1217
+ results,
1218
+ needsApproval,
1219
+ needsClientExecution,
1220
+ inputRequired,
1221
+ subagentInterrupts,
1222
+ }
1128
1223
  }
@@ -99,6 +99,7 @@ export interface ServerTool<
99
99
  outputSchema?: TOutput
100
100
  needsApproval?: TNeedsApproval
101
101
  approvalSchema?: TApprovalSchema
102
+ execution?: 'task'
102
103
  }
103
104
 
104
105
  /**
@@ -127,6 +128,7 @@ export interface ClientTool<
127
128
  outputSchema?: TOutput
128
129
  needsApproval?: TNeedsApproval
129
130
  approvalSchema?: TApprovalSchema
131
+ execution?: 'task'
130
132
  lazy?: boolean
131
133
  metadata?: Record<string, unknown>
132
134
  execute?: ToolExecuteFunction<TInput, TOutput, TContext>
@@ -158,6 +160,7 @@ export interface ToolDefinitionInstance<
158
160
  outputSchema: TOutput
159
161
  needsApproval?: TNeedsApproval
160
162
  approvalSchema: TApprovalSchema
163
+ execution?: 'task'
161
164
  readonly [toolApprovalCapability]?: {
162
165
  needsApproval: TNeedsApproval
163
166
  approvalSchema: TApprovalSchema
@@ -221,6 +224,7 @@ export type ToolDefinitionConfig<
221
224
  outputSchema?: TOutput
222
225
  lazy?: boolean
223
226
  metadata?: Record<string, unknown>
227
+ execution?: 'task'
224
228
  } & ApprovalConfig<TNeedsApproval, TApprovalSchema>
225
229
 
226
230
  /**
@@ -353,6 +357,7 @@ export function toolDefinition<
353
357
  const outputSchema = config.outputSchema as TOutput
354
358
  const approvalSchema = config.approvalSchema as TApprovalSchema
355
359
  const needsApproval = config.needsApproval as TNeedsApproval | undefined
360
+ const execution = config.execution
356
361
 
357
362
  const definition: ToolDefinition<
358
363
  TInput,
@@ -367,6 +372,7 @@ export function toolDefinition<
367
372
  outputSchema,
368
373
  approvalSchema,
369
374
  needsApproval,
375
+ execution,
370
376
  server<TContext = unknown>(
371
377
  execute: ToolExecuteFunction<TInput, TOutput, TContext>,
372
378
  ): ServerTool<
@@ -385,6 +391,7 @@ export function toolDefinition<
385
391
  outputSchema,
386
392
  approvalSchema,
387
393
  needsApproval,
394
+ execution,
388
395
  execute,
389
396
  }
390
397
  },
@@ -407,6 +414,7 @@ export function toolDefinition<
407
414
  outputSchema,
408
415
  approvalSchema,
409
416
  needsApproval,
417
+ execution,
410
418
  ...(execute !== undefined && { execute }),
411
419
  }
412
420
  },
@@ -39,6 +39,12 @@ export interface EmbeddingAdapter<
39
39
  readonly kind: 'embedding'
40
40
  /** Adapter name identifier */
41
41
  readonly name: string
42
+ /**
43
+ * Declares that this adapter can consume `{ type: 'file' }` content
44
+ * sources (provider Files API references). `embed()` rejects file sources
45
+ * in preflight for adapters that don't declare this.
46
+ */
47
+ readonly supportsFileSources?: boolean
42
48
  /** The model this adapter is configured for */
43
49
  readonly model: TModel
44
50
 
@@ -85,6 +91,7 @@ export abstract class BaseEmbeddingAdapter<
85
91
  > {
86
92
  readonly kind = 'embedding' as const
87
93
  abstract readonly name: string
94
+ readonly supportsFileSources: boolean = false
88
95
  readonly model: TModel
89
96
 
90
97
  // Type-only property - never assigned at runtime
@@ -15,6 +15,7 @@ import {
15
15
  runGenerationUsage,
16
16
  } from '../middleware/run'
17
17
  import { countEmbeddingInputModalities } from '../../utilities/embedding-input'
18
+ import { assertPromptFileSourceSupport } from '../../utilities/content-source'
18
19
  import type { InternalLogger } from '../../logger/internal-logger'
19
20
  import type { DebugOption } from '../../logger/types'
20
21
  import type { GenerationMiddleware } from '../middleware/types'
@@ -190,6 +191,10 @@ export async function embed<
190
191
  TAdapter extends EmbeddingAdapter<string, any, any, any>,
191
192
  >(options: EmbedOptions<TAdapter>): Promise<EmbeddingResult> {
192
193
  const { adapter, middleware } = options
194
+ // Fail closed on `{ type: 'file' }` sources before middleware start. No
195
+ // embedding adapter consumes file handles today; this matches chat /
196
+ // generateImage / generateVideo.
197
+ assertPromptFileSourceSupport(adapter, options.input)
193
198
  const model = adapter.model
194
199
  const requestId = createId('embedding')
195
200
  const startTime = Date.now()
@@ -131,6 +131,10 @@ export interface EvaluateAdapterResult {
131
131
  model: string
132
132
  answers: Record<string, WireAnswer>
133
133
  usage: TokenUsage
134
+ /** Provider response id, for example to look the request up later. */
135
+ id?: string
136
+ /** Upstream provider that served the request, when a router reports it. */
137
+ provider?: string
134
138
  }
135
139
 
136
140
  /**
@@ -100,6 +100,10 @@ export interface EvaluateResultMeta {
100
100
  /** Resolved model id from the provider. */
101
101
  model: string
102
102
  usage: TokenUsage
103
+ /** Provider response id, when the adapter returns one. */
104
+ id?: string
105
+ /** Upstream provider that served the request, when the adapter returns one. */
106
+ provider?: string
103
107
  }
104
108
 
105
109
  /**
@@ -576,6 +580,8 @@ export async function decide<
576
580
  return withMeta(answers, {
577
581
  model: result.model,
578
582
  usage: result.usage,
583
+ ...(result.id !== undefined && { id: result.id }),
584
+ ...(result.provider !== undefined && { provider: result.provider }),
579
585
  })
580
586
  } catch (error) {
581
587
  const duration = Date.now() - startTime
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Files Adapter
3
+ *
4
+ * Base class and interface for the `files` activity — a provider's native Files
5
+ * API (upload a media asset once, reference it later by the returned handle
6
+ * instead of re-sending base64 or a public URL each request).
7
+ *
8
+ * Providers with a native surface expose a factory (`openaiFiles()`,
9
+ * `anthropicFiles()`, `geminiFiles()`, `falFiles()`). `upload` is required;
10
+ * `get`/`delete` are optional because not every provider has a lifecycle API
11
+ * (fal's storage is upload-only).
12
+ */
13
+
14
+ import { base64ToArrayBuffer } from '@tanstack/ai-utils'
15
+
16
+ /**
17
+ * Input to {@link FilesAdapter.upload}. Either a `Blob` (memory-efficient,
18
+ * preferred for large assets) or base64 `data` plus its `mimeType`.
19
+ */
20
+ export type FileUploadInput =
21
+ | Blob
22
+ | {
23
+ /** Base64-encoded file bytes. */
24
+ data: string
25
+ /** MIME type of the bytes (e.g. `'image/png'`, `'application/pdf'`). */
26
+ mimeType: string
27
+ /** Optional filename hint sent to providers that accept one. */
28
+ filename?: string
29
+ }
30
+
31
+ /**
32
+ * A provider-issued file handle returned by {@link FilesAdapter.upload} /
33
+ * {@link FilesAdapter.get}. Reference it in a message via a `{ type: 'file' }`
34
+ * content source — use `fileSourceFromHandle` to build one.
35
+ *
36
+ * `TProvider` carries the issuing provider's name as a literal (`'openai'`,
37
+ * `'gemini'`, ...) when the handle came from a concrete files adapter, so
38
+ * cross-provider lifecycle calls (`deleteFile` with a foreign handle) fail at
39
+ * compile time. It defaults to `string` so wire-deserialized handles still fit.
40
+ */
41
+ export interface FileHandle<TProvider extends string = string> {
42
+ /**
43
+ * Provider handle used for lifecycle operations (`get`/`delete`): the
44
+ * OpenAI/Anthropic `file_id`, the Gemini file resource name (`files/...`), or
45
+ * the fal storage URL (fal itself has no lifecycle API — the URL doubles as
46
+ * the wire reference).
47
+ */
48
+ id: string
49
+ /** The provider that issued the handle (`'openai'`, `'gemini'`, ...). */
50
+ provider: TProvider
51
+ /**
52
+ * The handle's URL form when the provider exposes one (Gemini file URI, fal
53
+ * storage URL). For providers whose handle is an opaque id (OpenAI,
54
+ * Anthropic) this is `undefined`.
55
+ */
56
+ uri?: string
57
+ /** MIME type reported by the provider (or echoed from the upload input). */
58
+ mimeType?: string
59
+ /** File size in bytes when the provider reports it. */
60
+ sizeBytes?: number
61
+ /** Expiry as epoch milliseconds when the handle is scheduled to expire. */
62
+ expiresAt?: number
63
+ /** Original filename when the provider reports it. */
64
+ filename?: string
65
+ }
66
+
67
+ /**
68
+ * The `files` adapter contract. `upload` is required; `get`/`delete` are
69
+ * optional and present only when the provider has a lifecycle API.
70
+ *
71
+ * `TName` is the provider name literal (`'openai'`, `'gemini'`, ...); concrete
72
+ * adapters bind it so the handles they issue carry their provenance in the
73
+ * type system.
74
+ */
75
+ export interface FilesAdapter<TName extends string = string> {
76
+ readonly kind: 'files'
77
+ readonly name: TName
78
+ upload: (input: FileUploadInput) => Promise<FileHandle<TName>>
79
+ get?: (id: string) => Promise<FileHandle<TName>>
80
+ delete?: (id: string) => Promise<void>
81
+ }
82
+
83
+ export type AnyFilesAdapter = FilesAdapter<string>
84
+
85
+ /**
86
+ * Normalize a {@link FileUploadInput} to a `Blob` (plus best-effort MIME /
87
+ * filename) so provider adapters can hand it straight to their SDK. A `Blob`
88
+ * input passes through; base64 `{ data }` is decoded to bytes. Shared so
89
+ * provider files adapters don't each re-implement the decode.
90
+ */
91
+ export function normalizeFileUploadInput(input: FileUploadInput): {
92
+ blob: Blob
93
+ mimeType?: string
94
+ filename?: string
95
+ } {
96
+ if (input instanceof Blob) {
97
+ return { blob: input, mimeType: input.type || undefined }
98
+ }
99
+ const bytes = base64ToArrayBuffer(input.data)
100
+ return {
101
+ blob: new Blob([bytes], { type: input.mimeType }),
102
+ mimeType: input.mimeType,
103
+ filename: input.filename,
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Abstract base for provider files adapters. Subclasses bind `TName` to their
109
+ * provider literal, set `name`, implement `upload`, and may add `get`/`delete`
110
+ * (declared on {@link FilesAdapter}, not here, since not every provider has a
111
+ * lifecycle API).
112
+ */
113
+ export abstract class BaseFilesAdapter<
114
+ TName extends string = string,
115
+ > implements FilesAdapter<TName> {
116
+ readonly kind = 'files' as const
117
+ abstract readonly name: TName
118
+
119
+ abstract upload(input: FileUploadInput): Promise<FileHandle<TName>>
120
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Files Activity
3
+ *
4
+ * Dispatch functions for provider Files APIs. Each takes `{ adapter, ... }` and
5
+ * calls the adapter method directly (mirrors the other activity dispatchers).
6
+ * `get`/`delete` are optional on the adapter; the dispatchers throw a clear
7
+ * error when the selected provider has no lifecycle API.
8
+ */
9
+
10
+ import type { ContentPartFileSource } from '../../types'
11
+ import type { FileHandle, FileUploadInput, FilesAdapter } from './adapter'
12
+
13
+ /** The adapter kind this activity handles */
14
+ export const kind = 'files' as const
15
+
16
+ /**
17
+ * Upload a file to a provider's Files API and return its handle. The handle
18
+ * carries the provider name as a literal type, so passing it to another
19
+ * provider's lifecycle call is a compile error.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const files = openaiFiles()
24
+ * const handle = await uploadFile({ adapter: files, input: { data, mimeType: 'image/png' } })
25
+ * ```
26
+ */
27
+ export async function uploadFile<TName extends string>(options: {
28
+ adapter: FilesAdapter<TName> & { kind: typeof kind }
29
+ input: FileUploadInput
30
+ }): Promise<FileHandle<TName>> {
31
+ return options.adapter.upload(options.input)
32
+ }
33
+
34
+ /**
35
+ * Resolve a lifecycle id from either a raw id string or a {@link FileHandle}
36
+ * (whose `id` — not its `uri`/wire value — is the lifecycle currency).
37
+ */
38
+ function toLifecycleId(id: string | FileHandle): string {
39
+ return typeof id === 'string' ? id : id.id
40
+ }
41
+
42
+ /**
43
+ * Fetch metadata for a previously uploaded file. Accepts the handle itself
44
+ * (preferred — the provider-literal type rejects a foreign provider's handle
45
+ * at compile time) or its raw lifecycle id.
46
+ *
47
+ * @throws if the provider's files adapter has no `get` (e.g. fal storage).
48
+ */
49
+ export async function getFile<TName extends string>(options: {
50
+ adapter: FilesAdapter<TName> & { kind: typeof kind }
51
+ // `NoInfer` so `TName` comes from the adapter only. Otherwise a foreign
52
+ // handle widens it to a union and the call compiles.
53
+ id: string | FileHandle<NoInfer<TName>>
54
+ }): Promise<FileHandle<TName>> {
55
+ const { adapter } = options
56
+ if (!adapter.get) {
57
+ throw new Error(
58
+ `${adapter.name}: files adapter does not support get() — this provider ` +
59
+ `has no file-retrieval API.`,
60
+ )
61
+ }
62
+ return adapter.get(toLifecycleId(options.id))
63
+ }
64
+
65
+ /**
66
+ * Delete a previously uploaded file. Accepts the handle itself (preferred —
67
+ * the provider-literal type rejects a foreign provider's handle at compile
68
+ * time) or its raw lifecycle id.
69
+ *
70
+ * @throws if the provider's files adapter has no `delete` (e.g. fal storage).
71
+ */
72
+ export async function deleteFile<TName extends string>(options: {
73
+ adapter: FilesAdapter<TName> & { kind: typeof kind }
74
+ id: string | FileHandle<NoInfer<TName>>
75
+ }): Promise<void> {
76
+ const { adapter } = options
77
+ if (!adapter.delete) {
78
+ throw new Error(
79
+ `${adapter.name}: files adapter does not support delete() — this ` +
80
+ `provider has no file-deletion API.`,
81
+ )
82
+ }
83
+ return adapter.delete(toLifecycleId(options.id))
84
+ }
85
+
86
+ /**
87
+ * Build a `{ type: 'file' }` content source from an uploaded
88
+ * {@link FileHandle}, for use in a chat message (image/audio/document part
89
+ * `source`).
90
+ *
91
+ * The source's `value` is the handle's wire form: the handle URL when the
92
+ * provider exposes one (Gemini, fal, Grok), otherwise the opaque id (OpenAI,
93
+ * Anthropic). `provider` records the issuer, so an adapter for a different
94
+ * provider rejects the source rather than sending a handle it cannot resolve.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const handle = await uploadFile({ adapter: openaiFiles(), input })
99
+ * messages.push({ role: 'user', content: [
100
+ * { type: 'image', source: fileSourceFromHandle(handle) },
101
+ * ] })
102
+ * ```
103
+ */
104
+ export function fileSourceFromHandle<TProvider extends string>(
105
+ handle: FileHandle<TProvider>,
106
+ ): ContentPartFileSource<TProvider> {
107
+ return {
108
+ type: 'file',
109
+ value: handle.uri ?? handle.id,
110
+ provider: handle.provider,
111
+ ...(handle.mimeType ? { mimeType: handle.mimeType } : {}),
112
+ }
113
+ }
@@ -51,6 +51,13 @@ export interface ImageAdapter<
51
51
  readonly kind: 'image'
52
52
  /** Adapter name identifier */
53
53
  readonly name: string
54
+ /**
55
+ * Declares that this adapter can consume `{ type: 'file' }` content
56
+ * sources (provider Files API references). The activity dispatcher rejects
57
+ * file sources in preflight for adapters that don't declare this, so
58
+ * adapters written before the file arm existed fail closed.
59
+ */
60
+ readonly supportsFileSources?: boolean
54
61
  /** The model this adapter is configured for */
55
62
  readonly model: TModel
56
63
 
@@ -103,6 +110,7 @@ export abstract class BaseImageAdapter<
103
110
  > {
104
111
  readonly kind = 'image' as const
105
112
  abstract readonly name: string
113
+ readonly supportsFileSources: boolean = false
106
114
  readonly model: TModel
107
115
 
108
116
  // Type-only property - never assigned at runtime
@@ -24,6 +24,7 @@ import {
24
24
  raceWithAbort,
25
25
  } from '../../utilities/activity-abort'
26
26
  import { resolveMediaPrompt } from '../../utilities/media-prompt'
27
+ import { assertPromptFileSourceSupport } from '../../utilities/content-source'
27
28
  import type { InternalLogger } from '../../logger/internal-logger'
28
29
  import type { DebugOption } from '../../logger/types'
29
30
  import type { GenerationMiddleware } from '../middleware/types'
@@ -248,6 +249,9 @@ export function generateImage<
248
249
  >(
249
250
  options: ImageActivityOptions<TAdapter, TStream>,
250
251
  ): ImageActivityResult<TStream> {
252
+ // Fail closed before middleware start and before `stream: true` emits
253
+ // RUN_STARTED, so an unsupported file source never opens a run.
254
+ assertPromptFileSourceSupport(options.adapter, options.prompt)
251
255
  if (options.stream) {
252
256
  return streamGenerationResult(
253
257
  // Only `runId` is taken from the resolved wire identity. `threadId` stays