cd /news/large-language-models/show-hn-an-llm-agent-that-emits-type… Β· home β€Ί topics β€Ί large-language-models β€Ί article
[ARTICLE Β· art-38445] src=github.com β†— pub= topic=large-language-models verified=true sentiment=Β· neutral

Show HN: An LLM agent that emits typed intent

A developer released an open-source reference implementation of an LLM agent that emits typed intents using a domain vocabulary, separating intent from execution. The project introduces the Structured Intent Format (SIF) as a typed contract, with a deterministic layer that validates and executes intents without exposing the LLM to storage primitives. It includes working examples in legal and vet domains, exploring ontology-driven architectures for business applications.

read26 min views1 publishedJun 24, 2026
Show HN: An LLM agent that emits typed intent
Image: source

What this is.A reference implementation of apattern:intent-execution separationfor LLM agents β€” the model emits typed intent in domain vocabulary and never touches the substrate, while a deterministic layer validates, translates, scopes, and executes it. The project ships two deliverables, in deliberate priority order:

Primary β€” SIF and the intent-execution separation pattern it makes concrete.The typed intent contract, and the deterministic layer beneath it that validates, translates, scopes, and executes. The load-bearing contribution; the rest of the repo exists to give it a worked, running instance. See[SIF is the core].Secondary β€” the experimental "application as declarative content" approach.A working, agent-driven business application assembled per domain from three declarative artifacts β€” anontology(a typed domain model in plain YAML), a set ofbusiness rules(plain text the agent reads), and anagent prompt(persona + interaction protocol) β€” over a smalldomain.json

descriptor, withno hand-written service layer. Whether a genuinely useful application can emerge from "ontology + rules + prompt" instead of imperative code is the open question; thelegal

andvet

domains are the evidence offered. This builds on SIF and is deliberately lower-priority. Detail:[Where does the business logic live?].Full statement of the pattern:

[.]docs/design-intent-principle.md

Experimental, non-academic research project.Explores the boundaries of what LLMs can do as a business logic layer in ontology-driven architectures. Every hypothesis here is tested empirically against the example application shipped in this repo β€” thelegaldemo domain serves as the experimental harness, not as a showcase demo. Conclusions reached only by argument, without a running example to back them up, don't count.

How to read this.If you want to see the designrunningβ€” file layout, demo domains, REST surface, what the agent does in practice β€” keep reading this README and explore thedomains/

folder. If you want the designrationaleβ€” the tool-surface problem, the vocabulary discipline, why state machines live where they do β€” read[. Both views describe the same system from opposite ends.]docs/design-sif.md

SIF is the coreSee it in actionBeyond business applicationsHow it worksWhere does the business logic live?Tech stackPrerequisitesQuickstartConfigurationREST surfaceAdding a new domainTestsWindows notesDesign docsLicense

SIF β€” Structured Intent Format β€” is a typed intent contract over ontology entities. It is the load-bearing piece of this project; everything else is built around it.

  • The LLM emits structured intents (find

,create

,update

,delete

,link

,unlink

,transition

) using only theontology vocabulary of the active domain β€” classes, properties, relations, transitions, value sets. It never sees SQL, physical table names, user IDs, or any storage primitive. - The framework validates every intent against the ontology, thenroutes it to whichever adapter can fulfil it. Adapters live behind the federation SPI (DataSource

interface) and speak ontology types, not storage primitives. - An adapter can be SQL, document, key-value, REST, an internal method call, a vector store β€” anything that can fulfil ontology-level intents. The LLM never knows β€” and need not know β€” which adapter served a given entity.

Today two adapters ship: SqlDataSource

on PostgreSQL (full CRUD plus transitions) and MongoDataSource

on MongoDB (find

and create

; the remaining write verbs β€” update

/delete

/link

/unlink

/transition

β€” are planned and currently return a recoverable "not supported"). The legal

demo exercises the Mongo read path. The storage layer is a private implementation detail behind the SIF contract, not the framework's identity. Adding a method-call, REST, or vector-store adapter is a matter of writing a new DataSource

implementation β€” the SIF surface, the ontology, and the LLM prompt stay unchanged.

That separation buys two things at once:

Trust inversion. The LLM is the natural-language intent router; the framework is the trusted executor. The LLM is sandboxed by the JSON-Schema-bounded SIF grammar β€” it can express only declared verbs and ontology names, never SQL, shell, URLs, or any storage primitive β€” so a prompt-injected or hallucinating model has no expressible way to reach the substrate directly. Authorization is aseparateconcern: because everything below the SIF surface runs deterministically, there is one place after the LLM to enforce it. The demo ships a deliberately simple owner-scoping example there; a real RBAC/IdP plugs in at the same seam (seeWhere authorization fitsbelow). It is not an authorization model.Backend neutrality. The LLM expresseswhatit wants; the framework decideshowto fulfil it. No prompt change, no ontology change, no SIF change when an entity moves from a SQL table to a service method behind a REST endpoint.

A single ontology file in the project's compact YAML format is the source of truth for each domain. "Ontology" is informal shorthand throughout: the vocabulary is OWL-inspired, but what ships is a typed domain model in plain YAML, not an OWL ontology in the strict semantic-web sense β€” no reasoning, no SPARQL, no equivalence/restriction axioms, no subClassOf

-driven inference. See docs/design-sif.md ("About the word ontology") for what is kept from that tradition and what is dropped.

Two domains ship. legal β€” a law-firm matter manager covering matters, conflict checks, documents, billable time, invoicing and hearings β€” is the rich proof-of-concept. vet β€” a veterinary clinic (owners, pets, appointments, visits, treatments, billing) β€” is a deliberately simple, SQL-only second domain: the same engine serving a different ontology, with nothing but declarative content swapped. Both run a single managing identity that sees the whole domain.

The shipping legal demo routes one ontology to two substrates. Most entities β€” Lawyer

, Paralegal

, Client

, Matter

, TimeEntry

, Invoice

, Hearing

, ConflictCheck

β€” live in PostgreSQL through SqlDataSource

. The MatterNote

entity β€” free-form working memos attached to matters β€” lives in MongoDB through MongoDataSource

. The LLM sees one unified ontology and emits the same shape of SIF intent for both:

{
  "operations": [
    { "op": "find", "entity": "Lawyer",
      "filters": { "barNumber": "SBA-2026-1042" } },
    { "op": "find", "entity": "MatterNote",
      "filters": { "createdDate": "2026-06-12" } }
  ]
}

How the framework dispatches it:

Entity Adapter What the adapter does
Lawyer
SqlDataSource
Translates to SELECT … FROM lawyers WHERE bar_number = ? against Postgres. When a domain declares a scoping identity, an identity predicate is injected here too β€” the current single-role demos declare none (see Where authorization fits).
MatterNote
MongoDataSource
Translates the intent to a MongoDB query against the matter_notes collection. In the demo this collection is unscoped β€” notes are shared working memos. The adapter can scope a collection per session when its mapping declares an identityField (injected into every query); the legal demo's MatterNote mapping declares none, so every session sees all notes.

The LLM never knows one entity lives in a relational store and another in a document store. It emits the same shape of intent for both. The framework picks the adapter per resolved op's entity class via DataSourceRegistry.findFor(entity)

. If tomorrow a third entity moved to a method-call adapter or a REST endpoint, only the adapter implementation would change β€” the SIF intent, the ontology, and the LLM prompt would stay identical.

Configuration is per-domain. The legal demo's domain.json

declares two data sources (primary: sql

for the relational entities, documents: mongo

for MatterNote

), and per-adapter mapping files (mapping.yaml

, mapping.mongo.yaml

) bind each class to its substrate. Adding a third data source is a config addition plus a third mapping file β€” the SIF surface, the LLM prompt, and the rest of the ontology stay unchanged.

That is the federation primitive: not "the framework can talk to Postgres or Mongo," but "the framework serves a single ontology from multiple substrates at the same time, transparently to the LLM."

Where this fits in the value story. The heterogeneous case is where ontology most visibly earns its keep β€” a unified semantic vocabulary above disparate substrates. Ontology also pulls weight even within a single adapter (typed LLM tool surface, pre-execution validation, lifecycle protection, cross-domain prompt reuse, audit framing) β€” see docs/design-sif.md.

LLM agents are reliable with a handful of tools and less so as the count grows β€” each new tool is another choice the model can pick wrong on, another set of arguments it can hallucinate. SIF keeps the agent's per-turn choice small even as the domain gets bigger: seven op verbs and an ontology of typed names β€” entities, properties, relationships, transitions β€” injected into the tool schema as enums. The agent picks slots inside a small, typed grid; it doesn't navigate a sprawling tool catalog. That is the principle the whole architecture rests on β€” the LLM is an intent router, not an executor β€” and it is given its full treatment in docs/design-intent-principle.md.

Beyond the two payoffs already described β€” trust inversion and backend neutrality β€” the same seam yields, with no per-feature work:

Audit & replay. Every operation is a structured, human-readable intent record. The on-disk session logisthe audit trail.Conversational atomicity. One user intention ("set up the cosigner and link her to my new loan") = onesubmit_sif

batch = one transaction at the adapter. No bespoke saga/compensation code per use case.Refactor-safe. Physical schema changes (or moving an entity from a table to a service method) touch the adapter layer, not the LLM prompt. The same ontology can target multiple physical schemas (e.g. multi-tenant with isolated DBs) or mixed backends.Debuggable. When something breaks, the trace shows the LLM's original intent unchanged β€” the bug is either in the intent or strictly downstream of it. No "did the LLM hallucinate?" guessing.

The fair version of the critique: SIF's modifier vocabulary (filters

, aggregate

, sort

, limit

, relations

, resolve

) looks SQL-shaped. It is β€” because relational stores serve those modifiers uniformly, so the SQL-flavoured shape is the pragmatic surface for SQL-backed entities.

What that observation misses:

SQL is one adapter, not the framework. A method-backed entity and a SQL-backed entity look identical to the LLM. The framework decides which adapter serves which class. The LLM never writes SQL, never reads SQL, never knows SQL is involved.The verbs are backend-neutral.find

,create

,update

,delete

,link

,unlink

,transition

are ontology-level intents, not relational operations. A REST adapter fulfils them with HTTP calls; a method-call adapter dispatches them to service methods.The modifiers degrade gracefully. Non-SQL adapters honour the subset of modifiers their backend supports and reject the rest with a structuredFederationException

β€” the same recoverable refusal pattern the SQL translator already uses for "cannot build join path."SIF is deliberately smaller than SQL. NoGROUP BY

, noHAVING

, noUNION

, no window functions, no raw expressions, no subqueries outsideresolve

.*Not more expressive than SQL β€” less expressive than SQL, in exactly the dimensions where less is safer.*An established pattern in the same direction. GraphQL, OData$filter

, DynamoDBExpressionAttributes

β€” all chose smaller-than-SQL typed DSLs for the same reason: the calling layer cannot be fully trusted, so the language it speaks must be one the server can mechanically validate, scope, and execute.

The screenshots below are the legal

demo running end to end β€” nothing is mocked. In each, the left pane is the business user's conversation; the right "under the hood" pane is the live SIF trace: the typed intent the LLM emitted, the SQL or Mongo query it was deterministically translated to, and the result handed back.

A matter's structured record lives in PostgreSQL; its free-form working notes live in MongoDB. The user neither knows nor cares which store holds what β€” they ask one question, and the framework fans a single batch of intent out to both substrates.

Prompt:In a single batch, look up two things for matter MTR-2023-001: the Matter record itself, and its working notes (entity MatterNote, where aboutMatter is MTR-2023-001).

One submit_sif batch carries two find ops. The trace shows the first op translated to SELECT … FROM lgl_matters WHERE matter_number = :p1 against Postgres, and the conversation already shows the assembled answer β€” the matter record together with its three working notes.

Scrolling the same trace down to the second op: a green MONGO find against the matter_notes collection with query {"matterId": "MTR-2023-001"}, returning the notes from MongoDB. One intent, two substrates β€” and no SQL or Mongo syntax ever crosses the LLM boundary.

A single natural-language instruction that touches two entities. The agent resolves the matter and both staff members by name, then writes a billable time entry and a follow-up task as one atomic batch.

Prompt:On matter MTR-2023-001, log 3.5 hours of billable time for Derek Okafor reviewing the expert report, and open a task for Sofia Ramirez to assemble the exhibit bundle due 2024-04-12.

The conversation confirms both records were created. The trace opens with the resolution finds β€” locating the matter and the two staff members by name before any write happens.

The create intent itself: two ops β€” a TimeEntry and a Task β€” submitted together in a single submit_sif batch.

The deterministic translation: two INSERT … VALUES (…, (SELECT lgl_matter_id …), (SELECT lgl_staff_id …)) statements. Foreign keys are resolved by sub-select from the names the user gave, and both inserts commit in one transaction β€” one instruction, one atomic unit of work.

Lifecycle changes are governed by a declared state machine, not by the model's judgement. Here the user asks for a transition that isn't legal from the matter's current state.

Prompt:Run the Engage transition on matter MTR-2023-001 now.

The LLM emits a transition intent β€” and the engine refuses it: current state 'active' is not in the legal source set [conflict_check]. Engage is only legal from conflict_check, and this matter is already active. The model cannot override the guard; it receives the structured error and explains the refusal back to the user. The LLM proposes; the deterministic layer decides.

The shipping demo exercises a business-data domain (legal

), but the construction is general. Anywhere an agent acts on a typed, structured surface β€” files, devices, messages, deployments, regulated records β€” the same shape applies: a typed intent surface declared up front, a deterministic translator from intent to effect, identity injection after the LLM, and, for anything with a side effect, a declared transition bound to a post-action. The LLM only ever emits SIF β€” never a shell command, an SMTP exchange, or a device API call. Two concrete intents, in different domains:

Send an email β€” an action with a side effect. The model drafts the message field by field with create

, then fires it with a transition

; both ops ride in one batch, so they commit together:

{
  "operations": [
    { "op": "create", "entity": "Email",
      "data": { "to": "client@acme.com",
                "subject": "Engagement letter",
                "body": "Your engagement letter is ready to sign." } },
    { "op": "transition", "entity": "Email", "transition": "send" }
  ]
}

send

is a declared transition (draft β†’ sent

); its smtpSend

post-action opens the connection and dispatches the message. The wire protocol, auth, and retries live in code the model never authors. And like the vehicle below, send

can carry preconditions that run before the message leaves β€” a recipient allowlist, a content scan that blocks sensitive data in the body or attachments (DLP), or a human-approval gate. The model fills the fields; the framework decides whether the mail is allowed to go out.

Set a vehicle's speed β€” a safety-gated physical command. Vehicle

is the entity; the target speed is one of its fields, and applying it is a gated transition. The session is scoped to its own vehicle, so the intent names no VIN β€” it just writes the setpoint, then asks to enact it. Writing the field is inert, and the model does not get to decide whether enacting is safe:

{
  "operations": [
    { "op": "update", "entity": "Vehicle", "data": { "targetSpeedKph": 30 } },
    { "op": "transition", "entity": "Vehicle", "transition": "applySpeed" }
  ]
}

Writing targetSpeedKph

only records a wish β€” nothing moves. The applySpeed

transition is where authority lives: it runs its declared preconditions first β€” surroundingsClear

(sensor fusion), withinPostedSpeedLimit

(map and sign data), withinDynamicLimits

(traction and road conditions) β€” and only if all pass does the commandPowertrain

post-action send the setpoint to the drive controller. If a gate fails, the transition is rejected with a structured error the agent reads and reasons about; it cannot override the gate, raise the limit, or reach the controller directly. The agent supplies intent; the framework keeps authority over whether the car moves.

None of these domains ship (the demos run SQL + Mongo), but nothing about them is special-cased: each is a new adapter plus a few declared transitions behind the unchanged SIF surface. (For brevity the enacting snippets elide the row-locating filters

that update

and transition

carry in real SIF β€” here the target is the entity created in the same batch, or the session's identity-scoped row.) For more, framed as where the architecture could go rather than what ships, see docs/design-bounded-agency.md.

Authorization is not a pillar of this project, and the framework does not ship an authorization model. What it offers is a seam and a simple demonstration of it.

Because the LLM emits typed intent and never touches the substrate, there is a single deterministic point β€” after translation, before execution β€” where authorization can be enforced. That is a genuinely useful place: the model can't read or remove what's applied there, and every operation that passes through is a structured, auditable record (verb, entity, typed args, session identity, transaction) rather than a shell transcript or pixel stream.

The framework provides that seam: a flat post-translation injector scopes a session to the rows it owns (e.g. a client seeing only their own matters). That is a teaching example, deliberately minimal β€” not a real authorization system. Roles, fine-grained read/write, delegation, and hierarchies are the domain of dedicated engines (Keycloak, OPA, an RBAC/ABAC service), and they plug in at exactly this seam. The framework does not try to reimplement them.

Caveat β€” the shipped demos don't exercise scoping today.Bothlegal

andvet

now run a single managing identity (CaseManager

/ClinicManager

) that sees the whole domain: their identity entity is referenced by no other table, so the injector adds no predicate. The seam, the injector, and theIdentityScope

mechanism are exactly as described; they're simply dormant in the current single-operator demos. A domain that declares an owner-scoped identity (e.g. a client, a pet owner) reactivates the live demonstration with no code change β€” only content.

What makes the seam worth having, whichever engine fills it: enforcement is below the LLM. Identity predicates and preconditions are applied by the deterministic layer after the model runs; the LLM neither sees nor can bypass them. (That the LLM also can't emit effect code in the first place is the trust-inversion point from SIF is the core.)

There is also an approval seam between resolve and execute β€” the framework produces a structured plan there that could be gated (an interactive prompt, RBAC plus dry-run, cryptographic signoff). Nothing s there today; it is an extension point, not a feature.

Schema design pipeline (one-time per domain, fully deterministic β€” no LLM, no API key)

<domain>.ontology.yaml
    β†’ Planner            (deterministic)        β†’  <domain>_schema_plan.yaml
    β†’ Builder            (deterministic)        β†’  module_<name>.json per module
    β†’ Reconciler         (deterministic)        β†’  <domain>_schema.json
    β†’ SchemaInstaller    (deterministic)        β†’  CREATE TABLE … in Postgres
    β†’ loadSeedSql        (deterministic)        β†’  INSERT … from the checked-in
                                                  <domain>_seed_data.sql fixture

Every step is exposed under POST /api/domains/{domain}/provision/...

.

SIF Engine (deterministic, LLM-free at runtime)

SIF operation (ontology vocabulary)
    β†’ SifParser     β†’  typed Find / Create / Update / Link / Transition / ...
    β†’ SifResolver   β†’  resolved against the OntologyModel
    β†’ SifExecutor   β†’  picks the DataSource per resolved op's entity class
        ↓
    DataSource adapter (per ontology class)
      ─ SqlDataSource     β†’  translator β†’ IdentityInjector β†’ JDBC β†’ Postgres
      ─ MongoDataSource   β†’  translator β†’ MongoDB collection (find + create)
      ─ (future) MethodCallDataSource  β†’  dispatch to a service method
      ─ (future) RestDataSource        β†’  HTTP request to an external API

The SIF surface and the federation SPI (DataSource

interface) speak ontology types. Each adapter privately decides how to fulfil the intent. Today SqlDataSource

(full CRUD plus transitions) and MongoDataSource

(find

  • create

) ship; the others are concrete extension points the architecture already supports β€” same interface, same SIF contract, no LLM-facing changes when an adapter is added.

LLM Agent (one consumer of the SIF Engine)

User question
    β†’ ConversationAgent (Claude)
        β”œβ”€β”€ system prompt = ontology + business rules + persona
        β”œβ”€β”€ tool = submit_sif (entity/relation/transition names baked in)
        β”œβ”€β”€ loop: LLM β†’ tool_use β†’ SifExecutor β†’ tool_result β†’ LLM …
        └── final text reply

The agent speaks ontology vocabulary; everything below the SIF surface runs deterministically.

This section is the project's second deliverable (after SIF): the experiment of assembling a working application from declarative content β€” ontology + business rules + agent prompt β€” rather than from hand-written code. The rest of this section is the evidence for whether that holds up.

Switching domain does not switch applications.The sidebar's domain dropdown swaps the activeontology, its business rules, and the LLM persona β€” nothing else. The same engine, REST API, JVM, and codebase serve every domain. Adding a new domain is purely a content addition: four small files describe the domain, and the agent immediately speaks that vocabulary.

Side effects that aren't pure content:the per-domain DB schema must be installed (the provisioner builds and runs it deterministically from the ontology) and any transition handler classes are Java implementations shipping alongside the ontology underdomains/<name>/_generated/

. Those are the only places real code lives.

A working domain in this repo carries very little code. Take domains/legal/

:

domains/legal/
β”œβ”€β”€ domain.json              β€” descriptor: data sources, identity entities, file pointers
β”œβ”€β”€ legal.ontology.yaml      β€” entities, properties, relationships, transitions
β”œβ”€β”€ legal.rules              β€” business rules in plain text (=== SECTION === headers)
└── legal_prompt.txt         β€” agent persona + identity protocol

That's the whole per-domain surface. Four small files of declarative content β€” the demos ship ontology-only; the physical schema and mapping are generated by provisioning.

Everything else is produced by the framework:

  • the physical schema ( CREATE TABLE

statements, FK declarations) β€” generated deterministically from the ontology by the provisioner pipeline; - the SQL the agent runs at runtime β€” produced per request by the translator from the resolved op + mapping;

  • input validation that catches typos and out-of-vocabulary names before they reach the database β€” driven entirely by the ontology;
  • the audit trail, identity scoping, and transaction control β€” generic, applied uniformly to every domain;
  • demo data is a checked-in seed_data.sql

fixture loaded deterministically in one transaction β€” beyond that baseline, records are created by talking to the agent, not by any LLM seed-generation step (the provisioner has no LLM call at all).

The exception is the demonstration transitions. Generated Java handlers under domains/<domain>/_generated/

are wired to the precondition and post-action names declared in the YAML β€” conflictCheckCleared

, createEngagementLetter

, notifyClientInvoiceIssued

, and so on. In the demo these are no-op stubs: preconditions return success, post-actions trace the call and return. They exist to show where imperative business code plugs in β€” the registered-interceptor escape hatch described in docs/reference-sif-vocabulary.md

β€” not as a worked implementation. Even so, the names that bind to Java live in the ontology; the Java is just the implementation slot behind a declarative entry.

What's worth noting once the pieces come together: the reason it works isn't that the LLM is unusually powerful β€” it's that:

  • most of a domain (structure, lifecycle, constraints) really does fit in declarative form once you have the right primitives;
  • the framework does a lot of generic lifting below the surface β€” type resolution, SQL translation, identity injection, transaction control;
  • the LLM never writes code or SQL. It only picks ontology terms out of the typed grid the tool-schema builder injects, so what looks likebusiness reasoning is actually a constrained selection task.

This isn't free. The LLM has cost and non-determinism, prompts need iteration, the ontology format is opinionated, and not every domain fits cleanly. But for the kind of problem this targets β€” an agent operating on real business data with rules, lifecycles, and audit trails β€” the amount of new code needed to add a working application stays small. That is the observation worth highlighting.

Component Technology
Backend Java 21, Maven multi-module
Web framework Spring Boot 3.3 (only in ontocortex-app )
Ontology Compact YAML format, OWL-inspired vocabulary (Apache Jena 5 retained for the legacy TTL parser)
Federation SPI DataSource interface in ontocortex-federation β€” backend-neutral
Shipping adapters SqlDataSource on PostgreSQL 15 (JDBC) and MongoDataSource on MongoDB 7 (find + create ) β€” the SIF contract is the same for both
LLM Claude (Anthropic Messages API via the JDK HttpClient )
Frontend Vue 3 + Vite + TypeScript
Containers Docker β€” PostgreSQL + MongoDB (and Testcontainers spins up both for integration tests)
  • JDK 21 (configured as a Maven toolchain β€” see ~/.m2/toolchains.xml

) - Node 20+ (for the frontend)

  • Docker (for the bundled Postgres + Mongo and the Testcontainers-backed tests)
  • An Anthropic API key β€” console.anthropic.com.Only required for the LLM Agent (/chat

); SIF and the entire deterministic provisioner pipeline run without it.

The full path from a clean checkout to a working chat is the five steps below β€” do them in order.

Provisioning is manual and REST-driven.Creating the database tables and the demo data isnotdone by the UI anddoes nothappen automatically when the backend starts. You trigger it yourself by POSTing to the/provision/*

endpoints (step 4). Thedocker-compose

databases start empty; until you provision, the agent answers every question with no data. There is no CLI shim and no "provision" button β€” any HTTP client works.

Run the commands below from the repo root. The provisioning calls use curl

, which is cross-platform; any HTTP client works. On Windows substitute mvn.cmd

for mvn

(see Windows notes).

git clone https://github.com/gabert/ontocortex.git
cd ontocortex
docker-compose up -d

Brings up both containers β€” ontocortex_postgres

(the primary

SQL source) and ontocortex_mongo

(the documents

source for MatterNote

). On a fresh volume the Postgres container auto-creates the per-domain databases (legal_demo

, vet_demo

) from docker/postgres-init/

; step 4 then creates the tables inside them and loads the data. The stores otherwise come up empty. (If you have an old volume from before this init script existed, recreate it: docker-compose down -v && docker-compose up -d

.)

Runs on port 8080 β€” leave it running.

export ANTHROPIC_API_KEY=sk-...             # only needed for /chat; provisioning is keyless
mvn -f engine/pom.xml install -DskipTests    # build + install all modules
mvn -f engine/pom.xml -pl ontocortex-app spring-boot:run

On Windows use mvn.cmd

(and set the key with $env:ANTHROPIC_API_KEY = "sk-..."

) β€” see Windows notes. Without an API key the app still boots and the entire /provision/*

pipeline works β€” only /chat

returns HTTP 409 with a clear error.

Required, manual. This is the step that's easy to miss. The containers from step 2 are empty, so you must create the tables and load the demo data yourself. The deterministic build artifacts (schema_plan.yaml

, module_*.json

, schema.json

, seed_data.sql

) are already committed in the repo, so for the shipped legal

and vet

demos you only need the two calls that touch the live databases β€” create the tables, then load the seed data:

curl -X POST http://localhost:8080/api/domains/legal/provision/install        # CREATE TABLE …
curl -X POST http://localhost:8080/api/domains/legal/provision/install/seed   # load seed data

install

returns the created table names. ** install/seed loads every data source the domain declares in one call** β€” it runs

seed_data.sql

into Postgres and, for a domain with a document store (legal's MatterNote

), loads seed.mongo.json

into MongoDB. It returns { "inserts": <sql-rows>, "documents": <mongo-docs> }

; for legal

expect 6 documents. There is no separate Mongo seed step and nothing platform-specific β€” the same curl

does both stores.Repeat for vet

(or any domain you add) by swapping the name in the path (vet

is SQL-only, so its documents

count is 0):

curl -X POST http://localhost:8080/api/domains/vet/provision/install
curl -X POST http://localhost:8080/api/domains/vet/provision/install/seed

That's everything a fresh checkout needs β€” the committed schema artifacts already cover the planning steps. (The full pipeline plan

β†’ build

β†’ reconcile

β†’ install

β†’ install/seed

, plus install/drop

to start over, lives in the REST surface table; you only need it if you change a <domain>.ontology.yaml

.)

Runs on port 5173; proxies /api/ to the backend on :8080.*

cd frontend
npm install
npm run dev

Open http://localhost:5173. Pick a domain in the sidebar, optionally set X-User-Id

for identity scoping, and chat. The right pane shows, per turn, the LLM's submitted intent, each SIF operation (including transitions), the generated SQL or MongoDB query, and the row count for each.

All runtime config lives in engine/ontocortex-app/src/main/resources/application.yaml

under the engine:

key. Spring Boot's relaxed binding accepts environment-variable overrides (ENGINE_API_KEY

, ENGINE_MODELS_CHAT

, etc.).

Key Purpose
engine.api-key
Anthropic API key (typically ${ANTHROPIC_API_KEY} )
engine.models.{chat,analyzer}
Model IDs per pipeline role (chat agent + error-analysis post-mortem)
engine.chat.{max-tokens,max-retries,retry-delay,max-iterations}
Conversation runtime
engine.log-dir
Per-session debug JSONL + error post-mortems
engine.domains-dir
Where domain content folders live
engine.data-sources.<name>.{dbname,user,password,host,port}
Per-data-source DB credentials
Method + path Description
GET /api/domains
List available domains
GET /api/domains/{domain}
Domain summary
GET /api/domains/{domain}/schema
Reconciled schema.json
GET /api/domains/{domain}/schema/description
Human-readable schema dump
POST /api/domains/{domain}/provision/plan
Run the deterministic planner
POST /api/domains/{domain}/provision/build
Deterministic builder
POST /api/domains/{domain}/provision/reconcile
Merge module builds into schema.json
POST /api/domains/{domain}/provision/install
CREATE TABLE …
POST /api/domains/{domain}/provision/install/seed
Seed every data source: seed_data.sql β†’ Postgres, seed.mongo.json β†’ Mongo
POST /api/domains/{domain}/provision/install/drop
Drop every table
POST /api/domains/{domain}/sif
Execute a SIF batch (no LLM) β€” X-User-Id header for identity scoping
POST /api/domains/{domain}/chat
Talk to the agent (needs API key) β€” X-User-Id for identity scoping
DELETE /api/domains/{domain}/chat
Clear conversation history for this user
  • Create domains/<name>/

with:domain.json

β€” descriptor (name, data sources, file pointers)<name>.ontology.yaml

β€” ontology in the project's compact YAML format (seedocs/reference-ontology-yaml-format.md

)<name>.rules

β€” business rules (plain text with=== SECTION ===

headers)<name>_prompt.txt

β€” agent persona / system prompt fragment

  • Add a data-sources.<name>

block toapplication.yaml

. - Drive the provisioner pipeline via the REST endpoints above.

The framework discovers the new domain automatically on the next request β€” no code changes, no restart for content edits beyond re the affected application.yaml

.

mvn -f engine/pom.xml test

All 431 tests pass with Docker running. 62 of them are Testcontainers-backed integration tests β€” SifExecutorTest

, SchemaInstallerTest

, StiIntegrationTest

, DemoDomainProvisioningTest

, MongoDataSourceIntegrationTest

, and the 11-step EndToEndTest

β€” which spin up real Postgres + MongoDB containers; the remaining 369 are pure unit tests with no external dependency. All LLM-using code (BaseAgent retry, ConversationAgent tool-use loop, AgentPipeline rollback) is unit-tested against a fake LlmClient

β€” no live API calls in CI.

Use PowerShell mvn.cmd

(not the bash mvn

shell script). The bash environment on Windows often picks up the wrong JAVA_HOME

and a different trust store, which surfaces as toolchain mismatches or PKIX TLS errors when fetching dependencies. Maven 3.9+ with the JDK 21 toolchain config (already in engine/pom.xml

) works out of the box from cmd or PowerShell.

For Git over HTTPS the same PKIX wall can appear β€” use SSH for origin

to sidestep it.

In-depth design notes and reference specs live in docs/ β€” see

for an annotated index that tags each doc as reference or design.

docs/README.md

Copyright 2026 Robert Gallas.

── more in #large-language-models 4 stories Β· sorted by recency
── more on @structured intent format 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/show-hn-an-llm-agent…] indexed:0 read:26min 2026-06-24 Β· β€”