Welcome to the TaskListTool project! In this first chapter, we are going to lay the foundation for our AI tool. Before we write any logic or fancy prompts, we need to agree on a language.
Imagine you are running a busy professional kitchen. A waiter runs in and shouts, "Someone wants food!"
You would be confused. What food? A burger? A salad? Do they have allergies?
If the AI (the waiter) just sends random text to our code (the kitchen), our program will crash. Similarly, if our code sends a messy pile of data back to the AI, the AI won't know how to read it.
To solve this, we create a Schema. Think of a Schema as a standardized Kitchen Order Ticket.
In our code, we use a library called Zod to create these tickets. Zod acts as a strict gatekeeper: it ensures type safety and predictable data formats.
Let's look at our specific goal: We want the AI to List all tasks.
id, subject, status, owner, and blockedBy.
Let's look at how we define these rules in TaskListTool.ts.
Since we are just asking for a list of all tasks, we don't need to provide any arguments (like a search term or a specific ID).
import { z } from 'zod/v4'
import { lazySchema } from '../../utils/lazySchema.js'
// Defines what the AI sends TO us
const inputSchema = lazySchema(() => z.strictObject({}))
type InputSchema = ReturnType<typeof inputSchema>
Explanation:
z.strictObject({}): This creates a Zod object that is empty ({}).strict: This means "Don't add anything extra!" If the AI tries to send { filter: "urgent" }, this schema will reject it because we didn't ask for a filter.This is where the magic happens. We need to tell the system exactly what a "Task" looks like so the AI understands the data we give back.
const outputSchema = lazySchema(() =>
z.object({
tasks: z.array(
z.object({
id: z.string(),
subject: z.string(),
// ... continued below
}),
),
}),
)
Explanation:
z.object(...): The result is an object containing data.tasks: z.array(...): We are returning a list (array) of items.id: z.string(): Every task must have an ID, and it must be text (string).A task is more than just an ID. We need to define the status and relationships.
// ... inside the object defined above
status: TaskStatusSchema(),
owner: z.string().optional(),
blockedBy: z.array(z.string()),
Explanation:
status: Uses a helper TaskStatusSchema() (likely defines specific words like 'todo', 'done').owner: This is optional(). A task might not have an owner, and that's okay. Zod won't complain if it's missing.blockedBy: An array of strings (IDs of other tasks blocking this one).When the AI tries to use our tool, a process runs to ensure everything matches our schemas.
inputSchema. Since we defined an empty object, it ensures the AI didn't pass random arguments.id instead of a string, Zod throws an error. This protects the AI from confusing data.
In our actual code implementation, we have to make sure the data we fetch from our database matches that outputSchema we defined.
Here is how we map the raw data to match our Zod definition:
// Inside the call() function
const tasks = allTasks.map(task => ({
id: task.id,
subject: task.subject,
status: task.status,
owner: task.owner,
// Filter out blockers that are already finished
blockedBy: task.blockedBy.filter(id => !resolvedTaskIds.has(id)),
}))
Explanation:
allTasks.blockedBy: We do a little logic here to clean up the data before putting it into the schema format.
Finally, we return the data wrapped in the structure our outputSchema expects:
return {
data: {
tasks, // This matches z.array inside our outputSchema
},
}
We have successfully defined the "contract" for our tool.
Now that the shape of our data is defined, we need to tell the AI generally what this tool is and how it behaves.
Next Chapter: Tool Definition & Configuration
Generated by Code IQ