In the previous chapter, Extensibility (Plugins & Hooks), we learned how to let outside developers add new features to our agent.
But with great power comes great complexity. If a user says "The agent crashed," and they have 5 different plugins installed, running on a specific version of Linux, inside a Docker container... how do you debug that?
This chapter introduces Telemetry & Event Contracts.
In a simple script, you might use console.log("Something went wrong").
In a complex AI agent, "Something went wrong" is useless.
We need a Flight Recorder. When an airplane flies, it records everything: altitude, speed, wind direction, engine temperature. It doesn't write this in a diary; it writes it in a strict, mathematical format that never changes.
Our Telemetry system does the same. It defines a Strict Contract (a specific shape of data) that must be sent whenever an event happens. This ensures that whether the event comes from a Mac, a Windows machine, or a GitHub Action, the data looks exactly the same to our analytics backend.
We don't just send JSON blobs. We use strictly typed interfaces generated from Protobufs (Protocol Buffers). This is a way of defining data structures that works across any programming language.
EnvironmentMetadata)Context is everything. If the agent fails, the first question is: "Where are we?" This structure captures the state of the machine.
export interface EnvironmentMetadata {
platform?: string // e.g., "darwin" (macOS)
node_version?: string // e.g., "v18.1.0"
terminal?: string // e.g., "iTerm.app"
is_ci?: boolean // Are we running in a robot (CI/CD)?
is_github_action?: boolean // Specific CI check
}
Timestamp)
Time is tricky. Timezones, leap seconds, and different formats make sorting logs a nightmare.
We use a standardized Timestamp object.
export interface Timestamp {
seconds?: number // Seconds since 1970 (Unix Epoch)
nanos?: number // Nanoseconds (for extreme precision)
}
"2023-01-01" and another sending "Jan 1st". The math must match.ClaudeCodeInternalEvent)This is the envelope that holds everything. It combines what happened with where it happened.
export interface ClaudeCodeInternalEvent {
event_name?: string // e.g., "tengu_api_success"
session_id?: string // The conversation ID from Chapter 2
env?: EnvironmentMetadata // The machine info from above
auth?: PublicApiAuth // Who is this user?
client_timestamp?: Date // When did it happen?
}
Let's imagine the agent just successfully fixed a bug in your code. We want to record this victory.
First, the system looks at where it is running.
const myEnv: EnvironmentMetadata = {
platform: 'darwin', // macOS
terminal: 'vscode',
node_version: 'v20.5.0',
is_ci: false // A real human is watching
}
We create the strictly typed event object. If we try to add a field that doesn't exist (like mood: 'happy'), TypeScript will yell at us. This is the "Contract" in action.
const successEvent: ClaudeCodeInternalEvent = {
event_name: 'bug_fix_success',
session_id: 'sess_12345',
env: myEnv,
// usage metrics
additional_metadata: JSON.stringify({
files_changed: 2,
duration_ms: 500
})
}
Before sending this over the internet, we convert it to a standard format (JSON or Binary).
// The generated code provides a helper for this
const payload = ClaudeCodeInternalEvent.toJSON(successEvent);
// Result: A clean object ready for the API
// { "event_name": "...", "env": { "platform": "darwin" } }
toJSON method ensures that complex types (like Dates) are converted to strings in the exact format the server expects.How does an event get from your code to the database?
Let's look at the generated files provided in the project. These files (.ts) are usually auto-generated from a .proto definition file.
MessageFns)
You'll notice objects like EnvironmentMetadata are accompanied by a constant with the same name. This holds the tools to work with that data type.
// From generated/.../claude_code_internal_event.ts
export const EnvironmentMetadata: MessageFns<EnvironmentMetadata> = {
// Converts raw JSON into the strict Type
fromJSON(object: any): EnvironmentMetadata {
return {
platform: isSet(object.platform) ? String(object.platform) : '',
is_ci: isSet(object.is_ci) ? Boolean(object.is_ci) : false,
// ... checks every single field manually
}
},
// Converts strict Type to JSON for sending
toJSON(message: EnvironmentMetadata): unknown {
// ...
}
}
null checks, type conversions (String to Number), and defaults (setting false if is_ci is missing).
The ClaudeCodeInternalEvent is designed to be a "Union" of all possible things we might want to track.
export interface ClaudeCodeInternalEvent {
// Core Data
event_name?: string
// Specific Contexts (Optional)
slack?: SlackContext // Only used if running in Slack
swe_bench_run_id?: string // Only used if running benchmarks
marketplace_name?: string // Only used if installed via marketplace
}
ClaudeCodeInternalEvent.Telemetry & Event Contracts provide the nervous system for our agent.
This "Flight Recorder" ensures that when we improve the Command Architecture or add new Permissions, we can scientifically measure if we are making the agent better or worse.
This concludes the beginner's guide to the types project architecture! You now understand the full loop:
Happy Coding!
Generated by Code IQ