Welcome back! In the previous chapter, Model Context Protocol (MCP) Server, we built the "telephone line" that allows our agent to talk to the outside world.
But having a telephone line isn't enough. If you pick up the phone and speak French, but the person on the other end only speaks Japanese, you can't communicate.
In this chapter, we will define the Language and Grammar of our application. We call this the Data Model. It ensures that when the CLI, the Agent, and the UI talk to each other, they don't misunderstand the messages.
Imagine a team of three developers working on the same app:
{ "text": "Hello" }{ "content": "Hello" }{ "msg": "Hello" }The Problem: The application crashes because everyone is using different names for the same data.
The Solution:
We create Strict Schemas (Blueprints). We agree on a single "Source of Truth." If the blueprint says the field is named message, everyone must use message. If someone tries to send text, the system rejects it immediately.
Zod)To enforce these rules, we use a library called Zod. Think of Zod as a strict border control officer.
uuid string").Let's solve a real problem. The user is running a long, complex task, and they want to click a "Stop" button in the UI to interrupt the AI.
How do we define this "Stop" signal so the Agent understands it instantly?
We define this in sdk/controlSchemas.ts. We want a specific command called interrupt.
import { z } from 'zod'; // Import the validator
// Define the blueprint for an Interrupt Request
export const InterruptRequestSchema = z.object({
// The "subtype" acts like a stamp identifying the message type
subtype: z.literal('interrupt'),
});
Explanation:
This code says: "A valid Interrupt Request is an object that has exactly one property: subtype, and its value must be the word 'interrupt'."
The agent needs to understand many commands (Interrupt, Set Model, Get Settings). We group them into a "Union" (a list of allowed options).
// A control request can be ONE of these things
export const ControlRequestSchema = z.union([
InterruptRequestSchema,
SetModelRequestSchema,
GetSettingsRequestSchema,
]);
Explanation: This tells the system: "If a control message comes in, it must be an Interrupt OR a Set Model command OR a Get Settings command. Anything else is garbage."
When a message arrives from the UI, we check it against our blueprint.
// Incoming data from the network (we don't trust it yet)
const incomingData = { subtype: "interrupt" };
try {
// Zod checks the data against the blueprint
const validCommand = ControlRequestSchema.parse(incomingData);
console.log("Valid command received!");
} catch (error) {
console.error("Invalid data format!");
}
Explanation:
If incomingData was { subtype: "dance" }, the parse function would explode with an error because "dance" is not in our approved list of commands.
How does the data flow through the system? Let's visualize the "Border Control" process.
If you look at sdk/coreSchemas.ts, you will see something peculiar called lazySchema.
import { lazySchema } from '../../utils/lazySchema.js';
// We wrap the schema in a function
export const SDKUserMessageSchema = lazySchema(() =>
z.object({
type: z.literal('user'),
message: z.string(),
uuid: z.string(),
})
);
Why do we do this? Recall Chapter 1 CLI Entrypoint & Dispatch. We care deeply about Startup Speed.
Standard schemas run code the moment the file is loaded. If we have 500 definitions, defining them all takes time (CPU cycles), even if we only need one.
The project defines a massive "Union" type called SDKMessageSchema in sdk/coreSchemas.ts. This is the dictionary of every possible thing the Agent can say.
export const SDKMessageSchema = lazySchema(() =>
z.union([
SDKAssistantMessageSchema(), // The AI talking
SDKUserMessageSchema(), // The User talking
SDKResultSuccessSchema(), // A tool finished successfully
SDKResultErrorSchema(), // A tool failed
SDKStatusMessageSchema(), // "I am thinking..."
])
);
This ensures that the Agent SDK (which we built in Chapter 3) can safely type-check every message history.
We don't want to write the Zod blueprint and the TypeScript interface separately. That violates "Don't Repeat Yourself."
Zod allows us to infer the TypeScript type directly from the blueprint.
// coreTypes.ts
// Automatically create the TypeScript type from the Zod blueprint
export type SDKMessage = z.infer<typeof SDKMessageSchema>;
The Result:
When developers are writing code in the SDK, their IDE (VS Code) knows exactly what fields exist on a message. If they try to access message.content but the field is actually message.text, the editor highlights it in red.
In this chapter, we learned that the Data Model & Communication Protocol is the dictionary and grammar book of our application.
By strictly defining what an "Interrupt" signal looks like, we ensure that when the user presses Stop, the Agent stopsβevery single time.
Now that our Agent can talk to the world and understands the language, we need to make sure it doesn't accidentally delete your entire hard drive while trying to help you.
Next Chapter: Sandboxing & Security Configuration
Generated by Code IQ