Intermediate 25 min

Building the Agent Loop

The Complete Loop

Here’s the full agent loop in src/agent.ts. This is where everything comes together:

import OpenAI from 'openai';
import * as dotenv from 'dotenv';
import { addTodo, listTodos, completeTodo } from './tools';

dotenv.config();

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const SYSTEM_PROMPT = `You are a to-do list agent. You help users add, list, and complete tasks using the available tools.

When a user asks you to:
- Add a task: Use the addTodo tool
- List tasks: Use the listTodos tool
- Complete a task: Use the completeTodo tool

Always be helpful and confirm what you've done.`;

const tools = [
  {
    type: 'function' as const,
    function: {
      name: 'addTodo',
      description: 'Add a new todo item to the list',
      parameters: {
        type: 'object',
        properties: {
          text: { type: 'string', description: 'The text of the todo item' },
          dueDate: { type: 'string', description: 'Optional due date in YYYY-MM-DD format' },
        },
        required: ['text'],
      },
    },
  },
  {
    type: 'function' as const,
    function: {
      name: 'listTodos',
      description: 'List all todo items',
      parameters: { type: 'object', properties: {} },
    },
  },
  {
    type: 'function' as const,
    function: {
      name: 'completeTodo',
      description: 'Mark a todo item as completed',
      parameters: {
        type: 'object',
        properties: {
          id: { type: 'number', description: 'The ID of the todo to complete' },
        },
        required: ['id'],
      },
    },
  },
];

async function runAgent(userMessage: string, maxSteps = 5) {
  const messages: any[] = [
    { role: 'system', content: SYSTEM_PROMPT },
    { role: 'user', content: userMessage },
  ];

  let steps = 0;

  while (steps < maxSteps) {
    steps++;

    const response = await client.chat.completions.create({
      model: 'gpt-4o-mini',
      messages,
      tools,
      tool_choice: 'auto',
    });

    const message = response.choices[0].message;
    messages.push(message);

    // If no tool calls, we're done
    if (!message.tool_calls || message.tool_calls.length === 0) {
      return message.content;
    }

    // Execute tool calls
    for (const toolCall of message.tool_calls) {
      const functionName = toolCall.function.name;
      const args = JSON.parse(toolCall.function.arguments);

      console.log(`Calling tool: ${functionName} with args:`, args);

      let result;
      if (functionName === 'addTodo') {
        result = addTodo(args.text, args.dueDate);
      } else if (functionName === 'listTodos') {
        result = listTodos();
      } else if (functionName === 'completeTodo') {
        result = completeTodo(args.id);
      } else {
        result = { error: `Unknown function: ${functionName}` };
      }

      // Add tool result to messages
      messages.push({
        role: 'tool',
        content: JSON.stringify(result),
        tool_call_id: toolCall.id,
      });
    }
  }

  return 'Agent reached max steps limit.';
}

// Test it
async function main() {
  const response = await runAgent('I have a meeting with Ali tomorrow at 10, add it');
  console.log('Agent response:', response);
}

main().catch(console.error);

Understanding the Loop

  1. Start with system prompt and user message
  2. Call the model with tools
  3. Check for tool calls - if none, return the response
  4. Execute tools and add results to messages
  5. Loop back to step 2 (with a max steps limit)

Try It

Run the agent:

npx ts-node src/agent.ts

You should see:

  • Tool calls being logged
  • The agent’s final response

Try different messages:

  • “Add a task to buy groceries”
  • “List all my tasks”
  • “Complete task 1”

Adding a Simple Web UI

Create public/index.html:

<!DOCTYPE html>
<html>
<head>
  <title>To-Do Agent</title>
  <style>
    body {
      font-family: system-ui, -apple-system, sans-serif;
      max-width: 1200px;
      margin: 0 auto;
      padding: 2rem;
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 2rem;
    }
    .chat-panel {
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 1rem;
    }
    .tools-panel {
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 1rem;
    }
    #messages {
      height: 400px;
      overflow-y: auto;
      margin-bottom: 1rem;
      padding: 1rem;
      background: #f5f5f5;
      border-radius: 4px;
    }
    .message {
      margin-bottom: 0.5rem;
      padding: 0.5rem;
      background: white;
      border-radius: 4px;
    }
    .user-message {
      background: #e3f2fd;
    }
    .agent-message {
      background: #f1f8e9;
    }
    #input {
      width: 100%;
      padding: 0.5rem;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    button {
      padding: 0.5rem 1rem;
      background: #3b82f6;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    #toolsLog {
      height: 400px;
      overflow-y: auto;
      padding: 1rem;
      background: #f5f5f5;
      border-radius: 4px;
      font-family: monospace;
      font-size: 0.875rem;
    }
  </style>
</head>
<body>
  <div class="chat-panel">
    <h2>Chat</h2>
    <div id="messages"></div>
    <input type="text" id="input" placeholder="Type your message...">
    <button onclick="sendMessage()">Send</button>
  </div>
  <div class="tools-panel">
    <h2>Tools Log</h2>
    <div id="toolsLog"></div>
  </div>

  <script>
    async function sendMessage() {
      const input = document.getElementById('input');
      const message = input.value;
      if (!message) return;

      // Add user message to chat
      const messagesDiv = document.getElementById('messages');
      const userMsg = document.createElement('div');
      userMsg.className = 'message user-message';
      userMsg.textContent = `You: ${message}`;
      messagesDiv.appendChild(userMsg);

      input.value = '';

      try {
        const response = await fetch('/api/agent', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ message }),
        });

        const data = await response.json();

        // Add agent response
        const agentMsg = document.createElement('div');
        agentMsg.className = 'message agent-message';
        agentMsg.textContent = `Agent: ${data.response}`;
        messagesDiv.appendChild(agentMsg);

        // Update tools log
        if (data.toolsLog) {
          const toolsLog = document.getElementById('toolsLog');
          const logEntry = document.createElement('div');
          logEntry.textContent = JSON.stringify(data.toolsLog, null, 2);
          toolsLog.appendChild(logEntry);
        }
      } catch (error) {
        console.error('Error:', error);
      }
    }

    document.getElementById('input').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        sendMessage();
      }
    });
  </script>
</body>
</html>

You’ll need a simple Express server to serve this and handle the API. But the core agent is done!