@tanstack/ai 0.20.1 → 0.21.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.
Files changed (40) hide show
  1. package/dist/esm/activities/chat/index.d.ts +1 -1
  2. package/dist/esm/activities/chat/index.js +357 -258
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.js.map +1 -1
  5. package/dist/esm/activities/chat/middleware/compose.d.ts +10 -1
  6. package/dist/esm/activities/chat/middleware/compose.js +55 -0
  7. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  8. package/dist/esm/activities/chat/middleware/index.d.ts +1 -1
  9. package/dist/esm/activities/chat/middleware/types.d.ts +35 -3
  10. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  11. package/dist/esm/activities/chat/tools/schema-converter.d.ts +12 -1
  12. package/dist/esm/activities/chat/tools/schema-converter.js +13 -4
  13. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  14. package/dist/esm/activities/generateImage/index.js.map +1 -1
  15. package/dist/esm/extend-adapter.js.map +1 -1
  16. package/dist/esm/index.d.ts +2 -2
  17. package/dist/esm/index.js +2 -1
  18. package/dist/esm/middlewares/content-guard.js.map +1 -1
  19. package/dist/esm/middlewares/otel.js +1 -4
  20. package/dist/esm/middlewares/otel.js.map +1 -1
  21. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  22. package/dist/esm/utilities/ag-ui-wire.js +4 -1
  23. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  24. package/package.json +3 -3
  25. package/skills/ai-core/middleware/SKILL.md +124 -18
  26. package/skills/ai-core/structured-outputs/SKILL.md +13 -0
  27. package/src/activities/chat/index.ts +653 -370
  28. package/src/activities/chat/messages.ts +1 -1
  29. package/src/activities/chat/middleware/compose.ts +65 -5
  30. package/src/activities/chat/middleware/index.ts +1 -0
  31. package/src/activities/chat/middleware/types.ts +53 -2
  32. package/src/activities/chat/stream/processor.ts +2 -2
  33. package/src/activities/chat/tools/schema-converter.ts +22 -7
  34. package/src/activities/generateImage/index.ts +3 -3
  35. package/src/extend-adapter.ts +1 -1
  36. package/src/index.ts +5 -1
  37. package/src/middlewares/content-guard.ts +1 -1
  38. package/src/middlewares/otel.ts +7 -10
  39. package/src/strip-to-spec-middleware.ts +1 -4
  40. package/src/utilities/ag-ui-wire.ts +2 -1
@@ -31,11 +31,11 @@ const stream = chat({
31
31
  onStart: (ctx) => {
32
32
  console.log('Chat started:', ctx.model)
33
33
  },
34
- onFinish: (ctx) => {
35
- trackAnalytics({ model: ctx.model, tokens: ctx.usage })
34
+ onFinish: (ctx, info) => {
35
+ trackAnalytics({ model: ctx.model, tokens: info.usage?.totalTokens })
36
36
  },
37
- onError: (ctx) => {
38
- reportError(ctx.error)
37
+ onError: (ctx, info) => {
38
+ reportError(info.error)
39
39
  },
40
40
  },
41
41
  ],
@@ -50,23 +50,82 @@ Every hook receives a `ChatMiddlewareContext` as its first argument, which provi
50
50
  `requestId`, `streamId`, `phase`, `iteration`, `chunkIndex`, `model`, `provider`,
51
51
  `signal`, `abort()`, `defer()`, and more.
52
52
 
53
- | Hook | When | Second Argument |
54
- | --------------------- | ------------------------------------------------------------- | ------------------------------------------------ |
55
- | `onConfig` | Once at startup (`init`) + once per iteration (`beforeModel`) | `ChatMiddlewareConfig` (return partial to merge) |
56
- | `onStart` | Once after initial `onConfig` | none |
57
- | `onIteration` | Start of each agent loop iteration | `IterationInfo` |
58
- | `onChunk` | Every streamed chunk | `StreamChunk` (return void/chunk/chunk[]/null) |
59
- | `onBeforeToolCall` | Before each tool executes | `ToolCallHookContext` (return decision or void) |
60
- | `onAfterToolCall` | After each tool executes | `AfterToolCallInfo` |
61
- | `onToolPhaseComplete` | After all tool calls in an iteration | `ToolPhaseCompleteInfo` |
62
- | `onUsage` | When `RUN_FINISHED` includes usage data | `UsageInfo` |
63
- | `onFinish` | Run completed normally | `FinishInfo` |
64
- | `onAbort` | Run was aborted | `AbortInfo` |
65
- | `onError` | Unhandled error occurred | `ErrorInfo` |
53
+ | Hook | When | Second Argument |
54
+ | -------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
55
+ | `onConfig` | Once at startup (`init`) + once per iteration (`beforeModel`) + once at structured-output boundary | `ChatMiddlewareConfig` (return partial to merge) |
56
+ | `onStructuredOutputConfig` | Once at the structured-output boundary (only when `chat({ outputSchema })`) | `StructuredOutputMiddlewareConfig` (return partial) |
57
+ | `onStart` | Once after initial `onConfig` | none |
58
+ | `onIteration` | Start of each agent loop iteration | `IterationInfo` |
59
+ | `onChunk` | Every streamed chunk | `StreamChunk` (return void/chunk/chunk[]/null) |
60
+ | `onBeforeToolCall` | Before each tool executes | `ToolCallHookContext` (return decision or void) |
61
+ | `onAfterToolCall` | After each tool executes | `AfterToolCallInfo` |
62
+ | `onToolPhaseComplete` | After all tool calls in an iteration | `ToolPhaseCompleteInfo` |
63
+ | `onUsage` | When `RUN_FINISHED` includes usage data | `UsageInfo` |
64
+ | `onFinish` | Run completed normally | `FinishInfo` |
65
+ | `onAbort` | Run was aborted | `AbortInfo` |
66
+ | `onError` | Unhandled error occurred | `ErrorInfo` |
66
67
 
67
68
  Terminal hooks (`onFinish`, `onAbort`, `onError`) are **mutually exclusive** -- exactly
68
69
  one fires per `chat()` invocation.
69
70
 
71
+ ### Phase values
72
+
73
+ `ctx.phase` is one of:
74
+
75
+ | Phase | When |
76
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
77
+ | `'init'` | Initial setup (before the first `onConfig` snapshot is built). |
78
+ | `'beforeModel'` | Right before each agent-loop adapter call (`onConfig` re-fires here). |
79
+ | `'modelStream'` | During model streaming chunks within the agent loop. |
80
+ | `'beforeTools'` | Before tool execution phase. |
81
+ | `'afterTools'` | After tool execution phase. |
82
+ | `'structuredOutput'` | During the final structured-output adapter call (set for all chunks from `adapter.structuredOutputStream` or the synthesized fallback). Triggered only when `chat({ outputSchema })` is invoked; one phase transition per `chat()` invocation. |
83
+
84
+ **Structured-output lifecycle rules** (when `chat({ outputSchema })` is used):
85
+
86
+ - `onStructuredOutputConfig` fires **before** `onConfig` at the structured-output boundary.
87
+ - `onConfig` re-fires at the same boundary with `ctx.phase === 'structuredOutput'`, receiving the post-`onStructuredOutputConfig` view of the config (minus `outputSchema`).
88
+ - `onChunk` and `onUsage` fire for every chunk and usage event emitted by the structured-output call, with `ctx.phase === 'structuredOutput'`.
89
+ - `onIteration` does **not** fire for finalization — it is agent-loop-only.
90
+ - `onFinish` fires once at the end of the whole `chat()` invocation, **after** the structured-output finalization completes (not after the agent loop). Terminal-hook exclusivity still holds (one of `onFinish` / `onAbort` / `onError`).
91
+ - **Terminal `info` and structured-output:** `info.usage` / `info.finishReason` / `info.content` reflect the **agent loop's** terminal state, NOT the finalization step. Finalization state is intentionally segregated to keep agent-loop semantics clean. For a tools-less `chat({ outputSchema })` run, `info.usage` is `undefined` and `info.finishReason` is `null` (no agent-loop iteration produced `RUN_FINISHED`). To capture finalization tokens, use `onUsage` — it fires for both agent-loop iterations and the final call. For the structured-output result itself, observe the `structured-output.complete` CUSTOM event in `onChunk`.
92
+
93
+ ## onStructuredOutputConfig
94
+
95
+ A dedicated config hook that fires **only** at the structured-output boundary
96
+ (when `chat({ outputSchema })` is invoked). Use it to transform the JSON Schema
97
+ sent to the provider (inject `$defs`, strip vendor-incompatible keywords) or to
98
+ apply structured-output-specific config changes that should not affect the
99
+ agent-loop adapter calls.
100
+
101
+ **Signature:**
102
+
103
+ ```ts
104
+ onStructuredOutputConfig?: (
105
+ ctx: ChatMiddlewareContext,
106
+ config: StructuredOutputMiddlewareConfig,
107
+ ) =>
108
+ | void
109
+ | null
110
+ | Partial<StructuredOutputMiddlewareConfig>
111
+ | Promise<void | Partial<StructuredOutputMiddlewareConfig>>
112
+ ```
113
+
114
+ **`StructuredOutputMiddlewareConfig` shape:**
115
+
116
+ ```ts
117
+ interface StructuredOutputMiddlewareConfig extends ChatMiddlewareConfig {
118
+ outputSchema: JSONSchema // The JSON Schema being sent to the provider
119
+ }
120
+ ```
121
+
122
+ **Ordering rule:**
123
+
124
+ - `onStructuredOutputConfig` fires **before** `onConfig` at the structured-output boundary.
125
+ - `onConfig` re-fires at the same boundary with `ctx.phase === 'structuredOutput'`, receiving the post-`onStructuredOutputConfig` view of the config (minus `outputSchema`).
126
+ - Use `onConfig` for general-purpose transforms that apply to every adapter call (agent-loop iterations and the final structured-output call).
127
+ - Use `onStructuredOutputConfig` when you need to transform the JSON Schema or apply structured-output-specific behavior.
128
+
70
129
  ## Core Patterns
71
130
 
72
131
  ### Pattern 1: Analytics and Logging Middleware
@@ -173,7 +232,52 @@ const toolGuard: ChatMiddleware = {
173
232
  | `{ type: 'skip', result }` | Skip execution, use provided result (used by `toolCacheMiddleware`) |
174
233
  | `{ type: 'abort', reason? }` | Abort the entire chat run |
175
234
 
176
- ### Pattern 3: Multiple Middleware Composition
235
+ ### Pattern 3: Structured-Output Middleware
236
+
237
+ When `chat({ outputSchema })` is used, the final structured-output adapter call
238
+ now flows through the same middleware chain as the agent loop (with
239
+ `ctx.phase === 'structuredOutput'`). Before this change, the final call bypassed
240
+ middleware entirely — `onChunk`, `onUsage`, `onConfig`, and terminal hooks did
241
+ not see it.
242
+
243
+ **Example A — Observability (tracing every chunk, including finalization):**
244
+
245
+ ```typescript
246
+ import type { ChatMiddleware } from '@tanstack/ai'
247
+
248
+ const tracing: ChatMiddleware = {
249
+ name: 'tracing',
250
+ onChunk(ctx, chunk) {
251
+ span.addEvent('chunk', { phase: ctx.phase, type: chunk.type })
252
+ },
253
+ }
254
+ ```
255
+
256
+ This middleware now observes every chunk from the final structured-output call,
257
+ attributed to `ctx.phase === 'structuredOutput'`. Before the fix, the final
258
+ adapter call bypassed middleware entirely — `tracing` would only see agent-loop
259
+ chunks.
260
+
261
+ **Example B — Schema rewriting (inject shared `$defs`):**
262
+
263
+ ```typescript
264
+ import type { ChatMiddleware } from '@tanstack/ai'
265
+
266
+ const injectDefs: ChatMiddleware = {
267
+ name: 'inject-defs',
268
+ onStructuredOutputConfig(_ctx, config) {
269
+ return {
270
+ outputSchema: { ...config.outputSchema, $defs: { ...sharedDefs } },
271
+ }
272
+ },
273
+ }
274
+ ```
275
+
276
+ `onStructuredOutputConfig` is the right hook here because it has direct access
277
+ to `config.outputSchema` and runs only on the structured-output boundary —
278
+ schema rewrites do not leak into the agent-loop adapter calls.
279
+
280
+ ### Pattern 4: Multiple Middleware Composition
177
281
 
178
282
  Middleware executes in array order (left-to-right). Ordering matters for hooks that
179
283
  pipe or short-circuit:
@@ -222,6 +326,7 @@ const stream = chat({
222
326
  | Hook | Composition | Effect of Order |
223
327
  | -------------------------- | --------------------------------------------- | ------------------------------------------ |
224
328
  | `onConfig` | **Piped** -- each receives previous output | Earlier middleware transforms first |
329
+ | `onStructuredOutputConfig` | **Piped** -- each receives previous output | Earlier middleware transforms first |
225
330
  | `onStart` | Sequential | All run in order |
226
331
  | `onChunk` | **Piped** -- chunks flow through each | If first drops a chunk, later never see it |
227
332
  | `onBeforeToolCall` | **First-win** -- first non-void decision wins | Earlier middleware has priority |
@@ -334,3 +439,4 @@ Source: docs/advanced/middleware.md
334
439
  ## Cross-References
335
440
 
336
441
  - See also: **ai-core/chat-experience/SKILL.md** -- Middleware hooks into the chat lifecycle
442
+ - See also: **ai-core/structured-outputs/SKILL.md** -- Middleware now wraps the final structured-output call; use `onStructuredOutputConfig` for JSON-Schema transforms
@@ -480,8 +480,21 @@ already installed.
480
480
 
481
481
  Source: maintainer interview
482
482
 
483
+ ## Middleware coverage
484
+
485
+ The final structured-output adapter call runs through the same middleware
486
+ pipeline as the agent loop. `onChunk` observes chunks attributed to
487
+ `ctx.phase === 'structuredOutput'`; `onUsage` fires for the final call's
488
+ tokens; `onFinish` fires once at the end of the whole `chat()` invocation,
489
+ after the structured-output result is available.
490
+
491
+ For schema-aware middleware (e.g., transforming the JSON Schema before the
492
+ provider call, stripping system prompts), use the dedicated
493
+ `onStructuredOutputConfig` hook. See [middleware skill](../middleware/SKILL.md).
494
+
483
495
  ## Cross-References
484
496
 
485
497
  - See also: **ai-core/chat-experience/SKILL.md** — Base `useChat` surface; the structured-output additions documented here layer on top.
486
498
  - See also: **ai-core/adapter-configuration/SKILL.md** — Adapter handles structured-output strategy transparently.
487
499
  - See also: **ai-core/tool-calling/SKILL.md** — Combine `tools` with `outputSchema` for an agent loop that runs tools first and returns a typed object. Tool-approval and client-tool flows compose with structured runs without extra wiring; see [docs/structured-outputs/with-tools.md](https://github.com/TanStack/ai/blob/main/docs/structured-outputs/with-tools.md).
500
+ - See also: **ai-core/middleware/SKILL.md** — `onStructuredOutputConfig` hook and the `structuredOutput` phase for observing/transforming the final structured-output call.