Skip to content

ADR-0007: Branded ID types

Status: Accepted Date: 2026-04-20

The platform has several kinds of identifiers — AgentId, TaskId, ToolCallId, EventId, SessionId. At runtime they’re all strings. Function signatures that take more than one of these are easy to get wrong: pass a TaskId where an AgentId is expected and TypeScript happily accepts it.

This is the class of bug where the failure happens far from the mistake: a misrouted message, a delegation to the wrong agent, a looked-up record that doesn’t exist.

Define each ID type as a branded string:

export type AgentId = string & { readonly __brand: 'AgentId' };
export type TaskId = string & { readonly __brand: 'TaskId' };
// ...

Bare strings are not assignable to any of these types. The brand exists only at the type level — zero runtime cost.

  • Swapping the wrong ID into a function is a compile error.
  • Constructing an ID from an external string requires a cast (value as AgentId). Runtime packages will provide validated factories — cast inside the factory, then the rest of the codebase is safe.
  • The @agent-platform/core package deliberately does not export factory helpers, to stay types-only per ADR-0009.
  • Marginal cognitive overhead in type definitions. The payoff is catching bugs that would otherwise show up at runtime in production.
  • Serialization is a no-op — these ARE strings. JSON round-trips cleanly.
  • Plain strings: the status quo problem. No type safety.
  • Object wrappers (class AgentId { value: string }): runtime cost, ugly serialization, operator overloading doesn’t work in TS.
  • Unique-symbol brands (string & { [brand]: unique symbol }): slightly more opaque than string-literal brands; diagnostic messages are uglier. The string-literal approach is more readable without losing type safety.
  • Opaque types via a library (type-fest’s Opaque, or ts-brand): identical semantics, extra dependency. Not worth the import.