@tanstack/ai 0.23.1 → 0.24.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 (61) hide show
  1. package/dist/esm/activities/chat/index.d.ts +33 -9
  2. package/dist/esm/activities/chat/index.js +19 -9
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.js +2 -1
  5. package/dist/esm/activities/chat/messages.js.map +1 -1
  6. package/dist/esm/activities/chat/middleware/compose.d.ts +14 -14
  7. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  8. package/dist/esm/activities/chat/middleware/types.d.ts +16 -16
  9. package/dist/esm/activities/chat/runtime-context-types.d.ts +43 -0
  10. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  11. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  12. package/dist/esm/activities/chat/stream/processor.d.ts +1 -0
  13. package/dist/esm/activities/chat/stream/processor.js +35 -12
  14. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  15. package/dist/esm/activities/chat/tools/tool-calls.d.ts +15 -5
  16. package/dist/esm/activities/chat/tools/tool-calls.js +59 -19
  17. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  18. package/dist/esm/activities/chat/tools/tool-definition.d.ts +12 -8
  19. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  20. package/dist/esm/activities/error-payload.d.ts +26 -0
  21. package/dist/esm/activities/error-payload.js +12 -1
  22. package/dist/esm/activities/error-payload.js.map +1 -1
  23. package/dist/esm/adapter-internals.d.ts +1 -1
  24. package/dist/esm/adapter-internals.js +3 -2
  25. package/dist/esm/client.d.ts +1 -1
  26. package/dist/esm/client.js +3 -1
  27. package/dist/esm/client.js.map +1 -1
  28. package/dist/esm/index.d.ts +2 -1
  29. package/dist/esm/index.js +7 -1
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/tool-registry.d.ts +7 -7
  32. package/dist/esm/tool-registry.js +1 -1
  33. package/dist/esm/tool-registry.js.map +1 -1
  34. package/dist/esm/types.d.ts +40 -8
  35. package/dist/esm/utilities/ag-ui-wire.js +1 -1
  36. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  37. package/dist/esm/utilities/chat-params.d.ts +8 -3
  38. package/dist/esm/utilities/chat-params.js +6 -2
  39. package/dist/esm/utilities/chat-params.js.map +1 -1
  40. package/dist/esm/utilities/tool-result.d.ts +21 -0
  41. package/dist/esm/utilities/tool-result.js +37 -0
  42. package/dist/esm/utilities/tool-result.js.map +1 -0
  43. package/package.json +2 -2
  44. package/src/activities/chat/index.ts +219 -47
  45. package/src/activities/chat/messages.ts +2 -1
  46. package/src/activities/chat/middleware/compose.ts +23 -17
  47. package/src/activities/chat/middleware/types.ts +16 -16
  48. package/src/activities/chat/runtime-context-types.ts +68 -0
  49. package/src/activities/chat/stream/message-updaters.ts +2 -1
  50. package/src/activities/chat/stream/processor.ts +48 -8
  51. package/src/activities/chat/tools/tool-calls.ts +138 -43
  52. package/src/activities/chat/tools/tool-definition.ts +25 -31
  53. package/src/activities/error-payload.ts +44 -0
  54. package/src/adapter-internals.ts +4 -1
  55. package/src/client.ts +5 -1
  56. package/src/index.ts +7 -0
  57. package/src/tool-registry.ts +16 -14
  58. package/src/types.ts +86 -29
  59. package/src/utilities/ag-ui-wire.ts +4 -1
  60. package/src/utilities/chat-params.ts +22 -7
  61. package/src/utilities/tool-result.ts +60 -0
@@ -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
+ }
@@ -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
 
@@ -187,6 +189,11 @@ export {
187
189
  // AG-UI wire serialization (used internally by @tanstack/ai-client)
188
190
  export { uiMessagesToWire } from './utilities/ag-ui-wire'
189
191
  export type { WireMessage } from './utilities/ag-ui-wire'
192
+ export {
193
+ isContentPart,
194
+ isContentPartArray,
195
+ normalizeToolResult,
196
+ } from './utilities/tool-result'
190
197
 
191
198
  // Adapter extension utilities
192
199
  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
 
package/src/types.ts CHANGED
@@ -50,6 +50,8 @@ export type ToolResultState =
50
50
  | 'complete' // Result is complete
51
51
  | 'error' // Error occurred
52
52
 
53
+ export type ToolOutputState = 'output-available' | 'output-error'
54
+
53
55
  /**
54
56
  * JSON Schema type for defining tool input/output schemas as raw JSON Schema objects.
55
57
  * This allows tools to be defined without schema libraries when you have JSON Schema definitions available.
@@ -355,7 +357,7 @@ export interface ToolCallPart<TMetadata = unknown> {
355
357
  export interface ToolResultPart {
356
358
  type: 'tool-result'
357
359
  toolCallId: string
358
- content: string
360
+ content: string | Array<ContentPart>
359
361
  state: ToolResultState
360
362
  error?: string // Error message if state is "error"
361
363
  }
@@ -443,33 +445,75 @@ export type ConstrainedModelMessage<
443
445
  content: ConstrainedContent<TInputModalitiesTypes>
444
446
  }
445
447
 
448
+ type IsUnknown<T> = unknown extends T
449
+ ? [T] extends [unknown]
450
+ ? true
451
+ : false
452
+ : false
453
+
454
+ type RuntimeContextField<TContext> =
455
+ IsUnknown<TContext> extends true
456
+ ? {
457
+ /**
458
+ * Runtime context provided by the caller.
459
+ *
460
+ * This is request-local application state for tool and middleware
461
+ * implementations, not the AG-UI `Context[]` protocol field.
462
+ */
463
+ context?: TContext
464
+ }
465
+ : {
466
+ /**
467
+ * Runtime context provided by the caller.
468
+ *
469
+ * This is request-local application state for tool and middleware
470
+ * implementations, not the AG-UI `Context[]` protocol field.
471
+ */
472
+ context: TContext
473
+ }
474
+
446
475
  /**
447
476
  * Context passed to tool execute functions, providing capabilities like
448
477
  * emitting custom events during execution.
449
478
  */
450
- export interface ToolExecutionContext {
451
- /** The ID of the tool call being executed */
452
- toolCallId?: string
453
- /**
454
- * Emit a custom event during tool execution.
455
- * Events are streamed to the client in real-time as AG-UI CUSTOM events.
456
- *
457
- * @param eventName - Name of the custom event
458
- * @param value - Event payload value
459
- *
460
- * @example
461
- * ```ts
462
- * const tool = toolDefinition({ ... }).server(async (args, context) => {
463
- * context?.emitCustomEvent('progress', { step: 1, total: 3 })
464
- * // ... do work ...
465
- * context?.emitCustomEvent('progress', { step: 2, total: 3 })
466
- * // ... do more work ...
467
- * return result
468
- * })
469
- * ```
470
- */
471
- emitCustomEvent: (eventName: string, value: Record<string, any>) => void
472
- }
479
+ export type ToolExecutionContext<TContext = unknown> =
480
+ RuntimeContextField<TContext> & {
481
+ /** The ID of the tool call being executed */
482
+ toolCallId?: string
483
+ /**
484
+ * Emit a custom event during tool execution.
485
+ * Events are streamed to the client in real-time as AG-UI CUSTOM events.
486
+ *
487
+ * @param eventName - Name of the custom event
488
+ * @param value - Event payload value
489
+ *
490
+ * @example
491
+ * ```ts
492
+ * const tool = toolDefinition({ ... }).server(async (args, context) => {
493
+ * context?.emitCustomEvent('progress', { step: 1, total: 3 })
494
+ * // ... do work ...
495
+ * context?.emitCustomEvent('progress', { step: 2, total: 3 })
496
+ * // ... do more work ...
497
+ * return result
498
+ * })
499
+ * ```
500
+ */
501
+ emitCustomEvent: (eventName: string, value: Record<string, any>) => void
502
+ }
503
+
504
+ export type ToolExecuteFunction<
505
+ TInput extends SchemaInput = SchemaInput,
506
+ TOutput extends SchemaInput = SchemaInput,
507
+ TContext = unknown,
508
+ > = undefined extends TContext
509
+ ? (
510
+ args: InferSchemaType<TInput>,
511
+ context?: ToolExecutionContext<TContext>,
512
+ ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>
513
+ : (
514
+ args: InferSchemaType<TInput>,
515
+ context: ToolExecutionContext<TContext>,
516
+ ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>
473
517
 
474
518
  /**
475
519
  * Tool/Function definition for function calling.
@@ -488,6 +532,7 @@ export interface Tool<
488
532
  TInput extends SchemaInput = SchemaInput,
489
533
  TOutput extends SchemaInput = SchemaInput,
490
534
  TName extends string = string,
535
+ TContext = unknown,
491
536
  > {
492
537
  /**
493
538
  * Unique name of the tool (used by the model to call it).
@@ -587,9 +632,7 @@ export interface Tool<
587
632
  * return weather; // Can return object or string
588
633
  * }
589
634
  */
590
- execute?:
591
- | ((args: any, context?: ToolExecutionContext) => Promise<any> | any)
592
- | undefined
635
+ execute?: ToolExecuteFunction<TInput, TOutput, TContext> | undefined
593
636
 
594
637
  /** If true, tool execution requires user approval before running. Works with both server and client tools. */
595
638
  needsApproval?: boolean
@@ -601,6 +644,10 @@ export interface Tool<
601
644
  metadata?: Record<string, any> | undefined
602
645
  }
603
646
 
647
+ export type AnyTool = Omit<Tool<any, any, any, any>, 'execute'> & {
648
+ execute?: ((args: any, context?: any) => any) | undefined
649
+ }
650
+
604
651
  export interface ToolConfig {
605
652
  [key: string]: Tool
606
653
  }
@@ -729,10 +776,16 @@ export type AgentLoopStrategy = (state: AgentLoopState) => boolean
729
776
  export interface TextOptions<
730
777
  TProviderOptionsSuperset extends Record<string, any> = Record<string, any>,
731
778
  TProviderOptionsForModel = TProviderOptionsSuperset,
779
+ TContext = unknown,
732
780
  > {
733
781
  model: string
734
782
  messages: Array<ModelMessage>
735
- tools?: Array<Tool<any, any, any>> | undefined
783
+ tools?: Array<AnyTool> | undefined
784
+ /**
785
+ * Runtime context provided by the caller and passed to middleware and
786
+ * server-side tool implementations.
787
+ */
788
+ context?: TContext
736
789
  /**
737
790
  * System prompts to include with the request.
738
791
  *
@@ -1084,7 +1137,9 @@ export interface ToolCallEndEvent extends AGUIToolCallEndEvent {
1084
1137
  /** Final parsed input arguments (TanStack AI internal) */
1085
1138
  input?: unknown
1086
1139
  /** Tool execution result (TanStack AI internal) */
1087
- result?: string
1140
+ result?: string | Array<ContentPart>
1141
+ /** Tool execution output state (TanStack AI internal) */
1142
+ state?: ToolOutputState
1088
1143
  }
1089
1144
 
1090
1145
  /**
@@ -1096,6 +1151,8 @@ export interface ToolCallEndEvent extends AGUIToolCallEndEvent {
1096
1151
  export interface ToolCallResultEvent extends AGUIToolCallResultEvent {
1097
1152
  /** Model identifier for multi-model support */
1098
1153
  model?: string
1154
+ /** Tool execution output state (TanStack AI internal) */
1155
+ state?: ToolOutputState
1099
1156
  }
1100
1157
 
1101
1158
  /**
@@ -103,7 +103,10 @@ export function uiMessagesToWire(
103
103
  role: 'tool',
104
104
  id: deriveToolMessageId(part.toolCallId),
105
105
  toolCallId: part.toolCallId,
106
- content: part.content,
106
+ content:
107
+ typeof part.content === 'string'
108
+ ? part.content
109
+ : JSON.stringify(part.content),
107
110
  ...(part.error !== undefined && { error: part.error }),
108
111
  })
109
112
  }
@@ -1,6 +1,12 @@
1
1
  import { AGUIError, RunAgentInputSchema } from '@ag-ui/core'
2
2
  import type { Context as AGUIContext } from '@ag-ui/core'
3
- import type { JSONSchema, ModelMessage, Tool, UIMessage } from '../types'
3
+ import type {
4
+ JSONSchema,
5
+ ModelMessage,
6
+ SchemaInput,
7
+ Tool,
8
+ UIMessage,
9
+ } from '../types'
4
10
 
5
11
  const KNOWN_PART_TYPES = new Set([
6
12
  'text',
@@ -43,7 +49,12 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
43
49
  tools: Array<{ name: string; description: string; parameters: JSONSchema }>
44
50
  forwardedProps: Record<string, unknown>
45
51
  state: unknown
52
+ /**
53
+ * @deprecated Use `aguiContext` instead. This alias will be removed in a
54
+ * future release.
55
+ */
46
56
  context: Array<AGUIContext>
57
+ aguiContext: Array<AGUIContext>
47
58
  }> {
48
59
  const parseResult = RunAgentInputSchema.safeParse(body)
49
60
  if (!parseResult.success) {
@@ -58,6 +69,7 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
58
69
  }
59
70
 
60
71
  const parsed = parseResult.data
72
+ const aguiContext = parsed.context
61
73
 
62
74
  // AG-UI Zod uses `.strip()` so extra fields like `parts` on messages are
63
75
  // dropped during parse. We re-attach them from the original body so the
@@ -89,7 +101,8 @@ export function chatParamsFromRequestBody(body: unknown): Promise<{
89
101
  }>,
90
102
  forwardedProps: (parsed.forwardedProps ?? {}) as Record<string, unknown>,
91
103
  state: parsed.state,
92
- context: parsed.context,
104
+ context: aguiContext,
105
+ aguiContext,
93
106
  })
94
107
  }
95
108
 
@@ -171,16 +184,18 @@ export async function chatParamsFromRequest(
171
184
  * `chatParamsFromRequest(...)` / `chatParamsFromRequestBody(...)`.
172
185
  * @returns A merged array suitable for `chat({ tools })`.
173
186
  */
174
- export function mergeAgentTools(
175
- serverTools: ReadonlyArray<Tool>,
187
+ export function mergeAgentTools<TContext = unknown>(
188
+ serverTools: ReadonlyArray<Tool<SchemaInput, SchemaInput, string, TContext>>,
176
189
  clientTools: ReadonlyArray<{
177
190
  name: string
178
191
  description: string
179
192
  parameters: JSONSchema
180
193
  }>,
181
- ): Array<Tool> {
194
+ ): Array<Tool<SchemaInput, SchemaInput, string, TContext>> {
182
195
  const seen = new Set(serverTools.map((t) => t.name))
183
- const merged: Array<Tool> = [...serverTools]
196
+ const merged: Array<Tool<SchemaInput, SchemaInput, string, TContext>> = [
197
+ ...serverTools,
198
+ ]
184
199
  for (const ct of clientTools) {
185
200
  if (seen.has(ct.name)) {
186
201
  // Server wins on name collision.
@@ -193,7 +208,7 @@ export function mergeAgentTools(
193
208
  inputSchema: ct.parameters,
194
209
  // No `execute` — runtime treats this as a client-side tool and
195
210
  // emits ClientToolRequest events.
196
- } as Tool)
211
+ } as Tool<SchemaInput, SchemaInput, string, TContext>)
197
212
  }
198
213
  return merged
199
214
  }
@@ -0,0 +1,60 @@
1
+ import type { ContentPart } from '../types'
2
+
3
+ const CONTENT_PART_TYPES = new Set([
4
+ 'text',
5
+ 'image',
6
+ 'audio',
7
+ 'video',
8
+ 'document',
9
+ ])
10
+
11
+ /**
12
+ * Structural check for a single `ContentPart`. A text part must carry a string
13
+ * `content`; every other modality must carry a `source` with `type` of
14
+ * `'url' | 'data'` and a string `value`.
15
+ */
16
+ export function isContentPart(value: unknown): value is ContentPart {
17
+ if (typeof value !== 'object' || value === null) return false
18
+ const part = value as Record<string, unknown>
19
+ if (typeof part.type !== 'string' || !CONTENT_PART_TYPES.has(part.type)) {
20
+ return false
21
+ }
22
+ if (part.type === 'text') {
23
+ return typeof part.content === 'string'
24
+ }
25
+ const source = part.source
26
+ if (typeof source !== 'object' || source === null) return false
27
+ const src = source as Record<string, unknown>
28
+ if (typeof src.value !== 'string') return false
29
+ // `data` sources require a mimeType (matches ContentPartDataSource); `url`
30
+ // sources don't. Requiring it here keeps the runtime guard consistent with
31
+ // the type and avoids emitting `data:undefined;base64,...` downstream.
32
+ if (src.type === 'data') return typeof src.mimeType === 'string'
33
+ return src.type === 'url'
34
+ }
35
+
36
+ /**
37
+ * True iff `value` is a NON-EMPTY array whose every element is a valid
38
+ * `ContentPart`. Empty arrays and mixed arrays return false so they continue
39
+ * to be treated as ordinary (stringified) data — this keeps the auto-detection
40
+ * footgun narrow.
41
+ */
42
+ export function isContentPartArray(
43
+ value: unknown,
44
+ ): value is Array<ContentPart> {
45
+ return Array.isArray(value) && value.length > 0 && value.every(isContentPart)
46
+ }
47
+
48
+ /**
49
+ * Normalize a tool's return value for transport:
50
+ * - string → unchanged
51
+ * - ContentPart array → unchanged (multimodal, passed through to the adapter)
52
+ * - anything else → `JSON.stringify`
53
+ */
54
+ export function normalizeToolResult(
55
+ result: unknown,
56
+ ): string | Array<ContentPart> {
57
+ if (typeof result === 'string') return result
58
+ if (isContentPartArray(result)) return result
59
+ return JSON.stringify(result)
60
+ }