@tanstack/ai 0.23.1 → 0.25.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 (74) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +3 -1
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +33 -9
  4. package/dist/esm/activities/chat/index.js +19 -9
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/messages.js +2 -1
  7. package/dist/esm/activities/chat/messages.js.map +1 -1
  8. package/dist/esm/activities/chat/middleware/compose.d.ts +14 -14
  9. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  10. package/dist/esm/activities/chat/middleware/types.d.ts +21 -21
  11. package/dist/esm/activities/chat/runtime-context-types.d.ts +43 -0
  12. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  13. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  14. package/dist/esm/activities/chat/stream/processor.d.ts +1 -0
  15. package/dist/esm/activities/chat/stream/processor.js +35 -12
  16. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  17. package/dist/esm/activities/chat/tools/tool-calls.d.ts +15 -5
  18. package/dist/esm/activities/chat/tools/tool-calls.js +59 -19
  19. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  20. package/dist/esm/activities/chat/tools/tool-definition.d.ts +12 -8
  21. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  22. package/dist/esm/activities/error-payload.d.ts +26 -0
  23. package/dist/esm/activities/error-payload.js +12 -1
  24. package/dist/esm/activities/error-payload.js.map +1 -1
  25. package/dist/esm/activities/generateAudio/index.js +9 -0
  26. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  27. package/dist/esm/activities/generateSpeech/index.js +9 -0
  28. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  29. package/dist/esm/adapter-internals.d.ts +1 -1
  30. package/dist/esm/adapter-internals.js +3 -2
  31. package/dist/esm/client.d.ts +1 -1
  32. package/dist/esm/client.js +3 -1
  33. package/dist/esm/client.js.map +1 -1
  34. package/dist/esm/index.d.ts +3 -1
  35. package/dist/esm/index.js +9 -1
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/tool-registry.d.ts +7 -7
  38. package/dist/esm/tool-registry.js +1 -1
  39. package/dist/esm/tool-registry.js.map +1 -1
  40. package/dist/esm/types.d.ts +56 -59
  41. package/dist/esm/utilities/ag-ui-wire.js +1 -1
  42. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  43. package/dist/esm/utilities/chat-params.d.ts +8 -3
  44. package/dist/esm/utilities/chat-params.js +6 -2
  45. package/dist/esm/utilities/chat-params.js.map +1 -1
  46. package/dist/esm/utilities/tool-result.d.ts +21 -0
  47. package/dist/esm/utilities/tool-result.js +37 -0
  48. package/dist/esm/utilities/tool-result.js.map +1 -0
  49. package/dist/esm/utilities/usage.d.ts +31 -0
  50. package/dist/esm/utilities/usage.js +11 -0
  51. package/dist/esm/utilities/usage.js.map +1 -0
  52. package/package.json +2 -2
  53. package/src/activities/chat/adapter.ts +3 -0
  54. package/src/activities/chat/index.ts +219 -47
  55. package/src/activities/chat/messages.ts +2 -1
  56. package/src/activities/chat/middleware/compose.ts +23 -17
  57. package/src/activities/chat/middleware/types.ts +21 -21
  58. package/src/activities/chat/runtime-context-types.ts +68 -0
  59. package/src/activities/chat/stream/message-updaters.ts +2 -1
  60. package/src/activities/chat/stream/processor.ts +48 -8
  61. package/src/activities/chat/tools/tool-calls.ts +138 -43
  62. package/src/activities/chat/tools/tool-definition.ts +25 -31
  63. package/src/activities/error-payload.ts +44 -0
  64. package/src/activities/generateAudio/index.ts +10 -0
  65. package/src/activities/generateSpeech/index.ts +10 -0
  66. package/src/adapter-internals.ts +4 -1
  67. package/src/client.ts +5 -1
  68. package/src/index.ts +10 -0
  69. package/src/tool-registry.ts +16 -14
  70. package/src/types.ts +118 -79
  71. package/src/utilities/ag-ui-wire.ts +4 -1
  72. package/src/utilities/chat-params.ts +22 -7
  73. package/src/utilities/tool-result.ts +60 -0
  74. package/src/utilities/usage.ts +41 -0
@@ -1,5 +1,8 @@
1
+ import { normalizeToolResult } from '../../../utilities/tool-result'
1
2
  import { isStandardSchema, parseWithStandardSchema } from './schema-converter'
2
3
  import type {
4
+ AnyTool,
5
+ ContentPart,
3
6
  CustomEvent,
4
7
  ModelMessage,
5
8
  RunFinishedEvent,
@@ -9,11 +12,18 @@ import type {
9
12
  ToolCallEndEvent,
10
13
  ToolCallStartEvent,
11
14
  ToolExecutionContext,
15
+ ToolOutputState,
12
16
  } from '../../../types'
13
17
  import type {
14
18
  AfterToolCallInfo,
15
19
  BeforeToolCallDecision,
16
20
  } from '../middleware/types'
21
+ import type {
22
+ ContextFromTool,
23
+ DefinedContext,
24
+ MergeContext,
25
+ UnionToIntersection,
26
+ } from '../runtime-context-types'
17
27
 
18
28
  function safeJsonParse(value: string): unknown {
19
29
  try {
@@ -46,6 +56,36 @@ export class MiddlewareAbortError extends Error {
46
56
  }
47
57
  }
48
58
 
59
+ // The leaf context-inference primitives (ContextFromTool, MergeContext,
60
+ // UnionToIntersection, DefinedContext) are shared with the chat activity
61
+ // options layer — see ../runtime-context-types.
62
+ type RequiredContextFromToolUnion<T> = T extends unknown
63
+ ? undefined extends ContextFromTool<T>
64
+ ? never
65
+ : ContextFromTool<T>
66
+ : never
67
+
68
+ type ContextFromToolUnion<T> = [
69
+ UnionToIntersection<DefinedContext<ContextFromTool<T>>>,
70
+ ] extends [never]
71
+ ? unknown
72
+ : [RequiredContextFromToolUnion<T>] extends [never]
73
+ ? UnionToIntersection<DefinedContext<ContextFromTool<T>>> | undefined
74
+ : UnionToIntersection<DefinedContext<ContextFromTool<T>>>
75
+
76
+ type ContextFromTools<TTools> = TTools extends readonly [
77
+ infer THead,
78
+ ...infer TTail,
79
+ ]
80
+ ? MergeContext<ContextFromTool<THead>, ContextFromTools<TTail>>
81
+ : TTools extends ReadonlyArray<infer TTool>
82
+ ? ContextFromToolUnion<TTool>
83
+ : unknown
84
+
85
+ type ExecuteToolsContextArgs<TContext> = undefined extends TContext
86
+ ? [userContext?: TContext]
87
+ : [userContext: TContext]
88
+
49
89
  /**
50
90
  * Manages tool call accumulation and execution for the chat() method's automatic tool execution loop.
51
91
  *
@@ -80,11 +120,22 @@ export class MiddlewareAbortError extends Error {
80
120
  * }
81
121
  * ```
82
122
  */
83
- export class ToolCallManager {
123
+ export class ToolCallManager<
124
+ TToolsOrContext = ReadonlyArray<AnyTool>,
125
+ TContext = TToolsOrContext extends ReadonlyArray<AnyTool>
126
+ ? ContextFromTools<TToolsOrContext>
127
+ : TToolsOrContext,
128
+ > {
84
129
  private readonly toolCallsMap = new Map<number, ToolCall>()
85
- private readonly tools: ReadonlyArray<Tool>
86
-
87
- constructor(tools: ReadonlyArray<Tool>) {
130
+ private readonly tools: TToolsOrContext extends ReadonlyArray<AnyTool>
131
+ ? TToolsOrContext
132
+ : ReadonlyArray<AnyTool>
133
+
134
+ constructor(
135
+ tools: TToolsOrContext extends ReadonlyArray<AnyTool>
136
+ ? TToolsOrContext
137
+ : ReadonlyArray<AnyTool>,
138
+ ) {
88
139
  this.tools = tools
89
140
  }
90
141
 
@@ -161,14 +212,18 @@ export class ToolCallManager {
161
212
  */
162
213
  async *executeTools(
163
214
  finishEvent: RunFinishedEvent,
215
+ ...contextArgs: ExecuteToolsContextArgs<TContext>
164
216
  ): AsyncGenerator<ToolCallEndEvent, Array<ModelMessage>, void> {
165
217
  const toolCallsArray = this.getToolCalls()
166
218
  const toolResults: Array<ModelMessage> = []
219
+ const hasRuntimeContext = contextArgs.length > 0
220
+ const userContext = contextArgs[0]
167
221
 
168
222
  for (const toolCall of toolCallsArray) {
169
223
  const tool = this.tools.find((t) => t.name === toolCall.function.name)
170
224
 
171
- let toolResultContent: string
225
+ let toolResultContent: string | Array<ContentPart>
226
+ let toolResultState: ToolOutputState | undefined
172
227
  if (tool?.execute) {
173
228
  try {
174
229
  // Parse arguments (normalize null/non-object to {} for empty tool_use blocks)
@@ -199,15 +254,21 @@ export class ToolCallManager {
199
254
  }
200
255
 
201
256
  // Execute the tool
202
- let result = await tool.execute(args)
203
-
204
- // Validate output against outputSchema if provided (for Standard Schema compliant schemas)
205
- if (
206
- tool.outputSchema &&
207
- isStandardSchema(tool.outputSchema) &&
208
- result !== undefined &&
209
- result !== null
210
- ) {
257
+ const executionContext = {
258
+ toolCallId: toolCall.id,
259
+ context: userContext,
260
+ emitCustomEvent: () => {},
261
+ } as ToolExecutionContext<TContext>
262
+ let result = hasRuntimeContext
263
+ ? await tool.execute(args, executionContext)
264
+ : await tool.execute(args)
265
+
266
+ // Validate output against outputSchema if provided (for Standard
267
+ // Schema compliant schemas). Unlike the previous implementation we
268
+ // intentionally validate `undefined`/`null` results too, so a tool
269
+ // whose schema forbids them surfaces a validation error instead of
270
+ // silently passing — the schema itself decides whether they're valid.
271
+ if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
211
272
  try {
212
273
  result = parseWithStandardSchema(tool.outputSchema, result)
213
274
  } catch (validationError: unknown) {
@@ -221,13 +282,13 @@ export class ToolCallManager {
221
282
  }
222
283
  }
223
284
 
224
- toolResultContent =
225
- typeof result === 'string' ? result : JSON.stringify(result)
285
+ toolResultContent = normalizeToolResult(result)
226
286
  } catch (error: unknown) {
227
287
  // If tool execution fails, add error message
228
288
  const message =
229
289
  error instanceof Error ? error.message : 'Unknown error'
230
290
  toolResultContent = `Error executing tool: ${message}`
291
+ toolResultState = 'output-error'
231
292
  }
232
293
  } else {
233
294
  // Tool doesn't have execute function, add placeholder
@@ -243,6 +304,7 @@ export class ToolCallManager {
243
304
  model: finishEvent.model,
244
305
  timestamp: Date.now(),
245
306
  result: toolResultContent,
307
+ ...(toolResultState !== undefined && { state: toolResultState }),
246
308
  } as ToolCallEndEvent
247
309
 
248
310
  // Add tool result message
@@ -370,7 +432,7 @@ async function applyBeforeToolCallDecision(
370
432
  result:
371
433
  typeof skipResult === 'string'
372
434
  ? safeJsonParse(skipResult)
373
- : skipResult || null,
435
+ : (skipResult ?? null),
374
436
  duration: 0,
375
437
  })
376
438
  if (middlewareHooks.onAfterToolCall) {
@@ -394,12 +456,12 @@ async function applyBeforeToolCallDecision(
394
456
  * Execute a server-side tool with event polling, output validation, and middleware hooks.
395
457
  * Yields CustomEvent chunks during execution and pushes the result to the results array.
396
458
  */
397
- async function* executeServerTool(
459
+ async function* executeServerTool<TContext = unknown>(
398
460
  toolCall: ToolCall,
399
- tool: Tool,
461
+ tool: AnyTool,
400
462
  toolName: string,
401
463
  input: unknown,
402
- context: ToolExecutionContext,
464
+ context: ToolExecutionContext<TContext>,
403
465
  pendingEvents: Array<CustomEvent>,
404
466
  results: Array<ToolResult>,
405
467
  middlewareHooks?: ToolExecutionMiddlewareHooks,
@@ -419,18 +481,14 @@ async function* executeServerTool(
419
481
  yield pendingEvent
420
482
  }
421
483
 
422
- // Validate output against outputSchema if provided
423
- if (
424
- tool.outputSchema &&
425
- isStandardSchema(tool.outputSchema) &&
426
- result !== undefined &&
427
- result !== null
428
- ) {
484
+ // Validate output against outputSchema if provided. Validates
485
+ // `undefined`/`null` too — the schema decides whether they're valid.
486
+ if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
429
487
  result = parseWithStandardSchema(tool.outputSchema, result)
430
488
  }
431
489
 
432
490
  const finalResult =
433
- typeof result === 'string' ? safeJsonParse(result) : result || null
491
+ typeof result === 'string' ? safeJsonParse(result) : (result ?? null)
434
492
 
435
493
  results.push({
436
494
  toolCallId: toolCall.id,
@@ -486,6 +544,35 @@ async function* executeServerTool(
486
544
  }
487
545
  }
488
546
 
547
+ function buildClientToolResult(
548
+ toolCallId: string,
549
+ toolName: string,
550
+ tool: AnyTool,
551
+ rawResult: unknown,
552
+ ): ToolResult {
553
+ try {
554
+ let result = rawResult
555
+ if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
556
+ result = parseWithStandardSchema(tool.outputSchema, result)
557
+ }
558
+
559
+ return {
560
+ toolCallId,
561
+ toolName,
562
+ result:
563
+ typeof result === 'string' ? safeJsonParse(result) : (result ?? null),
564
+ }
565
+ } catch (error: unknown) {
566
+ const message = error instanceof Error ? error.message : 'Validation failed'
567
+ return {
568
+ toolCallId,
569
+ toolName,
570
+ result: { error: message },
571
+ state: 'output-error',
572
+ }
573
+ }
574
+ }
575
+
489
576
  /**
490
577
  * Execute tool calls based on their configuration.
491
578
  * Yields CustomEvent chunks during tool execution for real-time progress updates.
@@ -501,9 +588,9 @@ async function* executeServerTool(
501
588
  * @param clientResults - Map of client-side execution results (toolCallId -> result)
502
589
  * @param createCustomEventChunk - Factory to create CustomEvent chunks (optional)
503
590
  */
504
- export async function* executeToolCalls(
591
+ export async function* executeToolCalls<TContext = unknown>(
505
592
  toolCalls: Array<ToolCall>,
506
- tools: ReadonlyArray<Tool>,
593
+ tools: ReadonlyArray<AnyTool>,
507
594
  approvals: Map<string, boolean> = new Map(),
508
595
  clientResults: Map<string, any> = new Map(),
509
596
  createCustomEventChunk?: (
@@ -511,13 +598,14 @@ export async function* executeToolCalls(
511
598
  value: Record<string, any>,
512
599
  ) => CustomEvent,
513
600
  middlewareHooks?: ToolExecutionMiddlewareHooks,
601
+ userContext?: TContext,
514
602
  ): AsyncGenerator<CustomEvent, ExecuteToolCallsResult, void> {
515
603
  const results: Array<ToolResult> = []
516
604
  const needsApproval: Array<ApprovalRequest> = []
517
605
  const needsClientExecution: Array<ClientToolRequest> = []
518
606
 
519
607
  // Create tool lookup map
520
- const toolMap = new Map<string, Tool>()
608
+ const toolMap = new Map<string, AnyTool>()
521
609
  for (const tool of tools) {
522
610
  toolMap.set(tool.name, tool)
523
611
  }
@@ -588,8 +676,9 @@ export async function* executeToolCalls(
588
676
 
589
677
  // Create a ToolExecutionContext for this tool call with event emission
590
678
  const pendingEvents: Array<CustomEvent> = []
591
- const context: ToolExecutionContext = {
679
+ const context = {
592
680
  toolCallId: toolCall.id,
681
+ context: userContext,
593
682
  emitCustomEvent: (eventName: string, value: Record<string, any>) => {
594
683
  if (createCustomEventChunk) {
595
684
  pendingEvents.push(
@@ -600,7 +689,7 @@ export async function* executeToolCalls(
600
689
  )
601
690
  }
602
691
  },
603
- }
692
+ } as ToolExecutionContext<TContext>
604
693
 
605
694
  // CASE 1: Client-side tool (no execute function)
606
695
  if (!tool.execute) {
@@ -615,11 +704,14 @@ export async function* executeToolCalls(
615
704
  if (approved) {
616
705
  // Approved - check if client has executed
617
706
  if (clientResults.has(toolCall.id)) {
618
- results.push({
619
- toolCallId: toolCall.id,
620
- toolName,
621
- result: clientResults.get(toolCall.id),
622
- })
707
+ results.push(
708
+ buildClientToolResult(
709
+ toolCall.id,
710
+ toolName,
711
+ tool,
712
+ clientResults.get(toolCall.id),
713
+ ),
714
+ )
623
715
  } else {
624
716
  // Approved but not executed yet - request client execution
625
717
  needsClientExecution.push({
@@ -649,11 +741,14 @@ export async function* executeToolCalls(
649
741
  } else {
650
742
  // No approval needed - check if client has executed
651
743
  if (clientResults.has(toolCall.id)) {
652
- results.push({
653
- toolCallId: toolCall.id,
654
- toolName,
655
- result: clientResults.get(toolCall.id),
656
- })
744
+ results.push(
745
+ buildClientToolResult(
746
+ toolCall.id,
747
+ toolName,
748
+ tool,
749
+ clientResults.get(toolCall.id),
750
+ ),
751
+ )
657
752
  } else {
658
753
  // Request client execution
659
754
  needsClientExecution.push({
@@ -1,10 +1,9 @@
1
1
  import type { StandardJSONSchemaV1 } from '@standard-schema/spec'
2
2
  import type {
3
- InferSchemaType,
4
3
  JSONSchema,
5
4
  SchemaInput,
6
5
  Tool,
7
- ToolExecutionContext,
6
+ ToolExecuteFunction,
8
7
  } from '../../../types'
9
8
 
10
9
  /**
@@ -14,7 +13,8 @@ export interface ServerTool<
14
13
  TInput extends SchemaInput = SchemaInput,
15
14
  TOutput extends SchemaInput = SchemaInput,
16
15
  TName extends string = string,
17
- > extends Tool<TInput, TOutput, TName> {
16
+ TContext = unknown,
17
+ > extends Tool<TInput, TOutput, TName, TContext> {
18
18
  __toolSide: 'server'
19
19
  }
20
20
 
@@ -25,6 +25,7 @@ export interface ClientTool<
25
25
  TInput extends SchemaInput = SchemaInput,
26
26
  TOutput extends SchemaInput = SchemaInput,
27
27
  TName extends string = string,
28
+ TContext = unknown,
28
29
  > {
29
30
  __toolSide: 'client'
30
31
  name: TName
@@ -39,9 +40,7 @@ export interface ClientTool<
39
40
  needsApproval?: boolean
40
41
  lazy?: boolean
41
42
  metadata?: Record<string, unknown>
42
- execute?: (
43
- args: InferSchemaType<TInput>,
44
- ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>
43
+ execute?: ToolExecuteFunction<TInput, TOutput, TContext>
45
44
  }
46
45
 
47
46
  /**
@@ -51,7 +50,8 @@ export interface ToolDefinitionInstance<
51
50
  TInput extends SchemaInput = SchemaInput,
52
51
  TOutput extends SchemaInput = SchemaInput,
53
52
  TName extends string = string,
54
- > extends Tool<TInput, TOutput, TName> {
53
+ TContext = unknown,
54
+ > extends Tool<TInput, TOutput, TName, TContext> {
55
55
  __toolSide: 'definition'
56
56
  }
57
57
 
@@ -59,8 +59,12 @@ export interface ToolDefinitionInstance<
59
59
  * Union type for any kind of client-side tool (client tool or definition)
60
60
  */
61
61
  export type AnyClientTool =
62
- | ClientTool<SchemaInput, SchemaInput>
63
- | ToolDefinitionInstance<SchemaInput, SchemaInput>
62
+ | (Omit<ClientTool<any, any, string, any>, 'execute'> & {
63
+ execute?: ((args: any, context?: any) => any) | undefined
64
+ })
65
+ | (Omit<ToolDefinitionInstance<any, any, string, any>, 'execute'> & {
66
+ execute?: ((args: any, context?: any) => any) | undefined
67
+ })
64
68
 
65
69
  /**
66
70
  * Extract the tool name as a literal type
@@ -117,21 +121,16 @@ export interface ToolDefinition<
117
121
  /**
118
122
  * Create a server-side tool with execute function
119
123
  */
120
- server: (
121
- execute: (
122
- args: InferSchemaType<TInput>,
123
- context?: ToolExecutionContext,
124
- ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>,
125
- ) => ServerTool<TInput, TOutput, TName>
124
+ server: <TContext = unknown>(
125
+ execute: ToolExecuteFunction<TInput, TOutput, TContext>,
126
+ ) => ServerTool<TInput, TOutput, TName, TContext>
126
127
 
127
128
  /**
128
129
  * Create a client-side tool with optional execute function
129
130
  */
130
- client: (
131
- execute?: (
132
- args: InferSchemaType<TInput>,
133
- ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>,
134
- ) => ClientTool<TInput, TOutput, TName>
131
+ client: <TContext = unknown>(
132
+ execute?: ToolExecuteFunction<TInput, TOutput, TContext>,
133
+ ) => ClientTool<TInput, TOutput, TName, TContext>
135
134
  }
136
135
 
137
136
  /**
@@ -199,12 +198,9 @@ export function toolDefinition<
199
198
  const definition: ToolDefinition<TInput, TOutput, TName> = {
200
199
  __toolSide: 'definition',
201
200
  ...config,
202
- server(
203
- execute: (
204
- args: InferSchemaType<TInput>,
205
- context?: ToolExecutionContext,
206
- ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>,
207
- ): ServerTool<TInput, TOutput, TName> {
201
+ server<TContext = unknown>(
202
+ execute: ToolExecuteFunction<TInput, TOutput, TContext>,
203
+ ): ServerTool<TInput, TOutput, TName, TContext> {
208
204
  return {
209
205
  __toolSide: 'server',
210
206
  ...config,
@@ -212,11 +208,9 @@ export function toolDefinition<
212
208
  }
213
209
  },
214
210
 
215
- client(
216
- execute?: (
217
- args: InferSchemaType<TInput>,
218
- ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>,
219
- ): ClientTool<TInput, TOutput, TName> {
211
+ client<TContext = unknown>(
212
+ execute?: ToolExecuteFunction<TInput, TOutput, TContext>,
213
+ ): ClientTool<TInput, TOutput, TName, TContext> {
220
214
  return {
221
215
  __toolSide: 'client',
222
216
  ...config,
@@ -62,3 +62,47 @@ export function toRunErrorPayload(
62
62
  }
63
63
  return { message: fallbackMessage, code: undefined }
64
64
  }
65
+
66
+ /**
67
+ * Extract the provider's *structured error body* from a thrown value, to attach
68
+ * as the AG-UI `rawEvent` on a RUN_ERROR event. This is the recoverable upstream
69
+ * detail (provider name, the upstream model's error JSON, rate-limit/overload
70
+ * codes, etc.) that `toRunErrorPayload`'s `{ message, code }` deliberately drops.
71
+ *
72
+ * Security boundary: only known provider-response-body fields are forwarded —
73
+ * never the raw SDK exception object, which can carry request metadata such as
74
+ * auth headers or request ids. The recognized sources, in priority order:
75
+ *
76
+ * - `error.rawEvent` — a provider body an adapter attached explicitly (e.g. the
77
+ * OpenRouter mid-stream `chunk.error`).
78
+ * - `error.error` (object) — the parsed provider response body exposed by SDK
79
+ * `APIError` instances (OpenAI/Anthropic `{ type, message, code, param }`,
80
+ * OpenRouter typed errors whose `.error` carries `.metadata`). This is
81
+ * provider-shaped data, distinct from `.headers` / `.request_id`.
82
+ * - `error.metadata` — OpenRouter's `provider_name` + raw upstream body, when
83
+ * surfaced directly on the thrown error.
84
+ *
85
+ * Returns `undefined` when no structured provider body is present, so callers
86
+ * omit the field entirely rather than setting it to `null`:
87
+ *
88
+ * const rawEvent = toRunErrorRawEvent(error)
89
+ * yield { type: EventType.RUN_ERROR, ..., ...(rawEvent !== undefined && { rawEvent }) }
90
+ */
91
+ export function toRunErrorRawEvent(error: unknown): unknown {
92
+ if (!error || typeof error !== 'object') return undefined
93
+ const e = error as {
94
+ rawEvent?: unknown
95
+ error?: unknown
96
+ metadata?: unknown
97
+ }
98
+ if (e.rawEvent !== undefined && e.rawEvent !== null) return e.rawEvent
99
+ if (
100
+ e.error !== undefined &&
101
+ e.error !== null &&
102
+ typeof e.error === 'object'
103
+ ) {
104
+ return e.error
105
+ }
106
+ if (e.metadata !== undefined && e.metadata !== null) return e.metadata
107
+ return undefined
108
+ }
@@ -174,6 +174,16 @@ async function runGenerateAudio<
174
174
  timestamp: Date.now(),
175
175
  })
176
176
 
177
+ if (result.usage) {
178
+ aiEventClient.emit('audio:usage', {
179
+ requestId,
180
+ model,
181
+ usage: result.usage,
182
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
183
+ timestamp: Date.now(),
184
+ })
185
+ }
186
+
177
187
  logger.output(`activity=generateAudio provider=${providerName}`, {
178
188
  contentType: result.audio.contentType,
179
189
  audioDuration: result.audio.duration,
@@ -187,6 +187,16 @@ async function runGenerateSpeech<
187
187
  timestamp: Date.now(),
188
188
  })
189
189
 
190
+ if (result.usage) {
191
+ aiEventClient.emit('speech:usage', {
192
+ requestId,
193
+ model,
194
+ usage: result.usage,
195
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
196
+ timestamp: Date.now(),
197
+ })
198
+ }
199
+
190
200
  logger.output(`activity=generateSpeech bytes=${result.audio.length}`, {
191
201
  bytes: result.audio.length,
192
202
  contentType: result.contentType,
@@ -6,4 +6,7 @@ export type { ResolvedCategories } from './logger/internal-logger'
6
6
  export { InternalLogger } from './logger/internal-logger'
7
7
  export type { Logger } from './logger/types'
8
8
  export { resolveDebugOption } from './logger/resolve'
9
- export { toRunErrorPayload } from './activities/error-payload'
9
+ export {
10
+ toRunErrorPayload,
11
+ toRunErrorRawEvent,
12
+ } from './activities/error-payload'
package/src/client.ts CHANGED
@@ -46,7 +46,11 @@ export {
46
46
  type ToolDefinitionInstance,
47
47
  } from './activities/chat/tools/tool-definition'
48
48
 
49
- export { convertSchemaToJsonSchema } from './activities/chat/tools/schema-converter'
49
+ export {
50
+ convertSchemaToJsonSchema,
51
+ isStandardSchema,
52
+ parseWithStandardSchema,
53
+ } from './activities/chat/tools/schema-converter'
50
54
 
51
55
  export {
52
56
  convertMessagesToModelMessages,
package/src/index.ts CHANGED
@@ -55,6 +55,8 @@ export {
55
55
  // Schema conversion (Standard JSON Schema compliant)
56
56
  export {
57
57
  convertSchemaToJsonSchema,
58
+ isStandardSchema,
59
+ parseWithStandardSchema,
58
60
  StandardSchemaValidationError,
59
61
  } from './activities/chat/tools/schema-converter'
60
62
 
@@ -109,6 +111,9 @@ export type {
109
111
  // All types
110
112
  export * from './types'
111
113
 
114
+ // Usage utilities
115
+ export { buildBaseUsage, type BaseUsageInput } from './utilities/usage'
116
+
112
117
  // System prompts (type + normaliser used by adapters)
113
118
  export type { SystemPrompt, NormalizedSystemPrompt } from './system-prompts'
114
119
  export { normalizeSystemPrompts } from './system-prompts'
@@ -187,6 +192,11 @@ export {
187
192
  // AG-UI wire serialization (used internally by @tanstack/ai-client)
188
193
  export { uiMessagesToWire } from './utilities/ag-ui-wire'
189
194
  export type { WireMessage } from './utilities/ag-ui-wire'
195
+ export {
196
+ isContentPart,
197
+ isContentPartArray,
198
+ normalizeToolResult,
199
+ } from './utilities/tool-result'
190
200
 
191
201
  // Adapter extension utilities
192
202
  export { createModel, extendAdapter } from './extend-adapter'
@@ -1,4 +1,4 @@
1
- import type { Tool } from './types'
1
+ import type { AnyTool } from './types'
2
2
 
3
3
  /**
4
4
  * A registry that holds tools and allows dynamic tool management.
@@ -6,12 +6,12 @@ import type { Tool } from './types'
6
6
  * The registry can be either mutable (allowing additions/removals during execution)
7
7
  * or frozen (static tool list, for backward compatibility with tools arrays).
8
8
  */
9
- export interface ToolRegistry {
9
+ export interface ToolRegistry<TTool extends AnyTool = AnyTool> {
10
10
  /**
11
11
  * Get all current tools in the registry.
12
12
  * Called each agent loop iteration to get the latest tool list.
13
13
  */
14
- getTools: () => ReadonlyArray<Tool>
14
+ getTools: () => Array<TTool>
15
15
 
16
16
  /**
17
17
  * Add a tool to the registry dynamically.
@@ -19,7 +19,7 @@ export interface ToolRegistry {
19
19
  *
20
20
  * @param tool - The tool to add
21
21
  */
22
- add: (tool: Tool) => void
22
+ add: (tool: TTool) => void
23
23
 
24
24
  /**
25
25
  * Remove a tool from the registry by name.
@@ -43,7 +43,7 @@ export interface ToolRegistry {
43
43
  * @param name - The name of the tool to get
44
44
  * @returns The tool if found, undefined otherwise
45
45
  */
46
- get: (name: string) => Tool | undefined
46
+ get: (name: string) => TTool | undefined
47
47
 
48
48
  /**
49
49
  * Whether this registry is frozen (immutable).
@@ -75,10 +75,10 @@ export interface ToolRegistry {
75
75
  * registry.add(newTool) // Immediately available to LLM
76
76
  * ```
77
77
  */
78
- export function createToolRegistry(
79
- initialTools: Array<Tool> = [],
80
- ): ToolRegistry {
81
- const tools = new Map<string, Tool>()
78
+ export function createToolRegistry<TTool extends AnyTool = AnyTool>(
79
+ initialTools: Array<TTool> = [],
80
+ ): ToolRegistry<TTool> {
81
+ const tools = new Map<string, TTool>()
82
82
 
83
83
  for (const tool of initialTools) {
84
84
  tools.set(tool.name, tool)
@@ -87,7 +87,7 @@ export function createToolRegistry(
87
87
  return {
88
88
  getTools: () => Array.from(tools.values()),
89
89
 
90
- add: (tool: Tool) => {
90
+ add: (tool: TTool) => {
91
91
  tools.set(tool.name, tool)
92
92
  },
93
93
 
@@ -116,8 +116,10 @@ export function createToolRegistry(
116
116
  * @param tools - The static array of tools
117
117
  * @returns A frozen ToolRegistry
118
118
  */
119
- export function createFrozenRegistry(tools: Array<Tool> = []): ToolRegistry {
120
- const toolMap = new Map<string, Tool>()
119
+ export function createFrozenRegistry<TTool extends AnyTool = AnyTool>(
120
+ tools: Array<TTool> = [],
121
+ ): ToolRegistry<TTool> {
122
+ const toolMap = new Map<string, TTool>()
121
123
 
122
124
  for (const tool of tools) {
123
125
  toolMap.set(tool.name, tool)
@@ -126,9 +128,9 @@ export function createFrozenRegistry(tools: Array<Tool> = []): ToolRegistry {
126
128
  const frozenTools = Object.freeze([...tools])
127
129
 
128
130
  return {
129
- getTools: () => frozenTools,
131
+ getTools: () => [...frozenTools],
130
132
 
131
- add: (_tool: Tool) => {
133
+ add: (_tool: TTool) => {
132
134
  // No-op for frozen registry
133
135
  },
134
136