In the previous chapter, Tool Orchestrator, we introduced the FileEditTool as our "General Contractor." But a contractor cannot build a house based on a vague feeling. They need a blueprint.
In this chapter, we explore how we enforce strict rules on what data comes in and goes out of our tool.
Imagine you are at a restaurant.
Large Language Models (like Claude or GPT) are very smart, but they can be vague. If we ask the AI to "fix the typo," it might try to send us:
We cannot write code that handles infinite possibilities. We need a Contract.
We are still fixing hello.txt.
To make this happen safely, we need a rigid form that the AI must fill out. It must provide:
file_path)old_string)new_string)
If any of these are missing or wrong (e.g., file_path is a number instead of text), we must reject it immediately.
To enforce this contract, we use a library called Zod. Think of Zod as a bouncer at a club. It stands at the door of our function.
file_path must be a STRING. Access denied."This ensures that inside our actual logic, we never have to worry about weird data types.
Let's look at how we define this contract in types.ts. We use z.object to define the shape of the data.
We need three strings to perform an edit.
// File: types.ts
import { z } from 'zod/v4'
// Define the shape of valid input
const basicSchema = z.object({
file_path: z.string(),
old_string: z.string(),
new_string: z.string(),
})
Explanation: This tells our program: "I expect an object with exactly these three keys, and they all must be text strings."
The Schema isn't just for validation; it's also documentation. We use .describe() to tell the AI what to put in those fields.
// File: types.ts
z.strictObject({
file_path: z.string()
.describe('The absolute path to the file to modify'),
old_string: z.string()
.describe('The text to replace'),
new_string: z.string()
.describe('The text to replace it with'),
})
Explanation: When the AI asks "How do I use this tool?", it reads these descriptions. It's like the label on a form field. strictObject means "Do not send me any extra fields I didn't ask for."
Sometimes we want to replace every instance of a typo, not just the first one. This is optional.
// File: types.ts
// import { semanticBoolean } ...
// Inside the object definition:
replace_all: semanticBoolean(
z.boolean().default(false).optional()
).describe('Replace all occurrences (default false)'),
Explanation:
optional(): The AI doesn't have to provide this.default(false): If the AI doesn't say anything, we assume false.semanticBoolean: This is a special helper. AI sometimes says "true" (string) instead of true (boolean). This helper fixes that automatically.Before any code edits code, the data flows through this schema.
This automatic rejection is crucial. It protects the Tool Orchestrator from crashing due to bad inputs.
Contracts work both ways. When the tool finishes, it returns an object confirming what happened. This helps the AI understand the result of its action.
// File: types.ts
const outputSchema = lazySchema(() =>
z.object({
filePath: z.string(),
oldString: z.string(),
newString: z.string(),
// Did we actually change anything?
userModified: z.boolean(),
}),
)
Explanation: This guarantees that our tool always replies in a format the AI expects.
One of the most important parts of the output is the Diff (what lines changed).
// File: types.ts
structuredPatch: z
.array(hunkSchema())
.describe('Diff patch showing the changes'),
Explanation: Instead of just saying "Done," we return a visual representation of the change (like you see in GitHub Pull Requests). We will learn how to generate this visual in User Interface & Diff Visualization.
Because we use Zod, we don't need to manually write TypeScript interfaces. Zod can generate them for us! This prevents our code definitions from drifting apart from our runtime validation.
// File: types.ts
// Automatically create a TypeScript type from the Zod rules
export type FileEditInput = z.output<typeof inputSchema>
// Now we can use this type in our functions:
// function doEdit(args: FileEditInput) { ... }
Explanation: z.output extracts the Typescript shape. If we update the Zod definition (e.g., adding a new field), our TypeScript types update automatically.
In this chapter, we learned that Data Contracts (Schemas) are the blueprints of our tool.
We have defined what data we accept (old_string and new_string). But... text is tricky. What if the file has "Helo" with a capital H, but the AI searched for "helo"?
To handle this, we need to locate that string intelligently.
Next Chapter: Intelligent String Matching
Generated by Code IQ