Repository navigation
Hive language editing
A hive package is data, so an agent can change its own hive: a better prompt, a clarified playbook item, a sharper goal. The risk is that the agent edits whatever judges it instead.
An orchestrator or editor agent edits the package's files directly, as it
wants. The host then calls tinyhivemind_lang::check, which works like a
compiler: it runs every rule of the language and of the package's immutable
constitution, and either reports every violation, each located by file and
JSON Pointer, or accepts the edit and splits it into one classified patch per
edit class. Those patches are the recorded form of the edit, and
tinyhivemind_lang::lineage archives each evaluated design by its content, so
the host chooses the active design from the archive instead of trusting the
latest edit.
Nothing here evaluates or stores anything. The host runs the held-out evaluation, decides the verdict, and keeps the archive. The crate only checks edits and folds records.
This page walks through
self-improving,
the gallery package built for it: a solver, a critic, and an editor that sum
invoice amounts. Every edit and result below was run against that package;
tests/direct_edits.rs
pins them, and
tests/gallery_packages.rs
pins the first accepted and first refused patch.
{
"hard_constraints": [
"Never read the held-out invoices",
"Never edit the evaluator configuration"
],
"max_turns": 60,
"max_width": 8,
"permitted_edits": ["prompt", "context", "goal"],
"read_only_memories": ["eval-ledger"],
"protected_namespaces": ["team:self/project:heldout"],
"contamination_canaries": ["HELDOUT-7F3A"],
"evaluation": { "kind": "exact_match", "heldout_cases": 3 },
"acceptance": { "require_all_heldout": true, "min_improvement": 1 },
"telemetry": { "lineage": true }
}The editor may rewrite a seat prompt or role charter, a playbook item, or a
goal. It may not add a seat, loosen a policy, rewire the topology, or touch
memory. The held-out namespace is out of reach for every seat, the string
HELDOUT-7F3A must never appear in a candidate, and the evaluation ledger is
read-only.
The editor's loop is: copy the package, rewrite whatever files it wants, run the check, fix what it reports, repeat until clean.
cargo run -p tinyhivemind-lang --example hive_check -- \
crates/tinyhivemind-lang/examples/hives/self-improving edited/Suppose the editor raised its own turn budget in hive.json, pointed the
mission at a role that does not exist, raised max_turns in the
constitution, and pasted the held-out canary into the playbook. One run
reports all four, each where it lives, and exits 1:
constitution.json:/max_turns: immutable: the constitution is host-owned and no edit may change it
context/playbook.md: contamination: contains a contamination canary the constitution holds out
hive.json:/missions/0/role: reference: unresolved role reference ghost
hive.json:/policies/episode/turn_budget: forbidden_class: the constitution does not permit policy edits
4 findings
--json prints the same findings as an array of
{"file", "pointer", "rule", "message"} objects; pointer is null for a
Markdown file. A broken JSON file is reported with serde's position, such as
memory.json: json: invalid json in memory.json: expected value at line 3 column 1. Appending one line to seats/solver.md instead prints
ok: changes prompt and exits 0. With a single directory the command
diagnoses that package alone.
In code, a host calls check(&original_files, &edited_files) on two
BTreeMap<String, String> document maps, or check_packages on two decoded
packages. Either returns Result<Review, Vec<Diagnostic>>.
| Rule | Reported when |
|---|---|
immutable |
constitution.json changes; any evaluation, telemetry, logging or constitution key, or an acceptance key outside a goal, changes in the manifest or a frontmatter; an existing document's frontmatter changes |
forbidden_class |
a change belongs to an edit class the constitution does not permit |
unowned |
a change no class owns: hive.json:/name, a top-level README.md, or a file that is not part of a package at all |
contamination |
a constitution canary appears in the edited manifest, memory, or any document |
read_only |
a pinned memory identity, or its bindings, changes |
context |
a context document changes outside its items, items move, or a new item is not at the end |
sequence |
the per-class steps cannot each be valid on their own (see below) |
original |
the original package is invalid, which is the host's problem rather than the editor's |
Every validation finding of the edited package
is reported too, located: a seat's unresolved role at
hive.json:/seats/<i>/role, a missing prompt at the manifest field naming
it, bad frontmatter on the Markdown file, memory findings in memory.json.
diagnose(&package) and diagnose_documents(&files) give the same collected
report for one package; validate and parse still return the first finding
as their error.
An accepted edit returns a Review: candidate, the edited package, and
changes, one Patch per touched class in class order (prompt,
context, memory, roster, topology, policy, goal). Applying them in
order with patch::apply from the original passes a valid package at every
step and ends at exactly the candidate. The host records one lineage record
per change, each the parent of the next; the last record's id is the edited
package's content id. A host that evaluates only the whole edit gives every
step the whole's scores and verdict.
If the class order would pass through an invalid package — a new seat's
memory binding must follow the roster change that adds the seat — the first
order valid at every step is used. When none is, as for a new seat whose
prompt frontmatter restates a binding the same edit adds, the check returns
one sequence finding and the editor splits the edit.
Memory contents are not files. A learning operation still travels as a
memory patch through patch::apply, below.
A change belongs to exactly one class. The check infers the classes an edit
touched; a recorded Patch has one class and an ordered list of
operations, every one within that class, and the constitution must permit the
class.
| Class | May change |
|---|---|
prompt |
The body of a seats/*.md or roles/*.md document, keeping its frontmatter |
context |
Items in context/*.md; whole context/*.md documents created or retired; /manifest/context
|
memory |
Anything under /memory/; host learning operations |
roster |
/manifest/roles, /manifest/seats, /manifest/desks; seats/ and roles/ documents created or retired |
topology |
/manifest/topology, /manifest/referrals
|
policy |
/manifest/policies, /manifest/coordinator
|
goal |
/manifest/goals, /manifest/missions
|
No class can write the constitution. No class can write an evaluation,
telemetry, logging, or constitution key anywhere in the manifest or in
any frontmatter, or an acceptance key outside a goal. A goal's own
acceptance criteria are editable under goal. The host's acceptance
settings in the constitution are not.
check derives patches; a host can also build one itself and call
patch::apply, which it must do for memory learning. The rest of this
section describes that wire.
Each operation carries a precondition, the exact value it expects to find. If the package has moved on since the editor read it, the whole proposal fails and nothing is applied.
operation |
Fields | Meaning |
|---|---|---|
json |
pointer, before, after
|
Set or remove a JSON Pointer under /manifest/ or /memory/. before: null requires the value to be absent; after: null removes it. An array index equal to the length appends. |
document |
path, before, after
|
Create (before: null) or retire (after: null) a whole Markdown document. Exactly one side is present; rewriting an existing document this way is refused. |
prompt |
path, before, after
|
Replace a prompt or charter body. before must equal the current body exactly, and after may not start a new frontmatter block. |
context |
path, item, before, after
|
Add, replace, or remove one <!-- item:id --> item. before: null requires the item to be absent; after: null removes it. |
learning |
identity, entry, watermark, before, after
|
A memory write handed back to the host. The crate opens no store; the host checks before at watermark inside its own memory transaction. |
patch::apply(&package, &patch) builds the candidate from a private copy,
guards it, and returns an AppliedPatch holding the validated candidate and
any learning operations for the host. It never mutates the caller's package.
patch::guard(&original, &proposed, &patch) is the check on its own, for a
host that received a candidate from somewhere else. It refuses when:
- the class isn't in
permitted_edits(ForbiddenClass); - the constitution, or any protected key in the manifest or frontmatter,
differs (
Immutable); - replaying the patch on
originaldoesn't produce exactlyproposed(CandidateMismatch), which catches a forged candidate that slipped in an extra change; - a canary appears anywhere in the candidate's manifest, memory, or documents,
or in the patch itself (
Contamination); - a pinned memory identity or its bindings changed, or learning targets a
read-only identity (
ReadOnly); - learning names an unknown identity, an empty entry, or neither a
beforenor anafter(Learning); - the candidate fails package validation
(
Validation), so a patch can never produce a package thatparsewould reject.
While the candidate is being built, apply can also refuse with
ClassBoundary (an operation outside the class), Stale (a precondition
that no longer holds), Pointer (a malformed pointer or a missing parent),
Item (bad item markers), or UnknownField (a nested key that isn't part of
a policy schema).
patch::diff(&original, &proposed, class) works in the other direction. It
computes the patch between two packages, then guards it before returning:
- For
manifestandmemory, it emits onejsonoperation per differing top-level field, such as/manifest/goalsor/memory/bindings, carrying the whole before and after values rather than a minimal path. - A created or removed document becomes one
documentoperation. - A changed context document becomes one
contextoperation per changed item. - Any other changed document becomes one
promptoperation. Changed frontmatter fails withImmutable.
Because the result is guarded, a diff that crosses two classes fails instead of returning a patch the class couldn't apply.
Each edit below is shown as the patch it records. Editing the file directly
and running check derives exactly this patch.
The editor saw the solver drop canceled invoices, so it appends one line to the solver's prompt, which records as:
{
"class": "prompt",
"operations": [
{
"operation": "prompt",
"path": "seats/solver.md",
"before": "Sum every invoice amount you are given and return the total. Show the line items you added so the critic can check them.\n",
"after": "Sum every invoice amount you are given and return the total. Show the line items you added so the critic can check them.\nInclude canceled invoices.\n"
}
]
}apply returns Ok. prompt is permitted, seats/solver.md is a seat
document, the body matches exactly, and the candidate validates. Running
diff(&package, &candidate, PatchClass::Prompt) on the result gives back
this same patch.
The playbook is itemized, so the editor can rewrite one item and leave the others alone:
{
"class": "context",
"operations": [
{
"operation": "context",
"path": "context/playbook.md",
"item": "status",
"before": "Include every invoice regardless of status unless the task says otherwise.\n\n",
"after": "Include every invoice, canceled ones too, unless the task says otherwise.\n\n"
}
]
}An item's body runs up to the next marker, so before includes the blank
line that separates status from rounding. A new body must end in a
newline and may not contain a marker line of its own.
{
"class": "goal",
"operations": [
{
"operation": "json",
"pointer": "/manifest/goals/0/acceptance",
"before": ["Matches the held-out total exactly"],
"after": ["Matches the held-out total exactly", "Shows every line item"]
}
]
}An acceptance key inside goals is allowed. The same key anywhere else is
protected.
{
"class": "policy",
"operations": [
{
"operation": "json",
"pointer": "/manifest/policies/episode/turn_budget",
"before": 9,
"after": 12
}
]
}apply returns Err(ForbiddenClass), "edit class is not permitted". Giving
itself more turns is exactly what this constitution exists to prevent. This
is the refusal the gallery test pins.
Relabelling the patch as "class": "prompt" with the same json operation
fails with ClassBoundary("/manifest/policies/episode/turn_budget"). A
prompt patch can't carry a json operation at all.
These were also run against the package:
| Proposal | Result |
|---|---|
A context item whose body contains HELDOUT-7F3A
|
Contamination |
A prompt operation whose before no longer matches the body |
Stale("seats/solver.md") |
A prompt operation whose after begins with ---\n
|
Immutable |
A memory patch with a learning operation for solver-notes
|
ForbiddenClass (memory is not permitted here) |
A roster patch creating seats/auditor.md
|
ForbiddenClass |
guard given the accepted prompt patch but a candidate that also rewrote seats/critic.md
|
CandidateMismatch |
The examples/self_edit.rs example runs the direct-edit cycle — an editor
returning whole files, check, evaluation, one record per change, and
accepted_head — on a minimal in-code package rather than self-improving.
Its tampering editor also raises max_turns, and is refused with
constitution.json:/max_turns: immutable: ...:
cargo run -p tinyhivemind-lang --example self_editlive_self_edit does it with a live model as the editor, which returns the
whole new prompt file; see the
live hive language record.
An accepted patch is a candidate and nothing more. The host evaluates it, then
records the result in an archive it owns. tinyhivemind_lang::lineage
addresses each design by its content and folds the archive.
lineage::content_id(&package, &watermarks) returns
sha256:<64 hex digits>. The hash covers the canonical JSON of the whole
package, with object keys sorted recursively, together with a host event
watermark for every hive-lifecycle memory identity. Watermarks are part of
the identity because an unchanged package can behave differently once its
persistent memory has changed.
self-improving declares two hive-lifecycle identities, solver-notes and
eval-ledger. A watermark map must cover exactly those two: an empty map
fails with Watermarks ("persistent memory watermarks do not match
identities"). The same package with solver-notes at watermark 7 instead of 0
gets a different id. Every document, including an unreferenced README.md,
is part of the package, so two hosts that load different file sets get
different ids for what looks like the same package.
lineage::record(&candidate, metadata) validates the candidate, checks that
the scores are named, non-empty, and finite, and computes the id. It returns
an immutable Record:
{
"id": "sha256:784d…a1",
"parent": "sha256:3387…d3",
"patch": { "class": "prompt", "operations": [ ... ] },
"class": "prompt",
"rationale": "...",
"transcript_refs": [3],
"scores": {
"heldout": { "value": 3.0, "direction": "maximize" },
"turns": { "value": 6.0, "direction": "minimize" }
},
"verdict": "accepted",
"memory_watermarks": { "eval-ledger": 0, "solver-notes": 7 }
}The ids are shortened here. transcript_refs are transcript sequence numbers
and never transcript payloads. Every objective declares whether higher or
lower is better. lineage::verify(&record, &package) recomputes the id and
fails with Identity when the record doesn't describe that package: verifying
the accepted record above against the original, unedited package fails.
The archive is an ordered slice of records, rejected ones included. Both folds
check it first: every id must be well formed, no design may repeat
(Duplicate), every class must match its patch, and a parent must appear
before its child (Parent, "parent must precede child in archive").
-
lineage::accepted_head(&archive)returns the lastacceptedrecord. A rejected candidate stays in the archive as evidence but never becomes the active design. -
lineage::pareto_frontier(&archive)returns every non-dominated design in archive order, whether accepted or rejected, since a rejected design can still be a useful stepping stone. Every record must score the same objectives in the same directions, or the fold fails withIncomparable. A tie doesn't dominate, so tied designs are all kept.
In the walk-through, with illustrative scores standing in for a host's
evaluation, the archive is the original package (accepted,
heldout 1, turns 8), the prompt edit (accepted, heldout 3, turns 6), and a
follow-up that rewrote the prompt to "Guess the total." (rejected,
heldout 2, turns 7). accepted_head returns the prompt edit, and the
frontier is the prompt edit alone, since it is better on both objectives than
the other two.
- Running
checkon every edit, and acting only on an acceptedReview. - Running the held-out evaluation and choosing the verdict.
- Storing every candidate package and every record.
- Applying
learningoperations inside its own memory transaction, at the stated watermark. - Activating an accepted design between turns or sessions. Nothing here changes a running hive.
tinyhivemind is GPL-3.0-only. Built by @senamakel.
Start here
The algebra
- Shared medium
- Desks and rosters
- Mentions
- Cross-desk referral
- Transcript projection
- Threads
- Recall
- Private asides
- Responder ladder
Hive mechanics
- Hive episodes
- Trace grammar
- Transactive memory experiment
- Episode policy
- Benchmarks
- Benchmarks: live rooms
- Benchmarks: delegation
- Benchmarks: private exchange
- Benchmarks: scale
Hive language
Working on it
Reference