v0.5.2 release - Contributors, Sponsors and Enquiries are most welcome 😌

Structured Output

Type-safe structured outputs with JSON schema validation, Zod integration, and automatic retries for reliable LLM responses.

Structured Output ensures your LLM responses conform to exact schemas with automatic validation, type inference, and retry logic for malformed responses.

Installation

bash
pnpm add @lov3kaizen/agentsea-structured-output

Key Features

📐

Zod Integration

First-class Zod support with full type inference

🔄

Auto Retries

Automatic retry on malformed responses

Validation

JSON Schema validation with detailed errors

🔒

Type Safety

Full TypeScript inference from schemas

🌊

Streaming

Stream partial structured results

🔌

Multi-Provider

Works with OpenAI, Anthropic, Google, Ollama

Quick Start with Zod

Define your schema with Zod and get fully typed responses:

typescript
import { structuredOutput } from '@lov3kaizen/agentsea-structured-output';
import { z } from 'zod';

// Define your schema
const ProductSchema = z.object({
  name: z.string().describe('Product name'),
  price: z.number().positive().describe('Price in USD'),
  category: z.enum(['electronics', 'clothing', 'food']),
  inStock: z.boolean(),
  tags: z.array(z.string()).optional(),
});

// Get typed response
const result = await structuredOutput({
  model: 'gpt-4o',
  schema: ProductSchema,
  prompt: 'Extract product info: iPhone 15 Pro, $999, electronics, available',
});

// result is fully typed as z.infer<typeof ProductSchema>
console.log(result.name);     // "iPhone 15 Pro"
console.log(result.price);    // 999
console.log(result.category); // "electronics"
console.log(result.inStock);  // true

With AgentSea Agent

Use structured output with your existing agents:

typescript
import { Agent } from '@lov3kaizen/agentsea-core';
import { withStructuredOutput } from '@lov3kaizen/agentsea-structured-output';
import { z } from 'zod';

const agent = new Agent({
  provider: 'openai',
  model: 'gpt-4o',
});

const SentimentSchema = z.object({
  sentiment: z.enum(['positive', 'negative', 'neutral']),
  confidence: z.number().min(0).max(1),
  reasoning: z.string(),
});

// Wrap agent with structured output
const structuredAgent = withStructuredOutput(agent, SentimentSchema);

const result = await structuredAgent.run(
  'Analyze: "This product exceeded my expectations!"'
);

console.log(result.sentiment);   // "positive"
console.log(result.confidence);  // 0.95
console.log(result.reasoning);   // "The phrase 'exceeded expectations'..."

JSON Schema Support

Use standard JSON Schema if you prefer:

typescript
import { structuredOutput } from '@lov3kaizen/agentsea-structured-output';

const schema = {
  type: 'object',
  properties: {
    title: { type: 'string' },
    author: { type: 'string' },
    year: { type: 'integer', minimum: 1900 },
    genres: {
      type: 'array',
      items: { type: 'string' },
    },
  },
  required: ['title', 'author', 'year'],
} as const;

const result = await structuredOutput({
  model: 'gpt-4o',
  jsonSchema: schema,
  prompt: 'Extract book info: "1984 by George Orwell, published 1949, dystopian fiction"',
});

Automatic Retries

Handle malformed responses with automatic retry logic:

typescript
import { structuredOutput } from '@lov3kaizen/agentsea-structured-output';
import { z } from 'zod';

const StrictSchema = z.object({
  email: z.string().email(),
  age: z.number().int().positive().max(150),
  website: z.string().url().optional(),
});

const result = await structuredOutput({
  model: 'gpt-4o',
  schema: StrictSchema,
  prompt: 'Extract user info from: john.doe@email.com, 28 years old',
  retry: {
    maxAttempts: 3,
    onRetry: (error, attempt) => {
      console.log(`Attempt ${attempt} failed: ${error.message}`);
    },
  },
});

Complex Nested Schemas

Handle deeply nested structures with full type safety:

typescript
import { z } from 'zod';

const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string(),
  postalCode: z.string(),
});

const OrderItemSchema = z.object({
  productId: z.string(),
  name: z.string(),
  quantity: z.number().int().positive(),
  unitPrice: z.number().positive(),
});

const OrderSchema = z.object({
  orderId: z.string(),
  customer: z.object({
    name: z.string(),
    email: z.string().email(),
    address: AddressSchema,
  }),
  items: z.array(OrderItemSchema),
  total: z.number().positive(),
  status: z.enum(['pending', 'processing', 'shipped', 'delivered']),
  createdAt: z.string().datetime(),
});

const result = await structuredOutput({
  model: 'gpt-4o',
  schema: OrderSchema,
  prompt: `Extract order details from this email: ...order confirmation...`,
});

// Full type inference for nested objects
console.log(result.customer.address.city);
console.log(result.items[0].quantity);

Streaming Structured Output

Stream partial results as they become available:

typescript
import { streamStructuredOutput } from '@lov3kaizen/agentsea-structured-output';
import { z } from 'zod';

const ArticleSchema = z.object({
  title: z.string(),
  summary: z.string(),
  keyPoints: z.array(z.string()),
  wordCount: z.number(),
});

const stream = await streamStructuredOutput({
  model: 'gpt-4o',
  schema: ArticleSchema,
  prompt: 'Summarize this article...',
});

for await (const partial of stream) {
  // Receive partial results as fields are completed
  if (partial.title) console.log('Title:', partial.title);
  if (partial.keyPoints) console.log('Points so far:', partial.keyPoints.length);
}

// Final validated result
const result = await stream.finalResult();

Provider Support

Works with all major providers, using native structured output when available:

ProviderNative SupportMethod
OpenAI (GPT-4o)Yesresponse_format with json_schema
Anthropic (Claude)YesTool use with schema
Google (Gemini)YesresponseSchema parameter
OllamaPartialJSON mode + validation
OtherFallbackPrompt engineering + validation

Validation Modes

Choose how strictly to validate responses:

typescript
import { structuredOutput } from '@lov3kaizen/agentsea-structured-output';

// Strict mode - fail on any validation error
const result = await structuredOutput({
  schema: MySchema,
  prompt: '...',
  validation: 'strict',
});

// Coerce mode - attempt to coerce types (e.g., "42" -> 42)
const result = await structuredOutput({
  schema: MySchema,
  prompt: '...',
  validation: 'coerce',
});

// Partial mode - return partial results even if incomplete
const result = await structuredOutput({
  schema: MySchema,
  prompt: '...',
  validation: 'partial',
});

Error Handling

Handle validation errors gracefully:

typescript
import {
  structuredOutput,
  StructuredOutputError,
  ValidationError,
  MaxRetriesError,
} from '@lov3kaizen/agentsea-structured-output';

try {
  const result = await structuredOutput({
    schema: MySchema,
    prompt: '...',
  });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log('Schema validation failed:', error.issues);
    console.log('Raw response:', error.rawResponse);
  } else if (error instanceof MaxRetriesError) {
    console.log('Max retries exceeded:', error.attempts);
    console.log('Last error:', error.lastError);
  } else if (error instanceof StructuredOutputError) {
    console.log('Structured output error:', error.message);
  }
}

Configuration

typescript
interface StructuredOutputConfig {
  // The model to use
  model: string;

  // Zod schema for validation and type inference
  schema?: ZodSchema;

  // JSON Schema (alternative to Zod)
  jsonSchema?: JSONSchema;

  // The prompt/messages to send
  prompt: string | Message[];

  // Provider configuration
  provider?: ProviderConfig;

  // Validation mode
  validation?: 'strict' | 'coerce' | 'partial';

  // Retry configuration
  retry?: {
    maxAttempts: number;
    backoff?: 'linear' | 'exponential';
    onRetry?: (error: Error, attempt: number) => void;
  };

  // Temperature (lower = more deterministic)
  temperature?: number;
}

Best Practices

  • Use descriptive field names - LLMs understand semantic meaning from names
  • Add .describe() - Provide hints for ambiguous fields
  • Set reasonable constraints - Use min/max to guide valid ranges
  • Use enums for categories - Limit options to valid choices
  • Lower temperature for structured output - Recommended 0-0.3 for more deterministic results

Next Steps