UNPKG

@openai/agents-core

Version:

The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.

650 lines • 22.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.hasJsonSchemaObjectShape = hasJsonSchemaObjectShape; exports.zodJsonSchemaCompat = zodJsonSchemaCompat; exports.zodJsonSchemaCompatForOpenAIStrict = zodJsonSchemaCompatForOpenAIStrict; exports.assertLosslessOpenAIStrictZodSchemaConversion = assertLosslessOpenAIStrictZodSchemaConversion; exports.mergeJsonSchemaDescriptions = mergeJsonSchemaDescriptions; const errors_1 = require("../errors.js"); const zodCompat_1 = require("./zodCompat.js"); const JSON_SCHEMA_DRAFT_07 = 'http://json-schema.org/draft-07/schema#'; const OPTIONAL_WRAPPERS = new Set(['optional']); const DECORATOR_WRAPPERS = new Set([ 'brand', 'branded', 'catch', 'default', 'effects', 'pipeline', 'pipe', 'prefault', 'readonly', 'refinement', 'transform', ]); const LOSSLESS_STRICT_FALLBACK_DECORATORS = new Set([ 'brand', 'branded', 'catch', 'default', 'prefault', 'readonly', ]); // Primitive leaf nodes map 1:1 to JSON Schema types; everything else is handled // by the specialised builders further down. const SIMPLE_TYPE_MAPPING = { string: { type: 'string' }, number: { type: 'number' }, boolean: { type: 'boolean' }, }; function hasJsonSchemaObjectShape(value) { return (typeof value === 'object' && value !== null && value.type === 'object' && 'properties' in value && 'additionalProperties' in value); } function zodJsonSchemaCompat(input) { return convertZodObjectSchema(input, { lowerObjectIntersections: false, loweredObjectIntersection: false, loweredPrimitiveIntersection: false, losslessForWholeSchemaFallback: true, })?.schema; } function zodJsonSchemaCompatForOpenAIStrict(input) { return convertZodObjectSchema(input, { lowerObjectIntersections: true, loweredObjectIntersection: false, loweredPrimitiveIntersection: false, losslessForWholeSchemaFallback: true, }); } function convertZodObjectSchema(input, context) { const rootDefinition = (0, zodCompat_1.readZodDefinition)(input); if (context.lowerObjectIntersections && !isLosslessStrictFallbackNode('object', rootDefinition)) { context.losslessForWholeSchemaFallback = false; } // Attempt to build an object schema from Zod's internal shape. If we cannot // understand the structure we return undefined, letting callers raise a // descriptive error instead of emitting an invalid schema. const schema = buildObjectSchema(input, context); if (!schema) { return undefined; } if (context.loweredObjectIntersection && !context.losslessForWholeSchemaFallback) { throwLossyZodIntersectionFallback(); } if (!Array.isArray(schema.required)) { schema.required = []; } if (typeof schema.additionalProperties === 'undefined') { schema.additionalProperties = false; } if (typeof schema.$schema !== 'string') { schema.$schema = JSON_SCHEMA_DRAFT_07; } return { schema: schema, loweredObjectIntersection: context.loweredObjectIntersection, loweredPrimitiveIntersection: context.loweredPrimitiveIntersection, losslessForWholeSchemaFallback: context.losslessForWholeSchemaFallback, }; } function assertLosslessOpenAIStrictZodSchemaConversion(conversion) { if (!conversion.losslessForWholeSchemaFallback) { throwLossyZodIntersectionFallback(); } } function throwLossyZodIntersectionFallback() { throw new errors_1.UserError('Cannot convert this Zod schema with an intersection to OpenAI strict mode without losing constraints. Use unconstrained compatible intersections, combine the fields into one Zod object, or disable strict mode.'); } function mergeJsonSchemaDescriptions(target, source) { if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null) { return; } const targetRecord = target; const sourceRecord = source; if (typeof sourceRecord.description === 'string' && sourceRecord.description.trim() && !('description' in targetRecord)) { targetRecord.description = sourceRecord.description; } if (targetRecord.type === 'object' && sourceRecord.type === 'object' && typeof targetRecord.properties === 'object' && targetRecord.properties !== null && typeof sourceRecord.properties === 'object' && sourceRecord.properties !== null) { for (const [key, value] of Object.entries(sourceRecord.properties)) { const targetValue = targetRecord.properties[key]; if (targetValue) { mergeJsonSchemaDescriptions(targetValue, value); } } } if (targetRecord.type === 'array' && sourceRecord.type === 'array' && 'items' in targetRecord && 'items' in sourceRecord) { const targetItems = targetRecord.items; const sourceItems = sourceRecord.items; if (Array.isArray(targetItems) && Array.isArray(sourceItems)) { const limit = Math.min(targetItems.length, sourceItems.length); for (let index = 0; index < limit; index += 1) { mergeJsonSchemaDescriptions(targetItems[index], sourceItems[index]); } } else if (typeof targetItems === 'object' && targetItems !== null && typeof sourceItems === 'object' && sourceItems !== null) { mergeJsonSchemaDescriptions(targetItems, sourceItems); } } for (const keyword of ['anyOf', 'allOf', 'oneOf']) { const targetArray = targetRecord[keyword]; const sourceArray = sourceRecord[keyword]; if (Array.isArray(targetArray) && Array.isArray(sourceArray)) { const limit = Math.min(targetArray.length, sourceArray.length); for (let index = 0; index < limit; index += 1) { mergeJsonSchemaDescriptions(targetArray[index], sourceArray[index]); } } } } function buildObjectSchema(value, context) { const shape = readShape(value); if (!shape) { return undefined; } const properties = {}; const required = []; for (const [key, field] of Object.entries(shape)) { const { schema, optional } = convertProperty(field, context); if (!schema) { return undefined; } const description = (0, zodCompat_1.readZodDescription)(field); if (description && typeof schema === 'object' && schema !== null && !('description' in schema)) { schema.description = description; } properties[key] = schema; if (!optional) { required.push(key); } } const schema = { type: 'object', properties, required, additionalProperties: false, }; const description = (0, zodCompat_1.readZodDescription)(value); if (description) { schema.description = description; } return schema; } function convertProperty(value, context) { if (context.lowerObjectIntersections && hasUnsupportedStrictFallbackDecorator(value)) { context.losslessForWholeSchemaFallback = false; } // Remove wrapper decorators (brand, transform, etc.) before attempting to // classify the node, tracking whether we crossed an `optional` boundary so we // can populate the `required` array later. let current = unwrapDecorators(value); let optional = false; while (OPTIONAL_WRAPPERS.has((0, zodCompat_1.readZodType)(current) ?? '')) { optional = true; const def = (0, zodCompat_1.readZodDefinition)(current); const next = unwrapDecorators(def?.innerType); if (!next || next === current) { break; } current = next; } return { schema: convertSchema(current, context), optional }; } function convertSchema(value, context) { if (value === undefined) { return undefined; } if (context.lowerObjectIntersections && hasUnsupportedStrictFallbackDecorator(value)) { context.losslessForWholeSchemaFallback = false; } const unwrapped = unwrapDecorators(value); const type = (0, zodCompat_1.readZodType)(unwrapped); const def = (0, zodCompat_1.readZodDefinition)(unwrapped); if (!type) { return undefined; } if (context.lowerObjectIntersections && !isLosslessStrictFallbackNode(type, def)) { context.losslessForWholeSchemaFallback = false; } let schema; if (type in SIMPLE_TYPE_MAPPING) { schema = { ...SIMPLE_TYPE_MAPPING[type] }; } else { switch (type) { case 'object': schema = buildObjectSchema(unwrapped, context); break; case 'array': schema = buildArraySchema(def, context); break; case 'tuple': schema = buildTupleSchema(def, context); break; case 'union': case 'discriminatedunion': schema = buildUnionSchema(def, context); break; case 'intersection': schema = buildIntersectionSchema(def, context); break; case 'literal': schema = buildLiteral(def); break; case 'enum': case 'nativeenum': schema = buildEnum(def); break; case 'record': schema = buildRecordSchema(def, context); break; case 'nullable': schema = buildNullableSchema(def, context); break; default: return undefined; } } const description = (0, zodCompat_1.readZodDescription)(value); if (schema && description) { schema.description = description; } return schema; } // --- JSON Schema builders ------------------------------------------------- function buildArraySchema(def, context) { const items = convertSchema(extractFirst(def, 'element', 'items', 'type'), context); return items ? { type: 'array', items } : undefined; } function buildTupleSchema(def, context) { const items = convertAllOrFail(coerceArray(def?.items), context); if (!items || !items.length) { return undefined; } const schema = { type: 'array', items, minItems: items.length, }; if (!def?.rest) { schema.maxItems = items.length; } return schema; } function buildUnionSchema(def, context) { const options = convertAllOrFail(coerceArray(def?.options ?? def?.schemas), context); return options && options.length ? { anyOf: options } : undefined; } /** * Converts every member or fails the whole collection. Silently dropping an * unconvertible member would emit a schema that looks valid but forbids * outputs the Zod schema accepts — e.g. a discriminated-union action variant * containing `z.preprocess` disappears from the union, so a model constrained * by the emitted schema can never produce that action. Returning `undefined` * propagates the failure so callers surface a descriptive error instead. */ function convertAllOrFail(members, context) { const converted = []; for (const member of members) { const schema = convertSchema(member, context); if (!schema) { return undefined; } converted.push(schema); } return converted; } function buildIntersectionSchema(def, context) { const leftSource = def?.left; const rightSource = def?.right; const left = convertSchema(leftSource, context); const right = convertSchema(rightSource, context); if (!left || !right) { return undefined; } if (!context.lowerObjectIntersections) { return { allOf: [left, right] }; } // OpenAI Structured Outputs does not support `allOf`. The strict tool path // therefore lowers only closed object intersections that can be represented // without changing what the authoritative Zod parser forwards to the tool. if (!isClosedZodObjectIntersectionOperand(leftSource) || !isClosedZodObjectIntersectionOperand(rightSource)) { const mergedPrimitive = mergePrimitiveIntersectionSchemas(left, right); if (mergedPrimitive) { context.loweredPrimitiveIntersection = true; return mergedPrimitive; } context.losslessForWholeSchemaFallback = false; return { allOf: [left, right] }; } const merged = mergeObjectIntersectionSchemas(left, right); if (!merged) { throwUnsupportedZodIntersection(); } context.loweredObjectIntersection = true; return merged; } function buildRecordSchema(def, context) { const valueSchema = convertSchema(def?.valueType ?? def?.values, context); return valueSchema ? { type: 'object', additionalProperties: valueSchema } : undefined; } function buildNullableSchema(def, context) { const inner = convertSchema(def?.innerType ?? def?.type, context); return inner ? { anyOf: [inner, { type: 'null' }] } : undefined; } function isClosedZodObjectIntersectionOperand(value) { const unwrapped = unwrapDecorators(value); const type = (0, zodCompat_1.readZodType)(unwrapped); if (type === 'intersection') { return true; } if (type !== 'object') { return false; } const def = (0, zodCompat_1.readZodDefinition)(unwrapped); const catchall = def?.catchall; // Zod v3 strict object intersections reject the other branch's distinct // properties during parsing, so flattening them would widen the provider // schema beyond what the authoritative parser accepts. if (!def || def.unknownKeys === 'strict' || def.unknownKeys === 'passthrough' || (typeof catchall !== 'undefined' && (0, zodCompat_1.readZodType)(catchall) !== 'never')) { throwUnsupportedZodIntersection(); } return true; } function mergeObjectIntersectionSchemas(left, right) { if (!isClosedJsonObjectSchema(left) || !isClosedJsonObjectSchema(right)) { return undefined; } const properties = { ...left.properties }; for (const [key, value] of Object.entries(right.properties)) { if (Object.prototype.hasOwnProperty.call(properties, key)) { return undefined; } properties[key] = value; } const required = new Set(); for (const schema of [left, right]) { if (Array.isArray(schema.required)) { for (const key of schema.required) { required.add(String(key)); } } } const descriptions = [left.description, right.description].filter((description) => typeof description === 'string'); return { type: 'object', properties, required: [...required], additionalProperties: false, ...(descriptions.length > 0 ? { description: descriptions.join('\n\n') } : {}), }; } function mergePrimitiveIntersectionSchemas(left, right) { if (typeof left !== 'object' || left === null || typeof right !== 'object' || right === null || typeof left.type !== 'string' || left.type !== right.type || !['string', 'number', 'boolean'].includes(left.type) || Object.keys(left).some((key) => !['type', 'description'].includes(key)) || Object.keys(right).some((key) => !['type', 'description'].includes(key))) { return undefined; } const descriptions = [left.description, right.description].filter((description) => typeof description === 'string'); return { type: left.type, ...(descriptions.length > 0 ? { description: descriptions.join('\n\n') } : {}), }; } function isClosedJsonObjectSchema(value) { return (typeof value === 'object' && value !== null && value.type === 'object' && typeof value.properties === 'object' && value.properties !== null && value.additionalProperties === false); } function throwUnsupportedZodIntersection() { throw new errors_1.UserError('Cannot convert this Zod intersection to an OpenAI strict schema. Use compatible Zod object intersections with distinct properties and closed object branches, combine the fields into one Zod object, or disable strict mode.'); } function hasUnsupportedStrictFallbackDecorator(value) { let current = value; while (true) { const type = (0, zodCompat_1.readZodType)(current); if (!type) { return false; } if (OPTIONAL_WRAPPERS.has(type)) { const inner = (0, zodCompat_1.readZodDefinition)(current)?.innerType; if (!inner || inner === current) { return false; } current = inner; continue; } if (!DECORATOR_WRAPPERS.has(type)) { return false; } if (!LOSSLESS_STRICT_FALLBACK_DECORATORS.has(type)) { return true; } const def = (0, zodCompat_1.readZodDefinition)(current); const next = def?.innerType ?? def?.schema ?? def?.base ?? def?.type ?? def?.wrapped ?? def?.underlying; if (!next || next === current) { return true; } current = next; } } function isLosslessStrictFallbackNode(type, def) { if (!def) { return false; } if (Array.isArray(def.checks) && def.checks.length > 0) { return false; } if (typeof def.check === 'string') { return false; } if (type === 'array' && [def.minLength, def.maxLength, def.exactLength].some((constraint) => constraint !== null && constraint !== undefined)) { return false; } if (type === 'tuple' && typeof def.rest !== 'undefined') { return false; } if (type === 'record') { return false; } if (type === 'object') { const catchall = def.catchall; return (def.unknownKeys !== 'passthrough' && (typeof catchall === 'undefined' || (0, zodCompat_1.readZodType)(catchall) === 'never')); } return true; } function unwrapDecorators(value) { let current = value; while (DECORATOR_WRAPPERS.has((0, zodCompat_1.readZodType)(current) ?? '')) { const def = (0, zodCompat_1.readZodDefinition)(current); const next = def?.innerType ?? def?.schema ?? def?.base ?? def?.type ?? def?.wrapped ?? def?.underlying; if (!next || next === current) { return current; } current = next; } return current; } function extractFirst(def, ...keys) { if (!def) { return undefined; } for (const key of keys) { if (key in def && def[key] !== undefined) { return def[key]; } } return undefined; } function coerceArray(value) { if (Array.isArray(value)) { return value; } return value === undefined ? [] : [value]; } function buildLiteral(def) { if (!def) { return undefined; } const literal = extractFirst(def, 'value', 'literal'); if (literal === undefined && Array.isArray(def.values)) { const values = def.values; if (values.length === 1) { const [value] = values; return { const: value, type: value === null ? 'null' : typeof value, }; } if (values.length > 1) { return { enum: values, type: [ ...new Set(values.map((value) => (value === null ? 'null' : typeof value))), ], }; } } if (literal === undefined) { return undefined; } return { const: literal, type: literal === null ? 'null' : typeof literal, }; } function buildEnum(def) { if (!def) { return undefined; } let values; if (Array.isArray(def.values)) { // Zod v3 z.enum() — values is a string array values = def.values; } else if (def.entries && typeof def.entries === 'object') { // Zod v4 z.enum() / z.nativeEnum() — entries is an object values = resolveNativeEnumValues(def.entries); } else if (Array.isArray(def.options)) { values = def.options; } else if (def.values && typeof def.values === 'object') { // Zod v3 z.nativeEnum() — values is the TS enum object values = resolveNativeEnumValues(def.values); } else if (def.enum && typeof def.enum === 'object') { // Fallback for alternative Zod internals values = resolveNativeEnumValues(def.enum); } if (!values || !values.length) { return undefined; } // @see https://github.com/StefanTerdell/zod-to-json-schema/blob/master/src/parsers/enum.ts const parsedTypes = Array.from(new Set(values.map((v) => typeof v))); const type = parsedTypes.length === 1 ? parsedTypes[0] === 'string' ? 'string' : 'number' : ['string', 'number']; return { type, enum: values }; } /** * Filter TypeScript's reverse-mapping keys from numeric native enums. * e.g. `enum E { A = 0 }` compiles to `{ A: 0, "0": "A" }` — keep only forward mappings. * @see https://github.com/StefanTerdell/zod-to-json-schema/blob/master/src/parsers/nativeEnum.ts#L12-L15 */ function resolveNativeEnumValues(enumObj) { const actualKeys = Object.keys(enumObj).filter((key) => typeof enumObj[enumObj[key]] !== 'number'); return actualKeys.map((key) => enumObj[key]); } function readShape(input) { if (typeof input !== 'object' || input === null) { return undefined; } const candidate = input; if (candidate.shape && typeof candidate.shape === 'object') { return candidate.shape; } if (typeof candidate.shape === 'function') { try { return candidate.shape(); } catch (_error) { return undefined; } } const def = (0, zodCompat_1.readZodDefinition)(candidate); const shape = def?.shape; if (shape && typeof shape === 'object') { return shape; } if (typeof shape === 'function') { try { return shape(); } catch (_error) { return undefined; } } return undefined; } //# sourceMappingURL=zodJsonSchemaCompat.js.map