@tanstack/ai 0.12.0 → 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 (58) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +6 -1
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +8 -0
  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 +2 -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/types.d.ts +34 -0
  41. package/package.json +6 -2
  42. package/skills/ai-core/SKILL.md +5 -3
  43. package/skills/ai-core/debug-logging/SKILL.md +263 -0
  44. package/src/activities/chat/adapter.ts +6 -1
  45. package/src/activities/chat/index.ts +104 -22
  46. package/src/activities/chat/middleware/compose.ts +84 -1
  47. package/src/activities/generateImage/index.ts +29 -3
  48. package/src/activities/generateSpeech/index.ts +35 -3
  49. package/src/activities/generateTranscription/index.ts +45 -13
  50. package/src/activities/generateVideo/index.ts +65 -6
  51. package/src/activities/summarize/index.ts +66 -20
  52. package/src/adapter-internals.ts +7 -0
  53. package/src/index.ts +9 -0
  54. package/src/logger/console-logger.ts +49 -0
  55. package/src/logger/internal-logger.ts +107 -0
  56. package/src/logger/resolve.ts +72 -0
  57. package/src/logger/types.ts +78 -0
  58. 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,8 @@ 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'
54
57
  import type { ProviderTool } from '../../tools/provider-tool'
55
58
 
56
59
  // ===========================
@@ -181,6 +184,13 @@ export interface TextActivityOptions<
181
184
  * Can be used to pass request-scoped data (e.g., user ID, request context).
182
185
  */
183
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
184
194
  }
185
195
 
186
196
  // ===========================
@@ -293,7 +303,13 @@ class TextEngine<
293
303
  private middlewareAbortController?: AbortController
294
304
  private terminalHookCalled = false
295
305
 
296
- 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
297
313
  this.adapter = config.adapter
298
314
  this.params = config.params
299
315
  this.systemPrompts = config.params.systemPrompts || []
@@ -341,7 +357,7 @@ class TextEngine<
341
357
  ...(config.middleware || []),
342
358
  stripToSpecMiddleware(),
343
359
  ]
344
- this.middlewareRunner = new MiddlewareRunner(allMiddleware)
360
+ this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger)
345
361
  this.middlewareAbortController = new AbortController()
346
362
  this.middlewareCtx = {
347
363
  requestId: this.requestId,
@@ -393,6 +409,9 @@ class TextEngine<
393
409
 
394
410
  async *run(): AsyncGenerator<StreamChunk> {
395
411
  this.beforeRun()
412
+ this.logger.agentLoop('run started', {
413
+ conversationId: this.middlewareCtx.conversationId,
414
+ })
396
415
 
397
416
  try {
398
417
  // Run initial onConfig (phase = init)
@@ -417,6 +436,10 @@ class TextEngine<
417
436
  return
418
437
  }
419
438
 
439
+ this.logger.agentLoop(`iteration=${this.middlewareCtx.iteration}`, {
440
+ iteration: this.middlewareCtx.iteration,
441
+ })
442
+
420
443
  await this.beginCycle()
421
444
 
422
445
  if (this.cyclePhase === 'processText') {
@@ -438,6 +461,10 @@ class TextEngine<
438
461
  this.endCycle()
439
462
  } while (this.shouldContinue())
440
463
 
464
+ this.logger.agentLoop('run finished', {
465
+ finishReason: this.lastFinishReason,
466
+ })
467
+
441
468
  // Call terminal onFinish hook (skip when waiting for client — stream is paused, not finished)
442
469
  if (!this.terminalHookCalled && this.toolPhase !== 'wait') {
443
470
  this.terminalHookCalled = true
@@ -460,6 +487,10 @@ class TextEngine<
460
487
  })
461
488
  } else {
462
489
  // Genuine error — call onError
490
+ this.logger.errors('chat run failed', {
491
+ error,
492
+ conversationId: this.middlewareCtx.conversationId,
493
+ })
463
494
  await this.middlewareRunner.runOnError(this.middlewareCtx, {
464
495
  error,
465
496
  duration: Date.now() - this.streamStartTime,
@@ -555,6 +586,18 @@ class TextEngine<
555
586
 
556
587
  this.middlewareCtx.phase = 'modelStream'
557
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
+
558
601
  for await (const chunk of this.adapter.chatStream({
559
602
  model: this.params.model,
560
603
  messages: this.messages,
@@ -566,6 +609,7 @@ class TextEngine<
566
609
  request: this.effectiveRequest,
567
610
  modelOptions,
568
611
  systemPrompts: this.systemPrompts,
612
+ logger: this.logger,
569
613
  threadId: this.threadId,
570
614
  runId: this.runIdOverride,
571
615
  })) {
@@ -585,6 +629,7 @@ class TextEngine<
585
629
  chunk,
586
630
  )
587
631
  for (const outputChunk of outputChunks) {
632
+ this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk })
588
633
  yield outputChunk
589
634
  this.middlewareCtx.chunkIndex++
590
635
  }
@@ -741,6 +786,10 @@ class TextEngine<
741
786
  (eventName, data) => this.createCustomEventChunk(eventName, data),
742
787
  {
743
788
  onBeforeToolCall: async (toolCall, tool, args) => {
789
+ this.logger.tools(`phase=before name=${toolCall.function.name}`, {
790
+ name: toolCall.function.name,
791
+ args,
792
+ })
744
793
  const hookCtx = {
745
794
  toolCall,
746
795
  tool,
@@ -754,6 +803,10 @@ class TextEngine<
754
803
  )
755
804
  },
756
805
  onAfterToolCall: async (info) => {
806
+ this.logger.tools(`phase=after name=${info.toolName}`, {
807
+ name: info.toolName,
808
+ result: info.result,
809
+ })
757
810
  await this.middlewareRunner.runOnAfterToolCall(
758
811
  this.middlewareCtx,
759
812
  info,
@@ -894,6 +947,10 @@ class TextEngine<
894
947
  (eventName, data) => this.createCustomEventChunk(eventName, data),
895
948
  {
896
949
  onBeforeToolCall: async (toolCall, tool, args) => {
950
+ this.logger.tools(`phase=before name=${toolCall.function.name}`, {
951
+ name: toolCall.function.name,
952
+ args,
953
+ })
897
954
  const hookCtx = {
898
955
  toolCall,
899
956
  tool,
@@ -907,6 +964,10 @@ class TextEngine<
907
964
  )
908
965
  },
909
966
  onAfterToolCall: async (info) => {
967
+ this.logger.tools(`phase=after name=${info.toolName}`, {
968
+ name: info.toolName,
969
+ result: info.result,
970
+ })
910
971
  await this.middlewareRunner.runOnAfterToolCall(
911
972
  this.middlewareCtx,
912
973
  info,
@@ -1491,18 +1552,22 @@ export function chat<
1491
1552
  async function* runStreamingText(
1492
1553
  options: TextActivityOptions<AnyTextAdapter, undefined, true>,
1493
1554
  ): AsyncIterable<StreamChunk> {
1494
- const { adapter, middleware, context, ...textOptions } = options
1555
+ const { adapter, middleware, context, debug, ...textOptions } = options
1495
1556
  const model = adapter.model
1557
+ const logger = resolveDebugOption(debug)
1496
1558
 
1497
- const engine = new TextEngine({
1498
- adapter,
1499
- params: { ...textOptions, model } as TextOptions<
1500
- Record<string, any>,
1501
- Record<string, any>
1502
- >,
1503
- middleware,
1504
- context,
1505
- })
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
+ )
1506
1571
 
1507
1572
  for await (const chunk of engine.run()) {
1508
1573
  yield chunk
@@ -1533,23 +1598,28 @@ function runNonStreamingText(
1533
1598
  async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
1534
1599
  options: TextActivityOptions<AnyTextAdapter, TSchema, boolean>,
1535
1600
  ): Promise<InferSchemaType<TSchema>> {
1536
- const { adapter, outputSchema, middleware, context, ...textOptions } = options
1601
+ const { adapter, outputSchema, middleware, context, debug, ...textOptions } =
1602
+ options
1537
1603
  const model = adapter.model
1604
+ const logger = resolveDebugOption(debug)
1538
1605
 
1539
1606
  if (!outputSchema) {
1540
1607
  throw new Error('outputSchema is required for structured output')
1541
1608
  }
1542
1609
 
1543
1610
  // Create the engine and run the agentic loop
1544
- const engine = new TextEngine({
1545
- adapter,
1546
- params: { ...textOptions, model } as TextOptions<
1547
- Record<string, unknown>,
1548
- Record<string, unknown>
1549
- >,
1550
- middleware,
1551
- context,
1552
- })
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
+ )
1553
1623
 
1554
1624
  // Consume the stream to run the agentic loop
1555
1625
  for await (const _chunk of engine.run()) {
@@ -1573,6 +1643,17 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
1573
1643
  throw new Error('Failed to convert output schema to JSON Schema')
1574
1644
  }
1575
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
+
1576
1657
  // Call the adapter's structured output method with the conversation context
1577
1658
  // The adapter receives JSON Schema and can apply vendor-specific patches
1578
1659
  const result = await adapter.structuredOutput({
@@ -1580,6 +1661,7 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
1580
1661
  ...structuredTextOptions,
1581
1662
  model,
1582
1663
  messages: finalMessages,
1664
+ logger,
1583
1665
  },
1584
1666
  outputSchema: jsonSchema,
1585
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
  // ===========================
@@ -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 { TranscriptionAdapter } from './adapter'
11
14
  import type { StreamChunk, TranscriptionResult } from '../../types'
12
15
 
@@ -64,6 +67,12 @@ export interface TranscriptionActivityOptions<
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
  // ===========================
@@ -156,10 +165,15 @@ async function runGenerateTranscription<
156
165
  >(
157
166
  options: TranscriptionActivityOptions<TAdapter, boolean>,
158
167
  ): Promise<TranscriptionResult> {
159
- const { adapter, stream: _stream, ...rest } = options
168
+ const { adapter, stream: _stream, debug: _debug, ...rest } = options
160
169
  const model = adapter.model
161
170
  const requestId = createId('transcription')
162
171
  const startTime = Date.now()
172
+ const logger: InternalLogger = resolveDebugOption(options.debug)
173
+ const providerName =
174
+ (adapter as { name?: string; provider?: string }).provider ??
175
+ (adapter as { name?: string }).name ??
176
+ 'unknown'
163
177
 
164
178
  aiEventClient.emit('transcription:request:started', {
165
179
  requestId,
@@ -172,21 +186,39 @@ async function runGenerateTranscription<
172
186
  timestamp: startTime,
173
187
  })
174
188
 
175
- const result = await adapter.transcribe({ ...rest, model })
176
- const duration = Date.now() - startTime
177
-
178
- aiEventClient.emit('transcription:request:completed', {
179
- requestId,
180
- provider: adapter.name,
189
+ logger.request(`activity=generateTranscription provider=${providerName}`, {
190
+ provider: providerName,
181
191
  model,
182
- text: result.text,
183
- language: result.language,
184
- duration,
185
- modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
186
- timestamp: Date.now(),
187
192
  })
188
193
 
189
- return result
194
+ try {
195
+ const result = await adapter.transcribe({ ...rest, model, logger })
196
+ const duration = Date.now() - startTime
197
+
198
+ aiEventClient.emit('transcription:request:completed', {
199
+ requestId,
200
+ provider: adapter.name,
201
+ model,
202
+ text: result.text,
203
+ language: result.language,
204
+ duration,
205
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
206
+ timestamp: Date.now(),
207
+ })
208
+
209
+ logger.output(
210
+ `activity=generateTranscription length=${result.text.length}`,
211
+ { hasText: !!result.text },
212
+ )
213
+
214
+ return result
215
+ } catch (error) {
216
+ logger.errors('generateTranscription activity failed', {
217
+ error,
218
+ source: 'generateTranscription',
219
+ })
220
+ throw error
221
+ }
190
222
  }
191
223
 
192
224
  // ===========================