hivemind-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: hivemind-sdk
3
+ Version: 0.1.0
4
+ Summary: Client SDK for HiveMind: persistent shared memory and a working-context compiler for agent systems.
5
+ Author-email: "Militant.AI" <support@militant.ai>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://hivemind.militant.ai
8
+ Project-URL: Documentation, https://hivemind.militant.ai/docs
9
+ Keywords: ai,agents,memory,llm,context,rag
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+
21
+ # HiveMind Python SDK
22
+
23
+ Persistent memory and a working-context compiler for your agent, in three
24
+ lines inside your own loop. Your model, your key — HiveMind never calls an
25
+ LLM.
26
+
27
+ ```bash
28
+ pip install hivemind-sdk
29
+ ```
30
+
31
+ Python ≥ 3.10, zero dependencies. The import name is `hivemind`.
32
+ Docs: https://hivemind.militant.ai/docs
33
+
34
+ ```python
35
+ from hivemind import HiveMind
36
+
37
+ mind = HiveMind(base_url="...", api_key="...", tenant_id="...")
38
+ session = mind.session(budget_total=8192)
39
+
40
+ result = session.turn(user_input) # store -> recall -> compile
41
+ reply = call_your_llm(result.messages) # your model, your key
42
+ session.record(reply) # completes the exchange
43
+ ```
44
+
45
+ ## What one `turn()` does
46
+
47
+ 1. Stores the user message (receipted).
48
+ 2. Semantically recalls relevant memories and past conversation.
49
+ 3. Compiles local history + recalled records + active holds + your
50
+ operator briefing into a token-budgeted bundle.
51
+
52
+ `result.messages` is the **entire** prompt payload — send it as-is, splice
53
+ nothing in. `result.bundle["decisions"]` explains every admission under
54
+ the budget; `result.receipt` is the audit record. Empty recall on a young
55
+ tenant is normal, not an error.
56
+
57
+ `session.record(reply)` stores your model's reply as the other half of the
58
+ exchange, so the next turn — and every future session — remembers it.
59
+
60
+ ## Beyond the loop
61
+
62
+ - `mind.remember(content, metadata)` — deliberately store a durable
63
+ lesson, decision, fact, or outcome.
64
+ - `mind.recall(query)` / `mind.recall_filtered(query, metadata)` —
65
+ explicit recall, `[]` when nothing matches.
66
+ - `session.hold_set(key, content)` / `hold_clear(key)` — pin operational
67
+ state ("stop-order", "API is down") into every compile until cleared.
68
+ - `mind.receipts(session_id=...)` — the audit trail: what ran, what it
69
+ consumed, what it produced, with lineage.
70
+ - `mind.delete_by_metadata(metadata)` — destructive, audited deletion.
71
+ - `mind.client` — the raw HTTP client for anything not wrapped.
72
+
73
+ ## Configuration
74
+
75
+ Constructor arguments override environment:
76
+
77
+ | Env var | Meaning |
78
+ |---|---|
79
+ | `HIVEMIND_BASE_URL` | Service root (hosted or local — same API) |
80
+ | `HIVEMIND_API_KEY` | Sent as `Authorization: Bearer <key>` |
81
+ | `HIVEMIND_TENANT_ID` | Your tenant (`X-Tenant-ID`) |
82
+ | `HIVEMIND_TIMEOUT` | Request timeout, seconds (default 30) |
83
+ | `HIVEMIND_BUDGET_TOTAL` | Default compile token budget (default 4096) |
84
+
85
+ ## Examples
86
+
87
+ - [`examples/quickstart.py`](examples/quickstart.py) — the full loop with
88
+ no LLM key needed.
89
+ - [`examples/chat_loop.py`](examples/chat_loop.py) — a real chat with any
90
+ OpenAI-compatible model.
91
+
92
+ ## Development note (this repo)
93
+
94
+ The import name `hivemind` collides with the service package at the repo
95
+ root, so run SDK tests as their own invocation:
96
+
97
+ ```bash
98
+ python -m pytest sdk/python/tests
99
+ ```
@@ -0,0 +1,79 @@
1
+ # HiveMind Python SDK
2
+
3
+ Persistent memory and a working-context compiler for your agent, in three
4
+ lines inside your own loop. Your model, your key — HiveMind never calls an
5
+ LLM.
6
+
7
+ ```bash
8
+ pip install hivemind-sdk
9
+ ```
10
+
11
+ Python ≥ 3.10, zero dependencies. The import name is `hivemind`.
12
+ Docs: https://hivemind.militant.ai/docs
13
+
14
+ ```python
15
+ from hivemind import HiveMind
16
+
17
+ mind = HiveMind(base_url="...", api_key="...", tenant_id="...")
18
+ session = mind.session(budget_total=8192)
19
+
20
+ result = session.turn(user_input) # store -> recall -> compile
21
+ reply = call_your_llm(result.messages) # your model, your key
22
+ session.record(reply) # completes the exchange
23
+ ```
24
+
25
+ ## What one `turn()` does
26
+
27
+ 1. Stores the user message (receipted).
28
+ 2. Semantically recalls relevant memories and past conversation.
29
+ 3. Compiles local history + recalled records + active holds + your
30
+ operator briefing into a token-budgeted bundle.
31
+
32
+ `result.messages` is the **entire** prompt payload — send it as-is, splice
33
+ nothing in. `result.bundle["decisions"]` explains every admission under
34
+ the budget; `result.receipt` is the audit record. Empty recall on a young
35
+ tenant is normal, not an error.
36
+
37
+ `session.record(reply)` stores your model's reply as the other half of the
38
+ exchange, so the next turn — and every future session — remembers it.
39
+
40
+ ## Beyond the loop
41
+
42
+ - `mind.remember(content, metadata)` — deliberately store a durable
43
+ lesson, decision, fact, or outcome.
44
+ - `mind.recall(query)` / `mind.recall_filtered(query, metadata)` —
45
+ explicit recall, `[]` when nothing matches.
46
+ - `session.hold_set(key, content)` / `hold_clear(key)` — pin operational
47
+ state ("stop-order", "API is down") into every compile until cleared.
48
+ - `mind.receipts(session_id=...)` — the audit trail: what ran, what it
49
+ consumed, what it produced, with lineage.
50
+ - `mind.delete_by_metadata(metadata)` — destructive, audited deletion.
51
+ - `mind.client` — the raw HTTP client for anything not wrapped.
52
+
53
+ ## Configuration
54
+
55
+ Constructor arguments override environment:
56
+
57
+ | Env var | Meaning |
58
+ |---|---|
59
+ | `HIVEMIND_BASE_URL` | Service root (hosted or local — same API) |
60
+ | `HIVEMIND_API_KEY` | Sent as `Authorization: Bearer <key>` |
61
+ | `HIVEMIND_TENANT_ID` | Your tenant (`X-Tenant-ID`) |
62
+ | `HIVEMIND_TIMEOUT` | Request timeout, seconds (default 30) |
63
+ | `HIVEMIND_BUDGET_TOTAL` | Default compile token budget (default 4096) |
64
+
65
+ ## Examples
66
+
67
+ - [`examples/quickstart.py`](examples/quickstart.py) — the full loop with
68
+ no LLM key needed.
69
+ - [`examples/chat_loop.py`](examples/chat_loop.py) — a real chat with any
70
+ OpenAI-compatible model.
71
+
72
+ ## Development note (this repo)
73
+
74
+ The import name `hivemind` collides with the service package at the repo
75
+ root, so run SDK tests as their own invocation:
76
+
77
+ ```bash
78
+ python -m pytest sdk/python/tests
79
+ ```
@@ -0,0 +1,4 @@
1
+ from hivemind.client import HivemindClient, HivemindClientError
2
+ from hivemind.session import HiveMind, Session, TurnResult
3
+
4
+ __all__ = ["HiveMind", "Session", "TurnResult", "HivemindClient", "HivemindClientError"]
@@ -0,0 +1,256 @@
1
+ """Zero-dependency HTTP client for the HiveMind service API.
2
+
3
+ Uses only the standard library. This is the transport layer the Python
4
+ SDK builds on.
5
+
6
+ Configuration (constructor arguments override environment):
7
+
8
+ - ``HIVEMIND_BASE_URL`` — service root, default ``http://localhost:8000``
9
+ - ``HIVEMIND_API_KEY`` — optional; sent as ``Authorization: Bearer <key>``
10
+ - ``HIVEMIND_TENANT_ID`` — optional; sent as ``X-Tenant-ID`` (required unless
11
+ the service resolves tenancy from auth or runs with a local tenant)
12
+ - ``HIVEMIND_TIMEOUT`` — request timeout in seconds, default ``30``
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import urllib.error
20
+ import urllib.parse
21
+ import urllib.request
22
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
23
+
24
+ API_PREFIX = "/hivemind/v1"
25
+
26
+ # Transport signature: (request, timeout_seconds) -> (status_code, body_bytes)
27
+ Transport = Callable[[urllib.request.Request, float], Tuple[int, bytes]]
28
+
29
+
30
+ class HivemindClientError(RuntimeError):
31
+ """Raised when the HiveMind service returns an error response."""
32
+
33
+ def __init__(self, message: str, *, status: Optional[int] = None, detail: Any = None) -> None:
34
+ super().__init__(message)
35
+ self.status = status
36
+ self.detail = detail
37
+
38
+
39
+ def _default_transport(request: urllib.request.Request, timeout: float) -> Tuple[int, bytes]:
40
+ try:
41
+ with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 (caller controls URL)
42
+ return int(response.status), response.read()
43
+ except urllib.error.HTTPError as exc:
44
+ return int(exc.code), exc.read()
45
+
46
+
47
+ class HivemindClient:
48
+ """Thin, synchronous client over the HiveMind HTTP contract."""
49
+
50
+ def __init__(
51
+ self,
52
+ base_url: Optional[str] = None,
53
+ *,
54
+ api_key: Optional[str] = None,
55
+ tenant_id: Optional[str] = None,
56
+ project: Optional[str] = None,
57
+ timeout: Optional[float] = None,
58
+ transport: Optional[Transport] = None,
59
+ ) -> None:
60
+ self.base_url = (base_url or os.getenv("HIVEMIND_BASE_URL") or "http://localhost:8000").rstrip("/")
61
+ self.api_key = api_key if api_key is not None else os.getenv("HIVEMIND_API_KEY")
62
+ self.tenant_id = tenant_id if tenant_id is not None else os.getenv("HIVEMIND_TENANT_ID")
63
+ # Which project this client operates in; absent, the hive's default
64
+ # project serves. One hive can bind different projects to different
65
+ # storage (managed and BYO side by side).
66
+ self.project = project if project is not None else os.getenv("HIVEMIND_PROJECT")
67
+ self.timeout = float(timeout if timeout is not None else os.getenv("HIVEMIND_TIMEOUT") or 30)
68
+ self._transport = transport or _default_transport
69
+
70
+ # ── memory ────────────────────────────────────────────────────────────
71
+
72
+ def create_memory(self, content: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
73
+ return self._post("/create_memory", {"content": content, "metadata": metadata or {}})
74
+
75
+ def recall_memory(self, query: str, top_k: int = 5) -> Dict[str, Any]:
76
+ return self._post("/recall_memory", {"query": query, "top_k": int(top_k)})
77
+
78
+ def recall_with_metadata(
79
+ self, query: str, metadata: Dict[str, Any], top_k: int = 10
80
+ ) -> Dict[str, Any]:
81
+ return self._post(
82
+ "/recall_with_metadata",
83
+ {"query": query, "metadata": dict(metadata or {}), "top_k": int(top_k)},
84
+ )
85
+
86
+ def delete_by_metadata(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
87
+ return self._post("/delete_by_metadata", {"metadata": dict(metadata or {})})
88
+
89
+ # ── conversation ──────────────────────────────────────────────────────
90
+
91
+ def store_conversation(
92
+ self,
93
+ content: str,
94
+ session_id: str,
95
+ turn_number: int,
96
+ role: str,
97
+ exchange_pair_id: str,
98
+ additional_metadata: Optional[Dict[str, Any]] = None,
99
+ ) -> Dict[str, Any]:
100
+ return self._post(
101
+ "/conversation/store",
102
+ {
103
+ "content": content,
104
+ "session_id": session_id,
105
+ "turn_number": int(turn_number),
106
+ "role": role,
107
+ "exchange_pair_id": exchange_pair_id,
108
+ "additional_metadata": additional_metadata,
109
+ },
110
+ )
111
+
112
+ def recall_conversation(
113
+ self, query: str, session_id: Optional[str] = None, top_k: int = 10
114
+ ) -> Dict[str, Any]:
115
+ return self._post(
116
+ "/conversation/recall",
117
+ {"query": query, "session_id": session_id, "top_k": int(top_k)},
118
+ )
119
+
120
+ # ── context ───────────────────────────────────────────────────────────
121
+
122
+ def compile_context(
123
+ self,
124
+ session_id: str,
125
+ messages: Sequence[Dict[str, Any]],
126
+ turn_number: int,
127
+ budget_total: int,
128
+ *,
129
+ user_input: Optional[str] = None,
130
+ operator_briefing: Optional[str] = None,
131
+ holds: Optional[List[Dict[str, Any]]] = None,
132
+ artifacts_context: Optional[str] = None,
133
+ recalled_conversation: Optional[List[Dict[str, Any]]] = None,
134
+ recalled_memories: Optional[List[Dict[str, Any]]] = None,
135
+ policy: Optional[Dict[str, Any]] = None,
136
+ config: Optional[Dict[str, Any]] = None,
137
+ event_id: Optional[str] = None,
138
+ trace_id: Optional[str] = None,
139
+ timestamp: Optional[float] = None,
140
+ ) -> Dict[str, Any]:
141
+ payload: Dict[str, Any] = {
142
+ "session_id": session_id,
143
+ "messages": list(messages),
144
+ "turn_number": int(turn_number),
145
+ "budget_total": int(budget_total),
146
+ "user_input": user_input,
147
+ "operator_briefing": operator_briefing,
148
+ "holds": holds,
149
+ "artifacts_context": artifacts_context,
150
+ "recalled_conversation": recalled_conversation,
151
+ "recalled_memories": recalled_memories,
152
+ "policy": policy,
153
+ "config": config,
154
+ "event_id": event_id,
155
+ "trace_id": trace_id,
156
+ "timestamp": timestamp,
157
+ }
158
+ return self._post("/context/compile", payload)
159
+
160
+ # ── receipts ──────────────────────────────────────────────────────────
161
+
162
+ def list_receipts(
163
+ self,
164
+ limit: int = 50,
165
+ session_id: Optional[str] = None,
166
+ operation: Optional[str] = None,
167
+ ) -> Dict[str, Any]:
168
+ params: Dict[str, Any] = {"limit": int(limit)}
169
+ if session_id:
170
+ params["session_id"] = session_id
171
+ if operation:
172
+ params["operation"] = operation
173
+ return self._get("/receipts", params)
174
+
175
+ # ── export ────────────────────────────────────────────────────────────
176
+
177
+ def export(self, page_size: int = 200):
178
+ """Iterate every record the tenant owns: memories and conversations,
179
+ then receipts (including legacy). Yields dicts of
180
+ {id, content, metadata, phase}. Follows pagination to completion."""
181
+ cursor: Optional[str] = None
182
+ while True:
183
+ params: Dict[str, Any] = {"limit": int(page_size)}
184
+ if cursor:
185
+ params["cursor"] = cursor
186
+ envelope = self._get("/export", params)
187
+ data = envelope.get("data") or {}
188
+ phase = data.get("phase")
189
+ for record in data.get("records") or []:
190
+ yield {**record, "phase": phase}
191
+ if data.get("done") or not data.get("next_cursor"):
192
+ return
193
+ cursor = data["next_cursor"]
194
+
195
+ def export_to_file(self, path: str, page_size: int = 200) -> int:
196
+ """Write the full export to a JSONL file. Returns the record count."""
197
+ count = 0
198
+ with open(path, "w", encoding="utf-8") as handle:
199
+ for record in self.export(page_size=page_size):
200
+ handle.write(json.dumps(record, ensure_ascii=False, default=str) + "\n")
201
+ count += 1
202
+ return count
203
+
204
+ # ── transport plumbing ────────────────────────────────────────────────
205
+
206
+ def _headers(self) -> Dict[str, str]:
207
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
208
+ if self.api_key:
209
+ headers["Authorization"] = f"Bearer {self.api_key}"
210
+ if self.tenant_id:
211
+ headers["X-Tenant-ID"] = str(self.tenant_id)
212
+ if self.project:
213
+ headers["X-Hivemind-Project"] = str(self.project)
214
+ return headers
215
+
216
+ def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
217
+ body = json.dumps(payload, ensure_ascii=True).encode("utf-8")
218
+ request = urllib.request.Request( # noqa: S310
219
+ f"{self.base_url}{API_PREFIX}{path}",
220
+ data=body,
221
+ headers=self._headers(),
222
+ method="POST",
223
+ )
224
+ return self._execute(request)
225
+
226
+ def _get(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]:
227
+ query = urllib.parse.urlencode({k: v for k, v in params.items() if v is not None})
228
+ url = f"{self.base_url}{API_PREFIX}{path}"
229
+ if query:
230
+ url = f"{url}?{query}"
231
+ request = urllib.request.Request(url, headers=self._headers(), method="GET") # noqa: S310
232
+ return self._execute(request)
233
+
234
+ def _execute(self, request: urllib.request.Request) -> Dict[str, Any]:
235
+ try:
236
+ status, raw = self._transport(request, self.timeout)
237
+ except urllib.error.URLError as exc:
238
+ raise HivemindClientError(
239
+ f"HiveMind service unreachable at {self.base_url}: {exc.reason}"
240
+ ) from exc
241
+
242
+ try:
243
+ parsed = json.loads(raw.decode("utf-8")) if raw else {}
244
+ except Exception as exc:
245
+ raise HivemindClientError(
246
+ f"HiveMind returned non-JSON response (HTTP {status}).", status=status
247
+ ) from exc
248
+
249
+ if status >= 400 or (isinstance(parsed, dict) and parsed.get("ok") is False):
250
+ detail = parsed.get("detail") if isinstance(parsed, dict) else parsed
251
+ raise HivemindClientError(
252
+ f"HiveMind request failed (HTTP {status}): {json.dumps(detail, default=str)[:500]}",
253
+ status=status,
254
+ detail=detail,
255
+ )
256
+ return parsed if isinstance(parsed, dict) else {"ok": True, "data": parsed}
@@ -0,0 +1,340 @@
1
+ """Turn-loop session layer for the HiveMind SDK.
2
+
3
+ The integration contract, inside the customer's own agent loop:
4
+
5
+ from hivemind import HiveMind
6
+
7
+ mind = HiveMind(base_url="...", api_key="...", tenant_id="...")
8
+ session = mind.session(budget_total=8192)
9
+
10
+ result = session.turn(user_input) # store -> recall -> compile
11
+ reply = call_your_llm(result.messages) # your model, your key
12
+ session.record(reply) # completes the exchange
13
+
14
+ ``result.messages`` is the ENTIRE prompt payload: send it to your model
15
+ as-is and do not splice anything else in. Every step is receipted
16
+ server-side; ``result.receipt`` is the audit record for the compile and
17
+ ``result.bundle["decisions"]`` explains every admission under the token
18
+ budget.
19
+
20
+ One ``turn()`` call performs, in order:
21
+
22
+ 1. stores the user message as the user half of a conversation exchange
23
+ 2. semantically recalls relevant memories and past conversation
24
+ 3. compiles local history + recalled records + active holds + operator
25
+ briefing into a budgeted working-context bundle
26
+
27
+ ``record(reply)`` stores the assistant half of the exchange and keeps the
28
+ local history that feeds the next compile.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import os
34
+ import uuid
35
+ from dataclasses import dataclass, field
36
+ from typing import Any, Callable, Dict, List, Optional
37
+
38
+ from hivemind.client import HivemindClient, HivemindClientError
39
+
40
+ DEFAULT_BUDGET_TOTAL = 4096
41
+ DEFAULT_RECALL_TOP_K = 5
42
+ DEFAULT_LOCAL_HISTORY = 40
43
+
44
+
45
+ def _records(envelope: Dict[str, Any]) -> List[Dict[str, Any]]:
46
+ data = envelope.get("data") or {}
47
+ for key in ("memories", "exchanges", "results"):
48
+ value = data.get(key)
49
+ if isinstance(value, list):
50
+ return value
51
+ return []
52
+
53
+
54
+ @dataclass
55
+ class TurnResult:
56
+ """What one turn() call returns.
57
+
58
+ ``messages`` is the compiled prompt payload — the only field to send
59
+ to the model. Everything else is audit and diagnostics.
60
+ """
61
+
62
+ messages: List[Dict[str, Any]]
63
+ bundle: Dict[str, Any]
64
+ receipt: Dict[str, Any]
65
+ session_id: str
66
+ turn_number: int
67
+ exchange_pair_id: str
68
+ recalled_memories: int
69
+ recalled_conversation: int
70
+ warnings: List[str] = field(default_factory=list)
71
+
72
+ @property
73
+ def token_count(self) -> Optional[int]:
74
+ return self.bundle.get("token_count")
75
+
76
+ @property
77
+ def budget_total(self) -> Optional[int]:
78
+ return self.bundle.get("budget_total")
79
+
80
+ @property
81
+ def decisions(self) -> List[Dict[str, Any]]:
82
+ return list(self.bundle.get("decisions") or [])
83
+
84
+ @property
85
+ def receipt_id(self) -> Optional[str]:
86
+ return self.receipt.get("receipt_id") or self.receipt.get("id")
87
+
88
+
89
+ class Session:
90
+ """One conversation with memory maintained for you.
91
+
92
+ Not thread-safe; use one Session per concurrent conversation.
93
+ """
94
+
95
+ def __init__(
96
+ self,
97
+ client: HivemindClient,
98
+ *,
99
+ session_id: Optional[str] = None,
100
+ budget_total: int = DEFAULT_BUDGET_TOTAL,
101
+ operator_briefing: Optional[str] = None,
102
+ recall_top_k: int = DEFAULT_RECALL_TOP_K,
103
+ local_history_limit: int = DEFAULT_LOCAL_HISTORY,
104
+ ) -> None:
105
+ self._client = client
106
+ self.session_id = (session_id or "").strip() or f"sdk-{uuid.uuid4().hex[:12]}"
107
+ self.budget_total = int(budget_total)
108
+ self.operator_briefing = operator_briefing
109
+ self.recall_top_k = int(recall_top_k)
110
+ self.local_history_limit = int(local_history_limit)
111
+
112
+ self._turn = 0
113
+ self._history: List[Dict[str, str]] = []
114
+ self._open_exchange: Optional[Dict[str, Any]] = None
115
+ self._holds: Dict[str, Dict[str, Any]] = {}
116
+
117
+ # ── the loop ──────────────────────────────────────────────────────────
118
+
119
+ def turn(
120
+ self,
121
+ user_input: str,
122
+ *,
123
+ budget_total: Optional[int] = None,
124
+ operator_briefing: Optional[str] = None,
125
+ artifacts_context: Optional[str] = None,
126
+ policy: Optional[Dict[str, Any]] = None,
127
+ ) -> TurnResult:
128
+ """Run store -> recall -> compile for one turn.
129
+
130
+ Returns a TurnResult whose ``messages`` are the entire prompt for
131
+ your model call. Recall failures degrade to empty recall (noted in
132
+ ``warnings``); a compile failure raises HivemindClientError.
133
+ """
134
+ warnings: List[str] = []
135
+ self._turn += 1
136
+ turn_number = self._turn
137
+ exchange_id = f"xp-{uuid.uuid4().hex[:8]}"
138
+
139
+ # 1. Capture the user half of this exchange.
140
+ try:
141
+ self._client.store_conversation(
142
+ user_input, self.session_id, turn_number, "user", exchange_id
143
+ )
144
+ self._open_exchange = {"exchange_id": exchange_id, "turn": turn_number}
145
+ except HivemindClientError as exc:
146
+ warnings.append(f"user input not stored: {exc}")
147
+
148
+ # 2. Recall both lanes. 404 means "nothing stored yet", not failure.
149
+ recalled_memories = self._safe_recall(
150
+ lambda: self._client.recall_memory(user_input, self.recall_top_k),
151
+ warnings,
152
+ "memory recall",
153
+ )
154
+ recalled_conversation = self._safe_recall(
155
+ lambda: self._client.recall_conversation(user_input, None, self.recall_top_k),
156
+ warnings,
157
+ "conversation recall",
158
+ )
159
+
160
+ # 3. Compile local history + recall + holds under the budget.
161
+ current = {"role": "user", "content": user_input}
162
+ envelope = self._client.compile_context(
163
+ self.session_id,
164
+ [*self._history, current],
165
+ turn_number,
166
+ int(budget_total or self.budget_total),
167
+ user_input=user_input,
168
+ operator_briefing=operator_briefing or self.operator_briefing,
169
+ holds=self.holds() or None,
170
+ artifacts_context=artifacts_context,
171
+ recalled_conversation=recalled_conversation or None,
172
+ recalled_memories=recalled_memories or None,
173
+ policy=policy,
174
+ )
175
+ data = envelope.get("data") or {}
176
+ bundle = data.get("bundle") or {}
177
+
178
+ self._append_history(current)
179
+ return TurnResult(
180
+ messages=list(bundle.get("messages") or []),
181
+ bundle=bundle,
182
+ receipt=data.get("receipt") or {},
183
+ session_id=self.session_id,
184
+ turn_number=turn_number,
185
+ exchange_pair_id=exchange_id,
186
+ recalled_memories=len(recalled_memories),
187
+ recalled_conversation=len(recalled_conversation),
188
+ warnings=warnings,
189
+ )
190
+
191
+ def record(self, assistant_reply: str) -> Dict[str, Any]:
192
+ """Store the assistant half of the current exchange.
193
+
194
+ Call once per turn, after your model call. Uses the exchange the
195
+ last turn() opened; if none is open, stores under a fresh exchange
196
+ at the current turn.
197
+ """
198
+ open_exchange = self._open_exchange
199
+ self._open_exchange = None
200
+ turn_number = open_exchange["turn"] if open_exchange else max(1, self._turn)
201
+ exchange_id = (
202
+ open_exchange["exchange_id"] if open_exchange else f"xp-{uuid.uuid4().hex[:8]}"
203
+ )
204
+ result = self._client.store_conversation(
205
+ assistant_reply, self.session_id, turn_number, "assistant", exchange_id
206
+ )
207
+ self._append_history({"role": "assistant", "content": assistant_reply})
208
+ return result
209
+
210
+ # ── holds (pins injected into every compile) ─────────────────────────
211
+
212
+ def hold_set(self, key: str, content: str, title: Optional[str] = None) -> None:
213
+ key = str(key).strip()
214
+ if not key:
215
+ raise ValueError("hold key must be non-empty")
216
+ self._holds[key] = {
217
+ "id": key,
218
+ "title": (title or key).strip(),
219
+ "content": content,
220
+ "held_since": max(1, self._turn),
221
+ }
222
+
223
+ def hold_clear(self, key: str) -> bool:
224
+ return self._holds.pop(str(key).strip(), None) is not None
225
+
226
+ def holds(self) -> List[Dict[str, Any]]:
227
+ return [dict(value) for value in self._holds.values()]
228
+
229
+ # ── plumbing ──────────────────────────────────────────────────────────
230
+
231
+ def _append_history(self, message: Dict[str, str]) -> None:
232
+ self._history.append(message)
233
+ if len(self._history) > self.local_history_limit:
234
+ del self._history[: len(self._history) - self.local_history_limit]
235
+
236
+ @staticmethod
237
+ def _safe_recall(
238
+ call: Callable[[], Dict[str, Any]],
239
+ warnings: List[str],
240
+ label: str,
241
+ ) -> List[Dict[str, Any]]:
242
+ try:
243
+ return _records(call())
244
+ except HivemindClientError as exc:
245
+ if exc.status == 404:
246
+ return []
247
+ warnings.append(f"{label} failed: {exc}")
248
+ return []
249
+
250
+
251
+ class HiveMind:
252
+ """Entry point. Holds credentials; makes sessions and deliberate calls.
253
+
254
+ Constructor arguments override environment (HIVEMIND_BASE_URL,
255
+ HIVEMIND_API_KEY, HIVEMIND_TENANT_ID, HIVEMIND_TIMEOUT). The same code
256
+ works against the hosted service and a local one — only base_url
257
+ changes.
258
+ """
259
+
260
+ def __init__(
261
+ self,
262
+ base_url: Optional[str] = None,
263
+ *,
264
+ api_key: Optional[str] = None,
265
+ tenant_id: Optional[str] = None,
266
+ timeout: Optional[float] = None,
267
+ client: Optional[HivemindClient] = None,
268
+ ) -> None:
269
+ self._client = client or HivemindClient(
270
+ base_url, api_key=api_key, tenant_id=tenant_id, timeout=timeout
271
+ )
272
+
273
+ def session(
274
+ self,
275
+ session_id: Optional[str] = None,
276
+ *,
277
+ budget_total: Optional[int] = None,
278
+ operator_briefing: Optional[str] = None,
279
+ recall_top_k: int = DEFAULT_RECALL_TOP_K,
280
+ ) -> Session:
281
+ env_budget = os.getenv("HIVEMIND_BUDGET_TOTAL")
282
+ resolved_budget = (
283
+ int(budget_total)
284
+ if budget_total is not None
285
+ else int(env_budget) if env_budget else DEFAULT_BUDGET_TOTAL
286
+ )
287
+ return Session(
288
+ self._client,
289
+ session_id=session_id,
290
+ budget_total=resolved_budget,
291
+ operator_briefing=operator_briefing,
292
+ recall_top_k=recall_top_k,
293
+ )
294
+
295
+ # ── deliberate memory (outside the turn loop) ─────────────────────────
296
+
297
+ def remember(self, content: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
298
+ """Store a durable memory: a decision, lesson, fact, or outcome."""
299
+ return self._client.create_memory(content, metadata)
300
+
301
+ def recall(self, query: str, top_k: int = DEFAULT_RECALL_TOP_K) -> List[Dict[str, Any]]:
302
+ """Semantic recall. Returns [] when nothing matches."""
303
+ try:
304
+ return _records(self._client.recall_memory(query, top_k))
305
+ except HivemindClientError as exc:
306
+ if exc.status == 404:
307
+ return []
308
+ raise
309
+
310
+ def recall_filtered(
311
+ self, query: str, metadata: Dict[str, Any], top_k: int = 10
312
+ ) -> List[Dict[str, Any]]:
313
+ """Recall constrained to exact metadata matches. Returns [] when empty."""
314
+ try:
315
+ return _records(self._client.recall_with_metadata(query, metadata, top_k))
316
+ except HivemindClientError as exc:
317
+ if exc.status == 404:
318
+ return []
319
+ raise
320
+
321
+ def receipts(
322
+ self,
323
+ limit: int = 50,
324
+ session_id: Optional[str] = None,
325
+ operation: Optional[str] = None,
326
+ ) -> List[Dict[str, Any]]:
327
+ """The audit trail: what ran, what it consumed, what it produced."""
328
+ envelope = self._client.list_receipts(limit, session_id, operation)
329
+ data = envelope.get("data") or {}
330
+ receipts = data.get("receipts") or data.get("results") or []
331
+ return list(receipts) if isinstance(receipts, list) else []
332
+
333
+ def delete_by_metadata(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
334
+ """DESTRUCTIVE, audited: soft-delete memories matching all keys."""
335
+ return self._client.delete_by_metadata(metadata)
336
+
337
+ @property
338
+ def client(self) -> HivemindClient:
339
+ """The raw HTTP client, for anything the SDK doesn't wrap."""
340
+ return self._client
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: hivemind-sdk
3
+ Version: 0.1.0
4
+ Summary: Client SDK for HiveMind: persistent shared memory and a working-context compiler for agent systems.
5
+ Author-email: "Militant.AI" <support@militant.ai>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://hivemind.militant.ai
8
+ Project-URL: Documentation, https://hivemind.militant.ai/docs
9
+ Keywords: ai,agents,memory,llm,context,rag
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+
21
+ # HiveMind Python SDK
22
+
23
+ Persistent memory and a working-context compiler for your agent, in three
24
+ lines inside your own loop. Your model, your key — HiveMind never calls an
25
+ LLM.
26
+
27
+ ```bash
28
+ pip install hivemind-sdk
29
+ ```
30
+
31
+ Python ≥ 3.10, zero dependencies. The import name is `hivemind`.
32
+ Docs: https://hivemind.militant.ai/docs
33
+
34
+ ```python
35
+ from hivemind import HiveMind
36
+
37
+ mind = HiveMind(base_url="...", api_key="...", tenant_id="...")
38
+ session = mind.session(budget_total=8192)
39
+
40
+ result = session.turn(user_input) # store -> recall -> compile
41
+ reply = call_your_llm(result.messages) # your model, your key
42
+ session.record(reply) # completes the exchange
43
+ ```
44
+
45
+ ## What one `turn()` does
46
+
47
+ 1. Stores the user message (receipted).
48
+ 2. Semantically recalls relevant memories and past conversation.
49
+ 3. Compiles local history + recalled records + active holds + your
50
+ operator briefing into a token-budgeted bundle.
51
+
52
+ `result.messages` is the **entire** prompt payload — send it as-is, splice
53
+ nothing in. `result.bundle["decisions"]` explains every admission under
54
+ the budget; `result.receipt` is the audit record. Empty recall on a young
55
+ tenant is normal, not an error.
56
+
57
+ `session.record(reply)` stores your model's reply as the other half of the
58
+ exchange, so the next turn — and every future session — remembers it.
59
+
60
+ ## Beyond the loop
61
+
62
+ - `mind.remember(content, metadata)` — deliberately store a durable
63
+ lesson, decision, fact, or outcome.
64
+ - `mind.recall(query)` / `mind.recall_filtered(query, metadata)` —
65
+ explicit recall, `[]` when nothing matches.
66
+ - `session.hold_set(key, content)` / `hold_clear(key)` — pin operational
67
+ state ("stop-order", "API is down") into every compile until cleared.
68
+ - `mind.receipts(session_id=...)` — the audit trail: what ran, what it
69
+ consumed, what it produced, with lineage.
70
+ - `mind.delete_by_metadata(metadata)` — destructive, audited deletion.
71
+ - `mind.client` — the raw HTTP client for anything not wrapped.
72
+
73
+ ## Configuration
74
+
75
+ Constructor arguments override environment:
76
+
77
+ | Env var | Meaning |
78
+ |---|---|
79
+ | `HIVEMIND_BASE_URL` | Service root (hosted or local — same API) |
80
+ | `HIVEMIND_API_KEY` | Sent as `Authorization: Bearer <key>` |
81
+ | `HIVEMIND_TENANT_ID` | Your tenant (`X-Tenant-ID`) |
82
+ | `HIVEMIND_TIMEOUT` | Request timeout, seconds (default 30) |
83
+ | `HIVEMIND_BUDGET_TOTAL` | Default compile token budget (default 4096) |
84
+
85
+ ## Examples
86
+
87
+ - [`examples/quickstart.py`](examples/quickstart.py) — the full loop with
88
+ no LLM key needed.
89
+ - [`examples/chat_loop.py`](examples/chat_loop.py) — a real chat with any
90
+ OpenAI-compatible model.
91
+
92
+ ## Development note (this repo)
93
+
94
+ The import name `hivemind` collides with the service package at the repo
95
+ root, so run SDK tests as their own invocation:
96
+
97
+ ```bash
98
+ python -m pytest sdk/python/tests
99
+ ```
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ hivemind/__init__.py
4
+ hivemind/client.py
5
+ hivemind/session.py
6
+ hivemind_sdk.egg-info/PKG-INFO
7
+ hivemind_sdk.egg-info/SOURCES.txt
8
+ hivemind_sdk.egg-info/dependency_links.txt
9
+ hivemind_sdk.egg-info/top_level.txt
10
+ tests/test_session.py
@@ -0,0 +1 @@
1
+ hivemind
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # Distribution name: "hivemind" on PyPI belongs to an unrelated distributed
7
+ # deep-learning library, so the package installs as hivemind-sdk. The
8
+ # import name stays `hivemind` (which also means our SDK and that library
9
+ # cannot coexist in one environment — acceptable, non-overlapping audience).
10
+ name = "hivemind-sdk"
11
+ version = "0.1.0"
12
+ description = "Client SDK for HiveMind: persistent shared memory and a working-context compiler for agent systems."
13
+ readme = "README.md"
14
+ requires-python = ">=3.10"
15
+ license = { text = "Proprietary" }
16
+ authors = [{ name = "Militant.AI", email = "support@militant.ai" }]
17
+ keywords = ["ai", "agents", "memory", "llm", "context", "rag"]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Software Development :: Libraries",
26
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
27
+ ]
28
+ # No runtime dependencies: the client is pure standard library.
29
+ dependencies = []
30
+
31
+ [project.urls]
32
+ Homepage = "https://hivemind.militant.ai"
33
+ Documentation = "https://hivemind.militant.ai/docs"
34
+
35
+ [tool.setuptools]
36
+ packages = ["hivemind"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ import pytest
6
+
7
+ from hivemind.client import HivemindClientError
8
+ from hivemind.session import DEFAULT_BUDGET_TOTAL, HiveMind, Session
9
+
10
+
11
+ class FakeClient:
12
+ def __init__(self) -> None:
13
+ self.stores: List[Dict[str, Any]] = []
14
+ self.compiles: List[Dict[str, Any]] = []
15
+ self.memory_error: Optional[Exception] = None
16
+ self.conversation_error: Optional[Exception] = None
17
+ self.compile_error: Optional[Exception] = None
18
+
19
+ def store_conversation(
20
+ self,
21
+ content: str,
22
+ session_id: str,
23
+ turn_number: int,
24
+ role: str,
25
+ exchange_pair_id: str,
26
+ additional_metadata: Optional[Dict[str, Any]] = None,
27
+ ) -> Dict[str, Any]:
28
+ self.stores.append(
29
+ {
30
+ "content": content,
31
+ "session_id": session_id,
32
+ "turn_number": turn_number,
33
+ "role": role,
34
+ "exchange_pair_id": exchange_pair_id,
35
+ }
36
+ )
37
+ return {"ok": True, "data": {"id": f"conv-{len(self.stores)}", "receipt_id": "r"}}
38
+
39
+ def recall_memory(self, query: str, top_k: int = 5) -> Dict[str, Any]:
40
+ if self.memory_error is not None:
41
+ raise self.memory_error
42
+ return {"ok": True, "data": {"memories": [{"id": "m1", "content": "a lesson"}]}}
43
+
44
+ def recall_conversation(
45
+ self, query: str, session_id: Optional[str] = None, top_k: int = 10
46
+ ) -> Dict[str, Any]:
47
+ if self.conversation_error is not None:
48
+ raise self.conversation_error
49
+ return {"ok": True, "data": {"exchanges": [{"id": "c1", "content": "earlier"}]}}
50
+
51
+ def compile_context(
52
+ self,
53
+ session_id: str,
54
+ messages,
55
+ turn_number: int,
56
+ budget_total: int,
57
+ **kwargs: Any,
58
+ ) -> Dict[str, Any]:
59
+ if self.compile_error is not None:
60
+ raise self.compile_error
61
+ self.compiles.append(
62
+ {
63
+ "session_id": session_id,
64
+ "messages": list(messages),
65
+ "turn_number": turn_number,
66
+ "budget_total": budget_total,
67
+ "holds": kwargs.get("holds"),
68
+ "recalled_memories": kwargs.get("recalled_memories"),
69
+ "recalled_conversation": kwargs.get("recalled_conversation"),
70
+ "operator_briefing": kwargs.get("operator_briefing"),
71
+ }
72
+ )
73
+ return {
74
+ "ok": True,
75
+ "data": {
76
+ "bundle": {
77
+ "messages": [{"role": "system", "content": "compiled"}],
78
+ "token_count": 42,
79
+ "budget_total": budget_total,
80
+ "decisions": [{"action": "included"}],
81
+ },
82
+ "receipt": {"receipt_id": "rcpt-compile"},
83
+ },
84
+ }
85
+
86
+
87
+ @pytest.fixture()
88
+ def fake() -> FakeClient:
89
+ return FakeClient()
90
+
91
+
92
+ @pytest.fixture()
93
+ def session(fake: FakeClient) -> Session:
94
+ return Session(fake, session_id="s1")
95
+
96
+
97
+ def test_turn_stores_recalls_and_compiles(fake: FakeClient, session: Session) -> None:
98
+ result = session.turn("what do we know?")
99
+
100
+ assert fake.stores[0]["role"] == "user"
101
+ assert fake.stores[0]["turn_number"] == 1
102
+ compile_call = fake.compiles[0]
103
+ assert compile_call["messages"] == [{"role": "user", "content": "what do we know?"}]
104
+ assert compile_call["recalled_memories"] == [{"id": "m1", "content": "a lesson"}]
105
+ assert compile_call["recalled_conversation"] == [{"id": "c1", "content": "earlier"}]
106
+ assert compile_call["budget_total"] == DEFAULT_BUDGET_TOTAL
107
+
108
+ assert result.messages == [{"role": "system", "content": "compiled"}]
109
+ assert result.token_count == 42
110
+ assert result.receipt_id == "rcpt-compile"
111
+ assert result.turn_number == 1
112
+ assert result.recalled_memories == 1
113
+ assert result.warnings == []
114
+
115
+
116
+ def test_record_completes_the_exchange_and_feeds_next_compile(
117
+ fake: FakeClient, session: Session
118
+ ) -> None:
119
+ first = session.turn("hello")
120
+ session.record("hi, I remember you")
121
+ session.turn("second question")
122
+
123
+ roles = [(s["role"], s["turn_number"], s["exchange_pair_id"]) for s in fake.stores]
124
+ assert roles[0] == ("user", 1, first.exchange_pair_id)
125
+ assert roles[1] == ("assistant", 1, first.exchange_pair_id)
126
+ assert roles[2][0] == "user" and roles[2][1] == 2
127
+
128
+ # Local history flows into the next compile.
129
+ assert fake.compiles[1]["messages"] == [
130
+ {"role": "user", "content": "hello"},
131
+ {"role": "assistant", "content": "hi, I remember you"},
132
+ {"role": "user", "content": "second question"},
133
+ ]
134
+
135
+
136
+ def test_holds_are_injected_until_cleared(fake: FakeClient, session: Session) -> None:
137
+ session.hold_set("stop-order", "Do not deploy until QA signs off.")
138
+ session.turn("status?")
139
+ assert fake.compiles[0]["holds"][0]["id"] == "stop-order"
140
+
141
+ assert session.hold_clear("stop-order") is True
142
+ session.turn("and now?")
143
+ assert fake.compiles[1]["holds"] is None
144
+
145
+
146
+ def test_empty_recall_is_not_an_error(fake: FakeClient, session: Session) -> None:
147
+ fake.memory_error = HivemindClientError("nothing", status=404)
148
+ fake.conversation_error = HivemindClientError("nothing", status=404)
149
+
150
+ result = session.turn("first ever message")
151
+
152
+ assert result.warnings == []
153
+ assert result.recalled_memories == 0
154
+ assert fake.compiles[0]["recalled_memories"] is None
155
+
156
+
157
+ def test_recall_outage_degrades_with_warning(fake: FakeClient, session: Session) -> None:
158
+ fake.memory_error = HivemindClientError("boom", status=500)
159
+
160
+ result = session.turn("hello")
161
+
162
+ assert len(result.warnings) == 1
163
+ assert "memory recall failed" in result.warnings[0]
164
+ assert fake.compiles # compile still ran
165
+
166
+
167
+ def test_compile_failure_raises(fake: FakeClient, session: Session) -> None:
168
+ fake.compile_error = HivemindClientError("compiler down", status=500)
169
+ with pytest.raises(HivemindClientError):
170
+ session.turn("hello")
171
+
172
+
173
+ def test_hivemind_entrypoint_builds_sessions_and_passthroughs(fake: FakeClient) -> None:
174
+ mind = HiveMind(client=fake)
175
+ session = mind.session("named-session", budget_total=1024)
176
+
177
+ assert session.session_id == "named-session"
178
+ assert session.budget_total == 1024
179
+
180
+ generated = mind.session()
181
+ assert generated.session_id.startswith("sdk-")