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 three ports, and two 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.

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.

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.

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