synago-agent
v0.9.0
Published
Ontology-first, local-first agent framework — actions are first-class, governed by domain-object state.
Maintainers
Readme
Synago
A local-first, ontology-first agent framework. The moat is a user-definable, typed, persistent domain state layer where an action is a first-class citizen carrying preconditions, permissions, and limits bound to domain-object state — with a single-agent control layer on par with existing frameworks on top.
Status: built & green — 125 tests across 28 files, pnpm typecheck clean, pnpm tour runs the whole thing.
The gap it fills
Personal-agent frameworks (OpenClaw, Hermes Agent) are excellent at loose, stateless, one-shot tasks and do have real governance — but everything they structure and govern is the agent managing itself (its own dangerous commands, its own step budget, its own reminders, its own billing balance). None of it models the user's world.
Synago adds exactly that layer: a user-definable typed domain layer, plus a mechanism that binds an action's permissions and limits to domain-object state ("account balance insufficient → block the order") rather than to a counter, a command-string regex, or a role name.
Synago does not "fix" OpenClaw/Hermes. Their governance lives on the "agent manages itself" side; Synago covers the "model the user's world" side. Two different problems.
Two-layer architecture
Control layer (commodity — match, don't out-innovate)
ReAct loop (hooks · steering/follow-up · anti-thrash guardrail · budgets)
model-agnostic (ScriptedModel + Codex ChatGPT-OAuth provider)
progressive ActionType disclosure (large ontologies don't blow up the context)
│ the agent may only mutate domain state through a governed ActionType
▼
State layer / Ontology (the moat)
user-definable ObjectType / Property / LinkType
ActionType — first-class: target · precondition · effect · permission · limit (bound to object state)
event-sourced log → projection (single authoritative state) · per-ObjectType reducers · snapshots
§6 reality-sync: reconciliation (drift) + write-back (outbox)
event-driven subscriptions (state change → auto-respond, auto-actions still governed)Quick taste
npm i synago-agentimport { defineOntology, ActionEngine, InMemoryEventStore, OK, deny } from "synago-agent";
import { z } from "zod";
const ontology = defineOntology({
objectTypes: [{ name: "Account", properties: { ownerId: z.string(), balance: z.number() } }],
actionTypes: [{
name: "placeOrder",
targets: ["Account"],
args: z.object({ accountId: z.string(), cost: z.number() }),
permission: (ctx) => { // bound to object OWNERSHIP
const a = ctx.world.getObject("Account", ctx.args.accountId);
return a?.props.ownerId === ctx.actor.id ? OK : deny("not the owner");
},
preconditions: () => OK,
limits: (ctx) => { // bound to object BALANCE
const a = ctx.world.getObject("Account", ctx.args.accountId)!;
return ctx.args.cost <= Number(a.props.balance) ? OK : deny("balance insufficient → block order");
},
effects: (ctx) => [/* immutable events: create order, debit balance */],
}],
});
const engine = new ActionEngine(ontology, new InMemoryEventStore());
engine.execute("placeOrder", { accountId: "a1", cost: 20_000 }, { id: "alice", roles: ["trader"] });
// → denied [limit]: "balance insufficient → block order" (and NO event is appended)Run it:
pnpm install
pnpm tour # one narrated end-to-end run across every subsystem
pnpm test # 125 tests
# focused demos
pnpm scenarios # two helper-authored domains (clinic + brokerage) — rendered to plain language + governed
pnpm feeds # live external API data + freshness, captured as events
pnpm reactions # external-event triggers + a scheduler, all governed
pnpm visualize # writes ontology-report.html — a self-contained visual review page
pnpm ingest # messy wide table → ontology + lineage → supply-chain "revenue at risk"
pnpm demo # the healthcare governance spineWhat's built
State layer (the moat)
- Typed
ObjectType/Property/LinkType;ActionTypeas a first-class peer with five separately-inspectable parts (target · permission · precondition · limit · effect). - Governance bound to object state — permission to ownership, limit to a numeric property, precondition
to a state predicate. The thing
rm -rfcommand-safety can't catch (e.g. "amoxicillin to a penicillin-allergic patient"). - Event-sourced: append-only log → projection (single authoritative state); per-ObjectType reducers (events carry deltas); snapshots + incremental rebuild + indexed store (scales with the log).
- §6 "object = reality":
reconcile(drift detection + repair, itself audited) andOutboxwrite-back (commit outward + record confirm/fail as audited events). - Event-driven subscriptions: a state change auto-fires a governed action — auto-actions pass the SAME gates as manual ones (never looser).
- External conditions as first-class, captured as events: a
Feed+refreshpull live API data at decision time and record it as an audited event — gates stay pure, so every decision is replayable and auditable ("allowed because the risk API reported clear at 14:32, event #N"). Time helpers (freshWithin/notExpired) gate on freshness and deadlines; reactions (onExternalEvent+Scheduler) fire governed actions from external events and from the clock — all through the same gates. - Fail-closed by construction: a gate that throws denies (never crashes the app, never silently
allows); a faulty subscription can't break a committed write; opt-in
strictWritesvalidates every upserted instance against its ObjectType schema before it lands. - Schema evolution without rewriting the log:
EventMigrationupcasting (renameProperty,mapPatch) transforms old-shape events to the current schema at projection time — replay and live writes share one pipeline, and stale pre-migration snapshots are automatically ignored. - A visual client-review page in one call:
renderOntologyHtml()emits a single self-contained HTML file (graph + objects + governance in plain language + lint + a live allowed/denied scenario timeline) —pnpm visualizeshows it on the inventory "missing interface" example. - A data on-ramp with lineage built in:
ingestTable/ingestTablesmap a (cleaned) table to ObjectTypes + links via a declarativeTableMapping(column→property, FK→relationship; split a wide table with several mappings). Each row becomes an event whose payload records its source file + row, so the append-only log is the lineage —lineageOf(store, type, id)traces every object back to the source row it came from.pnpm ingestturns a denormalized order export into a 4-entity ontology and reenacts the "revenue at risk" supply-chain walk, every number traceable.
Control layer (commodity, kept solid)
- ReAct loop with hooks (
before/afterToolCall,onStep), steering/follow-up queues, anti-thrash guardrail, tool-call budget,stopReason. - Model-agnostic
Modelinterface; a Codex ChatGPT-OAuth provider (reuses~/.codex/auth.json, no API key); a deterministicScriptedModel. - Progressive ActionType disclosure — large catalogs defer behind
find/describe/invokebridges; the audit log always records the real action name.
Authoring an ontology (by hand or with Claude Code)
The state layer is meant to be stood up fast, per domain. A small governance helper vocabulary
(requireRole, requireOwner, exists, whenProp, excludesValue, limitAtMost, freshWithin,
notExpired, allOf) turns an action's gates from hand-rolled closures into a couple of declarations —
and makes the ontology renderable: describeOntology() prints it back as Markdown + a Mermaid diagram
with governance in plain English, and validateOntology() lints it for modeling errors. So the loop is:
describe the domain in natural language → an LLM authors the ontology with the helper vocabulary →
validateOntology + tsc check it → describeOntology renders it back for review. See
docs/AUTHORING.md.
Examples
examples/healthcare/— the regulatory slice (wellness vs prescriber; allergy/interaction preconditions; dose limit = f(renal function, weight)); reconciliation; subscriptions.examples/finance/— proves the engine is domain-agnostic (zero core changes): permission bound to account ownership, limit bound to balance, outbox write-back to a broker.examples/commitments/— a proactive agent on the moat: aCommitmentobject with a status state machine, system-only governed delivery, and a time-driven heartbeat (what OpenClaw bolts on with JSON + cron).examples/clinic/+examples/brokerage/— two helper-authored domains (pnpm scenarios): rendered back to plain language + Mermaid, linted, and governed live across permission/precondition/limit.examples/feeds/—placeOrdergated on live external risk data + freshness (pnpm feeds): forget to refresh and it fails safe; stale data is denied; the live value is respected.examples/reactions/— an order lifecycle driven by an external payment event and an overdue-cancel scheduler (pnpm reactions), every transition governed.
Design & docs
docs/AUTHORING.md— how to author a domain ontology (by hand or with an LLM): the governance helper vocabulary, thenvalidateOntology+describeOntologyto check and review it.docs/ANTIPATTERNS.md— the design guardrails (what Synago deliberately does NOT do).CONTRIBUTING.md— workflow, the 5 principles, and how to add a subsystem.
License
MIT — see LICENSE. Clean-room (derived from a private design doc and public sources).
Contributions welcome — see CONTRIBUTING.md.
