DEV Community

T. Alam
T. Alam

Posted on

Building a Simple Agent Runtime With Node.js

You built an agent that calls a model, picks a tool, and prints an answer. It works fine in a script. Then you try to run it for real, and it falls apart.

No memory between steps. No retry when a call fails. No record of what actually happened. That's the moment every builder finds out they didn't build an agent. They built a function pretending to be one.

What they actually needed was an agent runtime.

In this post, we'll build a small one in Node.js from scratch. No framework, no magic. Just the pieces that make an agent runtime work, so you understand what's happening under the hood before you reach for a bigger tool.

What Is an Agent Runtime, Really?

An agent runtime is the system that keeps an agent alive between calls. It holds state, decides what step comes next, and routes work to models and tools.

Without a runtime, your agent forgets everything the second the function returns. It can respond, but it can't act, retry, or remember. A runtime turns a single call into a process that runs, tracks, and finishes a task.

The Loop Every Agent Execution Engine Runs

Strip away the buzzwords and every agent execution engine does the same four things, over and over, until the task is done:

 ┌─────────┐
 │ Perceive│  read the current state + input
 └────┬────┘
      ▼
 ┌─────────┐
 │  Decide │  ask the model what to do next
 └────┬────┘
      ▼
 ┌─────────┐
 │   Act   │  call a tool or return an answer
 └────┬────┘
      ▼
 ┌─────────┐
 │ Observe │  save the result, update state
 └────┬────┘
      │
      └──────► back to Perceive
Enter fullscreen mode Exit fullscreen mode

That loop is the entire job of an agent runtime. Everything else, memory, tools, logging, is built around keeping this loop honest.

Setting Up the Project

Nothing fancy here. Just a plain Node project.

mkdir agent-runtime && cd agent-runtime
npm init -y
npm install node-fetch
Enter fullscreen mode Exit fullscreen mode

We'll keep everything in one file to start, then split it up once the pieces are clear.

Building the Agent Runtime Class

This is the core of an LLM agent runtime: a class that owns the loop, the state, and a hard limit on how many steps it can take.

class AgentRuntime {
  constructor({ model, tools = {}, maxSteps = 6 }) {
    this.model = model;
    this.tools = tools;
    this.maxSteps = maxSteps;
    this.state = { history: [], memory: {} };
  }

  async run(task) {
    this.state.history.push({ role: "user", content: task });

    for (let step = 0; step < this.maxSteps; step++) {
      const decision = await this.model(this.state);

      if (decision.type === "final_answer") {
        return decision.content;
      }

      if (decision.type === "tool_call") {
        const result = await this.callTool(decision.tool, decision.input);
        this.state.history.push({
          role: "tool",
          tool: decision.tool,
          content: result,
        });
      }
    }

    return "Runtime stopped: max steps reached.";
  }

  async callTool(name, input) {
    const tool = this.tools[name];
    if (!tool) return `No tool named ${name}`;
    try {
      return await tool(input);
    } catch (err) {
      return `Tool ${name} failed: ${err.message}`;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice the maxSteps guard. Without it, a confused model can loop forever. Every serious agent runtime architecture needs a hard stop like this.

Giving Your Agent Runtime Some Memory

Right now, state.history grows forever. That's fine for a demo, but a real agent state runtime needs to manage memory on purpose, not by accident.

addMemory(key, value) {
  this.state.memory[key] = value;
}

trimHistory(limit = 20) {
  if (this.state.history.length > limit) {
    this.state.history = this.state.history.slice(-limit);
  }
}
Enter fullscreen mode Exit fullscreen mode

Call trimHistory at the end of each loop. It keeps your context small and your costs predictable.

Wiring Up Tools

Tools are just functions. Register them by name, and the runtime handles the rest.

const tools = {
  getWeather: async (city) => {
    return `Weather in ${city}: 28°C, clear skies.`;
  },
  searchDocs: async (query) => {
    return `Top result for "${query}": use the trimHistory method.`;
  },
};

const runtime = new AgentRuntime({ model: myModelFn, tools });
Enter fullscreen mode Exit fullscreen mode

Your model function just needs to return { type: "tool_call", tool, input } or { type: "final_answer", content }. That's the whole contract.

Where a Simple Runtime Breaks in Production

The loop above works for a demo. It won't survive real traffic. Here's what changes once you move from a toy to a production agent runtime:

Concern Simple runtime Production runtime
Failures Crashes or hangs Retries with backoff
Visibility Console logs Full traceability of each step
Concurrency One task at a time Many agents running in parallel
Communication None Real-time updates via pub/sub
Debugging Guesswork Step-by-step monitoring

This is usually the point where teams stop hand-rolling everything. An autonomous agent runtime that handles many users needs monitoring, observability, and traceability baked in, not bolted on later.

That's the gap DNotifier is built to close. It gives you one SDK for orchestration, multi-agent coordination, and real-time pub/sub, so your agent runtime gets production features without you writing them from scratch. You still own the loop. You just stop reinventing the plumbing around it.

Testing the Loop

Before adding more features, write a fake model function that returns scripted decisions. Run it through your runtime and check the history at each step. If the loop behaves correctly with a fake model, it'll behave correctly with a real one.

const fakeModel = async (state) => {
  if (state.history.length < 3) {
    return { type: "tool_call", tool: "getWeather", input: "Lahore" };
  }
  return { type: "final_answer", content: "Done checking the weather." };
};
Enter fullscreen mode Exit fullscreen mode

This kind of test catches loop bugs before they cost you an API bill.

FAQ

What's the difference between an agent and an agent runtime?
An agent is a single decision-making call. An agent runtime is the system that runs that call repeatedly, tracks state, and manages tools and memory across steps.

Do I need a framework to build an agent runtime?
No. A small class with a loop, state, and a tool registry covers the basics. Frameworks help once you need retries, tracing, and multi-agent coordination at scale.

How do I add memory to an agent runtime?
Store history and key facts in a state object, then trim it on a schedule. Keep only what the next decision actually needs.

Can a simple agent runtime handle production traffic?
Not on its own. You'll need retries, monitoring, and concurrency handling. That's usually when teams bring in a platform like DNotifier instead of building it by hand.

Top comments (0)