@tanstack/ai 0.42.0 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/esm/activities/chat/adapter.js +23 -16
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agent-loop-strategies.d.ts +5 -36
- package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
- package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
- package/dist/esm/activities/chat/cancel.d.ts +40 -0
- package/dist/esm/activities/chat/cancel.js +54 -0
- package/dist/esm/activities/chat/cancel.js.map +1 -0
- package/dist/esm/activities/chat/index.d.ts +28 -21
- package/dist/esm/activities/chat/index.js +2100 -1813
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
- package/dist/esm/activities/chat/mcp/manager.js +90 -77
- package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
- package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
- package/dist/esm/activities/chat/messages.js +397 -346
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.js +17 -15
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
- package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
- package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
- package/dist/esm/activities/chat/middleware/compose.js +623 -531
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.js +12 -5
- package/dist/esm/activities/chat/middleware/define.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
- package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
- package/dist/esm/activities/chat/middleware/locks.js +71 -0
- package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
- package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
- package/dist/esm/activities/chat/middleware/run-store.js +176 -0
- package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
- package/dist/esm/activities/chat/middleware/validate.js +23 -28
- package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
- package/dist/esm/activities/chat/stream/json-parser.js +39 -25
- package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
- package/dist/esm/activities/chat/stream/processor.js +1341 -1542
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/strategies.js +69 -53
- package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
- package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
- package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
- package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
- package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
- package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/error-payload.js +85 -47
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/activities/generateAudio/adapter.js +22 -15
- package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +4 -0
- package/dist/esm/activities/generateAudio/index.js +141 -105
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.js +22 -15
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +4 -0
- package/dist/esm/activities/generateImage/index.js +155 -111
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/adapter.js +22 -15
- package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
- package/dist/esm/activities/generateSpeech/index.js +159 -110
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/adapter.js +22 -15
- package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
- package/dist/esm/activities/generateTranscription/index.js +159 -100
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.js +36 -29
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +143 -19
- package/dist/esm/activities/generateVideo/index.js +456 -279
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.js +60 -48
- package/dist/esm/activities/generateVideo/snap.js.map +1 -1
- package/dist/esm/activities/index.js +8 -34
- package/dist/esm/activities/middleware/index.d.ts +1 -1
- package/dist/esm/activities/middleware/run.d.ts +10 -0
- package/dist/esm/activities/middleware/run.js +53 -29
- package/dist/esm/activities/middleware/run.js.map +1 -1
- package/dist/esm/activities/middleware/types.d.ts +44 -6
- package/dist/esm/activities/stream-generation-result.d.ts +4 -1
- package/dist/esm/activities/stream-generation-result.js +79 -44
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/adapter.js +22 -15
- package/dist/esm/activities/summarize/adapter.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/activities/summarize/index.d.ts +27 -0
- package/dist/esm/activities/summarize/index.js +268 -102
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -1
- package/dist/esm/adapter-internals.js +4 -11
- package/dist/esm/client.d.ts +25 -3
- package/dist/esm/client.js +131 -64
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/custom-events.d.ts +76 -0
- package/dist/esm/custom-events.js +37 -0
- package/dist/esm/custom-events.js.map +1 -0
- package/dist/esm/delivery-detach.d.ts +50 -0
- package/dist/esm/delivery-detach.js +71 -0
- package/dist/esm/delivery-detach.js.map +1 -0
- package/dist/esm/delivery-disconnect.d.ts +62 -0
- package/dist/esm/delivery-disconnect.js +81 -0
- package/dist/esm/delivery-disconnect.js.map +1 -0
- package/dist/esm/extend-adapter.js +19 -17
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +24 -6
- package/dist/esm/index.js +30 -98
- package/dist/esm/interrupt-resume.d.ts +71 -0
- package/dist/esm/interrupt-resume.js +438 -0
- package/dist/esm/interrupt-resume.js.map +1 -0
- package/dist/esm/interrupt-serialization.d.ts +12 -0
- package/dist/esm/interrupt-serialization.js +178 -0
- package/dist/esm/interrupt-serialization.js.map +1 -0
- package/dist/esm/interrupts.d.ts +84 -0
- package/dist/esm/interrupts.js +31 -0
- package/dist/esm/interrupts.js.map +1 -0
- package/dist/esm/locks.d.ts +10 -0
- package/dist/esm/locks.js +2 -0
- package/dist/esm/logger/console-logger.js +101 -78
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/internal-logger.js +104 -89
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +54 -49
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/logger/types.d.ts +1 -1
- package/dist/esm/middlewares/content-guard.js +142 -148
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/middlewares/index.js +2 -6
- package/dist/esm/middlewares/otel.d.ts +3 -1
- package/dist/esm/middlewares/otel.js +599 -732
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.js +47 -40
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/realtime/event-emitter.js +24 -25
- package/dist/esm/realtime/event-emitter.js.map +1 -1
- package/dist/esm/realtime/index.d.ts +5 -9
- package/dist/esm/realtime/index.js +29 -6
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/scope.d.ts +47 -0
- package/dist/esm/stream-durability.d.ts +171 -0
- package/dist/esm/stream-durability.js +295 -0
- package/dist/esm/stream-durability.js.map +1 -0
- package/dist/esm/stream-to-response.d.ts +178 -13
- package/dist/esm/stream-to-response.js +663 -115
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js +30 -16
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/system-prompts.js +27 -21
- package/dist/esm/system-prompts.js.map +1 -1
- package/dist/esm/tool-registry.js +72 -45
- package/dist/esm/tool-registry.js.map +1 -1
- package/dist/esm/tools/provider-tool.js +14 -5
- package/dist/esm/tools/provider-tool.js.map +1 -1
- package/dist/esm/types.d.ts +321 -42
- package/dist/esm/types.js +2 -0
- package/dist/esm/utilities/ag-ui-wire.js +79 -93
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +26 -4
- package/dist/esm/utilities/chat-params.js +218 -92
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/errors.js +28 -18
- package/dist/esm/utilities/errors.js.map +1 -1
- package/dist/esm/utilities/media-prompt.js +46 -41
- package/dist/esm/utilities/media-prompt.js.map +1 -1
- package/dist/esm/utilities/numbers.js +13 -10
- package/dist/esm/utilities/numbers.js.map +1 -1
- package/dist/esm/utilities/provider-executed.js +20 -11
- package/dist/esm/utilities/provider-executed.js.map +1 -1
- package/dist/esm/utilities/sampling-keys.js +31 -19
- package/dist/esm/utilities/sampling-keys.js.map +1 -1
- package/dist/esm/utilities/tool-result.js +42 -30
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/dist/esm/utilities/usage.js +27 -9
- package/dist/esm/utilities/usage.js.map +1 -1
- package/dist/esm/utils.js +26 -18
- package/dist/esm/utils.js.map +1 -1
- package/package.json +10 -6
- package/skills/ai-core/SKILL.md +69 -18
- package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
- package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
- package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +98 -11
- package/skills/ai-core/client-persistence/SKILL.md +277 -0
- package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
- package/skills/ai-core/debug-logging/SKILL.md +1 -1
- package/skills/ai-core/locks/SKILL.md +143 -0
- package/skills/ai-core/media-generation/SKILL.md +144 -12
- package/skills/ai-core/middleware/SKILL.md +258 -33
- package/skills/ai-core/structured-outputs/SKILL.md +1 -1
- package/skills/ai-core/tool-calling/SKILL.md +54 -61
- package/src/activities/chat/agent-loop-strategies.ts +5 -39
- package/src/activities/chat/cancel.ts +81 -0
- package/src/activities/chat/index.ts +1091 -200
- package/src/activities/chat/mcp/manager.ts +4 -4
- package/src/activities/chat/mcp/types.ts +2 -2
- package/src/activities/chat/messages.ts +5 -3
- package/src/activities/chat/middleware/builder.ts +1 -1
- package/src/activities/chat/middleware/compose.ts +186 -9
- package/src/activities/chat/middleware/index.ts +26 -0
- package/src/activities/chat/middleware/locks.ts +102 -0
- package/src/activities/chat/middleware/pending-turn.ts +47 -0
- package/src/activities/chat/middleware/run-disconnect.ts +62 -0
- package/src/activities/chat/middleware/run-store.ts +412 -0
- package/src/activities/chat/middleware/types.ts +62 -1
- package/src/activities/chat/stream/processor.ts +189 -5
- package/src/activities/chat/tools/approval-schema.ts +205 -0
- package/src/activities/chat/tools/tool-calls.ts +106 -13
- package/src/activities/chat/tools/tool-definition.ts +210 -39
- package/src/activities/generateAudio/index.ts +20 -3
- package/src/activities/generateImage/index.ts +20 -3
- package/src/activities/generateSpeech/index.ts +25 -3
- package/src/activities/generateTranscription/index.ts +26 -3
- package/src/activities/generateVideo/index.ts +345 -82
- package/src/activities/middleware/index.ts +2 -0
- package/src/activities/middleware/run.ts +31 -0
- package/src/activities/middleware/types.ts +49 -5
- package/src/activities/stream-generation-result.ts +30 -2
- package/src/activities/summarize/chat-stream-summarize.ts +5 -0
- package/src/activities/summarize/index.ts +200 -10
- package/src/adapter-internals.ts +10 -1
- package/src/client.ts +244 -0
- package/src/custom-events.ts +107 -0
- package/src/delivery-detach.ts +72 -0
- package/src/delivery-disconnect.ts +84 -0
- package/src/index.ts +138 -1
- package/src/interrupt-resume.ts +824 -0
- package/src/interrupt-serialization.ts +183 -0
- package/src/interrupts.ts +146 -0
- package/src/locks.ts +17 -0
- package/src/logger/types.ts +1 -1
- package/src/middlewares/otel.ts +23 -5
- package/src/realtime/index.ts +5 -9
- package/src/scope.ts +47 -0
- package/src/stream-durability.ts +598 -0
- package/src/stream-to-response.ts +1051 -95
- package/src/strip-to-spec-middleware.ts +3 -3
- package/src/types.ts +405 -45
- package/src/utilities/chat-params.ts +245 -55
- package/dist/esm/activities/index.js.map +0 -1
- package/dist/esm/adapter-internals.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/middlewares/index.js.map +0 -1
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
name: ai-core/media-generation
|
|
3
3
|
description: >
|
|
4
4
|
Image, audio, video, speech (TTS), and transcription generation using
|
|
5
|
-
activity-specific adapters: generateImage() with openaiImage/geminiImage,
|
|
5
|
+
activity-specific adapters: generateImage() with openaiImage/geminiImage/byteplusImage,
|
|
6
6
|
generateAudio() with geminiAudio/falAudio, generateVideo() with async
|
|
7
|
-
polling (openaiVideo/geminiVideo/grokVideo/falVideo, per-model typed
|
|
8
|
-
durations), generateSpeech() with openaiSpeech, generateTranscription()
|
|
9
|
-
with openaiTranscription. React hooks: useGenerateImage, useGenerateAudio,
|
|
7
|
+
polling (openaiVideo/geminiVideo/grokVideo/falVideo/byteplusVideo, per-model typed
|
|
8
|
+
durations), generateSpeech() with openaiSpeech/byteplusSpeech, generateTranscription()
|
|
9
|
+
with openaiTranscription/byteplusTranscription. React hooks: useGenerateImage, useGenerateAudio,
|
|
10
10
|
useGenerateSpeech, useTranscription, useGenerateVideo.
|
|
11
11
|
TanStack Start server function integration with toServerSentEventsResponse.
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: tanstack-ai
|
|
14
|
-
library_version: '0.
|
|
14
|
+
library_version: '0.42.0'
|
|
15
15
|
sources:
|
|
16
16
|
- 'TanStack/ai:docs/media/generations.md'
|
|
17
17
|
- 'TanStack/ai:docs/media/generation-hooks.md'
|
|
@@ -150,8 +150,16 @@ function ImageGenerator() {
|
|
|
150
150
|
### 1. Image Generation
|
|
151
151
|
|
|
152
152
|
Supported adapters: `openaiImage` (dall-e-2, dall-e-3, gpt-image-1,
|
|
153
|
-
gpt-image-1-mini, gpt-image-2)
|
|
154
|
-
gemini-3.1-flash-lite-image, imagen-4.0-generate-001, etc.)
|
|
153
|
+
gpt-image-1-mini, gpt-image-2), `geminiImage` (gemini-3.1-flash-image-preview,
|
|
154
|
+
gemini-3.1-flash-lite-image, imagen-4.0-generate-001, etc.) and `byteplusImage`
|
|
155
|
+
(Seedream — `seedream-4-0-250828`, `seedream-4-5-251128`, the 5.0 family).
|
|
156
|
+
|
|
157
|
+
> **Seedream quirks:** `watermark` defaults to **`true`** (pass
|
|
158
|
+
> `modelOptions: { watermark: false }` for a clean image), `size` is a token
|
|
159
|
+
> (`'1K'` | `'2K'` | `'4K'`) **or** explicit `'2048x2048'` pixels but never a
|
|
160
|
+
> mix, and `numberOfImages` is an **upper bound** — Seedream has no `n`, so it
|
|
161
|
+
> maps onto group-image mode and the model may return fewer. Reads
|
|
162
|
+
> `ARK_API_KEY`.
|
|
155
163
|
|
|
156
164
|
```typescript
|
|
157
165
|
import { generateImage } from '@tanstack/ai'
|
|
@@ -333,7 +341,19 @@ const { generate, result, isLoading } = useGenerateAudio({
|
|
|
333
341
|
|
|
334
342
|
### 3. Text-to-Speech
|
|
335
343
|
|
|
336
|
-
|
|
344
|
+
Adapters: `openaiSpeech` (tts-1, tts-1-hd, gpt-4o-audio-preview) and
|
|
345
|
+
`byteplusSpeech` (`seed-audio-1.0`).
|
|
346
|
+
|
|
347
|
+
> **BytePlus Seed Speech is a separate product from ModelArk** — it reads
|
|
348
|
+
> **`BYTEPLUS_VOICE_API_KEY`**, not `ARK_API_KEY`, and an Ark key there fails
|
|
349
|
+
> with `45000010 Invalid X-Api-Key`. Output is capped at **120 seconds**.
|
|
350
|
+
> There is no top-level `speaker` field — `voice` is sent as
|
|
351
|
+
> `references: [{ speaker }]`, and `modelOptions.references` **replaces** that
|
|
352
|
+
> array rather than merging, so passing `references` for voice cloning silently
|
|
353
|
+
> drops `voice`. Voice ids ending `_uranus_bigtts` are TTS 2.0,
|
|
354
|
+
> `_mars_bigtts` / `_moon_bigtts` are TTS 1.0, and `*_emo_v2_*` are the 1.0
|
|
355
|
+
> voices that accept emotion tags. Formats: `wav`, `mp3`, `pcm`, `ogg_opus`;
|
|
356
|
+
> `watermark` is also available on `modelOptions`.
|
|
337
357
|
|
|
338
358
|
```typescript
|
|
339
359
|
import { generateSpeech } from '@tanstack/ai'
|
|
@@ -367,8 +387,10 @@ const { generate, result, isLoading } = useGenerateSpeech({
|
|
|
367
387
|
|
|
368
388
|
### 4. Audio Transcription
|
|
369
389
|
|
|
370
|
-
|
|
371
|
-
gpt-4o-mini-transcribe, gpt-4o-transcribe-diarize)
|
|
390
|
+
Adapters: `openaiTranscription` (whisper-1, gpt-4o-transcribe,
|
|
391
|
+
gpt-4o-mini-transcribe, gpt-4o-transcribe-diarize) and `byteplusTranscription`
|
|
392
|
+
(`seed-asr` — synchronous, no polling; audio up to 2 hours / 100 MB; also reads
|
|
393
|
+
**`BYTEPLUS_VOICE_API_KEY`**).
|
|
372
394
|
|
|
373
395
|
> **Capturing audio in the browser:** Use `useAudioRecorder` from `@tanstack/ai-react` to record directly in the browser, then pass the recording as the `audio` input to `generate()`, or use `recording.part` as a prompt part in chat/generation calls. No transcoding or extra dependencies required — the recorder returns the native browser format (`audio/webm` or `audio/mp4`). For transcription, wrap it as a `data:` URL so the provider gets the real content type; passing raw `recording.base64` makes the adapter assume `audio/mpeg` and mislabel the webm/mp4 bytes.
|
|
374
396
|
>
|
|
@@ -517,7 +539,20 @@ durations 4/8/12s, single `input_reference` image prompt part), `grokVideo(...)`
|
|
|
517
539
|
(`grok-imagine-video` does text-to-video + image-to-video; `grok-imagine-video-1.5` is
|
|
518
540
|
image-to-video only — needs an `image` prompt part as the starting frame, text-only throws;
|
|
519
541
|
aspect-ratio size template like `'16:9_720p'`, integer durations 1-15s, reports
|
|
520
|
-
`usage.unitsBilled` seconds and exact `usage.cost`),
|
|
542
|
+
`usage.unitsBilled` seconds and exact `usage.cost`), `byteplusVideo(...)` (Seedance —
|
|
543
|
+
aspect-ratio size template like `'16:9_720p'`, durations 4-15s on the 2.0 family,
|
|
544
|
+
4-12s on 1.5-pro, 2-12s on the 1.0-pro models; reads `ARK_API_KEY`), and
|
|
545
|
+
`falVideo(...)` (hosted models, see cost tracking below).
|
|
546
|
+
|
|
547
|
+
> **Seedance option applicability is per model and enforced server-side** —
|
|
548
|
+
> Ark returns a 400 for an inapplicable field rather than ignoring it.
|
|
549
|
+
> `service_tier` / `camera_fixed` are Seedance 1.x only, `frames` is
|
|
550
|
+
> 1-0-pro + 1-0-pro-fast only, `draft` is 1-5-pro only, `priority` is the 2.0
|
|
551
|
+
> family only, and `duration: -1` works on 2.0 + 1-5-pro. There is no 2K tier
|
|
552
|
+
> on any model and `4k` exists only on `dreamina-seedance-2-0-260128`.
|
|
553
|
+
> **Video URLs expire 24 hours after the task completes** (task record kept 7
|
|
554
|
+
> days). Seedance is also reachable via `falVideo` — `byteplusVideo` is the
|
|
555
|
+
> direct-to-BytePlus path.
|
|
521
556
|
|
|
522
557
|
Client hook with job tracking:
|
|
523
558
|
|
|
@@ -563,6 +598,94 @@ if (result.usage?.unitsBilled != null) {
|
|
|
563
598
|
For video, the units arrive with the completed result: `getVideoJobStatus()`
|
|
564
599
|
returns `usage` and emits a `video:usage` devtools event when fal reports it.
|
|
565
600
|
|
|
601
|
+
### 7. Durable persistence (job lifecycle + artifact bytes)
|
|
602
|
+
|
|
603
|
+
To make generations survive a server restart and be re-served later, add
|
|
604
|
+
`withGenerationPersistence` from `@tanstack/ai-persistence` as generation
|
|
605
|
+
middleware. It requires `stores.generationRuns` (a `GenerationRunStore`, keyed on
|
|
606
|
+
the run's own `runId`, with a required `threadId` naming the stable slot the run
|
|
607
|
+
fills — that is what a client hydrates by) and, when you also pass an `stores.artifacts` +
|
|
608
|
+
`stores.blobs` **pair** (both or neither), it persists the generated media bytes
|
|
609
|
+
at blob key `artifacts/<runId>/<artifactId>` with an `ArtifactRecord` per file.
|
|
610
|
+
`memoryPersistence()` ships all three for dev/tests.
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
import { generateImage, toServerSentEventsResponse } from '@tanstack/ai'
|
|
614
|
+
import { openaiImage } from '@tanstack/ai-openai'
|
|
615
|
+
import {
|
|
616
|
+
withGenerationPersistence,
|
|
617
|
+
memoryPersistence,
|
|
618
|
+
retrieveArtifact,
|
|
619
|
+
retrieveBlob,
|
|
620
|
+
reconstructGeneration,
|
|
621
|
+
} from '@tanstack/ai-persistence'
|
|
622
|
+
|
|
623
|
+
const persistence = memoryPersistence() // swap for your DB/object-store adapter
|
|
624
|
+
|
|
625
|
+
export async function POST(req: Request) {
|
|
626
|
+
const { prompt, threadId } = await req.json()
|
|
627
|
+
return toServerSentEventsResponse(
|
|
628
|
+
generateImage({
|
|
629
|
+
adapter: openaiImage('gpt-image-1'),
|
|
630
|
+
prompt,
|
|
631
|
+
threadId, // the slot recorded on the job + artifacts
|
|
632
|
+
stream: true,
|
|
633
|
+
middleware: [
|
|
634
|
+
withGenerationPersistence(persistence, {
|
|
635
|
+
// Stamp a durable app-origin serve URL (the GET route below) onto
|
|
636
|
+
// each persisted artifact ref, and rewrite the live result's media to
|
|
637
|
+
// it. Both live and restored results then render from your origin.
|
|
638
|
+
artifactUrl: (ref) => `/api/artifacts?id=${ref.artifactId}`,
|
|
639
|
+
}),
|
|
640
|
+
],
|
|
641
|
+
}),
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Serve the stored bytes back (GET /api/artifacts?id=…):
|
|
646
|
+
export async function GET(req: Request) {
|
|
647
|
+
const id = new URL(req.url).searchParams.get('id') ?? ''
|
|
648
|
+
const record = await retrieveArtifact(persistence, id)
|
|
649
|
+
if (!record) return new Response('Not found', { status: 404 })
|
|
650
|
+
const blob = await retrieveBlob(persistence, record)
|
|
651
|
+
if (!blob?.body) return new Response('Not found', { status: 404 })
|
|
652
|
+
return new Response(blob.body, {
|
|
653
|
+
headers: { 'content-type': record.mimeType },
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
That route is enough for images. **Video needs `Range`**: seeking a `<video>`
|
|
659
|
+
is built on `206` / `Content-Range`, and Safari refuses to play a source that
|
|
660
|
+
ignores `Range` at all. Resolve the header against `record.size` (`416` when it
|
|
661
|
+
does not fit), pass `retrieveBlob(persistence, record, { range })`, and answer
|
|
662
|
+
`206` from the returned `blob.range` plus `accept-ranges: bytes`. The full
|
|
663
|
+
route is in the persistence docs under **Serve video: honour `Range`**.
|
|
664
|
+
|
|
665
|
+
On the client, `persistence` is **boolean only**: `persistence: true` hydrates
|
|
666
|
+
the last generation for the thread on mount, via the connection's
|
|
667
|
+
`hydrateGeneration` handler backed by a `reconstructGeneration` GET route. There
|
|
668
|
+
is no storage-adapter mode, so nothing about a generation is cached in the
|
|
669
|
+
browser.
|
|
670
|
+
|
|
671
|
+
**`persistence: true` requires a stable `threadId`**, and it is a type error to
|
|
672
|
+
set one without the other. That is the generation's scope, the slot successive
|
|
673
|
+
runs fill (e.g. `video-9-start-frame`), not a link to a chat. `id` is the
|
|
674
|
+
devtools label only and never a persistence key.
|
|
675
|
+
|
|
676
|
+
The hooks are transparent (like `useChat`): a reload repaints `status` /
|
|
677
|
+
`result` / `error`, not a separate `resumeSnapshot`. Because the `artifactUrl`
|
|
678
|
+
above stamps a durable URL onto each ref (carried on `result.artifacts`), the
|
|
679
|
+
restored `result` rebuilds its media from those refs and serves from your own
|
|
680
|
+
origin. Without byte storage, a reload restores `status` / `error` and `result`
|
|
681
|
+
stays `null`.
|
|
682
|
+
|
|
683
|
+
- Building the R2/D1-backed byte stores for a Cloudflare Worker:
|
|
684
|
+
**ai-persistence/build-cloudflare-artifact-store**.
|
|
685
|
+
- Store contracts, `composePersistence`, and the wiring end-to-end:
|
|
686
|
+
`docs/persistence/generation-persistence.md` and the
|
|
687
|
+
`ai-core/client-persistence` sub-skill.
|
|
688
|
+
|
|
566
689
|
---
|
|
567
690
|
|
|
568
691
|
## Common Hook API
|
|
@@ -577,7 +700,16 @@ All generation hooks return the same shape:
|
|
|
577
700
|
| `error` | `Error \| undefined` | Current error |
|
|
578
701
|
| `status` | `GenerationClientState` | `'idle' \| 'generating' \| 'success' \| 'error'` |
|
|
579
702
|
| `stop` | `() => void` | Abort current generation |
|
|
580
|
-
| `reset` | `() => void` | Clear state
|
|
703
|
+
| `reset` | `() => void` | Clear state and the in-memory snapshot |
|
|
704
|
+
| `runId` | `string \| null` | Id of the job WHILE it runs; null when idle |
|
|
705
|
+
|
|
706
|
+
The hook is **transparent**, mirroring `useChat`: there is no `resumeSnapshot`,
|
|
707
|
+
`resumeState`, `pendingArtifacts`, or `resultArtifacts` field. Hooks also accept
|
|
708
|
+
`persistence: true` plus a stable `threadId`: on mount the client hydrates the
|
|
709
|
+
last run for that scope from the server and repaints the **normal** `status` /
|
|
710
|
+
`result` / `error` fields, so the last run survives a reload (metadata only,
|
|
711
|
+
never media bytes; `result`'s media returns only with server byte storage +
|
|
712
|
+
`artifactUrl`). See `ai-core/client-persistence` for details.
|
|
581
713
|
|
|
582
714
|
Provide either `connection` (streaming SSE transport) or `fetcher`
|
|
583
715
|
(direct async call / server function returning `Response`). Use `onResult`
|
|
@@ -8,10 +8,11 @@ description: >
|
|
|
8
8
|
execution order. NOT onEnd/onFinish callbacks on chat() — use middleware.
|
|
9
9
|
type: sub-skill
|
|
10
10
|
library: tanstack-ai
|
|
11
|
-
library_version: '0.
|
|
11
|
+
library_version: '0.42.0'
|
|
12
12
|
sources:
|
|
13
13
|
- 'TanStack/ai:docs/advanced/middleware.md'
|
|
14
14
|
- 'TanStack/ai:docs/sandbox/observability.md'
|
|
15
|
+
- 'TanStack/ai:docs/persistence/overview.md'
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
# Middleware
|
|
@@ -57,6 +58,7 @@ Every hook receives a `ChatMiddlewareContext` as its first argument, which provi
|
|
|
57
58
|
| `onStructuredOutputConfig` | Once at the structured-output boundary (only when `chat({ outputSchema })`) | `StructuredOutputMiddlewareConfig` (return partial) |
|
|
58
59
|
| `onStart` | Once after initial `onConfig` | none |
|
|
59
60
|
| `onIteration` | Start of each agent loop iteration | `IterationInfo` |
|
|
61
|
+
| `onShouldContinue` | Whether to start another agent-loop iteration (AND with strategy; `false` stops) | `AgentLoopState` |
|
|
60
62
|
| `onChunk` | Every streamed chunk | `StreamChunk` (return void/chunk/chunk[]/null) |
|
|
61
63
|
| `onBeforeToolCall` | Before each tool executes | `ToolCallHookContext` (return decision or void) |
|
|
62
64
|
| `onAfterToolCall` | After each tool executes | `AfterToolCallInfo` |
|
|
@@ -116,17 +118,25 @@ onStructuredOutputConfig?: (
|
|
|
116
118
|
| void
|
|
117
119
|
| null
|
|
118
120
|
| Partial<StructuredOutputMiddlewareConfig>
|
|
119
|
-
| Promise<void | Partial<StructuredOutputMiddlewareConfig>>
|
|
121
|
+
| Promise<void | null | Partial<StructuredOutputMiddlewareConfig>>
|
|
120
122
|
```
|
|
121
123
|
|
|
122
124
|
**`StructuredOutputMiddlewareConfig` shape:**
|
|
123
125
|
|
|
124
126
|
```ts
|
|
125
|
-
interface StructuredOutputMiddlewareConfig extends
|
|
127
|
+
interface StructuredOutputMiddlewareConfig extends Omit<
|
|
128
|
+
ChatMiddlewareConfig,
|
|
129
|
+
'tools'
|
|
130
|
+
> {
|
|
126
131
|
outputSchema: JSONSchema // The JSON Schema being sent to the provider
|
|
127
132
|
}
|
|
128
133
|
```
|
|
129
134
|
|
|
135
|
+
Note the `Omit<…, 'tools'>`: there is **no `config.tools`** on this hook. The
|
|
136
|
+
structured-output call is the final, tool-free call, so reading or returning
|
|
137
|
+
`tools` here is a compile error, not a no-op. Transform tools in `onConfig`
|
|
138
|
+
instead.
|
|
139
|
+
|
|
130
140
|
**Ordering rule:**
|
|
131
141
|
|
|
132
142
|
- `onStructuredOutputConfig` fires **before** `onConfig` at the structured-output boundary.
|
|
@@ -211,11 +221,18 @@ const toolGuard: ChatMiddleware = {
|
|
|
211
221
|
return { type: 'abort', reason: 'Dangerous operation blocked' }
|
|
212
222
|
}
|
|
213
223
|
|
|
214
|
-
// Enforce default arguments
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
args
|
|
224
|
+
// Enforce default arguments. `hookCtx.args` is `unknown` — the provider
|
|
225
|
+
// sent it — so narrow before reading it. No `as` casts.
|
|
226
|
+
if (hookCtx.toolName === 'search') {
|
|
227
|
+
const args =
|
|
228
|
+
typeof hookCtx.args === 'object' && hookCtx.args !== null
|
|
229
|
+
? hookCtx.args
|
|
230
|
+
: {}
|
|
231
|
+
if (!('limit' in args)) {
|
|
232
|
+
return {
|
|
233
|
+
type: 'transformArgs',
|
|
234
|
+
args: { ...args, limit: 10 },
|
|
235
|
+
}
|
|
219
236
|
}
|
|
220
237
|
}
|
|
221
238
|
|
|
@@ -346,6 +363,52 @@ const stream = chat({
|
|
|
346
363
|
| `onUsage` | Sequential | All run in order |
|
|
347
364
|
| `onFinish/onAbort/onError` | Sequential | All run in order |
|
|
348
365
|
|
|
366
|
+
## Pattern: tool-call budget (app-owned)
|
|
367
|
+
|
|
368
|
+
Not a built-in. Cap fan-out with `onBeforeToolCall` skip + `onShouldContinue`.
|
|
369
|
+
See `docs/chat/agentic-cycle.md` ("Tool-call budgets").
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import { chat, maxIterations, type ChatMiddleware } from '@tanstack/ai'
|
|
373
|
+
|
|
374
|
+
function toolCallBudget(opts: {
|
|
375
|
+
max?: number
|
|
376
|
+
maxPerTurn?: number
|
|
377
|
+
}): ChatMiddleware {
|
|
378
|
+
let perTurn = 0
|
|
379
|
+
return {
|
|
380
|
+
onIteration: () => {
|
|
381
|
+
perTurn = 0
|
|
382
|
+
},
|
|
383
|
+
onToolPhaseComplete: () => {
|
|
384
|
+
perTurn = 0
|
|
385
|
+
},
|
|
386
|
+
onBeforeToolCall: () => {
|
|
387
|
+
if (opts.maxPerTurn == null) return undefined
|
|
388
|
+
if (++perTurn > opts.maxPerTurn) {
|
|
389
|
+
return {
|
|
390
|
+
type: 'skip',
|
|
391
|
+
result: {
|
|
392
|
+
error: `Skipped: exceeded maxToolCallsPerTurn (${opts.maxPerTurn})`,
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return undefined
|
|
397
|
+
},
|
|
398
|
+
onShouldContinue: (_ctx, state) =>
|
|
399
|
+
opts.max != null && state.toolCallCount >= opts.max ? false : undefined,
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
chat({
|
|
404
|
+
adapter,
|
|
405
|
+
messages,
|
|
406
|
+
tools: [weatherTool],
|
|
407
|
+
agentLoopStrategy: maxIterations(20),
|
|
408
|
+
middleware: [toolCallBudget({ maxPerTurn: 10, max: 20 })],
|
|
409
|
+
})
|
|
410
|
+
```
|
|
411
|
+
|
|
349
412
|
## Built-in: toolCacheMiddleware
|
|
350
413
|
|
|
351
414
|
Caches tool call results by name + arguments. Import from `@tanstack/ai/middlewares`:
|
|
@@ -372,6 +435,148 @@ Options: `maxSize` (default 100), `ttl` (default Infinity), `toolNames` (default
|
|
|
372
435
|
`keyFn` (custom cache key), `storage` (custom backend like Redis). See
|
|
373
436
|
`docs/advanced/middleware.md` for custom storage examples.
|
|
374
437
|
|
|
438
|
+
## Server State Persistence: withPersistence
|
|
439
|
+
|
|
440
|
+
`withPersistence(persistence)` (from `@tanstack/ai-persistence`) is a
|
|
441
|
+
`ChatMiddleware` that persists **state** for `chat()` — thread messages, run
|
|
442
|
+
records (status/timing/usage/errors), and interrupt state — to a backend store.
|
|
443
|
+
Add it to the `middleware` array like any other middleware. It never mutates the
|
|
444
|
+
chunk stream; replaying a dropped/reloaded _stream_ is a separate transport-layer
|
|
445
|
+
concern (see ai-core/chat-experience/SKILL.md resumability, not this middleware).
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import {
|
|
449
|
+
chat,
|
|
450
|
+
chatParamsFromRequest,
|
|
451
|
+
toServerSentEventsResponse,
|
|
452
|
+
} from '@tanstack/ai'
|
|
453
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
454
|
+
import { withPersistence, memoryPersistence } from '@tanstack/ai-persistence'
|
|
455
|
+
|
|
456
|
+
// memoryPersistence() is the in-process reference backend (dev/tests). For a
|
|
457
|
+
// durable one, implement the store contracts against your database — see the
|
|
458
|
+
// @tanstack/ai-persistence skills.
|
|
459
|
+
const persistence = memoryPersistence()
|
|
460
|
+
|
|
461
|
+
export async function POST(request: Request) {
|
|
462
|
+
const params = await chatParamsFromRequest(request)
|
|
463
|
+
|
|
464
|
+
const stream = chat({
|
|
465
|
+
adapter: openaiText('gpt-5.5'),
|
|
466
|
+
messages: params.messages,
|
|
467
|
+
threadId: params.threadId,
|
|
468
|
+
runId: params.runId,
|
|
469
|
+
...(params.resume ? { resume: params.resume } : {}),
|
|
470
|
+
middleware: [withPersistence(persistence)],
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
return toServerSentEventsResponse(stream)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Authoritative-history contract
|
|
478
|
+
|
|
479
|
+
The middleware treats each request's `messages` as the source of truth for the
|
|
480
|
+
thread:
|
|
481
|
+
|
|
482
|
+
- **Non-empty `messages`** → on a successful finish (and at an interrupt
|
|
483
|
+
boundary) the middleware **overwrites** the entire stored thread with that
|
|
484
|
+
array. Post the **complete** transcript, never just the newest message(s) — a
|
|
485
|
+
delta would replace and destroy the stored history.
|
|
486
|
+
- **Empty `messages`** → the middleware **loads** the stored thread and runs the
|
|
487
|
+
turn from the server's copy. This is how you continue a conversation without
|
|
488
|
+
resending history from the client.
|
|
489
|
+
|
|
490
|
+
### Backends
|
|
491
|
+
|
|
492
|
+
`@tanstack/ai-persistence` ships **contracts, not a database backend**. It
|
|
493
|
+
provides the four store interfaces (`messages`, `runs`, `interrupts`,
|
|
494
|
+
`metadata`), the middleware that drives them, `memoryPersistence()` for
|
|
495
|
+
dev/tests, and a conformance testkit. For anything durable you implement the
|
|
496
|
+
stores against your own database and pass the result to `withPersistence`.
|
|
497
|
+
|
|
498
|
+
Annotate your factory with a named shape (`ChatPersistence` /
|
|
499
|
+
`ChatTranscriptPersistence`) — bare `AIPersistence` is the all-optional bag and
|
|
500
|
+
`withPersistence` rejects it.
|
|
501
|
+
|
|
502
|
+
Locks are separate from state and are **not** a `stores` key: wire a
|
|
503
|
+
`LockStore` with `withLocks(lockStore)`.
|
|
504
|
+
|
|
505
|
+
The `runs` store in that list is typed against `RunStore`, which ships in
|
|
506
|
+
`@tanstack/ai` alongside `RunRecord`, `RunStatus`, `TerminalRunStatus`,
|
|
507
|
+
`RunError`, `isTerminalRunStatus`, `defineRunStore`, and `InMemoryRunStore`. A
|
|
508
|
+
`RunRecord` tracks one run: `runId`, `threadId`, `status`, `startedAt`, plus
|
|
509
|
+
optional `finishedAt`, `error`, `usage`, `sandboxKey`, `detachedSince`,
|
|
510
|
+
`cancelRequested`, and `driverEpoch`. A backend must round-trip **all** of
|
|
511
|
+
them: `cancelRequested` is the durable out-of-band cancel channel
|
|
512
|
+
(`requestRunCancel` writes it, `wasCancelRequested` reads it), and
|
|
513
|
+
`driverEpoch` is the monotonic fencing token each host bumps when it claims a
|
|
514
|
+
run, so a superseded host can discover it lost. Omit either and a durable
|
|
515
|
+
sandboxed run loses a mechanism silently — Stop stops reaching a remote
|
|
516
|
+
driver, or nothing fences a dead host's writes.
|
|
517
|
+
`error` is a structured `RunError` (`{ message: string, code?: string }`), not
|
|
518
|
+
a bare string: `message` is the provider's prose, `code` is the stable,
|
|
519
|
+
machine-branchable classification a consumer switches on. Only
|
|
520
|
+
`createOrResume`, `update`, `get`, and `findActiveRun` are required on a
|
|
521
|
+
`RunStore`; `listByThread` and `listReclaimable` are optional, so a backend can
|
|
522
|
+
leave either out and callers feature-detect
|
|
523
|
+
(`store.listReclaimable?.(opts)`). Shape your own store with
|
|
524
|
+
`defineRunStore` for autocomplete without a separate `: RunStore` annotation,
|
|
525
|
+
matching `defineLock`; `defineRunStore<const T extends RunStore>(store: T): T`
|
|
526
|
+
returns the argument's own type, so an optional method your store implements
|
|
527
|
+
stays known-present on the result instead of collapsing to `| undefined`.
|
|
528
|
+
`isTerminalRunStatus(status)` is a type predicate narrowing `RunStatus` to
|
|
529
|
+
`TerminalRunStatus`, so code inside the guard can pass `status` where a
|
|
530
|
+
`TerminalRunStatus` is required without a cast. When a backend omits an
|
|
531
|
+
optional `RunStore` method, declare the omission when running the conformance
|
|
532
|
+
testkit (`ai-persistence/stores`'s `skipMethods` option) rather than leaving it
|
|
533
|
+
undeclared.
|
|
534
|
+
|
|
535
|
+
### `StreamDurability.snapshot()`
|
|
536
|
+
|
|
537
|
+
A `StreamDurability` (the event-log backend `memoryStream` / `durableStream`
|
|
538
|
+
implement, and what `@tanstack/ai-sandbox`'s run driver resolves per run — its
|
|
539
|
+
`RunDeps.durability` / `sandboxRunDriver({ durability })` is a factory
|
|
540
|
+
`(runId) => StreamDurability`, because one log is bound to one run) requires a
|
|
541
|
+
`snapshot()` method alongside `append`, `read`, and `close`:
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
snapshot: () => Promise<Array<{ offset: TOffset; chunk: StreamChunk }>>
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
It returns everything stored for a run right now, in append order, then
|
|
548
|
+
resolves. Use it, not `read()`, when a caller needs to inspect a run's stored
|
|
549
|
+
prefix and get an answer back: `read()` tails and only resolves once the log
|
|
550
|
+
is terminalized with `close()` or the caller aborts, so it never resolves
|
|
551
|
+
against a producer that crashed without calling `close()`, and its log stays
|
|
552
|
+
open indefinitely. `snapshot()` resolves immediately with what is stored,
|
|
553
|
+
including while the log is still open, and resolves to an empty array for a
|
|
554
|
+
run with nothing stored yet.
|
|
555
|
+
|
|
556
|
+
**Full guidance lives in the package's own skills** — start at
|
|
557
|
+
`node_modules/@tanstack/ai-persistence/skills/ai-persistence/SKILL.md`,
|
|
558
|
+
which routes to the server, client, stores, locks, and adapter-recipe
|
|
559
|
+
(Drizzle / Prisma / Cloudflare) sub-skills.
|
|
560
|
+
|
|
561
|
+
### Resume reconstruction is the middleware's job (server-authoritative path)
|
|
562
|
+
|
|
563
|
+
When a thread has pending interrupts, the middleware **records** them and
|
|
564
|
+
**gates** new input: a request that carries pending interrupts must include a
|
|
565
|
+
`resume` batch that references them, or `onConfig` throws. On a valid resume
|
|
566
|
+
batch the middleware also **builds `ChatResumeToolState`** (approvals /
|
|
567
|
+
client-tool results) and **clears `config.resume`** so the chat engine skips
|
|
568
|
+
its ephemeral reconstruction — that path needs client message history the
|
|
569
|
+
persistence flow deliberately omits when the server owns the transcript.
|
|
570
|
+
Resumes accepted in `onConfig` are committed (marked resolved/cancelled) only
|
|
571
|
+
once the run reaches a successful boundary, so a provider failure between
|
|
572
|
+
accepting a resume and finishing leaves the interrupt pending and a retry with
|
|
573
|
+
the same resume succeeds.
|
|
574
|
+
|
|
575
|
+
> A companion `withGenerationPersistence(persistence)` tracks run records for
|
|
576
|
+
> non-chat generation activities (image, audio, TTS, video, transcription).
|
|
577
|
+
|
|
578
|
+
Source: docs/persistence/overview.md
|
|
579
|
+
|
|
375
580
|
## Sandbox File-Event Hooks (`sandbox` group)
|
|
376
581
|
|
|
377
582
|
Declare a `sandbox: ChatSandboxHooks` group on `defineChatMiddleware` to react
|
|
@@ -507,35 +712,38 @@ has no effect on the stream output.
|
|
|
507
712
|
|
|
508
713
|
Source: docs/advanced/middleware.md
|
|
509
714
|
|
|
510
|
-
### b. MEDIUM: Middleware exceptions breaking the stream
|
|
715
|
+
### b. MEDIUM: Middleware exceptions breaking the stream — in `onChunk` / `onConfig`
|
|
716
|
+
|
|
717
|
+
Know which hooks the framework already guards. **The terminal hooks
|
|
718
|
+
(`onFinish`, `onAbort`, `onError`) are individually wrapped** by core's
|
|
719
|
+
`runTerminalHook`: a throw there is logged on the `errors` channel and the next
|
|
720
|
+
middleware's terminal hook still runs, so a failed analytics `POST` in `onFinish`
|
|
721
|
+
cannot break the stream or replace the abort reason. Guarding those is about
|
|
722
|
+
keeping your own bookkeeping intact, not about protecting the run.
|
|
723
|
+
|
|
724
|
+
**`onChunk` and `onConfig` are NOT guarded, deliberately** — they are transforms
|
|
725
|
+
on the data path, where swallowing a throw would forward a chunk or a config the
|
|
726
|
+
middleware had decided to reject. A throw from either fails the whole stream. That
|
|
727
|
+
is where an unhandled error actually costs you a response:
|
|
511
728
|
|
|
512
729
|
```typescript
|
|
513
|
-
// WRONG -- unhandled error kills the entire streaming response
|
|
730
|
+
// WRONG -- an unhandled error in onChunk kills the entire streaming response
|
|
514
731
|
const fragile: ChatMiddleware = {
|
|
515
|
-
name: 'fragile-
|
|
516
|
-
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
732
|
+
name: 'fragile-chunk-logger',
|
|
733
|
+
onChunk: (ctx, chunk) => {
|
|
734
|
+
// A logger that throws on an unexpected chunk shape takes the stream with it
|
|
735
|
+
logChunk(chunk)
|
|
736
|
+
},
|
|
737
|
+
onConfig: (ctx, config) => {
|
|
738
|
+
// Same for a config transform that reads an env var that is not set
|
|
739
|
+
return { model: requireEnv('MODEL_OVERRIDE') }
|
|
522
740
|
},
|
|
523
741
|
}
|
|
524
742
|
|
|
525
|
-
// CORRECT --
|
|
743
|
+
// CORRECT -- own the failure inside the unguarded hooks
|
|
526
744
|
const resilient: ChatMiddleware = {
|
|
527
|
-
name: 'resilient-
|
|
528
|
-
onFinish: (ctx, info) => {
|
|
529
|
-
// Option 1: defer (non-blocking, errors are isolated)
|
|
530
|
-
ctx.defer(
|
|
531
|
-
fetch('/api/analytics', {
|
|
532
|
-
method: 'POST',
|
|
533
|
-
body: JSON.stringify({ duration: info.duration }),
|
|
534
|
-
}),
|
|
535
|
-
)
|
|
536
|
-
},
|
|
745
|
+
name: 'resilient-chunk-logger',
|
|
537
746
|
onChunk: (ctx, chunk) => {
|
|
538
|
-
// Option 2: try-catch for synchronous/critical hooks
|
|
539
747
|
try {
|
|
540
748
|
logChunk(chunk)
|
|
541
749
|
} catch (err) {
|
|
@@ -543,17 +751,34 @@ const resilient: ChatMiddleware = {
|
|
|
543
751
|
}
|
|
544
752
|
// Return void to pass through
|
|
545
753
|
},
|
|
754
|
+
onConfig: (ctx, config) => {
|
|
755
|
+
const override = process.env.MODEL_OVERRIDE
|
|
756
|
+
// Decide, do not throw: no override means no transform.
|
|
757
|
+
return override === undefined ? undefined : { model: override }
|
|
758
|
+
},
|
|
759
|
+
onFinish: (ctx, info) => {
|
|
760
|
+
// Already guarded by core — but prefer ctx.defer() anyway, so a slow
|
|
761
|
+
// analytics call does not delay the terminal fan-out at all.
|
|
762
|
+
ctx.defer(
|
|
763
|
+
fetch('/api/analytics', {
|
|
764
|
+
method: 'POST',
|
|
765
|
+
body: JSON.stringify({ duration: info.duration }),
|
|
766
|
+
}),
|
|
767
|
+
)
|
|
768
|
+
},
|
|
546
769
|
}
|
|
547
770
|
```
|
|
548
771
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
772
|
+
Rule: put the try-catch where the framework has none — `onChunk` and `onConfig`
|
|
773
|
+
(and the other transform hooks: `onStructuredOutputConfig`, `onBeforeToolCall`,
|
|
774
|
+
`onAfterToolCall`). For async side effects in the terminal hooks, prefer
|
|
775
|
+
`ctx.defer()`, which runs after the terminal hook and isolates failures.
|
|
552
776
|
|
|
553
|
-
Source: docs/advanced/middleware.md
|
|
777
|
+
Source: docs/advanced/middleware.md, `packages/ai/src/activities/chat/middleware/compose.ts`
|
|
554
778
|
|
|
555
779
|
## Cross-References
|
|
556
780
|
|
|
557
781
|
- See also: **ai-core/chat-experience/SKILL.md** -- Middleware hooks into the chat lifecycle
|
|
558
782
|
- See also: **ai-core/structured-outputs/SKILL.md** -- Middleware now wraps the final structured-output call; use `onStructuredOutputConfig` for JSON-Schema transforms
|
|
559
783
|
- See also: **ai-core/ag-ui-protocol/SKILL.md** -- Reading the `sandbox.file` / `sandbox.file.diff` `CUSTOM` chunks the sandbox runtime emits alongside these `sandbox` hooks, via `ChatStream`'s typed `KnownCustomEvent` narrowing
|
|
784
|
+
- See also: **`@tanstack/ai-persistence` skills** (`skills/ai-persistence/SKILL.md` in that package) -- Full persistence suite (`withPersistence`, client storage, store contracts, adapter recipes, locks). This file only sketches server `withPersistence`.
|
|
@@ -13,7 +13,7 @@ description: >
|
|
|
13
13
|
part. convertSchemaToJsonSchema() for manual schema conversion.
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: tanstack-ai
|
|
16
|
-
library_version: '0.
|
|
16
|
+
library_version: '0.42.0'
|
|
17
17
|
sources:
|
|
18
18
|
- 'TanStack/ai:docs/structured-outputs/overview.md'
|
|
19
19
|
- 'TanStack/ai:docs/structured-outputs/one-shot.md'
|