LoopEngineBETA

Docs

Documentation

How LoopEngine works, from scaffold to durable approvals — agents, adapters, permission rules, and abilities.

Channels

RunAgentOptions.channel — 'cli' | 'http_stream' | 'http' — is which of the three ways a runAgent() call is reaching the agent, and it's what AgentConfig.httpNotifier's own resolution checks directly (it only ever matches 'http'). Two entry points produce it — adapters/cli.ts and adapters/http.ts (the latter serving both http and http_stream, plus every Web UI route) — files a scaffolded project owns and can edit, not logic hidden inside the loopengine package. npx loopengine run/serve/dev are thin wrappers that just shell out to your own copy of whichever file the command needs.

cli — one-shot, terminal-blocking

npx loopengine run customer-service --session s1 "order A-1001 arrived broken"
# equivalent to:
npx tsx adapters/cli.ts --agent customer-service --session s1 "..."

adapters/cli.ts passes no approver/questionHandler at all, so runAgent() falls all the way to its own hard defaults: actauth's ConsoleApprover for a gated tool call — prints the request, blocks on stdin for a decision — and CliQuestionHandler for a system_ask_user call, the same terminal-blocking shape. --json prints the same typed LoopEvent stream as NDJSON instead, one line per event, ending in done/error.

http_stream — live chat over SSE

curl -N -X POST localhost:8787/agents/customer-service/messages/stream \
  -H 'content-type: application/json' \
  -d '{"message":"..."}'

handleMessagesStream builds a fresh WebchatApprover per streamed turn and wires onQuestionPending to push approval:pending/ question:pending frames onto that exact same connection — nothing to configure, and AgentConfig.httpNotifier can't reach this channel at all (it only ever matches 'http'). A human responds via POST /approvals/:id/approve (or /deny) or POST /questions/:id/answer, and the original stream just continues once resolved.

http — plain request/response, durable

curl -X POST localhost:8787/agents/customer-service/messages \
  -H 'content-type: application/json' \
  -d '{"message":"..."}'

No live connection to push a pending signal onto, so the default approver is durable: defaultHttpWebhookNotifier if LOOPENGINE_DEFAULT_WEBHOOK_URL/_SECRET are set deployment-wide, otherwise a tracked WebchatApprover scoped to this one request (the response still races the turn against the first pending signal either way, same shape as the streaming route). This agent's own AgentConfig.httpNotifier — see HTTP Notifier — always wins outright over both defaults when it covers the event. A pending call comes back directly in the response body (stopReason: 'pending_approval'), resolved later through the same /approvals//questions routes, decoupled from this original request entirely — see Human in the Loop for the full durability design.

Quick comparison

cli http_stream http
Entry point adapters/cli.ts POST .../messages/stream POST .../messages
Default approver ConsoleApprover — blocks on stdin Fresh WebchatApprover per turn Webhook if configured, else a tracked WebchatApprover
Default question handler CliQuestionHandler Pushed onto the same SSE connection Webhook if configured, else the same tracked fallback
AgentConfig.httpNotifier reachable? No No Yes — wins outright when set
Survives a process restart No No Yes, once a pending item is checkpointed

Wire protocol

protocol/loop-event.schema.json is the formal wire-protocol spec any client language could validate against — the same LoopEvent union The Agent Loop describes, standardized across CLI, plain-HTTP, and SSE transport bindings. core/client.ts is the reference browser SDK implementing it, and what the Web UI's own playground is built on top of.