Module 2: Giving the AI "Hands" (Tool Calling)
The Core Mechanic
Introduction: From Talking to Doing
In Module 1, we built an agent that could think and communicate. But it couldn't do anything beyond generating text.
This module is where the magic happens. We're going to teach the AI how to use tools—functions that allow it to interact with the real world.
By the end of this module, your agent will be able to:
- Fetch live data from APIs
- Perform calculations
- Make autonomous decisions about which tool to use
- Chain multiple tool calls together
2.1 Defining Tools with Zod
What is Tool Calling?
Tool calling (also called function calling) is the mechanism that allows an LLM to:
- Recognize when it needs external data or capabilities
- Choose which function to call
- Generate the correct parameters for that function
- Receive the result and incorporate it into its response
Example conversation:
User: "What's the current price of Bitcoin?"
Agent (thinking):
"I don't have real-time data. I need to use the
getCryptoPrice tool with symbol 'BTC'"
Agent (calls tool): getCryptoPrice({ symbol: "BTC" })
Tool (returns): { symbol: "BTC", price: 43250.50, change24h: 2.3 }
Agent (responds):
"Bitcoin is currently trading at $43,250.50,
up 2.3% in the last 24 hours."
Why Zod?
Zod provides runtime type validation for your tool schemas. This is critical because:
- LLMs can make mistakes with parameter formats
- You need to validate inputs before executing functions
- Type safety prevents runtime errors
import { z } from 'zod'
const GetCryptoPriceSchema = z.object({
symbol: z.string().describe('The cryptocurrency symbol (e.g., BTC, ETH)')
})
// Type inference for free
type GetCryptoPriceParams = z.infer<typeof GetCryptoPriceSchema>
// { symbol: string }
Defining Your First Tool
import { generateText, tool } from 'ai'
import { z } from 'zod'
const tools = {
getCryptoPrice: tool({
description: 'Get the current price of a cryptocurrency',
parameters: z.object({
symbol: z.string().describe('Cryptocurrency symbol (BTC, ETH, etc.)')
}),
execute: async ({ symbol }) => {
// This is where you'd call a real API
// For now, we'll simulate data
const mockPrices: Record<string, number> = {
BTC: 43250.50,
ETH: 2280.75,
SOL: 98.30
}
return {
symbol,
price: mockPrices[symbol] || 0,
timestamp: new Date().toISOString()
}
}
})
}
Using Tools with the AI SDK
const { text, toolCalls } = await generateText({
model: openai('gpt-4-turbo'),
messages: [
{ role: 'user', content: 'What is the price of Bitcoin?' }
],
tools,
maxSteps: 5 // Allow the agent to use tools multiple times
})
console.log(text)
// "Bitcoin is currently trading at $43,250.50"
console.log(toolCalls)
// [{ toolName: 'getCryptoPrice', args: { symbol: 'BTC' }, result: {...} }]
2.2 Connecting APIs
Real-World Tool: Stock Price Checker
Let's build a tool that fetches real stock data from a free API.
Install dependencies:
npm install axios
Create the tool:
import axios from 'axios'
import { tool } from 'ai'
import { z } from 'zod'
const tools = {
getStockPrice: tool({
description: 'Get the current stock price for a given ticker symbol',
parameters: z.object({
ticker: z.string().describe('Stock ticker symbol (e.g., AAPL, TSLA, MSFT)').toUpperCase()
}),
execute: async ({ ticker }) => {
try {
// Using Alpha Vantage API (you'll need an API key)
const API_KEY = process.env.ALPHA_VANTAGE_API_KEY
const url = `https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${ticker}&apikey=${API_KEY}`
const response = await axios.get(url)
const quote = response.data['Global Quote']
if (!quote || Object.keys(quote).length === 0) {
return {
success: false,
error: `No data found for ticker ${ticker}`
}
}
return {
success: true,
ticker,
price: parseFloat(quote['05. price']),
change: quote['09. change'],
changePercent: quote['10. change percent'],
volume: quote['06. volume'],
lastUpdated: quote['07. latest trading day']
}
} catch (error) {
return {
success: false,
error: error.message
}
}
}
})
}
Error Handling in Tools
Always handle errors gracefully:
execute: async ({ ticker }) => {
try {
const data = await fetchStockData(ticker)
return { success: true, data }
} catch (error) {
// Return error info so the agent can respond appropriately
return {
success: false,
error: `Could not fetch data for ${ticker}: ${error.message}`
}
}
}
The LLM will see the error and can explain it to the user or try an alternative approach.
2.3 The Loop: Agent Execution Flow
Understanding maxSteps
When you enable tools, the agent enters a multi-step reasoning loop:
const { text } = await generateText({
model: openai('gpt-4-turbo'),
messages: [{ role: 'user', content: 'Compare Apple and Tesla stock prices' }],
tools: {
getStockPrice
},
maxSteps: 5
})
What happens behind the scenes:
Step 1: Agent receives user message
→ "I need to get stock prices for AAPL and TSLA"
→ Calls getStockPrice({ ticker: 'AAPL' })
Step 2: Agent receives AAPL data
→ Calls getStockPrice({ ticker: 'TSLA' })
Step 3: Agent receives TSLA data
→ "I now have both prices. Let me compare them."
→ Generates final response
Step 4: Returns text to user
→ "Apple (AAPL) is trading at $178.25, while Tesla (TSLA)
is at $242.80. Tesla is up 3.2% today while Apple..."
Manual Tool Loop (Advanced)
For more control, you can handle the tool loop manually:
import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
const messages = [
{ role: 'user', content: 'What is the price of Bitcoin?' }
]
let finished = false
let steps = 0
const maxSteps = 5
while (!finished && steps < maxSteps) {
const result = await generateText({
model: openai('gpt-4-turbo'),
messages,
tools,
maxSteps: 1 // Only allow one step at a time
})
// Add tool calls to history
for (const toolCall of result.toolCalls) {
messages.push({
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
args: toolCall.args
}
]
})
messages.push({
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
result: toolCall.result
}
]
})
}
// Check if agent is done
if (result.finishReason === 'stop') {
finished = true
console.log('Final response:', result.text)
}
steps++
}
This gives you control to:
- Log each step
- Implement custom logic between tool calls
- Stop execution early
- Add human approval (we'll cover this in Module 3)
Project: Crypto Price Assistant
Let's build a complete crypto assistant that can:
- Check current prices
- Calculate portfolio value
- Compare multiple cryptocurrencies
Setup
mkdir crypto-assistant
cd crypto-assistant
npm init -y
npm install ai @ai-sdk/openai zod dotenv axios
npm install -D typescript @types/node tsx
The Complete Agent
Create crypto-agent.ts:
import { generateText, tool } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
import axios from 'axios'
import * as dotenv from 'dotenv'
dotenv.config()
// Tool 1: Get Crypto Price
const getCryptoPrice = tool({
description: 'Get the current price of a cryptocurrency in USD',
parameters: z.object({
symbol: z.string().describe('Cryptocurrency symbol (BTC, ETH, SOL, etc.)')
}),
execute: async ({ symbol }) => {
try {
const response = await axios.get(
`https://api.coingecko.com/api/v3/simple/price?ids=${symbol.toLowerCase()}&vs_currencies=usd&include_24hr_change=true`
)
const data = response.data[symbol.toLowerCase()]
if (!data) {
return { success: false, error: `Cryptocurrency ${symbol} not found` }
}
return {
success: true,
symbol: symbol.toUpperCase(),
price: data.usd,
change24h: data.usd_24h_change
}
} catch (error) {
return { success: false, error: error.message }
}
}
})
// Tool 2: Calculate Portfolio Value
const calculatePortfolio = tool({
description: 'Calculate the total value of a cryptocurrency portfolio',
parameters: z.object({
holdings: z.array(
z.object({
symbol: z.string(),
amount: z.number()
})
).describe('Array of crypto holdings with symbol and amount')
}),
execute: async ({ holdings }) => {
let totalValue = 0
const details = []
for (const holding of holdings) {
try {
const response = await axios.get(
`https://api.coingecko.com/api/v3/simple/price?ids=${holding.symbol.toLowerCase()}&vs_currencies=usd`
)
const price = response.data[holding.symbol.toLowerCase()]?.usd || 0
const value = price * holding.amount
totalValue += value
details.push({
symbol: holding.symbol.toUpperCase(),
amount: holding.amount,
pricePerUnit: price,
totalValue: value
})
} catch (error) {
details.push({
symbol: holding.symbol.toUpperCase(),
error: 'Could not fetch price'
})
}
}
return {
success: true,
totalValue,
holdings: details
}
}
})
// Main agent function
async function runAgent(userMessage: string) {
console.log(`User: ${userMessage}\n`)
const { text, toolCalls } = await generateText({
model: openai('gpt-4-turbo'),
messages: [
{
role: 'system',
content: 'You are a helpful cryptocurrency assistant. Provide clear, concise information about crypto prices and portfolios.'
},
{
role: 'user',
content: userMessage
}
],
tools: {
getCryptoPrice,
calculatePortfolio
},
maxSteps: 5
})
console.log(`Agent: ${text}\n`)
if (toolCalls.length > 0) {
console.log('Tools used:')
toolCalls.forEach(call => {
console.log(` - ${call.toolName}`)
console.log(` Args:`, call.args)
console.log(` Result:`, call.result)
})
}
}
// Test the agent
async function main() {
await runAgent('What is the current price of Bitcoin?')
await runAgent('I have 2 BTC, 10 ETH, and 50 SOL. What is my portfolio worth?')
await runAgent('Compare the prices of Bitcoin and Ethereum')
}
main()
Run It
npx tsx crypto-agent.ts
Expected Output
User: What is the current price of Bitcoin?
Agent: Bitcoin (BTC) is currently trading at $43,250.50 USD, up 2.3% in the last 24 hours.
Tools used:
- getCryptoPrice
Args: { symbol: 'BTC' }
Result: { success: true, symbol: 'BTC', price: 43250.5, change24h: 2.3 }
User: I have 2 BTC, 10 ETH, and 50 SOL. What is my portfolio worth?
Agent: Your cryptocurrency portfolio is currently worth $137,890.50.
Here's the breakdown:
- 2 BTC at $43,250.50 each = $86,501.00
- 10 ETH at $2,280.75 each = $22,807.50
- 50 SOL at $579.64 each = $28,982.00
Tools used:
- calculatePortfolio
Args: { holdings: [...] }
Result: { success: true, totalValue: 137890.5, holdings: [...] }
Advanced: Multi-Tool Workflows
The agent can chain tools together automatically:
// User: "Find the current Bitcoin price and calculate how much 0.5 BTC is worth"
// Step 1: Agent calls getCryptoPrice({ symbol: 'BTC' })
// Step 2: Agent receives price: $43,250.50
// Step 3: Agent calculates: 0.5 * 43,250.50 = $21,625.25
// Step 4: Agent responds with the answer
No manual orchestration needed—the LLM figures out the workflow!
Key Takeaways
- Tools give agents the ability to interact with the real world
- Zod schemas provide type-safe, validated tool parameters
- The agent loop (maxSteps) allows autonomous multi-step reasoning
- Error handling in tools is critical for robust agents
- Tool chaining happens automatically when maxSteps > 1
Exercise: Build Your Own Tool
Create a new tool for the crypto assistant:
getHistoricalPrice: Fetch price from a specific dateconvertCurrency: Convert between different crypto pairsgetTopCryptos: List the top N cryptocurrencies by market cap
Next up: Module 3, where we learn to orchestrate complex agent workflows with LangGraph.

