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,
},
},
};
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
}
}
Key engineering decisions:
type-checkhascache: 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.validate-agentsdepends on both^buildandtype-check: Ensures all upstream type definitions are compiled and validated before runtime agent schema validation runs.inputsexplicitly list schema paths: Without this, changing a deeply nested conditional type in@workspace/agents/src/schemas/provider.tswouldn't invalidate thevalidate-agentscache, 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)