Documentation

From API key to a remembering agent  //  quickstart ~15 minutes

Hivemind gives your agents persistent shared memory and a working-context compiler: store what matters, recall it semantically, and assemble a token-budgeted, fully audited prompt for every turn. Your model, your key — Hivemind never calls an LLM.

Quickstart

1. Get your key

  1. Sign in and subscribe (14-day trial, card up front, nothing billed today).
  2. Under Projects, open your project and connect your Qdrant cluster — its URL and API key. Your memory lives there, not with us. Any reachable Qdrant works: self-host the open-source server on your own infrastructure, or use Qdrant Cloud — its free tier is enough to start.
  3. In the same panel, press New key, name it, and choose what it reaches — All projects, or a role, which reaches the projects that list it — and what it may delete. A new key can read and write but not delete until you allow it. It is shown once — store it as HIVEMIND_API_KEY.
  4. Copy the id of the project to work in, shown beside the project's name (proj_…), and store it as HIVEMIND_PROJECT.

2. Integrate — any language

Hivemind is a plain HTTPS API: the whole loop is two JSON calls per exchange, from any language. The wire protocol below is the complete integration contract, with the exchange translated into curl, JavaScript, Python, and Go.

3. Prove the connection

A new workspace starts empty — nothing enters your cluster except what you and your agents store, the collections the service creates on first request, and the operation receipts it keeps there for you. Prove the connection with one store and one recall:

curl -s https://api.hivemind.militant.ai/hivemind/v1/create_memory \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" \
  -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hivemind connected: this workspace remembers from here on.", "metadata": {"topic": "setup"}}'

curl -s https://api.hivemind.militant.ai/hivemind/v1/recall_memory \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" \
  -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{"query": "when did this workspace start remembering?", "top_k": 5}'

Point your agents at this documentation for how the system works — the loop, what to store, and the full reference are written to be read by them as much as by you.

The loop: three lines inside your agent

packet = POST /turn { user_input, session, budget, pair-id }            # store → recall → compile
reply  = your model, run on packet.bundle.messages                       # your model, your key
POST /conversation/store { reply, same pair-id }                         # completes the exchange

Working code for that loop, in four languages, is in the wire protocol.

One /turn is one request: the service stores the user message (receipted), semantically recalls relevant memories and past conversation, reads back the session’s recent conversation from what it stored, and compiles it with the recalled records and active holds into a token-budgeted bundle. Your host sends only the new message. The response’s bundle.messages is a contiguous, internally-ordered block: use it as the entire prompt, or surround it — your system prompt above, turn-local instructions below; the packet keeps its place either way. Never insert into or reorder inside the block. The store call files the reply and completes the exchange: the whole loop is two calls, with your model between them.

Conversation is remembered as call/response exchanges. Recall matches your query against both sides of every past exchange and returns whole exchanges — one result slot per exchange, rendered call-then-response, never an answer without the message that produced it. Facts that appear only in a reply are just as findable as the calls that prompted them. Durable facts that should stand alone — decisions, outcomes, lessons — belong in deliberate memory.

The wire protocol — any language

The service is language-agnostic. One exchange is two HTTPS calls, with your model between them; any client library is a convenience layer over exactly this.

Call 1 (/turn) stores the user message, recalls, and returns the compiled prompt packet — the response's data.bundle.messages is the prompt block for your model, data.receipt the audit record. Call 2 (/conversation/store) records the reply, completing the exchange with the same exchange_pair_id and turn_number. The whole exchange, translated:

# 1. compile the turn
curl -s https://api.hivemind.militant.ai/hivemind/v1/turn \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" \
  -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{
    "user_input": "Can we ship the migration tonight?",
    "session_id": "session-1",
    "turn_number": 7,
    "budget_total": 8192,
    "exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa"
  }'

# ... run your model on data.bundle.messages ...

# 2. record the reply (same exchange_pair_id + turn_number)
curl -s https://api.hivemind.militant.ai/hivemind/v1/conversation/store \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" \
  -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Yes — window is clear until 03:00 UTC.",
    "session_id": "session-1",
    "turn_number": 7,
    "role": "assistant",
    "exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa"
  }'

The protocol's rules — every client must honour these:

  • Pairing. Generate a fresh exchange_pair_id per turn; the record call must reuse it and the turn number, so the exchange is stored as one call/response pair. Any collision-resistant unique string works — the project's convention is a time-ordered UUIDv7.
  • Aborted generation: record nothing. If the user cancels mid-stream, make no call — the stored user message stands as an unanswered call, and the next message is a new turn. Never record a partial reply as if it was said, and never send empty content.
  • Never retry a timed-out turn. A timeout does not mean the request failed — re-sending can store the message twice. Retry only requests that failed before any response began.
  • Honour Retry-After. A 429 carries it; back off that many seconds and retry. That is the service asking your client to slow down, not an error.
  • Budget ceiling. budget_total at most 128,000; a 400 names the limit.
  • Name the project. Every request carries X-Hivemind-Project; without it the request is refused with 400.

Every other endpoint speaks the same JSON envelope with the same auth header. The full API reference documents every endpoint — request fields, response shapes, errors — with working examples in all four languages, plus the complete client implementation guide.

The working context — representative output

This is what one /turn (or /context/compile) hands back, rendered: the trace, the admitted items, the accounting, and the compiled messages your model receives. Toggle the response JSON to see the exact wire shape.

WORKING CONTEXT BUNDLE / CONTEXT.COMPILE RECEIPT e9b3c1a0

TRACE

Session ID
session_001
Turn Number
14
Built At
2026-08-30T12:11:37Z
Event ID
event_001
Trace ID
trace_001
User Input
Find the lowest-risk path to stabilise checkout latency before peak traffic.
Context Messages
19

ITEMS (KIND · SOURCE, 5 OF 19)

  • DIRECTIVE · DIRECTIVEdirective-0

    Prefer reversible mitigations and preserve checkout stability during active traffic.

  • MEMORY · HIVEMIND9d41c2f0…

    Checkout latency spiked during the 2026-06-21 payment provider failover; regional failback plus cache warmup restored p95 within 11 minutes.

  • MESSAGE · RECALLED_CONVERSATIONxp-310a

    No - the provider failover in June showed peak-hours changes cost us 11 minutes of degraded p95. Ship in the low-traffic window.

  • HOLD · SYSTEMhold-8c2f

    Checkout change freeze: avoid changes that increase regression risk during active checkout traffic.

  • MESSAGE · CONVERSATIONactive input

    Find the lowest-risk path to stabilise checkout latency before peak traffic.

+ the remaining 14 messages of this bundle, section markers included

ACCOUNTING (API DATA)

  • POLICYbalanced

    Balanced admission inside max_context_tokens with per-class caps on memory, tool, and hold items.

  • COUNTSsources

    Admitted items counted by source: conversation=5, directive=1, hivemind=4, recalled_conversation=2, system=3. Section markers ride along uncounted.

  • BUDGETeconomy

    token_count 481, budget_available 3615, conversation_tokens 133, hold_tokens 66 — reported on every compile.

  • TRACEsnapshot

    Compiler diagnostics remain inspectable alongside the assembled bundle.

MESSAGES (COMPILED 19)

  • SYSTEMsection marker

    === AGENT FIXED CONTEXT BUFFER ===

  • SYSTEMdirective

    Prefer reversible mitigations and preserve checkout stability during active traffic.

  • SYSTEMstatus line

    Context: 433/4096 tokens | 1 holds active (66 tokens) | conversation: 133 tokens | sources: conversation=5, directive=1, hivemind=4, recalled_conversation=2, system=2 | available: 3663 tokens

+ the remaining 16 messages: artifacts, hold, both recall buffers, and the active conversation

TOKEN COUNT: 481 BUDGET AVAILABLE: 3615 CONVERSATION TOKENS: 133 HOLD TOKENS: 66 SOURCES: conversation 5 / hivemind 4 / system 3 / recalled_conversation 2 / directive 1

Deliberate memory

# store a durable fact with your metadata
curl -s .../hivemind/v1/create_memory -H "$AUTH" -H "$JSON" -d '{
  "content": "The staging database is rebuilt every Monday 03:00 UTC; do not schedule migrations near that window.",
  "metadata": {"project": "atlas", "topic": "ops"}
}'

# recall it, semantically or filtered
curl -s .../hivemind/v1/recall_memory -H "$AUTH" -H "$JSON" \
  -d '{"query": "when is it safe to run a staging migration?", "top_k": 5}'
curl -s .../hivemind/v1/recall_with_metadata -H "$AUTH" -H "$JSON" \
  -d '{"query": "database", "metadata": {"project": "atlas"}, "top_k": 10}'

Store decisions and their rationale, lessons learned, durable facts, and outcomes. Attach consistent metadata keys — they are what make filtered recall and surgical, audited deletion possible later. /recall_memory searches deliberate memories only: conversation history is a separate record class, recalled automatically inside the loop (or explicitly via /conversation/recall).

The audit trail

curl -s ".../hivemind/v1/receipts?session_id=session-1&limit=50" -H "$AUTH"

Every operation — store, recall, compile, delete — emits a receipt: what ran, what it consumed (inputs_ref), and what it produced (outputs_ref), persisted to your cluster and queryable by session, turn, or operation. parent_receipt_id is on every receipt and reads null.

The context window

The compiled packet is not an opaque blob — it has a fixed anatomy, and knowing it is how you place every kind of content deliberately. A real compile looks like this (roles on the left):

system     === AGENT FIXED CONTEXT BUFFER ===
system     [your operator_briefing — persona, rules, mode flags]
system     Context: 3210/8192 tokens | 2 holds active (350 tokens) | ...
system     === HOLD === id: stop-order · title · tokens · held_since ...
system     [artifacts_context — "Available files:" when supplied]
system     === SEMANTIC RECALL BUFFER ===
system     [recalled deliberate memories, relevance-ranked]
system     === SEARCH RECALL BUFFER ===
user       [recalled exchange: the call]
assistant  [recalled exchange: the response]
system     === ACTIVE CONVERSATION BUFFER ===
user       [recent conversation, reading order]
assistant  ...
user       [the current message — always last]

Two things to read off that structure:

  • The fixed context buffer is the boundary your integration works within. Everything a developer would normally wrap around a prompt has a named slot inside the packet instead — and content inside is budget-managed, admission-decided, counted in the status line, and receipted; content wrapped outside is invisible to all accounting.
    External contentIts slot
    System prompt / persona / rulesoperator_briefing — protected system directive, re-sent per turn, never stored
    User profiling, standing operational stateholds — re-sent by you each turn until dropped; labeled, token-visible
    Documents, file excerpts, uploadsartifacts_context — this compile only
    Mode flags, turn-local steeringoperator_briefing per-turn override
    Tool definitionsnot context — host configuration; never sent, never stored (see the chat-host section for how tool results enter)
  • The packet is self-labeling and position-independent. Its section markers announce its regions; nothing inside references absolute position. Stack your own messages above or below and every internal relationship holds — Hivemind can be the entire window or a region within one.

API surface

Every call. All responses arrive as {"ok": true, "data": ..., "request_id": ...} envelopes; the envelope carries them.

EndpointWhat it does
POST /turnThe composed exchange: store the call, inline recall (both searches), compile
POST /conversation/storeThe reply half of the exchange, paired by exchange_pair_id
POST /create_memoryDeliberate durable memory with your metadata
POST /recall_memorySemantic recall over deliberate memories only
POST /recall_with_metadataSemantic recall scoped by exact metadata matches
POST /conversation/recallManual episode recall: session_id=None searches all sessions (one slot per exchange, each returned whole: the half that matched plus its other half); a session id replays that session (both halves, newest-first, up to top_k)
POST /conversation/forkStarts a new session as a fork of an existing one, cut now: the new session's turns and replay carry the parent's conversation up to the fork, then its own. See Forking a conversation
POST /context/compileRaw compile for custom loops built around your own recall
POST /purge_deleted_memoriesPermanently removes soft-deleted records older than older_than_days (default 30) — deletion becomes final here, not at delete time
POST /purge_memoriesHard-deletes all memory records immediately — the destructive full reset; always receipted
GET /receiptsAudit trail, newest-first; filter by session_id / operation
GET /memory/{id}Exact lookup of one record by id, workspace-scoped; 404 when unknown, foreign, or soft-deleted
GET /receipts/{id}Exact lookup of one receipt by receipt_id, workspace-scoped
DELETE /memory/{id}Soft-deletes exactly one record by id — what GET /memory/{id} would fetch: a long record goes whole, the other half of an exchange stays, receipts are never deletable. 404 when unknown, foreign, or already deleted. Out of recall immediately, recoverable until purged, receipted
GET /usageStorage, point counts, and the limits your subscription carries
GET /exportEvery record you own, paged. Portability by contract
POST /delete_by_metadataSoft-deletes everything matching all given keys. Your key is the authorization (included with your subscription). Out of recall immediately, recoverable until purged, always receipted. The response says what was removed — deleted_records, deleted_exchanges and deleted_points (stored rows) — and a filter that matches nothing answers with zeros. Mind filter breadth — what your agents may delete is your harness's policy

The turn, in detail

Everything turn() accepts, and what each input does:

ParameterBehavior
user_inputThe initiating message. Stored as the call half of a new exchange, used as the query for both recall searches, and always the packet's last message. Must be non-empty
budget_totalThe compile's token budget — the single number that governs the whole window. Set per session or per turn; if you wrap the packet, size it as your model window minus your wrapper. Maximum 128,000 — a larger value is rejected with 400 VALIDATION_ERROR whose details.max_budget_total names the limit. The budget bounds what is surfaced per turn, not what is remembered: memory is unbounded
messagesOptional. Only what the service does not store and later compiles should see: tool results, as {role, content, timestamp} dicts with real unix timestamps, or as interface events (see Tool use in Building a chat host). Never the conversation itself: the service reads the session's recent conversation back from what it stored, and adds user_input as the last message
operator_briefingYour system layer — compiled as a protected directive at the top of the packet, re-sent every turn, never stored, never recalled
artifacts_contextOne-compile content (document excerpts, file listings). Rendered in the fixed buffer this turn only; never stored
holdsPinned state riding every compile until you clear it — see the chat-host section for the lifetime contract
authorWho produced this call — identity metadata, stored on the record and surfaced as the message's name
recall_top_kOmit for a budget-scaled candidate pool (the default and usually right); set to force a fixed pool, gated by your subscription's ceiling
policyAdvanced: a compile-policy override (item caps, include switches) for this turn — most integrations never need it

And everything it returns:

FieldContents
bundle.messagesThe compiled packet — the block you send to your model. Each message is id, role, content, tool_call_id and name. id is the record's id for everything stored — your conversation, recalled exchanges, memories, and the message this turn stored — a uuid7, which carries the time the record was stored; what you sent keeps the ids you gave it; the service's own text carries its labels
bundle.token_count / budget_totalThe packet's measured size against the budget — the context meter
receiptThe compile's audit record; receipt_id links it in the receipts trail
storedThe stored call half's {id, receipt_id} — an empty object if the store degraded (see warnings)
recalled_memories / recalled_conversationHow many records each recall search contributed to the compile
session_historyHow many stored messages of this session were read back as the recent conversation: newest first, as many as budget_total could hold
warningsDegradations that didn't fail the turn — a failed store or recall search reports here and the compile proceeds without it
timingsServer-side phase breakdown, seconds: embed_s (preparing your input for the recall searches), store_and_recall_s (storing your message, both recall searches and reading back the session's recent conversation), completion_s (fetching the other half of recalled exchanges), compile_s (building the packet), receipts_s (writing the turn's receipts), total_s (the whole call). A slow turn names its own bottleneck

The client-side state these calls assume (the open exchange and holds) is specified in Building a chat host.

Conventional environment names

Nothing requires these, but examples and tooling use them:

Env varMeaning
HIVEMIND_BASE_URLService root — the host only (https://api.hivemind.militant.ai); clients append /hivemind/v1
HIVEMIND_API_KEYSent as Authorization: Bearer <key>; identifies and tenant-binds in one step
HIVEMIND_PROJECTRequired: the project to work in, sent as X-Hivemind-Project on every request
HIVEMIND_TIMEOUTClient request timeout in seconds — compiles at large budgets are multi-second operations; 120 is a reasonable default
HIVEMIND_BUDGET_TOTALDefault compile budget when your host does not set one per session

Memory & metadata

Every stored record carries a record class, stamped at write time, and the recall paths are class-scoped — with one deliberate exception:

ClassWhatRecalled by
semanticDeliberate memories (/create_memory)/recall_memory and the loop's semantic recall — relevance-ranked, this class only. /recall_with_metadata is the exception: it searches every embedded record matching your filter, semantic and temporal alike — scope it with recall_mode when you want one class
temporalConversation exchangesThe loop's conversation recall and /conversation/recall
diagnosticReceipts, document referencesNever semantically recalled; scrolled by explicit query only

Metadata is the control surface. Two kinds of keys live on every record. Service-stamped keys are on every record at write time — some copied from your request fields (session_id, turn_number, role, exchange_pair_id), the rest generated by the service. Filter on them freely, but don't overwrite them:

KeyMeaningWhy you care
typeWhat kind of record this is (conversation_exchange, hivemind_receipt, your own values via metadata)The broadest filter; conversation records are identified by it
session_id / turn_numberWhich conversation, which turnSession replay, receipts filtering, chronological ordering of recalled exchanges
rolePositional: which side of the exchange — user initiated (the call), assistant answered — not who was talkingDrives exchange pairing and rendering; identity lives in author, never here
exchange_pair_idThe identity of one exchange — both halves share it, nothing else ever doesDrives one-slot-per-exchange dedupe and pair completion in recall
recall_modeThe record class: semantic, temporal, or diagnosticDecides which recall path can ever return this record
storage_modeinline or chunked; document references carry referentialLong content comes back whole on recall
timestampUnix time at storageOrders conversation into reading order; drives newest-first returns
recall_countHow many times recall has returned this record — incremented automatically after every recall
semantic_relativity, resonance_scoreQuery-time relevance: the engine's similarity for this query, and the final ranking scoreRead them off recall results to understand why something ranked where it did
deleted_atSoft-deletion stampPresent = out of all recall, recoverable until purged

Caller-set keys are yours, supplied via metadata on /create_memory or additional_metadata on stores. Every one becomes an exact-match filter in /recall_with_metadata and a deletion selector in /delete_by_metadata — consistent keys are what make surgical operations possible later. Two semantics to respect: service-stamped keys in your metadata are dropped on write (the record is re-stamped), and type may not use the service's own record types (hivemind_receipt, conversation_exchange — a 400 names the field); and list-valued keys match any element, not the exact list — {"tags": ["contract"]} selects every record carrying that tag, which widens a deletion filter accordingly. The ones with built-in behavior:

KeyBuilt-in behavior
authorWho produced the message — surfaces as the chat message's name field in compiles and recalls; pass author on /turn and additional_metadata.author on the store
written_by_keySet by the service on every record: the id (never the secret) of the API key that wrote it. You cannot set or change it — a value you send is discarded — so unlike author, which is whatever the caller says, it is a fact about who wrote what. It filters like any key, so /recall_with_metadata and /delete_by_metadata can target one key's records
tagsA list; tags filter like any key
document_idTies records to a document; it scopes /recall_with_metadata for per-document recall
anything elseNo built-in behavior — pure filter/delete selector. project, customer_id, agent_run: whatever your operations need to slice by

Long content stays reachable at any depth; recall returns the whole record.

# write with your own keys — every key is a future filter and delete selector
curl -s -X POST "$BASE/create_memory" \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Acme renewal decided: 12 months, $48k, signed 2026-08-12.",
    "metadata": {"project": "acme", "tags": ["contract", "renewal"],
                 "document_id": "acme-msa-2026", "author": "ops-agent-2"}
  }'

# later — recall scoped by those keys...
curl -s -X POST "$BASE/recall_with_metadata" \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{"query": "renewal terms", "metadata": {"document_id": "acme-msa-2026"}, "top_k": 5}'

# ...and surgical deletion by the same keys
curl -s -X POST "$BASE/delete_by_metadata" \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{"metadata": {"project": "acme", "tags": ["contract"]}}'

Recall & ranking

Two recall paths, never conflated: inline — automatic inside turn(), on the raw user input, no query authored by anyone — and manual — the explicit endpoints with deliberate queries and explicit top_k. The inline path runs two searches: one over deliberate memories (the semantic record class) and one over conversation exchanges (the temporal record class).

The exchange is the recall unit

Ranked results are deduplicated by exchange_pair_id, so top-5 recall means five different past exchanges, never the same one twice. Dedupe is by identity, not similarity: three near-identical exchanges from three sessions have three pair ids and all three can rank. Every recalled exchange then renders whole. Session-scoped replay (/conversation/recall with a session_id) is exempt from pair-dedupe and ranking: the session's rows return newest-first by the time each was stored, whole, up to top_k — replaying a long span means asking with a top_k sized to it, within your subscription's ceiling.

# manual recall — each result carries its own ranking trace in metadata
curl -s -X POST "$BASE/recall_memory" \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{"query": "what did we decide about retry behavior?", "top_k": 5}'

# one returned record, abridged — why it ranked where it did:
# { "content": "Retries: exponential backoff, cap 3 attempts, decided 2026-07-30.",
#   "metadata": {
#     "semantic_relativity": 0.83,   # raw similarity to this query
#     "resonance_score":    0.91,   # final ranking score
#     "recall_count":       7,      # times recall has returned it
#     "tags": ["retries", "policy"], "exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa" } }

Budget & admission

budget_total is the authority: the compiler admits content until the budget is spent.

What fills the window, in what order

Protected core admits first, unconditionally: your operator_briefing and the current user message — the last user-role entry in messages, so always send the message you want protected — are in the packet whatever the budget. Everything else competes through balanced admission across sources — each candidate item belongs to one source, and sources are weighted:

SourceWhat fills itPriority
conversationThe session's recent conversation, read back from what it stored, plus anything you send in messages; newest prioritizedHighest — the active conversation has the highest priority in the window
semanticRecalled deliberate memoriesSecond — durable knowledge relevant to this turn
searchRecalled past exchanges, in reading orderThird — episodic context from other sessions and older turns
toolTool-output items: tool.call.finished events when messages is sent as interface events (plain role: "tool" messages count as conversation in the packet but compete in the fixed lane — see the chat-host section)Fourth — current-turn scaffolding
fixedHolds, artifacts_context, section markers, the status lineAdmitted with the fixed buffer; holds are capped only by max_hold_items if you set one

Weights bias, they don't ration: an empty source simply doesn't compete. With no recall to place, conversation takes all the space; a short conversation leaves the window to recall. Nothing is reserved for a source that has no candidates.

The meter

The status line reports the economy inside the packet every turn — Context: 3210/8192 tokens | 2 holds active (350 tokens) | ... — where the model can read it and act on it. result.token_count / budget_total report the same numbers to your code, and they are the authoritative source for any context meter: counting your own messages undercounts (it knows nothing about recall), and your LLM provider's usage measures a different tokenizer after your own additions.

# the budget dials on /turn (budget_total is required; recall_top_k is /turn-only)
{ "budget_total": 8192, "recall_top_k": 12 }

# what comes back, and what the model reads — the same numbers:
#   result.token_count = 3210, result.budget_available = 4982
#   status line inside the packet:
#   "Context: 3210/8192 tokens | 0 holds active (0 tokens) | conversation: 310 tokens
#    | sources: conversation=3, hivemind=4, system=3 | available: 4982 tokens"

A policy is an overlay: set only the fields you mean to change — everything you omit keeps its budget-derived value (max_context_tokens from budget_total, balanced admission, budget-scaled caps):

# cap memories at 6 and holds at 2; the 8192 window and balanced
# admission carry over from budget_total automatically
{ "budget_total": 8192,
  "policy": { "max_memory_items": 6, "max_hold_items": 2 } }

Building a chat host

The two-call loop is the whole integration contract — but a long-lived, restartable, streaming host needs a layer around it that the loop alone doesn't provide. This is that layer, stated at the wire level: your client keeps a little state, and these are its rules.

  • Your client's state is two things: a turn counter (increment per turn) and the open exchange (its exchange_pair_id and turn number, held between the /turn call and its /conversation/store). No conversation history: the service stores every message and reads the session's recent conversation back on every /turn. Send only the new message as user_input; messages is for tool results only (see Tool use below).
  • Stream first, compile second. /turn does real work — a store, semantic recall, and a full context compile — and at large budgets that takes seconds, not milliseconds. If you call it before sending anything, streaming clients receive no bytes and many treat the connection as failed. Establish the stream first (headers or a keep-alive frame), then compile, then stream your model.
  • Restarts: nothing to rebuild — the conversation lives in the service. Map your conversation id to session_id so a restarted host lands in the same session; when a user clears a thread, start a new session id so the fresh thread doesn't inherit what they asked to be rid of.
  • Aborted generation. A dropped stream is the same as a cancel: discard the open exchange and move on. The one legitimate partial case is a host that deliberately keeps what was shown on screen as what was said, decided after the stream ends.
  • Continuation hops: role marks position in the exchange — which side initiated, which side answered — not human vs. machine. So deliberately feeding the model's own final output back in as the next /turn (inner monologue, staged reasoning, "keep going") is correct use: the prior output is the initiating utterance of the next remembered exchange, and recall will later find the exchange by it. Mid-turn tool cycles are NOT continuation hops — they stay inside one exchange (see Tool use below).
  • Tool use — the lifecycle first, authoritatively: one user exchange is ONE /turn, however many tool cycles happen inside it:
    /turn opens the exchange (stores the user input, returns the packet)
      → your model requests a tool
      → your host executes it
      → your host continues the model with the result   # no Hivemind call
      → repeat, bounded
    /conversation/store records ONLY the final assistant reply
      — same exchange_pair_id and turn_number the /turn used
    Never route a tool cycle through another /turn: every /turn stores its user_input as a new exchange, so mid-loop calls write tool chatter into memory. Hivemind never calls your model and never sees tool definitions — how your host invokes tools is your host's business. Tool results are not stored by the service, so a later compile sees one only while you keep sending it in messages. They enter the compile two ways. The simple way: plain role: "tool" entries, carrying identity in metadata — they count as conversation in the packet and render with tool_call_id and name set:
    # a tool result as a plain message — conversation-classified
    "messages": [
      {"role": "tool",      "content": "billing_lookup: $4,120.50 across 3 invoices",
       "timestamp": 1756500004,
       "metadata": {"tool_call_id": "call_7f3a", "tool_name": "billing_lookup"}}
    ]
    The structured way: send messages as interface events (every entry {type, payload}; the list is one shape or the other, never mixed). tool.call.finished events become items in the dedicated tool admission source, governed by max_tool_items and include_tools, with success and failure recorded on the item:
    # a tool result as an interface event — tool-source classified, policy-capped
    "messages": [
      {"type": "tool.call.finished", "id": "call_7f3a", "ts": 1756500004,
       "payload": {"call_id": "call_7f3a", "ok": true,
                   "result": "billing_lookup: $4,120.50 across 3 invoices"}}
    ]
    Either way, don't /conversation/store tool chatter — the exchange worth remembering is the user's input (stored by /turn) and the assistant's final reply, so recall returns conclusions, not scaffolding.
  • Authorship: who produced a message is a separate question from which side it sits on: pass author on /turn and additional_metadata.author on the store — identity as ordinary metadata, surfaced as the chat message's name field in compiles and recalls. Multi-agent transcripts stay attributed, and it filters: /recall_with_metadata with {"author": ...}.
  • Turn-local instructions: where content enters decides how long it lives. artifacts_context is included in one compile and never stored. operator_briefing is also compiled-only, but re-send it every turn — standing instructions and mode flags. holds is a list of {id, title, content} objects (optional: tokens, held_since, priority — echoed into the rendered hold block as labels; the accounting's hold_tokens is measured by the compiler regardless) that you keep client-side and pass on every /turn until you drop them — pinned operational state ("stop-order", "API is down") that never enters memory: not stored, not recalled — visible only in the packet (and by item id in the compile receipt's outputs_ref). Durable facts go to /create_memory — forever, recallable from any session. Pick the shortest lifetime that does the job; anything longer persists into future context where it no longer belongs.
  • Control tokens: Hivemind stores and returns text verbatim — but recalled text re-enters your model's context later, and that model's tokenizer may remove its own special tokens or interpret them as control sequences. Strip or alter markers like <|...|> before storing.
  • Failure severities differ. A failed /conversation/store means the chat already succeeded — never surface it to the user. What to do next depends on the answer: a 5xx, or a failure before any response began, is retried out-of-band with a backoff that ends; a 4xx other than 429 is the service refusing that request and will refuse it again unchanged — fix it or set it aside, and never let it hold up the stores queued behind it. A timed-out store is delivery-uncertain: the write may have landed, and the service does not dedupe a re-sent reply half — verify first: replay the session with /conversation/recall and look for the reply under its exchange_pair_id, then re-send only if it is not there.
  • Agent self-service tools: beyond the automatic recall inside /turn, you can hand the agent deliberate memory operations as ordinary host-executed tools — the host maps tool names to endpoints, executes the HTTP call, and returns the response as a tool result. The results ride your messages into the next model call like any other tool output; nothing recalled this way is written back to memory — recall reads, only /create_memory and the exchange stores write. Authorization is the host's: expose read tools freely, gate write and delete tools per agent (the service additionally plan-gates destructive endpoints server-side). Artifacts and holds are client-managed levers the agent can ask for: a "pin this" tool just adds to the holds list your host sends each turn.
    # host-side tool table — each endpoint tool is one HTTP call
    search_memory      -> POST /recall_memory        {"query", "top_k"}               read
    search_by_metadata -> POST /recall_with_metadata {"query", "metadata", "top_k"}   read
    search_episodes    -> POST /conversation/recall  {"query", "top_k"}               read — cross-session
    replay_session     -> POST /conversation/recall  {"query", "session_id", "top_k"} read — one session, newest-first
    remember           -> POST /create_memory        {"content", "metadata"}          write — gate it
    
    # client-side tools: no HTTP call — the host mutates its own per-turn state
    pin_context        -> add a hold to the host's holds list
    release_context    -> remove a hold by id (the rendered hold block tells the
                          model exactly this: To release: context.release("id"))
    attach_artifact    -> set artifacts_context for the NEXT compile only
    schedule/list/cancel -> the host's own scheduler, never a Hivemind call
    
    # one cycle: the agent searches its own memory mid-turn
    model → tool_call search_memory {"query": "retry policy decisions", "top_k": 5}
    host  → POST /recall_memory → returns records
    host  → continues model with the records as the tool result
    # the records are context for THIS turn only — not re-stored, not a new exchange
    Three destinations, never confused: the same-turn tool result lives in your model provider's continuation transcript; what later compiles see is the stored conversation plus the tool results you keep sending in messages (plain or as interface events); and artifacts_context reaches the next compile only — nothing changes a packet that has already been returned. Scheduled activation is host-owned — Hivemind has no scheduler. A cron event wakes your agent by opening an ordinary attributed exchange: /turn with the trigger as user_input and author naming the scheduler ("cron"), stored and recallable like any other exchange.

Forking a conversation

To clone an agent so the clone continues from the same context, fork the agent's session into a new one and run the clone on the new session id. POST /conversation/fork with the session to fork and the new session id:

curl -s -X POST "$BASE/conversation/fork" \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" \
  -H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
  -H "Content-Type: application/json" \
  -d '{"session_id": "planner-main", "new_session_id": "planner-clone-2", "author": "planner"}'

From that moment the new session's conversation is the parent's conversation as it stood at the fork, followed by whatever the new session stores. The clone's first /turn sends only its new message, like any turn: the packet carries the parent's recent conversation, and /conversation/recall with the new session id replays it the same way.

Both sessions are in the same project, and the recall inside /turn searches all of its sessions, the parent's and the clone's included. Deleting the new session (/delete_by_metadata with {"session_id": ...}) removes its marker record with its messages.

Fields and the response: POST /conversation/fork.

Projects and storage

Your memory lives in a Qdrant cluster you own — we never hold your data. After subscribing, connect a cluster from the dashboard: paste its URL and API key, we validate it live (reachable, key accepted); the collection is created automatically on your first request, and requests start working within minutes. Until a cluster is connected, every request answers 400 with "No storage connected". Any Qdrant instance the service can reach works: Qdrant is open source, so you can self-host it on your own server or VPS and pay nobody for storage, or use Qdrant Cloud if you'd rather not operate it — its free tier is enough to start. Every turn round-trips to your cluster, so distance between the service and your cluster is latency, and your cluster's capacity sets your throughput. If your cluster is unreachable, requests fail closed rather than writing anywhere else.

A workspace holds multiple projects, each bound to one cluster (the same cluster or different ones). Projects are hard data separation: each resolves to its own storage, so records in one project are never candidates for recall in another — separate agents, environments, or customers of yours can share a workspace without sharing memory. Credentials can be rotated and clusters moved per project at any time from the dashboard.

Which projects a key reaches is set by roles, the same roles your workspace's members hold. Each project lists the roles that reach it, under Projects in the dashboard. A key is given All projects, or one role and reaches every project that lists that role: list the role on another project, or take it off one, and the key follows. A key cannot be given the admin role; give a key that needs every project All projects. Whichever it is, each request names one project in the header, and a request naming a project the key does not reach is refused with 403. No request ever spans two projects. A key that reaches no project — its role is listed on none — is refused with 403 on every request. A change takes about five minutes to reach the API; a new key works straight away.

A key also carries what it may remove, set the same way. Cannot delete is read and write only, and is what every new key starts as. Can delete allows the recoverable deletes: DELETE /memory/{id} and /delete_by_metadata. Can delete and purge adds the permanent ones: /purge_deleted_memories and /purge_memories, which erases the whole workspace. A key asked to do more than it may is refused with 403, and the attempt is written to your receipts as memory.destructive_refused, naming the route and the key. A host should never find this out by trying: GET /usage returns destructive_access.delete and destructive_access.purge for the calling key, along with the projects it can reach, and reading it writes nothing. Read it before asking a person to approve a delete, and keep the answer for a few minutes rather than reading it for every request — a change of level takes about five minutes to reach the API in any case. In a workspace several people or agents write to, give most keys Cannot delete and keep purging for a key you hold yourself. A key with no level set is not a usable key: every request is refused as an invalid key, 401. Keys created in the dashboard always have one.

# same key, different project — hard separation is one header
curl -s -X POST "$BASE/recall_memory" \
  -H "Authorization: Bearer $HIVEMIND_API_KEY" \
  -H "X-Hivemind-Project: proj_9f21ab04c3d2" \
  -H "Content-Type: application/json" \
  -d '{"query": "deploy checklist", "top_k": 5}'
# omit the header and the call is refused with 400 — there is no default

Errors and limits

StatusMeaning & what's in details
404 MEMORY_NOT_FOUNDNothing relevant stored yet — normal for a new workspace; proceed and store what you learn.
409 CONFLICTThe request conflicts with what is stored: /conversation/fork into a session id that already holds records. details.field names the field. Pick a new session id; the same request will be refused again
429 RATE_LIMITEDThe service is at capacity for this instant and refused the request rather than queue it. Subscriptions carry no requests-per-minute limit. The Retry-After header says exactly how long to sleep — use it instead of guessing a backoff. details.reason says which case: in_flight_ceiling (capacity, a couple of seconds), or pre_auth_ip (too many requests with a missing or invalid key from one address — requests with a valid key never count toward it)
400 VALIDATION_ERRORMalformed request: empty content, missing session id, an explicit recall_top_k above your subscription's ceiling, a budget_total above 128,000, or an oversized field (content and user_input over 200,000 characters, queries over 8,000, more than 1,000 messages or 100 holds, or metadata beyond its limits: 64 keys, keys of 128 characters, string values of 8,000 characters, list values of 64 items). A 400 is permanent for that request: fix it or drop it, never retry it unchanged. Whole requests over 8 MB are refused with 413 before they are read. details.field names the offending field (the recall_top_k rejection instead carries max_top_k and requested_top_k). Also "No storage connected" (details.action = connect_cluster): your workspace is active but no Qdrant cluster is connected yet — connect one on the dashboard
401 UNAUTHORIZEDMissing, invalid or revoked key, or an operation your subscription doesn't enable — the message names which. A key revoked in the dashboard, a device key whose person has left the workspace, and a key with no settings in the workspace are all "Invalid API key", never a 403. Revocations reach the API within ~5 minutes
403 UNAUTHORIZEDThe key is valid but may not do this; the message names which case. "Account is not active": the workspace is not active, and details.status says why — resubscribing brings every key back. "This key does not reach that project": the project named in X-Hivemind-Project is not one the key reaches — name one it reaches, or list the key's role on this project in the dashboard. "This key reaches no project": its role is listed on no project — list the role on a project, or give the key another role or All projects, in the dashboard. "This key may not delete records" or "may not purge records": the key's level does not allow the route — details.required names the level needed and details.key_level the one the key has; raise it in the dashboard. A 403 never succeeds on retry
5xx / timeoutService or storage trouble. A /turn whose message could not be stored answers 500 and compiles nothing: the message has no stored row, so the conversation would be missing it. Your call whether to fail open (proceed with local context only) or fail closed — decide once, document it in your host

All errors arrive as one machine-readable envelope shape: {"ok": false, "error": {"code", "message", "details"}, "request_id"}. The code values are stable strings (branch on them, not on messages), and request_id is the correlator to quote in a support request — it ties your call to our logs and receipts.

# a 429, exactly as it arrives
HTTP/1.1 429 Too Many Requests
Retry-After: 2

{"ok": false,
 "error": {"code": "RATE_LIMITED", "message": "Service is at capacity for this instant — retry shortly.", "details": {"reason": "in_flight_ceiling"}},
 "request_id": "0198c7a2-59f2-711e-8d2e-0242ac120002"}

# branch on error.code; sleep exactly Retry-After seconds; quote request_id to support
See our terms of service and how we use your data and protect your privacy. Questions, integration help, or something missing here: support@militant.ai.