Skip to content

ADR-0011: Schemas live in a separate package

ADR-0011: Schemas live in a separate package

Section titled “ADR-0011: Schemas live in a separate package”

Status: Accepted Date: 2026-04-20

ADR-0010 commits us to Zod. That raises a follow-on question: where do schemas live? Two options, each with a real trade-off.

  • Option A — schemas in @agent-platform/core. Single source of truth: define a schema once, derive the type via z.infer. Idiomatic Zod usage. But core becomes dependent on Zod, which means every consumer of core — including future Business Packs that only want types — inherits the Zod runtime dependency. ADR-0009 would be superseded.

  • Option B — separate @agent-platform/schemas package. Core stays types-only (ADR-0009 preserved). Schemas reference core types and assert equality at compile time. Consumers who only need types import from core; consumers who need validation import from schemas.

Option B. Schemas live in @agent-platform/schemas. Core types are authoritative; schemas are validated against them at compile time via Assert<Equals<z.infer<typeof Schema>, CoreType>>. If a schema drifts from its core type — by a field, by an enum value, by an optional-vs-required difference — the package fails to build.

  • ADR-0009 preserved. Consumers that only want types don’t pull Zod into their dependency graph. This matters when a Business Pack or a future UI package wants the AgentDefinition type without the 30KB validator library.
  • Two sources of truth, in theory. In practice, drift is caught at compile time, not at runtime or in review. The cost of this safety is the ~7 lines of Assert<Equals<>> declarations at the bottom of each schema file.
  • Clear split between contract and enforcement. core answers “what is the shape?”; schemas answers “is this input trustworthy?”. Separating these questions makes reasoning easier when the runtime gets complex.
  • Slightly more work per new type. Adding a type in core that needs runtime validation means adding a corresponding schema in schemas. Not all core types need schemas (see schemas/README.md — interfaces with function members are excluded), so this is not mechanical duplication.
  • Reversible. If single-source-of-truth pressure builds up (e.g., we find ourselves constantly editing two files to add a field), moving schemas into core is a package merge. The drift-check code becomes dead at that point and can be deleted.
  • @agent-platform/core has zero runtime dependencies. Still true.
  • @agent-platform/schemas depends on @agent-platform/core (workspace) and zod (runtime).
  • Future Business Packs depending on validated inputs will import from both; Business Packs that don’t do validation (unlikely but possible) can import from core alone.
  • Schemas in core (Option A above, supersedes ADR-0009). Single source of truth is genuinely elegant, but the dependency coupling is the dominant consideration at our size. If we later decide the Assert<Equals<>> pattern is more ceremony than it’s worth, merging is an easy path. Going the other direction (splitting after we’d collocated) is harder.
  • Generate schemas from types (codegen via TypeScript reflection). Tools in this space (e.g., ts-to-zod) exist but drop nominal types like branded IDs and sometimes lose readonly/exactOptional fidelity. We’re too strict for them.
  • Generate types from schemas. Inverse of the above: schemas as source, types derived via z.infer. Same problem as Option A (core must depend on Zod) plus the types become less readable — z.infer<typeof Foo> is worse to hover over than a plain interface.