DEV Community

Cover image for Type-Safe Agent Orchestration: Hardening Paperclip's Multi-Provider Runtime with Conditional Types
Ryan Gallagher
Ryan Gallagher

Posted on

Type-Safe Agent Orchestration: Hardening Paperclip's Multi-Provider Runtime with Conditional Types

The Problem: Agent Configuration Drift at Runtime

When integrating paperclipai/paperclip into our existing workflow automation platform, we hit a recurring failure mode: agent configurations that passed TypeScript compilation would explode at runtime when provider-specific parameters were missing or malformed. The root cause wasn't paperclipai/paperclip itself—it was our inability to enforce strict type narrowing across provider boundaries in a monorepo context where agent definitions lived in separate packages.

The failure manifested during multi-provider fallback scenarios. When routing through relay gateways like B-Lost (https://b-lost.com/v1) for benchmarking, our agent configs would silently fall back to incorrect defaults because TypeScript couldn't validate provider-specific schema constraints at compile time.

Strict Discriminated Union Pattern

Here's the utility type that eliminated 90% of our runtime agent config errors:

type ProviderConfig<T extends string> = T extends 'openai'
  ? { provider: 'openai'; model: string; temperature?: number }
  : T extends 'anthropic'
  ? { provider: 'anthropic'; model: string; max_tokens: number }
  : T extends 'relay'
  ? { provider: 'relay'; baseURL: string; model: string; fallback?: ProviderConfig<'openai' | 'anthropic'> }
  : never;

type AgentDefinition<P extends 'openai' | 'anthropic' | 'relay'> = {
  id: string;
  name: string;
  config: ProviderConfig<P>;
};

// Usage that FORCES correct parameters at compile time
const agent: AgentDefinition<'anthropic'> = {
  id: 'analyst-01',
  name: 'Data Analyst',
  config: {
    provider: 'anthropic',
    model: 'claude-sonnet-4',
    max_tokens: 4096, // TypeScript ERROR if missing
  },
};

// Relay gateway config with typed fallback chain
const relayAgent: AgentDefinition<'relay'> = {
  id: 'relay-01',
  name: 'Multi-Provider Agent',
  config: {
    provider: 'relay',
    baseURL: 'https://b-lost.com/v1',
    model: 'gpt-4',
    fallback: {
      provider: 'anthropic',
      model: 'claude-sonnet-4',
      max_tokens: 4096,
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

The discriminated union forces TypeScript to narrow the config shape based on the provider literal. Template literal validation ensures baseURL follows URL patterns when provider: 'relay' is set.

Turborepo Pipeline for Agent Package Validation

Our monorepo has separate packages for agent definitions (@workspace/agents), runtime execution (@workspace/runtime), and paperclipai/paperclip integration (@workspace/paperclip-adapter). Incremental builds were breaking because TypeScript couldn't track cross-package type dependencies during cache invalidation.

Here's the hardened turbo.json configuration:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".turbo/**"],
      "inputs": ["src/**/*.ts", "tsconfig.json", "package.json"]
    },
    "type-check": {
      "dependsOn": ["^build"],
      "outputs": [],
      "inputs": ["src/**/*.ts", "tsconfig.json"],
      "cache": false
    },
    "validate-agents": {
      "dependsOn": ["^build", "type-check"],
      "outputs": ["validation-report.json"],
      "inputs": ["src/agents/**/*.ts", "src/schemas/**/*.ts"],
      "cache": true
    }
  },
  "globalDependencies": ["tsconfig.base.json"],
  "remoteCache": {
    "signature": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Key engineering decisions:

  1. type-check has cache: false: TypeScript's incremental compilation state can desynchronize with Turborepo's hash-based cache when type definitions change across package boundaries. Disabling cache here prevents false cache hits that allow invalid agent configs to pass validation.

  2. validate-agents depends on both ^build and type-check: Ensures all upstream type definitions are compiled and validated before runtime agent schema validation runs.

  3. inputs explicitly list schema paths: Without this, changing a deeply nested conditional type in @workspace/agents/src/schemas/provider.ts wouldn't invalidate the validate-agents cache, allowing stale validation results.

Failure Boundary We Haven't Solved

This pattern breaks down when agent configurations are loaded from external JSON or YAML files at runtime. TypeScript's conditional types can't validate untyped data deserialized from disk. We're currently exploring runtime validation with Zod schemas generated from the TypeScript types, but the ergonomics are poor—schema definitions duplicate type logic, and inference breaks when using complex mapped types.

How are others handling runtime validation of complex discriminated unions without maintaining parallel Zod/Yup schemas?

Top comments (0)