Skip to content

Hive language

Steven Enamakel edited this page Oct 11, 2026 · 3 revisions

Hive language

A hive package is a hive written down as data: its roles, seats, desks, goals, policies, and memory layout. tinyhivemind-lang reads a package, checks it, and turns it into the core values a host needs to start the hive. Because the whole definition is data, an editor can edit its files directly and the host can check, evaluate, and archive that change. Hive language editing covers that part. Hive gallery walks through the seven example packages.

This page describes the format and the three calls that handle it: parse, validate, and lower. The accepted behavior is in the language specification, and the boundary decision is ADR 0032.

A package is a map of strings

The crate never touches the file system. A package is a BTreeMap<String, String> that maps a relative path to the file's contents, and the host builds that map however it likes: from a directory, a database row, or a test's include_str!. parse turns the map into a typed Package, validates it, and returns it.

The crate stays pure for the same reasons core does. It reads no files, runs no agents, calls no model, and owns no evaluation, archive, or memory store. lower produces the inputs a host needs to create seats, and the host decides when to activate them. The crate does not name OpenHuman types either. That conversion lives in tinyhivemind-openhuman's language module, so any host can use the language. It is in the pure_crates list that .github/scripts/assert-pure.sh checks.

File layout

review-board/
├── hive.json            # the manifest: roles, seats, desks, goals, policies
├── constitution.json    # host-pinned caps and edit authority
├── memory.json          # memory identities and bindings, never contents
├── roles/author.md      # one charter per role
├── roles/reviewer.md
├── seats/author.md      # one prompt per seat, optional JSON frontmatter
├── seats/reviewer-a.md
├── seats/reviewer-b.md
└── context/*.md         # shared documents split into stable items (optional)

parse sorts the keys it is given into three groups:

  • hive.json, constitution.json, and memory.json are required. Each is decoded into its typed section.
  • Every other key ending in .md, in any letter case, is kept in Package::documents. That includes Markdown no manifest entry references, such as a README.md. It is accepted and kept, so it counts toward the package's content identity.
  • Every other file is dropped. The gallery uses this: script.json and sim.json belong to the hive_view example, not to the package.

Every key has to be a safe relative path, including keys that end up dropped. A path fails if it is empty, starts with /, contains \ or a control character, has an empty, ., or .. segment, or contains :.

hive.json: the manifest

The examples on this page come from the review-board package, trimmed where marked. Every field is required except the nullable ones such as model, which may be left out, and unknown fields are rejected.

{
  "$schema": "https://tinyhumans.ai/schemas/hive/v1",
  "version": 1,
  "id": "review-board",
  "name": "Review board",
  "roles": [
    { "id": "author", "charter": "roles/author.md" },
    { "id": "reviewer", "charter": "roles/reviewer.md" }
  ],
  ...
}

$schema and version identify the wire version. Any other value fails with Error::Schema. The URI is a name and is never fetched. id is the package identity the host uses for activation and lineage. A role points to its charter in roles/.

  "seats": [
    {
      "id": "reviewer-a",
      "label": "reviewer-a",
      "role": "reviewer",
      "prompt": "seats/reviewer-a.md",
      "template": "hive-view",
      "config": { "max_iterations": 8, "temperature": 0.0 },
      "model": null,
      "tools": { "allow": ["file_read"], "deny": ["shell"] },
      "mcp": [],
      "skills": []
    }
  ],

A seat fills one role and has one prompt under seats/. template names a host factory and must not be blank. config is non-secret host configuration, passed through untouched. model, tools, mcp, and skills go to the host's seat factory. No credential ever appears here.

  "desks": [
    {
      "id": "board",
      "name": "Review board",
      "description": "Decide whether the retry-policy change merges",
      "members": ["author", "reviewer-a", "reviewer-b"],
      "responder_mode": "auto"
    }
  ],
  "goals": [],
  "missions": [],
  "topology": [],
  "referrals": [],
  "context": [],

A desk is core's own Desk value, in its own wire form; see Desks and rosters. Every member must be a declared seat, listed once. review-board leaves the rest empty. The other packages fill them in:

  • Goals have a description, acceptance criteria the host evaluates, and subgoals. The decomposition must be acyclic. research-debate splits a verdict goal into survey and critique.
  • Missions bind a role to the goals it owns, such as {"id": "decide", "role": "moderator", "goals": ["verdict"]}.
  • Topology and referrals are directed {"from", "to"} desk links. Topology is a graph and may have cycles; math-swarm chains five desks in a ring. See Cross-desk referral.
  • Context lists shared documents under context/ (see below).
  "policies": {
    "episode": {
      "turn_budget": 9, "round_width": 3, "revealed_width": 3,
      "blind_round": true, "dominance_cap": 50, "repetition_cap": 3,
      "distance": "sequence",
      "quorum": { "threshold": 2, "window": 30, "require_grounded": true },
      "weights": { "recency": 5, "importance": 30, "relevance": 20, "half_life": 20 }
    },
    "routing": {
      "minimum_confidence": 0, "high_impact_minimum_confidence": 0,
      "clarification_threshold": 1000000, "high_impact_threshold": 1000000,
      "round_width": 4, "choice_option_limit": 8
    },
    "approval": { "enabled": false, "default": "deny", "rules": [], ... },
    "aside": { ... }, "referral": { ... }, "digest": { ... }, "brevity": { ... },
    "directory": { ... }, "exchange": { ... }, "division": { ... },
    "conduct": { "child_turn_wall": 6, "turn_wall": 60 }
  },
  "coordinator": {
    "round_width": 4,
    "conduct_policy": { "child_turn_wall": 6, "turn_wall": 60 },
    "broadcast_budget": null,
    "retention": { "settled_episodes": null, "delivered": null,
                   "interrupted": null, "pending_per_agent": null }
  }

policies reuses core's policy structs in their own wire form: episode is EpisodePolicy, routing is the semantic routing policy, and so on. All eleven sections are required, and so is every non-nullable field inside them. A package that wants the defaults still writes them, so the file says exactly what will run. The routing probabilities are integer parts per million, where 1000000 is certainty and 250000 is one in four. coordinator holds host-neutral settings that the OpenHuman adapter copies field by field into its coordinator options.

constitution.json: what no editor may touch

{
  "hard_constraints": [],
  "max_turns": 60,
  "max_width": 8,
  "permitted_edits": [],
  "read_only_memories": [],
  "protected_namespaces": [],
  "contamination_canaries": [],
  "evaluation": null,
  "acceptance": null,
  "telemetry": null
}

The host supplies the constitution, and no edit can change it. max_turns and max_width cap every turn budget and round width in the manifest. permitted_edits lists the patch classes an editor may use, and review-board permits none. read_only_memories pins memory identities, protected_namespaces fences off memory no seat may reach, and contamination_canaries are strings no edit may introduce. hard_constraints, evaluation, acceptance, and telemetry are opaque. The crate carries them and blocks edits to them, but what they mean is the host's business. self-improving fills every field in.

memory.json: identities and bindings, never contents

review-board has no memory ({"root": "team:hive", "identities": [], "bindings": []}), so this section uses memory-guild.

{
  "root": "team:guild",
  "identities": [
    { "id": "archivist-notes", "namespace": "team:guild/agent:archivist",
      "kind": "agent", "lifecycle": "hive", "read_only": false, "fork_parent": null },
    { "id": "charter", "namespace": "team:guild/project:charter",
      "kind": "pool", "lifecycle": "hive", "read_only": true, "fork_parent": null },
    { "id": "scout-a-notes", "namespace": "team:guild/project:lore/agent:scout-a",
      "kind": "agent", "lifecycle": "run", "read_only": false, "fork_parent": "lore" }
  ],
  "bindings": [
    {
      "seat": "archivist",
      "identity": "archivist-notes",
      "reads": [
        { "at": "team:guild/agent:archivist", "inherit": false, "descendants": false },
        { "at": "team:guild/project:lore", "inherit": false, "descendants": true },
        { "at": "team:guild/project:charter", "inherit": false, "descendants": false }
      ],
      "recall_at": ["session_start", "rejoin", "compaction"],
      "budget_chars": 4000,
      "remember": ["observation", "outcome", "note"]
    }
  ]
}

An identity is either an agent memory or a shared pool. Its lifecycle is run (one run only) or hive (it persists across runs). It can be read-only, or a fork beneath a parent identity. A binding gives one seat one identity. reads sets the namespaces the seat may read, recall_at sets when the host recalls, and budget_chars bounds the recall. remember limits which entry kinds the seat may write. The package never holds remembered contents; the host's memory store does.

Markdown documents

A role charter or seat prompt is plain Markdown.

You review for correctness. Re-run the failing case in your head before you
speak; `!support #merge ^N` only when a cited message shows it passes.

The markers in the prompt are the trace grammar. The language doesn't interpret them. It only hands the prompt to the host.

JSON frontmatter

A seat prompt or role charter can repeat its manifest fields as frontmatter. memory-guild's seats/archivist.md:

---
{"id":"archivist","role":"archivist","memory":"archivist-notes","budget_chars":4000}
---
You are the archivist. Recall the lore pool and the pinned charter before you
speak, cite the decision record you rely on, and remember outcomes, not chatter.

Frontmatter is optional and only JSON is accepted. It is a JSON object between an opening ---\n, which must be the document's first four bytes, and the first \n---\n. The manifest is the source of truth, and frontmatter may only repeat it:

  • A seat's frontmatter may hold any field of its manifest seat (id, label, role, prompt, template, config, model, tools, mcp, skills) and its memory binding settings (reads, recall_at, budget_chars, remember, plus memory for the bound identity id). Each value must equal the manifest's value.
  • A role's frontmatter may hold only id and charter.
  • A document that opens with ---\n and never closes it, or whose frontmatter is not a JSON object, is rejected.

Each of these failures is Error::Frontmatter. Lowering strips frontmatter, so the host gets only the prompt body.

Context documents

A shared context document is split into items by stable markers. From research-debate's context/brief.md:

# Embedding index brief

<!-- item:latency -->
Rebuilding the index takes 41 seconds on the reference corpus; loading a cached
copy takes 1.2 seconds.

<!-- item:staleness -->
The corpus changes on roughly one run in five. A cache keyed on the corpus
digest is invalidated exactly then.

An item id uses ASCII letters, digits, -, and _, and must be unique within the document. An item's body runs from its marker line to the next marker. An editor can add, replace, or remove one item without rewriting the rest of the document, which keeps context that has built up over many runs intact.

Each document has to sit in the directory for its purpose. A seat's prompt must be under seats/, a role's charter under roles/, and a context path under context/, and all of them end in .md. That stops an edit class from reaching a document through another class's directory.

Validation

parse validates before it returns. A host that builds a Package some other way, such as the whole-package JSON form below, must call validate itself. lower validates first anyway. Each check fails with a typed Error. The messages below are what the gallery packages produce after one deliberate change.

Check Error Example message
A required file or referenced document is absent MissingDocument missing document seats/author.md
JSON does not match the schema, including unknown fields at any depth Json invalid json in hive.json: unknown nested schema field
Wrong $schema or version Schema unsupported package schema or version
An empty or duplicate id among roles, seats, desks, goals, missions, desk members, memory identities, namespaces, or bindings, or a blank template Identity invalid seat identity reviewer-a
A role, seat, desk, goal, or memory id that does not resolve Reference unresolved role reference auditor
An unsafe path, or a document outside its purpose directory Path unsafe relative path roles/author.md
A bound or constitution cap broken Bound invalid policy bound episode.width
A goal that is its own ancestor GoalCycle cyclic goal decomposition at <id>
A malformed memory namespace Namespace invalid memory namespace <ns>
A protected, out-of-root, inheriting, or wrongly pinned memory reach MemoryAccess forbidden memory access team:other
Malformed or duplicate context item markers Context invalid context item markers in <path>
Frontmatter disagreeing with the manifest Frontmatter frontmatter disagrees with manifest for author

A few rules are worth calling out:

  • Widths and budgets. turn_budget must be nonzero and at most max_turns. round_width must be nonzero and at most revealed_width, which in turn must be at most max_width. This is stricter than core, where the default blind width is 4 and the revealed width is 1, so a package has to choose compatible widths explicitly. Division, routing, and coordinator widths are also capped by max_width, and the conduct turn walls by max_turns. These caps apply whatever edit class produced the package.
  • Probabilities. A routing probability above 1000000 fails to decode (invalid json in hive.json: probability exceeds 1000000).
  • Unknown fields. serde's deny_unknown_fields catches unknown keys at the top level. A second pass compares the decoded value with its input to catch unknown keys nested inside reused core policies. The one exception is "responder_mode": "lead": core omits the lead mode when it serializes, so writing lead explicitly is accepted.
  • Memory. Namespaces use the tinymemory shape: up to eight kind:id parts joined by /, where kind is one of agent, team, user, ws, project, or source. Every identity and every read must be inside root. An agent identity's last part must be agent:<id>. A fork must sit beneath its parent's namespace. An identity the constitution pins must be declared read_only. No identity or read may touch a protected namespace, and inherit: true is always rejected because it would reach tinymemory's global root. validate_namespace and validate_path are exported for hosts that want those checks alone.

Lowering

lower(&package) validates, then returns a LoweredHive, a snapshot the host can activate:

Field What the host gets
id, name The package identity and display name
desks Core Desk values, ready for the roster
candidates One routing RouteCandidate per seat: the role filled in, the role's charter body as the description, and tools.allow as capabilities
seats One LoweredSeat per seat: the factory template and config, the prompt body without frontmatter, model, tools, MCP, skills, and the resolved memory binding
policies, coordinator The core policies and coordinator settings, unchanged
goals, missions, topology, referrals Kept as declared, for the host to install
context Each declared context path with its full contents
memory The whole memory spec, without contents

A seat with a memory binding gets a LoweredMemoryBinding. Its agent_id is the text after the namespace's last :, and its root is the text before the last /, or the spec's root when there is no /. In memory-guild, the archivist's team:guild/agent:archivist becomes agent archivist under team:guild, and scout-a's fork team:guild/project:lore/agent:scout-a becomes agent scout-a under team:guild/project:lore. The OpenHuman adapter accepts an agent identity only when its namespace is exactly <root>/agent:<agent_id>.

The host must enforce the lowered memory constraints when it registers a runner; lowering only passes them along. Activating a lowered snapshot is the host's decision. It happens between turns or sessions, and a turn already in flight keeps the snapshot it captured.

The whole-package form

Package also serializes as one JSON object with four fields: manifest, constitution, memory, and documents. A host can store or ship a package that way. live_self_edit writes one, and the OpenHuman live_language example reads it back. Reading it with serde_json skips parse's Markdown filter and its nested unknown-field pass, so call validate (or lower) before using it. Validation re-checks every document path.

Viewing a package

The hive_view example does the file reading the library won't do. It loads a package directory into the map, parses it, lowers it, and then renders it or runs it through core's hive::step, offline or with --live against a real model. A package of several desks runs one floor per desk, linked only by referral along its declared links (see Hive gallery):

H=crates/tinyhivemind-lang/examples/hives
cargo run -p tinyhivemind-lang --example hive_view -- $H/review-board            # text tree
cargo run -p tinyhivemind-lang --example hive_view -- $H/review-board --mermaid  # flowchart
cargo run -p tinyhivemind-lang --example hive_view -- $H/review-board --json     # one JSON object
cargo run -p tinyhivemind-lang --example hive_view -- $H/review-board --run      # play an episode

--run with no model replays review-board's script.json:

review-board on desk board — 3 seats, budget 9 turns, rounds of 3

round 1 (Deliberate, Blind)
          author  salience  blind saw 1/1  !propose #merge ^1 The retry policy caps attempts ...
      reviewer-a  salience  blind saw 1/2  !question ^1 Does the cap also apply to idempotent reads ...
      reviewer-b  salience  blind saw 1/3  !propose #revise ^1 I want a load test before this merges.
round 2 (Deliberate, Full)
          author  salience  full  saw 4/4  !evidence ^2 Rollback is a config flag ...
      reviewer-a  dissent   full  saw 4/5  !support #merge ^2 The 200/200 run covers the failing case ...
      reviewer-b  salience  full  saw 4/6  !support #merge ^5 The rollback flag answers my load concern ...
round 3 (Commit, Full)
          author  addressed full  saw 7/7  !commit #merge Recording the decision.

  converged on #merge
  3 rounds, 7 turns

saw 1/2 means the seat's projected view held one of the two lines in front of it. The opening round is blind, so no one sees their peers' work in progress. A package without a script runs a seeded default script instead, and a package with sim.json runs the math simulation. --run --frames streams NDJSON frames, --run --trace out.jsonl writes core telemetry, and --seed, --round-width, --turn-budget, and --problem change the run. The example's README pins the wire format.

In the live viewer

The lab viewer drives the same binary from a browser:

cd examples/lab/viewer
npm install
npm run dev            # http://localhost:8099

?view=hive opens the gallery, and ?hive=<name> opens one package in the player, for example http://localhost:8099/?hive=review-board. The player draws desks as boxes and seats as nodes, with topology as solid arrows and referrals as dashed ones. It replays rounds with speech bubbles and quorum standings, and shows exactly which lines each seat saw. &seed=N, &round_width=N, &turn_budget=N, and &problem=<id> rerun it with different settings. ?compare=math-panel,math-swarm&seeds=N opens the Compare tab. The server has no authentication, so keep the port on localhost. Its README covers the API and its bounds.

Clone this wiki locally