AI News HubLIVE
In-site rewrite3 min read

Show HN: A provider-agnostic agent loop built on ports and adapters

Open Agent Loops is a minimal, provider-agnostic agent loop where model, memory, tools, and stop conditions are all behind swappable interfaces. It is headless by default, allowing users to bring their own front end. Works with any OpenAI-compatible model and runs on Node, Bun, Deno, and browsers.

SourceHacker News AIAuthor: hopefulbutwary

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.

Get startedGitHub

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."

Full walkthrough in Getting Started →

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

agent_startsession paris-demo

turn_startturn 1

reasoning_deltareasoning…

tool_startweather({"city":"Paris"})

tool_endweather → Sunny in Paris

turn_startturn 2

text_delta"It's sunny"

text_delta"in Paris."

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.

✓Node✓Bun✓Deno✓Browser

Composable Building Blocks

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

Streaming by Default→

stream() returns an async iterable of StreamEvents — reasoning, text, and tool calls arrive incrementally.

Skills→

Bundle instructions, tools, and reference material the model loads on demand — then guard the bundle with a secret and an approval.

Planning Tools→

Give the model durable working memory: a to-do list and a scratchpad it keeps across turns, freezable into a replayable workflow.

Composable Agents→

Wrap an agent as a tool another agent calls — a multi-agent orchestrator over one chat, each sub-agent context-isolated.

Channels & Steering→

Feed a live, bursty transport (Slack, Discord) through one bounded, coalescing queue, and inject messages mid-run.

Goal Loops→

An outer runGoal loop with a grader seam drives the inner loop until the goal is met.

Tracing Built In→

A passive Tracer records the run as a timestamped timeline and per-turn trajectory, off the hot path.

Permissioned Tool Calls→

Gate the whole turn's tool calls up front with an allow / deny / ask policy — no race with parallel execution.

Independently Testable→

Every seam is verified in isolation with deterministic test doubles — zero network.

Build your agent

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

Get startedDocsGitHubnpm