Synaplan
开源 AI 软件平台,面向企业:自托管 AI 代理,符合 GDPR 的聊天机器人小部件,RAG 知识库,WhatsApp 与 Outlook AI,MCP,多模型路由。支持 Docker、Kubernetes 或在 Germ 托管。
AI 翻译
A minimal framework for building your own AI agent platforms.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗LoopEngine is a runtime for building AI agents: a persona, a set of tools,
and permission rules, run through a transparent ReAct loop. No chain DSL,
no hidden control flow — core/run-agent.ts is a single function you can read
top to bottom. Run agents over a CLI, an HTTP API, or both.
npx create-loopengine@latest scaffolds
a real, standalone project — its own repo, a starter agent, CLI and HTTP
adapters already wired up. npm install && npm run dev and you have a
running agent server, not a half-finished template to wire up yourself./agents/config in a browser to
see and edit a live agent: its resolved config, permission rules, skills,
tool connections, and secrets — no redeploy to change a rule or add a
skill. See Admin UI below.execute function. Permission rules gate
what happens without a human: safe reads auto-run, risky actions get
approval or are denied outright.subagents/ to
become a tool the parent can delegate to, own tools/rules/loop intact.So the payoff: you write one AgentConfig, and get a deployable,
persistent, permission-safe service — not just a prompt-and-response demo.
npx create-loopengine@latest my-agents
cd my-agents
npm install
cp .env.example .env # fill in ANTHROPIC_API_KEY
npm run dev # HTTP server on :8787 — same as `npx loopengine dev`
This scaffolds a standalone project — its own repo, with loopengine as
a dependency, a starter agent, and CLI + HTTP adapters already wired up.
That's the intended way to use LoopEngine: an independent project you own
and build agents in, not a library you import into an existing app.
npx loopengine add-agent weather-agent
# -> Created agents/weather-agent/index.ts
That's the fastest way to start: it generates agents/<name>/index.ts
with a working AgentConfig stub, so you don't have to memorize the
folder shape by hand. Or write it yourself — an agent is just an
AgentConfig: name, systemPrompt, tools, rules, and a model.
Drop it in a folder under agents/ — agents/<name>/index.ts — and it's
picked up automatically, no registry to edit, no import to add.
Most fields don't even need to be written out — they default to a conventional path under the agent's own folder:
| Field | Defaults to |
|---|---|
rules | agents/<name>/actauth.yml (missing → deny everything) |
tools | agents/<name>/tools/index.ts's exported tools (missing → no tools) |
skillsDirs | agents/<name>/skills (missing → no skills) |
There's no subagents field to omit — agents/<name>/subagents/* is its
own folder convention (drop another agent's folder there and it becomes
one of this agent's tools automatically), always merged into tools on
top of whatever the table above resolves to.
rules is how you gate what a tool can do without approval — each rule
maps a scope (tenant/environment) + tool name to allow/ask/deny,
with an ask decision routing to a human — see
Human in the loop below.
The simplest possible agent, agents/weather-agent/index.ts — tools,
rules, and skillsDirs all omitted, so they default to
agents/weather-agent/tools/, actauth.yml, and skills/:
import type { AgentConfig } from '../../agent-config.js'
export const config: AgentConfig = {
name: 'weather-agent',
systemPrompt: 'You answer questions about the weather.',
model: { provider: 'anthropic', model: 'claude-sonnet-5' }, // reads ANTHROPIC_API_KEY
}
See agents/customer-service/ and agents/file-agent/ for complete,
working examples.
Run the HTTP adapter and open http://localhost:8787/agents — one
console for every registered agent: a sidebar of agents (with a badge
when one still needs a setting), and for each agent a set of tabs. No
code changes or redeploy needed for most of it:
| Tab | What you can do |
|---|---|
| Chat | Talk to the agent, approve or deny tool calls as they come up, and follow each step (model calls, tool calls, permission checks) in the Run trace. Past conversations are under History. |
| Overview | A card per tab (tools, permissions, skills, settings), plus the system prompt, model, hooks and limits, each editable in place. |
| Abilities | Install, upgrade, and uninstall abilities (see Ability system). |
| Tools | Your hand-written tools, tools from integrations (e.g. Composio) and subagents, with whether each call runs on its own, asks first, or is denied. Build an HTTP tool or connect an integration without touching a file. |
| Skills | Create, edit, and delete SKILL.md files for this agent directly in the browser — write the body, preview the rendered markdown, save. |
| Permissions | Add, edit, and delete permission rules (actauth) — who, tool, decision — and choose what happens when no rule matches. |
| Schedules | One of the agent's workflows (with values for its parameters), or a custom message, run on a timer — once at a set time, or every N minutes/hours/days. A workflow schedule starts each run with the workflow's start message and steps as they are then. Run now, pause, resume, cancel, see each run's result, and optionally send results through the agent's notifier. |
| Environment | Every setting an installed ability reads, with its project-wide value and this agent's own value side by side, and which one the agent actually uses. A secret is never shown again once set. |
Models and Integrations in the sidebar show which model providers have a
key set and which outside tool sources are connected. Tabs deep-link:
/agents/config?agent=<name>&tab=tools, /playground?agent=<name> for
Chat.
Every tab is backed by a real API (GET /agents/:name/config, .../actauth,
.../env, ...) that reuses the exact same resolution runAgent() itself
uses — so what you see here can't drift out of sync with what a real
request actually gets, and never returns a model API key.
A risky tool call (actauth's ask decision) or a genuinely ambiguous
question the model itself raises both need a human — and LoopEngine
handles both the same way, live or durable:
cli/http_stream get this
automatically, with nothing to configure.Six channels ship as real, working notifiers — configure one and the "send a notification" side is done for you:
| Channel | What it does |
|---|---|
webhook | Signed HMAC-SHA256 POST to any URL you own |
slack | An interactive message via chat.postMessage, with Approve/Deny buttons |
lark | A Lark/Feishu interactive card |
email | An email per decision, through your own sendEmail: with signed, expiring magic links to your endpoints, or a link to this server's decision page |
database | Writes a row for your own worker/dashboard to poll |
redis | Pushes a queue entry for your own worker to consume |
export const config: AgentConfig = {
// ...
httpNotifier: {
channel: 'slack',
config: { botToken: process.env.SLACK_BOT_TOKEN!, channelId: process.env.SLACK_CHANNEL_ID! },
events: ['approval', 'question'],
},
}
Or skip the code: in the console, the agent's Messaging tab sets a
webhook, Slack, Lark or email target for one agent (email sends through
Resend, SendGrid or Postmark with your API key, each email linking to the
decision page) (saved to
agents/<name>/messaging.yml, with its secret in the agent's own .env;
an older notifier.yml is still read), sends a test and shows recent
deliveries. With Slack, people can also reply in a request's thread to
ask the agent about it, and it answers there (Answer replies in a
request's thread, on by default). Approvers, picked from the
channel's members, can also decide a request by replying ("yes, go
ahead", "no", or the answer to a question); a separate model call reads
only their own message to tell whether it's a decision, and anything
unclear is treated as a question. An approver can also change a request
first ("make it $40"): the change is checked against the tool's schema
and the permission rules and shown with Approve as changed / Keep
original / Deny, and nothing runs until they approve it. A plain "yes"
("approve", "go ahead", "好的", "はい", "sí", 👍…: the whole message,
matched against a fixed list of phrases in common languages, not by a
model) runs it; any other reply read as approval ("sure, send
it", "yes but check the amount") shows exactly what will run with a
Confirm button, and only that click runs it. Arguments are shown
as labelled fields, with long text such as an email body quoted, not as
JSON. Once approvers are listed, only their button clicks count too. The same tab lists every decision
still waiting with Approve/Deny buttons (its count shows on the tab), and
warns you when an agent has ask rules but nobody would be told.
Not sure what to automate? The console's Find your first agent
(/interview, linked from the sidebar and the Agents page) is a short
interview about your work, in any language. It suggests up to three
agents worth building, ranked by the time they'd save, and creates the
one you pick: its instructions, permissions where every tool asks you
first, and a paused schedule if it runs on a timer, plus a checklist of
what's left to connect. The interviewer runs on the model of one of your
agents, can't see your systems, and creates nothing until you click.
Or skip the interview: Templates (/templates, linked from the
sidebar and the Agents page) are ready-made agents: answer customer
emails, refund approvals, triage incidents, check expense claims, lead
follow-up, inbox to tasks, and a weekly research digest. Preview one's instructions and setup checklist, then create it
the same way, as many times as you like. Templates live in
core/system-agents/templates.ts, in the same shape the interview
suggests.
Every notification links to a decision page: a signed, single-use
link (/decide/<token>, valid 7 days) where whoever got the message can
approve, deny, edit the arguments or answer a question, with no login and
no app of your own. So the quickest setup is Slack: create the Slack app
from the manifest the console shows, paste its bot token, pick a channel
from the list, save; each approval arrives with an "Open to approve or
deny" link. Add the app's signing secret too and the message also gets
Approve/Deny buttons, handled by LoopEngine itself at
/hooks/slack/<agent>, with no handler to deploy. Lark cards carry the
same link. Links and buttons need the server to be reachable from where
people click (a public HTTPS address or a tunnel).
For a third-party app calling an agent over plain HTTP:
POST /agents/<name>/messages → when a call needs approval, the reply
has stopReason: "pending_approval" and a pending list, each item
with its pendingId and resolvePath.pendingId, resolvePath (and a full
resolveUrl when LOOPENGINE_PUBLIC_URL is set), signed with
X-Actauth-Signature. Failed deliveries are retried.POST <resolvePath> with {"decision":"approve"} (or "deny") runs
the tool, resumes the turn, and returns the agent's reply.
GET /agents/<name>/pending lists what's waiting, for apps that poll
instead of receiving webhooks.Reference implementations for the receiving side (verifying a Slack
click, a webhook HMAC signature, a magic-link token) ship under
examples/notifier-handler/ for the four channels that need one. See
HUMAN_IN_THE_LOOP.md for the full setup guide —
live and durable, worked examples, and how resumption actually works
under the hood.
The server runs one scheduler (core/scheduler.ts) that sends a message
to an agent later — once (run_at) or on an interval (every: "1h"),
optionally from a set first run (start_at), which every later run keeps:
every: "1d" from a 9:00 start runs daily at 9:00, every: "7d" from a
Monday runs on Mondays. at: "09:00" (plus weekday: "monday") picks
that start for you. Times of day are in the workspace's time zone, chosen
in the console's Settings (LOOPENGINE_TIME_ZONE; the server's own
when unset): a timestamp without an offset is read there, and daily and
weekly tasks keep their time of day when the clocks change. Schedule
from the console's Schedules tab, the GET/POST /agents/<name>/schedules API, or the agent itself: every agent has
built-in system_schedule_task, system_list_scheduled_tasks,
system_check_scheduled_task and system_cancel_scheduled_task tools,
so "remind me tomorrow at 9" just works. An
agent can only schedule, see or cancel its own tasks, up to 25 active at
a time (LOOPENGINE_SCHEDULER_MAX_PER_AGENT); selfScheduling: false in
its config removes the tools. Tasks left by the old lp-task-scheduler
ability are imported at start. Each run goes through the same
path as an HTTP call: an approval goes wherever the Permissions tab sends
it (and resumes the run once decided); with nowhere set, a call that
needs approval is denied. Intervals keep their original slot (an hourly
task created at 10:07 stays on :07); a run missed while the server was
down fires once when it's back. Tasks live in .scheduler/ (or
LOOPENGINE_SCHEDULER_DIR); the tick is LOOPENGINE_SCHEDULER_TICK_MS
(10 s); LOOPENGINE_SCHEDULER=off turns the loop off for this process.
Behind a hosting platform, GET /internal/activity reports nextWakeAt
and POST /internal/scheduler/tick fires whatever is due, so a stopped
server can be woken in time.
Giving an agent a new capability is usually three separate, hand-authored
things: a tool file, a SKILL.md teaching the model when to use it, and
an actauth rule allowing it to actually run. A loopengine ability
bundles all three into one installable unit:
npx loopengine add-ability <spec> --agent customer-service
<spec> is anything npm pack understands — a public or private npm
package, a scoped package on a private registry, or a plain git repo. The
tool files, skill directory, and actauth rules all land as real files in
the agent's own tree, copied in rather than imported as a node_modules
dependency — reviewable, diffable, and editable the same as anything you
would have hand-written, not an opaque black box.
npx loopengine upgrade-ability <abilityName> --agent customer-service
npx loopengine remove-ability <abilityName> --agent customer-service
Upgrading does a real three-way merge per file — a hand-edit since install
survives, a genuine conflict leaves <<<<<<< markers to resolve by hand
instead of silently overwriting either side. Removing refuses a file
that's been modified since install unless you pass --force. An ability
can also declare the env vars its tools need, which then show up in the
Admin UI's Environment tab automatically once installed. See
ABILITIES.md for the full design — format, publishing,
and how the merge/upgrade mechanics work in detail.
adapters/cli.ts and adapters/http.ts are files your own scaffolded
project owns (see "Quick start" above), not something hidden inside the
loopengine package — run them directly with tsx, or through shorter
loopengine commands that just call the same files for you:
npx loopengine run customer-service --session s1 "order A-1001 arrived broken"
npx loopengine run customer-service --session s1 --input "order A-1001 arrived broken"
npx loopengine serve # HTTP server on :8787
npx loopengine dev # same server, restarts on file changes (tsx watch)
--session <id> is just an arbitrary string you pick — s1 above isn't
special, it's only a label. Omit it entirely for a fresh, one-off
conversation each call; reuse the same value on a later call to continue
that exact conversation (the id is printed to stderr if you omit it, so
you can capture it for next time). --input "<message>" is an
alternative to the trailing positional message, for scripts that build
the argument list programmatically and would rather not depend on the
message always being the last argument.
run/serve/dev are thin wrappers, not a separate implementation —
each shells out to your project's own adapters/cli.ts or
adapters/http.ts via npx tsx (so it's always your copy that runs,
edits included, resolved from your project's own node_modules), and
fails with a clear message pointing at create-loopengine if that file
doesn't exist yet. .env is loaded automatically for all three,
equivalent to passing --env-file-if-exists=.env to tsx yourself.
The same thing, spelled out without the wrapper:
CLI:
npx tsx adapters/cli.ts --agent customer-service --session s1 "order A-1001 arrived broken"
HTTP:
npx tsx --env-file-if-exists=.env adapters/http.ts
curl -X POST localhost:8787/agents/customer-service/messages \
-H 'content-type: application/json' \
-d '{"customerEmail":"[email protected]","message":"order A-1001 arrived broken"}'
Add /stream to the URL for a Server-Sent Events response — one event per
loop step (tool call, permission decision, budget check) instead of a
single reply at the end.
Three small browser pages, all cross-linked, share one look
(adapters/dev-ui-styles.ts), and never need a build step — each is a
self-contained HTML string served straight out of adapters/http.ts:
Agents list: open http://localhost:8787/agents in a browser (the
same route returns plain {agents: [...]} JSON to a non-browser client —
content-negotiated on the Accept header, so nothing that already calls
it as an API needs to change) to see every registered agent with links
into the playground and config page below.
Chat: open http://localhost:8787/playground (optionally
?agent=<name> to preselect one) — the console's Chat tab: talk to an
agent and watch the loop-step event stream render live in the Run trace
instead of reading raw SSE frames. Same /messages/stream route
underneath.
Building your own client: the playground is one UI on top of a typed
event protocol, not the only way to consume a turn — see
PROTOCOL.md for the full LoopEvent catalog, the SSE/
plain-HTTP/CLI transport bindings, and a JSON Schema
(protocol/loop-event.schema.json) any language can validate against.
core/client.ts and the examples/chatbox/react/examples/chatbox/vue
hooks are reference implementations of it.
Agent config page: this is the Admin UI described above
— open http://localhost:8787/agents/config (optionally ?agent=<name>).
Declare AgentConfig.model and the runtime builds a real ModelCall for
you, using the matching API key from the environment:
provider | env var | model required? |
|---|---|---|
'anthropic' | ANTHROPIC_API_KEY | no — defaults to claude-sonnet-5 |
'openai' | OPENAI_API_KEY | yes |
'deepseek' | DEEPSEEK_API_KEY | yes |
'kimi' | MOONSHOT_API_KEY | yes |
'glm' | GLM_API_KEY | yes |
'gemini' | GEMINI_API_KEY | yes |
'xai' | XAI_API_KEY | yes |
'meta' | META_API_KEY | yes |
'kimi''s env var is MOONSHOT_API_KEY, not KIMI_API_KEY — deliberate,
matching Moonshot AI's own docs (the API/company behind Kimi) rather than
this package's own provider name. 'meta' (Meta's Muse models, e.g.
muse-spark-1.3, through the Meta Model API) reads META_API_KEY,
LiteLLM's name, rather than the generic MODEL_API_KEY in Meta's own
examples. openai/deepseek/kimi/glm/gemini/xai/meta all reuse the same OpenAI-Chat-Completions-compatible request
translation, just pointed at each provider's own base URL — see
core/model-calls/*.ts for the provider-specific details (base URLs,
max_completion_tokens vs max_tokens, and Gemini's own "still in beta"
caveat on Google's compatibility layer).
The console's model dropdown (New agent, and an agent's Model > Edit)
lists each provider's models live from its API once its key is saved. To
choose the list yourself, add a models.yaml next to .env mapping each
provider to its models; a provider listed there offers exactly those, in
that order, with no key needed, and edits show the next time the
dropdown loads (LOOPENGINE_MODELS_FILE points at a different file):
anthropic:
- claude-opus-5-5
- claude-sonnet-5-5
deepseek:
- deepseek-v4-pro
For anything else (a custom SDK client, a canned/simulated model for
testing), export your own createModelCall(): ModelCall instead.
| Package | Responsibility |
|---|---|
actauth | Permission gating (allow/ask/deny/pending) with live and durable human-approval hooks |
core/budget.ts / core/compaction.ts | Context-window budget tracking and tail-preserving compaction (vendored in-repo, not an external dependency) |
core/recovery.ts | Retries prompt-too-long / truncated-output failures (vendored in-repo, not an external dependency) |
core/durable-approvals.ts | TurnCheckpoint/CheckpointStore (file/Redis) backing durable, resumable ask decisions — see HUMAN_IN_THE_LOOP.md |
core/sessionknit.ts | Durable session log with crash-interruption detection (vendored in-repo, not an external dependency) |
core/skillgarden/ | SKILL.md discovery and lazy loading (vendored in-repo, not an external dependency) |
core/toollane.ts | Parallel/solo tool-call scheduling (vendored in-repo, not an external dependency) |
core/mcpplug.ts | Gateway tool sourcing (e.g. Composio) (vendored in-repo, not an external dependency) |
Installing loopengine pulls in actauth as a regular dependency —
install it directly only if you want to use it standalone. Everything
else in this table ships inside loopengine itself.
docker compose up --build
Builds the HTTP adapter into a container and starts it alongside Redis.
For production, push the image to any container platform with REDIS_URL
and your model/tool API keys set as environment variables.
These settings are for running LoopEngine inside a hosting platform (one workspace per user, behind a router that has already authenticated them). All are off by default; a self-hosted install that doesn't set them behaves exactly as described above.
| Setting | What it does |
|---|---|
LOOPENGINE_TRUSTED_PROXY_SECRET | Every request must carry an X-LoopEngine-Identity header signed with this secret (signProxyIdentity creates one; unsigned requests get 401). LOOPENGINE_ADMIN_AUTH, if also set, still works for direct access. Also enables GET /internal/activity: in-flight turns, open streams and idle time, so the host never stops a server mid-turn. |
LOOPENGINE_EXIT_ON_CODE_CHANGE=1 | After an ability install, upgrade, uninstall or dependency install through the Admin UI, the server drains and exits so the platform restarts it with the new code loaded. Only set this when something restarts the process. |
LOOPENGINE_USAGE_LOG | stdout (one JSON line per model call) or a file path (JSON lines) — tokens per call with agent, tenant, session and model. Or call setUsageSink() in code. |
setSecretStore() | In code, at startup: keep env values set through the Admin UI somewhere other than .env files (a vault, a secret manager). The default FileSecretStore is today's behaviour. |
GET /healthz answers {"ok":true} with no login, on every server,
for health checks; it returns 503 once the server starts shutting down,
and doesn't count as activity.
deploy/workspace/Dockerfile is a runtime image for this: it serves a
scaffolded project mounted at /workspace instead of baking agents in.
LoopEngine is under the Functional Source License, Version 1.1, MIT Future License (FSL-1.1-MIT). In short:
The license text is what applies; this summary isn't legal advice. Third-party software included in LoopEngine keeps its own license: see THIRD_PARTY_NOTICES.md.