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
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
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}`;
}
}
}
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);
}
}
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 });
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." };
};
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)