@tanstack/ai 0.52.3 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +14 -13
  2. package/dist/esm/activities/chat/index.js +5 -3
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/generateLiveVideo/adapter.d.ts +69 -0
  5. package/dist/esm/activities/generateLiveVideo/adapter.js +23 -0
  6. package/dist/esm/activities/generateLiveVideo/adapter.js.map +1 -0
  7. package/dist/esm/activities/generateLiveVideo/index.d.ts +99 -0
  8. package/dist/esm/activities/generateLiveVideo/index.js +162 -0
  9. package/dist/esm/activities/generateLiveVideo/index.js.map +1 -0
  10. package/dist/esm/activities/generateVideo/index.js +3 -1
  11. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  12. package/dist/esm/activities/generateWorld/adapter.d.ts +69 -0
  13. package/dist/esm/activities/generateWorld/adapter.js +23 -0
  14. package/dist/esm/activities/generateWorld/adapter.js.map +1 -0
  15. package/dist/esm/activities/generateWorld/index.d.ts +99 -0
  16. package/dist/esm/activities/generateWorld/index.js +162 -0
  17. package/dist/esm/activities/generateWorld/index.js.map +1 -0
  18. package/dist/esm/activities/index.d.ts +8 -2
  19. package/dist/esm/activities/index.js +11 -7
  20. package/dist/esm/activities/middleware/types.d.ts +1 -1
  21. package/dist/esm/activities/summarize/chat-stream-summarize.js +2 -1
  22. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  23. package/dist/esm/byok/define-provider.d.ts +6 -0
  24. package/dist/esm/byok/define-provider.js +2 -1
  25. package/dist/esm/byok/define-provider.js.map +1 -1
  26. package/dist/esm/byok/get-key.d.ts +7 -0
  27. package/dist/esm/byok/get-key.js +8 -1
  28. package/dist/esm/byok/get-key.js.map +1 -1
  29. package/dist/esm/byok/server.d.ts +1 -1
  30. package/dist/esm/byok/server.js +2 -2
  31. package/dist/esm/client.d.ts +4 -2
  32. package/dist/esm/client.js +3 -1
  33. package/dist/esm/client.js.map +1 -1
  34. package/dist/esm/index.d.ts +4 -2
  35. package/dist/esm/index.js +3 -1
  36. package/dist/esm/middlewares/otel.js +3 -1
  37. package/dist/esm/middlewares/otel.js.map +1 -1
  38. package/dist/esm/types.d.ts +112 -0
  39. package/package.json +2 -2
  40. package/skills/ai-core/adapter-configuration/SKILL.md +103 -54
  41. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +39 -21
  42. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +5 -0
  43. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +14 -6
  44. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +33 -25
  45. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +7 -2
  46. package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +25 -12
  47. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +19 -9
  48. package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +34 -21
  49. package/skills/ai-core/ag-ui-protocol/SKILL.md +16 -10
  50. package/skills/ai-core/chat-experience/SKILL.md +228 -108
  51. package/skills/ai-core/client-persistence/SKILL.md +21 -9
  52. package/skills/ai-core/custom-backend-integration/SKILL.md +86 -52
  53. package/skills/ai-core/debug-logging/SKILL.md +100 -18
  54. package/skills/ai-core/locks/SKILL.md +35 -7
  55. package/skills/ai-core/media-generation/SKILL.md +114 -49
  56. package/skills/ai-core/middleware/SKILL.md +174 -69
  57. package/skills/ai-core/structured-outputs/SKILL.md +99 -49
  58. package/skills/ai-core/tool-calling/SKILL.md +245 -158
  59. package/src/activities/chat/index.ts +6 -7
  60. package/src/activities/generateLiveVideo/adapter.ts +99 -0
  61. package/src/activities/generateLiveVideo/index.ts +339 -0
  62. package/src/activities/generateVideo/index.ts +3 -4
  63. package/src/activities/generateWorld/adapter.ts +96 -0
  64. package/src/activities/generateWorld/index.ts +339 -0
  65. package/src/activities/index.ts +44 -0
  66. package/src/activities/middleware/types.ts +2 -0
  67. package/src/activities/summarize/chat-stream-summarize.ts +2 -0
  68. package/src/byok/define-provider.ts +7 -0
  69. package/src/byok/get-key.ts +18 -0
  70. package/src/byok/server.ts +1 -1
  71. package/src/client.ts +8 -0
  72. package/src/index.ts +8 -0
  73. package/src/middlewares/otel.ts +2 -0
  74. package/src/types.ts +128 -0
@@ -22,8 +22,9 @@ This skill builds on ai-core and ai-core/chat-experience. Read them first.
22
22
 
23
23
  Connect `useChat` to a custom SSE backend with auth headers:
24
24
 
25
- ```typescript
25
+ ```tsx
26
26
  import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
27
+ import { token } from './auth'
27
28
 
28
29
  function Chat() {
29
30
  const { messages, sendMessage, isLoading } = useChat({
@@ -69,6 +70,7 @@ framing. This is the recommended default.
69
70
 
70
71
  ```typescript
71
72
  import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
73
+ import { token, tenantId } from './auth'
72
74
 
73
75
  const { messages, sendMessage } = useChat({
74
76
  connection: fetchServerSentEvents('https://my-api.com/chat', {
@@ -85,6 +87,7 @@ const { messages, sendMessage } = useChat({
85
87
 
86
88
  ```typescript
87
89
  import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
90
+ import { sessionId, getAccessToken } from './auth'
88
91
 
89
92
  const { messages, sendMessage } = useChat({
90
93
  connection: fetchServerSentEvents(
@@ -95,7 +98,7 @@ const { messages, sendMessage } = useChat({
95
98
  },
96
99
  body: {
97
100
  provider: 'openai',
98
- model: 'gpt-4o',
101
+ model: 'gpt-5.5',
99
102
  },
100
103
  }),
101
104
  ),
@@ -110,6 +113,10 @@ The `body` field in options is merged into the POST request body alongside
110
113
  ```typescript
111
114
  import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
112
115
 
116
+ // Same signature as globalThis.fetch — wrap it however you need.
117
+ const myCustomFetch: typeof fetch = (input, init) =>
118
+ fetch(input, { ...init, credentials: 'include' })
119
+
113
120
  const { messages, sendMessage } = useChat({
114
121
  connection: fetchServerSentEvents('/api/chat', {
115
122
  fetchClient: myCustomFetch,
@@ -124,6 +131,7 @@ instead of SSE. Each line is one JSON-encoded `StreamChunk` followed by `\n`.
124
131
 
125
132
  ```typescript
126
133
  import { useChat, fetchHttpStream } from '@tanstack/ai-react'
134
+ import { token } from './auth'
127
135
 
128
136
  const { messages, sendMessage } = useChat({
129
137
  connection: fetchHttpStream('https://my-api.com/chat', {
@@ -143,6 +151,7 @@ JSON object per line.
143
151
 
144
152
  ```typescript
145
153
  import { useChat, fetchHttpStream } from '@tanstack/ai-react'
154
+ import { region, refreshToken } from './auth'
146
155
 
147
156
  const { messages, sendMessage } = useChat({
148
157
  connection: fetchHttpStream(
@@ -168,15 +177,11 @@ This is the simpler model and covers most HTTP-based protocols.
168
177
 
169
178
  ```typescript
170
179
  import { useChat } from '@tanstack/ai-react'
171
- import type { ConnectionAdapter } from '@tanstack/ai-react'
172
- import type { StreamChunk, UIMessage } from '@tanstack/ai'
173
-
174
- const websocketAdapter: ConnectionAdapter = {
175
- async *connect(
176
- messages: Array<UIMessage>,
177
- data?: Record<string, any>,
178
- abortSignal?: AbortSignal,
179
- ): AsyncGenerator<StreamChunk> {
180
+ import type { ConnectConnectionAdapter } from '@tanstack/ai-react'
181
+ import type { StreamChunk } from '@tanstack/ai'
182
+
183
+ const websocketAdapter: ConnectConnectionAdapter = {
184
+ async *connect(messages, data, abortSignal) {
180
185
  const ws = new WebSocket('wss://my-api.com/chat')
181
186
 
182
187
  // Wait for connection
@@ -243,25 +248,51 @@ returns an `AsyncIterable<StreamChunk>` that stays open, and `send` dispatches
243
248
  messages through it.
244
249
 
245
250
  ```typescript
246
- import type { StreamChunk, UIMessage } from '@tanstack/ai'
247
-
248
- // SubscribeConnectionAdapter is exported from @tanstack/ai-client
249
- // (not re-exported by framework packages -- use ConnectionAdapter
250
- // union type from @tanstack/ai-react for typing)
251
- const pushAdapter = {
252
- subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {
253
- // Return a long-lived async iterable that yields chunks
254
- // whenever the server pushes them
255
- return createPersistentStream(abortSignal)
251
+ import { useChat } from '@tanstack/ai-react'
252
+ import type { SubscribeConnectionAdapter } from '@tanstack/ai-react'
253
+ import type { StreamChunk } from '@tanstack/ai'
254
+
255
+ // One socket for the lifetime of the client; every run's chunks arrive on it.
256
+ const ws = new WebSocket('wss://my-api.com/chat')
257
+ const ready = new Promise<void>((resolve) => {
258
+ ws.addEventListener('open', () => resolve(), { once: true })
259
+ })
260
+
261
+ const pushAdapter: SubscribeConnectionAdapter = {
262
+ async *subscribe(abortSignal) {
263
+ // Long-lived async iterable: yields chunks whenever the server pushes
264
+ // them, until the socket closes or the signal aborts
265
+ const queue: Array<StreamChunk> = []
266
+ let wake: (() => void) | null = null
267
+ let closed = false
268
+
269
+ ws.addEventListener('message', (event) => {
270
+ const chunk: StreamChunk = JSON.parse(event.data)
271
+ queue.push(chunk)
272
+ wake?.()
273
+ })
274
+ ws.addEventListener('close', () => {
275
+ closed = true
276
+ wake?.()
277
+ })
278
+ abortSignal?.addEventListener('abort', () => ws.close())
279
+
280
+ while (!closed || queue.length > 0) {
281
+ const next = queue.shift()
282
+ if (next !== undefined) {
283
+ yield next
284
+ continue
285
+ }
286
+ await new Promise<void>((r) => {
287
+ wake = r
288
+ })
289
+ }
256
290
  },
257
291
 
258
- async send(
259
- messages: Array<UIMessage>,
260
- data?: Record<string, any>,
261
- abortSignal?: AbortSignal,
262
- ): Promise<void> {
292
+ async send(messages, data) {
263
293
  // Dispatch messages; chunks arrive through subscribe()
264
- await persistentConnection.send(JSON.stringify({ messages, ...data }))
294
+ await ready
295
+ ws.send(JSON.stringify({ messages, ...data }))
265
296
  },
266
297
  }
267
298
 
@@ -279,12 +310,9 @@ a shorthand for creating a `ConnectConnectionAdapter` from an async generator:
279
310
 
280
311
  ```typescript
281
312
  import { useChat, stream } from '@tanstack/ai-react'
282
- import type { StreamChunk, UIMessage } from '@tanstack/ai'
313
+ import type { StreamChunk } from '@tanstack/ai'
283
314
 
284
- const directAdapter = stream(async function* (
285
- messages: Array<UIMessage>,
286
- data?: Record<string, any>,
287
- ): AsyncGenerator<StreamChunk> {
315
+ const directAdapter = stream(async function* (messages, data) {
288
316
  const response = await fetch('https://my-api.com/chat', {
289
317
  method: 'POST',
290
318
  headers: { 'Content-Type': 'application/json' },
@@ -305,7 +333,8 @@ const directAdapter = stream(async function* (
305
333
 
306
334
  for (const line of lines) {
307
335
  if (line.trim()) {
308
- yield JSON.parse(line) as StreamChunk
336
+ const chunk: StreamChunk = JSON.parse(line)
337
+ yield chunk
309
338
  }
310
339
  }
311
340
  }
@@ -324,35 +353,43 @@ The `ConnectionAdapter` interface has two mutually exclusive modes. Providing
324
353
  both throws at runtime.
325
354
 
326
355
  ```typescript
327
- // WRONG -- throws "Connection adapter must provide either connect or both
328
- // subscribe and send, not both modes"
329
- const adapter = {
356
+ import type {
357
+ ConnectConnectionAdapter,
358
+ ConnectionAdapter,
359
+ SubscribeConnectionAdapter,
360
+ } from '@tanstack/ai-react'
361
+ import { channel } from './channel'
362
+
363
+ // WRONG -- type-checks (ConnectionAdapter is a union) but throws at runtime:
364
+ // "Connection adapter must provide either connect or both subscribe and
365
+ // send, not both modes"
366
+ const adapter: ConnectionAdapter = {
330
367
  async *connect(messages) {
331
368
  /* ... */
332
369
  },
333
370
  subscribe(signal) {
334
- /* ... */
371
+ return channel.chunks(signal)
335
372
  },
336
373
  async send(messages) {
337
- /* ... */
374
+ await channel.send(messages)
338
375
  },
339
376
  }
340
377
 
341
378
  // CORRECT -- pick one mode
342
379
  // Option A: ConnectConnectionAdapter (pull-based)
343
- const pullAdapter = {
380
+ const pullAdapter: ConnectConnectionAdapter = {
344
381
  async *connect(messages, data, abortSignal) {
345
382
  // ... yield StreamChunks
346
383
  },
347
384
  }
348
385
 
349
386
  // Option B: SubscribeConnectionAdapter (push-based)
350
- const pushAdapter = {
387
+ const pushAdapter: SubscribeConnectionAdapter = {
351
388
  subscribe(abortSignal) {
352
- return longLivedAsyncIterable
389
+ return channel.chunks(abortSignal)
353
390
  },
354
391
  async send(messages, data, abortSignal) {
355
- await connection.dispatch({ messages, ...data })
392
+ await channel.send({ messages, ...data }, abortSignal)
356
393
  },
357
394
  }
358
395
  ```
@@ -388,15 +425,11 @@ streaming, implement retry logic in your connection adapter:
388
425
 
389
426
  ```typescript
390
427
  import { useChat } from '@tanstack/ai-react'
391
- import type { ConnectionAdapter } from '@tanstack/ai-react'
392
- import type { StreamChunk, UIMessage } from '@tanstack/ai'
393
-
394
- const resilientAdapter: ConnectionAdapter = {
395
- async *connect(
396
- messages: Array<UIMessage>,
397
- data?: Record<string, any>,
398
- abortSignal?: AbortSignal,
399
- ): AsyncGenerator<StreamChunk> {
428
+ import type { ConnectConnectionAdapter } from '@tanstack/ai-react'
429
+ import type { StreamChunk } from '@tanstack/ai'
430
+
431
+ const resilientAdapter: ConnectConnectionAdapter = {
432
+ async *connect(messages, data, abortSignal) {
400
433
  const maxRetries = 3
401
434
  let attempt = 0
402
435
 
@@ -427,7 +460,8 @@ const resilientAdapter: ConnectionAdapter = {
427
460
 
428
461
  for (const line of lines) {
429
462
  if (line.trim()) {
430
- yield JSON.parse(line) as StreamChunk
463
+ const chunk: StreamChunk = JSON.parse(line)
464
+ yield chunk
431
465
  }
432
466
  }
433
467
  }
@@ -30,8 +30,10 @@ printed, or pipe logs into a custom logger (pino, winston, etc.). The same
30
30
  import { chat } from '@tanstack/ai'
31
31
  import { openaiText } from '@tanstack/ai-openai'
32
32
 
33
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
34
+
33
35
  const stream = chat({
34
- adapter: openaiText('gpt-5.2'),
36
+ adapter: openaiText('gpt-5.5'),
35
37
  messages,
36
38
  debug: true, // all categories on, prints to console
37
39
  })
@@ -49,8 +51,13 @@ Each log line is prefixed with an emoji and `[tanstack-ai:<category>]`:
49
51
  ## Turn it off
50
52
 
51
53
  ```typescript
54
+ import { chat } from '@tanstack/ai'
55
+ import { openaiText } from '@tanstack/ai-openai'
56
+
57
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
58
+
52
59
  chat({
53
- adapter: openaiText('gpt-5.2'),
60
+ adapter: openaiText('gpt-5.5'),
54
61
  messages,
55
62
  debug: false, // silence everything, including errors
56
63
  })
@@ -63,6 +70,9 @@ Omitting `debug` is **not** the same as `debug: false`. When omitted, the
63
70
  ## `DebugOption` — the accepted shapes
64
71
 
65
72
  ```typescript
73
+ import type { Logger } from '@tanstack/ai'
74
+
75
+ // As exported by '@tanstack/ai'
66
76
  type DebugOption = boolean | DebugConfig
67
77
 
68
78
  interface DebugConfig {
@@ -95,8 +105,13 @@ Pass a `DebugConfig` object. Unspecified categories default to `true`, so it's
95
105
  easiest to toggle by setting specific flags to `false`:
96
106
 
97
107
  ```typescript
108
+ import { chat } from '@tanstack/ai'
109
+ import { openaiText } from '@tanstack/ai-openai'
110
+
111
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
112
+
98
113
  chat({
99
- adapter: openaiText('gpt-5.2'),
114
+ adapter: openaiText('gpt-5.5'),
100
115
  messages,
101
116
  debug: { middleware: false }, // everything except middleware
102
117
  })
@@ -105,8 +120,13 @@ chat({
105
120
  To print only a specific set, set the rest to `false` explicitly:
106
121
 
107
122
  ```typescript
123
+ import { chat } from '@tanstack/ai'
124
+ import { openaiText } from '@tanstack/ai-openai'
125
+
126
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
127
+
108
128
  chat({
109
- adapter: openaiText('gpt-5.2'),
129
+ adapter: openaiText('gpt-5.5'),
110
130
  messages,
111
131
  debug: {
112
132
  provider: true,
@@ -124,7 +144,8 @@ chat({
124
144
  ## Pipe into your own logger
125
145
 
126
146
  ```typescript
127
- import type { Logger } from '@tanstack/ai'
147
+ import { chat, type Logger } from '@tanstack/ai'
148
+ import { openaiText } from '@tanstack/ai-openai'
128
149
  import pino from 'pino'
129
150
 
130
151
  const pinoLogger = pino()
@@ -135,8 +156,10 @@ const logger: Logger = {
135
156
  error: (msg, meta) => pinoLogger.error(meta, msg),
136
157
  }
137
158
 
159
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
160
+
138
161
  chat({
139
- adapter: openaiText('gpt-5.2'),
162
+ adapter: openaiText('gpt-5.5'),
140
163
  messages,
141
164
  debug: { logger }, // all categories on, piped to pino
142
165
  })
@@ -170,11 +193,48 @@ concepts don't exist in their pipelines.
170
193
  Same `debug` option everywhere:
171
194
 
172
195
  ```typescript
173
- summarize({ adapter, text, debug: true })
174
- generateImage({ adapter, prompt: 'a cat', debug: { logger } })
175
- generateSpeech({ adapter, text, debug: { request: true } })
176
- generateTranscription({ adapter, audio, debug: false })
177
- generateVideo({ adapter, prompt: 'a wave', debug: { output: true } })
196
+ import {
197
+ summarize,
198
+ generateImage,
199
+ generateSpeech,
200
+ generateTranscription,
201
+ generateVideo,
202
+ } from '@tanstack/ai'
203
+ import {
204
+ openaiSummarize,
205
+ openaiImage,
206
+ openaiSpeech,
207
+ openaiTranscription,
208
+ openaiVideo,
209
+ } from '@tanstack/ai-openai'
210
+ import { logger } from './logger'
211
+ import { audio } from './recording'
212
+
213
+ summarize({
214
+ adapter: openaiSummarize('gpt-5.5'),
215
+ text: 'Long article…',
216
+ debug: true,
217
+ })
218
+ generateImage({
219
+ adapter: openaiImage('gpt-image-2'),
220
+ prompt: 'a cat',
221
+ debug: { logger },
222
+ })
223
+ generateSpeech({
224
+ adapter: openaiSpeech('tts-1-hd'),
225
+ text: 'Hello',
226
+ debug: { request: true },
227
+ })
228
+ generateTranscription({
229
+ adapter: openaiTranscription('gpt-4o-transcribe'),
230
+ audio,
231
+ debug: false,
232
+ })
233
+ generateVideo({
234
+ adapter: openaiVideo('sora-2'),
235
+ prompt: 'a wave',
236
+ debug: { output: true },
237
+ })
178
238
  ```
179
239
 
180
240
  Realtime session adapters in provider packages (e.g. `openaiRealtime`,
@@ -187,6 +247,12 @@ categories don't apply.
187
247
  ### a. HIGH: Treating omitted `debug` as silent
188
248
 
189
249
  ```typescript
250
+ import { chat } from '@tanstack/ai'
251
+ import { openaiText } from '@tanstack/ai-openai'
252
+
253
+ const adapter = openaiText('gpt-5.5')
254
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
255
+
190
256
  // WRONG — expecting this to be completely silent
191
257
  chat({ adapter, messages })
192
258
  // Errors still print via [tanstack-ai:errors] ... on failure.
@@ -203,6 +269,12 @@ Source: docs/advanced/debug-logging.md
203
269
  ### b. MEDIUM: Reaching for middleware when `debug` would do
204
270
 
205
271
  ```typescript
272
+ import { chat, type ChatMiddleware } from '@tanstack/ai'
273
+ import { openaiText } from '@tanstack/ai-openai'
274
+
275
+ const adapter = openaiText('gpt-5.5')
276
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
277
+
206
278
  // WRONG — writing logging middleware to see chunks flow
207
279
  const chunkLogger: ChatMiddleware = {
208
280
  name: 'chunk-logger',
@@ -234,22 +306,32 @@ prefer implementations that don't throw — silenced exceptions are harder to
234
306
  debug than loud ones.
235
307
 
236
308
  ```typescript
309
+ import type { Logger } from '@tanstack/ai'
310
+
237
311
  // WRONG — a logger that can throw on serialization
238
312
  const fragile: Logger = {
239
313
  debug: (msg, meta) => console.debug(msg, JSON.stringify(meta)), // cyclic meta → throws
240
- /* ... */
314
+ info: (msg, meta) => console.info(msg, JSON.stringify(meta)),
315
+ warn: (msg, meta) => console.warn(msg, JSON.stringify(meta)),
316
+ error: (msg, meta) => console.error(msg, JSON.stringify(meta)),
241
317
  }
242
318
 
243
319
  // CORRECT — guard serialization in the logger itself
244
- const safe: Logger = {
245
- debug: (msg, meta) => {
320
+ const guarded =
321
+ (log: (...args: Array<unknown>) => void): Logger['debug'] =>
322
+ (msg, meta) => {
246
323
  try {
247
- console.debug(msg, meta)
324
+ log(msg, JSON.stringify(meta))
248
325
  } catch {
249
- console.debug(msg)
326
+ log(msg) // fall back to the bare message rather than throw
250
327
  }
251
- },
252
- /* ... */
328
+ }
329
+
330
+ const safe: Logger = {
331
+ debug: guarded(console.debug),
332
+ info: guarded(console.info),
333
+ warn: guarded(console.warn),
334
+ error: guarded(console.error),
253
335
  }
254
336
  ```
255
337
 
@@ -35,20 +35,40 @@ per-thread (or other) lock yourself when multi-writer races matter.
35
35
  ## Wire locks
36
36
 
37
37
  ```ts
38
+ import { chat } from '@tanstack/ai'
38
39
  import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
40
+ import { openaiText } from '@tanstack/ai-openai'
39
41
 
40
- middleware: [
41
- withLocks(new InMemoryLockStore()), // single process
42
- ]
42
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
43
+
44
+ chat({
45
+ adapter: openaiText('gpt-5.6'),
46
+ messages,
47
+ middleware: [
48
+ withLocks(new InMemoryLockStore()), // single process
49
+ ],
50
+ })
43
51
  ```
44
52
 
45
53
  Alongside persistence — optional, locks do not require it:
46
54
 
47
55
  ```ts
56
+ import { chat } from '@tanstack/ai'
48
57
  import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
49
- import { withPersistence } from '@tanstack/ai-persistence'
50
-
51
- middleware: [withPersistence(persistence), withLocks(new InMemoryLockStore())]
58
+ import { openaiText } from '@tanstack/ai-openai'
59
+ import { memoryPersistence, withPersistence } from '@tanstack/ai-persistence'
60
+
61
+ const persistence = memoryPersistence()
62
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
63
+
64
+ chat({
65
+ adapter: openaiText('gpt-5.6'),
66
+ messages,
67
+ middleware: [
68
+ withPersistence(persistence),
69
+ withLocks(new InMemoryLockStore()),
70
+ ],
71
+ })
52
72
  ```
53
73
 
54
74
  `withLocks` provides `LocksCapability` for downstream middleware (e.g.
@@ -73,7 +93,9 @@ annotation), then hand it to `withLocks`. Acquire the key, run `fn`, release whe
73
93
  `fn` settles:
74
94
 
75
95
  ```ts
96
+ import { chat } from '@tanstack/ai'
76
97
  import { defineLock, withLocks } from '@tanstack/ai/locks'
98
+ import { openaiText } from '@tanstack/ai-openai'
77
99
  import { acquire } from './my-lock-backend'
78
100
 
79
101
  const locks = defineLock({
@@ -87,7 +109,13 @@ const locks = defineLock({
87
109
  },
88
110
  })
89
111
 
90
- middleware: [withLocks(locks)]
112
+ const messages = [{ role: 'user' as const, content: 'Hello' }]
113
+
114
+ chat({
115
+ adapter: openaiText('gpt-5.6'),
116
+ messages,
117
+ middleware: [withLocks(locks)],
118
+ })
91
119
  ```
92
120
 
93
121
  ## Lease semantics