diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 1d84dbca0c..2c74912a47 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -2373,6 +2373,71 @@ describe("Codex app-server provider", () => { }); }); + test("keeps an output field named properties as a field", () => { + const input = { + type: "object", + properties: { properties: { type: "string" } }, + required: ["properties"], + additionalProperties: false, + }; + + expect(normalizeCodexOutputSchema(input)).toEqual(input); + }); + + test("normalizes schemas under keywords and leaves literal values alone", () => { + const literal = { type: "object", properties: { type: "string" } }; + const input = { + type: "object", + properties: { + result: { $ref: "#/$defs/result" }, + choice: { anyOf: [{ type: "object", properties: { id: { type: "string" } } }] }, + }, + $defs: { + properties: { type: "string" }, + result: { + type: "object", + properties: { value: { type: "string" } }, + default: literal, + examples: [literal], + enum: [literal], + const: literal, + }, + }, + }; + + expect(normalizeCodexOutputSchema(input)).toEqual({ + type: "object", + properties: { + result: { $ref: "#/$defs/result" }, + choice: { + anyOf: [ + { + type: "object", + properties: { id: { type: "string" } }, + required: ["id"], + additionalProperties: false, + }, + ], + }, + }, + required: ["result", "choice"], + additionalProperties: false, + $defs: { + properties: { type: "string" }, + result: { + type: "object", + properties: { value: { type: "string" } }, + default: literal, + examples: [literal], + enum: [literal], + const: literal, + required: ["value"], + additionalProperties: false, + }, + }, + }); + }); + test("passes a normalized output schema to turn/start", async () => { const session = createSession(); const request = vi.fn(async (method: string) => { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 1d0aca552c..f16c5fb6d8 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -365,19 +365,67 @@ function isObjectSchemaNode(schema: Record): boolean { ); } -function normalizeCodexOutputSchemaNode(schema: unknown, schemaPath: string): unknown { +// Keywords whose value maps names to schemas. +const SCHEMA_MAP_KEYWORDS = [ + "properties", + "patternProperties", + "$defs", + "definitions", + "dependentSchemas", +] as const; + +// Keywords whose value is a schema or an array of schemas. +const SUBSCHEMA_KEYWORDS = [ + "items", + "prefixItems", + "additionalItems", + "contains", + "additionalProperties", + "unevaluatedProperties", + "unevaluatedItems", + "propertyNames", + "allOf", + "anyOf", + "oneOf", + "not", + "if", + "then", + "else", +] as const; + +function normalizeCodexOutputSubschema(schema: unknown, schemaPath: string): unknown { if (Array.isArray(schema)) { return schema.map((entry, index) => normalizeCodexOutputSchemaNode(entry, `${schemaPath}[${index}]`), ); } + return normalizeCodexOutputSchemaNode(schema, schemaPath); +} + +function normalizeCodexOutputSchemaNode(schema: unknown, schemaPath: string): unknown { if (!isSchemaRecord(schema)) { return schema; } - const normalized: Record = {}; - for (const [key, value] of Object.entries(schema)) { - normalized[key] = normalizeCodexOutputSchemaNode(value, `${schemaPath}.${key}`); + const normalized: Record = { ...schema }; + for (const keyword of SCHEMA_MAP_KEYWORDS) { + const schemas = schema[keyword]; + if (isSchemaRecord(schemas)) { + normalized[keyword] = Object.fromEntries( + Object.entries(schemas).map(([name, child]) => [ + name, + normalizeCodexOutputSchemaNode(child, `${schemaPath}.${keyword}.${name}`), + ]), + ); + } + } + for (const keyword of SUBSCHEMA_KEYWORDS) { + if (keyword in schema) { + normalized[keyword] = normalizeCodexOutputSubschema( + schema[keyword], + `${schemaPath}.${keyword}`, + ); + } } if (!isObjectSchemaNode(normalized)) {