Skip to content

Responder ladder

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

Responder ladder

A message arrives. Exactly one agent has to answer it, or none does. The ladder is how that is decided, and every rung of it is deterministic except one, which is optional and has a deterministic fallback.

responder_plan returns either a decision or a bounded selector request. It never dispatches a turn. The pure half lives in tinyhivemind-core/src/responder/, the waiting half in tinyhivemind/src/responder/.

The rungs

rung when
ExplicitMention the body directly mentions an active agent
DeskDefault the message is on a desk, and the desk resolves to its first effective active member
AutoSelection the desk is in Auto mode with two or more effective members, so a selector is asked
DirectAgent the chat identity is a direct message or a bare agent chat
Orchestrator nothing above matched, so the host's fallback answers

Reading order is the tiebreak everywhere it applies. direct_responder takes the first non-quiet mention of an active agent, and a later mention is never used as a fallback if the first one turns out to be unusable.

The selector rung

ResponderMode::Auto is the only place a model gets a say, and the shape of that say is narrow on purpose. The library builds a SelectionRequest holding the message, the desk id, and the candidate list. The host implements one port:

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

Whatever comes back goes through accept_selection, which accepts the output only if it names exactly one candidate. Matching is ASCII case insensitive and returns the candidate's canonical id. It tolerates one trailing period and one matching quote or backtick wrapper, because models add those, and nothing else.

Two candidates matching is a rejection, not a coin flip.

Every outcome at that boundary is named in SelectionDisposition: NotApplicable, Selected, Disabled, Unavailable, and the case where the output did not name exactly one candidate. Each of the failure cases falls back to the deterministic first effective member, so a selector that is down, slow, or wrong degrades to Lead behaviour rather than to no answer.

SelectionPolicy::Disabled skips the rung entirely without changing which agent ends up answering.

Mention dispatch

The one place an agent's own reply can start another turn, and the place where a hive mind would ordinarily turn into an unbounded cascade.

mention_dispatch looks at a committed agent reply and decides whether it may enqueue exactly one mentioned agent. Evaluation is fail closed. Once the first reading-order, non-quiet direct agent mention is found, a self-mention or an inactive target stops the decision there. It does not go looking for a second candidate.

Refusals are typed rather than silent: Disabled, HopLimitReached, SourceInactive, and the target-side reasons.

The hop limit is host supplied, finite, and configurable. The library adds no hard ceiling of its own, and the whole feature is off unless host policy explicitly enables it. OpenCompany runs it at two hops.

The queue boundary

The runtime crate carries the edge from that pure decision to a host-owned atomic queue:

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

A host implementation must do one atomic transaction keyed by the conversation plus the trigger sequence. Inside it, re-read the stored reply and verify its source, content, conversation and sequence; revalidate live policy, authorization and target availability; and durably enqueue at most one child turn. A duplicate returns Already. An expected live rejection returns Refused. A rollback must leave neither an idempotency record nor a child turn.

This port is the only idempotency boundary in the system. tinyhivemind owns no journal, and it does not retry a failure or a refusal. A no-dispatch decision calls the queue zero times; a one-target decision calls it exactly once.

Clone this wiki locally