feat(11-01): create mode-specific output schemas

- Export questionItemSchema and add QuestionItem type
- Add Decision type for discuss mode (topic/decision/reason)
- Add PhaseBreakdown type for breakdown mode
- Create discussOutputSchema (questions/context_complete/error)
- Create breakdownOutputSchema (questions/breakdown_complete/error)
- Add discussOutputJsonSchema for Claude CLI --json-schema
- Add breakdownOutputJsonSchema for Claude CLI --json-schema
This commit is contained in:
Lukas May
2026-01-31 19:05:27 +01:00
parent 67cfd4d201
commit 3f8d6d5357

View File

@@ -3,10 +3,19 @@
* *
* Defines structured output schema for Claude agents using discriminated unions. * Defines structured output schema for Claude agents using discriminated unions.
* Replaces broken AskUserQuestion detection with explicit agent status signaling. * Replaces broken AskUserQuestion detection with explicit agent status signaling.
*
* Mode-specific schemas:
* - execute: Standard task execution (done/questions/error)
* - discuss: Gather context through questions, output decisions
* - breakdown: Decompose initiative into phases
*/ */
import { z } from 'zod'; import { z } from 'zod';
// =============================================================================
// SHARED SCHEMAS
// =============================================================================
/** /**
* Option for questions - allows agent to present choices to user * Option for questions - allows agent to present choices to user
*/ */
@@ -18,15 +27,46 @@ const optionSchema = z.object({
/** /**
* Individual question item with unique ID for answer matching * Individual question item with unique ID for answer matching
*/ */
const questionItemSchema = z.object({ export const questionItemSchema = z.object({
id: z.string(), id: z.string(),
question: z.string(), question: z.string(),
options: z.array(optionSchema).optional(), options: z.array(optionSchema).optional(),
multiSelect: z.boolean().optional(), multiSelect: z.boolean().optional(),
}); });
export type QuestionItem = z.infer<typeof questionItemSchema>;
/** /**
* Discriminated union for agent output. * A decision captured during discussion.
* Prompt instructs: { "topic": "Auth", "decision": "JWT", "reason": "Stateless" }
*/
const decisionSchema = z.object({
topic: z.string(),
decision: z.string(),
reason: z.string(),
});
export type Decision = z.infer<typeof decisionSchema>;
/**
* A phase from breakdown output.
* Prompt instructs: { "number": 1, "name": "...", "description": "...", "dependencies": [0] }
*/
const phaseBreakdownSchema = z.object({
number: z.number().int().positive(),
name: z.string().min(1),
description: z.string(),
dependencies: z.array(z.number().int()).optional().default([]),
});
export type PhaseBreakdown = z.infer<typeof phaseBreakdownSchema>;
// =============================================================================
// EXECUTE MODE SCHEMA (default)
// =============================================================================
/**
* Discriminated union for agent output in execute mode.
* *
* Agent must return one of: * Agent must return one of:
* - done: Task completed successfully * - done: Task completed successfully
@@ -111,3 +151,198 @@ export const agentOutputJsonSchema = {
}, },
], ],
}; };
// =============================================================================
// DISCUSS MODE SCHEMA
// =============================================================================
/**
* Discuss mode output schema.
* Agent asks questions OR completes with decisions.
*
* Prompt tells agent:
* - Output "questions" status with questions array when needing input
* - Output "context_complete" status with decisions array when done
*/
export const discussOutputSchema = z.discriminatedUnion('status', [
// Agent needs more information
z.object({
status: z.literal('questions'),
questions: z.array(questionItemSchema),
}),
// Agent has captured all decisions
z.object({
status: z.literal('context_complete'),
decisions: z.array(decisionSchema),
summary: z.string(), // Brief summary of all decisions
}),
// Unrecoverable error
z.object({
status: z.literal('unrecoverable_error'),
error: z.string(),
}),
]);
export type DiscussOutput = z.infer<typeof discussOutputSchema>;
/**
* JSON Schema for discuss mode (passed to Claude CLI --json-schema)
*/
export const discussOutputJsonSchema = {
type: 'object',
oneOf: [
{
properties: {
status: { const: 'questions' },
questions: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
question: { type: 'string' },
options: {
type: 'array',
items: {
type: 'object',
properties: {
label: { type: 'string' },
description: { type: 'string' },
},
required: ['label'],
},
},
multiSelect: { type: 'boolean' },
},
required: ['id', 'question'],
},
},
},
required: ['status', 'questions'],
},
{
properties: {
status: { const: 'context_complete' },
decisions: {
type: 'array',
items: {
type: 'object',
properties: {
topic: { type: 'string' },
decision: { type: 'string' },
reason: { type: 'string' },
},
required: ['topic', 'decision', 'reason'],
},
},
summary: { type: 'string' },
},
required: ['status', 'decisions', 'summary'],
},
{
properties: {
status: { const: 'unrecoverable_error' },
error: { type: 'string' },
},
required: ['status', 'error'],
},
],
};
// =============================================================================
// BREAKDOWN MODE SCHEMA
// =============================================================================
/**
* Breakdown mode output schema.
* Agent asks questions OR completes with phases.
*
* Prompt tells agent:
* - Output "questions" status when needing clarification
* - Output "breakdown_complete" status with phases array when done
*/
export const breakdownOutputSchema = z.discriminatedUnion('status', [
// Agent needs clarification
z.object({
status: z.literal('questions'),
questions: z.array(questionItemSchema),
}),
// Agent has decomposed initiative into phases
z.object({
status: z.literal('breakdown_complete'),
phases: z.array(phaseBreakdownSchema),
}),
// Unrecoverable error
z.object({
status: z.literal('unrecoverable_error'),
error: z.string(),
}),
]);
export type BreakdownOutput = z.infer<typeof breakdownOutputSchema>;
/**
* JSON Schema for breakdown mode (passed to Claude CLI --json-schema)
*/
export const breakdownOutputJsonSchema = {
type: 'object',
oneOf: [
{
properties: {
status: { const: 'questions' },
questions: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
question: { type: 'string' },
options: {
type: 'array',
items: {
type: 'object',
properties: {
label: { type: 'string' },
description: { type: 'string' },
},
required: ['label'],
},
},
},
required: ['id', 'question'],
},
},
},
required: ['status', 'questions'],
},
{
properties: {
status: { const: 'breakdown_complete' },
phases: {
type: 'array',
items: {
type: 'object',
properties: {
number: { type: 'integer', minimum: 1 },
name: { type: 'string', minLength: 1 },
description: { type: 'string' },
dependencies: {
type: 'array',
items: { type: 'integer' },
},
},
required: ['number', 'name', 'description'],
},
},
},
required: ['status', 'phases'],
},
{
properties: {
status: { const: 'unrecoverable_error' },
error: { type: 'string' },
},
required: ['status', 'error'],
},
],
};