headless · provider-agnostic · one dependency

Open Agent Loops

A minimal, provider-agnostic agent loop.

Model, memory, tools, stop conditions — every piece sits behind a swappable interface. Headless by default, so you bring your own front end. The pieces to build your own agent: your Jarvis, your Cortana, your Samantha.

runAgent()the loopMemoryload historyModelClientstream turnToolrun toolsStopConditionstop?

From zero to a running agent

Define a tool, hand it to the loop, render the stream. That's the whole API surface.

import { runAgent, SessionMemoryStore, defineTool } from "@open-agent-loops/core";
import { OpenAICompatibleModel } from "@open-agent-loops/core/providers/openai";
import { z } from "zod";

// A tool is a name, a schema, and a function.
const weather = defineTool({
  name: "weather",
  description: "Get the weather for a city.",
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => ({ content: `Sunny in ${city}` }),
});

const result = await runAgent({
  model: new OpenAICompatibleModel({ baseURL, apiKey, model }),
  memory: new SessionMemoryStore(),
  sessionId: "demo",
  prompt: "What's the weather in Paris?",
  tools: [weather],
  onEvent: (e) => render(e), // the loop is headless — render events your way
});

console.log(result.messages.at(-1)?.content); // "It's sunny in Paris."

Swap any seam

The loop depends only on the interface. Re-route a station — file store to Redis, mock model to a real one — without touching the line. Click a card to swap its implementation.

Bring your own front end

The loop never writes to a screen — it emits one typed AgentEvent stream, so a trace is just data. Scrub the same run and watch three front ends rebuild in lockstep.

› CLI · stdout
▶ start · session paris-demo
— turn 1 —
The user wants the weather in Paris — I'll call the weather tool.
→ weather({"city":"Paris"})
← weather [ok]: Sunny in Paris
— turn 2 —
It's sunny in Paris.
■ done · 2 steps
◴ DOM · timeline
  1. agent_startsession paris-demo
  2. turn_startturn 1
  3. reasoning_deltareasoning…
  4. tool_startweather({"city":"Paris"})
  5. tool_endweather → Sunny in Paris
  6. turn_startturn 2
  7. text_delta"It's sunny"
  8. text_delta"in Paris."
  9. agent_end2 steps
{} raw · JSONL
{"type":"agent_start","sessionId":"paris-demo"}
{"type":"turn_start","step":1}
{"type":"reasoning_delta","text":"The user wants the weather in Paris — I'll call the weather tool."}
{"type":"tool_start","toolCallId":"call_0","toolName":"weather","args":{"city":"Paris"}}
{"type":"tool_end","toolCallId":"call_0","toolName":"weather","result":"Sunny in Paris","isError":false}
{"type":"turn_start","step":2}
{"type":"text_delta","text":"It's sunny "}
{"type":"text_delta","text":"in Paris."}
{"type":"agent_end","steps":2}

Ride the line

One pass through the loop, seam by seam — each is just a field on runAgent(). Scroll to ride along.

Memory
load history
memory: new SessionMemoryStore()

Conversation history loads before the first turn.

ModelClient
stream the turn
model: new OpenAICompatibleModel({
  baseURL, apiKey, model,
})

The one seam that talks to an LLM — a single stream() method.

Tool
run tools
tools: [
  defineTool({ name: "weather", parameters, execute }),
]

Tool calls run in parallel; results fold back into the loop.

StopCondition
stop?
stopWhen: maxSteps(10)

Stop on a final answer, a terminate flag, or your own predicate.

Hooks
extend
hooks: {
  gateToolCalls: permissionGate(store, prompter),
}

Five optional hooks: gate tools, reshape context, steer mid-run.

Works with any OpenAI-compatible model

The ModelClient seam targets the OpenAI chat-completions wire format, so any endpoint that speaks it drops straight in. Exercised across these open-model families on Featherless:

DeepSeekGLMQwenKimiMiniMaxGemmaStep
Bring your own model client →

Runs anywhere

No platform APIs, a single zod dependency, universal ESM. The same build drives a CLI and a browser tab — unchanged.

NodeBunDenoBrowser

Composable Building Blocks

Skills, planning, sub-agents, channels — each built over runAgent(), never into it. Add what you need, ignore the rest.

Build your agent

Start from the loop, plug in your seams, render it your way.