bilinc 2.3.7__tar.gz → 2.3.8__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.
- {bilinc-2.3.7/src/bilinc.egg-info → bilinc-2.3.8}/PKG-INFO +37 -6
- {bilinc-2.3.7 → bilinc-2.3.8}/README.md +35 -4
- {bilinc-2.3.7 → bilinc-2.3.8}/pyproject.toml +2 -2
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc/__init__.py +2 -2
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc/cli/main.py +209 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc/client.py +200 -4
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc/cloud_mcp.py +100 -1
- {bilinc-2.3.7 → bilinc-2.3.8/src/bilinc.egg-info}/PKG-INFO +37 -6
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc.egg-info/requires.txt +1 -1
- {bilinc-2.3.7 → bilinc-2.3.8}/tests/test_cloud_only_package.py +6 -6
- {bilinc-2.3.7 → bilinc-2.3.8}/LICENSE +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/MANIFEST.in +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/setup.cfg +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/setup.py +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc/cli/__init__.py +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc/cli/__main__.py +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc/cli/login.py +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc.egg-info/SOURCES.txt +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc.egg-info/dependency_links.txt +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc.egg-info/entry_points.txt +0 -0
- {bilinc-2.3.7 → bilinc-2.3.8}/src/bilinc.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bilinc
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.8
|
|
4
4
|
Summary: Cloud-only memory SDK and MCP adapter for autonomous agents
|
|
5
5
|
Author: Bilinc Team
|
|
6
6
|
License-Expression: BUSL-1.1
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.10
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: certifi>=2024.2.2
|
|
24
|
-
Requires-Dist: mcp<2.0,>=1.
|
|
24
|
+
Requires-Dist: mcp<2.0,>=1.10.0
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: build<2.0,>=1.2; extra == "dev"
|
|
27
27
|
Requires-Dist: pytest<9.0,>=7.0; extra == "dev"
|
|
@@ -56,7 +56,7 @@ Dynamic: license-file
|
|
|
56
56
|
|
|
57
57
|
Retrieval answers *"what is similar to this?"*. Long-running agents also need to answer *"who wrote this state, was it verified, did it contradict what we already knew, and can we undo it?"* — that is the layer Bilinc provides.
|
|
58
58
|
|
|
59
|
-
Bilinc 2.3.
|
|
59
|
+
Bilinc 2.3.8 on PyPI is the public cloud-only package: a thin Python SDK, CLI, and MCP adapter for Bilinc Cloud. It does not ship the local StatePlane, storage backends, eval, observability, integrations, or server runtime internals.
|
|
60
60
|
|
|
61
61
|
> **Frozen regression receipt** — LongMemEval-s cleaned retrieval fixture, 500 questions: **Hit@5 98.0%**, **NDCG@5 0.913**, no LLM reranker, no paid API. This is an isolated retrieval guardrail, not a current hosted SLA, end-to-end agent score, or competitor ranking — see [Benchmark receipt](#benchmark-receipt) for the full scope and qualification.
|
|
62
62
|
|
|
@@ -157,7 +157,7 @@ Cloud status check.
|
|
|
157
157
|
To reproduce this release exactly:
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
pip install -U bilinc==2.3.
|
|
160
|
+
pip install -U bilinc==2.3.8
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
If you prefer a browser guide, open https://bilinc.space/install and follow the
|
|
@@ -191,7 +191,7 @@ bilinc mcp install # plain mcpServers JSON for any othe
|
|
|
191
191
|
`command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
|
|
192
192
|
the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
Eleven tools: the eight-step memory lifecycle plus three review tools.
|
|
195
195
|
|
|
196
196
|
| Tool | What it does |
|
|
197
197
|
| --- | --- |
|
|
@@ -203,8 +203,11 @@ Eight tools — the core memory lifecycle, and nothing else:
|
|
|
203
203
|
| `snapshot` | Checkpoint a project before risky work, or list existing checkpoints. |
|
|
204
204
|
| `diff` | Compare a checkpoint against another checkpoint or current state. Values are redacted by default. |
|
|
205
205
|
| `rollback` | **Destructive in execute mode.** Restore a checkpoint through a free preview plus an explicitly confirmed execute. |
|
|
206
|
+
| `list_memories` | Browse what is stored, ordered by key, with prefix, type and updated-time filters. Read-only and free. |
|
|
207
|
+
| `history` | Show one memory's recorded changes, newest first, with the value before and after. Read-only and free. |
|
|
208
|
+
| `confirm` | Record that a memory is still accurate without resending its value. Costs one write. |
|
|
206
209
|
|
|
207
|
-
Operator and debug tooling — health probes, benchmarks,
|
|
210
|
+
Operator and debug tooling — health probes, benchmarks, bulk import, workspace replay — stays
|
|
208
211
|
local-only, as do the epistemic read tools for claims, contradictions, and graph queries. The hosted
|
|
209
212
|
adapter does not bundle local runtime internals.
|
|
210
213
|
|
|
@@ -243,6 +246,24 @@ client.status() # what can this key do?
|
|
|
243
246
|
client.health() # is the service reachable?
|
|
244
247
|
```
|
|
245
248
|
|
|
249
|
+
### Review what Bilinc remembers
|
|
250
|
+
|
|
251
|
+
Listing, history and export are reads and are not billed. Confirming costs one write.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
page = client.list_memories(prefix="agent.", limit=50) # one page, ordered by key
|
|
255
|
+
for entry in client.iter_memories(memory_type="semantic"): # every page
|
|
256
|
+
print(entry["key"], entry["updatedAt"])
|
|
257
|
+
|
|
258
|
+
client.history("agent.goal") # newest changes first (20 by default, up to 100); "truncated" marks older ones
|
|
259
|
+
client.confirm("agent.goal", expected_version=page["entries"][0]["entryVersion"]) # still true
|
|
260
|
+
|
|
261
|
+
backup = client.export() # every stored memory with its full value, as one dict
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Forgetting a memory removes its value from history as well: earlier values of a forgotten
|
|
265
|
+
memory are not returned.
|
|
266
|
+
|
|
246
267
|
For server, CI, and hosted agent runtimes, store the key as `BILINC_API_KEY`.
|
|
247
268
|
|
|
248
269
|
## CLI
|
|
@@ -260,6 +281,16 @@ bilinc forget --key agent.goal --reason "superseded by the planner service"
|
|
|
260
281
|
bilinc doctor
|
|
261
282
|
```
|
|
262
283
|
|
|
284
|
+
Review what is stored:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
bilinc list --prefix agent. # table of keys, types, update times, value previews
|
|
288
|
+
bilinc list --all --json # every page as JSON
|
|
289
|
+
bilinc history agent.goal # recorded changes, newest first
|
|
290
|
+
bilinc confirm agent.goal # still accurate; the value is unchanged
|
|
291
|
+
bilinc export -o bilinc-export.json # full values; the file is readable only by you
|
|
292
|
+
```
|
|
293
|
+
|
|
263
294
|
Rollback is two stages. Execute takes the token from the preview and never prompts interactively,
|
|
264
295
|
so it stays safe inside automation:
|
|
265
296
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
Retrieval answers *"what is similar to this?"*. Long-running agents also need to answer *"who wrote this state, was it verified, did it contradict what we already knew, and can we undo it?"* — that is the layer Bilinc provides.
|
|
16
16
|
|
|
17
|
-
Bilinc 2.3.
|
|
17
|
+
Bilinc 2.3.8 on PyPI is the public cloud-only package: a thin Python SDK, CLI, and MCP adapter for Bilinc Cloud. It does not ship the local StatePlane, storage backends, eval, observability, integrations, or server runtime internals.
|
|
18
18
|
|
|
19
19
|
> **Frozen regression receipt** — LongMemEval-s cleaned retrieval fixture, 500 questions: **Hit@5 98.0%**, **NDCG@5 0.913**, no LLM reranker, no paid API. This is an isolated retrieval guardrail, not a current hosted SLA, end-to-end agent score, or competitor ranking — see [Benchmark receipt](#benchmark-receipt) for the full scope and qualification.
|
|
20
20
|
|
|
@@ -115,7 +115,7 @@ Cloud status check.
|
|
|
115
115
|
To reproduce this release exactly:
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
-
pip install -U bilinc==2.3.
|
|
118
|
+
pip install -U bilinc==2.3.8
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
If you prefer a browser guide, open https://bilinc.space/install and follow the
|
|
@@ -149,7 +149,7 @@ bilinc mcp install # plain mcpServers JSON for any othe
|
|
|
149
149
|
`command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
|
|
150
150
|
the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
Eleven tools: the eight-step memory lifecycle plus three review tools.
|
|
153
153
|
|
|
154
154
|
| Tool | What it does |
|
|
155
155
|
| --- | --- |
|
|
@@ -161,8 +161,11 @@ Eight tools — the core memory lifecycle, and nothing else:
|
|
|
161
161
|
| `snapshot` | Checkpoint a project before risky work, or list existing checkpoints. |
|
|
162
162
|
| `diff` | Compare a checkpoint against another checkpoint or current state. Values are redacted by default. |
|
|
163
163
|
| `rollback` | **Destructive in execute mode.** Restore a checkpoint through a free preview plus an explicitly confirmed execute. |
|
|
164
|
+
| `list_memories` | Browse what is stored, ordered by key, with prefix, type and updated-time filters. Read-only and free. |
|
|
165
|
+
| `history` | Show one memory's recorded changes, newest first, with the value before and after. Read-only and free. |
|
|
166
|
+
| `confirm` | Record that a memory is still accurate without resending its value. Costs one write. |
|
|
164
167
|
|
|
165
|
-
Operator and debug tooling — health probes, benchmarks,
|
|
168
|
+
Operator and debug tooling — health probes, benchmarks, bulk import, workspace replay — stays
|
|
166
169
|
local-only, as do the epistemic read tools for claims, contradictions, and graph queries. The hosted
|
|
167
170
|
adapter does not bundle local runtime internals.
|
|
168
171
|
|
|
@@ -201,6 +204,24 @@ client.status() # what can this key do?
|
|
|
201
204
|
client.health() # is the service reachable?
|
|
202
205
|
```
|
|
203
206
|
|
|
207
|
+
### Review what Bilinc remembers
|
|
208
|
+
|
|
209
|
+
Listing, history and export are reads and are not billed. Confirming costs one write.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
page = client.list_memories(prefix="agent.", limit=50) # one page, ordered by key
|
|
213
|
+
for entry in client.iter_memories(memory_type="semantic"): # every page
|
|
214
|
+
print(entry["key"], entry["updatedAt"])
|
|
215
|
+
|
|
216
|
+
client.history("agent.goal") # newest changes first (20 by default, up to 100); "truncated" marks older ones
|
|
217
|
+
client.confirm("agent.goal", expected_version=page["entries"][0]["entryVersion"]) # still true
|
|
218
|
+
|
|
219
|
+
backup = client.export() # every stored memory with its full value, as one dict
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Forgetting a memory removes its value from history as well: earlier values of a forgotten
|
|
223
|
+
memory are not returned.
|
|
224
|
+
|
|
204
225
|
For server, CI, and hosted agent runtimes, store the key as `BILINC_API_KEY`.
|
|
205
226
|
|
|
206
227
|
## CLI
|
|
@@ -218,6 +239,16 @@ bilinc forget --key agent.goal --reason "superseded by the planner service"
|
|
|
218
239
|
bilinc doctor
|
|
219
240
|
```
|
|
220
241
|
|
|
242
|
+
Review what is stored:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
bilinc list --prefix agent. # table of keys, types, update times, value previews
|
|
246
|
+
bilinc list --all --json # every page as JSON
|
|
247
|
+
bilinc history agent.goal # recorded changes, newest first
|
|
248
|
+
bilinc confirm agent.goal # still accurate; the value is unchanged
|
|
249
|
+
bilinc export -o bilinc-export.json # full values; the file is readable only by you
|
|
250
|
+
```
|
|
251
|
+
|
|
221
252
|
Rollback is two stages. Execute takes the token from the preview and never prompts interactively,
|
|
222
253
|
so it stays safe inside automation:
|
|
223
254
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bilinc"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.8"
|
|
8
8
|
description = "Cloud-only memory SDK and MCP adapter for autonomous agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BUSL-1.1"
|
|
@@ -33,7 +33,7 @@ classifiers = [
|
|
|
33
33
|
|
|
34
34
|
dependencies = [
|
|
35
35
|
"certifi>=2024.2.2",
|
|
36
|
-
"mcp>=1.
|
|
36
|
+
"mcp>=1.10.0,<2.0",
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
[project.urls]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Bilinc 2.3.
|
|
1
|
+
"""Bilinc 2.3.8: cloud-only SDK for agent memory."""
|
|
2
2
|
|
|
3
3
|
from bilinc.client import (
|
|
4
4
|
ACTIVATION_SIGNUP_URL,
|
|
@@ -25,7 +25,7 @@ from bilinc.client import (
|
|
|
25
25
|
CloudClient,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
__version__ = "2.3.
|
|
28
|
+
__version__ = "2.3.8"
|
|
29
29
|
version = __version__
|
|
30
30
|
|
|
31
31
|
__all__ = [
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import contextlib
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
import shlex
|
|
10
11
|
import subprocess
|
|
11
12
|
import sys
|
|
13
|
+
import tempfile
|
|
12
14
|
import time
|
|
13
15
|
import webbrowser
|
|
16
|
+
from datetime import datetime
|
|
14
17
|
from typing import Any
|
|
15
18
|
|
|
16
19
|
from bilinc import __version__
|
|
@@ -18,9 +21,12 @@ from bilinc.client import (
|
|
|
18
21
|
ACTIVATION_SIGNUP_URL,
|
|
19
22
|
BilincApiKeyRequired,
|
|
20
23
|
BilincCloudError,
|
|
24
|
+
BilincValidationError,
|
|
21
25
|
CloudClient,
|
|
22
26
|
INSTALL_URL,
|
|
27
|
+
MEMORY_TYPES,
|
|
23
28
|
SIGNUP_URL,
|
|
29
|
+
VALUE_MODES,
|
|
24
30
|
config_path,
|
|
25
31
|
load_config_api_key,
|
|
26
32
|
save_config_api_key,
|
|
@@ -39,6 +45,125 @@ def _print(payload: dict[str, Any]) -> None:
|
|
|
39
45
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
40
46
|
|
|
41
47
|
|
|
48
|
+
def _short_time(value: Any) -> str:
|
|
49
|
+
"""Render an ISO-8601 timestamp as `YYYY-MM-DD HH:MM:SS` UTC for tables."""
|
|
50
|
+
if not isinstance(value, str) or not value:
|
|
51
|
+
return "-"
|
|
52
|
+
try:
|
|
53
|
+
moment = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
54
|
+
except ValueError:
|
|
55
|
+
return value
|
|
56
|
+
return moment.strftime("%Y-%m-%d %H:%M:%S")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _short_value(value: Any, width: int = 60) -> str:
|
|
60
|
+
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
|
|
61
|
+
text = " ".join(text.split())
|
|
62
|
+
return text if len(text) <= width else text[: width - 1] + "…"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _print_history(payload: dict[str, Any]) -> None:
|
|
66
|
+
key = payload.get("key", "")
|
|
67
|
+
entries = payload.get("entries") or []
|
|
68
|
+
if not entries:
|
|
69
|
+
print(f"No recorded changes for {key}.")
|
|
70
|
+
return
|
|
71
|
+
forgotten = any(entry.get("op") == "forget" for entry in entries[:1])
|
|
72
|
+
state = "" if payload.get("exists", True) else (", forgotten" if forgotten else ", not currently stored")
|
|
73
|
+
print(f"{key} ({len(entries)} shown, newest first{state})")
|
|
74
|
+
for entry in entries:
|
|
75
|
+
op = str(entry.get("op", "?"))
|
|
76
|
+
line = f" {_short_time(entry.get('at'))} {op:<8}"
|
|
77
|
+
if entry.get("valuesRedacted"):
|
|
78
|
+
line += " (value not shown: the memory was forgotten)"
|
|
79
|
+
elif "before" in entry and "after" in entry and op != "confirm":
|
|
80
|
+
line += f" {_short_value(entry['before'], 30)} -> {_short_value(entry['after'], 30)}"
|
|
81
|
+
elif "after" in entry:
|
|
82
|
+
line += f" {_short_value(entry['after'])}"
|
|
83
|
+
if entry.get("reason"):
|
|
84
|
+
line += f" reason: {_short_value(entry['reason'], 40)}"
|
|
85
|
+
if entry.get("source"):
|
|
86
|
+
line += f" source: {entry['source']}"
|
|
87
|
+
print(line)
|
|
88
|
+
if payload.get("truncated"):
|
|
89
|
+
print(" … older changes not shown; raise --limit to see more.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _continuation_command(next_cursor: str, filters: dict[str, Any] | None) -> str:
|
|
93
|
+
"""The `bilinc list` command for the next page, with the same filters.
|
|
94
|
+
|
|
95
|
+
A cursor only marks a position; the filters must be repeated or the next
|
|
96
|
+
page would cover a different set of memories.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
parts = ["bilinc list"]
|
|
100
|
+
flags = {
|
|
101
|
+
"prefix": "--prefix",
|
|
102
|
+
"memory_type": "--type",
|
|
103
|
+
"updated_after": "--updated-after",
|
|
104
|
+
"updated_before": "--updated-before",
|
|
105
|
+
"limit": "--limit",
|
|
106
|
+
}
|
|
107
|
+
for name, flag in flags.items():
|
|
108
|
+
value = (filters or {}).get(name)
|
|
109
|
+
if value is not None and value != "":
|
|
110
|
+
parts.append(f"{flag} {shlex.quote(str(value))}")
|
|
111
|
+
values = (filters or {}).get("values")
|
|
112
|
+
if values and values != "preview":
|
|
113
|
+
parts.append(f"--values {shlex.quote(str(values))}")
|
|
114
|
+
parts.append(f"--cursor {shlex.quote(next_cursor)}")
|
|
115
|
+
return " ".join(parts)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _print_memories(
|
|
119
|
+
entries: list[dict[str, Any]],
|
|
120
|
+
*,
|
|
121
|
+
total: Any,
|
|
122
|
+
next_cursor: Any,
|
|
123
|
+
filters: dict[str, Any] | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
if not entries:
|
|
126
|
+
print("No memories match.")
|
|
127
|
+
return
|
|
128
|
+
key_width = min(max(len(str(entry.get("key", ""))) for entry in entries), 40)
|
|
129
|
+
print(f"{'KEY':<{key_width}} {'TYPE':<10} {'UPDATED (UTC)':<19} VALUE")
|
|
130
|
+
for entry in entries:
|
|
131
|
+
key = _short_value(str(entry.get("key", "")), key_width)
|
|
132
|
+
value = _short_value(entry["value"]) if "value" in entry else ""
|
|
133
|
+
print(
|
|
134
|
+
f"{key:<{key_width}} {str(entry.get('memoryType', '-')):<10} "
|
|
135
|
+
f"{_short_time(entry.get('updatedAt')):<19} {value}"
|
|
136
|
+
)
|
|
137
|
+
summary = f"{len(entries)} shown"
|
|
138
|
+
if isinstance(total, int):
|
|
139
|
+
summary += f" of {total}"
|
|
140
|
+
print(summary)
|
|
141
|
+
if isinstance(next_cursor, str) and next_cursor:
|
|
142
|
+
print(f"More: {_continuation_command(next_cursor, filters)} (or --all)")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _write_private_json(path: str, payload: dict[str, Any]) -> None:
|
|
146
|
+
"""Write an export owner-only: it holds the full memory contents.
|
|
147
|
+
|
|
148
|
+
The content goes to a new temporary file (created 0600) beside the
|
|
149
|
+
target, which then atomically replaces it. An existing file at ``path``
|
|
150
|
+
keeps its old contents until the write has fully succeeded, and the
|
|
151
|
+
result never inherits a wider mode from the file it replaces. This avoids
|
|
152
|
+
``os.fchmod``, which Windows lacks before Python 3.13.
|
|
153
|
+
"""
|
|
154
|
+
directory = os.path.dirname(os.path.abspath(path))
|
|
155
|
+
fd, temporary = tempfile.mkstemp(prefix=".bilinc-export-", suffix=".json", dir=directory)
|
|
156
|
+
try:
|
|
157
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
158
|
+
handle.write(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
159
|
+
handle.write("\n")
|
|
160
|
+
os.replace(temporary, path)
|
|
161
|
+
except BaseException:
|
|
162
|
+
with contextlib.suppress(FileNotFoundError):
|
|
163
|
+
os.unlink(temporary)
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
|
|
42
167
|
def _client(args: argparse.Namespace) -> CloudClient:
|
|
43
168
|
return CloudClient(api_key=args.api_key, base_url=args.base_url, timeout=args.timeout)
|
|
44
169
|
|
|
@@ -165,6 +290,33 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
165
290
|
forget.add_argument("--reason", required=True, help="Required audit reason for the deletion")
|
|
166
291
|
forget.add_argument("--expected-version", help="Fail if the memory changed since this version")
|
|
167
292
|
|
|
293
|
+
confirm = sub.add_parser("confirm", help="Record that an existing memory is still accurate")
|
|
294
|
+
confirm.add_argument("key", help="Memory key")
|
|
295
|
+
confirm.add_argument("--expected-version", help="Fail if the memory changed since this version")
|
|
296
|
+
confirm.add_argument("--reason", help="Optional note recorded in the memory's history")
|
|
297
|
+
|
|
298
|
+
history = sub.add_parser("history", help="Show one memory's recorded changes, newest first")
|
|
299
|
+
history.add_argument("key", help="Memory key")
|
|
300
|
+
history.add_argument("--limit", type=int, default=20)
|
|
301
|
+
history.add_argument("--values", choices=VALUE_MODES, default="full", help="How much of each value to show")
|
|
302
|
+
history.add_argument("--json", action="store_true", help="Print the raw JSON response")
|
|
303
|
+
|
|
304
|
+
list_cmd = sub.add_parser("list", help="List stored memories, ordered by key")
|
|
305
|
+
list_cmd.add_argument("--prefix", help="Only keys starting with this text")
|
|
306
|
+
list_cmd.add_argument("--type", choices=MEMORY_TYPES, help="Only this memory type")
|
|
307
|
+
list_cmd.add_argument("--updated-after", help="Only memories updated after this ISO-8601 time")
|
|
308
|
+
list_cmd.add_argument("--updated-before", help="Only memories updated before this ISO-8601 time")
|
|
309
|
+
list_cmd.add_argument("--limit", type=int, default=50, help="Page size (1-100)")
|
|
310
|
+
list_cmd.add_argument("--cursor", help="Continue from a previous page")
|
|
311
|
+
list_cmd.add_argument("--all", action="store_true", help="Follow every page")
|
|
312
|
+
list_cmd.add_argument("--values", choices=VALUE_MODES, default="preview", help="How much of each value to show")
|
|
313
|
+
list_cmd.add_argument("--json", action="store_true", help="Print the raw JSON response")
|
|
314
|
+
|
|
315
|
+
export = sub.add_parser("export", help="Export every stored memory with its full value as JSON")
|
|
316
|
+
export.add_argument("-o", "--output", help="Write to this file (owner-only) instead of stdout")
|
|
317
|
+
export.add_argument("--prefix", help="Only keys starting with this text")
|
|
318
|
+
export.add_argument("--type", choices=MEMORY_TYPES, help="Only this memory type")
|
|
319
|
+
|
|
168
320
|
snapshot = sub.add_parser("snapshot", help="Create or list project checkpoints")
|
|
169
321
|
snapshot.add_argument("action", choices=["create", "list"], nargs="?", default="create")
|
|
170
322
|
snapshot.add_argument("--label", help="Human-readable label for a new checkpoint")
|
|
@@ -340,6 +492,63 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
340
492
|
expected_version=args.expected_version,
|
|
341
493
|
)
|
|
342
494
|
)
|
|
495
|
+
elif args.command == "confirm":
|
|
496
|
+
_print(
|
|
497
|
+
client.confirm(
|
|
498
|
+
args.key,
|
|
499
|
+
expected_version=args.expected_version,
|
|
500
|
+
reason=args.reason,
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
elif args.command == "history":
|
|
504
|
+
try:
|
|
505
|
+
result = client.history(args.key, limit=args.limit, values=args.values)
|
|
506
|
+
except BilincValidationError as exc:
|
|
507
|
+
if (exc.details or {}).get("reason") != "response_too_large":
|
|
508
|
+
raise
|
|
509
|
+
raise BilincValidationError(
|
|
510
|
+
"This history is too large to return at once. "
|
|
511
|
+
"Try --limit 1, or --values preview.",
|
|
512
|
+
code=exc.code,
|
|
513
|
+
status=exc.status,
|
|
514
|
+
details=exc.details,
|
|
515
|
+
) from exc
|
|
516
|
+
if args.json:
|
|
517
|
+
_print(result)
|
|
518
|
+
else:
|
|
519
|
+
_print_history(result)
|
|
520
|
+
elif args.command == "list":
|
|
521
|
+
filters = {
|
|
522
|
+
"prefix": args.prefix,
|
|
523
|
+
"memory_type": args.type,
|
|
524
|
+
"updated_after": args.updated_after,
|
|
525
|
+
"updated_before": args.updated_before,
|
|
526
|
+
"values": args.values,
|
|
527
|
+
}
|
|
528
|
+
if args.all:
|
|
529
|
+
entries = list(client.iter_memories(page_size=args.limit, cursor=args.cursor, **filters))
|
|
530
|
+
if args.json:
|
|
531
|
+
_print({"entries": entries, "count": len(entries)})
|
|
532
|
+
else:
|
|
533
|
+
_print_memories(entries, total=len(entries), next_cursor=None)
|
|
534
|
+
else:
|
|
535
|
+
result = client.list_memories(cursor=args.cursor, limit=args.limit, **filters)
|
|
536
|
+
if args.json:
|
|
537
|
+
_print(result)
|
|
538
|
+
else:
|
|
539
|
+
_print_memories(
|
|
540
|
+
result.get("entries") or [],
|
|
541
|
+
total=result.get("total"),
|
|
542
|
+
next_cursor=result.get("nextCursor"),
|
|
543
|
+
filters={**filters, "limit": args.limit},
|
|
544
|
+
)
|
|
545
|
+
elif args.command == "export":
|
|
546
|
+
exported = client.export(prefix=args.prefix, memory_type=args.type)
|
|
547
|
+
if args.output:
|
|
548
|
+
_write_private_json(args.output, exported)
|
|
549
|
+
print(f"Exported {exported['count']} memories to {args.output}", file=sys.stderr)
|
|
550
|
+
else:
|
|
551
|
+
_print(exported)
|
|
343
552
|
elif args.command == "snapshot":
|
|
344
553
|
if args.action == "list":
|
|
345
554
|
_print(client.list_snapshots(limit=args.limit))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Bilinc Cloud client.
|
|
2
2
|
|
|
3
|
-
Bilinc 2.3.
|
|
3
|
+
Bilinc 2.3.8 is cloud-only: the PyPI package is a thin SDK and MCP adapter for
|
|
4
4
|
https://bilinc.space. Local self-hosted StatePlane internals are no longer
|
|
5
5
|
shipped in the public package.
|
|
6
6
|
"""
|
|
@@ -15,10 +15,11 @@ import urllib.error
|
|
|
15
15
|
import urllib.parse
|
|
16
16
|
import urllib.request
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
+
from datetime import datetime, timezone
|
|
18
19
|
from pathlib import Path
|
|
19
|
-
from typing import Any, Callable
|
|
20
|
+
from typing import Any, Callable, Iterator
|
|
20
21
|
|
|
21
|
-
__version__ = "2.3.
|
|
22
|
+
__version__ = "2.3.8"
|
|
22
23
|
DEFAULT_BASE_URL = "https://bilinc.space"
|
|
23
24
|
SIGNUP_URL = "https://bilinc.space/signup"
|
|
24
25
|
ACTIVATION_CAMPAIGN = "activation_2_3_4"
|
|
@@ -46,6 +47,7 @@ CANONICAL_ERROR_CODES: dict[str, int] = {
|
|
|
46
47
|
"payment_required": 402,
|
|
47
48
|
"invalid_request": 400,
|
|
48
49
|
"memory_not_found": 404,
|
|
50
|
+
"memory_exists": 409,
|
|
49
51
|
"snapshot_not_found": 404,
|
|
50
52
|
"version_conflict": 409,
|
|
51
53
|
"idempotency_conflict": 409,
|
|
@@ -53,9 +55,13 @@ CANONICAL_ERROR_CODES: dict[str, int] = {
|
|
|
53
55
|
"rollback_confirmation_expired": 410,
|
|
54
56
|
"rate_limited": 429,
|
|
55
57
|
"cloud_runtime_unavailable": 503,
|
|
58
|
+
"capability_unavailable": 503,
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
RETRYABLE_ERROR_CODES = frozenset({"rate_limited", "cloud_runtime_unavailable", "connection_failed"})
|
|
62
|
+
#: Answered with 503, but retrying cannot help: the running service lacks the
|
|
63
|
+
#: feature until it is upgraded.
|
|
64
|
+
NON_RETRYABLE_ERROR_CODES = frozenset({"capability_unavailable"})
|
|
59
65
|
|
|
60
66
|
|
|
61
67
|
class BilincError(RuntimeError):
|
|
@@ -153,7 +159,9 @@ def error_for_response(status: int, payload: Any) -> BilincCloudError:
|
|
|
153
159
|
message = body.get("message") if isinstance(body.get("message"), str) else None
|
|
154
160
|
request_id = body.get("requestId") if isinstance(body.get("requestId"), str) else None
|
|
155
161
|
retryable = body.get("retryable")
|
|
156
|
-
if
|
|
162
|
+
if code in NON_RETRYABLE_ERROR_CODES:
|
|
163
|
+
retryable = False
|
|
164
|
+
elif not isinstance(retryable, bool):
|
|
157
165
|
retryable = status in (429, 503) or status >= 500 or code in RETRYABLE_ERROR_CODES
|
|
158
166
|
|
|
159
167
|
error_class = _STATUS_ERRORS.get(status, BilincCloudError)
|
|
@@ -181,6 +189,12 @@ MAX_REASON_LENGTH = 512
|
|
|
181
189
|
MAX_RECALL_LIMIT = 100
|
|
182
190
|
MAX_SNAPSHOT_LIST_LIMIT = 100
|
|
183
191
|
MAX_DIFF_LIMIT = 500
|
|
192
|
+
MAX_HISTORY_LIMIT = 100
|
|
193
|
+
MAX_LIST_LIMIT = 100
|
|
194
|
+
|
|
195
|
+
#: How much of each stored value history and list return: the whole value, a
|
|
196
|
+
#: short single-string preview, or nothing (keys and metadata only).
|
|
197
|
+
VALUE_MODES = ("full", "preview", "none")
|
|
184
198
|
|
|
185
199
|
#: Bounded retries for read-only calls. Mutations are never retried
|
|
186
200
|
#: automatically: billing idempotency alone does not prove that a retried write
|
|
@@ -279,6 +293,15 @@ def _optional_text(value: Any, field: str) -> str | None:
|
|
|
279
293
|
return value.strip()
|
|
280
294
|
|
|
281
295
|
|
|
296
|
+
def _optional_raw_text(value: Any, field: str) -> str | None:
|
|
297
|
+
"""Like :func:`_optional_text`, but keeps the value exactly as given."""
|
|
298
|
+
if value is None:
|
|
299
|
+
return None
|
|
300
|
+
if not isinstance(value, str) or value == "":
|
|
301
|
+
raise _invalid(field, "must be a non-empty string when provided")
|
|
302
|
+
return value
|
|
303
|
+
|
|
304
|
+
|
|
282
305
|
def _optional_query_timestamp(value: Any) -> str | None:
|
|
283
306
|
"""Validate the bounded public representation of a point-in-time query.
|
|
284
307
|
|
|
@@ -296,6 +319,24 @@ def _optional_query_timestamp(value: Any) -> str | None:
|
|
|
296
319
|
return parsed
|
|
297
320
|
|
|
298
321
|
|
|
322
|
+
def _optional_timestamp(value: Any, field: str) -> str | None:
|
|
323
|
+
"""Accept a datetime or an ISO-8601 string; send ISO-8601.
|
|
324
|
+
|
|
325
|
+
A naive datetime is read as UTC, so the same call means the same instant
|
|
326
|
+
on every machine.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
if value is None:
|
|
330
|
+
return None
|
|
331
|
+
if isinstance(value, datetime):
|
|
332
|
+
moment = value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
333
|
+
return moment.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
334
|
+
parsed = _optional_text(value, field)
|
|
335
|
+
if parsed is not None and len(parsed) > MAX_QUERY_TIMESTAMP_LENGTH:
|
|
336
|
+
raise _invalid(field, f"must be at most {MAX_QUERY_TIMESTAMP_LENGTH} characters")
|
|
337
|
+
return parsed
|
|
338
|
+
|
|
339
|
+
|
|
299
340
|
def _optional_bool(value: Any, field: str) -> bool | None:
|
|
300
341
|
if value is None:
|
|
301
342
|
return None
|
|
@@ -443,6 +484,11 @@ def _default_transport(
|
|
|
443
484
|
) from exc
|
|
444
485
|
|
|
445
486
|
|
|
487
|
+
def _is_response_too_large(error: BilincCloudError) -> bool:
|
|
488
|
+
details = getattr(error, "details", None)
|
|
489
|
+
return isinstance(details, dict) and details.get("reason") == "response_too_large"
|
|
490
|
+
|
|
491
|
+
|
|
446
492
|
@dataclass(slots=True)
|
|
447
493
|
class CloudClient:
|
|
448
494
|
"""Minimal Bilinc Cloud SDK client."""
|
|
@@ -598,6 +644,153 @@ class CloudClient:
|
|
|
598
644
|
idempotency_key=_optional_text(idempotency_key, "idempotency_key"),
|
|
599
645
|
)
|
|
600
646
|
|
|
647
|
+
def confirm(
|
|
648
|
+
self,
|
|
649
|
+
key: str,
|
|
650
|
+
*,
|
|
651
|
+
expected_version: str | None = None,
|
|
652
|
+
reason: str | None = None,
|
|
653
|
+
idempotency_key: str | None = None,
|
|
654
|
+
) -> dict[str, Any]:
|
|
655
|
+
"""Record that an existing memory is still accurate.
|
|
656
|
+
|
|
657
|
+
The value stays exactly as stored; only its last-updated time moves to
|
|
658
|
+
now, and history records the step as a confirmation rather than an
|
|
659
|
+
edit. Costs one write operation. Pass ``expected_version`` to get
|
|
660
|
+
``version_conflict`` if the memory changed since you read it.
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
payload: dict[str, Any] = {"key": _require_key(key)}
|
|
664
|
+
_put_optional(payload, "expectedVersion", _optional_text(expected_version, "expected_version"))
|
|
665
|
+
_put_optional(payload, "reason", _optional_text(reason, "reason"))
|
|
666
|
+
|
|
667
|
+
return self._post(
|
|
668
|
+
"/api/cloud/memory/confirm",
|
|
669
|
+
payload,
|
|
670
|
+
idempotency_key=_optional_text(idempotency_key, "idempotency_key"),
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def history(self, key: str, *, limit: int = 20, values: str = "full") -> dict[str, Any]:
|
|
674
|
+
"""Return one memory's recorded changes, newest first.
|
|
675
|
+
|
|
676
|
+
Read-only and not billed. Each entry carries the operation, when it
|
|
677
|
+
happened, the recorded reason and source, and (unless ``values`` is
|
|
678
|
+
``"none"``) the value before and after. Values recorded before the
|
|
679
|
+
memory was forgotten are never returned.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
return self._read(
|
|
683
|
+
"/api/cloud/memory/history",
|
|
684
|
+
{
|
|
685
|
+
"key": _require_key(key),
|
|
686
|
+
"limit": _require_limit(limit, "limit", MAX_HISTORY_LIMIT),
|
|
687
|
+
"values": _require_choice(values, "values", VALUE_MODES),
|
|
688
|
+
},
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
def list_memories(
|
|
692
|
+
self,
|
|
693
|
+
*,
|
|
694
|
+
prefix: str | None = None,
|
|
695
|
+
memory_type: str | None = None,
|
|
696
|
+
updated_after: datetime | str | None = None,
|
|
697
|
+
updated_before: datetime | str | None = None,
|
|
698
|
+
cursor: str | None = None,
|
|
699
|
+
limit: int = 50,
|
|
700
|
+
values: str = "preview",
|
|
701
|
+
) -> dict[str, Any]:
|
|
702
|
+
"""Return one page of stored memories, ordered by key.
|
|
703
|
+
|
|
704
|
+
Read-only and not billed. ``total`` counts every memory matching the
|
|
705
|
+
filters; pass ``nextCursor`` back as ``cursor`` for the next page, or
|
|
706
|
+
use :meth:`iter_memories` to walk every page.
|
|
707
|
+
"""
|
|
708
|
+
|
|
709
|
+
payload: dict[str, Any] = {
|
|
710
|
+
"limit": _require_limit(limit, "limit", MAX_LIST_LIMIT),
|
|
711
|
+
"values": _require_choice(values, "values", VALUE_MODES),
|
|
712
|
+
}
|
|
713
|
+
# Not stripped: a trailing space is part of the prefix ("note " is not "note").
|
|
714
|
+
_put_optional(payload, "prefix", _optional_raw_text(prefix, "prefix"))
|
|
715
|
+
if memory_type is not None:
|
|
716
|
+
payload["memoryType"] = _require_memory_type(memory_type)
|
|
717
|
+
_put_optional(payload, "updatedAfter", _optional_timestamp(updated_after, "updated_after"))
|
|
718
|
+
_put_optional(payload, "updatedBefore", _optional_timestamp(updated_before, "updated_before"))
|
|
719
|
+
_put_optional(payload, "cursor", _optional_text(cursor, "cursor"))
|
|
720
|
+
|
|
721
|
+
return self._read("/api/cloud/memory/list", payload)
|
|
722
|
+
|
|
723
|
+
def iter_memories(
|
|
724
|
+
self,
|
|
725
|
+
*,
|
|
726
|
+
prefix: str | None = None,
|
|
727
|
+
memory_type: str | None = None,
|
|
728
|
+
updated_after: datetime | str | None = None,
|
|
729
|
+
updated_before: datetime | str | None = None,
|
|
730
|
+
page_size: int = MAX_LIST_LIMIT,
|
|
731
|
+
values: str = "preview",
|
|
732
|
+
cursor: str | None = None,
|
|
733
|
+
) -> Iterator[dict[str, Any]]:
|
|
734
|
+
"""Yield every stored memory matching the filters, page by page.
|
|
735
|
+
|
|
736
|
+
Starts after ``cursor`` when one is given. With ``values="full"`` a
|
|
737
|
+
page too large for one response is fetched again in smaller pages,
|
|
738
|
+
and the walk steps back up one size after each page that fits; Bilinc
|
|
739
|
+
Cloud always returns a single entry whole, so it never fails on size.
|
|
740
|
+
"""
|
|
741
|
+
|
|
742
|
+
sizes = sorted({page_size, min(page_size, 10), 1}, reverse=True)
|
|
743
|
+
size_index = 0
|
|
744
|
+
seen: set[str] = set()
|
|
745
|
+
while True:
|
|
746
|
+
try:
|
|
747
|
+
page = self.list_memories(
|
|
748
|
+
prefix=prefix,
|
|
749
|
+
memory_type=memory_type,
|
|
750
|
+
updated_after=updated_after,
|
|
751
|
+
updated_before=updated_before,
|
|
752
|
+
cursor=cursor,
|
|
753
|
+
limit=sizes[size_index],
|
|
754
|
+
values=values,
|
|
755
|
+
)
|
|
756
|
+
except BilincValidationError as exc:
|
|
757
|
+
if not _is_response_too_large(exc) or size_index == len(sizes) - 1:
|
|
758
|
+
raise
|
|
759
|
+
size_index += 1
|
|
760
|
+
continue
|
|
761
|
+
# Step back up after a page that fit, so one large value does not
|
|
762
|
+
# leave the rest of the walk at one entry per request.
|
|
763
|
+
size_index = max(0, size_index - 1)
|
|
764
|
+
entries = page.get("entries")
|
|
765
|
+
yield from entries if isinstance(entries, list) else []
|
|
766
|
+
next_cursor = page.get("nextCursor")
|
|
767
|
+
if not isinstance(next_cursor, str) or not next_cursor:
|
|
768
|
+
return
|
|
769
|
+
if next_cursor in seen:
|
|
770
|
+
raise BilincCloudError(
|
|
771
|
+
"Bilinc Cloud returned a repeated list cursor",
|
|
772
|
+
code="invalid_response",
|
|
773
|
+
)
|
|
774
|
+
seen.add(next_cursor)
|
|
775
|
+
cursor = next_cursor
|
|
776
|
+
|
|
777
|
+
def export(
|
|
778
|
+
self,
|
|
779
|
+
*,
|
|
780
|
+
prefix: str | None = None,
|
|
781
|
+
memory_type: str | None = None,
|
|
782
|
+
) -> dict[str, Any]:
|
|
783
|
+
"""Return every stored memory with its full value, as one JSON-ready dict.
|
|
784
|
+
|
|
785
|
+
Pages through :meth:`iter_memories`; reads are not billed. Every
|
|
786
|
+
exported memory carries its full value. History is not included; use
|
|
787
|
+
:meth:`history` per key for that.
|
|
788
|
+
"""
|
|
789
|
+
|
|
790
|
+
exported_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
791
|
+
entries = list(self.iter_memories(prefix=prefix, memory_type=memory_type, values="full"))
|
|
792
|
+
return {"exported_at": exported_at, "count": len(entries), "entries": entries}
|
|
793
|
+
|
|
601
794
|
def snapshot(
|
|
602
795
|
self,
|
|
603
796
|
action: str = "create",
|
|
@@ -834,6 +1027,8 @@ __all__ = [
|
|
|
834
1027
|
"INSTALL_URL",
|
|
835
1028
|
"load_config_api_key",
|
|
836
1029
|
"MAX_DIFF_LIMIT",
|
|
1030
|
+
"MAX_HISTORY_LIMIT",
|
|
1031
|
+
"MAX_LIST_LIMIT",
|
|
837
1032
|
"MAX_RECALL_LIMIT",
|
|
838
1033
|
"MAX_SNAPSHOT_LIST_LIMIT",
|
|
839
1034
|
"MEMORY_TYPES",
|
|
@@ -842,4 +1037,5 @@ __all__ = [
|
|
|
842
1037
|
"REVISION_STRATEGIES",
|
|
843
1038
|
"save_config_api_key",
|
|
844
1039
|
"SIGNUP_URL",
|
|
1040
|
+
"VALUE_MODES",
|
|
845
1041
|
]
|
|
@@ -9,7 +9,9 @@ Requires BILINC_API_KEY at call time, not at import time.
|
|
|
9
9
|
This adapter exposes the eight core memory-lifecycle capabilities of hosted
|
|
10
10
|
Bilinc — write, recall, deliberately revise, deliberately forget, checkpoint,
|
|
11
11
|
inspect a change, restore a known-good state, and inspect runtime status —
|
|
12
|
-
|
|
12
|
+
plus three review tools (list what is stored, show one memory's history, and
|
|
13
|
+
confirm a memory is still accurate), without bundling local StatePlane or
|
|
14
|
+
storage internals.
|
|
13
15
|
|
|
14
16
|
Operator/debug tooling (health, benchmark, export/import, workspace replay) and
|
|
15
17
|
the epistemic read tools (verify, claims, contradictions, graph queries) stay
|
|
@@ -33,6 +35,9 @@ CLOUD_MCP_TOOLS = (
|
|
|
33
35
|
"snapshot",
|
|
34
36
|
"diff",
|
|
35
37
|
"rollback",
|
|
38
|
+
"list_memories",
|
|
39
|
+
"history",
|
|
40
|
+
"confirm",
|
|
36
41
|
)
|
|
37
42
|
|
|
38
43
|
|
|
@@ -50,6 +55,8 @@ def build_server():
|
|
|
50
55
|
except Exception as exc: # pragma: no cover - depends on optional MCP runtime import path
|
|
51
56
|
raise RuntimeError("Bilinc Cloud MCP adapter requires mcp>=1.0.0") from exc
|
|
52
57
|
|
|
58
|
+
from mcp.types import ToolAnnotations
|
|
59
|
+
|
|
53
60
|
mcp = FastMCP("bilinc")
|
|
54
61
|
|
|
55
62
|
@mcp.tool()
|
|
@@ -265,6 +272,98 @@ def build_server():
|
|
|
265
272
|
)
|
|
266
273
|
raise ValueError("mode must be either preview or execute")
|
|
267
274
|
|
|
275
|
+
@mcp.tool(
|
|
276
|
+
title="List stored memories",
|
|
277
|
+
annotations=ToolAnnotations(
|
|
278
|
+
readOnlyHint=True,
|
|
279
|
+
destructiveHint=False,
|
|
280
|
+
idempotentHint=True,
|
|
281
|
+
openWorldHint=False,
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
def list_memories(
|
|
285
|
+
prefix: str | None = None,
|
|
286
|
+
memory_type: str | None = None,
|
|
287
|
+
updated_after: str | None = None,
|
|
288
|
+
updated_before: str | None = None,
|
|
289
|
+
cursor: str | None = None,
|
|
290
|
+
limit: int = 50,
|
|
291
|
+
values: str = "preview",
|
|
292
|
+
) -> dict[str, Any]:
|
|
293
|
+
"""List the memories stored in this Bilinc Cloud project, ordered by key.
|
|
294
|
+
|
|
295
|
+
Read-only and free. Filters: key `prefix`, `memory_type`, and ISO-8601
|
|
296
|
+
`updated_after` / `updated_before`. Returns up to `limit` entries
|
|
297
|
+
(1-100) with key, type, importance, source, created and updated times,
|
|
298
|
+
entry version, and a value whose size follows `values` ("preview" by
|
|
299
|
+
default, "full", or "none"). `total` counts all matches; `nextCursor`
|
|
300
|
+
is the `cursor` for the next page and is null on the last page.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
return create_client().list_memories(
|
|
304
|
+
prefix=prefix,
|
|
305
|
+
memory_type=memory_type,
|
|
306
|
+
updated_after=updated_after,
|
|
307
|
+
updated_before=updated_before,
|
|
308
|
+
cursor=cursor,
|
|
309
|
+
limit=limit,
|
|
310
|
+
values=values,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
@mcp.tool(
|
|
314
|
+
title="Show a memory's history",
|
|
315
|
+
annotations=ToolAnnotations(
|
|
316
|
+
readOnlyHint=True,
|
|
317
|
+
destructiveHint=False,
|
|
318
|
+
idempotentHint=True,
|
|
319
|
+
openWorldHint=False,
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
def history(key: str, limit: int = 20, values: str = "full") -> dict[str, Any]:
|
|
323
|
+
"""Show the recorded changes to one memory key, newest first.
|
|
324
|
+
|
|
325
|
+
Read-only and free. Each entry has the operation (create, update,
|
|
326
|
+
confirm, forget or delete), its time, the recorded reason and source,
|
|
327
|
+
and the value before and after unless `values` is "none". A change
|
|
328
|
+
made by a rollback keeps its create, update or delete operation and
|
|
329
|
+
also carries `rollbackSnapshotId`.
|
|
330
|
+
Values recorded before a memory was forgotten are not returned.
|
|
331
|
+
`exists` is false when the key is not currently stored.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
return create_client().history(key, limit=limit, values=values)
|
|
335
|
+
|
|
336
|
+
@mcp.tool(
|
|
337
|
+
title="Confirm a memory is still accurate",
|
|
338
|
+
annotations=ToolAnnotations(
|
|
339
|
+
readOnlyHint=False,
|
|
340
|
+
destructiveHint=False,
|
|
341
|
+
# Each call adds a history entry, so a repeat is not a no-op.
|
|
342
|
+
idempotentHint=False,
|
|
343
|
+
openWorldHint=False,
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
def confirm(
|
|
347
|
+
key: str,
|
|
348
|
+
expected_version: str | None = None,
|
|
349
|
+
reason: str | None = None,
|
|
350
|
+
idempotency_key: str | None = None,
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
"""Record that an existing memory is still accurate.
|
|
353
|
+
|
|
354
|
+
The stored value is not changed; the memory's updated time moves to now
|
|
355
|
+
and its history gains a confirm entry. Costs one write operation.
|
|
356
|
+
Fails with `memory_not_found` when the key does not exist, and with
|
|
357
|
+
`version_conflict` when `expected_version` no longer matches.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
return create_client().confirm(
|
|
361
|
+
key,
|
|
362
|
+
expected_version=expected_version,
|
|
363
|
+
reason=reason,
|
|
364
|
+
idempotency_key=idempotency_key,
|
|
365
|
+
)
|
|
366
|
+
|
|
268
367
|
return mcp
|
|
269
368
|
|
|
270
369
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bilinc
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.8
|
|
4
4
|
Summary: Cloud-only memory SDK and MCP adapter for autonomous agents
|
|
5
5
|
Author: Bilinc Team
|
|
6
6
|
License-Expression: BUSL-1.1
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.10
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: certifi>=2024.2.2
|
|
24
|
-
Requires-Dist: mcp<2.0,>=1.
|
|
24
|
+
Requires-Dist: mcp<2.0,>=1.10.0
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: build<2.0,>=1.2; extra == "dev"
|
|
27
27
|
Requires-Dist: pytest<9.0,>=7.0; extra == "dev"
|
|
@@ -56,7 +56,7 @@ Dynamic: license-file
|
|
|
56
56
|
|
|
57
57
|
Retrieval answers *"what is similar to this?"*. Long-running agents also need to answer *"who wrote this state, was it verified, did it contradict what we already knew, and can we undo it?"* — that is the layer Bilinc provides.
|
|
58
58
|
|
|
59
|
-
Bilinc 2.3.
|
|
59
|
+
Bilinc 2.3.8 on PyPI is the public cloud-only package: a thin Python SDK, CLI, and MCP adapter for Bilinc Cloud. It does not ship the local StatePlane, storage backends, eval, observability, integrations, or server runtime internals.
|
|
60
60
|
|
|
61
61
|
> **Frozen regression receipt** — LongMemEval-s cleaned retrieval fixture, 500 questions: **Hit@5 98.0%**, **NDCG@5 0.913**, no LLM reranker, no paid API. This is an isolated retrieval guardrail, not a current hosted SLA, end-to-end agent score, or competitor ranking — see [Benchmark receipt](#benchmark-receipt) for the full scope and qualification.
|
|
62
62
|
|
|
@@ -157,7 +157,7 @@ Cloud status check.
|
|
|
157
157
|
To reproduce this release exactly:
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
pip install -U bilinc==2.3.
|
|
160
|
+
pip install -U bilinc==2.3.8
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
If you prefer a browser guide, open https://bilinc.space/install and follow the
|
|
@@ -191,7 +191,7 @@ bilinc mcp install # plain mcpServers JSON for any othe
|
|
|
191
191
|
`command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
|
|
192
192
|
the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
Eleven tools: the eight-step memory lifecycle plus three review tools.
|
|
195
195
|
|
|
196
196
|
| Tool | What it does |
|
|
197
197
|
| --- | --- |
|
|
@@ -203,8 +203,11 @@ Eight tools — the core memory lifecycle, and nothing else:
|
|
|
203
203
|
| `snapshot` | Checkpoint a project before risky work, or list existing checkpoints. |
|
|
204
204
|
| `diff` | Compare a checkpoint against another checkpoint or current state. Values are redacted by default. |
|
|
205
205
|
| `rollback` | **Destructive in execute mode.** Restore a checkpoint through a free preview plus an explicitly confirmed execute. |
|
|
206
|
+
| `list_memories` | Browse what is stored, ordered by key, with prefix, type and updated-time filters. Read-only and free. |
|
|
207
|
+
| `history` | Show one memory's recorded changes, newest first, with the value before and after. Read-only and free. |
|
|
208
|
+
| `confirm` | Record that a memory is still accurate without resending its value. Costs one write. |
|
|
206
209
|
|
|
207
|
-
Operator and debug tooling — health probes, benchmarks,
|
|
210
|
+
Operator and debug tooling — health probes, benchmarks, bulk import, workspace replay — stays
|
|
208
211
|
local-only, as do the epistemic read tools for claims, contradictions, and graph queries. The hosted
|
|
209
212
|
adapter does not bundle local runtime internals.
|
|
210
213
|
|
|
@@ -243,6 +246,24 @@ client.status() # what can this key do?
|
|
|
243
246
|
client.health() # is the service reachable?
|
|
244
247
|
```
|
|
245
248
|
|
|
249
|
+
### Review what Bilinc remembers
|
|
250
|
+
|
|
251
|
+
Listing, history and export are reads and are not billed. Confirming costs one write.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
page = client.list_memories(prefix="agent.", limit=50) # one page, ordered by key
|
|
255
|
+
for entry in client.iter_memories(memory_type="semantic"): # every page
|
|
256
|
+
print(entry["key"], entry["updatedAt"])
|
|
257
|
+
|
|
258
|
+
client.history("agent.goal") # newest changes first (20 by default, up to 100); "truncated" marks older ones
|
|
259
|
+
client.confirm("agent.goal", expected_version=page["entries"][0]["entryVersion"]) # still true
|
|
260
|
+
|
|
261
|
+
backup = client.export() # every stored memory with its full value, as one dict
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Forgetting a memory removes its value from history as well: earlier values of a forgotten
|
|
265
|
+
memory are not returned.
|
|
266
|
+
|
|
246
267
|
For server, CI, and hosted agent runtimes, store the key as `BILINC_API_KEY`.
|
|
247
268
|
|
|
248
269
|
## CLI
|
|
@@ -260,6 +281,16 @@ bilinc forget --key agent.goal --reason "superseded by the planner service"
|
|
|
260
281
|
bilinc doctor
|
|
261
282
|
```
|
|
262
283
|
|
|
284
|
+
Review what is stored:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
bilinc list --prefix agent. # table of keys, types, update times, value previews
|
|
288
|
+
bilinc list --all --json # every page as JSON
|
|
289
|
+
bilinc history agent.goal # recorded changes, newest first
|
|
290
|
+
bilinc confirm agent.goal # still accurate; the value is unchanged
|
|
291
|
+
bilinc export -o bilinc-export.json # full values; the file is readable only by you
|
|
292
|
+
```
|
|
293
|
+
|
|
263
294
|
Rollback is two stages. Execute takes the token from the preview and never prompts interactively,
|
|
264
295
|
so it stays safe inside automation:
|
|
265
296
|
|
|
@@ -12,8 +12,8 @@ import pytest
|
|
|
12
12
|
def test_public_api_is_cloud_only():
|
|
13
13
|
import bilinc
|
|
14
14
|
|
|
15
|
-
assert bilinc.__version__ == "2.3.
|
|
16
|
-
assert bilinc.version == "2.3.
|
|
15
|
+
assert bilinc.__version__ == "2.3.8"
|
|
16
|
+
assert bilinc.version == "2.3.8"
|
|
17
17
|
assert "utm_campaign=activation_2_3_4" in bilinc.ACTIVATION_SIGNUP_URL
|
|
18
18
|
assert hasattr(bilinc, "Bilinc")
|
|
19
19
|
assert hasattr(bilinc, "CloudClient")
|
|
@@ -32,7 +32,7 @@ def test_declared_wheel_packages_stay_cloud_only():
|
|
|
32
32
|
pyproject = tomllib.load(handle)
|
|
33
33
|
|
|
34
34
|
assert pyproject["tool"]["setuptools"]["packages"] == ["bilinc", "bilinc.cli"]
|
|
35
|
-
assert set(pyproject["project"]["dependencies"]) == {"certifi>=2024.2.2", "mcp>=1.
|
|
35
|
+
assert set(pyproject["project"]["dependencies"]) == {"certifi>=2024.2.2", "mcp>=1.10.0,<2.0"}
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class RecordingTransport:
|
|
@@ -110,9 +110,9 @@ def test_cloud_client_commit_posts_to_hosted_api():
|
|
|
110
110
|
"headers": {
|
|
111
111
|
"Authorization": "Bearer bil_live_example",
|
|
112
112
|
"Content-Type": "application/json",
|
|
113
|
-
"User-Agent": "bilinc-python/2.3.
|
|
113
|
+
"User-Agent": "bilinc-python/2.3.8",
|
|
114
114
|
"X-Bilinc-Client": "python-sdk",
|
|
115
|
-
"X-Bilinc-Client-Version": "2.3.
|
|
115
|
+
"X-Bilinc-Client-Version": "2.3.8",
|
|
116
116
|
},
|
|
117
117
|
"body": json.dumps(
|
|
118
118
|
{
|
|
@@ -188,7 +188,7 @@ def test_cloud_client_status_reads_the_authenticated_status_endpoint():
|
|
|
188
188
|
"url": "https://bilinc.space/api/cloud/status",
|
|
189
189
|
"headers": {
|
|
190
190
|
"Authorization": "Bearer bil_live_example",
|
|
191
|
-
"User-Agent": "bilinc-python/2.3.
|
|
191
|
+
"User-Agent": "bilinc-python/2.3.8",
|
|
192
192
|
},
|
|
193
193
|
"body": None,
|
|
194
194
|
"timeout": 30.0,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|