feat(08.1-01): define agent output schema with Zod

- Discriminated union with done/question/unrecoverable_error status
- Options schema for structured question choices
- JSON schema export for Claude CLI --json-schema flag
- Type export for runtime validation
This commit is contained in:
Lukas May
2026-01-31 15:21:09 +01:00
parent 3168b30185
commit 41598f577f

95
src/agent/schema.ts Normal file
View File

@@ -0,0 +1,95 @@
/**
* Agent Output Schema
*
* Defines structured output schema for Claude agents using discriminated unions.
* Replaces broken AskUserQuestion detection with explicit agent status signaling.
*/
import { z } from 'zod';
/**
* Option for questions - allows agent to present choices to user
*/
const optionSchema = z.object({
label: z.string(),
description: z.string().optional(),
});
/**
* Discriminated union for agent output.
*
* Agent must return one of:
* - done: Task completed successfully
* - question: Agent needs user input to continue
* - unrecoverable_error: Agent hit an error it cannot recover from
*/
export const agentOutputSchema = z.discriminatedUnion('status', [
// Agent completed successfully
z.object({
status: z.literal('done'),
result: z.string(),
filesModified: z.array(z.string()).optional(),
}),
// Agent needs user input to continue
z.object({
status: z.literal('question'),
question: z.string(),
options: z.array(optionSchema).optional(),
multiSelect: z.boolean().optional(),
}),
// Agent hit unrecoverable error
z.object({
status: z.literal('unrecoverable_error'),
error: z.string(),
attempted: z.string().optional(),
}),
]);
export type AgentOutput = z.infer<typeof agentOutputSchema>;
/**
* JSON Schema for --json-schema flag (convert Zod to JSON Schema).
* This is passed to Claude CLI to enforce structured output.
*/
export const agentOutputJsonSchema = {
type: 'object',
oneOf: [
{
properties: {
status: { const: 'done' },
result: { type: 'string' },
filesModified: { type: 'array', items: { type: 'string' } },
},
required: ['status', 'result'],
},
{
properties: {
status: { const: 'question' },
question: { type: 'string' },
options: {
type: 'array',
items: {
type: 'object',
properties: {
label: { type: 'string' },
description: { type: 'string' },
},
required: ['label'],
},
},
multiSelect: { type: 'boolean' },
},
required: ['status', 'question'],
},
{
properties: {
status: { const: 'unrecoverable_error' },
error: { type: 'string' },
attempted: { type: 'string' },
},
required: ['status', 'error'],
},
],
};