zhangcun1995521/deepwork--packages-ontology ↗★ 0

@zhangcun123/ontology

Schema-driven ontology engine for DeepWork: declare an enterprise world once, and derive the agent's tool surface and permissions from it.

套件
@zhangcun123/ontology
相容性
待驗證
版本
0.1.9
授權
MIT
最近更新
2026年10月13日

安裝

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:zhangcun1995521/deepwork#945543c8e956d3e08e43bf9f6ee91e033d9b5add&path:packages/ontology

@zhangcun123/ontology

Declare an enterprise world once, and derive the agent's tools and permissions from it.

A plugin for DeepSeek Harness (DSH). Built on DSH, not a fork of it.

functions:
  open_issues_of:
    description: Issues still open against a given project.
    use: traverse
    relation: reported_on
    direction: incoming
    by: project_id
    where: { status: open }

Those eight lines add a tool the agent can call. No TypeScript, no registration, no rebuild.

Why

DSH ships a complete agent harness — loop, tools, skills, workflows, plan mode, approval, subagents, sandboxing. What it does not ship is any notion of the enterprise world the agent is working in. A general agent sees document_001, project_782, user_23891. An enterprise agent needs to know what a project is, who owns it, which customer it belongs to, and what it is allowed to touch.

That gap is semantic, not mechanical, and it is what this package fills.

Install

dsh plugin --profile web add @zhangcun123/ontology

Then point it at a domain pack — a directory holding schema.yaml and the instance files it names:

DEEPWORK_ONTOLOGY_ROOT=/path/to/domain dsh web

The tool surface

Four tools, and only one grows with the domain:

ToolArgumentsWhat it does
ontology_gettype, idfetch one entity
ontology_findtype, field?, value?, query?find entities by exact field and/or substring
ontology_linksrelation, fromfollow a declared relation
ontology_querymode, plus declared argumentsrun the domain's own declared functions

The first three are parameterized by type rather than duplicated per type, so a domain with forty entities still exposes three tools. type and relation are closed enums built from the schema, so the model cannot name something that does not exist. Only ontology_query's mode enum grows, and an enum value costs far less than a tool whose description crowds out the task.

A fourth tool appears when you say who may write:

ToolArgumentsWhat it does
ontology_writetype, id, datacreate or update one entity

Relations need no separate verb because a relation is a field — naming a relation's field inside data asserts the edge.

Authoring a domain

A domain pack is data. Five declarations, and nothing else is needed:

object_types:
  Project:
    primary_key: project_id                   # identity, and the file name
    required_fields: [project_id, status]      # what a write must supply
    file_location: objects/projects/P-*.yaml   # where instances live, and how far a write may reach
    written_by: [delivery-lead]                # who may write this type
    fields:
      status: { type: enum, values: [OnTrack, AtRisk, Delayed] }

link_types:
  reported_on:
    from: Issue
    to: Project
    via: project            # the field carrying the edge; defaults to the relation's name

functions:
  at_risk_projects:
    use: filter_by          # filter_by | traverse | search
    on: Project
    where: { status: [AtRisk, Delayed] }   # a list reads as "either of these"

  projects_for_customer:
    use: filter_by
    on: Project
    by: customer            # the field that becomes an argument, typed from its declaration

Five things worth knowing:

  • A relation is a field. An edge exists when an instance carries the field its relation names, and the instance's type is that relation's declared from. Both halves matter: without the second, a field that happens to share a relation's name produces a self-loop.
  • One field name may carry several relations. An issue and a document both reach their project through project; the instance's own type decides which relation applies.
  • where narrows both kinds of declaration. On a filter it narrows what you start from; on a traversal it narrows what you arrive at.
  • direction: incoming walks a relation backwards. reported_on runs Issue → Project, but "what is blocking this project" starts at the project. An inverse relation cannot be derived from the data, so it is asked for at query time.
  • Nothing is asserted by declaring. A relation no instance carries produces no edges, and the loader says which ones those are rather than leaving it to be discovered.

See examples/project-delivery for a complete pack.

Governance

Permissions are declared, not coded. There is no per-tool check to remember to add.

DEEPWORK_ONTOLOGY_ROLES='[{"prefix":"session-","role":"delivery-lead"}]'

The role map is the gate on the write tool. With none configured, ontology_write is not registered at all — an unconfigured deployment is a read-only one, rather than one that refuses every call. Each write is then checked against the target type's own declarations: written_by for who, file_location for how far the path may reach, required_fields for what must be present.

Every attempt is audited to /.audit.jsonl, refusals included:

{"at":"2026-09-29T06:38:26.612Z","action":"ontology_write","agentId":"session-1a8b…","role":"analyst","type":"Project","id":"P-003","outcome":"allowed","fields":["customer","owner","status"]}
{"at":"2026-09-29T06:37:58.367Z","action":"ontology_write","agentId":"session-f7fa…","type":"Project","id":"P-003","outcome":"denied","reason":"no-role","detail":"agent … matches no configured role"}

Field names are recorded; field values never are. An audit trail that copies domain content into a second file doubles the surface that content can leak from, which inverts what an audit is for. Refusals are recorded because "who tried and was turned away" is the question asked after something goes wrong.

What the model sees

Nothing about this package appears in the prompt until a domain is loaded. After that, the model sees the four tool definitions above and nothing else — no entity listing, no schema dump. Tool descriptions are generated from the domain's own words: each mode's description in ontology_query is the description its declaration supplied.

Arguments are typed from declarations rather than guessed. by: customer on a Project function takes the type and description of Project.customer, so a ref field yields a string described as "a Customer id".

A capped result always says it was capped. total, returned and truncated are required fields of every tool's output, because a result that hides its own truncation invites the model to read a partial answer as the whole one.

Known limitations

  • Reads reflect the loaded graph. A write refreshes it in place, but a domain edited by another process is only picked up on the next load or write.
  • Traversal is single-hop. ontology_links and traverse follow one relation. Multi-hop questions are currently several calls.
  • Writes are not transactional. A write rewrites its instance file; there is no rollback and no concurrent-write protection.
  • Role resolution is prefix-based. It matches an agent id against the configured map, so it inherits whatever the runtime's naming gives — in DSH's headless profile an agent id is a bare session- with no config-id prefix. Run once with roles unset and read the id out of the refusal rather than guessing.
  • The domain format is ours. It is not a standard, and there is no migration tool.

Compatibility

Tested against @deepseek-ai/dsh 0.1.7-rc.2. DSH is on an 0.1.x alpha/RC line where container types move quickly, and engines.dsh is declarative only — DSH does not enforce it, so a newer host will not refuse to load this. Re-test after each upstream release.

License

MIT.