Build a Multi-Step Agent
Implement an agent that can plan, execute, and iterate on multi-step tasks with tool use and state management.
1. Understand the Scenario
You're building a research agent that can answer complex questions by searching the web, reading documents, and synthesizing information across multiple steps.
Learning Objectives
- Understand the plan-execute-observe loop
- Implement state management across steps
- Handle tool failures gracefully
- Know when to stop (termination conditions)
Concepts You'll Practice
2. Follow the Instructions
What Makes an Agent?
An agent is more than a chatbot with tools. It's a system that:
- Plans — Decides what steps to take
- Executes — Takes actions using tools
- Observes — Processes results
- Iterates — Adjusts plan based on observations
- Terminates — Knows when it's done
The Agent Loop
┌─────────────────────────────────────────┐
│ │
│ User Query │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ PLAN │ ◄─────────────┐ │
│ └─────┬─────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌───────────┐ │ │
│ │ EXECUTE │ (use tools) │ │
│ └─────┬─────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌───────────┐ │ │
│ │ OBSERVE │ (process) │ │
│ └─────┬─────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌───────────┐ No │ │
│ │ DONE? │ ──────────────┘ │
│ └─────┬─────┘ │
│ │ Yes │
│ ▼ │
│ Final Answer │
│ │
└─────────────────────────────────────────┘
Step 1: Define Agent State
The agent needs to track its progress across iterations.
interface AgentState {
query: string; // Original user query
plan: string[]; // Current plan steps
completed_steps: string[]; // What we've done
observations: Observation[]; // Results from tools
current_step: number; // Where we are in the plan
max_iterations: number; // Safety limit
iteration: number; // Current iteration
status: 'planning' | 'executing' | 'complete' | 'failed';
}
interface Observation {
step: string;
tool_used: string;
result: any;
success: boolean;
} Step 2: Define Tools
Our research agent has three tools: search, read, and synthesize.
const tools: OpenAI.ChatCompletionTool[] = [
{
type: 'function',
function: {
name: 'web_search',
description: 'Search the web for information',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' }
},
required: ['query']
}
}
},
{
type: 'function',
function: {
name: 'read_url',
description: 'Read and extract content from a URL',
parameters: {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to read' }
},
required: ['url']
}
}
},
{
type: 'function',
function: {
name: 'finish',
description: 'Complete the task and provide final answer',
parameters: {
type: 'object',
properties: {
answer: { type: 'string', description: 'Final synthesized answer' },
sources: {
type: 'array',
items: { type: 'string' },
description: 'URLs used'
}
},
required: ['answer', 'sources']
}
}
}
]; Step 3: The Agent Loop
This is the core execution loop. It continues until done or max iterations.
async function runAgent(query: string): Promise<string> {
const state: AgentState = {
query,
plan: [],
completed_steps: [],
observations: [],
current_step: 0,
max_iterations: 10,
iteration: 0,
status: 'planning'
};
while (state.status !== 'complete' && state.iteration < state.max_iterations) {
state.iteration++;
// Build context from state
const messages = buildMessages(state);
// Call the model
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools
});
const assistantMessage = response.choices[0].message;
// Handle tool calls or completion
if (assistantMessage.tool_calls) {
for (const call of assistantMessage.tool_calls) {
const result = await executeTool(call);
if (call.function.name === 'finish') {
state.status = 'complete';
return result.answer;
}
state.observations.push({
step: call.function.name,
tool_used: call.function.name,
result,
success: !result.error
});
}
}
}
return 'Agent reached max iterations without completing.';
} Your Task
Complete the agent implementation with:
- State management
- Tool execution
- Message building that includes observations
- Proper termination handling
3. Try It Yourself
import OpenAI from 'openai';
const openai = new OpenAI();
// Agent state
interface AgentState {
query: string;
observations: { tool: string; result: any }[];
iteration: number;
max_iterations: number;
status: 'running' | 'complete' | 'failed';
}
// Mock tool implementations
async function webSearch(query: string): Promise<{ results: { title: string; url: string; snippet: string }[] }> {
// Simulate search results
return {
results: [
{ title: 'Example Result 1', url: 'https://example.com/1', snippet: 'Some information...' },
{ title: 'Example Result 2', url: 'https://example.com/2', snippet: 'More information...' }
]
};
}
async function readUrl(url: string): Promise<{ content: string; title: string }> {
// Simulate reading a URL
return {
title: 'Page Title',
content: 'This is the extracted content from the page...'
};
}
// TODO: Define tools array
const tools: OpenAI.ChatCompletionTool[] = [];
// TODO: Execute a tool call and return the result
async function executeTool(call: OpenAI.ChatCompletionMessageToolCall): Promise<any> {
throw new Error('Not implemented');
}
// TODO: Build messages array including observations
function buildMessages(state: AgentState): OpenAI.ChatCompletionMessageParam[] {
throw new Error('Not implemented');
}
// TODO: Main agent loop
async function runAgent(query: string): Promise<{
answer: string;
sources: string[];
iterations: number;
}> {
throw new Error('Not implemented');
}
// Test the agent
const testQueries = [
"What are the latest developments in quantum computing?",
"Compare React and Vue.js for building web applications"
];
for (const query of testQueries) {
console.log(`\nQuery: ${query}`);
runAgent(query).then(result => {
console.log(`Answer: ${result.answer}`);
console.log(`Sources: ${result.sources.join(', ')}`);
console.log(`Iterations: ${result.iterations}`);
});
} This typescript exercise requires local setup. Copy the code to your IDE to run.
4. Get Help (If Needed)
Reveal progressive hints
5. Check the Solution
Reveal the complete solution
import OpenAI from 'openai';
const openai = new OpenAI();
interface AgentState {
query: string;
observations: { tool: string; args: any; result: any; success: boolean }[];
iteration: number;
max_iterations: number;
status: 'running' | 'complete' | 'failed';
final_answer?: string;
sources: string[];
}
// Mock tool implementations
async function webSearch(query: string): Promise<{
results: { title: string; url: string; snippet: string }[]
}> {
console.log(` 🔍 Searching: ${query}`);
return {
results: [
{
title: 'Quantum Computing Advances 2024',
url: 'https://example.com/quantum-2024',
snippet: 'Major breakthroughs in error correction...'
},
{
title: 'IBM Quantum Roadmap',
url: 'https://ibm.com/quantum',
snippet: '1000+ qubit processors by 2025...'
}
]
};
}
async function readUrl(url: string): Promise<{ content: string; title: string }> {
console.log(` 📖 Reading: ${url}`);
return {
title: 'Detailed Article',
content: `Content from ${url}: Quantum computing has made significant progress in 2024, with error correction improvements and larger qubit counts...`
};
}
const tools: OpenAI.ChatCompletionTool[] = [
{
type: 'function',
function: {
name: 'web_search',
description: 'Search the web for information on a topic',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' }
},
required: ['query']
}
}
},
{
type: 'function',
function: {
name: 'read_url',
description: 'Read and extract content from a URL',
parameters: {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to read' }
},
required: ['url']
}
}
},
{
type: 'function',
function: {
name: 'finish',
description: 'Complete the research and provide final answer',
parameters: {
type: 'object',
properties: {
answer: { type: 'string', description: 'Synthesized answer' },
sources: {
type: 'array',
items: { type: 'string' },
description: 'URLs used as sources'
}
},
required: ['answer', 'sources']
}
}
}
];
async function executeTool(
call: OpenAI.ChatCompletionMessageToolCall
): Promise<any> {
const args = JSON.parse(call.function.arguments);
try {
switch (call.function.name) {
case 'web_search':
return await webSearch(args.query);
case 'read_url':
return await readUrl(args.url);
case 'finish':
return { answer: args.answer, sources: args.sources };
default:
return { error: `Unknown tool: ${call.function.name}` };
}
} catch (error) {
return { error: String(error) };
}
}
// Prevent context window explosion with sliding window
const MAX_OBSERVATIONS_IN_CONTEXT = 5;
const MAX_RESULT_LENGTH = 500; // Truncate large tool results
function truncateResult(result: any): string {
const str = JSON.stringify(result, null, 2);
if (str.length > MAX_RESULT_LENGTH) {
return str.substring(0, MAX_RESULT_LENGTH) + '\n... (truncated)';
}
return str;
}
function buildMessages(state: AgentState): OpenAI.ChatCompletionMessageParam[] {
const messages: OpenAI.ChatCompletionMessageParam[] = [
{
role: 'system',
content: `You are a research agent. Your task is to answer questions by:
1. Searching the web for relevant information
2. Reading promising URLs for details
3. Synthesizing findings into a comprehensive answer
Use web_search to find sources, read_url to get details, and finish when ready.
Always cite your sources. Be thorough but efficient.`
},
{
role: 'user',
content: state.query
}
];
// Add observations from previous iterations (with sliding window)
if (state.observations.length > 0) {
// Keep only recent observations to prevent context explosion
const recentObservations = state.observations.slice(-MAX_OBSERVATIONS_IN_CONTEXT);
const omittedCount = state.observations.length - recentObservations.length;
let observationSummary = '';
if (omittedCount > 0) {
observationSummary += `(${omittedCount} earlier observations omitted for brevity)\n\n`;
}
observationSummary += recentObservations
.map((obs, i) => {
const actualIndex = state.observations.length - recentObservations.length + i + 1;
const status = obs.success ? '✓' : '✗';
return `[${actualIndex}] ${status} ${obs.tool}(${JSON.stringify(obs.args)})\nResult: ${truncateResult(obs.result)}`;
})
.join('\n\n');
messages.push({
role: 'assistant',
content: `Previous observations:\n${observationSummary}\n\nContinuing research...`
});
}
return messages;
}
async function runAgent(query: string): Promise<{
answer: string;
sources: string[];
iterations: number;
}> {
const state: AgentState = {
query,
observations: [],
iteration: 0,
max_iterations: 10,
status: 'running',
sources: []
};
console.log(`\n🤖 Starting agent for: "${query}"`);
while (state.status === 'running' && state.iteration < state.max_iterations) {
state.iteration++;
console.log(`\n--- Iteration ${state.iteration} ---`);
const messages = buildMessages(state);
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools
});
const assistantMessage = response.choices[0].message;
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
// No tool calls - shouldn't happen with our setup, but handle it
console.log(' ⚠️ No tool calls, prompting to continue or finish');
continue;
}
for (const call of assistantMessage.tool_calls) {
const args = JSON.parse(call.function.arguments);
const result = await executeTool(call);
if (call.function.name === 'finish') {
state.status = 'complete';
state.final_answer = result.answer;
state.sources = result.sources;
console.log(' ✅ Agent finished');
break;
}
state.observations.push({
tool: call.function.name,
args,
result,
success: !result.error
});
// Track sources from search results
if (call.function.name === 'web_search' && result.results) {
for (const r of result.results) {
if (!state.sources.includes(r.url)) {
state.sources.push(r.url);
}
}
}
}
}
if (state.status !== 'complete') {
state.status = 'failed';
return {
answer: 'Agent could not complete the task within iteration limit.',
sources: state.sources,
iterations: state.iteration
};
}
return {
answer: state.final_answer || '',
sources: state.sources,
iterations: state.iteration
};
}
// Test
runAgent('What are the latest developments in quantum computing?').then(result => {
console.log('\n=== Final Result ===');
console.log(`Answer: ${result.answer}`);
console.log(`Sources: ${result.sources.join(', ')}`);
console.log(`Iterations: ${result.iterations}`);
});
/* Expected output:
🤖 Starting agent for: "What are the latest developments in quantum computing?"
--- Iteration 1 ---
🔍 Searching: quantum computing 2024 developments
--- Iteration 2 ---
📖 Reading: https://example.com/quantum-2024
--- Iteration 3 ---
✅ Agent finished
=== Final Result ===
Answer: Recent developments in quantum computing include significant advances in error correction...
Sources: https://example.com/quantum-2024, https://ibm.com/quantum
Iterations: 3
*/ Common Mistakes
Not including previous observations in messages
Why it's wrong: The model has no memory between API calls. Without observations, it will repeat the same actions.
How to fix: Add observations to the message history so the model knows what it has already learned.
No iteration limit
Why it's wrong: Agents can get stuck in loops, wasting API calls and never completing.
How to fix: Always set max_iterations and handle the case when it's exceeded.
No explicit 'finish' tool
Why it's wrong: Detecting completion by absence of tool calls is unreliable. The model might stop for other reasons.
How to fix: Add a 'finish' tool that the agent calls when done, with the final answer as a parameter.
Not tracking state across iterations
Why it's wrong: Without state, you can't know what the agent has done or provide that context to the model.
How to fix: Use an AgentState object that persists across the loop iterations.
Appending all observations without pruning
Why it's wrong: If tools return large JSON objects or the loop runs 10+ iterations, you hit the model's token limit or degrade reasoning with context noise.
How to fix: Implement a sliding window (keep last N observations) and truncate large tool results. Optionally summarize omitted observations.
Test Cases
Uses multiple tools
Agent should search and then read at least one URL
Research quantum computingUses web_search, then read_url, then finishRespects iteration limit
Agent should not exceed max_iterations
Research an obscure topicTerminates at or before max_iterationsProvides sources
Final answer should include sources
Any research querysources array is non-emptyHandles tool failures
Agent should continue if a tool fails
Query that causes tool errorAgent continues or gracefully fails