@tanstack/ai 0.40.0 → 0.42.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/agent-loop-strategies.d.ts +40 -3
- package/dist/esm/activities/chat/agent-loop-strategies.js +4 -0
- package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +5 -0
- package/dist/esm/activities/chat/index.js +102 -33
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +7 -0
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -0
- package/dist/esm/activities/chat/stream/message-updaters.js +3 -1
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +18 -1
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +14 -11
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +1 -1
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +1 -1
- package/dist/esm/activities/generateImage/index.js +1 -1
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +1 -1
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +1 -1
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +12 -12
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/realtime/event-emitter.d.ts +5 -0
- package/dist/esm/realtime/event-emitter.js +27 -0
- package/dist/esm/realtime/event-emitter.js.map +1 -0
- package/dist/esm/realtime/index.d.ts +1 -0
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/realtime/types.d.ts +9 -1
- package/dist/esm/types.d.ts +43 -2
- package/package.json +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +58 -0
- package/skills/ai-core/media-generation/SKILL.md +42 -2
- package/skills/ai-core/middleware/SKILL.md +18 -3
- package/skills/ai-core/tool-calling/SKILL.md +15 -1
- package/src/activities/chat/agent-loop-strategies.ts +43 -3
- package/src/activities/chat/index.ts +144 -36
- package/src/activities/chat/messages.ts +9 -0
- package/src/activities/chat/stream/message-updaters.ts +7 -1
- package/src/activities/chat/stream/processor.ts +30 -2
- package/src/activities/chat/tools/tool-definition.ts +33 -11
- package/src/activities/generateAudio/index.ts +2 -2
- package/src/activities/generateImage/index.ts +2 -2
- package/src/activities/generateSpeech/index.ts +2 -2
- package/src/activities/generateTranscription/index.ts +2 -2
- package/src/activities/generateVideo/index.ts +29 -15
- package/src/index.ts +3 -1
- package/src/realtime/event-emitter.ts +46 -0
- package/src/realtime/index.ts +2 -0
- package/src/realtime/types.ts +9 -0
- package/src/types.ts +43 -2
package/dist/esm/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { ToolCallManager } from './activities/chat/tools/tool-calls.js';
|
|
|
16
16
|
export { DISCOVERY_TOOL_NAME } from './activities/chat/tools/lazy-tool-manager.js';
|
|
17
17
|
export type { ProviderTool } from './tools/provider-tool.js';
|
|
18
18
|
export { brandProviderTool } from './tools/provider-tool.js';
|
|
19
|
-
export { maxIterations, untilFinishReason, combineStrategies, } from './activities/chat/agent-loop-strategies.js';
|
|
19
|
+
export { maxIterations, maxToolCalls, untilFinishReason, combineStrategies, } from './activities/chat/agent-loop-strategies.js';
|
|
20
20
|
export { createToolRegistry, createFrozenRegistry, type ToolRegistry, } from './tool-registry.js';
|
|
21
21
|
export type { ChatMiddleware, ChatMiddlewareContext, ChatMiddlewarePhase, ChatMiddlewareConfig, StructuredOutputMiddlewareConfig, ToolCallHookContext, BeforeToolCallDecision, AfterToolCallInfo, IterationInfo, ToolPhaseCompleteInfo, UsageInfo, FinishInfo, AbortInfo, ErrorInfo, SandboxFileEvent, SandboxFileHookEvent, ChatSandboxHooks, } from './activities/chat/middleware/index.js';
|
|
22
22
|
export type { GenerationMiddleware, GenerationMiddlewareContext, GenerationActivity, GenerationUsageInfo, GenerationFinishInfo, GenerationAbortInfo, GenerationErrorInfo, AnyGenerationMiddleware, } from './activities/middleware/index.js';
|
|
@@ -30,8 +30,8 @@ export type { ResolvedMediaPrompt } from './utilities/media-prompt.js';
|
|
|
30
30
|
export type { SystemPrompt, NormalizedSystemPrompt } from './system-prompts.js';
|
|
31
31
|
export { normalizeSystemPrompts } from './system-prompts.js';
|
|
32
32
|
export { detectImageMimeType } from './utils.js';
|
|
33
|
-
export { realtimeToken } from './realtime/index.js';
|
|
34
|
-
export type { RealtimeToken, RealtimeTokenAdapter, RealtimeTokenOptions, RealtimeSessionConfig, VADConfig, RealtimeMessage, RealtimeMessagePart, RealtimeTextPart, RealtimeAudioPart, RealtimeToolCallPart, RealtimeToolResultPart, RealtimeImagePart, RealtimeStatus, RealtimeMode, AudioVisualization, RealtimeEvent, RealtimeEventPayloads, RealtimeEventHandler, RealtimeErrorCode, RealtimeError, RealtimeAdapter, RealtimeConnection, } from './realtime/index.js';
|
|
33
|
+
export { realtimeToken, createRealtimeEventEmitter } from './realtime/index.js';
|
|
34
|
+
export type { RealtimeToken, RealtimeTokenAdapter, RealtimeTokenOptions, RealtimeSessionConfig, RealtimeToolConfig, VADConfig, RealtimeMessage, RealtimeMessagePart, RealtimeTextPart, RealtimeAudioPart, RealtimeToolCallPart, RealtimeToolResultPart, RealtimeImagePart, RealtimeStatus, RealtimeMode, AudioVisualization, RealtimeEvent, RealtimeEventPayloads, RealtimeEventHandler, RealtimeErrorCode, RealtimeError, RealtimeAdapter, RealtimeConnection, } from './realtime/index.js';
|
|
35
35
|
export { convertMessagesToModelMessages, generateMessageId, uiMessageToModelMessages, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, } from './activities/chat/messages.js';
|
|
36
36
|
export { StreamProcessor, createReplayStream, ImmediateStrategy, PunctuationStrategy, BatchStrategy, WordBoundaryStrategy, CompositeStrategy, PartialJSONParser, defaultJSONParser, parsePartialJSON, } from './activities/chat/stream/index.js';
|
|
37
37
|
export type { ChunkStrategy, ChunkRecording, InternalToolCallState, ProcessorResult, ProcessorState, StreamProcessorEvents, StreamProcessorOptions, ToolCallState, ToolResultState, JSONParser, } from './activities/chat/stream/index.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { streamToText, toHttpResponse, toHttpStream, toServerSentEventsResponse,
|
|
|
12
12
|
import { ToolCallManager } from "./activities/chat/tools/tool-calls.js";
|
|
13
13
|
import { DISCOVERY_TOOL_NAME } from "./activities/chat/tools/lazy-tool-manager.js";
|
|
14
14
|
import { brandProviderTool } from "./tools/provider-tool.js";
|
|
15
|
-
import { combineStrategies, maxIterations, untilFinishReason } from "./activities/chat/agent-loop-strategies.js";
|
|
15
|
+
import { combineStrategies, maxIterations, maxToolCalls, untilFinishReason } from "./activities/chat/agent-loop-strategies.js";
|
|
16
16
|
import { createFrozenRegistry, createToolRegistry } from "./tool-registry.js";
|
|
17
17
|
import { firstSentence, renderLazyCatalogEntry } from "./activities/chat/tools/lazy-tools.js";
|
|
18
18
|
import { buildBaseUsage } from "./utilities/usage.js";
|
|
@@ -33,6 +33,7 @@ import { PartialJSONParser, defaultJSONParser, parsePartialJSON } from "./activi
|
|
|
33
33
|
import { StreamProcessor, createReplayStream } from "./activities/chat/stream/processor.js";
|
|
34
34
|
import { createCapability } from "./activities/chat/middleware/capabilities.js";
|
|
35
35
|
import { createChatMiddleware } from "./activities/chat/middleware/builder.js";
|
|
36
|
+
import { createRealtimeEventEmitter } from "./realtime/event-emitter.js";
|
|
36
37
|
import { defineChatMiddleware } from "./activities/chat/middleware/define.js";
|
|
37
38
|
export {
|
|
38
39
|
BatchStrategy,
|
|
@@ -63,6 +64,7 @@ export {
|
|
|
63
64
|
createFrozenRegistry,
|
|
64
65
|
createImageOptions,
|
|
65
66
|
createModel,
|
|
67
|
+
createRealtimeEventEmitter,
|
|
66
68
|
createReplayStream,
|
|
67
69
|
createSpeechOptions,
|
|
68
70
|
createSummarizeOptions,
|
|
@@ -87,6 +89,7 @@ export {
|
|
|
87
89
|
isProviderExecutedToolCall,
|
|
88
90
|
isStandardSchema,
|
|
89
91
|
maxIterations,
|
|
92
|
+
maxToolCalls,
|
|
90
93
|
mergeAgentTools,
|
|
91
94
|
modelMessageToUIMessage,
|
|
92
95
|
modelMessagesToUIMessages,
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { RealtimeEvent, RealtimeEventHandler, RealtimeEventPayloads } from './types.js';
|
|
2
|
+
export declare function createRealtimeEventEmitter(): {
|
|
3
|
+
emit<TEvent extends RealtimeEvent>(event: TEvent, payload: RealtimeEventPayloads[TEvent]): void;
|
|
4
|
+
on<TEvent extends RealtimeEvent>(event: TEvent, handler: RealtimeEventHandler<TEvent>): () => void;
|
|
5
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function createRealtimeEventEmitter() {
|
|
2
|
+
const eventHandlers = /* @__PURE__ */ new Map();
|
|
3
|
+
return {
|
|
4
|
+
emit(event, payload) {
|
|
5
|
+
const handlers = eventHandlers.get(event);
|
|
6
|
+
if (!handlers) return;
|
|
7
|
+
for (const handler of handlers) {
|
|
8
|
+
handler(payload);
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
on(event, handler) {
|
|
12
|
+
let handlers = eventHandlers.get(event);
|
|
13
|
+
if (!handlers) {
|
|
14
|
+
handlers = /* @__PURE__ */ new Set();
|
|
15
|
+
eventHandlers.set(event, handlers);
|
|
16
|
+
}
|
|
17
|
+
handlers.add(handler);
|
|
18
|
+
return () => {
|
|
19
|
+
handlers.delete(handler);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
createRealtimeEventEmitter
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=event-emitter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-emitter.js","sources":["../../../src/realtime/event-emitter.ts"],"sourcesContent":["import type {\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeEventPayloads,\n} from './types'\n\n/**\n * Handlers are stored with a `never` payload so any specific\n * `RealtimeEventHandler<TEvent>` is assignable in (contravariance), keeping the\n * heterogeneous handler map type-safe without `any`. `emit` narrows back to the\n * event's real payload type via its signature; the lone `as never` at the call\n * site is the inverse of that stored `never`.\n */\ntype StoredHandler = (payload: never) => void\n\nexport function createRealtimeEventEmitter() {\n const eventHandlers = new Map<RealtimeEvent, Set<StoredHandler>>()\n\n return {\n emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: RealtimeEventPayloads[TEvent],\n ) {\n const handlers = eventHandlers.get(event)\n if (!handlers) return\n for (const handler of handlers) {\n handler(payload as never)\n }\n },\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n let handlers = eventHandlers.get(event)\n if (!handlers) {\n handlers = new Set<StoredHandler>()\n eventHandlers.set(event, handlers)\n }\n handlers.add(handler)\n\n return () => {\n handlers.delete(handler)\n }\n },\n }\n}\n"],"names":[],"mappings":"AAeO,SAAS,6BAA6B;AAC3C,QAAM,oCAAoB,IAAA;AAE1B,SAAO;AAAA,IACL,KACE,OACA,SACA;AACA,YAAM,WAAW,cAAc,IAAI,KAAK;AACxC,UAAI,CAAC,SAAU;AACf,iBAAW,WAAW,UAAU;AAC9B,gBAAQ,OAAgB;AAAA,MAC1B;AAAA,IACF;AAAA,IACA,GACE,OACA,SACY;AACZ,UAAI,WAAW,cAAc,IAAI,KAAK;AACtC,UAAI,CAAC,UAAU;AACb,uCAAe,IAAA;AACf,sBAAc,IAAI,OAAO,QAAQ;AAAA,MACnC;AACA,eAAS,IAAI,OAAO;AAEpB,aAAO,MAAM;AACX,iBAAS,OAAO,OAAO;AAAA,MACzB;AAAA,IACF;AAAA,EAAA;AAEJ;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../src/realtime/index.ts"],"sourcesContent":["import type { RealtimeToken, RealtimeTokenOptions } from './types'\n\n// Re-export all types\nexport type * from './types'\n\n/**\n * Generate a realtime token using the provided adapter.\n *\n * This function is used on the server to generate ephemeral tokens\n * that clients can use to establish realtime connections.\n *\n * @param options - Token generation options including the adapter\n * @returns Promise resolving to a RealtimeToken\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { openaiRealtimeToken } from '@tanstack/ai-openai'\n *\n * // Server function (TanStack Start example)\n * export const getRealtimeToken = createServerFn()\n * .handler(async () => {\n * return realtimeToken({\n * adapter: openaiRealtimeToken({\n * model: 'gpt-realtime',\n * }),\n * })\n * })\n * ```\n */\nexport async function realtimeToken(\n options: RealtimeTokenOptions,\n): Promise<RealtimeToken> {\n const { adapter } = options\n return adapter.generateToken()\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/realtime/index.ts"],"sourcesContent":["import type { RealtimeToken, RealtimeTokenOptions } from './types'\n\nexport { createRealtimeEventEmitter } from './event-emitter'\n\n// Re-export all types\nexport type * from './types'\n\n/**\n * Generate a realtime token using the provided adapter.\n *\n * This function is used on the server to generate ephemeral tokens\n * that clients can use to establish realtime connections.\n *\n * @param options - Token generation options including the adapter\n * @returns Promise resolving to a RealtimeToken\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { openaiRealtimeToken } from '@tanstack/ai-openai'\n *\n * // Server function (TanStack Start example)\n * export const getRealtimeToken = createServerFn()\n * .handler(async () => {\n * return realtimeToken({\n * adapter: openaiRealtimeToken({\n * model: 'gpt-realtime',\n * }),\n * })\n * })\n * ```\n */\nexport async function realtimeToken(\n options: RealtimeTokenOptions,\n): Promise<RealtimeToken> {\n const { adapter } = options\n return adapter.generateToken()\n}\n"],"names":[],"mappings":"AAgCA,eAAsB,cACpB,SACwB;AACxB,QAAM,EAAE,YAAY;AACpB,SAAO,QAAQ,cAAA;AACjB;"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AnyClientTool } from '../activities/chat/tools/tool-definition.js';
|
|
2
|
+
import { UsageInfo } from '../activities/chat/middleware/types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Voice activity detection configuration
|
|
4
5
|
*/
|
|
@@ -18,6 +19,7 @@ export interface RealtimeToolConfig {
|
|
|
18
19
|
name: string;
|
|
19
20
|
description: string;
|
|
20
21
|
inputSchema?: Record<string, any>;
|
|
22
|
+
outputSchema?: Record<string, any>;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Configuration for a realtime session
|
|
@@ -182,7 +184,7 @@ export interface AudioVisualization {
|
|
|
182
184
|
/**
|
|
183
185
|
* Events emitted by the realtime connection
|
|
184
186
|
*/
|
|
185
|
-
export type RealtimeEvent = 'status_change' | 'mode_change' | 'transcript' | 'audio_chunk' | 'tool_call' | 'message_complete' | 'interrupted' | 'error';
|
|
187
|
+
export type RealtimeEvent = 'status_change' | 'mode_change' | 'transcript' | 'audio_chunk' | 'tool_call' | 'message_complete' | 'interrupted' | 'error' | 'go_away' | 'usage';
|
|
186
188
|
/**
|
|
187
189
|
* Event payloads for realtime events
|
|
188
190
|
*/
|
|
@@ -216,6 +218,10 @@ export interface RealtimeEventPayloads {
|
|
|
216
218
|
error: {
|
|
217
219
|
error: Error;
|
|
218
220
|
};
|
|
221
|
+
go_away: {
|
|
222
|
+
timeLeft?: string;
|
|
223
|
+
};
|
|
224
|
+
usage: UsageInfo;
|
|
219
225
|
}
|
|
220
226
|
/**
|
|
221
227
|
* Handler type for realtime events
|
|
@@ -273,6 +279,8 @@ export interface RealtimeConnection {
|
|
|
273
279
|
sendToolResult: (callId: string, result: string) => void;
|
|
274
280
|
/** Update session configuration */
|
|
275
281
|
updateSession: (config: Partial<RealtimeSessionConfig>) => void;
|
|
282
|
+
/** Update the ephemeral token (e.g. on refresh); provider may reconnect */
|
|
283
|
+
updateToken?: (token: RealtimeToken) => void;
|
|
276
284
|
/** Interrupt the current response */
|
|
277
285
|
interrupt: () => void;
|
|
278
286
|
/** Subscribe to connection events */
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -261,6 +261,15 @@ export interface ToolCallPart<TMetadata = unknown> {
|
|
|
261
261
|
id: string;
|
|
262
262
|
name: string;
|
|
263
263
|
arguments: string;
|
|
264
|
+
/**
|
|
265
|
+
* Parsed tool input. Set from the parsed arguments once they are complete
|
|
266
|
+
* (`state: 'input-complete'` and later). `undefined` while the raw
|
|
267
|
+
* `arguments` string is still streaming, and may stay `undefined` for a call
|
|
268
|
+
* that terminates in an error state — the raw `arguments` string is always
|
|
269
|
+
* available as a fallback. Typed per-tool on the client `ToolCallPart` (see
|
|
270
|
+
* `@tanstack/ai-client`); `unknown` on this base type.
|
|
271
|
+
*/
|
|
272
|
+
input?: unknown;
|
|
264
273
|
state: ToolCallState;
|
|
265
274
|
/** Approval metadata if tool requires user approval */
|
|
266
275
|
approval?: {
|
|
@@ -645,12 +654,24 @@ export interface ResponseFormat<TData = any> {
|
|
|
645
654
|
* State passed to agent loop strategy for determining whether to continue
|
|
646
655
|
*/
|
|
647
656
|
export interface AgentLoopState {
|
|
648
|
-
/** Current iteration count (0-indexed) */
|
|
657
|
+
/** Current iteration count (0-indexed). One iteration = one model turn. */
|
|
649
658
|
iterationCount: number;
|
|
650
659
|
/** Current messages array */
|
|
651
660
|
messages: Array<ModelMessage>;
|
|
652
661
|
/** Finish reason from the last response */
|
|
653
662
|
finishReason: string | null;
|
|
663
|
+
/**
|
|
664
|
+
* Cumulative tool calls counted so far in this run (model-emitted during the
|
|
665
|
+
* agent loop, including ones skipped by `maxToolCallsPerTurn`, and pending
|
|
666
|
+
* tools from the inbound message list when resumed). Not a recount of full
|
|
667
|
+
* message history; not model turns.
|
|
668
|
+
*/
|
|
669
|
+
toolCallCount: number;
|
|
670
|
+
/**
|
|
671
|
+
* Tool calls in the most recent budgeted batch — a live model turn or a
|
|
672
|
+
* pending/resume batch (0 when the last phase produced no tool calls).
|
|
673
|
+
*/
|
|
674
|
+
lastTurnToolCallCount: number;
|
|
654
675
|
}
|
|
655
676
|
/**
|
|
656
677
|
* Strategy function that determines whether the agent loop should continue
|
|
@@ -660,8 +681,10 @@ export interface AgentLoopState {
|
|
|
660
681
|
*
|
|
661
682
|
* @example
|
|
662
683
|
* ```typescript
|
|
663
|
-
* // Continue for up to 5 iterations
|
|
684
|
+
* // Continue for up to 5 iterations (model turns, not tool calls)
|
|
664
685
|
* const strategy: AgentLoopStrategy = ({ iterationCount }) => iterationCount < 5;
|
|
686
|
+
* // Cap total tool calls across the run
|
|
687
|
+
* const byTools: AgentLoopStrategy = ({ toolCallCount }) => toolCallCount < 20;
|
|
665
688
|
* ```
|
|
666
689
|
*/
|
|
667
690
|
export type AgentLoopStrategy = (state: AgentLoopState) => boolean;
|
|
@@ -693,6 +716,24 @@ export interface TextOptions<TProviderOptionsSuperset extends Record<string, any
|
|
|
693
716
|
*/
|
|
694
717
|
systemPrompts?: Array<SystemPrompt>;
|
|
695
718
|
agentLoopStrategy?: AgentLoopStrategy;
|
|
719
|
+
/**
|
|
720
|
+
* Maximum number of tool calls to **execute** from a single model turn (or
|
|
721
|
+
* pending/resume batch). `0` skips all execution for that batch.
|
|
722
|
+
*
|
|
723
|
+
* Models can emit many parallel tool calls in one turn. `agentLoopStrategy`
|
|
724
|
+
* (including `maxIterations` / `maxToolCalls`) is only evaluated between
|
|
725
|
+
* turns, so without this cap a single runaway turn can still execute an
|
|
726
|
+
* unbounded fan-out.
|
|
727
|
+
*
|
|
728
|
+
* When set, only the first `maxToolCallsPerTurn` calls are executed; the
|
|
729
|
+
* remainder receive error tool results so the message history stays
|
|
730
|
+
* consistent. Unset means no per-turn execution cap. Must be a non-negative
|
|
731
|
+
* finite number when set.
|
|
732
|
+
*
|
|
733
|
+
* Pair with the `maxToolCalls(n)` strategy for a cumulative **emitted**-call
|
|
734
|
+
* budget across the run (skipped calls still count toward that budget).
|
|
735
|
+
*/
|
|
736
|
+
maxToolCallsPerTurn?: number;
|
|
696
737
|
/**
|
|
697
738
|
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
698
739
|
* Tunes how much of each lazy tool's description appears in the discovery
|
package/package.json
CHANGED
|
@@ -409,6 +409,64 @@ export const Route = createFileRoute('/api/chat')({
|
|
|
409
409
|
})
|
|
410
410
|
```
|
|
411
411
|
|
|
412
|
+
### 7. Queueing Messages Sent While Streaming
|
|
413
|
+
|
|
414
|
+
By default, a `sendMessage` call that arrives while a stream is in flight is
|
|
415
|
+
**queued** and sent automatically once the run settles **successfully** —
|
|
416
|
+
this is a behavior change: such sends used to be silently dropped. Configure
|
|
417
|
+
it with the `queue` option on `useChat`:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
421
|
+
|
|
422
|
+
const { messages, queue, sendMessage, cancelQueued, isLoading } = useChat({
|
|
423
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
424
|
+
queue: { whenBusy: 'queue', drain: 'fifo', maxSize: 5, onOverflow: 'reject' },
|
|
425
|
+
})
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
- **`whenBusy`** — `'queue'` (default) holds the message until a successful
|
|
429
|
+
settle; `'drop'` ignores the send (never appears in `queue`/`messages`);
|
|
430
|
+
`'interrupt'` aborts the current stream and sends immediately (unlike
|
|
431
|
+
`stop()`, does **not** flush already-queued items — they drain after the
|
|
432
|
+
interrupting send **succeeds**).
|
|
433
|
+
- **`drain`** — `'fifo'` (default) sends queued items one at a time in
|
|
434
|
+
order; `'batch'` merges everything queued into a single send once the
|
|
435
|
+
run settles successfully.
|
|
436
|
+
- **`maxSize`** / **`onOverflow`** — cap the queue length; `'reject'`
|
|
437
|
+
(default) silently ignores overflow sends (does not throw),
|
|
438
|
+
`'drop-oldest'` evicts the oldest queued item to make room.
|
|
439
|
+
|
|
440
|
+
The top-level `queue` option also accepts a plain `WhenBusy` string
|
|
441
|
+
shorthand (e.g. `queue: 'interrupt'`) or a `QueueStrategy` function for
|
|
442
|
+
per-send action control. Strategy form always drains FIFO; actions are
|
|
443
|
+
`'queue' | 'drop' | 'interrupt'`.
|
|
444
|
+
|
|
445
|
+
**Drain vs flush:** queued messages auto-send only after a **successful**
|
|
446
|
+
settle. They are **discarded** on stream error/abort of the active
|
|
447
|
+
generation, `stop()`, `clear()`, `unsubscribe()`, and `reload()`.
|
|
448
|
+
`interrupt` does not flush.
|
|
449
|
+
|
|
450
|
+
`queue: Array<QueuedMessage>` (`{ id, content, createdAt }`) is separate
|
|
451
|
+
from `messages` — render pending sends distinctly and cancel with
|
|
452
|
+
`cancelQueued(id)`:
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
{queue.map((q) => (
|
|
456
|
+
<div key={q.id}>
|
|
457
|
+
{typeof q.content === 'string' ? q.content : '[attachment]'}
|
|
458
|
+
<button onClick={() => cancelQueued(q.id)}>Cancel</button>
|
|
459
|
+
</div>
|
|
460
|
+
))}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Override the configured policy for a single send with the second argument
|
|
464
|
+
to `sendMessage`:
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
sendMessage('Never mind, do this instead', { whenBusy: 'interrupt' })
|
|
468
|
+
```
|
|
469
|
+
|
|
412
470
|
## Common Mistakes
|
|
413
471
|
|
|
414
472
|
### a. CRITICAL: Using Vercel AI SDK patterns (streamText, generateText)
|
|
@@ -261,6 +261,17 @@ await generateVideo({
|
|
|
261
261
|
})
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
+
**URL inputs that require an upload throw by default.** Most adapters pass a
|
|
265
|
+
`type: 'url'` source straight through to the provider. Three paths can't —
|
|
266
|
+
OpenAI `images.edit()`, OpenAI Sora `input_reference`, and Gemini **Veo** —
|
|
267
|
+
because the provider only accepts uploaded bytes (Veo also takes a `gs://`
|
|
268
|
+
reference). For those, an HTTP(S) URL would have to be downloaded and buffered
|
|
269
|
+
in memory, which can OOM constrained runtimes, so they **throw** on an HTTP(S)
|
|
270
|
+
URL image input by default. Pass a `data:` URI (or `gs://` for Veo), or opt in
|
|
271
|
+
with `allowUrlFetch: true` on the adapter config
|
|
272
|
+
(`createOpenaiImage(model, apiKey, { allowUrlFetch: true })`, and likewise on
|
|
273
|
+
`createOpenaiVideo` / `createGeminiVideo`). `data:` URIs never need the flag.
|
|
274
|
+
|
|
264
275
|
**Role hints** (`metadata.role`):
|
|
265
276
|
|
|
266
277
|
| Role | Maps to |
|
|
@@ -448,8 +459,8 @@ return toServerSentEventsResponse(stream)
|
|
|
448
459
|
```
|
|
449
460
|
|
|
450
461
|
Google Veo (`@tanstack/ai-gemini`) uses the same jobs/polling flow. Its
|
|
451
|
-
`duration` option is typed per model (
|
|
452
|
-
|
|
462
|
+
`duration` option is typed per model (`4 | 6 | 8` for the Veo 3.1 models);
|
|
463
|
+
use `adapter.snapDuration(seconds)` to coerce raw
|
|
453
464
|
seconds and `adapter.availableDurations()` to enumerate the valid set.
|
|
454
465
|
Image prompt parts route by `metadata.role`: first un-roled /
|
|
455
466
|
`'start_frame'` image → input image, `'end_frame'` → `lastFrame`,
|
|
@@ -472,6 +483,35 @@ const { jobId } = await generateVideo({
|
|
|
472
483
|
// (x-goog-api-key header or ?key= query parameter).
|
|
473
484
|
```
|
|
474
485
|
|
|
486
|
+
Gemini Omni Flash (`geminiVideo('gemini-omni-flash-preview')`) is served by
|
|
487
|
+
the Interactions API instead of Veo's operations flow — same adapter, routed
|
|
488
|
+
by model. Clips are 720p; `duration` is any number of seconds in the 3–10
|
|
489
|
+
range (fractional ok, default 10 — availableDurations() reports the range),
|
|
490
|
+
`size` is the aspect ratio (`'16:9' | '9:16'`), and the finished video arrives
|
|
491
|
+
**inline** as a `data:video/mp4;base64,…` URL (no key needed to use it).
|
|
492
|
+
Image/video prompt parts are sent as interaction content blocks, grouped
|
|
493
|
+
as images, then videos, then text (no
|
|
494
|
+
`metadata.role` routing); `data` sources go inline, `url` sources pass
|
|
495
|
+
through as-is (never downloaded — use Gemini Files API URIs for remote
|
|
496
|
+
media). For conversational editing, pass a prior generation's `jobId` as
|
|
497
|
+
`modelOptions.previous_interaction_id` with a prompt describing the change:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import { geminiVideo } from '@tanstack/ai-gemini'
|
|
501
|
+
|
|
502
|
+
const omni = geminiVideo('gemini-omni-flash-preview')
|
|
503
|
+
const first = await generateVideo({
|
|
504
|
+
adapter: omni,
|
|
505
|
+
prompt: 'A violinist outdoors',
|
|
506
|
+
})
|
|
507
|
+
// …poll first.jobId to completion, then edit it:
|
|
508
|
+
const edited = await generateVideo({
|
|
509
|
+
adapter: omni,
|
|
510
|
+
prompt: 'Make the violin invisible',
|
|
511
|
+
modelOptions: { previous_interaction_id: first.jobId },
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
475
515
|
Other video adapters: `openaiVideo('sora-2')` (pixel sizes like `'1280x720'`,
|
|
476
516
|
durations 4/8/12s, single `input_reference` image prompt part), `grokVideo(...)`
|
|
477
517
|
(`grok-imagine-video` does text-to-video + image-to-video; `grok-imagine-video-1.5` is
|
|
@@ -453,11 +453,26 @@ accessors throw: a deleted file resolves `after()` to `''` (it still has
|
|
|
453
453
|
a non-git workspace resolves **both** `before()` and `after()` to `''` and
|
|
454
454
|
makes `diff()` fall back to a synthesized add-patch built from `after()` —
|
|
455
455
|
except for a `delete` event in a non-git workspace, where there's nothing to
|
|
456
|
-
synthesize and `diff()` resolves to `''`.
|
|
456
|
+
synthesize and `diff()` resolves to `''`. In a git workspace a file git
|
|
457
|
+
**isn't tracking yet** (a file the agent created, and every later edit to it)
|
|
458
|
+
diffs empty because `git diff` ignores untracked files, so `diff()` falls
|
|
459
|
+
back to the same synthesized add-patch whenever the file is absent at the
|
|
460
|
+
baseline — a create-or-edit of an untracked file never streams an empty diff.
|
|
461
|
+
An empty diff for a **tracked** file (identical to the baseline) stays empty,
|
|
462
|
+
as it should. A **git-ignored** file is withheld: the file event still fires
|
|
463
|
+
(you're notified it changed) but `diff()` returns `''`, so a secret like a
|
|
464
|
+
`.env` never has its contents surfaced in the diff feed.
|
|
465
|
+
|
|
466
|
+
**Failures are logged, not silent.** Every git/exec/fs failure behind these
|
|
467
|
+
accessors (and behind the `find`-poll watcher) still falls back to `''`/an
|
|
468
|
+
empty snapshot, but logs first: real anomalies (a failed `git diff`, an
|
|
469
|
+
unreadable file, a lost `find` poll) under the `errors` category (on by
|
|
470
|
+
default); expected-empty conditions (a new file's `before()`, a non-git
|
|
471
|
+
baseline) under the `sandbox` debug category.
|
|
457
472
|
|
|
458
473
|
**Hook errors are swallowed per hook.** A throwing `sandbox` hook is caught
|
|
459
|
-
and logged under the `
|
|
460
|
-
stop other hooks (or the `sandbox.file` chunk) from continuing.
|
|
474
|
+
and logged under the `errors` category (on by default) — it cannot break the
|
|
475
|
+
run or stop other hooks (or the `sandbox.file` chunk) from continuing.
|
|
461
476
|
|
|
462
477
|
Source: docs/sandbox/observability.md
|
|
463
478
|
|
|
@@ -289,7 +289,10 @@ function ChatPage() {
|
|
|
289
289
|
return (
|
|
290
290
|
<div key={part.id}>
|
|
291
291
|
<p>Approve "{part.name}"?</p>
|
|
292
|
-
|
|
292
|
+
{/* `part.input` is the parsed, typed object (populated once
|
|
293
|
+
the arguments are complete, as they are at approval
|
|
294
|
+
time); `part.arguments` remains the raw JSON string. */}
|
|
295
|
+
<pre>{JSON.stringify(part.input, null, 2)}</pre>
|
|
293
296
|
<button
|
|
294
297
|
onClick={() =>
|
|
295
298
|
addToolApprovalResponse({
|
|
@@ -322,6 +325,15 @@ function ChatPage() {
|
|
|
322
325
|
}
|
|
323
326
|
```
|
|
324
327
|
|
|
328
|
+
> **Type-safe approval:** With typed `tools`, `part.approval` exists **only**
|
|
329
|
+
> on parts for tools defined with `needsApproval: true`. Tools without approval
|
|
330
|
+
> have no `approval` field (reading it is a compile error). For a
|
|
331
|
+
> tool-agnostic handler over a typed union, narrow with `'approval' in part`
|
|
332
|
+
> (`if (part.type === 'tool-call' && 'approval' in part && part.approval)`),
|
|
333
|
+
> or type a shared component against the base `ToolCallPart`. An untyped
|
|
334
|
+
> `useChat()` keeps `approval` on every tool-call part, which is why the
|
|
335
|
+
> snippet above (no `tools` generic) reads it directly.
|
|
336
|
+
|
|
325
337
|
### Pattern 4: Lazy Tool Discovery
|
|
326
338
|
|
|
327
339
|
Set `lazy: true` on rarely-needed tools. The LLM sees their names via a synthetic
|
|
@@ -363,6 +375,8 @@ export async function POST(request: Request) {
|
|
|
363
375
|
adapter: openaiText('gpt-5.5'),
|
|
364
376
|
messages,
|
|
365
377
|
tools: [getProducts, compareProducts],
|
|
378
|
+
// maxIterations bounds model turns, not tool calls. Prefer maxToolCalls
|
|
379
|
+
// (and maxToolCallsPerTurn) when you need a tool-call budget.
|
|
366
380
|
agentLoopStrategy: maxIterations(20),
|
|
367
381
|
})
|
|
368
382
|
return toServerSentEventsResponse(stream)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { AgentLoopStrategy } from '../../types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Creates a strategy that continues for a maximum number of
|
|
4
|
+
* Creates a strategy that continues for a maximum number of **model turns**
|
|
5
|
+
* (iterations), not tool calls.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
+
* One iteration can still emit many parallel tool calls. Prefer
|
|
8
|
+
* {@link maxToolCalls} (and optionally `maxToolCallsPerTurn` on `chat()`)
|
|
9
|
+
* when you need a tool-call budget.
|
|
10
|
+
*
|
|
11
|
+
* @param max - Maximum number of model turns to allow
|
|
7
12
|
* @returns AgentLoopStrategy that stops after max iterations
|
|
8
13
|
*
|
|
9
14
|
* @example
|
|
@@ -13,7 +18,7 @@ import type { AgentLoopStrategy } from '../../types'
|
|
|
13
18
|
* model: "gpt-4o",
|
|
14
19
|
* messages: [...],
|
|
15
20
|
* tools: [weatherTool],
|
|
16
|
-
* agentLoopStrategy: maxIterations(3), // Max 3
|
|
21
|
+
* agentLoopStrategy: maxIterations(3), // Max 3 model turns
|
|
17
22
|
* });
|
|
18
23
|
* ```
|
|
19
24
|
*/
|
|
@@ -21,6 +26,40 @@ export function maxIterations(max: number): AgentLoopStrategy {
|
|
|
21
26
|
return ({ iterationCount }) => iterationCount < max
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Creates a strategy that continues while `toolCallCount < max`.
|
|
31
|
+
*
|
|
32
|
+
* Unlike {@link maxIterations} (which counts model turns), this bounds
|
|
33
|
+
* **emitted** tool calls counted during the run (including ones skipped by
|
|
34
|
+
* `maxToolCallsPerTurn`). Strategies only run between turns, so the turn that
|
|
35
|
+
* crosses `max` is not truncated — the final count (and executions, unless
|
|
36
|
+
* `maxToolCallsPerTurn` is set) may exceed `max`. Pair with
|
|
37
|
+
* `chat({ maxToolCallsPerTurn })` to also cap parallel fan-out inside a single
|
|
38
|
+
* turn.
|
|
39
|
+
*
|
|
40
|
+
* @param max - Maximum cumulative emitted tool calls before stopping further turns
|
|
41
|
+
* @returns AgentLoopStrategy that returns true while `toolCallCount < max`
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { chat, combineStrategies, maxIterations, maxToolCalls } from '@tanstack/ai'
|
|
46
|
+
*
|
|
47
|
+
* const stream = chat({
|
|
48
|
+
* adapter: openaiText('gpt-4o'),
|
|
49
|
+
* messages: [...],
|
|
50
|
+
* tools: [weatherTool],
|
|
51
|
+
* maxToolCallsPerTurn: 10,
|
|
52
|
+
* agentLoopStrategy: combineStrategies([
|
|
53
|
+
* maxIterations(20),
|
|
54
|
+
* maxToolCalls(20),
|
|
55
|
+
* ]),
|
|
56
|
+
* })
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function maxToolCalls(max: number): AgentLoopStrategy {
|
|
60
|
+
return ({ toolCallCount }) => toolCallCount < max
|
|
61
|
+
}
|
|
62
|
+
|
|
24
63
|
/**
|
|
25
64
|
* Creates a strategy that continues until a specific finish reason is encountered
|
|
26
65
|
*
|
|
@@ -71,6 +110,7 @@ export function untilFinishReason(
|
|
|
71
110
|
* tools: [weatherTool],
|
|
72
111
|
* agentLoopStrategy: combineStrategies([
|
|
73
112
|
* maxIterations(10),
|
|
113
|
+
* maxToolCalls(20),
|
|
74
114
|
* ({ messages }) => messages.length < 100,
|
|
75
115
|
* ]),
|
|
76
116
|
* });
|