πŸ€– HarDojo
Log in Sign up

Build an AI Agent with Tool Calling

Use the OpenAI/Anthropic API to build an agent that plans, calls tools, and loops.

In this project you'll build a minimal coding agent from scratch β€” one that can read files, edit them, and run commands, all driven by an LLM with tool calling.

What you'll build

A Node.js script agent.js that:

  1. Takes a task description as input.
  2. Sends it to an LLM with tool definitions.
  3. Executes the tool calls the model requests.
  4. Feeds results back until the task is done.

Step 1: Define the tools

// agent.js
const tools = [
  {
    type: "function",
    function: {
      name: "read_file",
      description: "Read a file from disk. Returns the file contents.",
      parameters: {
        type: "object",
        properties: {
          path: { type: "string", description: "File path to read" }
        },
        required: ["path"]
      }
    }
  },
  {
    type: "function",
    function: {
      name: "write_file",
      description: "Write content to a file. Creates or overwrites.",
      parameters: {
        type: "object",
        properties: {
          path: { type: "string" },
          content: { type: "string" }
        },
        required: ["path", "content"]
      }
    }
  },
  {
    type: "function",
    function: {
      name: "run_command",
      description: "Run a shell command and return its output.",
      parameters: {
        type: "object",
        properties: {
          command: { type: "string" }
        },
        required: ["command"]
      }
    }
  }
];

Step 2: Implement tool execution

const fs = require("fs");
const { execSync } = require("child_process");

function executeTool(name, args) {
  switch (name) {
    case "read_file":
      return fs.readFileSync(args.path, "utf8");
    case "write_file":
      fs.writeFileSync(args.path, args.content);
      return `Wrote ${args.content.length} bytes to ${args.path}`;
    case "run_command":
      return execSync(args.command, { encoding: "utf8", timeout: 10000 });
    default:
      return `Unknown tool: ${name}`;
  }
}

Step 3: The agent loop

async function agentLoop(task, maxSteps = 10) {
  const messages = [
    { role: "system", content: "You are a coding assistant. Use tools to complete tasks. When done, reply with just 'DONE'." },
    { role: "user", content: task }
  ];

  for (let step = 0; step < maxSteps; step++) {
    // Call the LLM (replace with your API)
    const response = await callLLM(messages, tools);

    // If the model replied with text (no tool calls), check if done
    if (response.content) {
      console.log(`[step ${step}] ${response.content}`);
      if (response.content.trim() === "DONE") return;
      messages.push({ role: "assistant", content: response.content });
    }

    // Execute any tool calls
    if (response.tool_calls) {
      for (const call of response.tool_calls) {
        const result = executeTool(call.function.name, JSON.parse(call.function.arguments));
        console.log(`[tool] ${call.function.name} β†’ ${result.slice(0, 100)}...`);
        messages.push({ role: "assistant", tool_calls: [call] });
        messages.push({ role: "tool", tool_call_id: call.id, content: result });
      }
    }
  }
  console.log("Reached max steps.");
}

Step 4: Wire up the LLM

async function callLLM(messages, tools) {
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
    },
    body: JSON.stringify({
      model: "gpt-4o",
      messages,
      tools
    })
  });
  const data = await res.json();
  return data.choices[0].message;
}

Step 5: Run it

export OPENAI_API_KEY=sk-...
node agent.js "Create a file called hello.py with a function that prints 'Hello, World!'"

Checkpoint

You should see the agent:

  1. Call write_file to create hello.py.
  2. Call run_command to run python hello.py.
  3. Reply "DONE" after seeing the output.

What you learned

  • The agent loop: decide β†’ act β†’ observe β†’ repeat.
  • Tool definitions as JSON schemas the model reads.
  • Tool execution β€” your code runs the actual operations.
  • Message history β€” tool results go back as context for the next LLM call.
  • This is the same pattern behind Claude Code, Codex CLI, and every coding agent.
";