tancheng33/dsh-ontology

dsh-ontology

A typed, inference-capable ontology (TBox + ABox) plugin for DeepSeek Harness — durable domain knowledge the agent declares, asserts against, and queries under schema constraints.

Uncategorizedagentcordisdeepseek-harness

dsh-ontology

English | 中文

A typed, inference-capable ontology for DeepSeek Harness — durable domain knowledge the agent declares, asserts against, and queries under schema constraints.

Most agent memory is a bag of strings: the agent writes prose, later reads prose, and nothing ever tells it that what it just recorded contradicts what it recorded last week. This plugin gives the agent a vocabulary it must respect. You declare what kinds of things exist and how they may relate; every claim is then checked against that model before it is stored, and claims that follow from other claims are derived rather than repeated.

ontology_define   declare classes and typed relations   (the TBox)
ontology_assert   record entities and facts             (the ABox, validated)
ontology_query    look things up, traverse, infer
ontology_retract  remove, with dependency protection

Why a TBox, not a notes file

notes-style memorydsh-ontology
Structurefree textclasses, typed relations, entities, triples
Bad inputsilently storedrejected, naming the constraint it broke
Implied factsrestated by hand, drift apartderived on read from transitive / symmetric / inverseOf
Retractionorphaned prose lingersfacts cascade with their entity; derived facts vanish with their premise
Reading it backgrep and hopequery by class (subclass-aware), triple pattern, neighbourhood, or shortest path

The constraint is the point. When the agent asserts ada depends_on api and depends_on is declared Component -> Component, it gets back:

REJECTED ada depends_on api: ada is not in the domain of depends_on
  (requires one of: Component; has: Person)

That is a real signal — either the claim is wrong, or the model of the domain is incomplete. Free-text memory can produce neither.

Install

dsh plugin --profile <name> add dsh-ontology
dsh --profile <name>

Or from a checkout / git host:

dsh plugin --profile <name> add ./dsh-ontology
dsh plugin --profile <name> add github:tancheng33/dsh-ontology

A github: install builds from source, so pnpm ≥10 needs the build allowlisted. Add allowBuilds: { dsh-ontology: true } to the profile's pnpm-workspace.yaml and re-run, and pin a commit (#<sha>) so a later push cannot silently change what runs. Installing from npm needs no allowance.

The ontology persists through the storage stack (dsh-storage + dsh-storage-json + dsh-storage-domain) that dsh-base ships since dsh 0.1.5, so the bundle contributes only its own ontology row. It therefore needs dsh ≥ 0.1.5-rc.2; on older harnesses add the three storage rows to your profile patch yourself. Data lands in $DSH_HOME/storages/<domain>.json.

Configuration

Override any of these in your profile's cordis.patch.yml:

KeyDefaultMeaning
domainontologyStorage domain name; one name is one isolated graph. Must match /^[a-z][a-z0-9_]*$/.
stricttrueReject facts that violate the vocabulary. false stores them with the violations recorded, for exploratory modelling.
inferencetrueAllow queries to return entailed facts.
maxInferred20000Cap on derived facts per read. Past it, results carry inferenceTruncated: true and the rendered text says so; raise it for graphs with large transitive closures.
promptSectiontrueContribute the vocabulary summary to the system prompt.
promptMaxTerms60Upper bound on terms in that summary.
promptOrder150Where the section lands in the assembled prompt.
defaultLimit / maxLimit50 / 500Query result sizes.
maxEntities / maxFacts20000 / 100000Capacity guards; a write past the bound fails loudly.

Running two isolated graphs is just two rows:

- insert:
    - id: ontology-team
      name: dsh-ontology
      config: { domain: team_ontology, strict: true, inference: true, maxInferred: 20000,
                promptSection: true, promptMaxTerms: 60, promptOrder: 150,
                defaultLimit: 50, maxLimit: 500, maxEntities: 20000, maxFacts: 100000 }

(A patch replaces a row's whole config, so restate every key.)

A worked example

Declare the vocabulary. Order does not matter — entries are resolved by repeated passes, so a subclass may precede its parent and a relation may precede its inverse.

// ontology_define
{
  "classes": [
    { "id": "Service", "subClassOf": ["Component"] },
    { "id": "Component", "comment": "A deployable unit of the system" },
    { "id": "Person" }
  ],
  "relations": [
    { "id": "depends_on", "domain": ["Component"], "range": ["Component"],
      "characteristics": ["transitive"] },
    { "id": "owns", "domain": ["Person"], "range": ["Component"], "inverseOf": "owned_by" },
    { "id": "owned_by", "domain": ["Component"], "range": ["Person"] },
    { "id": "version", "domain": ["Component"], "rangeKind": "literal" }
  ]
}

Assert instances. Entities are created before the facts of the same call, so one call introduces an individual and its relationships together.

// ontology_assert
{
  "entities": [
    { "id": "api", "classes": ["Service"] },
    { "id": "auth", "classes": ["Service"] },
    { "id": "pg", "classes": ["Component"] },
    { "id": "ada", "classes": ["Person"] }
  ],
  "facts": [
    { "subject": "api", "predicate": "depends_on", "object": "auth" },
    { "subject": "auth", "predicate": "depends_on", "object": "pg" },
    { "subject": "ada", "predicate": "owns", "object": "api", "source": "CODEOWNERS" },
    { "subject": "api", "predicate": "version", "object": "2.1.0" }
  ]
}

Query, including what was never asserted.

// ontology_query
{ "mode": "facts", "subject": "api", "predicate": "depends_on", "includeInferred": true }
api depends_on auth
api depends_on pg (inferred: transitive)
// ontology_query
{ "mode": "path", "from": "ada", "to": "pg", "depth": 4 }
connected in 3 step(s):
  ada owns api
  api depends_on auth
  auth depends_on pg

Query modes

ModeAnswers
schemaWhat vocabulary exists? (Start here.)
statsHow big is the graph, and how much is inferable?
entitiesWhich individuals are in class X (including its subclasses), or match text Y?
factsWhich triples match this pattern? Each omitted position is a wildcard.
neighborsWhat surrounds this entity, out to depth N?
pathHow are these two entities connected?
dumpEverything asserted — terms, entities, facts — as data, for export or backup. Re-import is ontology_define + ontology_assert.

The rules it enforces

Subsumption. subClassOf is transitive: an entity declared Service is also a Component, so it satisfies a Component domain and is returned by a Component query. Cycles are refused at definition time — a cycle would make "is an X" unfalsifiable for every class on it.

Domain and range. A relation's domain constrains subjects, its range constrains objects, both closed under subsumption. An empty list means unconstrained, which is the honest encoding of "not yet decided" rather than a silent allow-all.

Entity vs literal. rangeKind: "literal" makes a relation attribute-valued (a version string, a date). A literal-valued relation can be neither symmetric, transitive, nor inverted — the derived triple would have a literal in subject position — and that incoherence is refused at definition time rather than producing nonsense later.

Functional cardinality. A functional relation admits at most one object per subject. A second, different object is a functional-conflict, not a silent overwrite. Re-asserting the same object stays idempotent.

Entailment. transitive, symmetric, and inverseOf are applied to a fixpoint, so the rules compose (the inverse of a transitive relation is itself closed transitively). Entailed facts are derived on read and never stored — retract a premise and everything resting on it disappears, with no stale derivation left behind. Each carries via naming the rule that produced it.

Retraction safety. Retracting an entity cascades to every fact mentioning it. Retracting a term is refused while anything still depends on it — a class that still classifies an entity or appears in a relation signature, a relation still asserted — and the refusal names the dependent.

Redefinition safety. Redefining a term replaces it wholesale, so the stored facts are re-checked against the would-be vocabulary: narrowing a domain or range, flipping rangeKind, adding functional over a subject that already has two objects, or cutting a class out of a subClassOf chain that facts rely on is refused under strict, naming the facts it would break. Under strict: false it is applied and the damage is returned as warnings.

Duplicate guard. A new entity whose id differs from an existing one only by case, separators, or a ns: prefix (mod:api beside api), or whose label is an existing id, is accepted but comes back with a possible-duplicate warning that spells out the merge to run. The store cannot know whether they are one thing, so it says so instead of guessing.

Merging. When two ids turn out to name one individual, ontology_assert with merges: [{ from, into }] rewrites every fact about from onto into, unions the classes, keeps into's label and comment (filling gaps from from), and drops from. Rewrites that collide with existing triples collapse; a rewrite that would violate the vocabulary (a functional relation seeing two objects) refuses the merge under strict.

Scale

Reads are indexed and memoized per write generation: the first query after a write derives the closure once (a worklist fixpoint over an adjacency index, not a quadratic scan), and every query until the next write reuses it. Writes keep a subject index so a functional check costs the subject's degree, not a table scan. On a 21k-entity / 101k-fact synthetic dependency graph (pnpm bench 20000 3, Apple Silicon):

OperationBefore 0.20.2
assert 101k facts in batches of 500340 s0.5 s
first inferred query after a write> 6 s at 10k facts0.1 s
repeated inferred queries> 6 s each< 1 ms
shortest path, depth 8, inferred> 6 s16 ms

Entailment stops at maxInferred derived facts and says so rather than silently returning a prefix of the closure.

The storage medium is the default dsh-storage-json unit (one JSON document per domain, rewritten atomically per write). For graphs in the tens of thousands of facts, override the storage-json row in your profile patch with dsh-storage-sqlite; the plugin does not care which backend serves its domain.

The prompt section is assembled once per turn by the harness, so vocabulary defined during a turn appears in the standing context from the next turn on.

Code Mode

Every tool is reachable as a typed call, with the canonical JSON value (not the rendered prose) as the result:

const { facts } = await tools.ontology_query({
  mode: 'facts', predicate: 'depends_on', includeInferred: true,
})
const upstream = facts.filter(fact => fact.via === 'transitive').map(fact => fact.object)

Using the reasoning core on its own

The rule engine is pure — no IO, no storage, no Cordis — and ships as its own entry point, so it can be used outside a harness:

import { entail, findPath, validateFactInput } from 'dsh-ontology/ontology'

Development

pnpm install
pnpm test        # 60 unit tests: rules, tools, prompt section
pnpm typecheck
pnpm build
pnpm smoke       # boot smoke against a real dsh profile (see below)
pnpm bench 2000 3

tests/memory-domain.ts is an in-memory stand-in for one open storage domain, so the store and the tools are tested end to end — real validation, real entailment, real tool results — without a backend.

pnpm smoke is the test the unit suite cannot be. It installs the packed tarball into a fresh dsh profile created from the shipped headless template (so dsh-base is present), boots it against a scripted local LLM that speaks both wire formats the harness has used, drives ontology_define → ontology_assert → ontology_query with inference, then boots a second time over the same $DSH_HOME to check persistence and that the prompt section now carries the vocabulary. No network beyond the npm registry, no API key. DSH_VERSION picks the harness (CI runs the oldest supported line and next); DSH_BIN reuses an installed one; SMOKE_PLUGIN_SPEC=dsh-ontology@0.1.0 demonstrates that the test catches the duplicate-storage-row boot crash of that release.

Requirements

  • DeepSeek Harness >= 0.1.5-rc.2 (dsh-base must ship the storage stack)
  • Node ^22.19 || >=24

License

MIT