This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Reason given by maintainers: SDK Depreciated.
HiveMind Python SDK
Persistent memory and a working-context compiler for your agent, in three lines inside your own loop. Your model, your key — HiveMind never calls an LLM.
pip install hivemind-sdk
Python ≥ 3.10. The import name is hivemind.
Docs: https://hivemind.militant.ai/docs
Async
Agent systems run on the event loop; so does this SDK. AsyncHiveMind
mirrors the sync facade — same three-liner, nothing blocks, pooled
connections underneath:
from hivemind import AsyncHiveMind
async with AsyncHiveMind(base_url="...", api_key="...") as mind:
session = mind.session(budget_total=8192)
result = await session.turn(user_input)
reply = await call_your_llm(result.messages)
await session.record(reply)
AsyncHivemindClient underneath mirrors the engine's native
HivemindOperations surface name-for-name (compile_working_context,
store_conversation_exchange, recall_memory_with_metadata, …) — code
written against the ops layer speaks to the hosted service with the same
vocabulary. The sync client remains pure standard library.
from hivemind import HiveMind
mind = HiveMind(base_url="...", api_key="...", tenant_id="...")
session = mind.session(budget_total=8192)
result = session.turn(user_input) # store -> recall -> compile
reply = call_your_llm(result.messages) # your model, your key
session.record(reply) # completes the exchange
What one turn() does
One HTTP request (POST /turn) — the service composes, in order:
- Stores the user message (receipted).
- Semantically recalls relevant memories and past conversation.
- Compiles local history + recalled records + active holds + your operator briefing into a token-budgeted bundle.
With record(), the whole exchange is two calls — your model runs
between them.
result.messages is a contiguous, internally-ordered block. Use it
as the entire prompt, or surround it — your own system prompt above,
turn-local instructions below; the packet keeps its place either way.
The one rule: never insert into or reorder inside the block — the
compiler ordered it. If you wrap, size budget_total as your model
window minus your wrapper's tokens (token_count measures the packet
only), and prefer the packet's own slots (operator_briefing,
artifacts_context, holds) where you can: content inside is
budget-managed and receipted; content wrapped outside is invisible to
the accounting. budget_total is capped at 128,000 tokens on the
hosted service — a larger value is rejected with a 400 naming the limit.
The budget bounds what is surfaced per turn, not what is remembered:
memory is unbounded. result.receipt is the audit record. Empty recall
on a young tenant is normal, not an error.
session.record(reply) stores your model's reply as the other half of the
exchange, so the next turn — and every future session — remembers it.
How conversation memory recalls
Conversation is remembered as call/response exchanges — a user question, an agent's instruction, whatever the initiating text was, plus the reply it produced. 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 (and vice versa). Facts that appear only in a reply are just as findable as the calls that prompted them.
The recall pool is sized automatically from your session's token budget —
a bigger budget_total recalls more candidates, and the compiler's
budget admission decides what actually enters the bundle (with every
decision receipted). Pass recall_top_k to a session only if you want to
force a fixed pool.
Still worth designing around: record() files the reply and completes
the exchange — treat it as part of the loop. And durable facts that
should stand alone — decisions, outcomes, lessons — belong in
mind.remember(...), where you control their metadata and lifecycle.
Every turn also reports where its time went: result.timings carries the
server-side phase breakdown in seconds (embed_s, store_and_recall_s,
completion_s, compile_s, total_s) — a slow turn names its own
bottleneck.
Beyond the loop
mind.remember(content, metadata)— deliberately store a durable lesson, decision, fact, or outcome.mind.recall(query)/mind.recall_filtered(query, metadata)— explicit recall over deliberate memories only ([]when nothing matches). Conversation history is a separate record class, recalled automatically inside the loop — or explicitly viaclient.recall_conversation.session.hold_set(key, content)/hold_clear(key)— pin operational state ("stop-order", "API is down") into every compile until cleared. Hold content never enters memory: not stored, not recalled, not in receipts — it rides each turn's compile and nothing else. Hold state lives in your session process; if holds should survive a restart, persist and re-apply them alongside your conversation (hydrate()restores history only). Durable facts belong inremember(), not holds.mind.receipts(session_id=..., operation=...)— the audit trail: what ran, what it consumed, what it produced, with lineage.mind.usage()— your workspace's storage against its allowance, point counts, and effective plan. Watch this instead of discovering the storage cap through a 402 mid-write.mind.export(page_size=...)— iterate every record your workspace owns (memories, conversation, receipts);client.export_to_file(path)writes it as JSONL. Your data is portable by contract — this is the door.client.recall_conversation(query, session_id=None, top_k=...)— manual episode recall, two modes: withsession_id=Noneit searches semantically across all sessions and returns one result per exchange; with asession_idit returns that session's exchanges in full (both halves, newest-first) — the replay path.mind.delete_by_metadata(metadata)— soft-deletes everything matching all given metadata keys. It's your data: your API key is the only authorization needed (plan-gated; the hive plan includes it). Deletion is soft — deleted records leave recall immediately but remain recoverable until purged — and every deletion is receipted. Mind the breadth of your filter: fewer keys match more records, and what your agents are allowed to delete is your harness's policy to enforce.mind.client— the raw HTTP client for anything not wrapped, includingPOST /context/compileif you're building a custom loop around your own recall instead of/turn.
Raw-API note (skip this if you use the SDK sessions): when storing
exchange halves yourself via POST /conversation/store, two metadata
conventions make recall render properly — put the initiating message's
text under additional_metadata.semantic_content on the reply half
(it lets a recalled reply carry its question), and additional_metadata.
author on either half to attribute it. The SDK's turn()/record() do
both for you.
Building a chat host (not a script)
The loop contract above is complete; a long-lived, restartable, streaming host needs the layer around it:
Stream first, compile second. turn() does real work — a store, two
semantic recalls, and a full context compile — and at large budgets that
is a multi-second operation, not a lookup. If your HTTP handler calls
turn() before sending anything, the client sees pure silence for those
seconds, and most streaming clients (SSE, WebSocket, fetch readers) treat
prolonged silence as a dead stream and give up. So establish the stream
first: send headers or a heartbeat frame, then compile, then stream your
model. The compile's phase breakdown comes back in result.timings, so
your own logs can say where a slow turn went without guessing:
yield sse_headers() # first byte now — the stream is alive
result = await session.turn(user_input) # then the multi-second compile
yield {"tokens": result.token_count, "timings": result.timings}
# stream the model's reply, generated from result.messages
await session.record(reply)
Restart: session.hydrate(messages). A Session keeps a small local
history in process memory — it is the "recent conversation" the compiler
weaves together with recalled memory each turn. After a worker restart or
redeploy, that history is gone, and without it the next compile sees only
what semantic recall happens to surface. hydrate() rebuilds it from your
own database: pass {role, content, timestamp} dicts, oldest first, with
real timestamps — the compiler orders conversation by timestamp, so
fabricated or missing times scramble reading order. Nothing is re-stored
on the service; hydrate is purely local. The turn counter resumes
automatically (or pass turn_number= explicitly). Map your conversation
id to session_id so a restarted host lands back in the same session —
and when the user clears a thread, start a new session id rather than
hydrating the old one, so the fresh thread doesn't inherit history the
user asked to be rid of.
Aborted stream: abort, never record. The model run happens between
Hivemind calls — turn() before it, record() after it finishes. While
tokens are streaming, make no Hivemind call. If the user cancels or sends
a new message mid-stream, call session.abort_open_exchange() and move
on: the user's message was already stored by turn() and remains
recallable as an unanswered call — which is what happened — and the next
user message is a new turn(), not a reason to close the old exchange. Do not record(partial) on interrupt, and never
record("") (the service rejects empty content). Recording an interrupted
stream misfiles a reply the user never received, and it puts an extra
round-trip in front of the turn() the user is now waiting on. The one
legitimate partial case is a host that deliberately keeps what was shown
on screen as what was said — that is a product decision, made after the
stream has ended, never a cleanup step.
Continuation hops. role in an exchange is positional — it marks
which side initiated (call) and which side answered (response), not
whether a human or a machine was talking. That means a harness that feeds
the model's own output back in as the next input — tool loops, inner
monologue, "keep going" patterns — is using turn(prior_output) exactly
as intended: the prior output is the initiating utterance of the next
exchange, and recall will later find that exchange by it. If your host
needs to reshape local history around such hops (say, to drop
intermediate hops from the visible transcript), hydrate() is the
supported way to rebuild history in the shape you want.
Authorship. Who produced a message is a separate question from
which side of the exchange it sits on, and it gets a separate field:
turn(text, author="operator-7") and
record(reply, author="scout-agent") stamp identity as ordinary
metadata. The compiler surfaces it as the chat message's name field —
the slot chat APIs already accept for named participants — in both live
history and recalled exchanges. This is what keeps multi-agent
transcripts attributed (agent A's calls and agent B's replies each carry
their own name) and what keeps a self-continuation hop from ever showing
the model its own words under someone else's label. Because it is plain
metadata, it also filters:
recall_filtered(query, {"author": "scout-agent"}) returns only that
agent's records. Untagged messages behave exactly as before.
Turn-local instructions. Everything in result.messages came from
somewhere, and where it came from decides how long it lives. The ladder,
shortest-lived first: artifacts_context rides one compile and is
never stored or recalled — use it for document excerpts and one-shot
notes. operator_briefing is also compiled-only (never stored), but the
session re-sends it every turn — use it for standing instructions and
mode flags. Holds persist across turns until you clear them — use
hold_set/hold_clear around a tool burst or an operational condition
("API is down"). remember() is durable forever and recallable from any
session — use it for decisions, outcomes, lessons. Pick the shortest
lifetime that does the job; anything longer pollutes future context.
Context meter. If you show users (or your logs) a context gauge, read
it from result.token_count against result.budget_total — those are
the compiled packet's actual numbers, measured by the thing that built
it. Local history length undercounts (it knows nothing about recall), and
your LLM provider's usage.input_tokens measures a different tokenizer
after your own additions. One packet, one authoritative meter.
Error policy — memory failures and chat failures are different severities, and your host should decide deliberately which is which instead of letting an exception decide for it. The cases:
| Case | Do this |
|---|---|
| 404 (nothing stored yet) | normal on young tenants — proceed |
| 429 | sleep exc.retry_after, retry |
| 402 storage cap | writes blocked, recall and compile keep working — don't fail the chat |
| timeout / 5xx | your call: fail-open to local context or fail-closed — pick one and say so |
record() fails |
the chat already succeeded — retry it out-of-band |
Don't store model control tokens. HiveMind stores and returns text
verbatim — nothing is sanitized. But recalled text re-enters your model's
context on later turns, and if it contains that model's own special
tokens (<|...|> markers and the like), the tokenizer may swallow them
or, worse, obey them as control sequences. Strip or defang such markers
before turn/record; if you need a durable marker in stored text, pick
a form no tokenizer treats as special.
Base URL is the host root. The client appends /hivemind/v1 to every
request itself. Pasting a URL that already contains the prefix (say, from
the OpenAPI page) double-prefixes every path and produces 404s that look
like a broken service. Use https://api.hivemind.militant.ai — never
.../hivemind/v1.
Configuration
Constructor arguments override environment:
| Env var | Meaning |
|---|---|
HIVEMIND_BASE_URL |
Service root (hosted or local — same API) |
HIVEMIND_API_KEY |
Sent as Authorization: Bearer <key> |
HIVEMIND_TENANT_ID |
Your tenant (X-Tenant-ID) |
HIVEMIND_PROJECT |
Project binding (X-Hivemind-Project), optional |
HIVEMIND_TIMEOUT |
Request timeout, seconds (default 120) |
HIVEMIND_BUDGET_TOTAL |
Default compile token budget (default 4096) |
Transport: connection reuse and DNS
Since 0.3.0 the sync client's default transport (stdlib only) keeps one
HTTPS connection alive per client: DNS is resolved and TLS negotiated
once per connection, not once per call. A request that fails before the
response starts (stale keep-alive socket) is transparently retried once on
a fresh connection; a request whose response has started is never re-sent.
Call mind.close() (or use the client as a context manager) to release
the connection. The async client (AsyncHiveMind) pools via httpx and
always has.
Why it matters: behind some resolvers — Docker Desktop is the known case —
a cold DNS lookup can stall ~25 s before the request is even sent. The
service never sees that wait, so its reported timings stay normal while
your wall clock doesn't. With connection reuse you pay any such stall once
per connection lifetime instead of once per turn. If your host's resolver
is unreliable, additionally pin the record (extra_hosts in Docker
Compose, or a local caching resolver).
To take transport control entirely, pass transport= — any callable
(urllib.request.Request, timeout_seconds) -> (status, body_bytes,
headers_dict):
import httpx
from hivemind import HiveMind
_pool = httpx.Client() # your own pool, your own policy
def pooled(request, timeout):
r = _pool.request(
request.get_method(), request.full_url,
content=request.data, headers=dict(request.header_items()),
timeout=timeout,
)
return r.status_code, r.content, dict(r.headers)
mind = HiveMind(transport=pooled)
Development note (this repo)
The import name hivemind collides with the service package at the repo
root, so run SDK tests as their own invocation:
python -m pytest sdk/python/tests
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hivemind_sdk-0.3.0.tar.gz.
File metadata
- Download URL: hivemind_sdk-0.3.0.tar.gz
- Upload date:
- Size: 35.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87572eb9f559f44f931fe19fd0b0a17489941ed6aa1a7caaae9c111e1c10c9d0
|
|
| MD5 |
260e34c39a1527de86357b2a14c9f1c2
|
|
| BLAKE2b-256 |
1aa73e937b8bd256131004602b76a7ebd4201aa3d8ad4ef25125bdda2da67fe3
|
File details
Details for the file hivemind_sdk-0.3.0-py3-none-any.whl.
File metadata
- Download URL: hivemind_sdk-0.3.0-py3-none-any.whl
- Upload date:
- Size: 25.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e5fe704df487c2a595e323f06009b68565e18ae6afd06d5bce92cd6037b9fda
|
|
| MD5 |
9aaf59f2e35dc0b99f39f32ed78ec3e7
|
|
| BLAKE2b-256 |
7eee554289e4645d2a970711d78ecc8c51de06bc889c15449a9262cd781a8fba
|