@looprun-ai/server
v0.20.0
Published
looprun model server: expose governed LoopRunAgents behind an OpenAI-compatible /v1/chat/completions endpoint, so any harness that speaks the OpenAI protocol can call a governed agent as if it were a model. The full governed turn (guards, tools, redrive)
Maintainers
Readme
@looprun-ai/server
Expose governed LoopRun agents behind an OpenAI-compatible endpoint —
the "agent-as-model" pattern. Any harness that can point a custom provider at a base_url
(personal-agent frameworks, Open WebUI, IDE assistants, plain OpenAI SDKs) calls a governed agent
as if it were a model: the full governed turn (guards → tools → redrive) runs inside each
/v1/chat/completions request and returns one final assistant message.
New to looprun? Start with the tutorial: 01 · Concepts — six chapters, one running example; chapter 06 serves this agent over HTTP.
import { createModelServer } from '@looprun-ai/server';
import { LoopRunAgent } from '@looprun-ai/mastra';
const agent = new LoopRunAgent({ spec, world: worldFactory, toolDefs, model });
const server = await createModelServer({ agents: { 'inbox-triage': agent }, port: 8099 });
console.log(server.url); // http://127.0.0.1:8099/v1Point the harness at it:
# e.g. a harness config.yaml
model:
provider: custom
base_url: "http://127.0.0.1:8099/v1" # model field selects the agent: "inbox-triage"
context_length: 128000The mapping law (what the facade does with the incoming request)
The server implements the protocol as a facade — the harness believes it is talking to a model, so parts of the request that would fight the spec are deliberately not honored:
| Incoming | Treatment | Why |
|---|---|---|
| model | routes to the registered agent | one server, N agents as N "models" |
| last user message | the governed turn's input | the agent's own session is the canonical memory |
| earlier history | ignored (transport-only) | harnesses compress/rewrite it; replay would desync the governed state |
| system message | discarded | the AgentSpec renders its own assembled prompt (byte-stable, cache-friendly) |
| tools, tool_choice | ignored | the spec owns the tool surface; guards govern every call |
| temperature etc. | ignored | spec.controls.sampling governs |
| stream: true | honored (see below) | |
Sessions
The protocol is stateless; the agent is stateful. Session id resolution, first hit wins:
x-looprun-sessionheader (explicit — always safe; OpenAI SDKs supportdefault_headers),- the OpenAI-standard
userfield, - fingerprint fallback: hash of
model+ the first user message — stable for a conversation unless the harness compresses that message away (mitigated by the highcontext_lengthreported by/v1/models). A changed fingerprint starts a fresh session: degraded, never unsafe.
Concurrent requests on the same session serialize; different sessions run concurrently.
Optional sessionTtlMs evicts idle sessions via agent.endSession().
Streaming
stream: true still runs the governed turn to completion (streaming cannot be governed at the
reply level), then emits a valid SSE stream: an immediate role delta, : keepalive comments while
the turn runs, one content delta with the full governed text, a finish chunk, [DONE].
Observability
Every response carries a non-standard looprun field (sessionId, turnIndex, corrections,
exhausted, violations) — OpenAI SDKs ignore it; integration harnesses can assert on it.
onTurn fires server-side after every governed turn with the same metadata.
API
createModelServer(config) → { url, port, handler, close() }— node:http server, ephemeral port by default. The returnedhandleris the bare fetch-style(req: Request) => Promise<Response>, so you can embed the governed endpoint in any web server without taking the node:http listener: build one withport: 0, use.handler, andclose()when done.config:agents(model id →LoopRunAgent),port,hostname,contextLength,apiKey(optional bearer check),resolveSession,sessionTtlMs,onTurn.
