@tanstack/ai 0.28.0 → 0.31.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 (67) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +10 -0
  2. package/dist/esm/activities/chat/adapter.js +1 -0
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/index.d.ts +3 -2
  5. package/dist/esm/activities/chat/index.js +23 -11
  6. package/dist/esm/activities/chat/index.js.map +1 -1
  7. package/dist/esm/activities/chat/messages.js +1 -1
  8. package/dist/esm/activities/chat/messages.js.map +1 -1
  9. package/dist/esm/activities/chat/middleware/builder.d.ts +46 -0
  10. package/dist/esm/activities/chat/middleware/builder.js +17 -0
  11. package/dist/esm/activities/chat/middleware/builder.js.map +1 -0
  12. package/dist/esm/activities/chat/middleware/capabilities.d.ts +93 -0
  13. package/dist/esm/activities/chat/middleware/capabilities.js +45 -0
  14. package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -0
  15. package/dist/esm/activities/chat/middleware/compose.d.ts +10 -0
  16. package/dist/esm/activities/chat/middleware/compose.js +47 -0
  17. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  18. package/dist/esm/activities/chat/middleware/define.d.ts +20 -0
  19. package/dist/esm/activities/chat/middleware/define.js +7 -0
  20. package/dist/esm/activities/chat/middleware/define.js.map +1 -0
  21. package/dist/esm/activities/chat/middleware/index.d.ts +8 -0
  22. package/dist/esm/activities/chat/middleware/types.d.ts +51 -0
  23. package/dist/esm/activities/chat/middleware/validate.d.ts +19 -0
  24. package/dist/esm/activities/chat/middleware/validate.js +30 -0
  25. package/dist/esm/activities/chat/middleware/validate.js.map +1 -0
  26. package/dist/esm/activities/chat/stream/message-updaters.js +1 -1
  27. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  28. package/dist/esm/activities/chat/stream/processor.d.ts +6 -0
  29. package/dist/esm/activities/chat/stream/processor.js +18 -3
  30. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  31. package/dist/esm/activities/generateVideo/index.d.ts +2 -1
  32. package/dist/esm/activities/generateVideo/index.js +12 -2
  33. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  34. package/dist/esm/index.d.ts +2 -0
  35. package/dist/esm/index.js +6 -0
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/logger/console-logger.d.ts +18 -0
  38. package/dist/esm/logger/console-logger.js +64 -8
  39. package/dist/esm/logger/console-logger.js.map +1 -1
  40. package/dist/esm/logger/types.d.ts +4 -4
  41. package/dist/esm/middlewares/otel.js +40 -12
  42. package/dist/esm/middlewares/otel.js.map +1 -1
  43. package/dist/esm/realtime/index.d.ts +1 -3
  44. package/dist/esm/realtime/index.js.map +1 -1
  45. package/dist/esm/types.d.ts +7 -1
  46. package/package.json +10 -2
  47. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -2
  48. package/skills/ai-core/media-generation/SKILL.md +29 -1
  49. package/src/activities/chat/adapter.ts +11 -0
  50. package/src/activities/chat/index.ts +34 -14
  51. package/src/activities/chat/messages.ts +1 -0
  52. package/src/activities/chat/middleware/builder.ts +109 -0
  53. package/src/activities/chat/middleware/capabilities.ts +162 -0
  54. package/src/activities/chat/middleware/compose.ts +51 -0
  55. package/src/activities/chat/middleware/define.ts +34 -0
  56. package/src/activities/chat/middleware/index.ts +20 -0
  57. package/src/activities/chat/middleware/types.ts +60 -0
  58. package/src/activities/chat/middleware/validate.ts +55 -0
  59. package/src/activities/chat/stream/message-updaters.ts +1 -1
  60. package/src/activities/chat/stream/processor.ts +30 -3
  61. package/src/activities/generateVideo/index.ts +12 -0
  62. package/src/index.ts +14 -0
  63. package/src/logger/console-logger.ts +112 -17
  64. package/src/logger/types.ts +4 -4
  65. package/src/middlewares/otel.ts +57 -12
  66. package/src/realtime/index.ts +1 -3
  67. package/src/types.ts +7 -0
@@ -1,5 +1,86 @@
1
1
  import type { Logger } from './types'
2
2
 
3
+ /**
4
+ * `util.inspect` options used with `console.dir` on Node so deeply nested
5
+ * structures (e.g. provider chunk payloads with `usage`, `output`,
6
+ * `reasoning`, `tools`) render in full instead of truncating to
7
+ * `[Object]` / `[Array]`.
8
+ */
9
+ const DIR_OPTIONS = { depth: null, colors: true } as const
10
+
11
+ /**
12
+ * How `meta` should be rendered on the current runtime:
13
+ *
14
+ * - `dir` — Node. `console.dir(meta, { depth: null, colors: true })` gives a
15
+ * depth-unlimited, colored inspect dump.
16
+ * - `json` — Cloudflare Workers / workerd. workerd never forwards
17
+ * `console.dir` output to the terminal (with or without options), and its
18
+ * own inspect of extra console arguments truncates nested objects, so the
19
+ * payload is appended as circular-safe pretty-printed JSON instead.
20
+ * - `arg` — everything else (browsers, Deno, Bun). `meta` is passed as an
21
+ * extra console argument: devtools keep collapsible object trees and the
22
+ * runtime's inspect handles circular references natively.
23
+ */
24
+ type MetaStrategy = 'dir' | 'json' | 'arg'
25
+
26
+ function resolveMetaStrategy(): MetaStrategy {
27
+ // workerd must be detected before the Node check: under the `nodejs_compat`
28
+ // flag it emulates `process.versions.node`, but still drops `console.dir`.
29
+ try {
30
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- navigator is missing on Node < 21 despite the DOM lib typing it as always present
31
+ if (globalThis.navigator?.userAgent === 'Cloudflare-Workers') return 'json'
32
+ } catch {
33
+ // A locked-down runtime with a throwing `userAgent` getter is not workerd;
34
+ // fall through to the remaining checks rather than crash the log call.
35
+ }
36
+ if (
37
+ typeof process !== 'undefined' &&
38
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- a partial process global (bundler shims) may lack versions
39
+ typeof process.versions?.node === 'string'
40
+ ) {
41
+ return 'dir'
42
+ }
43
+ return 'arg'
44
+ }
45
+
46
+ /**
47
+ * `JSON.stringify` hardened for debug payloads: circular references collapse
48
+ * to `"[Circular]"`, `Error` instances expand to `name`/`message`/`stack`
49
+ * (they would otherwise stringify to `{}`), and `bigint` values become
50
+ * strings (they would otherwise throw). Never throws — falls back to
51
+ * `String(value)` and, if even that coercion throws, a placeholder.
52
+ */
53
+ function stringifyMetaSafely(value: unknown): string {
54
+ const seen = new WeakSet<object>()
55
+ try {
56
+ return JSON.stringify(
57
+ value,
58
+ (_key, entry: unknown) => {
59
+ if (typeof entry === 'bigint') return entry.toString()
60
+ if (entry instanceof Error) {
61
+ return {
62
+ name: entry.name,
63
+ message: entry.message,
64
+ stack: entry.stack,
65
+ }
66
+ }
67
+ if (typeof entry === 'object' && entry !== null) {
68
+ if (seen.has(entry)) return '[Circular]'
69
+ seen.add(entry)
70
+ }
71
+ return entry
72
+ },
73
+ 2,
74
+ )
75
+ } catch {
76
+ try {
77
+ return String(value)
78
+ } catch {
79
+ return '[Unserializable meta]'
80
+ }
81
+ }
82
+ }
83
+
3
84
  /**
4
85
  * Default `Logger` implementation that routes each level to the matching
5
86
  * `console` method:
@@ -9,41 +90,55 @@ import type { Logger } from './types'
9
90
  * - `warn` → `console.warn`
10
91
  * - `error` → `console.error`
11
92
  *
12
- * When a `meta` object is supplied, the message is logged first and the meta
13
- * object is then printed via `console.dir(meta, { depth: null, colors: true })`
14
- * so deeply nested structures (e.g. provider chunk payloads with `usage`,
15
- * `output`, `reasoning`, `tools`) render in full instead of truncating to
16
- * `[Object]` / `[Array]`. On Node this produces a depth-unlimited inspect
17
- * dump; browsers present the object as an interactive tree (extra options
18
- * are ignored).
93
+ * When a `meta` object is supplied it is rendered with the strategy that
94
+ * actually surfaces it on the current runtime (see {@link MetaStrategy}):
95
+ * depth-unlimited `console.dir` on Node, circular-safe JSON on Cloudflare
96
+ * Workers, and an extra console argument everywhere else.
19
97
  *
20
98
  * This is the logger used when `debug` is enabled on any activity and no
21
99
  * custom `logger` is supplied via `debug: { logger }`.
22
100
  */
23
- const DIR_OPTIONS = { depth: null, colors: true } as const
24
-
25
101
  export class ConsoleLogger implements Logger {
26
102
  /** Log a debug-level message; forwards to `console.debug`. */
27
103
  debug(message: string, meta?: Record<string, unknown>): void {
28
- console.debug(message)
29
- if (meta !== undefined) console.dir(meta, DIR_OPTIONS)
104
+ this.emit('debug', message, meta)
30
105
  }
31
106
 
32
107
  /** Log an info-level message; forwards to `console.info`. */
33
108
  info(message: string, meta?: Record<string, unknown>): void {
34
- console.info(message)
35
- if (meta !== undefined) console.dir(meta, DIR_OPTIONS)
109
+ this.emit('info', message, meta)
36
110
  }
37
111
 
38
112
  /** Log a warning-level message; forwards to `console.warn`. */
39
113
  warn(message: string, meta?: Record<string, unknown>): void {
40
- console.warn(message)
41
- if (meta !== undefined) console.dir(meta, DIR_OPTIONS)
114
+ this.emit('warn', message, meta)
42
115
  }
43
116
 
44
117
  /** Log an error-level message; forwards to `console.error`. */
45
118
  error(message: string, meta?: Record<string, unknown>): void {
46
- console.error(message)
47
- if (meta !== undefined) console.dir(meta, DIR_OPTIONS)
119
+ this.emit('error', message, meta)
120
+ }
121
+
122
+ private emit(
123
+ level: 'debug' | 'info' | 'warn' | 'error',
124
+ message: string,
125
+ meta?: Record<string, unknown>,
126
+ ): void {
127
+ if (meta === undefined) {
128
+ console[level](message)
129
+ return
130
+ }
131
+ switch (resolveMetaStrategy()) {
132
+ case 'dir':
133
+ console[level](message)
134
+ console.dir(meta, DIR_OPTIONS)
135
+ break
136
+ case 'json':
137
+ console[level](`${message}\n${stringifyMetaSafely(meta)}`)
138
+ break
139
+ case 'arg':
140
+ console[level](message, meta)
141
+ break
142
+ }
48
143
  }
49
144
  }
@@ -4,22 +4,22 @@
4
4
  export interface Logger {
5
5
  /**
6
6
  * Called for chunk-level diagnostic output (raw provider chunks, per-chunk output, agent-loop iteration markers).
7
- * @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; console-based loggers pass it as the second argument to `console.<level>`.
7
+ * @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; the default `ConsoleLogger` renders it with a runtime-appropriate strategy (depth-unlimited `console.dir` on Node, JSON appended to the message on Cloudflare Workers, a second `console.<level>` argument elsewhere).
8
8
  */
9
9
  debug: (message: string, meta?: Record<string, unknown>) => void
10
10
  /**
11
11
  * Called for notable informational events (outgoing requests, tool invocations, middleware transitions).
12
- * @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; console-based loggers pass it as the second argument to `console.<level>`.
12
+ * @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; the default `ConsoleLogger` renders it with a runtime-appropriate strategy (depth-unlimited `console.dir` on Node, JSON appended to the message on Cloudflare Workers, a second `console.<level>` argument elsewhere).
13
13
  */
14
14
  info: (message: string, meta?: Record<string, unknown>) => void
15
15
  /**
16
16
  * Called for notable warnings that don't halt execution (deprecations, recoverable anomalies).
17
- * @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; console-based loggers pass it as the second argument to `console.<level>`.
17
+ * @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; the default `ConsoleLogger` renders it with a runtime-appropriate strategy (depth-unlimited `console.dir` on Node, JSON appended to the message on Cloudflare Workers, a second `console.<level>` argument elsewhere).
18
18
  */
19
19
  warn: (message: string, meta?: Record<string, unknown>) => void
20
20
  /**
21
21
  * Called for caught exceptions throughout the pipeline.
22
- * @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; console-based loggers pass it as the second argument to `console.<level>`.
22
+ * @param meta Structured data forwarded to the underlying logger. Loggers like pino will preserve this as a structured record; the default `ConsoleLogger` renders it with a runtime-appropriate strategy (depth-unlimited `console.dir` on Node, JSON appended to the message on Cloudflare Workers, a second `console.<level>` argument elsewhere).
23
23
  */
24
24
  error: (message: string, meta?: Record<string, unknown>) => void
25
25
  }
@@ -20,6 +20,7 @@ import type {
20
20
  ChatMiddleware,
21
21
  ChatMiddlewareContext,
22
22
  } from '../activities/chat/middleware/types'
23
+ import type { TokenUsage } from '../types'
23
24
 
24
25
  /**
25
26
  * Scope (role) of an OTel span emitted by this middleware.
@@ -179,6 +180,59 @@ function firstNumber(...candidates: Array<unknown>): number | undefined {
179
180
  return undefined
180
181
  }
181
182
 
183
+ /**
184
+ * Build the full set of `gen_ai.usage.*` span attributes from a `TokenUsage`.
185
+ *
186
+ * Beyond input/output tokens, this emits provider-reported cost, total tokens,
187
+ * cache and reasoning breakdowns, and duration-based billing — every field is
188
+ * guarded so spans stay clean when a provider doesn't report it. Cache and
189
+ * reasoning use the official GenAI semconv names; `gen_ai.usage.cost` and
190
+ * `gen_ai.usage.total_tokens` are de-facto extensions consumed by backends
191
+ * like PostHog (which otherwise re-derive cost from their own price tables,
192
+ * losing cache discounts and gateway markup). Fields with no semconv or
193
+ * de-facto convention (`costDetails`, `durationSeconds`) are
194
+ * TanStack-namespaced. Deliberately not emitted: `unitsBilled`,
195
+ * `providerUsageDetails`, and the per-modality token breakdowns — those are
196
+ * media-oriented; media-activity observability is tracked in #720.
197
+ */
198
+ function usageAttributes(usage: TokenUsage): Record<string, AttributeValue> {
199
+ const attrs: Record<string, AttributeValue> = {
200
+ 'gen_ai.usage.input_tokens': usage.promptTokens,
201
+ 'gen_ai.usage.output_tokens': usage.completionTokens,
202
+ }
203
+ const optional: Array<[key: string, value: unknown]> = [
204
+ ['gen_ai.usage.total_tokens', usage.totalTokens],
205
+ ['gen_ai.usage.cost', usage.cost],
206
+ [
207
+ 'gen_ai.usage.cache_read.input_tokens',
208
+ usage.promptTokensDetails?.cachedTokens,
209
+ ],
210
+ [
211
+ 'gen_ai.usage.cache_creation.input_tokens',
212
+ usage.promptTokensDetails?.cacheWriteTokens,
213
+ ],
214
+ [
215
+ 'gen_ai.usage.reasoning.output_tokens',
216
+ usage.completionTokensDetails?.reasoningTokens,
217
+ ],
218
+ ['tanstack.ai.usage.duration_seconds', usage.durationSeconds],
219
+ ['tanstack.ai.usage.upstream_cost', usage.costDetails?.upstreamCost],
220
+ [
221
+ 'tanstack.ai.usage.upstream_input_cost',
222
+ usage.costDetails?.upstreamInputCost,
223
+ ],
224
+ [
225
+ 'tanstack.ai.usage.upstream_output_cost',
226
+ usage.costDetails?.upstreamOutputCost,
227
+ ],
228
+ ]
229
+ for (const [key, value] of optional) {
230
+ const num = firstNumber(value)
231
+ if (num !== undefined) attrs[key] = num
232
+ }
233
+ return attrs
234
+ }
235
+
182
236
  function errorMessage(err: unknown): string | undefined {
183
237
  if (err instanceof Error) return err.message
184
238
  if (typeof err === 'string') return err
@@ -524,10 +578,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
524
578
  // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the
525
579
  // canonical place for the metric. Recording in both would double-count.
526
580
  if (chunk.usage) {
527
- span.setAttributes({
528
- 'gen_ai.usage.input_tokens': chunk.usage.promptTokens,
529
- 'gen_ai.usage.output_tokens': chunk.usage.completionTokens,
530
- })
581
+ span.setAttributes(usageAttributes(chunk.usage))
531
582
  }
532
583
 
533
584
  if (captureContent && state.assistantTextBuffer.length > 0) {
@@ -584,10 +635,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
584
635
  }
585
636
 
586
637
  const span = state.currentIterationSpan ?? state.rootSpan
587
- span.setAttributes({
588
- 'gen_ai.usage.input_tokens': usage.promptTokens,
589
- 'gen_ai.usage.output_tokens': usage.completionTokens,
590
- })
638
+ span.setAttributes(usageAttributes(usage))
591
639
  })
592
640
  },
593
641
 
@@ -905,10 +953,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
905
953
  }
906
954
 
907
955
  if (info.usage) {
908
- state.rootSpan.setAttributes({
909
- 'gen_ai.usage.input_tokens': info.usage.promptTokens,
910
- 'gen_ai.usage.output_tokens': info.usage.completionTokens,
911
- })
956
+ state.rootSpan.setAttributes(usageAttributes(info.usage))
912
957
  }
913
958
  if (info.finishReason) {
914
959
  state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [
@@ -22,9 +22,7 @@ export type * from './types'
22
22
  * .handler(async () => {
23
23
  * return realtimeToken({
24
24
  * adapter: openaiRealtimeToken({
25
- * model: 'gpt-4o-realtime-preview',
26
- * voice: 'alloy',
27
- * instructions: 'You are a helpful assistant...',
25
+ * model: 'gpt-realtime',
28
26
  * }),
29
27
  * })
30
28
  * })
package/src/types.ts CHANGED
@@ -51,6 +51,7 @@ export type ToolCallState =
51
51
  | 'approval-requested' // Waiting for user approval
52
52
  | 'approval-responded' // User has approved/denied
53
53
  | 'complete' // Result is complete
54
+ | 'error' // Tool execution failed (terminal)
54
55
 
55
56
  /**
56
57
  * Tool result states - track the lifecycle of a tool result
@@ -1656,6 +1657,12 @@ export interface VideoUrlResult {
1656
1657
  url: string
1657
1658
  /** When the URL expires, if applicable */
1658
1659
  expiresAt?: Date
1660
+ /**
1661
+ * Usage information for the completed generation, when the adapter can report
1662
+ * it. For usage-based providers (e.g. fal) this carries `unitsBilled` — the
1663
+ * real billed quantity — so consumers can compute exact cost.
1664
+ */
1665
+ usage?: TokenUsage
1659
1666
  }
1660
1667
 
1661
1668
  // ============================================================================