@tanstack/ai 0.1.0 → 0.2.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.
@@ -8,20 +8,25 @@
8
8
  import { aiEventClient } from '../../event-client.js'
9
9
  import { streamToText } from '../../stream-to-response.js'
10
10
  import { ToolCallManager, executeToolCalls } from './tools/tool-calls'
11
- import { convertZodToJsonSchema } from './tools/zod-converter'
11
+ import {
12
+ convertSchemaToJsonSchema,
13
+ isStandardSchema,
14
+ parseWithStandardSchema,
15
+ } from './tools/schema-converter'
12
16
  import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
13
17
  import type {
14
18
  ApprovalRequest,
15
19
  ClientToolRequest,
16
20
  ToolResult,
17
21
  } from './tools/tool-calls'
18
- import type { z } from 'zod'
19
22
  import type { AnyTextAdapter } from './adapter'
20
23
  import type {
21
24
  AgentLoopStrategy,
22
25
  ConstrainedModelMessage,
23
26
  DoneStreamChunk,
27
+ InferSchemaType,
24
28
  ModelMessage,
29
+ SchemaInput,
25
30
  StreamChunk,
26
31
  TextOptions,
27
32
  Tool,
@@ -44,12 +49,12 @@ export const kind = 'text' as const
44
49
  * Types are extracted directly from the adapter (which has pre-resolved generics).
45
50
  *
46
51
  * @template TAdapter - The text adapter type (created by a provider function)
47
- * @template TSchema - Optional Zod schema for structured output
52
+ * @template TSchema - Optional Standard Schema for structured output
48
53
  * @template TStream - Whether to stream the output (default: true)
49
54
  */
50
55
  export interface TextActivityOptions<
51
56
  TAdapter extends AnyTextAdapter,
52
- TSchema extends z.ZodType | undefined,
57
+ TSchema extends SchemaInput | undefined,
53
58
  TStream extends boolean,
54
59
  > {
55
60
  /** The text adapter to use (created by a provider function like openaiText('gpt-4o')) */
@@ -82,11 +87,13 @@ export interface TextActivityOptions<
82
87
  /** Unique conversation identifier for tracking */
83
88
  conversationId?: TextOptions['conversationId']
84
89
  /**
85
- * Optional Zod schema for structured output.
90
+ * Optional Standard Schema for structured output.
86
91
  * When provided, the activity will:
87
92
  * 1. Run the full agentic loop (executing tools as needed)
88
93
  * 2. Once complete, return a Promise with the parsed output matching the schema
89
94
  *
95
+ * Supports any Standard Schema compliant library (Zod v4+, ArkType, Valibot, etc.)
96
+ *
90
97
  * @example
91
98
  * ```ts
92
99
  * const result = await chat({
@@ -104,7 +111,7 @@ export interface TextActivityOptions<
104
111
  * When false, returns a Promise<string> with the collected text content.
105
112
  *
106
113
  * Note: If outputSchema is provided, this option is ignored and the result
107
- * is always a Promise<z.infer<TSchema>>.
114
+ * is always a Promise<InferSchemaType<TSchema>>.
108
115
  *
109
116
  * @default true
110
117
  *
@@ -140,7 +147,7 @@ export interface TextActivityOptions<
140
147
  */
141
148
  export function createChatOptions<
142
149
  TAdapter extends AnyTextAdapter,
143
- TSchema extends z.ZodType | undefined = undefined,
150
+ TSchema extends SchemaInput | undefined = undefined,
144
151
  TStream extends boolean = true,
145
152
  >(
146
153
  options: TextActivityOptions<TAdapter, TSchema, TStream>,
@@ -154,15 +161,15 @@ export function createChatOptions<
154
161
 
155
162
  /**
156
163
  * Result type for the text activity.
157
- * - If outputSchema is provided: Promise<z.infer<TSchema>>
164
+ * - If outputSchema is provided: Promise<InferSchemaType<TSchema>>
158
165
  * - If stream is false: Promise<string>
159
166
  * - Otherwise (stream is true, default): AsyncIterable<StreamChunk>
160
167
  */
161
168
  export type TextActivityResult<
162
- TSchema extends z.ZodType | undefined,
169
+ TSchema extends SchemaInput | undefined,
163
170
  TStream extends boolean = true,
164
- > = TSchema extends z.ZodType
165
- ? Promise<z.infer<TSchema>>
171
+ > = TSchema extends SchemaInput
172
+ ? Promise<InferSchemaType<TSchema>>
166
173
  : TStream extends false
167
174
  ? Promise<string>
168
175
  : AsyncIterable<StreamChunk>
@@ -366,14 +373,14 @@ class TextEngine<
366
373
  const { temperature, topP, maxTokens, metadata, modelOptions } = this.params
367
374
  const tools = this.params.tools
368
375
 
369
- // Convert tool schemas from Zod to JSON Schema before passing to adapter
376
+ // Convert tool schemas to JSON Schema before passing to adapter
370
377
  const toolsWithJsonSchemas = tools?.map((tool) => ({
371
378
  ...tool,
372
379
  inputSchema: tool.inputSchema
373
- ? convertZodToJsonSchema(tool.inputSchema)
380
+ ? convertSchemaToJsonSchema(tool.inputSchema)
374
381
  : undefined,
375
382
  outputSchema: tool.outputSchema
376
- ? convertZodToJsonSchema(tool.outputSchema)
383
+ ? convertSchemaToJsonSchema(tool.outputSchema)
377
384
  : undefined,
378
385
  }))
379
386
 
@@ -968,7 +975,7 @@ class TextEngine<
968
975
  */
969
976
  export function chat<
970
977
  TAdapter extends AnyTextAdapter,
971
- TSchema extends z.ZodType | undefined = undefined,
978
+ TSchema extends SchemaInput | undefined = undefined,
972
979
  TStream extends boolean = true,
973
980
  >(
974
981
  options: TextActivityOptions<TAdapter, TSchema, TStream>,
@@ -980,7 +987,7 @@ export function chat<
980
987
  return runAgenticStructuredOutput(
981
988
  options as unknown as TextActivityOptions<
982
989
  AnyTextAdapter,
983
- z.ZodType,
990
+ SchemaInput,
984
991
  boolean
985
992
  >,
986
993
  ) as TextActivityResult<TSchema, TStream>
@@ -1046,9 +1053,9 @@ function runNonStreamingText(
1046
1053
  * 2. Once complete, call adapter.structuredOutput with the conversation context
1047
1054
  * 3. Validate and return the structured result
1048
1055
  */
1049
- async function runAgenticStructuredOutput<TSchema extends z.ZodType>(
1056
+ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
1050
1057
  options: TextActivityOptions<AnyTextAdapter, TSchema, boolean>,
1051
- ): Promise<z.infer<TSchema>> {
1058
+ ): Promise<InferSchemaType<TSchema>> {
1052
1059
  const { adapter, outputSchema, ...textOptions } = options
1053
1060
  const model = adapter.model
1054
1061
 
@@ -1060,8 +1067,8 @@ async function runAgenticStructuredOutput<TSchema extends z.ZodType>(
1060
1067
  const engine = new TextEngine({
1061
1068
  adapter,
1062
1069
  params: { ...textOptions, model } as TextOptions<
1063
- Record<string, any>,
1064
- Record<string, any>
1070
+ Record<string, unknown>,
1071
+ Record<string, unknown>
1065
1072
  >,
1066
1073
  })
1067
1074
 
@@ -1081,8 +1088,8 @@ async function runAgenticStructuredOutput<TSchema extends z.ZodType>(
1081
1088
  ...structuredTextOptions
1082
1089
  } = textOptions
1083
1090
 
1084
- // Convert the Zod schema to JSON Schema before passing to the adapter
1085
- const jsonSchema = convertZodToJsonSchema(outputSchema)
1091
+ // Convert the schema to JSON Schema before passing to the adapter
1092
+ const jsonSchema = convertSchemaToJsonSchema(outputSchema)
1086
1093
  if (!jsonSchema) {
1087
1094
  throw new Error('Failed to convert output schema to JSON Schema')
1088
1095
  }
@@ -1098,15 +1105,16 @@ async function runAgenticStructuredOutput<TSchema extends z.ZodType>(
1098
1105
  outputSchema: jsonSchema,
1099
1106
  })
1100
1107
 
1101
- // Validate the result against the Zod schema
1102
- const validationResult = outputSchema.safeParse(result.data)
1103
- if (!validationResult.success) {
1104
- throw new Error(
1105
- `Structured output validation failed: ${validationResult.error.message}`,
1108
+ // Validate the result against the schema if it's a Standard Schema
1109
+ if (isStandardSchema(outputSchema)) {
1110
+ return parseWithStandardSchema<InferSchemaType<TSchema>>(
1111
+ outputSchema,
1112
+ result.data,
1106
1113
  )
1107
1114
  }
1108
1115
 
1109
- return validationResult.data
1116
+ // For plain JSON Schema, return the data as-is
1117
+ return result.data as InferSchemaType<TSchema>
1110
1118
  }
1111
1119
 
1112
1120
  // Re-export adapter types
@@ -0,0 +1,332 @@
1
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2
+
3
+ import type {
4
+ StandardJSONSchemaV1,
5
+ StandardSchemaV1,
6
+ } from '@standard-schema/spec'
7
+ import type { JSONSchema, SchemaInput } from '../../../types'
8
+
9
+ /**
10
+ * Check if a value is a Standard JSON Schema compliant schema.
11
+ * Standard JSON Schema compliant libraries (Zod v4+, ArkType, Valibot with toStandardJsonSchema, etc.)
12
+ * implement the '~standard' property with jsonSchema converter methods.
13
+ */
14
+ export function isStandardJSONSchema(
15
+ schema: unknown,
16
+ ): schema is StandardJSONSchemaV1 {
17
+ return (
18
+ typeof schema === 'object' &&
19
+ schema !== null &&
20
+ '~standard' in schema &&
21
+ typeof (schema as StandardJSONSchemaV1)['~standard'] === 'object' &&
22
+ (schema as StandardJSONSchemaV1)['~standard'].version === 1 &&
23
+ typeof (schema as StandardJSONSchemaV1)['~standard'].jsonSchema ===
24
+ 'object' &&
25
+ typeof (schema as StandardJSONSchemaV1)['~standard'].jsonSchema.input ===
26
+ 'function'
27
+ )
28
+ }
29
+
30
+ /**
31
+ * Check if a value is a Standard Schema compliant schema (for validation).
32
+ * Standard Schema compliant libraries implement the '~standard' property with a validate function.
33
+ */
34
+ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
35
+ return (
36
+ typeof schema === 'object' &&
37
+ schema !== null &&
38
+ '~standard' in schema &&
39
+ typeof schema['~standard'] === 'object' &&
40
+ schema !== null &&
41
+ schema['~standard'] !== null &&
42
+ 'version' in schema['~standard'] &&
43
+ schema['~standard'].version === 1 &&
44
+ 'validate' in schema['~standard'] &&
45
+ typeof schema['~standard'].validate === 'function'
46
+ )
47
+ }
48
+
49
+ /**
50
+ * Transform a JSON schema to be compatible with OpenAI's structured output requirements.
51
+ * OpenAI requires:
52
+ * - All properties must be in the `required` array
53
+ * - Optional fields should have null added to their type union
54
+ * - additionalProperties must be false for objects
55
+ *
56
+ * @param schema - JSON schema to transform
57
+ * @param originalRequired - Original required array (to know which fields were optional)
58
+ * @returns Transformed schema compatible with OpenAI structured output
59
+ */
60
+ function makeStructuredOutputCompatible(
61
+ schema: Record<string, any>,
62
+ originalRequired: Array<string> = [],
63
+ ): Record<string, any> {
64
+ const result = { ...schema }
65
+
66
+ // Handle object types
67
+ if (result.type === 'object' && result.properties) {
68
+ const properties = { ...result.properties }
69
+ const allPropertyNames = Object.keys(properties)
70
+
71
+ // Transform each property
72
+ for (const propName of allPropertyNames) {
73
+ const prop = properties[propName]
74
+ const wasOptional = !originalRequired.includes(propName)
75
+
76
+ // Recursively transform nested objects/arrays
77
+ if (prop.type === 'object' && prop.properties) {
78
+ properties[propName] = makeStructuredOutputCompatible(
79
+ prop,
80
+ prop.required || [],
81
+ )
82
+ } else if (prop.type === 'array' && prop.items) {
83
+ properties[propName] = {
84
+ ...prop,
85
+ items: makeStructuredOutputCompatible(
86
+ prop.items,
87
+ prop.items.required || [],
88
+ ),
89
+ }
90
+ } else if (wasOptional) {
91
+ // Make optional fields nullable by adding null to the type
92
+ if (prop.type && !Array.isArray(prop.type)) {
93
+ properties[propName] = {
94
+ ...prop,
95
+ type: [prop.type, 'null'],
96
+ }
97
+ } else if (Array.isArray(prop.type) && !prop.type.includes('null')) {
98
+ properties[propName] = {
99
+ ...prop,
100
+ type: [...prop.type, 'null'],
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ result.properties = properties
107
+ // ALL properties must be required for OpenAI structured output
108
+ result.required = allPropertyNames
109
+ // additionalProperties must be false
110
+ result.additionalProperties = false
111
+ }
112
+
113
+ // Handle array types with object items
114
+ if (result.type === 'array' && result.items) {
115
+ result.items = makeStructuredOutputCompatible(
116
+ result.items,
117
+ result.items.required || [],
118
+ )
119
+ }
120
+
121
+ return result
122
+ }
123
+
124
+ /**
125
+ * Options for schema conversion
126
+ */
127
+ export interface ConvertSchemaOptions {
128
+ /**
129
+ * When true, transforms the schema to be compatible with OpenAI's structured output requirements:
130
+ * - All properties are added to the `required` array
131
+ * - Optional fields get null added to their type union
132
+ * - additionalProperties is set to false for all objects
133
+ *
134
+ * @default false
135
+ */
136
+ forStructuredOutput?: boolean
137
+ }
138
+
139
+ /**
140
+ * Converts a Standard JSON Schema compliant schema or plain JSONSchema to JSON Schema format
141
+ * compatible with LLM providers.
142
+ *
143
+ * Supports any schema library that implements the Standard JSON Schema spec (v1):
144
+ * - Zod v4+ (natively supports StandardJSONSchemaV1)
145
+ * - ArkType (natively supports StandardJSONSchemaV1)
146
+ * - Valibot (via `toStandardJsonSchema()` from `@valibot/to-json-schema`)
147
+ *
148
+ * If the input is already a plain JSONSchema object, it is returned as-is.
149
+ *
150
+ * @param schema - Standard JSON Schema compliant schema or plain JSONSchema object to convert
151
+ * @param options - Conversion options
152
+ * @returns JSON Schema object that can be sent to LLM providers
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * // Using Zod v4+ (natively supports Standard JSON Schema)
157
+ * import * as z from 'zod';
158
+ *
159
+ * const zodSchema = z.object({
160
+ * location: z.string().describe('City name'),
161
+ * unit: z.enum(['celsius', 'fahrenheit']).optional()
162
+ * });
163
+ *
164
+ * const jsonSchema = convertSchemaToJsonSchema(zodSchema);
165
+ *
166
+ * @example
167
+ * // Using ArkType (natively supports Standard JSON Schema)
168
+ * import { type } from 'arktype';
169
+ *
170
+ * const arkSchema = type({
171
+ * location: 'string',
172
+ * unit: "'celsius' | 'fahrenheit'"
173
+ * });
174
+ *
175
+ * const jsonSchema = convertSchemaToJsonSchema(arkSchema);
176
+ *
177
+ * @example
178
+ * // Using Valibot (via toStandardJsonSchema)
179
+ * import * as v from 'valibot';
180
+ * import { toStandardJsonSchema } from '@valibot/to-json-schema';
181
+ *
182
+ * const valibotSchema = toStandardJsonSchema(v.object({
183
+ * location: v.string(),
184
+ * unit: v.optional(v.picklist(['celsius', 'fahrenheit']))
185
+ * }));
186
+ *
187
+ * const jsonSchema = convertSchemaToJsonSchema(valibotSchema);
188
+ *
189
+ * @example
190
+ * // Using JSONSchema directly (passes through unchanged)
191
+ * const rawSchema = {
192
+ * type: 'object',
193
+ * properties: { location: { type: 'string' } },
194
+ * required: ['location']
195
+ * };
196
+ * const result = convertSchemaToJsonSchema(rawSchema);
197
+ * ```
198
+ */
199
+ export function convertSchemaToJsonSchema(
200
+ schema: SchemaInput | undefined,
201
+ options: ConvertSchemaOptions = {},
202
+ ): JSONSchema | undefined {
203
+ if (!schema) return undefined
204
+
205
+ const { forStructuredOutput = false } = options
206
+
207
+ // If it's a Standard JSON Schema compliant schema, use the standard interface
208
+ if (isStandardJSONSchema(schema)) {
209
+ const jsonSchema = schema['~standard'].jsonSchema.input({
210
+ target: 'draft-07',
211
+ })
212
+
213
+ let result = jsonSchema
214
+
215
+ if (typeof result === 'object' && '$schema' in result) {
216
+ // Remove $schema property as it's not needed for LLM providers
217
+ const { $schema, ...rest } = result
218
+ result = rest
219
+ }
220
+
221
+ // Ensure object schemas always have type: "object"
222
+
223
+ if (typeof result === 'object') {
224
+ // If it has properties (even empty), it should be an object type
225
+ if ('properties' in result && !result.type) {
226
+ result.type = 'object'
227
+ }
228
+
229
+ // Ensure properties exists for object types (even if empty)
230
+ if (result.type === 'object' && !('properties' in result)) {
231
+ result.properties = {}
232
+ }
233
+
234
+ // Ensure required exists for object types (even if empty array)
235
+ if (result.type === 'object' && !('required' in result)) {
236
+ result.required = []
237
+ }
238
+
239
+ // Apply structured output transformation if requested
240
+ if (forStructuredOutput) {
241
+ result = makeStructuredOutputCompatible(
242
+ result,
243
+ (result.required as Array<string>) || [],
244
+ )
245
+ }
246
+ }
247
+
248
+ return result as JSONSchema
249
+ }
250
+
251
+ // If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through
252
+ // Still apply structured output transformation if requested
253
+
254
+ if (forStructuredOutput && typeof schema === 'object') {
255
+ return makeStructuredOutputCompatible(
256
+ schema as Record<string, any>,
257
+ ((schema as JSONSchema).required as Array<string>) || [],
258
+ ) as JSONSchema
259
+ }
260
+
261
+ return schema as JSONSchema
262
+ }
263
+
264
+ /**
265
+ * Validates data against a Standard Schema compliant schema.
266
+ *
267
+ * @param schema - Standard Schema compliant schema
268
+ * @param data - Data to validate
269
+ * @returns Validation result with success status, data or issues
270
+ */
271
+ export async function validateWithStandardSchema<T>(
272
+ schema: unknown,
273
+ data: unknown,
274
+ ): Promise<
275
+ | { success: true; data: T }
276
+ | { success: false; issues: Array<{ message: string; path?: Array<string> }> }
277
+ > {
278
+ if (!isStandardSchema(schema)) {
279
+ // If it's not a Standard Schema, just return the data as-is
280
+ return { success: true, data: data as T }
281
+ }
282
+
283
+ const result = await schema['~standard'].validate(data)
284
+
285
+ if (!result.issues) {
286
+ return { success: true, data: result.value as T }
287
+ }
288
+
289
+ return {
290
+ success: false,
291
+ issues: result.issues.map((issue) => ({
292
+ message: issue.message || 'Validation failed',
293
+ path: issue.path?.map(String),
294
+ })),
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Synchronously validates data against a Standard Schema compliant schema.
300
+ * Note: Some Standard Schema implementations may only support async validation.
301
+ * In those cases, this function will throw.
302
+ *
303
+ * @param schema - Standard Schema compliant schema
304
+ * @param data - Data to validate
305
+ * @returns Parsed/validated data
306
+ * @throws Error if validation fails or if the schema only supports async validation
307
+ */
308
+ export function parseWithStandardSchema<T>(schema: unknown, data: unknown): T {
309
+ if (!isStandardSchema(schema)) {
310
+ // If it's not a Standard Schema, just return the data as-is
311
+ return data as T
312
+ }
313
+
314
+ const result = schema['~standard'].validate(data)
315
+
316
+ // Handle async result (Promise)
317
+ if (result instanceof Promise) {
318
+ throw new Error(
319
+ 'Schema validation returned a Promise. Use validateWithStandardSchema for async validation.',
320
+ )
321
+ }
322
+ // Standard Schema validation returns { value } for success or { issues } for failure
323
+ if (!result.issues) {
324
+ return result.value as T
325
+ }
326
+
327
+ // invalid validation, throw error with all issues
328
+ const errorMessages = result.issues
329
+ .map((issue) => issue.message || 'Validation failed')
330
+ .join(', ')
331
+ throw new Error(`Validation failed: ${errorMessages}`)
332
+ }