diff --git a/.changeset/ninety-clubs-sink.md b/.changeset/ninety-clubs-sink.md new file mode 100644 index 00000000..9dfd23c1 --- /dev/null +++ b/.changeset/ninety-clubs-sink.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +fix: #316 developer-friendly message for output type errors diff --git a/packages/agents-core/src/runImplementation.ts b/packages/agents-core/src/runImplementation.ts index 2e24ef66..53c832ee 100644 --- a/packages/agents-core/src/runImplementation.ts +++ b/packages/agents-core/src/runImplementation.ts @@ -173,6 +173,44 @@ function getApprovalIdentity(approval: ApprovalItemLike): string | undefined { } } +function formatFinalOutputTypeError(error: unknown): string { + // Surface structured output validation hints without echoing potentially large or sensitive payloads. + try { + if (error instanceof z.ZodError) { + const issue = error.issues[0]; + if (issue) { + const issuePathParts = Array.isArray(issue.path) ? issue.path : []; + const issuePath = + issuePathParts.length > 0 + ? issuePathParts.map((part) => String(part)).join('.') + : '(root)'; + const message = truncateForDeveloper(issue.message ?? ''); + return `Invalid output type: final assistant output failed schema validation at "${issuePath}" (${message}).`; + } + return 'Invalid output type: final assistant output failed schema validation.'; + } + + if (error instanceof Error && error.message) { + return `Invalid output type: ${truncateForDeveloper(error.message)}`; + } + } catch { + // Swallow formatting errors so we can return a generic message below. + } + + return 'Invalid output type: final assistant output did not match the expected schema.'; +} + +function truncateForDeveloper(message: string, maxLength = 160): string { + const trimmed = message.trim(); + if (!trimmed) { + return 'Schema validation failed.'; + } + if (trimmed.length <= maxLength) { + return trimmed; + } + return `${trimmed.slice(0, maxLength - 3)}...`; +} + /** * @internal * Walks a raw model response and classifies each item so the runner can schedule follow-up work. @@ -898,13 +936,14 @@ export async function resolveTurnAfterModelResponse( ); const [error] = await safeExecute(() => parser(potentialFinalOutput)); if (error) { + const outputErrorMessage = formatFinalOutputTypeError(error); addErrorToCurrentSpan({ - message: 'Invalid output type', + message: outputErrorMessage, data: { error: String(error), }, }); - throw new ModelBehaviorError('Invalid output type'); + throw new ModelBehaviorError(outputErrorMessage); } return new SingleStepResult( diff --git a/packages/agents-core/test/runImplementation.test.ts b/packages/agents-core/test/runImplementation.test.ts index 9bb70c61..0e535104 100644 --- a/packages/agents-core/test/runImplementation.test.ts +++ b/packages/agents-core/test/runImplementation.test.ts @@ -2317,6 +2317,73 @@ describe('resolveTurnAfterModelResponse', () => { } }); + it('throws descriptive error when structured output validation fails', async () => { + const structuredAgent = new Agent({ + name: 'StructuredAgent', + outputType: z.object({ + summary: z.string(), + sections: z.array( + z.object({ + title: z.string(), + points: z.array( + z.object({ + label: z.string(), + score: z.number().min(0).max(1), + }), + ), + }), + ), + }), + }); + + const response: ModelResponse = { + output: [ + fakeModelMessage( + JSON.stringify({ + summary: 'Example', + sections: [ + { + title: 'One', + points: [{ label: 42, score: 0.5 }], + }, + ], + }), + ), + ], + usage: new Usage(), + } as any; + + const processedResponse = processModelResponse( + response, + structuredAgent, + [], + [], + ); + + const structuredState = new RunState( + new RunContext(), + 'test input', + structuredAgent, + 1, + ); + + await expect( + withTrace('test', () => + resolveTurnAfterModelResponse( + structuredAgent, + 'test input', + [], + response, + processedResponse, + runner, + structuredState, + ), + ), + ).rejects.toThrowError( + /Invalid output type: final assistant output failed schema validation at "sections\.0\.points\.0\.label" \(Expected string, received number\)./, + ); + }); + it('does not finalize after computer actions in the same turn; runs again', async () => { const computerAgent = new Agent({ name: 'ComputerAgent',