@tanstack/ai 0.51.0 → 0.52.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.
@@ -67,6 +67,9 @@ export {
67
67
  } from './locks'
68
68
  export type { LockStore } from './locks'
69
69
 
70
+ export { MetadataCapability, getMetadata, provideMetadata } from './metadata'
71
+ export type { MetadataStore } from './metadata'
72
+
70
73
  export {
71
74
  isRunStatus,
72
75
  isTerminalRunStatus,
@@ -0,0 +1,20 @@
1
+ import { createCapability } from './capabilities'
2
+
3
+ /**
4
+ * Namespaced key/value store for app and middleware metadata.
5
+ *
6
+ * `(namespace, key)` is the composite identity. Keep both values separate;
7
+ * joining them with a delimiter can create collisions.
8
+ */
9
+ export interface MetadataStore {
10
+ /** Return the value for `(namespace, key)`, or `null` when it is absent. */
11
+ get: (namespace: string, key: string) => Promise<unknown | null>
12
+ /** Insert or replace the value for `(namespace, key)`. */
13
+ set: (namespace: string, key: string, value: unknown) => Promise<void>
14
+ /** Delete `(namespace, key)`. Do nothing when it is absent. */
15
+ delete: (namespace: string, key: string) => Promise<void>
16
+ }
17
+
18
+ export const MetadataCapability = createCapability<MetadataStore>()('metadata')
19
+
20
+ export const [getMetadata, provideMetadata] = MetadataCapability
@@ -213,6 +213,12 @@ export interface ChatMiddlewareContext<TContext = unknown> {
213
213
  signal?: AbortSignal
214
214
  /** Abort the chat run with a reason */
215
215
  abort: (reason?: string) => void
216
+ /**
217
+ * Push a `CUSTOM` chunk onto the chat stream immediately.
218
+ * The engine yields it as soon as it can (including while `onConfig`
219
+ * is still awaiting work such as a summarize call).
220
+ */
221
+ emitCustomEvent: (name: string, value: Record<string, any>) => void
216
222
  /** Runtime context provided by chat() options */
217
223
  context: TContext
218
224
  /**
@@ -305,7 +311,10 @@ export interface ChatMiddlewareContext<TContext = unknown> {
305
311
  * that middleware is allowed to modify.
306
312
  */
307
313
  export interface ChatMiddlewareConfig {
314
+ /** Canonical conversation history. Middleware and persistence read this. */
308
315
  messages: Array<ModelMessage>
316
+ /** Provider-only context. Defaults to `messages` when it is not set. */
317
+ providerMessages?: Array<ModelMessage> | undefined
309
318
  systemPrompts: Array<SystemPrompt>
310
319
  tools: Array<Tool>
311
320
  resume?: Array<RunAgentResumeItem> | undefined
@@ -1357,6 +1357,12 @@ export class StreamProcessor {
1357
1357
  state.hasToolCallsSinceTextStart = false
1358
1358
  }
1359
1359
 
1360
+ // A segment begins at its first delta, not its second. Left set, the flag
1361
+ // makes the second delta look like a further segment: the accumulation
1362
+ // resets and updateTextPart writes over the part the first delta created.
1363
+ // A later tool call sets it again in handleToolCallStartEvent.
1364
+ state.hasToolCallsSinceTextStart = false
1365
+
1360
1366
  const currentText = state.currentSegmentText
1361
1367
  const delta = chunk.delta || ''
1362
1368
  const nextText = delta !== '' ? currentText + delta : currentText
@@ -99,6 +99,26 @@ function pruneMap(map: NullWideningMap): NullWideningMap | undefined {
99
99
  return Object.keys(map).length > 0 ? map : undefined
100
100
  }
101
101
 
102
+ function coerceArrayItems(items: JSONSchema | Array<JSONSchema>): {
103
+ schema: JSONSchema | Array<JSONSchema>
104
+ itemMap: NullWideningMap | Array<NullWideningMap> | undefined
105
+ } {
106
+ if (Array.isArray(items)) {
107
+ const nested = items.map((item) =>
108
+ makeStructuredOutputCompatible(item, item.required || []),
109
+ )
110
+ const itemMaps = nested.map((entry) => entry.nullWidening ?? {})
111
+ return {
112
+ schema: nested.map((entry) => entry.schema),
113
+ itemMap: itemMaps.some((entry) => Object.keys(entry).length > 0)
114
+ ? itemMaps
115
+ : undefined,
116
+ }
117
+ }
118
+ const nested = makeStructuredOutputCompatible(items, items.required || [])
119
+ return { schema: nested.schema, itemMap: nested.nullWidening }
120
+ }
121
+
102
122
  /**
103
123
  * Transform a JSON schema to be compatible with OpenAI's structured output requirements.
104
124
  * OpenAI requires:
@@ -146,18 +166,15 @@ function makeStructuredOutputCompatible(
146
166
  widenedHere = wasOptional
147
167
  childMap = nested.nullWidening
148
168
  } else if (prop.type === 'array' && prop.items) {
149
- const items = Array.isArray(prop.items) ? prop.items[0] : prop.items
150
- const nestedItems = items
151
- ? makeStructuredOutputCompatible(items, items.required || [])
152
- : undefined
169
+ const nestedItems = coerceArrayItems(prop.items)
153
170
  properties[propName] = {
154
171
  ...prop,
155
- items: nestedItems ? nestedItems.schema : prop.items,
172
+ items: nestedItems.schema,
156
173
  ...(wasOptional ? { type: ['array', 'null'] } : {}),
157
174
  }
158
175
  widenedHere = wasOptional
159
- childMap = nestedItems?.nullWidening
160
- ? { items: nestedItems.nullWidening }
176
+ childMap = nestedItems.itemMap
177
+ ? { items: nestedItems.itemMap }
161
178
  : undefined
162
179
  } else if (wasOptional) {
163
180
  // Make optional fields nullable by adding null to the type. Mark
@@ -188,17 +205,13 @@ function makeStructuredOutputCompatible(
188
205
  if (Object.keys(propertyMaps).length > 0) map.properties = propertyMaps
189
206
  }
190
207
 
191
- // Handle array types with object items
208
+ // Handle array item schemas. A tuple (`items: [a, b, …]`) keeps every
209
+ // position. A homogeneous schema stays a single items map so
210
+ // `undoNullWidening` applies it to every element.
192
211
  if (result.type === 'array' && result.items) {
193
- const items = Array.isArray(result.items) ? result.items[0] : result.items
194
- if (items) {
195
- const nestedItems = makeStructuredOutputCompatible(
196
- items,
197
- items.required || [],
198
- )
199
- result.items = nestedItems.schema
200
- if (nestedItems.nullWidening) map.items = nestedItems.nullWidening
201
- }
212
+ const nestedItems = coerceArrayItems(result.items)
213
+ result.items = nestedItems.schema
214
+ if (nestedItems.itemMap) map.items = nestedItems.itemMap
202
215
  }
203
216
 
204
217
  return { schema: result, nullWidening: pruneMap(map) }
package/src/index.ts CHANGED
@@ -283,6 +283,9 @@ export {
283
283
  createCapability,
284
284
  defineChatMiddleware,
285
285
  createChatMiddleware,
286
+ MetadataCapability,
287
+ getMetadata,
288
+ provideMetadata,
286
289
  } from './activities/chat/middleware/index'
287
290
  export type {
288
291
  Capability,
@@ -292,6 +295,7 @@ export type {
292
295
  CapabilityProvider,
293
296
  DefinedChatMiddleware,
294
297
  AnyChatMiddleware,
298
+ MetadataStore,
295
299
  } from './activities/chat/middleware/index'
296
300
  // Locks are a distributed-mutex primitive — coordination, not chat state — and
297
301
  // live behind their own subpath: `@tanstack/ai/locks` (see ./locks.ts).