Channels
On this page
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.