diff --git a/src/agent/schema.ts b/src/agent/schema.ts new file mode 100644 index 0000000..5632e21 --- /dev/null +++ b/src/agent/schema.ts @@ -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; + +/** + * 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'], + }, + ], +};