Skip to content

Build a Multi-Step Agent

Build advanced 60 min typescript
Sources not yet verified

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)

2. Follow the Instructions

What Makes an Agent?

An agent is more than a chatbot with tools. It's a system that:

  1. Plans — Decides what steps to take
  2. Executes — Takes actions using tools
  3. Observes — Processes results
  4. Iterates — Adjusts plan based on observations
  5. 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']
      }
    }
  }
];

💡 TIP: The 'finish' Tool

Give the agent an explicit way to signal completion. This is clearer than detecting when it has no more tool calls.

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.';
}

⚠️ WARNING: Always Set Max Iterations

Agents can get stuck in loops. Always set a maximum iteration limit and handle the case when it's exceeded.

Your Task

Complete the agent implementation with:

  1. State management
  2. Tool execution
  3. Message building that includes observations
  4. Proper termination handling

3. Try It Yourself

starter_code.ts
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
Hint 1: The state needs to track observations (tool results) so the model knows what it has already learned.
Hint 2: Build messages by including a summary of previous observations. This gives the model context about what it has already done.
Hint 3: Use a 'finish' tool to let the agent signal it's done. Check for this tool call in the loop to exit cleanly.

5. Check the Solution

Reveal the complete solution
solution.ts
/**
 * Key Points:
 * - Line ~6: State tracks everything the agent needs across iterations
 * - Line ~77: The 'finish' tool lets the agent explicitly signal completion
 * - Line ~117: Include previous observations so the model can build on them
 * - Line ~152: Always check iteration limit to prevent infinite loops
 * - Line ~184: Track sources as we go for proper citation
 */
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> {
  // Type guard for function calls
  if (call.type !== 'function') {
    return { error: 'Not a function call' };
  }

  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) };
  }
}

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
  if (state.observations.length > 0) {
    const observationSummary = state.observations
      .map((obs, i) => {
        const status = obs.success ? '✓' : '✗';
        return `[${i + 1}] ${status} ${obs.tool}(${JSON.stringify(obs.args)})\nResult: ${JSON.stringify(obs.result, null, 2)}`;
      })
      .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) {
      // Skip non-function calls
      if (call.type !== 'function') continue;

      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
*/
L6: State tracks everything the agent needs across iterations
L77: The 'finish' tool lets the agent explicitly signal completion
L117: Include previous observations so the model can build on them
L152: Always check iteration limit to prevent infinite loops
L184: Track sources as we go for proper citation

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.

Test Cases

Uses multiple tools

Agent should search and then read at least one URL

Input: Research quantum computing
Expected: Uses web_search, then read_url, then finish

Respects iteration limit

Agent should not exceed max_iterations

Input: Research an obscure topic
Expected: Terminates at or before max_iterations

Provides sources

Final answer should include sources

Input: Any research query
Expected: sources array is non-empty

Handles tool failures

Agent should continue if a tool fails

Input: Query that causes tool error
Expected: Agent continues or gracefully fails

Sources

Tempered AI Forged Through Practice, Not Hype

Keyboard Shortcuts

j
Next page
k
Previous page
h
Section home
/
Search
?
Show shortcuts
m
Toggle sidebar
Esc
Close modal
Shift+R
Reset all progress
? Keyboard shortcuts