The type registry started with a frustration. Jay was writing a Go client that produced turn payloads, and Navan was building React renderers that consumed them, and neither of them could be sure that the field names matched without looking at two files in two different languages and comparing them manually.
"This is how bugs happen," Jay said. "I call it toolCallId in Go. You call it tool_call_id in TypeScript. The Rust server doesn't care because it's storing Msgpack with numeric tags. But somewhere in the projection, one of us is wrong."
"We're both wrong," Navan said. "The point is that we shouldn't be naming things independently. There should be one source of truth."
Justin had been listening from across the room. "Build a registry."
The registry lived in the Go codebase, because Go was where the clients lived—the writers, the things that produced turns. Each type definition was a Go struct annotated with numeric field tags. Field 1: content, string. Field 2: role, string. Field 3: timestamp, int64. Field 4: tool_calls, array. The numbers were permanent. Once assigned, a field tag number could never be reused. Like protocol buffer field numbers, they were immutable contracts.
Jay wrote the first type definitions in an afternoon. Twenty turn types, covering every payload shape the agents currently produced: conversation messages, tool calls, tool results, error reports, system prompts, structured outputs.
The registry bundle was a JSON file generated from these Go definitions. It contained every type, every field, every tag number, every data type. The bundle was published to the Rust server, which loaded it on startup and used it for Msgpack-to-JSON projection. The same bundle was published to the React UI, which used it to generate TypeScript interfaces.
"One file," Navan said, holding up the registry bundle. "This one file defines the contract between three languages. Go writes it. Rust reads it for projection. TypeScript reads it for rendering. If a field exists in the registry, it exists everywhere. If it doesn't, it doesn't."
"And adding a new type?" Justin asked.
"Add the Go struct. Assign the field tags. Regenerate the bundle. Publish to the server and the UI. The Rust projector picks up the new type. The React renderer gets the new TypeScript interface. Two commands, five seconds."
Jay tested it by adding a new turn type: AgentThought. An internal monologue field that agents could use to record their reasoning. He defined the struct in Go, assigned three field tags, regenerated the bundle. The Rust server projected it without modification. Navan's generic renderer displayed it in the UI as a collapsible card.
"Five minutes," Jay said. "From new type to rendered in the browser. Five minutes."
"Without changing a single line of Rust," Navan added.
The registry grew over the following weeks, but it never became complicated. It was a list of types, a list of fields, a list of numbers. The simplest part of the system, and the part that held everything else together.
Immutable field tag numbers are underrated as an API contract mechanism. Once you assign tag 4 to tool_calls, it's tool_calls forever. No migration headaches.