Skip to content

Host integration

Steven Enamakel edited this page Sep 1, 2026 · 3 revisions

Host integration

The host owns storage, transports, model clients, HTTP routes, authorization, rendering, and the choice of async executor. tinyhivemind owns none of that and never will. What it asks for is four ports, and three of them are optional.

SessionLog

Required for anything in the runtime crate.

pub trait SessionLog: Send + Sync {
    fn read_before(&self, before: Option<Sequence>, limit: usize) -> SessionFuture<'_>;
}

Contract:

  • return rows newest first;
  • treat before as an exclusive sequence bound;
  • honour limit;
  • keep sequences unique.

The trait is object safe and returns a boxed future, so it picks no executor for you. Violating the ordering, uniqueness, size, or cursor contract produces a typed page-validation error rather than a plausible-looking history, which is deliberate: a silently wrong transcript is worse than a failed turn.

See Transcript projection.

Selector

Optional. Needed only if some desk uses ResponderMode::Auto.

pub trait Selector: Send + Sync {
    fn select<'a>(&'a self, request: &'a SelectionRequest) -> SelectorFuture<'a>;
}

It receives a raw message, a canonical desk id, and a bounded candidate list. No transcript, no tools, no host handles. It returns text intended to name one candidate id, and accept_selection accepts it only if it does.

A selector that is unavailable, disabled, or ambiguous falls back to the deterministic first effective member. Nothing breaks when the model is down.

See Responder ladder.

MentionTurnQueue

Optional. Needed only if you enable mention dispatch.

pub trait MentionTurnQueue: Send + Sync {
    fn enqueue_once(&self, request: MentionTurnRequest) -> MentionTurnFuture<'_>;
}

This is the only idempotency boundary in the system, and the contract is the strictest of the three. One atomic transaction keyed by the bound conversation plus request.key.trigger_sequence. Inside it, re-read the stored committed agent reply and verify its source, content, conversation and sequence; revalidate live feature policy, authorization and target availability; then durably enqueue at most one child turn. Return Already for a duplicate and Refused for an expected live rejection. A rollback must leave neither an idempotency record nor a child turn.

The library owns no journal and does not retry a failure or a refusal.

ReferralQueue

Optional, and needed only if you enable cross-desk referral.

pub trait ReferralQueue: Send + Sync {
    fn enqueue_once(&self, referral: Referral) -> ReferralFuture<'_>;
}

Everything MentionTurnQueue requires, plus one thing that follows from the only difference: the child turn may run on a different conversation from the one that triggered it. A Referral therefore names two — from, where the trigger was committed, and to, where the child turn runs — and the idempotency key is referral.from plus referral.key.trigger_sequence.

Revalidate authorization and target availability on both conversations. A crossing referral writes into a channel its author is not a member of, so a transaction that authorizes only the source desk has authorized nothing.

One more obligation is yours and has no equivalent in the other ports: when you run a child turn that carried a ReferralOrigin, pass that origin back in the next ReferralInput, or the answer has no way home. The library does not remember it.

What you own that the library will not do for you

Prompting. The trace grammar is a parser, not a teacher. Live runs showed that models split a topic across two names, restate their own last line, !commit during deliberation, and drop the # sigil. Every one of those is fixed in the prompt, and each fix is a host obligation. See Trace grammar for the four and what to do about them, and the taxonomy of multi-agent failures for how ordinary they are.

Two more, from the federated live runs, if you enable referral. A move that is not in the list of moves is not offered: two runs and fifty turns produced zero cross-channel messages while @#deskid was explained above the marker list rather than placed in it. And an answering turn needs the answerer's own private facts — without them the only thing a desk can send across a channel is its opinion, and in one run a desk exported its wrong hypothesis to the one desk that had been reasoning correctly.

Committing state. EpisodeState and SharingState are returned, never applied. Commit after the turn is durably appended, so a crash leaves the watermark behind rather than ahead.

Policy. Hop limits, quorum thresholds, turn budgets, and whether mention dispatch is on at all are yours. The library ships conservative defaults and adds no hidden ceiling of its own.

Breadth, where depth is not enough. max_hops bounds how deep a referral chain goes. Nothing bounds how many channels one desk may ask, because only you know what a question costs you. The benchmark's host caps it at one question per peer desk, and that cap is the host's, not the library's.

Timing, when a room can decide before an answer lands. A desk whose members share a blind spot reaches quorum inside its own blind opening round, so a cross-channel answer arriving after that is information the desk has already voted past. Whether your agents ask before or after backing something is the single largest effect the federated benchmark measured, and it is entirely yours. See Cross-desk referral.

A turn, end to end

  1. A message lands. You journal it.
  2. resolve reads its mentions against the current roster and desk set.
  3. responder_plan returns a decision, or a selector request with a deterministic fallback.
  4. If it returned a request, call your Selector once and run the output through accept_selection.
  5. prepare_delta tells you what the chosen agent has not yet seen, or asks for a full re-initialization with a named reason.
  6. The agent takes its turn. You append the reply.
  7. Commit the sharing state.
  8. If mention dispatch is enabled, dispatch_mention decides whether that reply may enqueue exactly one more turn, and calls your queue at most once.

For a deliberating room, steps 3 and 5 are replaced by a single step call that returns the authorized turn along with the state to commit at step 7. See Hive episodes.

Pointer bumps are the release

Every crate is publish = false. You pin the repository as a submodule and take the crates as path dependencies, so the pinned commit is the version.

Work lands here first, then the pointer bump lands in your repository. A submodule cannot point at an unmerged commit, which is what makes the dependency direction self-enforcing.

Clone this wiki locally