Welcome back! In Chapter 2: Token Budget Control, we learned how to stop our agent from spending too much money (tokens) by monitoring its fuel usage.
Now, we need to talk about the tools the agent uses to drive.
Imagine you are filming a movie. You have a scene where the main character jumps off a building.
In software, our "Building Jump" is calling the LLM API (like Claude or GPT).
The Solution: We create a Dependency Injection Interface. This is a "Plugboard." We don't hard-code the API call inside the agent. Instead, we say: "Hey Agent, here is a box of tools. Use whatever is inside."
In production, we put the Real API in the box. In testing, we put a "Fake" API in the box.
We use a specific type called QueryDeps to define our tool belt.
First, we define the shape of the tools we need. We don't care how they work yet, just what they look like.
// deps.ts - The Contract
import { queryModelWithStreaming } from '../services/api/claude.js'
export type QueryDeps = {
// The heavy lifter: Calling the AI Model
callModel: typeof queryModelWithStreaming
// A simple utility: Generating IDs
uuid: () => string
// (We also include compaction tools here, omitted for brevity)
}
Explanation: This type is our socket. It says: "If you want to run a query, you must provide a callModel function and a uuid function."
Now we create the "Real" tool belt. This is what we use when the application is actually running live.
// deps.ts - The Real Deal
import { randomUUID } from 'crypto'
// Import the REAL api client
import { queryModelWithStreaming } from '../services/api/claude.js'
export function productionDeps(): QueryDeps {
return {
callModel: queryModelWithStreaming,
uuid: randomUUID,
// ... other real tools
}
}
Explanation: This factory function bundles up the heavy, expensive, real-world functions into a single object that matches our interface.
This is where the magic happens. When writing a test, we create a "Fake" tool belt.
// test-file.ts - The Stunt Double
const mockDeps: QueryDeps = {
// Fake Model: Always returns "Hello" instantly for free
callModel: async () => ({ text: "Hello from Mock!", usage: 10 }),
// Fake UUID: Always returns "123" so tests are predictable
uuid: () => "123-123-123",
// ... other fake tools
}
Explanation: The agent doesn't know the difference! It calls callModel, gets a response, and keeps working. But we didn't spend a penny or wait for a network request.
How does the agent use these tools? It's all about passing arguments.
Inside the core query function, we don't import randomUUID or the API client directly. We simply use the deps object passed to us.
The query function requires QueryDeps as an argument.
// query.ts (Simplified)
import type { QueryDeps } from './deps.js'
// The function asks for dependencies explicitly
export async function query(
prompt: string,
deps: QueryDeps // <--- INJECTED HERE
) {
// Logic follows...
}
Inside the function, we access the tools via deps.toolName.
// Inside query.ts logic
// 1. We need an ID. Don't import randomUUID! Use deps.
const completionId = deps.uuid()
// 2. We need to talk to the AI. Use deps.
const response = await deps.callModel({
prompt: prompt,
id: completionId
})
return response
Explanation: By forcing the code to use deps.uuid() instead of import { randomUUID } ..., we gain complete control over the behavior of the system from the outside.
Without Dependency Injection, writing tests for AI agents is a nightmare. You have to use complex "Mocking" libraries (like jest.spyOn) to hack the module loading system and intercept imports. It is brittle and hard to debug.
With this pattern, testing is simple plain JavaScript:
In this chapter, we learned:
Now the agent has its Configuration, its Budget, and its Tools. But what happens when the agent finishes a turn? We might need to save data or log analytics.
Next Chapter: Post-Turn Lifecycle Hooks
Generated by Code IQ