From Responder to Actor
Every chapter up to this point has covered AI as a responder: you give it a prompt, it gives you output. But the next generation of AI applications is built on agents β systems that can plan a course of action, execute steps, observe the results, and adjust their approach.
This chapter covers the architecture and code to build agents that do real work.
---
What Makes Something an Agent?
An agent has four properties that a simple LLM call does not:
- Goal: A higher-level objective, not just a specific prompt
- Tools: The ability to take actions in the world (call APIs, search, read/write files)
- Memory: Context that persists across the reasoning steps
- Iteration: The ability to observe results and try again if something fails
The simplest agent architecture is the ReAct loop:
code
REASON β ACT β OBSERVE β REASON β ACT β OBSERVE β ... β DONE
The model reasons about what to do, takes an action using a tool, observes the result, reasons about the next step, and continues until the goal is achieved.
---
Building a Web Research Agent
Let's build a practical agent that can research a topic by searching the web, reading pages, and synthesising the findings.
javascript
// agent/tools.js
import OpenAI from 'openai';
export const tools = [
{
type: 'function',
function: {
name: 'search_web',
description: 'Search the web for information on a topic. Returns a list of relevant URLs and snippets.',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'The search query' },
num_results: { type: 'number', description: 'Number of results (1-10)', default: 5 },
},
required: ['query'],
},
},
},
{
type: 'function',
function: {
name: 'read_webpage',
description: 'Fetch and read the content of a webpage. Returns the main text content.',
parameters: {
type: 'object',
properties: {
url: { type: 'string', description: 'The URL to read' },
},
required: ['url'],
},
},
},
{
type: 'function',
function: {
name: 'save_finding',
description: 'Save an important finding to memory for inclusion in the final report.',
parameters: {
type: 'object',
properties: {
finding: { type: 'string', description: 'The key finding to save' },
source: { type: 'string', description: 'The URL source of this finding' },
},
required: ['finding', 'source'],
},
},
},
];
javascript
// agent/tool-handlers.js
// Serper API for web search (or use your preferred search API)
export async function search_web({ query, num_results = 5 }) {
const response = await fetch('https://google.serper.dev/search', {
method: 'POST',
headers: {
'X-API-KEY': process.env.SERPER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ q: query, num: num_results }),
});
const data = await response.json();
return data.organic?.map(r => ({ title: r.title, url: r.link, snippet: r.snippet })) || [];
}
export async function read_webpage({ url }) {
try {
const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
const html = await response.text();
// Strip HTML tags and return plain text (simplified)
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 5000);
} catch (err) {
return `Error reading page: ${err.message}`;
}
}
const findings = []; // In-memory for this session
export async function save_finding({ finding, source }) {
findings.push({ finding, source });
return `Finding saved. Total findings: ${findings.length}`;
}
export function getFindings() { return findings; }
The Agent Loop
javascript
// agent/research-agent.js
import OpenAI from 'openai';
import { tools } from './tools';
import * as handlers from './tool-handlers';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function runResearchAgent(topic, maxIterations = 10) {
const messages = [
{
role: 'system',
content: `You are a thorough research agent. Your goal is to research the given topic comprehensively.
Process:
1. Start by searching for the topic to get an overview
2. Read the most relevant pages in full
3. Search for specific sub-topics that need more depth
4. Save your key findings as you go
5. When you have gathered sufficient information, synthesise a final comprehensive report
Be thorough. Read at least 3-5 sources before concluding.`
},
{ role: 'user', content: `Research this topic thoroughly: ${topic}` }
];
for (let i = 0; i < maxIterations; i++) {
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages,
tools,
tool_choice: 'auto',
});
const choice = response.choices[0];
messages.push(choice.message);
// If model is done reasoning, get final answer
if (choice.finish_reason === 'stop') {
return {
report: choice.message.content,
findings: handlers.getFindings(),
iterations: i + 1,
};
}
// Execute all requested tool calls
if (choice.finish_reason === 'tool_calls') {
for (const toolCall of choice.message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
const handler = handlers[toolCall.function.name];
let result;
try {
result = await handler(args);
} catch (err) {
result = { error: err.message };
}
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result),
});
}
}
}
return { report: 'Max iterations reached', findings: handlers.getFindings(), iterations: maxIterations };
}
---
Agent Design Principles
Give agents a way to signal completion. Without a stop condition (a "done" tool or a natural stopping point), agents loop unnecessarily.
Cap iterations. Always set a maxIterations limit to prevent runaway agents consuming infinite tokens.
Make tools deterministic. Tools should return consistent, predictable results. Ambiguous tool outputs confuse the agent's reasoning.
Log everything. Agents are hard to debug without full logs. Log every tool call, every result, and every model response.
Design for partial failure. A tool might fail mid-run. Good agents notice this, try an alternative approach, and do not just crash.