@tanstack/ai 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/esm/activities/chat/messages.js +7 -0
  2. package/dist/esm/activities/chat/messages.js.map +1 -1
  3. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -0
  4. package/dist/esm/activities/chat/stream/message-updaters.js +3 -1
  5. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  6. package/dist/esm/activities/chat/stream/processor.js +18 -1
  7. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  8. package/dist/esm/activities/chat/tools/tool-definition.d.ts +14 -11
  9. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  10. package/dist/esm/activities/generateAudio/index.d.ts +1 -1
  11. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  12. package/dist/esm/activities/generateImage/index.d.ts +1 -1
  13. package/dist/esm/activities/generateImage/index.js +1 -1
  14. package/dist/esm/activities/generateImage/index.js.map +1 -1
  15. package/dist/esm/activities/generateSpeech/index.d.ts +1 -1
  16. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  17. package/dist/esm/activities/generateTranscription/index.d.ts +1 -1
  18. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  19. package/dist/esm/activities/generateVideo/index.d.ts +12 -12
  20. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  21. package/dist/esm/index.d.ts +2 -2
  22. package/dist/esm/index.js +2 -0
  23. package/dist/esm/index.js.map +1 -1
  24. package/dist/esm/realtime/event-emitter.d.ts +5 -0
  25. package/dist/esm/realtime/event-emitter.js +27 -0
  26. package/dist/esm/realtime/event-emitter.js.map +1 -0
  27. package/dist/esm/realtime/index.d.ts +1 -0
  28. package/dist/esm/realtime/index.js.map +1 -1
  29. package/dist/esm/realtime/types.d.ts +9 -1
  30. package/dist/esm/types.d.ts +9 -0
  31. package/package.json +1 -1
  32. package/skills/ai-core/media-generation/SKILL.md +42 -2
  33. package/skills/ai-core/middleware/SKILL.md +18 -3
  34. package/skills/ai-core/tool-calling/SKILL.md +13 -1
  35. package/src/activities/chat/messages.ts +9 -0
  36. package/src/activities/chat/stream/message-updaters.ts +7 -1
  37. package/src/activities/chat/stream/processor.ts +30 -2
  38. package/src/activities/chat/tools/tool-definition.ts +33 -11
  39. package/src/activities/generateAudio/index.ts +2 -2
  40. package/src/activities/generateImage/index.ts +2 -2
  41. package/src/activities/generateSpeech/index.ts +2 -2
  42. package/src/activities/generateTranscription/index.ts +2 -2
  43. package/src/activities/generateVideo/index.ts +29 -15
  44. package/src/index.ts +2 -1
  45. package/src/realtime/event-emitter.ts +46 -0
  46. package/src/realtime/index.ts +2 -0
  47. package/src/realtime/types.ts +9 -0
  48. package/src/types.ts +9 -0
package/dist/esm/index.js CHANGED
@@ -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,
@@ -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,4 +1,5 @@
1
1
  import { RealtimeToken, RealtimeTokenOptions } from './types.js';
2
+ export { createRealtimeEventEmitter } from './event-emitter.js';
2
3
  export type * from './types.js';
3
4
  /**
4
5
  * Generate a realtime token using the provided adapter.
@@ -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":"AA8BA,eAAsB,cACpB,SACwB;AACxB,QAAM,EAAE,YAAY;AACpB,SAAO,QAAQ,cAAA;AACjB;"}
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 */
@@ -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?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -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 (e.g. `4 | 6 | 8` for Veo 3.x,
452
- `5 | 6 | 8` for Veo 2); use `adapter.snapDuration(seconds)` to coerce raw
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 `sandbox` debug category — it cannot break the run or
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
- <pre>{part.arguments}</pre>
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
@@ -444,12 +444,21 @@ export function modelMessageToUIMessage(
444
444
  // Handle tool calls
445
445
  if (modelMessage.toolCalls && modelMessage.toolCalls.length > 0) {
446
446
  for (const toolCall of modelMessage.toolCalls) {
447
+ // Model-message arguments are complete, so surface the parsed input.
448
+ // A malformed arguments string just leaves `input` undefined.
449
+ let input: unknown
450
+ try {
451
+ input = JSON.parse(toolCall.function.arguments)
452
+ } catch {
453
+ input = undefined
454
+ }
447
455
  parts.push({
448
456
  type: 'tool-call',
449
457
  id: toolCall.id,
450
458
  name: toolCall.function.name,
451
459
  arguments: toolCall.function.arguments,
452
460
  state: 'input-complete', // Model messages have complete arguments
461
+ ...(input !== undefined && { input }),
453
462
  ...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
454
463
  })
455
464
  }
@@ -58,6 +58,8 @@ export function updateToolCallPart(
58
58
  name: string
59
59
  arguments: string
60
60
  state: ToolCallState
61
+ /** Parsed input — set when the arguments are complete. */
62
+ input?: unknown
61
63
  metadata?: Record<string, unknown>
62
64
  },
63
65
  ): Array<UIMessage> {
@@ -76,6 +78,9 @@ export function updateToolCallPart(
76
78
  // Gemini's thoughtSignature on TOOL_CALL_START) we must not lose it on
77
79
  // subsequent updates that don't re-supply it.
78
80
  const metadata = toolCall.metadata ?? existing?.metadata
81
+ // Same for the parsed input: it's supplied once at completion, so
82
+ // subsequent arg-less updates (approval, etc.) must not drop it.
83
+ const input = toolCall.input ?? existing?.input
79
84
 
80
85
  const toolCallPart: ToolCallPart = {
81
86
  type: 'tool-call',
@@ -83,9 +88,10 @@ export function updateToolCallPart(
83
88
  name: toolCall.name,
84
89
  arguments: toolCall.arguments,
85
90
  state: toolCall.state,
86
- // Carry forward approval and output from the existing part
91
+ // Carry forward approval, output and parsed input from the existing part
87
92
  ...(existing?.approval && { approval: { ...existing.approval } }),
88
93
  ...(existing?.output !== undefined && { output: existing.output }),
94
+ ...(input !== undefined && { input }),
89
95
  ...(metadata !== undefined && { metadata }),
90
96
  }
91
97
 
@@ -1298,15 +1298,35 @@ export class StreamProcessor {
1298
1298
  // received, back-fill the arguments string so the UIMessage ToolCallPart
1299
1299
  // carries the correct value (defensive against adapters that skip ARGS).
1300
1300
  if (chunk.input !== undefined && !existingToolCall.arguments) {
1301
- existingToolCall.arguments = JSON.stringify(chunk.input)
1301
+ try {
1302
+ existingToolCall.arguments = JSON.stringify(chunk.input)
1303
+ } catch {
1304
+ // circular refs, BigInt, etc. — leave arguments empty rather than
1305
+ // aborting stream processing
1306
+ }
1302
1307
  }
1303
1308
 
1304
1309
  const index = msgState.toolCallOrder.indexOf(chunk.toolCallId)
1305
1310
  this.completeToolCall(messageId, index, existingToolCall)
1306
1311
  // If TOOL_CALL_END provides parsed input, use it as the canonical parsed
1307
1312
  // arguments (overrides the accumulated string parse from completeToolCall)
1313
+ // and refresh the rendered part's `input` so it reflects the canonical
1314
+ // value rather than the possibly-divergent accumulated-args parse that
1315
+ // completeToolCall wrote (e.g. an adapter that coerces values differently
1316
+ // between the streamed args and the final structured input).
1308
1317
  if (chunk.input !== undefined) {
1309
1318
  existingToolCall.parsedArguments = chunk.input
1319
+ this.messages = updateToolCallPart(this.messages, messageId, {
1320
+ id: existingToolCall.id,
1321
+ name: existingToolCall.name,
1322
+ arguments: existingToolCall.arguments,
1323
+ state: 'input-complete',
1324
+ input: chunk.input,
1325
+ ...(existingToolCall.metadata !== undefined && {
1326
+ metadata: existingToolCall.metadata,
1327
+ }),
1328
+ })
1329
+ this.emitMessagesChange()
1310
1330
  }
1311
1331
  }
1312
1332
 
@@ -1890,12 +1910,20 @@ export class StreamProcessor {
1890
1910
  return
1891
1911
  }
1892
1912
 
1893
- // Update UIMessage
1913
+ // Update UIMessage. The arguments are complete now, so surface the parsed
1914
+ // input on the part. For adapters that skip TOOL_CALL_ARGS the arguments
1915
+ // string was back-filled from TOOL_CALL_END.input, so this parse matches
1916
+ // the canonical input. If a TOOL_CALL_END.input diverges from the
1917
+ // accumulated args, handleToolCallEndEvent re-updates the part with the
1918
+ // canonical value after this call.
1894
1919
  this.messages = updateToolCallPart(this.messages, messageId, {
1895
1920
  id: toolCall.id,
1896
1921
  name: toolCall.name,
1897
1922
  arguments: toolCall.arguments,
1898
1923
  state: 'input-complete',
1924
+ ...(toolCall.parsedArguments !== undefined && {
1925
+ input: toolCall.parsedArguments,
1926
+ }),
1899
1927
  ...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
1900
1928
  })
1901
1929
  this.emitMessagesChange()
@@ -26,6 +26,10 @@ export interface ClientTool<
26
26
  TOutput extends SchemaInput = SchemaInput,
27
27
  TName extends string = string,
28
28
  TContext = unknown,
29
+ // Captured as a literal (`true` / `false`) so downstream types — notably
30
+ // the tool-call part's `approval` field — can be gated on it. Defaults to
31
+ // `false` when the tool config omits `needsApproval`.
32
+ TNeedsApproval extends boolean = false,
29
33
  > {
30
34
  __toolSide: 'client'
31
35
  name: TName
@@ -37,7 +41,7 @@ export interface ClientTool<
37
41
  // because `undefined` doesn't extend the schema constraint.
38
42
  inputSchema?: TInput
39
43
  outputSchema?: TOutput
40
- needsApproval?: boolean
44
+ needsApproval?: TNeedsApproval
41
45
  lazy?: boolean
42
46
  metadata?: Record<string, unknown>
43
47
  execute?: ToolExecuteFunction<TInput, TOutput, TContext>
@@ -51,18 +55,22 @@ export interface ToolDefinitionInstance<
51
55
  TOutput extends SchemaInput = SchemaInput,
52
56
  TName extends string = string,
53
57
  TContext = unknown,
58
+ TNeedsApproval extends boolean = false,
54
59
  > extends Tool<TInput, TOutput, TName, TContext> {
55
60
  __toolSide: 'definition'
61
+ // Narrow the base `needsApproval?: boolean` to the captured literal so it
62
+ // survives into `ToolCallPartForTool`'s approval gate.
63
+ needsApproval?: TNeedsApproval
56
64
  }
57
65
 
58
66
  /**
59
67
  * Union type for any kind of client-side tool (client tool or definition)
60
68
  */
61
69
  export type AnyClientTool =
62
- | (Omit<ClientTool<any, any, string, any>, 'execute'> & {
70
+ | (Omit<ClientTool<any, any, string, any, boolean>, 'execute'> & {
63
71
  execute?: ((args: any, context?: any) => any) | undefined
64
72
  })
65
- | (Omit<ToolDefinitionInstance<any, any, string, any>, 'execute'> & {
73
+ | (Omit<ToolDefinitionInstance<any, any, string, any, boolean>, 'execute'> & {
66
74
  execute?: ((args: any, context?: any) => any) | undefined
67
75
  })
68
76
 
@@ -100,12 +108,13 @@ export interface ToolDefinitionConfig<
100
108
  TInput extends SchemaInput = SchemaInput,
101
109
  TOutput extends SchemaInput = SchemaInput,
102
110
  TName extends string = string,
111
+ TNeedsApproval extends boolean = false,
103
112
  > {
104
113
  name: TName
105
114
  description: string
106
115
  inputSchema?: TInput
107
116
  outputSchema?: TOutput
108
- needsApproval?: boolean
117
+ needsApproval?: TNeedsApproval
109
118
  lazy?: boolean
110
119
  metadata?: Record<string, unknown>
111
120
  }
@@ -117,7 +126,14 @@ export interface ToolDefinition<
117
126
  TInput extends SchemaInput = SchemaInput,
118
127
  TOutput extends SchemaInput = SchemaInput,
119
128
  TName extends string = string,
120
- > extends ToolDefinitionInstance<TInput, TOutput, TName> {
129
+ TNeedsApproval extends boolean = false,
130
+ > extends ToolDefinitionInstance<
131
+ TInput,
132
+ TOutput,
133
+ TName,
134
+ unknown,
135
+ TNeedsApproval
136
+ > {
121
137
  /**
122
138
  * Create a server-side tool with execute function
123
139
  */
@@ -126,11 +142,13 @@ export interface ToolDefinition<
126
142
  ) => ServerTool<TInput, TOutput, TName, TContext>
127
143
 
128
144
  /**
129
- * Create a client-side tool with optional execute function
145
+ * Create a client-side tool with optional execute function.
146
+ * Carries the definition's `needsApproval` literal through to the client
147
+ * tool so the tool-call part's `approval` field stays gated on it.
130
148
  */
131
149
  client: <TContext = unknown>(
132
150
  execute?: ToolExecuteFunction<TInput, TOutput, TContext>,
133
- ) => ClientTool<TInput, TOutput, TName, TContext>
151
+ ) => ClientTool<TInput, TOutput, TName, TContext, TNeedsApproval>
134
152
  }
135
153
 
136
154
  /**
@@ -192,10 +210,14 @@ export function toolDefinition<
192
210
  TInput extends SchemaInput = SchemaInput,
193
211
  TOutput extends SchemaInput = SchemaInput,
194
212
  TName extends string = string,
213
+ // `const` forces the literal (`true` / `false`) to be captured from the
214
+ // config's optional `needsApproval` — without it TS widens to `boolean`,
215
+ // which collapses the approval gate in `ToolCallPartForTool`.
216
+ const TNeedsApproval extends boolean = false,
195
217
  >(
196
- config: ToolDefinitionConfig<TInput, TOutput, TName>,
197
- ): ToolDefinition<TInput, TOutput, TName> {
198
- const definition: ToolDefinition<TInput, TOutput, TName> = {
218
+ config: ToolDefinitionConfig<TInput, TOutput, TName, TNeedsApproval>,
219
+ ): ToolDefinition<TInput, TOutput, TName, TNeedsApproval> {
220
+ const definition: ToolDefinition<TInput, TOutput, TName, TNeedsApproval> = {
199
221
  __toolSide: 'definition',
200
222
  ...config,
201
223
  server<TContext = unknown>(
@@ -210,7 +232,7 @@ export function toolDefinition<
210
232
 
211
233
  client<TContext = unknown>(
212
234
  execute?: ToolExecuteFunction<TInput, TOutput, TContext>,
213
- ): ClientTool<TInput, TOutput, TName, TContext> {
235
+ ): ClientTool<TInput, TOutput, TName, TContext, TNeedsApproval> {
214
236
  return {
215
237
  __toolSide: 'client',
216
238
  ...config,
@@ -14,10 +14,10 @@ import {
14
14
  runGenerationFinish,
15
15
  runGenerationStart,
16
16
  runGenerationUsage,
17
- } from '../middleware'
17
+ } from '../middleware/run'
18
18
  import type { InternalLogger } from '../../logger/internal-logger'
19
19
  import type { DebugOption } from '../../logger/types'
20
- import type { GenerationMiddleware } from '../middleware'
20
+ import type { GenerationMiddleware } from '../middleware/types'
21
21
  import type { AudioAdapter } from './adapter'
22
22
  import type { AudioGenerationResult, StreamChunk } from '../../types'
23
23
 
@@ -14,11 +14,11 @@ import {
14
14
  runGenerationFinish,
15
15
  runGenerationStart,
16
16
  runGenerationUsage,
17
- } from '../middleware'
17
+ } from '../middleware/run'
18
18
  import { resolveMediaPrompt } from '../../utilities/media-prompt'
19
19
  import type { InternalLogger } from '../../logger/internal-logger'
20
20
  import type { DebugOption } from '../../logger/types'
21
- import type { GenerationMiddleware } from '../middleware'
21
+ import type { GenerationMiddleware } from '../middleware/types'
22
22
  import type { ImageAdapter } from './adapter'
23
23
  import type {
24
24
  ImageGenerationResult,
@@ -14,10 +14,10 @@ import {
14
14
  runGenerationFinish,
15
15
  runGenerationStart,
16
16
  runGenerationUsage,
17
- } from '../middleware'
17
+ } from '../middleware/run'
18
18
  import type { InternalLogger } from '../../logger/internal-logger'
19
19
  import type { DebugOption } from '../../logger/types'
20
- import type { GenerationMiddleware } from '../middleware'
20
+ import type { GenerationMiddleware } from '../middleware/types'
21
21
  import type { TTSAdapter } from './adapter'
22
22
  import type { StreamChunk, TTSResult } from '../../types'
23
23
 
@@ -14,10 +14,10 @@ import {
14
14
  runGenerationFinish,
15
15
  runGenerationStart,
16
16
  runGenerationUsage,
17
- } from '../middleware'
17
+ } from '../middleware/run'
18
18
  import type { InternalLogger } from '../../logger/internal-logger'
19
19
  import type { DebugOption } from '../../logger/types'
20
- import type { GenerationMiddleware } from '../middleware'
20
+ import type { GenerationMiddleware } from '../middleware/types'
21
21
  import type { TranscriptionAdapter } from './adapter'
22
22
  import type {
23
23
  StreamChunk,