@tanstack/ai 0.63.0 → 0.64.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/dist/esm/activities/chat/agents/spawn.js +2 -28
- package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +2 -0
- package/dist/esm/activities/chat/index.js +10 -3
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +13 -4
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +4 -1
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.d.ts +8 -0
- package/dist/esm/activities/chat/tools/schema-converter.js +6 -5
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +5 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +73 -22
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +1 -1
- package/dist/esm/activities/generateSpeech/index.js +1 -1
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +14 -6
- package/dist/esm/activities/generateVideo/adapter.js +6 -3
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +5 -4
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.d.ts +12 -3
- package/dist/esm/activities/generateVideo/snap.js +47 -8
- package/dist/esm/activities/generateVideo/snap.js.map +1 -1
- package/dist/esm/activities/generateVoice/index.d.ts +1 -1
- package/dist/esm/activities/generateVoice/index.js +1 -1
- package/dist/esm/activities/generateVoice/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +2 -2
- package/dist/esm/activities/index.js +2 -2
- package/dist/esm/adapter-internals.d.ts +1 -0
- package/dist/esm/adapter-internals.js +2 -1
- package/dist/esm/types.d.ts +28 -3
- package/dist/esm/utilities/ag-ui-wire.js +3 -1
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/merge-streams.d.ts +6 -0
- package/dist/esm/utilities/merge-streams.js +37 -0
- package/dist/esm/utilities/merge-streams.js.map +1 -0
- package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js +11 -1
- package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -1
- package/package.json +1 -1
- package/skills/ai-core/media-generation/SKILL.md +3 -3
- package/src/activities/chat/agents/spawn.ts +2 -31
- package/src/activities/chat/index.ts +13 -3
- package/src/activities/chat/messages.ts +14 -2
- package/src/activities/chat/stream/message-updaters.ts +5 -0
- package/src/activities/chat/tools/schema-converter.ts +17 -5
- package/src/activities/chat/tools/tool-calls.ts +121 -63
- package/src/activities/generateSpeech/index.ts +1 -1
- package/src/activities/generateVideo/adapter.ts +21 -7
- package/src/activities/generateVideo/index.ts +5 -4
- package/src/activities/generateVideo/snap.ts +64 -6
- package/src/activities/generateVoice/index.ts +1 -1
- package/src/activities/index.ts +2 -1
- package/src/adapter-internals.ts +1 -0
- package/src/types.ts +28 -4
- package/src/utilities/ag-ui-wire.ts +5 -1
- package/src/utilities/merge-streams.ts +34 -0
- package/src/utilities/reasoning-encrypted-value.ts +12 -0
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
import { subagentHostMessageId } from '../../utilities/subagent-wire'
|
|
42
42
|
import { withDurabilityBatchHint } from '../../utilities/durability-batch'
|
|
43
43
|
import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
|
|
44
|
+
import { isRedactedThinkingId } from '../../utilities/reasoning-encrypted-value'
|
|
44
45
|
import { restorePublicUsage } from '../../utilities/restore-inbound-chunk'
|
|
45
46
|
import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
|
|
46
47
|
import {
|
|
@@ -523,6 +524,8 @@ export interface TextActivityOptions<
|
|
|
523
524
|
abortController?: TextOptions['abortController']
|
|
524
525
|
/** Strategy for controlling the agent loop */
|
|
525
526
|
agentLoopStrategy?: TextOptions['agentLoopStrategy']
|
|
527
|
+
/** How the server tools of one model turn run. Default `'parallel'`. */
|
|
528
|
+
toolExecution?: TextOptions['toolExecution']
|
|
526
529
|
/**
|
|
527
530
|
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
528
531
|
* Tunes how much of each lazy tool's description appears in the discovery
|
|
@@ -853,8 +856,7 @@ class TextEngine<
|
|
|
853
856
|
private currentMessageCreatedAt: Date | null = null
|
|
854
857
|
private streamIdentityCaptured = false
|
|
855
858
|
private accumulatedContent = ''
|
|
856
|
-
private accumulatedThinking:
|
|
857
|
-
[]
|
|
859
|
+
private accumulatedThinking: NonNullable<ModelMessage['thinking']> = []
|
|
858
860
|
/**
|
|
859
861
|
* Arrival order of this iteration's thinking steps, text and tool calls.
|
|
860
862
|
* A ModelMessage keeps `thinking` apart from `content`/`toolCalls`, so a
|
|
@@ -866,6 +868,7 @@ class TextEngine<
|
|
|
866
868
|
private turnParts: Array<TurnPart> | null = []
|
|
867
869
|
private currentThinkingContent = ''
|
|
868
870
|
private currentThinkingSignature = ''
|
|
871
|
+
private currentThinkingRedacted = false
|
|
869
872
|
private eventOptions?: Record<string, unknown> | undefined
|
|
870
873
|
private eventToolNames?: Array<string>
|
|
871
874
|
private finishedEvent: RunFinishedEvent | null = null
|
|
@@ -1524,6 +1527,7 @@ class TextEngine<
|
|
|
1524
1527
|
this.turnParts = []
|
|
1525
1528
|
this.currentThinkingContent = ''
|
|
1526
1529
|
this.currentThinkingSignature = ''
|
|
1530
|
+
this.currentThinkingRedacted = false
|
|
1527
1531
|
|
|
1528
1532
|
this.finishedEvent = null
|
|
1529
1533
|
this.streamedToolErrorResults.clear()
|
|
@@ -1992,6 +1996,7 @@ class TextEngine<
|
|
|
1992
1996
|
...(this.currentThinkingSignature && {
|
|
1993
1997
|
signature: this.currentThinkingSignature,
|
|
1994
1998
|
}),
|
|
1999
|
+
...(this.currentThinkingRedacted && { redacted: true }),
|
|
1995
2000
|
})
|
|
1996
2001
|
if (this.turnParts) {
|
|
1997
2002
|
const placeholder = [...this.turnParts]
|
|
@@ -2009,6 +2014,7 @@ class TextEngine<
|
|
|
2009
2014
|
}
|
|
2010
2015
|
this.currentThinkingContent = ''
|
|
2011
2016
|
this.currentThinkingSignature = ''
|
|
2017
|
+
this.currentThinkingRedacted = false
|
|
2012
2018
|
}
|
|
2013
2019
|
}
|
|
2014
2020
|
|
|
@@ -2036,6 +2042,7 @@ class TextEngine<
|
|
|
2036
2042
|
if (typeof chunk.signature === 'string' && chunk.signature !== '') {
|
|
2037
2043
|
this.noteThinkingStepPosition()
|
|
2038
2044
|
this.currentThinkingSignature = chunk.signature
|
|
2045
|
+
this.currentThinkingRedacted = isRedactedThinkingId(chunk.stepId)
|
|
2039
2046
|
}
|
|
2040
2047
|
}
|
|
2041
2048
|
|
|
@@ -2065,6 +2072,7 @@ class TextEngine<
|
|
|
2065
2072
|
}
|
|
2066
2073
|
this.noteThinkingStepPosition()
|
|
2067
2074
|
this.currentThinkingSignature = chunk.encryptedValue
|
|
2075
|
+
this.currentThinkingRedacted = isRedactedThinkingId(chunk.entityId)
|
|
2068
2076
|
}
|
|
2069
2077
|
|
|
2070
2078
|
/**
|
|
@@ -2202,6 +2210,7 @@ class TextEngine<
|
|
|
2202
2210
|
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
2203
2211
|
inputResponses: this.resumeInputResponses,
|
|
2204
2212
|
},
|
|
2213
|
+
this.params.toolExecution,
|
|
2205
2214
|
)
|
|
2206
2215
|
|
|
2207
2216
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -2390,6 +2399,7 @@ class TextEngine<
|
|
|
2390
2399
|
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
2391
2400
|
inputResponses: this.resumeInputResponses,
|
|
2392
2401
|
},
|
|
2402
|
+
this.params.toolExecution,
|
|
2393
2403
|
)
|
|
2394
2404
|
|
|
2395
2405
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -2579,7 +2589,7 @@ class TextEngine<
|
|
|
2579
2589
|
),
|
|
2580
2590
|
)
|
|
2581
2591
|
type Segment = {
|
|
2582
|
-
thinking:
|
|
2592
|
+
thinking: NonNullable<ModelMessage['thinking']>
|
|
2583
2593
|
text: string
|
|
2584
2594
|
callIds: Array<string>
|
|
2585
2595
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
tanstackMetadata,
|
|
12
12
|
withTanstackMetadata,
|
|
13
13
|
} from '../../utilities/merge-metadata'
|
|
14
|
+
import { isRedactedThinkingId } from '../../utilities/reasoning-encrypted-value'
|
|
14
15
|
import {
|
|
15
16
|
splitSubagentWire,
|
|
16
17
|
subagentWireText,
|
|
@@ -72,6 +73,13 @@ function encryptedValueFrom(value: object): string | undefined {
|
|
|
72
73
|
return nonEmptyString(tanstackMetadata(value)?.signature)
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/** `{ redacted: true }` when a reasoning message's id marks a redacted block. */
|
|
77
|
+
function redactedFrom(value: object) {
|
|
78
|
+
return 'id' in value && isRedactedThinkingId(value.id)
|
|
79
|
+
? { redacted: true }
|
|
80
|
+
: {}
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
function toolCallFromWire(toolCall: ToolCall, bag: unknown): ToolCall {
|
|
76
84
|
const fromBag =
|
|
77
85
|
bag != null && typeof bag === 'object' && !Array.isArray(bag)
|
|
@@ -246,7 +254,7 @@ function convertOwnMessages(
|
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
const modelMessages: Array<ModelMessage> = []
|
|
249
|
-
let pendingThinking:
|
|
257
|
+
let pendingThinking: NonNullable<ModelMessage['thinking']> = []
|
|
250
258
|
for (const msg of messages) {
|
|
251
259
|
if ('parts' in msg) {
|
|
252
260
|
modelMessages.push(...uiMessageToModelMessages(msg))
|
|
@@ -273,6 +281,7 @@ function convertOwnMessages(
|
|
|
273
281
|
pendingThinking.push({
|
|
274
282
|
content: typeof content === 'string' ? content : '',
|
|
275
283
|
...(signature !== undefined ? { signature } : {}),
|
|
284
|
+
...redactedFrom(msg),
|
|
276
285
|
})
|
|
277
286
|
}
|
|
278
287
|
continue
|
|
@@ -663,7 +672,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
663
672
|
// shared UI id on each one so persistence can retain the original identity.
|
|
664
673
|
const messageList: Array<ModelMessage> = []
|
|
665
674
|
let current = createSegment()
|
|
666
|
-
let pendingThinking:
|
|
675
|
+
let pendingThinking: NonNullable<ModelMessage['thinking']> = []
|
|
667
676
|
|
|
668
677
|
// Track emitted tool result IDs to avoid duplicates.
|
|
669
678
|
// A tool call can have BOTH an explicit tool-result part AND an output
|
|
@@ -764,6 +773,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
|
764
773
|
pendingThinking.push({
|
|
765
774
|
content: part.content,
|
|
766
775
|
...(part.signature && { signature: part.signature }),
|
|
776
|
+
...(part.redacted && { redacted: true }),
|
|
767
777
|
})
|
|
768
778
|
}
|
|
769
779
|
break
|
|
@@ -898,6 +908,7 @@ export function modelMessageToUIMessage(
|
|
|
898
908
|
type: 'thinking',
|
|
899
909
|
content: thinking.content,
|
|
900
910
|
...(thinking.signature && { signature: thinking.signature }),
|
|
911
|
+
...(thinking.redacted && { redacted: true }),
|
|
901
912
|
})
|
|
902
913
|
}
|
|
903
914
|
}
|
|
@@ -1121,6 +1132,7 @@ export function aguiSnapshotMessageToUIMessage(
|
|
|
1121
1132
|
type: 'thinking' as const,
|
|
1122
1133
|
content,
|
|
1123
1134
|
...(signature !== undefined ? { signature } : {}),
|
|
1135
|
+
...redactedFrom(message),
|
|
1124
1136
|
},
|
|
1125
1137
|
]
|
|
1126
1138
|
: [],
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* These are used by StreamProcessor to manage the message array.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isRedactedThinkingId } from '../../../utilities/reasoning-encrypted-value'
|
|
8
9
|
import { parsePartialJSON } from './json-parser'
|
|
9
10
|
import type {
|
|
10
11
|
ContentPart,
|
|
@@ -483,12 +484,16 @@ export function updateThinkingPart(
|
|
|
483
484
|
// not carry one; losing it would strip the provider's encrypted reasoning
|
|
484
485
|
// from a message that is about to be sent back.
|
|
485
486
|
const nextSignature = signature ?? adopted?.signature
|
|
487
|
+
// A hydrated part has no stepId to carry the redacted marker, so it keeps
|
|
488
|
+
// its own flag.
|
|
489
|
+
const redacted = isRedactedThinkingId(stepId) || adopted?.redacted === true
|
|
486
490
|
|
|
487
491
|
const thinkingPart: ThinkingPart = {
|
|
488
492
|
type: 'thinking',
|
|
489
493
|
content,
|
|
490
494
|
stepId,
|
|
491
495
|
...(nextSignature && { signature: nextSignature }),
|
|
496
|
+
...(redacted && { redacted: true }),
|
|
492
497
|
}
|
|
493
498
|
|
|
494
499
|
if (thinkingPartIndex >= 0) {
|
|
@@ -230,6 +230,14 @@ export interface ConvertSchemaOptions {
|
|
|
230
230
|
* @default false
|
|
231
231
|
*/
|
|
232
232
|
forStructuredOutput?: boolean
|
|
233
|
+
/**
|
|
234
|
+
* Which view of a Standard JSON Schema to convert. A schema with a
|
|
235
|
+
* transform or a pipe has a different `output` view. Use `'output'` to
|
|
236
|
+
* describe the value that parsing returns.
|
|
237
|
+
*
|
|
238
|
+
* @default 'input'
|
|
239
|
+
*/
|
|
240
|
+
io?: 'input' | 'output'
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
/**
|
|
@@ -238,16 +246,20 @@ export interface ConvertSchemaOptions {
|
|
|
238
246
|
*
|
|
239
247
|
* - Standard JSON Schemas are rebuilt structurally (dropping `$schema`, which
|
|
240
248
|
* LLM providers ignore) and given the explicit `type`/`properties`/`required`
|
|
241
|
-
* defaults object shapes need downstream.
|
|
249
|
+
* defaults object shapes need downstream. `io` picks the `input` (default)
|
|
250
|
+
* or `output` view.
|
|
242
251
|
* - Plain `JSONSchema` inputs are rebuilt into the typed view; non-object inputs
|
|
243
252
|
* are surfaced untouched (they can't be widened).
|
|
244
253
|
* - Standard Schema validators lacking a `~standard.jsonSchema` converter throw
|
|
245
254
|
* with actionable guidance, rather than shipping `{ '~standard': … }` to the
|
|
246
255
|
* provider and producing an opaque downstream error.
|
|
247
256
|
*/
|
|
248
|
-
function toTypedJsonSchema(
|
|
257
|
+
function toTypedJsonSchema(
|
|
258
|
+
schema: SchemaInput,
|
|
259
|
+
io: 'input' | 'output' = 'input',
|
|
260
|
+
): JSONSchema | undefined {
|
|
249
261
|
if (isStandardJSONSchema(schema)) {
|
|
250
|
-
const jsonSchema = schema['~standard'].jsonSchema
|
|
262
|
+
const jsonSchema = schema['~standard'].jsonSchema[io]({
|
|
251
263
|
target: 'draft-07',
|
|
252
264
|
})
|
|
253
265
|
const result: JSONSchema = toJsonSchema(jsonSchema)
|
|
@@ -340,7 +352,7 @@ export function convertSchemaToJsonSchema(
|
|
|
340
352
|
): JSONSchema | undefined {
|
|
341
353
|
if (!schema) return undefined
|
|
342
354
|
|
|
343
|
-
const { forStructuredOutput = false } = options
|
|
355
|
+
const { forStructuredOutput = false, io } = options
|
|
344
356
|
|
|
345
357
|
// Plain-JSONSchema passthrough: with no widening requested, return the schema
|
|
346
358
|
// by reference so callers comparing via `===` keep identity. Only the widening
|
|
@@ -353,7 +365,7 @@ export function convertSchemaToJsonSchema(
|
|
|
353
365
|
return schema
|
|
354
366
|
}
|
|
355
367
|
|
|
356
|
-
const base = toTypedJsonSchema(schema)
|
|
368
|
+
const base = toTypedJsonSchema(schema, io)
|
|
357
369
|
// Non-object inputs can't be widened; surface them untouched.
|
|
358
370
|
if (!base || typeof base !== 'object') return base
|
|
359
371
|
if (!forStructuredOutput) return base
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeToolResult } from '../../../utilities/tool-result'
|
|
2
2
|
import { tanstackMetadata } from '../../../utilities/merge-metadata'
|
|
3
3
|
import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
|
|
4
|
+
import { mergeStreams } from '../../../utilities/merge-streams'
|
|
4
5
|
import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
|
|
5
6
|
import {
|
|
6
7
|
StandardSchemaValidationError,
|
|
@@ -18,6 +19,7 @@ import type {
|
|
|
18
19
|
ModelMessage,
|
|
19
20
|
RunFinishedEvent,
|
|
20
21
|
StreamChunk,
|
|
22
|
+
TextOptions,
|
|
21
23
|
Tool,
|
|
22
24
|
ToolCall,
|
|
23
25
|
ToolCallArgsEvent,
|
|
@@ -887,6 +889,9 @@ async function buildClientToolResult(
|
|
|
887
889
|
* @param approvals - Map keyed by toolCallId (or `approval_${toolCallId}`) → ToolApprovalResolution
|
|
888
890
|
* @param clientResults - Map of client-side execution results (toolCallId -> result)
|
|
889
891
|
* @param createCustomEventChunk - Factory to create CustomEvent chunks (optional)
|
|
892
|
+
* @param toolExecution - `'parallel'` (default) prepares every call in call
|
|
893
|
+
* order, then starts the server tools together. `'sequential'` runs one
|
|
894
|
+
* call at a time. Results come back in call order either way.
|
|
890
895
|
*/
|
|
891
896
|
export async function* executeToolCalls<TContext = unknown>(
|
|
892
897
|
toolCalls: Array<ToolCall>,
|
|
@@ -902,12 +907,12 @@ export async function* executeToolCalls<TContext = unknown>(
|
|
|
902
907
|
userContext?: TContext,
|
|
903
908
|
abortSignal?: AbortSignal,
|
|
904
909
|
resumeState?: ToolResumeExecutionState,
|
|
910
|
+
toolExecution: NonNullable<TextOptions['toolExecution']> = 'parallel',
|
|
905
911
|
): AsyncGenerator<CustomEvent | StreamChunk, ExecuteToolCallsResult, void> {
|
|
906
912
|
const results: Array<ToolResult> = []
|
|
907
913
|
const needsApproval: Array<ApprovalRequest> = []
|
|
908
914
|
const needsClientExecution: Array<ClientToolRequest> = []
|
|
909
915
|
const inputRequired: Array<McpInputRequest> = []
|
|
910
|
-
const subagentInterrupts: Array<Interrupt> = []
|
|
911
916
|
|
|
912
917
|
// Create tool lookup map
|
|
913
918
|
const toolMap = new Map<string, AnyTool>()
|
|
@@ -915,6 +920,66 @@ export async function* executeToolCalls<TContext = unknown>(
|
|
|
915
920
|
toolMap.set(tool.name, tool)
|
|
916
921
|
}
|
|
917
922
|
|
|
923
|
+
const runsInOrder = toolExecution === 'sequential'
|
|
924
|
+
// Parallel mode collects the server runs here. `mergeStreams` starts them
|
|
925
|
+
// together after the loop.
|
|
926
|
+
const runs: Array<AsyncGenerator<CustomEvent | StreamChunk, void, void>> = []
|
|
927
|
+
// Errors thrown by a run or a before-hook. They are rethrown only after every
|
|
928
|
+
// started tool has finished, so no tool outlives the batch.
|
|
929
|
+
const failures: Array<unknown> = []
|
|
930
|
+
// Each call's subagent interrupts, so they come back in call order.
|
|
931
|
+
const interruptsByCall = new Map<string, Array<Interrupt>>()
|
|
932
|
+
|
|
933
|
+
// A tool that has not started when the run aborts never starts. It gets an
|
|
934
|
+
// error result, so every call of the batch still has a result.
|
|
935
|
+
async function* runServerTool(
|
|
936
|
+
toolCall: ToolCall,
|
|
937
|
+
tool: AnyTool,
|
|
938
|
+
toolName: string,
|
|
939
|
+
input: unknown,
|
|
940
|
+
context: ToolExecutionContext<TContext>,
|
|
941
|
+
pendingEvents: Array<CustomEvent | StreamChunk>,
|
|
942
|
+
): AsyncGenerator<CustomEvent | StreamChunk, void, void> {
|
|
943
|
+
try {
|
|
944
|
+
if (abortSignal?.aborted) {
|
|
945
|
+
results.push({
|
|
946
|
+
toolCallId: toolCall.id,
|
|
947
|
+
toolName,
|
|
948
|
+
result: { error: 'Operation aborted' },
|
|
949
|
+
input,
|
|
950
|
+
state: 'output-error',
|
|
951
|
+
duration: 0,
|
|
952
|
+
})
|
|
953
|
+
await middlewareHooks?.onAfterToolCall?.({
|
|
954
|
+
toolCall,
|
|
955
|
+
tool,
|
|
956
|
+
toolName,
|
|
957
|
+
toolCallId: toolCall.id,
|
|
958
|
+
ok: false,
|
|
959
|
+
duration: 0,
|
|
960
|
+
error: new Error('Operation aborted'),
|
|
961
|
+
})
|
|
962
|
+
return
|
|
963
|
+
}
|
|
964
|
+
const interrupts: Array<Interrupt> = []
|
|
965
|
+
interruptsByCall.set(toolCall.id, interrupts)
|
|
966
|
+
yield* executeServerTool(
|
|
967
|
+
toolCall,
|
|
968
|
+
tool,
|
|
969
|
+
toolName,
|
|
970
|
+
input,
|
|
971
|
+
context,
|
|
972
|
+
pendingEvents,
|
|
973
|
+
results,
|
|
974
|
+
middlewareHooks,
|
|
975
|
+
inputRequired,
|
|
976
|
+
interrupts,
|
|
977
|
+
)
|
|
978
|
+
} catch (error) {
|
|
979
|
+
failures.push(error)
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
918
983
|
// Batch gating: when any tool in the batch still needs an approval decision,
|
|
919
984
|
// defer all execution so side effects don't happen before the user decides.
|
|
920
985
|
const hasPendingApprovals = toolCalls.some((tc) => {
|
|
@@ -1128,52 +1193,7 @@ export async function* executeToolCalls<TContext = unknown>(
|
|
|
1128
1193
|
const approvalId = `approval_${toolCall.id}`
|
|
1129
1194
|
const resolution = approvalResolution(approvals, toolCall.id)
|
|
1130
1195
|
|
|
1131
|
-
|
|
1132
|
-
if (resolution !== undefined) {
|
|
1133
|
-
const approved = isApproved(resolution)
|
|
1134
|
-
|
|
1135
|
-
if (approved) {
|
|
1136
|
-
input = editedApprovalArgs(resolution) ?? input
|
|
1137
|
-
// Apply middleware before-hook for approved tools
|
|
1138
|
-
if (middlewareHooks) {
|
|
1139
|
-
const decision = await applyBeforeToolCallDecision(
|
|
1140
|
-
toolCall,
|
|
1141
|
-
tool,
|
|
1142
|
-
input,
|
|
1143
|
-
toolName,
|
|
1144
|
-
middlewareHooks,
|
|
1145
|
-
results,
|
|
1146
|
-
)
|
|
1147
|
-
if (!decision.proceed) continue
|
|
1148
|
-
input = decision.input
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
yield* executeServerTool(
|
|
1152
|
-
toolCall,
|
|
1153
|
-
tool,
|
|
1154
|
-
toolName,
|
|
1155
|
-
input,
|
|
1156
|
-
context,
|
|
1157
|
-
pendingEvents,
|
|
1158
|
-
results,
|
|
1159
|
-
middlewareHooks,
|
|
1160
|
-
inputRequired,
|
|
1161
|
-
subagentInterrupts,
|
|
1162
|
-
)
|
|
1163
|
-
} else {
|
|
1164
|
-
// User declined
|
|
1165
|
-
results.push({
|
|
1166
|
-
toolCallId: toolCall.id,
|
|
1167
|
-
toolName,
|
|
1168
|
-
result:
|
|
1169
|
-
resumeState?.deniedToolResults?.get(toolCall.id) ??
|
|
1170
|
-
deniedApprovalResult(resolution),
|
|
1171
|
-
input,
|
|
1172
|
-
state: 'output-error',
|
|
1173
|
-
outcome: 'denied',
|
|
1174
|
-
})
|
|
1175
|
-
}
|
|
1176
|
-
} else {
|
|
1196
|
+
if (resolution === undefined) {
|
|
1177
1197
|
// Need approval
|
|
1178
1198
|
needsApproval.push({
|
|
1179
1199
|
toolCallId: toolCall.id,
|
|
@@ -1181,43 +1201,81 @@ export async function* executeToolCalls<TContext = unknown>(
|
|
|
1181
1201
|
input,
|
|
1182
1202
|
approvalId,
|
|
1183
1203
|
})
|
|
1204
|
+
continue
|
|
1184
1205
|
}
|
|
1185
|
-
|
|
1206
|
+
if (!isApproved(resolution)) {
|
|
1207
|
+
// User declined
|
|
1208
|
+
results.push({
|
|
1209
|
+
toolCallId: toolCall.id,
|
|
1210
|
+
toolName,
|
|
1211
|
+
result:
|
|
1212
|
+
resumeState?.deniedToolResults?.get(toolCall.id) ??
|
|
1213
|
+
deniedApprovalResult(resolution),
|
|
1214
|
+
input,
|
|
1215
|
+
state: 'output-error',
|
|
1216
|
+
outcome: 'denied',
|
|
1217
|
+
})
|
|
1218
|
+
continue
|
|
1219
|
+
}
|
|
1220
|
+
// Approved: run it like any other server tool below.
|
|
1221
|
+
input = editedApprovalArgs(resolution) ?? input
|
|
1186
1222
|
}
|
|
1187
1223
|
|
|
1188
|
-
// CASE 3:
|
|
1224
|
+
// CASE 3: Server tool, approved or with no approval
|
|
1189
1225
|
if (middlewareHooks) {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1226
|
+
let decision: Awaited<ReturnType<typeof applyBeforeToolCallDecision>>
|
|
1227
|
+
try {
|
|
1228
|
+
decision = await applyBeforeToolCallDecision(
|
|
1229
|
+
toolCall,
|
|
1230
|
+
tool,
|
|
1231
|
+
input,
|
|
1232
|
+
toolName,
|
|
1233
|
+
middlewareHooks,
|
|
1234
|
+
results,
|
|
1235
|
+
)
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
// The calls prepared so far still run, as they would one at a time.
|
|
1238
|
+
failures.push(error)
|
|
1239
|
+
break
|
|
1240
|
+
}
|
|
1198
1241
|
if (!decision.proceed) continue
|
|
1199
1242
|
input = decision.input
|
|
1200
1243
|
}
|
|
1201
1244
|
|
|
1202
|
-
|
|
1245
|
+
const run = runServerTool(
|
|
1203
1246
|
toolCall,
|
|
1204
1247
|
tool,
|
|
1205
1248
|
toolName,
|
|
1206
1249
|
input,
|
|
1207
1250
|
context,
|
|
1208
1251
|
pendingEvents,
|
|
1209
|
-
results,
|
|
1210
|
-
middlewareHooks,
|
|
1211
|
-
inputRequired,
|
|
1212
|
-
subagentInterrupts,
|
|
1213
1252
|
)
|
|
1253
|
+
if (!runsInOrder) {
|
|
1254
|
+
runs.push(run)
|
|
1255
|
+
continue
|
|
1256
|
+
}
|
|
1257
|
+
yield* run
|
|
1258
|
+
if (failures.length > 0) break
|
|
1214
1259
|
}
|
|
1215
1260
|
|
|
1261
|
+
yield* mergeStreams(runs)
|
|
1262
|
+
if (failures.length > 0) throw failures[0]
|
|
1263
|
+
|
|
1264
|
+
// Parallel tools finish in any order. The model gets the results, input
|
|
1265
|
+
// requests, and interrupts in the order it made the calls.
|
|
1266
|
+
const callOrder = new Map(toolCalls.map((tc, index) => [tc.id, index]))
|
|
1267
|
+
const byCallOrder = (a: { toolCallId: string }, b: { toolCallId: string }) =>
|
|
1268
|
+
(callOrder.get(a.toolCallId) ?? 0) - (callOrder.get(b.toolCallId) ?? 0)
|
|
1269
|
+
results.sort(byCallOrder)
|
|
1270
|
+
inputRequired.sort(byCallOrder)
|
|
1271
|
+
|
|
1216
1272
|
return {
|
|
1217
1273
|
results,
|
|
1218
1274
|
needsApproval,
|
|
1219
1275
|
needsClientExecution,
|
|
1220
1276
|
inputRequired,
|
|
1221
|
-
subagentInterrupts
|
|
1277
|
+
subagentInterrupts: toolCalls.flatMap(
|
|
1278
|
+
(tc) => interruptsByCall.get(tc.id) ?? [],
|
|
1279
|
+
),
|
|
1222
1280
|
}
|
|
1223
1281
|
}
|
|
@@ -445,7 +445,7 @@ export interface ListVoicesActivityOptions<
|
|
|
445
445
|
* import { elevenlabsSpeech } from '@tanstack/ai-elevenlabs'
|
|
446
446
|
*
|
|
447
447
|
* const { voices } = await listVoices({
|
|
448
|
-
* adapter: elevenlabsSpeech('
|
|
448
|
+
* adapter: elevenlabsSpeech('eleven_v4'),
|
|
449
449
|
* origins: ['generated', 'cloned'],
|
|
450
450
|
* })
|
|
451
451
|
* ```
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { snapToDurationOption } from './snap'
|
|
1
2
|
import type {
|
|
2
3
|
ModelInputModalitiesByName,
|
|
3
4
|
VideoGenerationOptions,
|
|
@@ -25,6 +26,12 @@ export type DurationOptions<T extends string | number | undefined> =
|
|
|
25
26
|
}
|
|
26
27
|
| { kind: 'none' }
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Spellings of one clip length: the number `6`, the string `"6"`, or the
|
|
31
|
+
* template `"6s"`.
|
|
32
|
+
*/
|
|
33
|
+
export type VideoDurationSpell<N extends number> = N | `${N}` | `${N}s`
|
|
34
|
+
|
|
28
35
|
/**
|
|
29
36
|
* Configuration for video adapter instances
|
|
30
37
|
*
|
|
@@ -126,10 +133,15 @@ export interface VideoAdapter<
|
|
|
126
133
|
availableDurations: () => DurationOptions<TModelDurationByName[TModel]>
|
|
127
134
|
|
|
128
135
|
/**
|
|
129
|
-
* Coerce
|
|
130
|
-
*
|
|
136
|
+
* Coerce `input` to the closest duration this model accepts.
|
|
137
|
+
* `input` may be seconds (`7`), a numeric string (`"7"`), a template
|
|
138
|
+
* (`"6s"`), or a keyword the model lists (`"auto"`).
|
|
139
|
+
* Returns `undefined` when the model has no duration field, or when
|
|
140
|
+
* `input` is a keyword that model does not list.
|
|
131
141
|
*/
|
|
132
|
-
snapDuration: (
|
|
142
|
+
snapDuration: (
|
|
143
|
+
input: number | string,
|
|
144
|
+
) => TModelDurationByName[TModel] | undefined
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
/**
|
|
@@ -208,11 +220,13 @@ export abstract class BaseVideoAdapter<
|
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
223
|
+
* Uses `availableDurations()`. Adapters that declare a duration map only
|
|
224
|
+
* need to override that method.
|
|
213
225
|
*/
|
|
214
|
-
snapDuration(
|
|
215
|
-
|
|
226
|
+
snapDuration(
|
|
227
|
+
input: number | string,
|
|
228
|
+
): TModelDurationByName[TModel] | undefined {
|
|
229
|
+
return snapToDurationOption(input, this.availableDurations())
|
|
216
230
|
}
|
|
217
231
|
|
|
218
232
|
protected generateId(): string {
|
|
@@ -173,10 +173,11 @@ export type VideoCreateOptions<
|
|
|
173
173
|
/** Video size — format depends on the provider (e.g., "16:9", "1280x720") */
|
|
174
174
|
size?: VideoSizeForAdapter<TAdapter>
|
|
175
175
|
/**
|
|
176
|
-
* Video duration
|
|
177
|
-
*
|
|
178
|
-
* Pass `adapter.snapDuration(
|
|
179
|
-
* value.
|
|
176
|
+
* Video duration. Adapters that declare a per-model duration map narrow
|
|
177
|
+
* this to that model's union (for example `4 | 6 | 8` for Veo 3, or
|
|
178
|
+
* `4 | "4" | "4s"` for Sora). Pass `adapter.snapDuration(input)` to coerce
|
|
179
|
+
* a raw value. `input` may be seconds, a `"6s"` template, or `"auto"` when
|
|
180
|
+
* the model lists it.
|
|
180
181
|
*/
|
|
181
182
|
duration?: VideoDurationForAdapter<TAdapter>
|
|
182
183
|
/**
|
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import type { DurationOptions } from './adapter'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* `"6"`, `"6s"`, and `"6.5s"` are seconds. Anything else (`"auto"`) is a
|
|
5
|
+
* keyword the model must list exactly.
|
|
6
|
+
*/
|
|
7
|
+
const DURATION_TEMPLATE = /^(\d+(?:\.\d+)?)s?$/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Seconds from a caller-supplied template, or `null` when `input` is a
|
|
11
|
+
* keyword (`"auto"`) rather than a length.
|
|
12
|
+
*/
|
|
13
|
+
function templateToSeconds(input: string): number | null {
|
|
14
|
+
const match = DURATION_TEMPLATE.exec(input)
|
|
15
|
+
const digits = match?.[1]
|
|
16
|
+
if (digits === undefined) return null
|
|
17
|
+
const seconds = Number(digits)
|
|
18
|
+
return Number.isFinite(seconds) ? seconds : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Seconds from a duration a caller wrote: `6`, `"6"`, or `"6s"`.
|
|
23
|
+
* Returns `undefined` for keywords such as `"auto"` and for non-finite numbers.
|
|
24
|
+
*/
|
|
25
|
+
export function durationToSeconds(input: number | string): number | undefined {
|
|
26
|
+
if (typeof input === 'number') {
|
|
27
|
+
return Number.isFinite(input) ? input : undefined
|
|
28
|
+
}
|
|
29
|
+
const seconds = templateToSeconds(input)
|
|
30
|
+
return seconds === null ? undefined : seconds
|
|
31
|
+
}
|
|
32
|
+
|
|
3
33
|
/**
|
|
4
34
|
* Extract a numeric seconds value from a `DurationOptions` entry. Returns
|
|
5
|
-
* `null` for entries that don't parse as a number
|
|
35
|
+
* `null` for entries that don't parse as a number, for example `'auto'`.
|
|
6
36
|
*
|
|
7
37
|
* Handles the keyword-with-unit form FAL uses for Luma/Veo (`'8s'`, `'9s'`)
|
|
8
38
|
* by stripping a trailing `s`. Pure-numeric strings (`'5'`, `'10'`) parse via
|
|
@@ -12,14 +42,16 @@ function entryToSeconds(entry: string | number): number | null {
|
|
|
12
42
|
if (typeof entry === 'number') {
|
|
13
43
|
return Number.isFinite(entry) ? entry : null
|
|
14
44
|
}
|
|
15
|
-
|
|
16
|
-
const parsed = Number(stripped)
|
|
17
|
-
return Number.isFinite(parsed) ? parsed : null
|
|
45
|
+
return templateToSeconds(entry)
|
|
18
46
|
}
|
|
19
47
|
|
|
20
48
|
/**
|
|
21
|
-
* Snap a
|
|
22
|
-
*
|
|
49
|
+
* Snap a caller duration to the closest valid option.
|
|
50
|
+
*
|
|
51
|
+
* `input` may be seconds (`7`), a numeric string (`"7"`), a template
|
|
52
|
+
* (`"6s"`), or a keyword the model lists (`"auto"`). A keyword that is not
|
|
53
|
+
* in the set returns `undefined`. Equal numeric distances keep the earlier
|
|
54
|
+
* option.
|
|
23
55
|
*
|
|
24
56
|
* - `none` → `undefined`
|
|
25
57
|
* - `discrete` → closest numeric-parseable entry; if none parse,
|
|
@@ -30,6 +62,32 @@ function entryToSeconds(entry: string | number): number | null {
|
|
|
30
62
|
* @experimental Video generation is an experimental feature and may change.
|
|
31
63
|
*/
|
|
32
64
|
export function snapToDurationOption<T extends string | number | undefined>(
|
|
65
|
+
input: number | string,
|
|
66
|
+
options: DurationOptions<T>,
|
|
67
|
+
): T | undefined {
|
|
68
|
+
// NaN is not a length. Infinity still clamps inside snapSeconds.
|
|
69
|
+
if (typeof input === 'number' && Number.isNaN(input)) return undefined
|
|
70
|
+
|
|
71
|
+
if (typeof input === 'string') {
|
|
72
|
+
const seconds = templateToSeconds(input)
|
|
73
|
+
if (seconds === null) return matchKeyword(input, options)
|
|
74
|
+
return snapSeconds(seconds, options)
|
|
75
|
+
}
|
|
76
|
+
return snapSeconds(input, options)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function matchKeyword<T extends string | number | undefined>(
|
|
80
|
+
keyword: string,
|
|
81
|
+
options: DurationOptions<T>,
|
|
82
|
+
): T | undefined {
|
|
83
|
+
if (options.kind !== 'discrete' && options.kind !== 'mixed') return undefined
|
|
84
|
+
for (const value of options.values) {
|
|
85
|
+
if (value === keyword) return value
|
|
86
|
+
}
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function snapSeconds<T extends string | number | undefined>(
|
|
33
91
|
seconds: number,
|
|
34
92
|
options: DurationOptions<T>,
|
|
35
93
|
): T | undefined {
|