Welcome back! In Chapter 3: Lazy Schema Validation, we learned how to ensure our tool receives the correct data types (like verifying that status is a valid string).
Now that we can validate individual tasks, we need to look at the "Big Picture." In a real project, tasks rarely exist in isolation. They are connected.
Imagine you are managing a construction site for a new house. You have two tasks:
If you send a painter (an AI agent) to do Task B before the bricklayer has finished Task A, the painter will try to paint thin air. This wastes time and resources.
Task Dependency Management is the system of rules that says: "Do not let Task B start until Task A is finished."
In our system, we allow the AI to define these relationships in two ways. This flexibility helps the AI "think" naturally depending on which task it is currently looking at.
addBlocks (Forward looking):If the AI is editing the Walls (Task #1), it can say: "I am blocking the Painting task."
taskId: "1", addBlocks: ["2"]addBlockedBy (Backward looking):If the AI is editing the Painting (Task #2), it can say: "I am blocked by the Walls task."
taskId: "2", addBlockedBy: ["1"]Both inputs result in the exact same link in the database.
We defined these fields in our schema in the previous chapter. Let's look closely at how simple they are.
// Inside the inputSchema definition
addBlocks: z.array(z.string()).optional()
.describe('Task IDs that this task blocks'),
addBlockedBy: z.array(z.string()).optional()
.describe('Task IDs that block this task'),
Explanation:
[]) of strings.
Inside our call function (which we structured in Chapter 2: Task Lifecycle Workflow), we handle these dependencies after we handle basic updates like status changes.
addBlocks
When the AI tells us Task A blocks Task B, we call a helper function blockTask.
// Inside call() function
if (addBlocks && addBlocks.length > 0) {
// 1. Filter out tasks we already blocked (avoid duplicates)
const newBlocks = addBlocks.filter(
id => !existingTask.blocks.includes(id)
)
// 2. Create the dependency link
for (const blockId of newBlocks) {
await blockTask(taskListId, taskId, blockId)
}
}
Explanation:
existingTask.blocks to see if the link already exists. We don't want to do double work.blockTask(blocker, victim).taskListId is passed because these tasks live in a specific list.addBlockedBy (The Reverse)This is slightly different. If Task B says "I am blocked by A," we actually need to go update Task A to say "I block B."
// Inside call() function
if (addBlockedBy && addBlockedBy.length > 0) {
const newBlockedBy = addBlockedBy.filter(
id => !existingTask.blockedBy.includes(id),
)
// NOTE THE ORDER: blockTask(Blocker, Victim)
for (const blockerId of newBlockedBy) {
await blockTask(taskListId, blockerId, taskId)
}
}
Explanation:
Notice the arguments for blockTask:
addBlocks, we passed (taskId, blockId).addBlockedBy, we pass (blockerId, taskId).We normalize the data. Regardless of how the AI phrased it ("I block him" or "He blocks me"), the database always stores it as: A blocks B.
What happens when the AI creates this link?
By establishing these links, we create a "Smart Schedule."
completed, the system knows exactly who to notify next. "Hey Painter! The walls are ready."This automated notification system is critical for multi-agent teams. If you have one AI writing code and another AI writing tests, the Tester needs to know the exact moment the Coder finishes.
In this chapter, we learned:
addBlocks allows a task to declare what it is holding up.addBlockedBy allows a task to declare what it is waiting for.But what happens when the "Walls" are finally built? How does the "Painter" know it's time to work? We need a way for agents to talk to each other and trigger actions automatically.
Next Chapter: Agent Collaboration & Hooks
Generated by Code IQ