Skip to content

Commit 08c4861

Browse files
authored
Merge branch 'main' into fix/interleaved-text-tool-args-input
2 parents ff06a18 + 826cfed commit 08c4861

256 files changed

Lines changed: 23323 additions & 2134 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@tanstack/ai-persistence': minor
3+
---
4+
5+
The artifact options for `withGenerationPersistence` are now named
6+
`ArtifactPersistenceOptions`.
7+
8+
They were declared as a second `export interface WithPersistenceOptions`, which
9+
TypeScript merged with the chat middleware's options of the same name. The merge
10+
was invisible but not harmless: `withPersistence(chat, …)` silently accepted
11+
`extractArtifacts` / `storageKey` / `allowInputUrl` / `artifactFetch`, and
12+
`WithGenerationPersistenceOptions` — which extends it — advertised
13+
`snapshotStreaming` / `snapshotIntervalMs`. Every one of those is a no-op on the
14+
other middleware, so autocomplete offered options that did nothing.
15+
16+
`WithPersistenceOptions` keeps its meaning: the chat middleware's options.
17+
`WithGenerationPersistenceOptions` is unchanged in shape and is still what you
18+
pass to `withGenerationPersistence`, so only code that named the artifact
19+
interface directly needs an edit:
20+
21+
```diff
22+
-import type { WithPersistenceOptions } from '@tanstack/ai-persistence'
23+
-function artifactOptions(): WithPersistenceOptions {
24+
+import type { ArtifactPersistenceOptions } from '@tanstack/ai-persistence'
25+
+function artifactOptions(): ArtifactPersistenceOptions {
26+
return { storageKey: ({ runId, artifactId }) => `media/${runId}/${artifactId}` }
27+
}
28+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@tanstack/ai-client': minor
3+
---
4+
5+
A restored generation whose result can't be rebuilt now reports an error instead
6+
of repainting as a blank success.
7+
8+
Every `reconstructResult` mapper in `generation-reconstruct.ts` (and the video
9+
client's built-in `reconstructVideoResult`) returns `null` when the persisted
10+
record lacks what it needs — most commonly an output artifact stored without a
11+
serve `url`, which is possible because `artifactUrl` is optional server-side.
12+
`repaintFromSnapshot` silently skipped `setResult` in that case, leaving
13+
`status: 'success'` with `result: null`: a state no consumer can render, and one
14+
that hides the real cause.
15+
16+
When a mapper declines a snapshot whose status is `complete`, the restore now
17+
settles on `status: 'error'` with an explanatory message and fires `onError`. A
18+
decline on any other status is still silent — a `running` snapshot has no result
19+
yet by definition, and the rejoin delivers it. A client with no
20+
`reconstructResult` mapper at all is unaffected.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@tanstack/ai-client': minor
3+
---
4+
5+
Server-driven generation hydration no longer swallows every failure.
6+
7+
`GenerationClient` / `VideoGenerationClient` mount hydration
8+
(`persistence: true`) wrapped the whole `hydrateGeneration` call in a bare
9+
`try { … } catch { return }`, collapsing a transport error, a `403` from the
10+
`reconstructGeneration` authorize gate, an unparseable body, and "no record for
11+
this thread" into one indistinguishable silent no-op — so an app could not tell a
12+
broken server from a fresh thread, and had no signal to retry.
13+
14+
- A genuine **miss** (the server reports no record) stays silent, as before.
15+
- A genuine **failure** now surfaces on `status` / `error` and fires `onError`,
16+
with a message naming the cause. A record the client's own validator rejects
17+
(unknown schema version, missing/invalid `status` or `resumeState`) counts as a
18+
failure, not a miss.
19+
- The failure is skipped when a `generate()` took ownership of the client while
20+
the hydrate request was in flight — the live run still wins.
21+
22+
Relatedly, `fetchServerSentEvents` / `fetchHttpStream` `hydrateGeneration` now
23+
only treats a `200` carrying `null` as a miss. Any other non-object body (a
24+
string, an array) rejects instead of being reported as an empty thread, so a
25+
misconfigured route no longer masquerades as a fresh one.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@tanstack/ai-client': minor
3+
---
4+
5+
A generation stream that ends without a terminal chunk now settles to `error`
6+
instead of wedging the client on `generating` forever.
7+
8+
`GenerationClient.processStream` / `VideoGenerationClient.processStream` only
9+
settled the status on `RUN_FINISHED` or `RUN_ERROR`. A `for await` loop over a
10+
stream that simply _ends_ — a proxy/load-balancer idle timeout, a server restart
11+
mid-run, or a durable log whose terminal append never landed — returns normally,
12+
so no catch fired and the client came to rest on
13+
`status: 'generating'`, `isLoading: false`, `result: null`, with `onError` never
14+
called. Worse, the resume snapshot stayed `running`, so every subsequent mount
15+
rejoined the same dead run and repeated the same outcome.
16+
17+
Both clients now throw when the stream ends with no terminal chunk seen (and the
18+
read wasn't aborted by `stop()` / `dispose()`), which routes the failure through
19+
the existing error path: `status: 'error'`, `error` set, `onError` fired, and the
20+
resume snapshot rewritten to a terminal `error` with a null `resumeState` so
21+
nothing chases it again. This applies to both the initial `generate()` path and
22+
the mount-time `rejoinInFlight` path. A rejoin failure now also fires `onError`,
23+
matching `generate()`.
24+
25+
This is the sibling of the earlier "rejoin settles to error" fix, which covered a
26+
missing and a throwing `joinRun` but not a join that returns cleanly with no
27+
terminal chunk.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@tanstack/ai-client': minor
3+
---
4+
5+
`localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`
6+
are no longer generic. Each returns a `ChatStorageAdapter<ChatPersistedState>`,
7+
and `WebStoragePersistenceOptions` types its `serialize` / `deserialize` codec
8+
over `ChatPersistedState`.
9+
10+
The type parameter existed so one adapter could back both the chat and the
11+
generation `persistence` option. Generation `persistence` is now `boolean`
12+
(server-driven only), so chat is the sole option that takes a storage adapter and
13+
the parameter had no second value to hold.
14+
15+
A bare `localStoragePersistence()` is unchanged. A call that passed an explicit
16+
type argument for a standalone store, `localStoragePersistence<MyValue>()`, no
17+
longer compiles: build that store with your own object literal, since these
18+
factories are for chat state.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@tanstack/ai-persistence': minor
3+
---
4+
5+
Extend the shared conformance testkit to the generation stores.
6+
7+
**Migration — every existing adapter must update its conformance call.** The suite now fails loudly on a store that is absent without being declared, so a chat-only adapter that used to pass unchanged will start failing on `generationRuns` / `artifacts` / `blobs`. Declare them absent:
8+
9+
```diff
10+
- runPersistenceConformance('my-adapter', () => makePersistence())
11+
+ runPersistenceConformance('my-adapter', () => makePersistence(), {
12+
+ skip: ['generationRuns', 'artifacts', 'blobs'],
13+
+ })
14+
```
15+
16+
Drop an entry from `skip` as you implement that store — the suite then holds it to the contract below. Declaring absence is deliberate: a silently skipped store is how an adapter ships a `generationRuns` implementation that was never exercised.
17+
18+
`runPersistenceConformance` now exercises `generationRuns`, `artifacts`, and `blobs` alongside the four chat state stores, so a hand-rolled generation backend is held to the same gate as a chat one: `createOrResume` idempotency and `findLatestForThread` (latest by `startedAt`, thread-scoped, terminal runs included) on the run store; upsert `save`, `list(runId)` ordering, and `delete` / `deleteForRun` scoping on the artifact store; and byte/metadata round-trips, overwrite, silent absent-key `delete`, and `list` prefix + cursor paging on the blob store. Two invariants that were easy to get wrong and are now checked: `list`'s `prefix` matches **literally and case-sensitively** (a SQL backend using `LIKE` fails on both counts, since SQLite's `LIKE` is case-insensitive for ASCII and treats `%` / `_` as wildcards), and cursor paging visits every key exactly once.
19+
20+
`examples/ts-react-chat`'s self-contained `node:sqlite` adapter implements all seven stores and runs the full suite; its server-side generation route is backed by that adapter, so generated images survive a dev-server restart.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tanstack/ai': patch
3+
---
4+
5+
Fix `@tanstack/ai` breaking non-React TanStack Start builds.
6+
7+
A JSDoc example on `replayRunStream` inlined a server-function builder chain. Comments survive into `dist`, and Start's server-fn Vite plugin decides whether a module needs compiling by regex-matching the source — so it treated this package as a server-fn module and tried to resolve the framework's `@tanstack/*-start` package, failing the build of any Solid/Vue/Svelte Start app (`could not resolve "@tanstack/solid-start"`). The example now declares the generator separately and no longer trips the match.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tanstack/ai': minor
3+
---
4+
5+
`GenerationMiddlewareContext.resultTransforms` is now required.
6+
7+
Middleware registers a result transform by pushing onto the array, so an optional one let a host that builds its own context omit it and silently no-op every registration — generation persistence would then mark a run completed with neither its result nor its artifacts written, with nothing to observe but the missing data. Every context the library builds already comes from `createGenerationContext`, which always sets `[]`, so this only affects code that constructs a `GenerationMiddlewareContext` by hand: set `resultTransforms: []`.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@tanstack/ai': minor
3+
---
4+
5+
`summarize()` accepts generation middleware, so summaries can be persisted.
6+
7+
`useSummarize({ persistence: true, threadId })` type-checked exactly like the six media hooks, but `summarize()` took no `middleware`, so no library path could ever write its run record and a reload restored nothing. It now takes `middleware` like the `generate*` activities: one `onStart`, the result transforms applied to the `SummarizationResult`, then `onFinish` / `onError`, in both streaming and non-streaming mode (a consumer that disconnects mid-summary fires `onAbort`). In streaming mode the transformed result is what is yielded, so the client and the persisted record hold the same object.
8+
9+
`GenerationActivity` gained `'summarize'`, and `otelMiddleware` maps it to the `summarize` operation name. Summaries are text, so there are no artifacts: a persistence middleware stores the run record and its result and nothing else.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@tanstack/ai': minor
3+
---
4+
5+
Fix non-streaming `generateVideo()` losing the generation when persistence is on.
6+
7+
A non-streaming `generateVideo()` call only SUBMITS a job — the video does not exist until a later poll — but it fired `onFinish` as soon as the job was queued and never applied the result transforms, and it never put the caller's `threadId` on the middleware context. With `withGenerationPersistence` that meant `generateVideo({ threadId, middleware })` threw for want of a scope, and (once given one) would have stamped the run `completed` with no result, no url, and no stored bytes, while the eventual result had nowhere to land.
8+
9+
Submitting a job now OPENS the run and `getVideoJobStatus()` closes it, with the two calls correlated by the provider's **`jobId`** — the one id a poller structurally cannot be missing, since it cannot poll without it. Nothing else has to be threaded through:
10+
11+
- `generateVideo()` (non-streaming) passes `threadId` and the prompt inputs to middleware, files the run under an id derived from the provider + `jobId`, applies the result transforms to the submission (so the run record captures the `jobId` and stays resumable from a later request or process), and fires **no terminal hook**.
12+
- `getVideoJobStatus()` accepts `threadId` and `middleware`, and recomputes the same run id from `adapter` + `jobId`. On the poll that first observes a terminal job state it resumes that run, applies the result transforms — which is where persistence copies the video into the blob store and rewrites `url` to a durable one, so the returned result and the stored record carry the same urls — and fires `onFinish`, or `onError` when the job (or the url fetch) failed. Intermediate polls invoke nothing. Its result gained `jobId`, `expiresAt`, and `artifacts`; `VideoJobResult` gained `artifacts` (refs for persisted prompt INPUTS, e.g. a start frame).
13+
- `runId` on a non-streaming `generateVideo()` call is **ignored** (it remains the wire run id in stream mode). The run id has to be recomputable by the poll from the `jobId` alone; honoring a custom one would reintroduce the failure this avoids — a caller who set it on the submit and forgot it on the poll would silently open a second record while the first sat unfinished forever.
14+
15+
Two consequences worth knowing. Because the job id only exists once the provider accepts the job, `onStart` now fires AFTER the submit request, so an `otelMiddleware()` span covers the run from acceptance onward rather than the submit round-trip, and a submission that FAILS (no job to key on) opens and immediately fails a run under the call's `requestId` — terminal and unresumable, but filed under the thread so a hydrating client sees the failure. And `threadId` must reach the poll: omitting it makes generation persistence throw loudly rather than file the finished video where nothing can hydrate it.

0 commit comments

Comments
 (0)