@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.
- 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 +2 -2
- package/dist/esm/index.js +2 -0
- 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 +9 -0
- package/package.json +1 -1
- 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 +13 -1
- 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 +2 -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 +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,
|
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?: {
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -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
|
|
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
|
-
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
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,
|