@tanstack/ai 0.11.1 → 0.13.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 (60) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +12 -4
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +23 -3
  4. package/dist/esm/activities/chat/index.js +78 -17
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/middleware/compose.d.ts +3 -1
  7. package/dist/esm/activities/chat/middleware/compose.js +79 -1
  8. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  9. package/dist/esm/activities/generateImage/index.d.ts +7 -0
  10. package/dist/esm/activities/generateImage/index.js +19 -3
  11. package/dist/esm/activities/generateImage/index.js.map +1 -1
  12. package/dist/esm/activities/generateSpeech/index.d.ts +7 -0
  13. package/dist/esm/activities/generateSpeech/index.js +21 -3
  14. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  15. package/dist/esm/activities/generateTranscription/index.d.ts +7 -0
  16. package/dist/esm/activities/generateTranscription/index.js +32 -13
  17. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  18. package/dist/esm/activities/generateVideo/index.d.ts +7 -0
  19. package/dist/esm/activities/generateVideo/index.js +49 -7
  20. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  21. package/dist/esm/activities/summarize/index.d.ts +7 -0
  22. package/dist/esm/activities/summarize/index.js +54 -19
  23. package/dist/esm/activities/summarize/index.js.map +1 -1
  24. package/dist/esm/adapter-internals.d.ts +3 -0
  25. package/dist/esm/adapter-internals.js +7 -0
  26. package/dist/esm/adapter-internals.js.map +1 -0
  27. package/dist/esm/index.d.ts +3 -0
  28. package/dist/esm/index.js +2 -0
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/logger/console-logger.d.ts +11 -0
  31. package/dist/esm/logger/console-logger.js +27 -0
  32. package/dist/esm/logger/console-logger.js.map +1 -0
  33. package/dist/esm/logger/internal-logger.d.ts +33 -0
  34. package/dist/esm/logger/internal-logger.js +69 -0
  35. package/dist/esm/logger/internal-logger.js.map +1 -0
  36. package/dist/esm/logger/resolve.d.ts +14 -0
  37. package/dist/esm/logger/resolve.js +54 -0
  38. package/dist/esm/logger/resolve.js.map +1 -0
  39. package/dist/esm/logger/types.d.ts +75 -0
  40. package/dist/esm/tools/provider-tool.d.ts +21 -0
  41. package/dist/esm/types.d.ts +34 -0
  42. package/package.json +6 -2
  43. package/skills/ai-core/SKILL.md +5 -3
  44. package/skills/ai-core/debug-logging/SKILL.md +263 -0
  45. package/src/activities/chat/adapter.ts +14 -3
  46. package/src/activities/chat/index.ts +119 -24
  47. package/src/activities/chat/middleware/compose.ts +84 -1
  48. package/src/activities/generateImage/index.ts +29 -3
  49. package/src/activities/generateSpeech/index.ts +35 -3
  50. package/src/activities/generateTranscription/index.ts +45 -13
  51. package/src/activities/generateVideo/index.ts +65 -6
  52. package/src/activities/summarize/index.ts +66 -20
  53. package/src/adapter-internals.ts +7 -0
  54. package/src/index.ts +12 -0
  55. package/src/logger/console-logger.ts +49 -0
  56. package/src/logger/internal-logger.ts +107 -0
  57. package/src/logger/resolve.ts +72 -0
  58. package/src/logger/types.ts +78 -0
  59. package/src/tools/provider-tool.ts +25 -0
  60. package/src/types.ts +36 -0
@@ -8,6 +8,7 @@
8
8
  import { devtoolsMiddleware } from '@tanstack/ai-event-client'
9
9
  import { stripToSpecMiddleware } from '../../strip-to-spec-middleware'
10
10
  import { streamToText } from '../../stream-to-response.js'
11
+ import { resolveDebugOption } from '../../logger/resolve'
11
12
  import { LazyToolManager } from './tools/lazy-tool-manager'
12
13
  import {
13
14
  MiddlewareAbortError,
@@ -51,6 +52,9 @@ import type {
51
52
  ChatMiddlewareContext,
52
53
  ChatMiddlewarePhase,
53
54
  } from './middleware/types'
55
+ import type { InternalLogger } from '../../logger/internal-logger'
56
+ import type { DebugOption } from '../../logger/types'
57
+ import type { ProviderTool } from '../../tools/provider-tool'
54
58
 
55
59
  // ===========================
56
60
  // Activity Kind
@@ -87,8 +91,20 @@ export interface TextActivityOptions<
87
91
  >
88
92
  /** System prompts to prepend to the conversation */
89
93
  systemPrompts?: TextOptions['systemPrompts']
90
- /** Tools for function calling (auto-executed when called) */
91
- tools?: TextOptions['tools']
94
+ /**
95
+ * Tools for function calling (auto-executed when called).
96
+ *
97
+ * Accepts two shapes:
98
+ * - User-defined tools via `toolDefinition()` — plain `Tool`, always assignable.
99
+ * - Provider tools from `@tanstack/ai-<provider>/tools` (e.g. `webSearchTool`)
100
+ * — branded and type-checked against the selected model's
101
+ * `supports.tools` list. Passing an unsupported tool produces a
102
+ * compile-time error on the array element.
103
+ */
104
+ tools?: Array<
105
+ | (Tool & { readonly '~toolKind'?: never })
106
+ | ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
107
+ >
92
108
  /** Controls the randomness of the output. Higher values make output more random. Range: [0.0, 2.0] */
93
109
  temperature?: TextOptions['temperature']
94
110
  /** Nucleus sampling parameter. The model considers tokens with topP probability mass. */
@@ -168,6 +184,13 @@ export interface TextActivityOptions<
168
184
  * Can be used to pass request-scoped data (e.g., user ID, request context).
169
185
  */
170
186
  context?: unknown
187
+ /**
188
+ * Enable debug logging. Pass `true` to enable all categories with the default
189
+ * console logger, `false` to silence everything, or a `DebugConfig` object for
190
+ * granular control and/or a custom `Logger`. Defaults to `undefined`, which
191
+ * means only the `errors` category is active.
192
+ */
193
+ debug?: DebugOption
171
194
  }
172
195
 
173
196
  // ===========================
@@ -280,7 +303,13 @@ class TextEngine<
280
303
  private middlewareAbortController?: AbortController
281
304
  private terminalHookCalled = false
282
305
 
283
- constructor(config: TextEngineConfig<TAdapter, TParams>) {
306
+ private readonly logger: InternalLogger
307
+
308
+ constructor(
309
+ config: TextEngineConfig<TAdapter, TParams>,
310
+ logger: InternalLogger,
311
+ ) {
312
+ this.logger = logger
284
313
  this.adapter = config.adapter
285
314
  this.params = config.params
286
315
  this.systemPrompts = config.params.systemPrompts || []
@@ -328,7 +357,7 @@ class TextEngine<
328
357
  ...(config.middleware || []),
329
358
  stripToSpecMiddleware(),
330
359
  ]
331
- this.middlewareRunner = new MiddlewareRunner(allMiddleware)
360
+ this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger)
332
361
  this.middlewareAbortController = new AbortController()
333
362
  this.middlewareCtx = {
334
363
  requestId: this.requestId,
@@ -380,6 +409,9 @@ class TextEngine<
380
409
 
381
410
  async *run(): AsyncGenerator<StreamChunk> {
382
411
  this.beforeRun()
412
+ this.logger.agentLoop('run started', {
413
+ conversationId: this.middlewareCtx.conversationId,
414
+ })
383
415
 
384
416
  try {
385
417
  // Run initial onConfig (phase = init)
@@ -404,6 +436,10 @@ class TextEngine<
404
436
  return
405
437
  }
406
438
 
439
+ this.logger.agentLoop(`iteration=${this.middlewareCtx.iteration}`, {
440
+ iteration: this.middlewareCtx.iteration,
441
+ })
442
+
407
443
  await this.beginCycle()
408
444
 
409
445
  if (this.cyclePhase === 'processText') {
@@ -425,6 +461,10 @@ class TextEngine<
425
461
  this.endCycle()
426
462
  } while (this.shouldContinue())
427
463
 
464
+ this.logger.agentLoop('run finished', {
465
+ finishReason: this.lastFinishReason,
466
+ })
467
+
428
468
  // Call terminal onFinish hook (skip when waiting for client — stream is paused, not finished)
429
469
  if (!this.terminalHookCalled && this.toolPhase !== 'wait') {
430
470
  this.terminalHookCalled = true
@@ -447,6 +487,10 @@ class TextEngine<
447
487
  })
448
488
  } else {
449
489
  // Genuine error — call onError
490
+ this.logger.errors('chat run failed', {
491
+ error,
492
+ conversationId: this.middlewareCtx.conversationId,
493
+ })
450
494
  await this.middlewareRunner.runOnError(this.middlewareCtx, {
451
495
  error,
452
496
  duration: Date.now() - this.streamStartTime,
@@ -542,6 +586,18 @@ class TextEngine<
542
586
 
543
587
  this.middlewareCtx.phase = 'modelStream'
544
588
 
589
+ const providerName =
590
+ (this.adapter as { provider?: string }).provider ?? this.adapter.name
591
+ this.logger.request(
592
+ `activity=chat provider=${providerName} model=${this.params.model} messages=${this.messages.length} tools=${this.tools.length} stream=true`,
593
+ {
594
+ provider: providerName,
595
+ model: this.params.model,
596
+ messageCount: this.messages.length,
597
+ toolCount: this.tools.length,
598
+ },
599
+ )
600
+
545
601
  for await (const chunk of this.adapter.chatStream({
546
602
  model: this.params.model,
547
603
  messages: this.messages,
@@ -553,6 +609,7 @@ class TextEngine<
553
609
  request: this.effectiveRequest,
554
610
  modelOptions,
555
611
  systemPrompts: this.systemPrompts,
612
+ logger: this.logger,
556
613
  threadId: this.threadId,
557
614
  runId: this.runIdOverride,
558
615
  })) {
@@ -572,6 +629,7 @@ class TextEngine<
572
629
  chunk,
573
630
  )
574
631
  for (const outputChunk of outputChunks) {
632
+ this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk })
575
633
  yield outputChunk
576
634
  this.middlewareCtx.chunkIndex++
577
635
  }
@@ -728,6 +786,10 @@ class TextEngine<
728
786
  (eventName, data) => this.createCustomEventChunk(eventName, data),
729
787
  {
730
788
  onBeforeToolCall: async (toolCall, tool, args) => {
789
+ this.logger.tools(`phase=before name=${toolCall.function.name}`, {
790
+ name: toolCall.function.name,
791
+ args,
792
+ })
731
793
  const hookCtx = {
732
794
  toolCall,
733
795
  tool,
@@ -741,6 +803,10 @@ class TextEngine<
741
803
  )
742
804
  },
743
805
  onAfterToolCall: async (info) => {
806
+ this.logger.tools(`phase=after name=${info.toolName}`, {
807
+ name: info.toolName,
808
+ result: info.result,
809
+ })
744
810
  await this.middlewareRunner.runOnAfterToolCall(
745
811
  this.middlewareCtx,
746
812
  info,
@@ -881,6 +947,10 @@ class TextEngine<
881
947
  (eventName, data) => this.createCustomEventChunk(eventName, data),
882
948
  {
883
949
  onBeforeToolCall: async (toolCall, tool, args) => {
950
+ this.logger.tools(`phase=before name=${toolCall.function.name}`, {
951
+ name: toolCall.function.name,
952
+ args,
953
+ })
884
954
  const hookCtx = {
885
955
  toolCall,
886
956
  tool,
@@ -894,6 +964,10 @@ class TextEngine<
894
964
  )
895
965
  },
896
966
  onAfterToolCall: async (info) => {
967
+ this.logger.tools(`phase=after name=${info.toolName}`, {
968
+ name: info.toolName,
969
+ result: info.result,
970
+ })
897
971
  await this.middlewareRunner.runOnAfterToolCall(
898
972
  this.middlewareCtx,
899
973
  info,
@@ -1478,18 +1552,22 @@ export function chat<
1478
1552
  async function* runStreamingText(
1479
1553
  options: TextActivityOptions<AnyTextAdapter, undefined, true>,
1480
1554
  ): AsyncIterable<StreamChunk> {
1481
- const { adapter, middleware, context, ...textOptions } = options
1555
+ const { adapter, middleware, context, debug, ...textOptions } = options
1482
1556
  const model = adapter.model
1557
+ const logger = resolveDebugOption(debug)
1483
1558
 
1484
- const engine = new TextEngine({
1485
- adapter,
1486
- params: { ...textOptions, model } as TextOptions<
1487
- Record<string, any>,
1488
- Record<string, any>
1489
- >,
1490
- middleware,
1491
- context,
1492
- })
1559
+ const engine = new TextEngine(
1560
+ {
1561
+ adapter,
1562
+ params: { ...textOptions, model, logger } as TextOptions<
1563
+ Record<string, any>,
1564
+ Record<string, any>
1565
+ >,
1566
+ middleware,
1567
+ context,
1568
+ },
1569
+ logger,
1570
+ )
1493
1571
 
1494
1572
  for await (const chunk of engine.run()) {
1495
1573
  yield chunk
@@ -1520,23 +1598,28 @@ function runNonStreamingText(
1520
1598
  async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
1521
1599
  options: TextActivityOptions<AnyTextAdapter, TSchema, boolean>,
1522
1600
  ): Promise<InferSchemaType<TSchema>> {
1523
- const { adapter, outputSchema, middleware, context, ...textOptions } = options
1601
+ const { adapter, outputSchema, middleware, context, debug, ...textOptions } =
1602
+ options
1524
1603
  const model = adapter.model
1604
+ const logger = resolveDebugOption(debug)
1525
1605
 
1526
1606
  if (!outputSchema) {
1527
1607
  throw new Error('outputSchema is required for structured output')
1528
1608
  }
1529
1609
 
1530
1610
  // Create the engine and run the agentic loop
1531
- const engine = new TextEngine({
1532
- adapter,
1533
- params: { ...textOptions, model } as TextOptions<
1534
- Record<string, unknown>,
1535
- Record<string, unknown>
1536
- >,
1537
- middleware,
1538
- context,
1539
- })
1611
+ const engine = new TextEngine(
1612
+ {
1613
+ adapter,
1614
+ params: { ...textOptions, model, logger } as TextOptions<
1615
+ Record<string, unknown>,
1616
+ Record<string, unknown>
1617
+ >,
1618
+ middleware,
1619
+ context,
1620
+ },
1621
+ logger,
1622
+ )
1540
1623
 
1541
1624
  // Consume the stream to run the agentic loop
1542
1625
  for await (const _chunk of engine.run()) {
@@ -1560,6 +1643,17 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
1560
1643
  throw new Error('Failed to convert output schema to JSON Schema')
1561
1644
  }
1562
1645
 
1646
+ const providerName =
1647
+ (adapter as { provider?: string }).provider ?? adapter.name
1648
+ logger.request(
1649
+ `activity=chat-structured provider=${providerName} model=${model} messages=${finalMessages.length}`,
1650
+ {
1651
+ provider: providerName,
1652
+ model,
1653
+ messageCount: finalMessages.length,
1654
+ },
1655
+ )
1656
+
1563
1657
  // Call the adapter's structured output method with the conversation context
1564
1658
  // The adapter receives JSON Schema and can apply vendor-specific patches
1565
1659
  const result = await adapter.structuredOutput({
@@ -1567,6 +1661,7 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
1567
1661
  ...structuredTextOptions,
1568
1662
  model,
1569
1663
  messages: finalMessages,
1664
+ logger,
1570
1665
  },
1571
1666
  outputSchema: jsonSchema,
1572
1667
  })
@@ -1,5 +1,6 @@
1
1
  import { aiEventClient } from '@tanstack/ai-event-client'
2
2
  import type { StreamChunk } from '../../../types'
3
+ import type { InternalLogger } from '../../../logger/internal-logger'
3
4
  import type {
4
5
  AbortInfo,
5
6
  AfterToolCallInfo,
@@ -36,9 +37,14 @@ function instrumentCtx(ctx: ChatMiddlewareContext) {
36
37
  */
37
38
  export class MiddlewareRunner {
38
39
  private readonly middlewares: ReadonlyArray<ChatMiddleware>
40
+ private readonly logger: InternalLogger
39
41
 
40
- constructor(middlewares: ReadonlyArray<ChatMiddleware>) {
42
+ constructor(
43
+ middlewares: ReadonlyArray<ChatMiddleware>,
44
+ logger: InternalLogger,
45
+ ) {
41
46
  this.middlewares = middlewares
47
+ this.logger = logger
42
48
  }
43
49
 
44
50
  get hasMiddleware(): boolean {
@@ -63,6 +69,15 @@ export class MiddlewareRunner {
63
69
  const hasTransform = result !== undefined && result !== null
64
70
  if (hasTransform) {
65
71
  current = { ...current, ...result }
72
+ if (!skip) {
73
+ this.logger.config(
74
+ `middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result as object).join(',')}`,
75
+ {
76
+ middleware: mw.name ?? 'unnamed',
77
+ changes: result,
78
+ },
79
+ )
80
+ }
66
81
  }
67
82
  if (!skip) {
68
83
  const base = instrumentCtx(ctx)
@@ -98,6 +113,10 @@ export class MiddlewareRunner {
98
113
  const start = Date.now()
99
114
  await mw.onStart(ctx)
100
115
  if (!skip) {
116
+ this.logger.middleware(
117
+ `hook=onStart middleware=${mw.name ?? 'unnamed'}`,
118
+ { middleware: mw.name ?? 'unnamed', hook: 'onStart' },
119
+ )
101
120
  aiEventClient.emit('middleware:hook:executed', {
102
121
  ...instrumentCtx(ctx),
103
122
  middlewareName: mw.name || 'unnamed',
@@ -134,10 +153,24 @@ export class MiddlewareRunner {
134
153
  for (const c of chunks) {
135
154
  // Cast: @ag-ui/core Zod passthrough types prevent direct `.type` access
136
155
  const chunkType = (c as StreamChunk & { type: string }).type
156
+ if (!skip) {
157
+ this.logger.middleware(
158
+ `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType}`,
159
+ { middleware: mw.name ?? 'unnamed', hook: 'onChunk', in: c },
160
+ )
161
+ }
137
162
  const result = await mw.onChunk(ctx, c)
138
163
  if (result === null) {
139
164
  // Drop this chunk
140
165
  if (!skip) {
166
+ this.logger.middleware(
167
+ `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=<dropped>`,
168
+ {
169
+ middleware: mw.name ?? 'unnamed',
170
+ hook: 'onChunk',
171
+ dropped: true,
172
+ },
173
+ )
141
174
  aiEventClient.emit('middleware:chunk:transformed', {
142
175
  ...instrumentCtx(ctx),
143
176
  middlewareName: mw.name || 'unnamed',
@@ -154,6 +187,15 @@ export class MiddlewareRunner {
154
187
  // Expand
155
188
  nextChunks.push(...result)
156
189
  if (!skip) {
190
+ this.logger.middleware(
191
+ `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=[${result.map((r: StreamChunk) => (r as StreamChunk & { type: string }).type).join(',')}]`,
192
+ {
193
+ middleware: mw.name ?? 'unnamed',
194
+ hook: 'onChunk',
195
+ in: c,
196
+ out: result,
197
+ },
198
+ )
157
199
  aiEventClient.emit('middleware:chunk:transformed', {
158
200
  ...instrumentCtx(ctx),
159
201
  middlewareName: mw.name || 'unnamed',
@@ -166,6 +208,15 @@ export class MiddlewareRunner {
166
208
  // Replace
167
209
  nextChunks.push(result)
168
210
  if (!skip) {
211
+ this.logger.middleware(
212
+ `hook=onChunk middleware=${mw.name ?? 'unnamed'} in=${chunkType} out=${(result as StreamChunk & { type: string }).type}`,
213
+ {
214
+ middleware: mw.name ?? 'unnamed',
215
+ hook: 'onChunk',
216
+ in: c,
217
+ out: result,
218
+ },
219
+ )
169
220
  aiEventClient.emit('middleware:chunk:transformed', {
170
221
  ...instrumentCtx(ctx),
171
222
  middlewareName: mw.name || 'unnamed',
@@ -197,6 +248,10 @@ export class MiddlewareRunner {
197
248
  const decision = await mw.onBeforeToolCall(ctx, hookCtx)
198
249
  const hasTransform = decision !== undefined && decision !== null
199
250
  if (!skip) {
251
+ this.logger.middleware(
252
+ `hook=onBeforeToolCall middleware=${mw.name ?? 'unnamed'}`,
253
+ { middleware: mw.name ?? 'unnamed', hook: 'onBeforeToolCall' },
254
+ )
200
255
  aiEventClient.emit('middleware:hook:executed', {
201
256
  ...instrumentCtx(ctx),
202
257
  middlewareName: mw.name || 'unnamed',
@@ -227,6 +282,10 @@ export class MiddlewareRunner {
227
282
  const start = Date.now()
228
283
  await mw.onAfterToolCall(ctx, info)
229
284
  if (!skip) {
285
+ this.logger.middleware(
286
+ `hook=onAfterToolCall middleware=${mw.name ?? 'unnamed'}`,
287
+ { middleware: mw.name ?? 'unnamed', hook: 'onAfterToolCall' },
288
+ )
230
289
  aiEventClient.emit('middleware:hook:executed', {
231
290
  ...instrumentCtx(ctx),
232
291
  middlewareName: mw.name || 'unnamed',
@@ -253,6 +312,10 @@ export class MiddlewareRunner {
253
312
  const start = Date.now()
254
313
  await mw.onUsage(ctx, usage)
255
314
  if (!skip) {
315
+ this.logger.middleware(
316
+ `hook=onUsage middleware=${mw.name ?? 'unnamed'}`,
317
+ { middleware: mw.name ?? 'unnamed', hook: 'onUsage' },
318
+ )
256
319
  aiEventClient.emit('middleware:hook:executed', {
257
320
  ...instrumentCtx(ctx),
258
321
  middlewareName: mw.name || 'unnamed',
@@ -279,6 +342,10 @@ export class MiddlewareRunner {
279
342
  const start = Date.now()
280
343
  await mw.onFinish(ctx, info)
281
344
  if (!skip) {
345
+ this.logger.middleware(
346
+ `hook=onFinish middleware=${mw.name ?? 'unnamed'}`,
347
+ { middleware: mw.name ?? 'unnamed', hook: 'onFinish' },
348
+ )
282
349
  aiEventClient.emit('middleware:hook:executed', {
283
350
  ...instrumentCtx(ctx),
284
351
  middlewareName: mw.name || 'unnamed',
@@ -302,6 +369,10 @@ export class MiddlewareRunner {
302
369
  const start = Date.now()
303
370
  await mw.onAbort(ctx, info)
304
371
  if (!skip) {
372
+ this.logger.middleware(
373
+ `hook=onAbort middleware=${mw.name ?? 'unnamed'}`,
374
+ { middleware: mw.name ?? 'unnamed', hook: 'onAbort' },
375
+ )
305
376
  aiEventClient.emit('middleware:hook:executed', {
306
377
  ...instrumentCtx(ctx),
307
378
  middlewareName: mw.name || 'unnamed',
@@ -325,6 +396,10 @@ export class MiddlewareRunner {
325
396
  const start = Date.now()
326
397
  await mw.onError(ctx, info)
327
398
  if (!skip) {
399
+ this.logger.middleware(
400
+ `hook=onError middleware=${mw.name ?? 'unnamed'}`,
401
+ { middleware: mw.name ?? 'unnamed', hook: 'onError' },
402
+ )
328
403
  aiEventClient.emit('middleware:hook:executed', {
329
404
  ...instrumentCtx(ctx),
330
405
  middlewareName: mw.name || 'unnamed',
@@ -352,6 +427,10 @@ export class MiddlewareRunner {
352
427
  const start = Date.now()
353
428
  await mw.onIteration(ctx, info)
354
429
  if (!skip) {
430
+ this.logger.middleware(
431
+ `hook=onIteration middleware=${mw.name ?? 'unnamed'}`,
432
+ { middleware: mw.name ?? 'unnamed', hook: 'onIteration' },
433
+ )
355
434
  aiEventClient.emit('middleware:hook:executed', {
356
435
  ...instrumentCtx(ctx),
357
436
  middlewareName: mw.name || 'unnamed',
@@ -379,6 +458,10 @@ export class MiddlewareRunner {
379
458
  const start = Date.now()
380
459
  await mw.onToolPhaseComplete(ctx, info)
381
460
  if (!skip) {
461
+ this.logger.middleware(
462
+ `hook=onToolPhaseComplete middleware=${mw.name ?? 'unnamed'}`,
463
+ { middleware: mw.name ?? 'unnamed', hook: 'onToolPhaseComplete' },
464
+ )
382
465
  aiEventClient.emit('middleware:hook:executed', {
383
466
  ...instrumentCtx(ctx),
384
467
  middlewareName: mw.name || 'unnamed',
@@ -7,6 +7,9 @@
7
7
 
8
8
  import { aiEventClient } from '@tanstack/ai-event-client'
9
9
  import { streamGenerationResult } from '../stream-generation-result.js'
10
+ import { resolveDebugOption } from '../../logger/resolve'
11
+ import type { InternalLogger } from '../../logger/internal-logger'
12
+ import type { DebugOption } from '../../logger/types'
10
13
  import type { ImageAdapter } from './adapter'
11
14
  import type { ImageGenerationResult, StreamChunk } from '../../types'
12
15
 
@@ -83,6 +86,12 @@ export type ImageActivityOptions<
83
86
  * @default false
84
87
  */
85
88
  stream?: TStream
89
+ /**
90
+ * Enable debug logging. Pass `true` to enable all categories, `false` to
91
+ * silence everything including errors, or a `DebugConfig` object for granular
92
+ * control and/or a custom `Logger`.
93
+ */
94
+ debug?: DebugOption
86
95
  } & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']>
87
96
  ? {
88
97
  /** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<
@@ -188,10 +197,11 @@ async function runGenerateImage<
188
197
  >(
189
198
  options: ImageActivityOptions<TAdapter, boolean>,
190
199
  ): Promise<ImageGenerationResult> {
191
- const { adapter, stream: _stream, ...rest } = options
200
+ const { adapter, stream: _stream, debug: _debug, ...rest } = options
192
201
  const model = adapter.model
193
202
  const requestId = createId('image')
194
203
  const startTime = Date.now()
204
+ const logger: InternalLogger = resolveDebugOption(options.debug)
195
205
 
196
206
  aiEventClient.emit('image:request:started', {
197
207
  requestId,
@@ -204,7 +214,13 @@ async function runGenerateImage<
204
214
  timestamp: startTime,
205
215
  })
206
216
 
207
- return adapter.generateImages({ ...rest, model }).then((result) => {
217
+ logger.request(`activity=generateImage provider=${adapter.name}`, {
218
+ provider: adapter.name,
219
+ model,
220
+ })
221
+
222
+ try {
223
+ const result = await adapter.generateImages({ ...rest, model, logger })
208
224
  const duration = Date.now() - startTime
209
225
 
210
226
  aiEventClient.emit('image:request:completed', {
@@ -230,8 +246,18 @@ async function runGenerateImage<
230
246
  })
231
247
  }
232
248
 
249
+ logger.output(`activity=generateImage count=${result.images.length}`, {
250
+ count: result.images.length,
251
+ })
252
+
233
253
  return result
234
- })
254
+ } catch (error) {
255
+ logger.errors('generateImage activity failed', {
256
+ error,
257
+ source: 'generateImage',
258
+ })
259
+ throw error
260
+ }
235
261
  }
236
262
 
237
263
  // ===========================
@@ -7,6 +7,9 @@
7
7
 
8
8
  import { aiEventClient } from '@tanstack/ai-event-client'
9
9
  import { streamGenerationResult } from '../stream-generation-result.js'
10
+ import { resolveDebugOption } from '../../logger/resolve'
11
+ import type { InternalLogger } from '../../logger/internal-logger'
12
+ import type { DebugOption } from '../../logger/types'
10
13
  import type { TTSAdapter } from './adapter'
11
14
  import type { StreamChunk, TTSResult } from '../../types'
12
15
 
@@ -64,6 +67,12 @@ export interface TTSActivityOptions<
64
67
  * @default false
65
68
  */
66
69
  stream?: TStream
70
+ /**
71
+ * Enable debug logging. Pass `true` to enable all categories, `false` to
72
+ * silence everything including errors, or a `DebugConfig` object for granular
73
+ * control and/or a custom `Logger`.
74
+ */
75
+ debug?: DebugOption
67
76
  }
68
77
 
69
78
  // ===========================
@@ -134,10 +143,15 @@ export function generateSpeech<
134
143
  async function runGenerateSpeech<TAdapter extends TTSAdapter<string, object>>(
135
144
  options: TTSActivityOptions<TAdapter, boolean>,
136
145
  ): Promise<TTSResult> {
137
- const { adapter, stream: _stream, ...rest } = options
146
+ const { adapter, stream: _stream, debug: _debug, ...rest } = options
138
147
  const model = adapter.model
139
148
  const requestId = createId('speech')
140
149
  const startTime = Date.now()
150
+ const logger: InternalLogger = resolveDebugOption(options.debug)
151
+ const providerName =
152
+ (adapter as { name?: string; provider?: string }).provider ??
153
+ (adapter as { name?: string }).name ??
154
+ 'unknown'
141
155
 
142
156
  aiEventClient.emit('speech:request:started', {
143
157
  requestId,
@@ -151,7 +165,14 @@ async function runGenerateSpeech<TAdapter extends TTSAdapter<string, object>>(
151
165
  timestamp: startTime,
152
166
  })
153
167
 
154
- return adapter.generateSpeech({ ...rest, model }).then((result) => {
168
+ logger.request(`activity=generateSpeech provider=${providerName}`, {
169
+ provider: providerName,
170
+ model,
171
+ })
172
+
173
+ try {
174
+ const result = await adapter.generateSpeech({ ...rest, model, logger })
175
+
155
176
  const duration = Date.now() - startTime
156
177
 
157
178
  aiEventClient.emit('speech:request:completed', {
@@ -167,8 +188,19 @@ async function runGenerateSpeech<TAdapter extends TTSAdapter<string, object>>(
167
188
  timestamp: Date.now(),
168
189
  })
169
190
 
191
+ logger.output(`activity=generateSpeech bytes=${result.audio.length}`, {
192
+ bytes: result.audio.length,
193
+ contentType: result.contentType,
194
+ })
195
+
170
196
  return result
171
- })
197
+ } catch (error) {
198
+ logger.errors('generateSpeech activity failed', {
199
+ error,
200
+ source: 'generateSpeech',
201
+ })
202
+ throw error
203
+ }
172
204
  }
173
205
 
174
206
  // ===========================