@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.
- package/README.md +1 -0
- package/dist/esm/activities/chat/adapter.d.ts +9 -0
- package/dist/esm/activities/chat/adapter.js +1 -0
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
- package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
- package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
- package/dist/esm/activities/chat/agents/spawn.js +8 -5
- package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
- package/dist/esm/activities/chat/index.js +289 -78
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +35 -19
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
- package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
- package/dist/esm/activities/chat/stream/processor.js +51 -28
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
- package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/embed/adapter.d.ts +7 -0
- package/dist/esm/activities/embed/adapter.js +1 -0
- package/dist/esm/activities/embed/adapter.js.map +1 -1
- package/dist/esm/activities/embed/index.js +2 -0
- package/dist/esm/activities/embed/index.js.map +1 -1
- package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
- package/dist/esm/activities/evaluate/adapter.js.map +1 -1
- package/dist/esm/activities/evaluate/index.d.ts +4 -0
- package/dist/esm/activities/evaluate/index.js +3 -1
- package/dist/esm/activities/evaluate/index.js.map +1 -1
- package/dist/esm/activities/files/adapter.d.ts +97 -0
- package/dist/esm/activities/files/adapter.js +45 -0
- package/dist/esm/activities/files/adapter.js.map +1 -0
- package/dist/esm/activities/files/index.d.ts +66 -0
- package/dist/esm/activities/files/index.js +78 -0
- package/dist/esm/activities/files/index.js.map +1 -0
- package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
- package/dist/esm/activities/generateImage/adapter.js +1 -0
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.js +2 -0
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
- package/dist/esm/activities/generateVideo/adapter.js +1 -0
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +3 -0
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
- package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
- package/dist/esm/activities/generateWorld/index.d.ts +4 -3
- package/dist/esm/activities/generateWorld/index.js +5 -4
- package/dist/esm/activities/generateWorld/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +6 -3
- package/dist/esm/activities/index.js +13 -11
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/client.d.ts +3 -1
- package/dist/esm/client.js +2 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -3
- package/dist/esm/index.js +5 -3
- package/dist/esm/interrupt-resume.js +29 -4
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +5 -2
- package/dist/esm/middlewares/otel.js +114 -0
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +114 -14
- package/dist/esm/utilities/ag-ui-wire.js +39 -16
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/content-source.d.ts +60 -0
- package/dist/esm/utilities/content-source.js +85 -0
- package/dist/esm/utilities/content-source.js.map +1 -0
- package/dist/esm/utilities/provider-executed.d.ts +7 -0
- package/dist/esm/utilities/provider-executed.js +10 -1
- package/dist/esm/utilities/provider-executed.js.map +1 -1
- package/dist/esm/utilities/tool-result.d.ts +14 -3
- package/dist/esm/utilities/tool-result.js +26 -3
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/package.json +4 -4
- package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
- package/skills/ai-core/chat-experience/SKILL.md +134 -0
- package/skills/ai-core/media-generation/SKILL.md +8 -0
- package/skills/ai-core/tool-calling/SKILL.md +103 -0
- package/src/activities/chat/adapter.ts +10 -0
- package/src/activities/chat/agents/define-agent.ts +20 -3
- package/src/activities/chat/agents/spawn.ts +20 -12
- package/src/activities/chat/index.ts +458 -99
- package/src/activities/chat/messages.ts +52 -6
- package/src/activities/chat/middleware/types.ts +1 -0
- package/src/activities/chat/stream/message-updaters.ts +27 -2
- package/src/activities/chat/stream/processor.ts +81 -49
- package/src/activities/chat/tools/tool-calls.ts +104 -9
- package/src/activities/chat/tools/tool-definition.ts +8 -0
- package/src/activities/embed/adapter.ts +7 -0
- package/src/activities/embed/index.ts +5 -0
- package/src/activities/evaluate/adapter.ts +4 -0
- package/src/activities/evaluate/index.ts +6 -0
- package/src/activities/files/adapter.ts +120 -0
- package/src/activities/files/index.ts +113 -0
- package/src/activities/generateImage/adapter.ts +8 -0
- package/src/activities/generateImage/index.ts +4 -0
- package/src/activities/generateVideo/adapter.ts +8 -0
- package/src/activities/generateVideo/index.ts +7 -0
- package/src/activities/generateWorld/adapter.ts +4 -2
- package/src/activities/generateWorld/index.ts +7 -6
- package/src/activities/index.ts +25 -1
- package/src/activities/summarize/chat-stream-summarize.ts +22 -12
- package/src/client.ts +8 -0
- package/src/index.ts +17 -0
- package/src/interrupt-resume.ts +55 -4
- package/src/middlewares/otel.ts +161 -3
- package/src/types.ts +114 -14
- package/src/utilities/ag-ui-wire.ts +72 -17
- package/src/utilities/content-source.ts +138 -0
- package/src/utilities/provider-executed.ts +13 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|