Skip to content

Commit 37dc210

Browse files
authored
Merge branch 'main' into feat/billed-usage-unit
2 parents 091319a + 05280a5 commit 37dc210

177 files changed

Lines changed: 16353 additions & 1314 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@tanstack/ai-client': minor
3+
'@tanstack/ai-react': minor
4+
'@tanstack/ai-solid': minor
5+
'@tanstack/ai-vue': minor
6+
'@tanstack/ai-svelte': minor
7+
'@tanstack/ai-angular': minor
8+
'@tanstack/ai-preact': minor
9+
---
10+
11+
Add browser-refresh durability to the `persistence` option.
12+
13+
The client `persistence` adapter now stores one combined record per chat id, the message transcript plus a resume snapshot, so a full page reload restores the conversation, rehydrates any pending interrupt, and rejoins a run that was still streaming (via `joinRun`, when the connection is durability-backed). A bare `UIMessage[]` from an older store is still read for backward compatibility.
14+
15+
**If you hand-rolled a `persistence` adapter, update its write path.** `setItem` now receives the combined `{ messages, resume? }` record where it used to receive a bare `UIMessage[]`, so an adapter that assumed an array will write the new shape and then fail to parse it back — and because adapter reads are best-effort, the failure is silent: the conversation simply does not restore. Read `{ messages, resume? }` in `getItem` (a bare array is still accepted), or switch to the `localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence` adapters below, which handle it for you.
16+
17+
The `persistence` option also accepts `true` for a server-authoritative chat: the client caches nothing, and on mount it hydrates the thread from the server by its `threadId` (painting the stored transcript and tailing any run still generating). Use it to keep large transcripts off the client while the server stays authoritative for history; it needs a connection with a `hydrate` handler and a server GET endpoint (`reconstructChat`). Passing an adapter is client-authoritative; omitting `persistence` (or `false`) is ephemeral, in-memory only.
18+
19+
New web storage adapters are exported for this: `localStoragePersistence`, `sessionStoragePersistence`, and `indexedDBPersistence` (plus `StorageUnavailableError` and the `ChatPersistedState` / `ChatStorageAdapter` / `ChatPersistenceOption` types). Because durability rides the existing `persistence` option, every framework integration (`react`, `solid`, `vue`, `svelte`, `angular`, `preact`) gets it with no framework-specific code.

.changeset/define-lock.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@tanstack/ai': minor
3+
---
4+
5+
Add `defineLock` to `@tanstack/ai/locks`: an identity typer for a `LockStore`
6+
implementation, matching the `define*Store` helpers in `@tanstack/ai-persistence`.
7+
Pass a `withLock` object and get autocomplete and contract checking inline, with
8+
no `: LockStore` annotation, then hand it to `withLocks`.
9+
10+
```ts
11+
import { defineLock, withLocks } from '@tanstack/ai/locks'
12+
13+
const locks = defineLock({
14+
async withLock(key, fn) {
15+
const { release, signal } = await acquire(key)
16+
try {
17+
return await fn(signal)
18+
} finally {
19+
release()
20+
}
21+
},
22+
})
23+
24+
const middleware = [withLocks(locks)]
25+
```

.changeset/define-store-helpers.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@tanstack/ai-persistence': minor
3+
---
4+
5+
Add per-store typer helpers: `defineMessageStore`, `defineRunStore`,
6+
`defineInterruptStore`, `defineMetadataStore`.
7+
8+
Each takes a store implementation and returns it typed against the contract, so
9+
you get autocomplete and checking on the object literal inline — no separate
10+
`: MessageStore` return annotation. They compose into `defineAIPersistence`,
11+
which already infers **exact presence**: a store you define is a defined,
12+
non-optional, autocompleted key on `persistence.stores`, and accessing a store
13+
you did not define is a compile error.
14+
15+
```ts
16+
import {
17+
defineAIPersistence,
18+
defineMessageStore,
19+
defineRunStore,
20+
} from '@tanstack/ai-persistence'
21+
22+
const persistence = defineAIPersistence({
23+
stores: {
24+
messages: defineMessageStore({ loadThread, saveThread }),
25+
runs: defineRunStore({ createOrResume, update, get, findActiveRun }),
26+
},
27+
})
28+
29+
persistence.stores.runs // RunStore (defined)
30+
persistence.stores.interrupts // compile error — not provided
31+
```

.changeset/fast-fail-rejoin.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@tanstack/ai': minor
3+
'@tanstack/ai-client': patch
4+
---
5+
6+
Make a reload rejoin fast, robust, and repeatable.
7+
8+
- **`memoryStream` first-chunk deadline now defaults to 100ms** (was 30s). The
9+
common from-start join is a reload rejoining a run whose producer ran in a
10+
prior request: an in-flight run's log already holds chunks (it streams
11+
immediately, the deadline never applies), and an empty log means the run is
12+
gone — so failing fast lets the client re-enable input near-instantly instead
13+
of holding a dead connection open. Raise `firstChunkDeadlineMs` for a backend
14+
whose producer can legitimately start well after a joiner attaches.
15+
- **`ChatClient` reload rejoin hardened:** it bounds the wait for the first
16+
chunk and clears a dead resume pointer (so a stale pointer can't pin the UI in
17+
a loading state and can't be retried on the next load); it drops the hydrated
18+
in-flight partial only when real content arrives (never on `RUN_STARTED`
19+
alone), so a rejoin that connects but delivers nothing can't leave an empty
20+
assistant bubble; and it no longer lets a replayed `RUN_STARTED` (which
21+
carries the provider run id) overwrite the persisted resume pointer with an id
22+
the durability log isn't keyed by — so a SECOND consecutive reload still
23+
re-attaches and continues.

.changeset/fresh-client-tail.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@tanstack/ai-persistence': minor
3+
'@tanstack/ai-client': minor
4+
---
5+
6+
Server-authoritative reconnect is now automatic and keyed on the thread, not the run.
7+
8+
A chat's durable identity is its **thread**; run ids are ephemeral (a single turn
9+
can span several runs via interrupts or tool continuations), so basing reconnect
10+
on a client-cached run id goes stale the moment a turn rolls to a new run. This
11+
moves the whole reconnect story onto the stable thread id, resolved by the server.
12+
13+
- **`RunStore.findActiveRun(threadId)`** — new optional, feature-detected store
14+
method returning the most recent `'running'` run for a thread. Implemented by
15+
the in-memory reference backend and covered by the conformance testkit, so any
16+
adapter that provides it is held to the same invariants (most-recent-running
17+
wins, thread-scoped, null when idle).
18+
- **`reconstructChat` now returns `{ messages, activeRun, interrupts }`** (was a
19+
bare message array): the stored transcript as UI messages, a cursor to an
20+
in-flight run if one exists, and any pending human-in-the-loop interrupts (tool
21+
approvals / waits) plus the run they paused. It reads the active run before the
22+
transcript so observing "no active run" guarantees the transcript is final
23+
(closing a finish-window race).
24+
- **`@tanstack/ai-client` hydrates itself on mount.** In server-authoritative
25+
mode (`persistence: true`) the client caches no transcript and no run
26+
pointer: on mount `useChat`/`ChatClient` calls the connection's new
27+
`hydrate(threadId)` (a JSON GET against the same endpoint), paints the returned
28+
transcript, and — if a run is in flight — tails it via the existing `joinRun`
29+
durability replay. A reload and the same thread opened on another device are the
30+
identical, server-resolved path. No loader, no `initialMessages`, no
31+
`initialResumeSnapshot`, no app-side fetching required.
32+
- **Interrupts reconstruct from the server too.** A paused approval (a tool with
33+
`needsApproval`) is restored from `reconstructChat`'s `interrupts` exactly as a
34+
persisted resume snapshot would be, so a reload — or another device — re-prompts
35+
the same approve/reject decision and resumes the run it paused. Previously the
36+
pending interrupt was only recoverable from client storage, so a fresh client
37+
showed the paused tool call with no way to resolve it.
38+
39+
Apps keep the single GET endpoint they already have (durability replay when a
40+
resume cursor is present, else `reconstructChat`); everything else is handled by
41+
the hook.

.changeset/locks-to-core.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@tanstack/ai': minor
3+
'@tanstack/ai-persistence': minor
4+
'@tanstack/ai-sandbox': minor
5+
---
6+
7+
Move multi-instance **locks** to `@tanstack/ai` under a dedicated `@tanstack/ai/locks` subpath, and nest persistence agent skills like `ai-core`.
8+
9+
- **`LockStore` / `InMemoryLockStore` / `LocksCapability` / `getLocks` / `provideLocks` / `withLocks`** live in `@tanstack/ai/locks` (not the main `@tanstack/ai` barrel, and not `@tanstack/ai-persistence`).
10+
- `@tanstack/ai-sandbox` consumes the core `LocksCapability` token (no local lock re-export).
11+
- The locks agent skill moves with the code: `ai-core/locks` in `@tanstack/ai`, not `ai-persistence/locks`.
12+
- Agent skills under `@tanstack/ai-persistence` nest as `skills/ai-persistence/{stores,server,build-*-adapter}/`.
13+
- Docs: locks guide under advanced middleware.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
'@tanstack/ai': minor
3+
---
4+
5+
Rework tool-call fan-out budgets as middleware hooks (unreleased #965 API).
6+
7+
- **Remove** (never released): `maxToolCalls()` strategy and `chat({ maxToolCallsPerTurn })`
8+
- **Add** `onShouldContinue` middleware hook so policies can stop further agent turns without aborting
9+
- **Keep** `AgentLoopState.toolCallCount` / `lastTurnToolCallCount` for strategies and middleware
10+
- Tool-call budgets are an **app-owned middleware recipe** (docs), not a built-in export
11+
12+
```ts
13+
import { chat, maxIterations, type ChatMiddleware } from '@tanstack/ai'
14+
15+
function toolCallBudget({
16+
max,
17+
maxPerTurn,
18+
}: {
19+
max?: number
20+
maxPerTurn?: number
21+
}): ChatMiddleware {
22+
let perTurn = 0
23+
return {
24+
onIteration: () => {
25+
perTurn = 0
26+
},
27+
onToolPhaseComplete: () => {
28+
perTurn = 0
29+
},
30+
onBeforeToolCall: () => {
31+
if (maxPerTurn == null) return
32+
if (++perTurn > maxPerTurn) {
33+
return {
34+
type: 'skip',
35+
result: {
36+
error: `Skipped: exceeded maxToolCallsPerTurn (${maxPerTurn})`,
37+
},
38+
}
39+
}
40+
},
41+
onShouldContinue: (_ctx, state) =>
42+
max != null && state.toolCallCount >= max ? false : undefined,
43+
}
44+
}
45+
46+
chat({
47+
adapter,
48+
messages,
49+
tools,
50+
agentLoopStrategy: maxIterations(20),
51+
middleware: [toolCallBudget({ maxPerTurn: 10, max: 20 })],
52+
})
53+
```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@tanstack/ai': patch
3+
---
4+
5+
Fix `memoryStream` truncating a tool-calling (agent-loop) run at its first tool
6+
call.
7+
8+
An agent-loop run emits one `RUN_STARTED`/`RUN_FINISHED` pair per iteration
9+
(`finishReason: "tool_calls"` for a turn that calls a tool, then `"stop"` for the
10+
final answer). `memoryStream` treated the _first_ terminal chunk as the end of
11+
the log — both marking the log complete on append and ending the reader on read —
12+
so a run that called a tool was delivered only up to that first `RUN_FINISHED`:
13+
the tool result and everything after (the model's actual answer) never reached
14+
the client, leaving the tool call stuck "running" and the reply missing, on the
15+
initial stream and on any reconnect/reload.
16+
17+
Completion is now driven solely by the producer calling `close()` (which it does
18+
on every exit — the documented `StreamDurability.close` contract, honored by
19+
`toServerSentEventsResponse`/`resumeServerSentEventsResponse` and detached
20+
producers). The reader tails across per-iteration terminals and ends when the
21+
producer closes, so a tool-calling run is delivered in full — live, on rejoin,
22+
and on a server-authoritative reload.

.changeset/persistence-packages.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@tanstack/ai-persistence': minor
3+
---
4+
5+
Add server-side persistence for `chat()`: durable thread messages, run records, and interrupts.
6+
7+
`withPersistence(persistence)` is a chat middleware that stores the conversation transcript, tracks each run's status, and records interrupt state so a paused run (tool approval, client-tool execution, generic interrupt) survives a server restart.
8+
9+
`@tanstack/ai-persistence` ships the **contract**, not a backend for your database:
10+
11+
- The four store interfaces — `MessageStore`, `RunStore`, `InterruptStore`, `MetadataStore` — with the invariants the middleware depends on (full-replace `saveThread`, idempotent `createOrResume`, insert-if-absent interrupt `create`, `requestedAt`-ascending listings).
12+
- The `withPersistence` / `withGenerationPersistence` middleware, plus `composePersistence` to assemble stores that live in different systems.
13+
- `memoryPersistence()`, an in-process reference backend for dev and tests.
14+
- `LockStore` / `withLocks` / `InMemoryLockStore` for cross-worker coordination — deliberately **not** a state store, and not composable through `composePersistence`.
15+
- A shared conformance testkit at `@tanstack/ai-persistence/testkit`. `runPersistenceConformance` exercises every method of every store you provide and fails loudly on a store that is missing without being declared in `skip`.
16+
17+
Implement the stores against whatever database you already run and hand the result to `withPersistence` — the core never inspects your tables, so the schema stays yours. The [Build Your Own Adapter](https://tanstack.com/ai/latest/docs/persistence/build-your-own-adapter) guide walks through a complete `node:sqlite` backend end to end, and the package ships Agent Skills with worked Drizzle, Prisma, and Cloudflare D1 recipes (`npx @tanstack/intent@latest install`). `examples/ts-react-chat` runs on a self-contained `node:sqlite` adapter built this way and verified by the conformance testkit.
18+
19+
Resume reconstruction is delegated to the chat engine: persistence records interrupts and gates new input on a thread with pending interrupts, while the engine rebuilds the resume tool state from the resume batch and the interrupt bindings carried in the (server-loaded) message history.
20+
21+
`reconstructChat(persistence, request)` is a server helper that returns a thread's stored messages as a JSON `Response`, so a server-authoritative client can hydrate its transcript on load from a one-line `GET` handler.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@tanstack/ai-react': patch
3+
---
4+
5+
Fix `useChat` aborting an in-flight delivery resume on mount. When `live` was
6+
not enabled, the mount effect called `client.unsubscribe()` unconditionally,
7+
which cancelled the shared in-flight stream — including the `joinRun` rejoin the
8+
client had just started for a reloaded run. The result was a mid-stream reload
9+
that caught up to the buffered point and then froze instead of continuing.
10+
`useChat` now only tears down a subscription it actually started, so a reload
11+
rejoins and streams the run through to completion.

0 commit comments

Comments
 (0)