Types & Interfaces

KERN has a full type system that compiles to TypeScript. Types, interfaces, discriminated unions, and configs — all defined in .kern, all emitted as idiomatic TS.

Type aliases

The type node creates union types or aliases. Use values for pipe-separated unions:

type name=StepState values="pending|running|completed|failed|skipped"
type name=ApprovalLevel values="auto|plan|step"
type name=StepEffect values="read|write|exec|network"

Compiles to:

export type StepState = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
export type ApprovalLevel = 'auto' | 'plan' | 'step';
export type StepEffect = 'read' | 'write' | 'exec' | 'network';

Aliasing existing types

Use alias instead of values to alias an existing type expression:

type name=UserMap alias="Record<string, User>"

Export control

Add export=false to keep a type internal:

type name=InternalState values="init|ready" export=false

Interfaces

The interface node defines object shapes with typed fields:

interface name=StepAttempt
  field name=startedAt type=string
  field name=finishedAt type=string optional=true
  field name=exitCode type=number optional=true
  field name=error type=string optional=true

Compiles to:

export interface StepAttempt {
  startedAt: string;
  finishedAt?: string;
  exitCode?: number;
  error?: string;
}

Fields

Each field child specifies a property:

  • name — field name
  • type — any TypeScript type: string, number, boolean, "Type[]", "Record<K,V>", unions
  • optional=true — generates the ? modifier

Complex field types

interface name=Plan
  field name=id type=string
  field name=steps type="PlanStep[]"
  field name=state type=PlanState
  field name=currentStepId type="string|null"
  field name=action type=PlanAction
  field name=workspace type=WorkspaceSnapshot

Quoted types allow any TypeScript expression: arrays, unions, generics, string literal unions like "'patch'|'diff'|'output'".

Inheritance

Use extends to inherit from another interface:

interface name=AdminUser extends=User
  field name=role type=string
  field name=permissions type="string[]"

Discriminated unions

The union node creates tagged unions with a discriminant field (defaults to type):

union name=ContentSegment discriminant=type
  variant name=prose
    field name=text type=string
  variant name=code
    field name=language type=string
    field name=code type=string

Compiles to:

export type ContentSegment =
  | { type: 'prose'; text: string }
  | { type: 'code'; language: string; code: string };

The discriminant field is automatically added to each variant. Variants can have optional=true fields just like interfaces.

Config

The config node generates an interface plus a defaults constant:

config name=AgonConfig
  field name=timeout type=number default=120
  field name=forgeEnableSynthesis type=boolean default=false
  field name=approvalLevel type=ApprovalLevel default=plan

Compiles to:

export interface AgonConfig {
  timeout?: number;
  forgeEnableSynthesis?: boolean;
  approvalLevel?: ApprovalLevel;
}

export const DEFAULT_AGON_CONFIG: Required<AgonConfig> = {
  timeout: 120,
  forgeEnableSynthesis: false,
  approvalLevel: 'plan',
};

Fields with default values become optional in the interface. The defaults object uses Required<T> to guarantee all fields are present.

Error classes

The error node generates typed Error subclasses:

error name=PlanStateError extends=AgonError message="Invalid plan state: expected ${expectedStr}, got ${actual}"
  field name=expected type="string|string[]"
  field name=actual type=string

Compiles to:

export class PlanStateError extends AgonError {
  constructor(
    public readonly expected: string | string[],
    public readonly actual: string,
  ) {
    const expectedStr = Array.isArray(expected)
      ? expected.join(' | ') : expected;
    super(`Invalid plan state: expected ${expectedStr}, got ${actual}`);
    this.name = 'PlanStateError';
  }
}

Fields become readonly constructor parameters. Array fields are auto-joined to strings when used in the message template.

Supported type expressions

Any valid TypeScript type expression works inside type="...":

PatternExample
Primitivesstring, number, boolean
Arrays"string[]", "PlanStep[]"
Unions"string|null", "'patch'|'diff'"
Generics"Record<string, unknown>", "Promise<Data>"
Function types"(key: string) => Promise<Data>"
Tuples"[string, number, boolean]"
Intersections"BaseUser & AdminPerms"

The parser is depth-aware — it correctly handles nested angle brackets, parentheses, and braces inside type expressions.

Full example

A real-world .kern file combining types, interfaces, and config:

type name=StepState values="pending|running|completed|failed|skipped"
type name=ApprovalLevel values="auto|plan|step"

interface name=ArtifactRef
  field name=type type="'patch'|'diff'|'output'|'manifest'"
  field name=path type=string
  field name=engineId type=string optional=true

interface name=Plan
  field name=id type=string
  field name=steps type="PlanStep[]"
  field name=state type=PlanState
  field name=currentStepId type="string|null"

config name=AgonConfig
  field name=timeout type=number default=120
  field name=approvalLevel type=ApprovalLevel default=plan

Next: Functions — how KERN declares functions, methods, and services.