@tanstack/ai 0.32.0 → 0.34.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 (64) hide show
  1. package/dist/esm/activities/chat/index.js +47 -20
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/middleware/types.d.ts +7 -0
  4. package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +25 -1
  5. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +26 -2
  6. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  7. package/dist/esm/activities/chat/tools/schema-converter.d.ts +13 -0
  8. package/dist/esm/activities/chat/tools/schema-converter.js +61 -33
  9. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  10. package/dist/esm/activities/generateAudio/index.d.ts +7 -0
  11. package/dist/esm/activities/generateAudio/index.js +26 -1
  12. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  13. package/dist/esm/activities/generateImage/index.d.ts +7 -0
  14. package/dist/esm/activities/generateImage/index.js +26 -1
  15. package/dist/esm/activities/generateImage/index.js.map +1 -1
  16. package/dist/esm/activities/generateSpeech/index.d.ts +7 -0
  17. package/dist/esm/activities/generateSpeech/index.js +26 -1
  18. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  19. package/dist/esm/activities/generateTranscription/index.d.ts +7 -0
  20. package/dist/esm/activities/generateTranscription/index.js +26 -1
  21. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  22. package/dist/esm/activities/generateVideo/index.d.ts +9 -0
  23. package/dist/esm/activities/generateVideo/index.js +52 -2
  24. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  25. package/dist/esm/activities/middleware/index.d.ts +2 -0
  26. package/dist/esm/activities/middleware/run.d.ts +20 -0
  27. package/dist/esm/activities/middleware/run.js +42 -0
  28. package/dist/esm/activities/middleware/run.js.map +1 -0
  29. package/dist/esm/activities/middleware/types.d.ts +118 -0
  30. package/dist/esm/index.d.ts +2 -0
  31. package/dist/esm/index.js +2 -0
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/middlewares/otel.d.ts +8 -2
  34. package/dist/esm/middlewares/otel.js +145 -95
  35. package/dist/esm/middlewares/otel.js.map +1 -1
  36. package/dist/esm/middlewares/usage-attributes.d.ts +24 -0
  37. package/dist/esm/middlewares/usage-attributes.js +43 -0
  38. package/dist/esm/middlewares/usage-attributes.js.map +1 -0
  39. package/dist/esm/types.d.ts +7 -7
  40. package/dist/esm/utilities/errors.d.ts +13 -0
  41. package/dist/esm/utilities/errors.js +22 -0
  42. package/dist/esm/utilities/errors.js.map +1 -0
  43. package/dist/esm/utilities/numbers.d.ts +8 -0
  44. package/dist/esm/utilities/numbers.js +12 -0
  45. package/dist/esm/utilities/numbers.js.map +1 -0
  46. package/package.json +3 -2
  47. package/src/activities/chat/index.ts +125 -35
  48. package/src/activities/chat/middleware/types.ts +7 -0
  49. package/src/activities/chat/tools/lazy-tool-manager.ts +46 -4
  50. package/src/activities/chat/tools/schema-converter.ts +146 -93
  51. package/src/activities/generateAudio/index.ts +42 -1
  52. package/src/activities/generateImage/index.ts +42 -1
  53. package/src/activities/generateSpeech/index.ts +42 -1
  54. package/src/activities/generateTranscription/index.ts +42 -1
  55. package/src/activities/generateVideo/index.ts +88 -2
  56. package/src/activities/middleware/index.ts +20 -0
  57. package/src/activities/middleware/run.ts +88 -0
  58. package/src/activities/middleware/types.ts +173 -0
  59. package/src/index.ts +19 -0
  60. package/src/middlewares/otel.ts +195 -120
  61. package/src/middlewares/usage-attributes.ts +65 -0
  62. package/src/types.ts +7 -7
  63. package/src/utilities/errors.ts +29 -0
  64. package/src/utilities/numbers.ts +15 -0
@@ -10,8 +10,17 @@
10
10
  import { aiEventClient } from '@tanstack/ai-event-client'
11
11
  import { toRunErrorPayload } from '../error-payload'
12
12
  import { resolveDebugOption } from '../../logger/resolve'
13
+ import {
14
+ createGenerationContext,
15
+ runGenerationAbort,
16
+ runGenerationError,
17
+ runGenerationFinish,
18
+ runGenerationStart,
19
+ runGenerationUsage,
20
+ } from '../middleware'
13
21
  import type { InternalLogger } from '../../logger/internal-logger'
14
22
  import type { DebugOption } from '../../logger/types'
23
+ import type { GenerationMiddleware } from '../middleware'
15
24
  import type { VideoAdapter } from './adapter'
16
25
  import type {
17
26
  MediaPrompt,
@@ -160,6 +169,14 @@ export type VideoCreateOptions<
160
169
  * control and/or a custom `Logger`.
161
170
  */
162
171
  debug?: DebugOption
172
+ /**
173
+ * Observe-only middleware notified on start, usage, success, and error. Pass
174
+ * `otelMiddleware()` to emit OpenTelemetry spans, or implement the
175
+ * `GenerationMiddleware` contract for a custom backend. In streaming mode the
176
+ * span covers the full create→poll→complete lifecycle; in non-streaming mode
177
+ * it covers job submission. An abandoned stream fires `onAbort`.
178
+ */
179
+ middleware?: Array<GenerationMiddleware>
163
180
  } & ({} extends VideoProviderOptions<TAdapter>
164
181
  ? {
165
182
  /** Provider-specific options for video generation */ modelOptions?: VideoProviderOptions<TAdapter>
@@ -299,14 +316,27 @@ export function generateVideo<
299
316
  async function runCreateVideoJob<
300
317
  TAdapter extends VideoAdapter<string, any, any, any>,
301
318
  >(options: VideoCreateOptions<TAdapter, boolean>): Promise<VideoJobResult> {
302
- const { adapter, prompt, size, duration, modelOptions } = options
319
+ const { adapter, prompt, size, duration, modelOptions, middleware } = options
303
320
  const model = adapter.model
321
+ const requestId = createId('video')
322
+ const startTime = Date.now()
304
323
  const logger: InternalLogger = resolveDebugOption(options.debug)
305
324
  const providerName =
306
325
  (adapter as { name?: string; provider?: string }).provider ??
307
326
  (adapter as { name?: string }).name ??
308
327
  'unknown'
309
328
 
329
+ const mwCtx = createGenerationContext({
330
+ requestId,
331
+ activity: 'video',
332
+ provider: adapter.name,
333
+ model,
334
+ modelOptions,
335
+ createId,
336
+ })
337
+
338
+ await runGenerationStart(middleware, mwCtx)
339
+
310
340
  logger.request(`activity=generateVideo provider=${providerName}`, {
311
341
  provider: providerName,
312
342
  model,
@@ -325,8 +355,17 @@ async function runCreateVideoJob<
325
355
  jobId: result.jobId,
326
356
  model: result.model,
327
357
  })
358
+ // Non-streaming create only submits the job; usage isn't known until the
359
+ // job completes via polling, so the span covers submission only.
360
+ await runGenerationFinish(middleware, mwCtx, {
361
+ duration: Date.now() - startTime,
362
+ })
328
363
  return result
329
364
  } catch (error) {
365
+ await runGenerationError(middleware, mwCtx, {
366
+ error,
367
+ duration: Date.now() - startTime,
368
+ })
330
369
  logger.errors('generateVideo activity failed', {
331
370
  error,
332
371
  source: 'generateVideo',
@@ -346,9 +385,11 @@ function sleep(ms: number): Promise<void> {
346
385
  async function* runStreamingVideoGeneration<
347
386
  TAdapter extends VideoAdapter<string, any, any, any>,
348
387
  >(options: VideoCreateOptions<TAdapter, true>): AsyncIterable<StreamChunk> {
349
- const { adapter, prompt, size, duration, modelOptions } = options
388
+ const { adapter, prompt, size, duration, modelOptions, middleware } = options
350
389
  const model = adapter.model
351
390
  const runId = options.runId ?? createId('run')
391
+ const requestId = createId('video')
392
+ const obsStartTime = Date.now()
352
393
  const pollingInterval = options.pollingInterval ?? 2000
353
394
  const maxDuration = options.maxDuration ?? 600_000
354
395
  const logger: InternalLogger = resolveDebugOption(options.debug)
@@ -366,6 +407,17 @@ async function* runStreamingVideoGeneration<
366
407
  timestamp: Date.now(),
367
408
  } as StreamChunk
368
409
 
410
+ const mwCtx = createGenerationContext({
411
+ requestId,
412
+ activity: 'video',
413
+ provider: adapter.name,
414
+ model,
415
+ modelOptions,
416
+ createId,
417
+ })
418
+
419
+ await runGenerationStart(middleware, mwCtx)
420
+
369
421
  logger.request(
370
422
  `activity=generateVideo provider=${providerName} stream=true`,
371
423
  {
@@ -374,6 +426,9 @@ async function* runStreamingVideoGeneration<
374
426
  },
375
427
  )
376
428
 
429
+ // Tracks whether a terminal observer event (finish/error) has already fired,
430
+ // so the `finally` below can fire one on abandonment without double-firing.
431
+ let settled = false
377
432
  try {
378
433
  // Create the video generation job
379
434
  const jobResult = await adapter.createVideoJob({
@@ -422,6 +477,18 @@ async function* runStreamingVideoGeneration<
422
477
  },
423
478
  )
424
479
 
480
+ // Fire finish before yielding the terminal chunks: the generation has
481
+ // succeeded, so a consumer that stops reading after `generation:result`
482
+ // (without pulling `RUN_FINISHED`) must not trip the abandonment path in
483
+ // `finally`, which would otherwise report a spurious cancellation.
484
+ if (urlResult.usage)
485
+ await runGenerationUsage(middleware, mwCtx, urlResult.usage)
486
+ await runGenerationFinish(middleware, mwCtx, {
487
+ duration: Date.now() - obsStartTime,
488
+ usage: urlResult.usage,
489
+ })
490
+ settled = true
491
+
425
492
  yield {
426
493
  type: 'CUSTOM',
427
494
  name: 'generation:result',
@@ -453,6 +520,14 @@ async function* runStreamingVideoGeneration<
453
520
  throw new Error('Video generation timed out')
454
521
  } catch (error: unknown) {
455
522
  const payload = toRunErrorPayload(error, 'Video generation failed')
523
+ // Mark settled before firing onError: if a user error-hook throws, the
524
+ // `finally` below must still not double-fire onAbort over the same op
525
+ // (which would mask the original error and end the span twice).
526
+ settled = true
527
+ await runGenerationError(middleware, mwCtx, {
528
+ error,
529
+ duration: Date.now() - obsStartTime,
530
+ })
456
531
  logger.errors('generateVideo activity failed', {
457
532
  message: payload.message,
458
533
  code: payload.code,
@@ -467,6 +542,17 @@ async function* runStreamingVideoGeneration<
467
542
  error: payload,
468
543
  timestamp: Date.now(),
469
544
  } as StreamChunk
545
+ } finally {
546
+ if (!settled) {
547
+ // The consumer abandoned the stream (broke the `for await` loop or
548
+ // disconnected) before completion, so the generator is being unwound at
549
+ // a `yield` without reaching finish/error. Fire `onAbort` — a cancel, not
550
+ // an error — so otelMiddleware ends its span instead of leaking it.
551
+ await runGenerationAbort(middleware, mwCtx, {
552
+ reason: 'Video generation stream abandoned before completion',
553
+ duration: Date.now() - obsStartTime,
554
+ })
555
+ }
470
556
  }
471
557
  }
472
558
 
@@ -0,0 +1,20 @@
1
+ // Base, activity-agnostic middleware shared by chat and the media activities.
2
+ // The `ChatMiddleware` superset lives at `../chat/middleware`.
3
+ export type {
4
+ GenerationActivity,
5
+ GenerationMiddleware,
6
+ GenerationMiddlewareContext,
7
+ GenerationUsageInfo,
8
+ GenerationFinishInfo,
9
+ GenerationAbortInfo,
10
+ GenerationErrorInfo,
11
+ AnyGenerationMiddleware,
12
+ } from './types'
13
+ export {
14
+ createGenerationContext,
15
+ runGenerationStart,
16
+ runGenerationUsage,
17
+ runGenerationFinish,
18
+ runGenerationAbort,
19
+ runGenerationError,
20
+ } from './run'
@@ -0,0 +1,88 @@
1
+ import type {
2
+ GenerationAbortInfo,
3
+ GenerationErrorInfo,
4
+ GenerationFinishInfo,
5
+ GenerationMiddleware,
6
+ GenerationMiddlewareContext,
7
+ GenerationUsageInfo,
8
+ } from './types'
9
+
10
+ /**
11
+ * Build the stable context for a single media-activity call.
12
+ *
13
+ * Media activities are always server-side and carry no user runtime context,
14
+ * so `source` is fixed to `'server'` and `context` to `undefined`.
15
+ */
16
+ export function createGenerationContext(args: {
17
+ requestId: string
18
+ activity: GenerationMiddlewareContext['activity']
19
+ provider: string
20
+ model: string
21
+ modelOptions?: unknown
22
+ createId: (prefix: string) => string
23
+ }): GenerationMiddlewareContext {
24
+ return {
25
+ requestId: args.requestId,
26
+ activity: args.activity,
27
+ provider: args.provider,
28
+ model: args.model,
29
+ modelOptions: args.modelOptions,
30
+ source: 'server',
31
+ createId: args.createId,
32
+ context: undefined,
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Run a single lifecycle hook across each middleware in registration order,
38
+ * awaiting each. Exceptions PROPAGATE (matching `chat()` middleware) — a
39
+ * broken middleware fails the activity rather than being silently swallowed.
40
+ */
41
+ async function run(
42
+ middleware: ReadonlyArray<GenerationMiddleware> | undefined,
43
+ invoke: (mw: GenerationMiddleware) => void | Promise<void>,
44
+ ): Promise<void> {
45
+ if (!middleware || middleware.length === 0) return
46
+ for (const mw of middleware) {
47
+ await invoke(mw)
48
+ }
49
+ }
50
+
51
+ export function runGenerationStart(
52
+ middleware: ReadonlyArray<GenerationMiddleware> | undefined,
53
+ ctx: GenerationMiddlewareContext,
54
+ ): Promise<void> {
55
+ return run(middleware, (mw) => mw.onStart?.(ctx))
56
+ }
57
+
58
+ export function runGenerationUsage(
59
+ middleware: ReadonlyArray<GenerationMiddleware> | undefined,
60
+ ctx: GenerationMiddlewareContext,
61
+ usage: GenerationUsageInfo,
62
+ ): Promise<void> {
63
+ return run(middleware, (mw) => mw.onUsage?.(ctx, usage))
64
+ }
65
+
66
+ export function runGenerationFinish(
67
+ middleware: ReadonlyArray<GenerationMiddleware> | undefined,
68
+ ctx: GenerationMiddlewareContext,
69
+ info: GenerationFinishInfo,
70
+ ): Promise<void> {
71
+ return run(middleware, (mw) => mw.onFinish?.(ctx, info))
72
+ }
73
+
74
+ export function runGenerationAbort(
75
+ middleware: ReadonlyArray<GenerationMiddleware> | undefined,
76
+ ctx: GenerationMiddlewareContext,
77
+ info: GenerationAbortInfo,
78
+ ): Promise<void> {
79
+ return run(middleware, (mw) => mw.onAbort?.(ctx, info))
80
+ }
81
+
82
+ export function runGenerationError(
83
+ middleware: ReadonlyArray<GenerationMiddleware> | undefined,
84
+ ctx: GenerationMiddlewareContext,
85
+ info: GenerationErrorInfo,
86
+ ): Promise<void> {
87
+ return run(middleware, (mw) => mw.onError?.(ctx, info))
88
+ }
@@ -0,0 +1,173 @@
1
+ import type { TokenUsage } from '../../types'
2
+
3
+ // ===========================
4
+ // Generation middleware
5
+ // ===========================
6
+ //
7
+ // The base, activity-agnostic middleware contract. Every activity — chat and
8
+ // the media activities — runs middleware that satisfies this shape. `chat()`
9
+ // accepts the richer `ChatMiddleware` superset (it adds config/chunk/tool
10
+ // hooks and capability primitives on top of these lifecycle hooks); media
11
+ // activities accept `GenerationMiddleware` directly.
12
+ //
13
+ // The relationship is intentionally STRUCTURAL, not nominal: `ChatMiddleware`
14
+ // does not `extends GenerationMiddleware`. Chat hooks use function-property
15
+ // syntax, so under `strictFunctionTypes` a narrowed-context subtype would be
16
+ // rejected; declaring it via inheritance would force method syntax and reopen
17
+ // a bivariance hole (a chat hook reading `ctx.messages` slotted where only a
18
+ // base context exists). Instead, the base context/info types are SUPERTYPES
19
+ // (fewer fields) and the chat context/info types are SUBTYPES (more fields),
20
+ // so a single value whose lifecycle hooks are authored against the base — like
21
+ // `otelMiddleware()` — satisfies `GenerationMiddleware & ChatMiddleware` by
22
+ // contravariance, while an arbitrary `ChatMiddleware` is NOT assignable to
23
+ // `GenerationMiddleware`.
24
+
25
+ /**
26
+ * The activity an observability event describes.
27
+ *
28
+ * Mirrors the public surface a caller reaches for: `'chat'` for `chat()`, and
29
+ * the media kinds for the `generate*` activities. `'tts'` matches the speech
30
+ * adapter's kind (the public discriminator avoids inventing a parallel
31
+ * `'speech'`/`'text'` vocabulary). `otelMiddleware` maps each to its
32
+ * `gen_ai.operation.name`.
33
+ */
34
+ export type GenerationActivity =
35
+ | 'chat'
36
+ | 'image'
37
+ | 'video'
38
+ | 'audio'
39
+ | 'tts'
40
+ | 'transcription'
41
+
42
+ /**
43
+ * Stable context passed to every {@link GenerationMiddleware} hook. Created
44
+ * once per activity call and shared across the hooks of that call.
45
+ *
46
+ * Carries only fields every activity can honor. `ChatMiddlewareContext`
47
+ * structurally includes all of these plus chat-only state (messages,
48
+ * iteration, capabilities, …), which is why a chat middleware that reads those
49
+ * extra fields is not assignable to `GenerationMiddleware`.
50
+ */
51
+ export interface GenerationMiddlewareContext<TContext = unknown> {
52
+ /**
53
+ * Stable id correlating the `onStart` / `onFinish` / `onError` / `onAbort`
54
+ * hooks of a single activity call.
55
+ */
56
+ requestId: string
57
+ /** Which activity this call is. Discriminates media from chat. */
58
+ activity: GenerationActivity
59
+ /** Provider/adapter name (e.g. `"openai"`). Emitted as `gen_ai.system`. */
60
+ provider: string
61
+ /** Model id. Emitted as `gen_ai.request.model`. */
62
+ model: string
63
+ /**
64
+ * Provider-specific options passed to the activity, if any. Typed `unknown`
65
+ * because each activity's options are strongly typed per model; a supertype
66
+ * of `ChatMiddlewareContext`'s `modelOptions`.
67
+ */
68
+ modelOptions?: unknown
69
+ /** Where the call originates. Always `'server'` for media activities. */
70
+ source: 'client' | 'server'
71
+ /** Generate a unique id with the given prefix. */
72
+ createId: (prefix: string) => string
73
+ /** Runtime context provided by the activity options, if any. */
74
+ context: TContext
75
+ }
76
+
77
+ // ===========================
78
+ // Hook payloads
79
+ // ===========================
80
+
81
+ /**
82
+ * Token usage passed to {@link GenerationMiddleware.onUsage}. Kept as an
83
+ * interface extending `TokenUsage` to preserve declaration merging for this
84
+ * publicly exported type.
85
+ */
86
+ export interface GenerationUsageInfo extends TokenUsage {}
87
+
88
+ /** Information passed to {@link GenerationMiddleware.onFinish}. */
89
+ export interface GenerationFinishInfo {
90
+ /** Wall-clock duration of the activity call, in milliseconds. */
91
+ duration: number
92
+ /** Unified usage, when the provider reported it. */
93
+ usage?: TokenUsage | undefined
94
+ }
95
+
96
+ /** Information passed to {@link GenerationMiddleware.onAbort}. */
97
+ export interface GenerationAbortInfo {
98
+ /** The reason for the abort, if provided. */
99
+ reason?: string
100
+ /** Wall-clock duration until the abort, in milliseconds. */
101
+ duration: number
102
+ }
103
+
104
+ /** Information passed to {@link GenerationMiddleware.onError}. */
105
+ export interface GenerationErrorInfo {
106
+ /** The thrown value (typically an `Error`). */
107
+ error: unknown
108
+ /** Wall-clock duration until the failure, in milliseconds. */
109
+ duration: number
110
+ }
111
+
112
+ // ===========================
113
+ // Middleware interface
114
+ // ===========================
115
+
116
+ /**
117
+ * Activity-agnostic, observe-only middleware.
118
+ *
119
+ * A thin lifecycle observer registerable on any activity via its `middleware`
120
+ * option. Unlike `ChatMiddleware` (which can also rewrite config, chunks, and
121
+ * tool calls), these hooks only observe — the right fit for the single
122
+ * request → response shape of media activities. Pass `otelMiddleware()` for
123
+ * OpenTelemetry, or implement the hooks directly for a custom backend.
124
+ *
125
+ * Hooks are awaited in registration order. A hook that throws PROPAGATES and
126
+ * fails the activity — matching `chat()` middleware semantics. Keep them cheap;
127
+ * they run inline with the request.
128
+ *
129
+ * Exactly one of `onFinish` / `onAbort` / `onError` fires per call.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * import { generateImage } from '@tanstack/ai'
134
+ * import { otelMiddleware } from '@tanstack/ai/middlewares/otel'
135
+ * import { openaiImage } from '@tanstack/ai-openai'
136
+ * import { trace } from '@opentelemetry/api'
137
+ *
138
+ * await generateImage({
139
+ * adapter: openaiImage('gpt-image-1'),
140
+ * prompt: 'A serene mountain landscape at sunset',
141
+ * middleware: [otelMiddleware({ tracer: trace.getTracer('my-app') })],
142
+ * })
143
+ * ```
144
+ */
145
+ export interface GenerationMiddleware<TContext = unknown> {
146
+ /** Optional name, surfaced in diagnostics. */
147
+ name?: string
148
+ /** Called before the adapter request begins. */
149
+ onStart?: (ctx: GenerationMiddlewareContext<TContext>) => void | Promise<void>
150
+ /** Called when the provider reports usage, before `onFinish`. */
151
+ onUsage?: (
152
+ ctx: GenerationMiddlewareContext<TContext>,
153
+ usage: GenerationUsageInfo,
154
+ ) => void | Promise<void>
155
+ /** Called after the activity completes successfully. */
156
+ onFinish?: (
157
+ ctx: GenerationMiddlewareContext<TContext>,
158
+ info: GenerationFinishInfo,
159
+ ) => void | Promise<void>
160
+ /** Called when the activity is aborted (e.g. an abandoned stream). */
161
+ onAbort?: (
162
+ ctx: GenerationMiddlewareContext<TContext>,
163
+ info: GenerationAbortInfo,
164
+ ) => void | Promise<void>
165
+ /** Called when the activity throws before completing. */
166
+ onError?: (
167
+ ctx: GenerationMiddlewareContext<TContext>,
168
+ info: GenerationErrorInfo,
169
+ ) => void | Promise<void>
170
+ }
171
+
172
+ /** A `GenerationMiddleware` with a permissive context — for use as a constraint. */
173
+ export type AnyGenerationMiddleware = GenerationMiddleware<any>
package/src/index.ts CHANGED
@@ -82,6 +82,10 @@ export {
82
82
  // Tool call management
83
83
  export { ToolCallManager } from './activities/chat/tools/tool-calls'
84
84
 
85
+ // Lazy tool discovery (name of the synthetic discovery tool, for custom
86
+ // message-compaction logic that needs to reference it)
87
+ export { DISCOVERY_TOOL_NAME } from './activities/chat/tools/lazy-tool-manager'
88
+
85
89
  // Provider tool type
86
90
  export type { ProviderTool } from './tools/provider-tool'
87
91
  export { brandProviderTool } from './tools/provider-tool'
@@ -118,6 +122,21 @@ export type {
118
122
  ErrorInfo,
119
123
  } from './activities/chat/middleware/index'
120
124
 
125
+ // Base, activity-agnostic middleware. The observe-only superset that media
126
+ // activities accept via their `middleware` option; `ChatMiddleware` adds the
127
+ // chat-only hooks on top. Pure types only — the `otelMiddleware` value lives at
128
+ // `@tanstack/ai/middlewares/otel` so the root barrel never requires the
129
+ // optional `@opentelemetry/api` peer dependency.
130
+ export type {
131
+ GenerationMiddleware,
132
+ GenerationMiddlewareContext,
133
+ GenerationActivity,
134
+ GenerationUsageInfo,
135
+ GenerationFinishInfo,
136
+ GenerationAbortInfo,
137
+ GenerationErrorInfo,
138
+ AnyGenerationMiddleware,
139
+ } from './activities/middleware/index'
121
140
  // Capability primitives + middleware builder
122
141
  export {
123
142
  createCapability,