keb-consol 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.
- keb_consol-0.1.0/.gitignore +26 -0
- keb_consol-0.1.0/LICENSE +21 -0
- keb_consol-0.1.0/PKG-INFO +203 -0
- keb_consol-0.1.0/README.md +193 -0
- keb_consol-0.1.0/pyproject.toml +15 -0
- keb_consol-0.1.0/src/consol/__init__.py +18 -0
- keb_consol-0.1.0/src/consol/client.py +755 -0
- keb_consol-0.1.0/src/consol/py.typed +1 -0
- keb_consol-0.1.0/src/consol/types.py +310 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# build output
|
|
2
|
+
/target/
|
|
3
|
+
node_modules/
|
|
4
|
+
dist/
|
|
5
|
+
*.tgz
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.pyc
|
|
8
|
+
.venv/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
|
|
11
|
+
# secrets and local state: never commit
|
|
12
|
+
.env
|
|
13
|
+
.env.*
|
|
14
|
+
*.sqlite
|
|
15
|
+
*.sqlite-shm
|
|
16
|
+
*.sqlite-wal
|
|
17
|
+
.consol.lock
|
|
18
|
+
/vault/
|
|
19
|
+
/models/
|
|
20
|
+
|
|
21
|
+
# examples: own cargo target, default vault root
|
|
22
|
+
/examples/axum/target/
|
|
23
|
+
/examples/*/vaults/
|
|
24
|
+
/examples/axum/Cargo.lock
|
|
25
|
+
/examples/*/bun.lock
|
|
26
|
+
/examples/*/uv.lock
|
keb_consol-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kryat, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: keb-consol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the consol memory engine: a local vault, a hosted client, and the backend API
|
|
5
|
+
Author: Khas
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# keb-consol
|
|
12
|
+
|
|
13
|
+
Python SDK for the consol memory engine. Distribution `keb-consol`, import name `consol`; Python
|
|
14
|
+
>= 3.10, no dependencies, synchronous and thread-safe, typed payloads (`TypedDict`s, `py.typed`).
|
|
15
|
+
Three uses, one behaviour: a vault in your process, a client of a hosted backend, or your own
|
|
16
|
+
backend. Not published yet; install from a checkout with `uv pip install ./bindings/py`.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- **The `consol` binary.** The wheel does not bundle it. The SDK uses the first of: the
|
|
21
|
+
`binary_path` argument, the `CONSOL_BIN` environment variable, `consol` (`consol.exe`) on `PATH`.
|
|
22
|
+
An explicit path that points nowhere fails at once with `binary_not_found`.
|
|
23
|
+
- **A database** for local and backend use: PostgreSQL with pgvector, or CockroachDB. Pass
|
|
24
|
+
`db='postgres://...'` or set `CONSOL_DB_URL` (the one `CONSOL_*` variable the child inherits).
|
|
25
|
+
|
|
26
|
+
## 1. Local: a vault in your process
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from consol import create_vault
|
|
30
|
+
|
|
31
|
+
with create_vault('./memory', agent='assistant') as vault:
|
|
32
|
+
vault.remember(id='deploy-day', content='Deploys run on Fridays.')
|
|
33
|
+
hits = vault.recall(query='deploys run')
|
|
34
|
+
assert 'deploy-day' in [card['source']['id'] for card in hits['candidates']]
|
|
35
|
+
print(vault.read(id='deploy-day')['text']) # the complete Markdown file, frontmatter included
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`create_vault` starts `consol stdio --vault ./memory` and creates the vault (`vault_exists` if there
|
|
39
|
+
is one); `open_vault('./memory', agent=...)` opens an existing one (`vault_not_found`). Both check
|
|
40
|
+
the server version against the SDK's before they return. For an LLM tool loop, `call_tool(name,
|
|
41
|
+
args)` returns the whole reply and never raises for a tool error: send `reply['text']` back to the
|
|
42
|
+
model. `consol schema` prints the JSON Schema of the wire.
|
|
43
|
+
|
|
44
|
+
## 2. Hosted: a client of a backend
|
|
45
|
+
|
|
46
|
+
```py
|
|
47
|
+
from consol import open_vault
|
|
48
|
+
|
|
49
|
+
with open_vault(url='https://memory.example.com/v1/consol',
|
|
50
|
+
headers={'Authorization': 'Bearer <your key>'}, # whatever your backend checks
|
|
51
|
+
vault='acme', agent='alice') as vault:
|
|
52
|
+
vault.remember(id='deploy-day', content='Deploys run on Fridays.')
|
|
53
|
+
print(vault.recall(query='deploys run')['candidates'][0]['source']['id'])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The same methods over HTTP: each call POSTs `{"tool", "vault", "agent", "args"}` with your headers.
|
|
57
|
+
There is no create: backends create vaults. `vault` and `agent` may be left out when the backend
|
|
58
|
+
pins them. Redirects are never followed (they would carry your credentials). Transport failures are
|
|
59
|
+
`request_failed` (`status`; retryable for connect errors and 502/503/504) and `forbidden` for a 401
|
|
60
|
+
or 403 that is not a consol reply; a consol error body is a normal tool error whatever its status.
|
|
61
|
+
|
|
62
|
+
## 3. Backend: your own endpoint
|
|
63
|
+
|
|
64
|
+
```py
|
|
65
|
+
import asyncio
|
|
66
|
+
from consol import Consol, Context, http_status
|
|
67
|
+
from fastapi import FastAPI, Header, HTTPException, Request
|
|
68
|
+
from fastapi.responses import JSONResponse
|
|
69
|
+
|
|
70
|
+
consol = Consol.start(vault_root='./vaults') # or vault='./one-vault'; db= or CONSOL_DB_URL
|
|
71
|
+
app = FastAPI()
|
|
72
|
+
USERS = {'alice-token': ('acme', 'alice')} # YOUR auth: token -> (vault, agent)
|
|
73
|
+
|
|
74
|
+
@app.post('/v1/consol')
|
|
75
|
+
async def wire(request: Request, authorization: str = Header('')):
|
|
76
|
+
user = USERS.get(authorization.removeprefix('Bearer '))
|
|
77
|
+
if user is None:
|
|
78
|
+
raise HTTPException(401, 'unauthorized')
|
|
79
|
+
ctx = Context(vaults=[user[0]], agent=user[1]) # from your auth, NEVER from the request
|
|
80
|
+
reply = await asyncio.to_thread(consol.handle, await request.json(), ctx) # handle is sync
|
|
81
|
+
return JSONResponse(reply, status_code=http_status(reply))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`ctx` decides which vaults and which agent a request may use: build it from your own authentication,
|
|
85
|
+
never from the body or headers a client controls (`{'vaults': None, ...}` means every vault; write it
|
|
86
|
+
deliberately). `handle(body, ctx)` returns the reply dict as is (`version`, `text`, `result` or
|
|
87
|
+
`error`); a tool error is in the reply, never raised, and `http_status(reply)` is the status to
|
|
88
|
+
answer with. One child serves every thread. Creating a vault is the backend's job, never the wire's:
|
|
89
|
+
`consol.create_vault('acme')` (`vault_exists`), `consol.open_vault('acme')` (`vault_not_found`).
|
|
90
|
+
`Consol.start` takes `vault_root` (a directory or `s3://bucket/prefix`; vault `x` is `<root>/x`) or
|
|
91
|
+
`vault` (one vault, named `default`), plus `db`, `embedding`, `jev={'url', 'api_key', 'model',
|
|
92
|
+
'only', 'auto'}`, `binary_path`, `env` and `timeout`.
|
|
93
|
+
|
|
94
|
+
## Methods
|
|
95
|
+
|
|
96
|
+
Keyword-only arguments with the tool's wire names, plus `timeout=` in seconds; they return the
|
|
97
|
+
result payload as a plain dict and raise `ConsolError` on a tool error.
|
|
98
|
+
|
|
99
|
+
| Method | Arguments | Returns |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `recall` | `query`, `include_history`, `limit`, `agent` | `RecallResult`: `text` is the progressive disclosure to give a model (cards by tier, intent block, hints); `candidates` are the same cards as data. Cards are leads: read a note before relying on it |
|
|
102
|
+
| `read` | `id`, `continuation`, `agent` | `ReadResult`; `text` is the exact file |
|
|
103
|
+
| `remember` | `id`, `content`, `expected_version`, `summary`, `entities`, `facets`, `scope`, `team`, `valid_from`, `valid_to`, `reflection`, `agent` | `RememberResult` |
|
|
104
|
+
| `remove` | `id`, `expected_version`, `reflection`, `agent` | `RemoveResult`: the note is archived, its body stays readable |
|
|
105
|
+
| `reflect` | one mode: none (discovery), `ids`, `work_ids` or `decisions`; `limit`, `agent` | `ReflectResult`; delivered sources carry their `text` |
|
|
106
|
+
| `call_tool` | `(name, args)` with raw wire arguments | the whole `Reply`; a tool error is a reply with `error` |
|
|
107
|
+
| `close` | | final and idempotent |
|
|
108
|
+
|
|
109
|
+
- A call's `agent` goes into the envelope and overrides the vault's default.
|
|
110
|
+
- `remember` without `expected_version` is create-only. To update, pass `source.content_version`
|
|
111
|
+
from your last `read`; a stale one fails with `version_conflict`. `None` keeps a stored value,
|
|
112
|
+
`''` or `[]` clears it. `remember` and `remove` also take `resume=True` with `reflection`
|
|
113
|
+
(`{'work_id', 'basis_hash', 'action_key'}`) to resume a pending reflection-bound write.
|
|
114
|
+
- `byte_range` values are UTF-8 byte offsets: `slice_utf8(text, byte_range)` slices a `str` by them.
|
|
115
|
+
|
|
116
|
+
## Loading many notes
|
|
117
|
+
|
|
118
|
+
Each `remember` is independent, and the vault is safe to call from several threads, so load a
|
|
119
|
+
corpus in parallel: with a hosted embedding provider most of a write is waiting on it, and
|
|
120
|
+
parallel writes overlap that wait (8 writers loaded 3.8x faster than one in a measured run).
|
|
121
|
+
|
|
122
|
+
```py
|
|
123
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
124
|
+
|
|
125
|
+
with ThreadPoolExecutor(8) as pool:
|
|
126
|
+
list(pool.map(lambda doc: vault.remember(**doc), docs)) # docs: [{"id": ..., "content": ...}]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Errors
|
|
130
|
+
|
|
131
|
+
Every failure is a `ConsolError`. Branch on `code` and `retryable`, never on the message:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from consol import ConsolError, open_vault
|
|
135
|
+
|
|
136
|
+
with open_vault('./memory', agent='assistant') as vault:
|
|
137
|
+
try:
|
|
138
|
+
vault.remember(id='deploy-day', content='Deploys run on Mondays.') # create-only: the id exists
|
|
139
|
+
except ConsolError as error:
|
|
140
|
+
if error.code != 'already_exists':
|
|
141
|
+
raise
|
|
142
|
+
print(error.code, error.retryable, error.tool) # already_exists False remember
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- `field` names the input at fault, relative to `args` (`limit`, `decisions[2].temporal`); `tool`
|
|
146
|
+
the tool; `result` keeps the raw reply of a failed call, including the durable state of a pending
|
|
147
|
+
write (`error.result['result']`).
|
|
148
|
+
- Engine codes (accept unknown future ones): `invalid_argument`, `forbidden`, `not_found`,
|
|
149
|
+
`already_exists`, `version_conflict`, `vault_exists`, `vault_not_found`, `team_not_attached`,
|
|
150
|
+
`secret_rejected`, `content_too_large`, `unsupported`, `work_not_found`, `basis_mismatch`,
|
|
151
|
+
`already_decided`, `unresolved_writes`, `postcondition_failed`, `index_unavailable`, `storage_unavailable`, `vault_busy`,
|
|
152
|
+
`internal`.
|
|
153
|
+
- SDK codes: `request_failed` (hosted), `incompatible_server` (server major.minor differs from the
|
|
154
|
+
SDK's), `binary_not_found`, `server_exited`, `timeout`, `closed`.
|
|
155
|
+
- `retryable: True` (`vault_busy`, `index_unavailable`, `storage_unavailable`, connect errors) means the identical request
|
|
156
|
+
may succeed later. The SDK never retries by itself.
|
|
157
|
+
|
|
158
|
+
## Lifecycle
|
|
159
|
+
|
|
160
|
+
- Use the vault (or `Consol`) as a context manager or call `close()`: final and idempotent.
|
|
161
|
+
In-flight and later calls fail with `closed`; a concurrent `close()` returns once the child exited.
|
|
162
|
+
- Calls may overlap; replies are matched by id. Every call has a deadline: 300 s by default,
|
|
163
|
+
`timeout=` per vault or per call, `0` or `None` disables it. A timeout only stops waiting: a write
|
|
164
|
+
may still complete, so read the note before you retry.
|
|
165
|
+
- When the child crashes, in-flight calls fail with `server_exited`. The next call starts it once
|
|
166
|
+
more (a local vault is re-opened first: deleted meanwhile means `vault_not_found`).
|
|
167
|
+
- Run one child per vault; a second process waits up to 5 s for the vault lock, then `vault_busy`.
|
|
168
|
+
|
|
169
|
+
## Configuration and logging
|
|
170
|
+
|
|
171
|
+
Arguments become the child's environment, never its argv. Inherited `CONSOL_*` and `RUST_LOG` are
|
|
172
|
+
dropped first (except `CONSOL_DB_URL`), so a stray host variable cannot change the agent or the vault.
|
|
173
|
+
|
|
174
|
+
| Argument | Meaning |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `path` / `vault_root` / `vault` | the vault location (made absolute) or, for `Consol`, the vaults' root |
|
|
177
|
+
| `agent` | default agent of every call; a call's `agent` overrides it |
|
|
178
|
+
| `db` | database URL (`CONSOL_DB_URL`, required unless the environment has it) |
|
|
179
|
+
| `embedding` | `{'url', 'model', 'api_key', 'dims', 'query_prefix', 'batch_size', 'timeout_ms'}` (`CONSOL_EMBED_*`; another key fails with `invalid_argument`); without it recall is lexical-only |
|
|
180
|
+
| `env` | any other child variable, applied last |
|
|
181
|
+
| `binary_path` | the binary, see above |
|
|
182
|
+
| `timeout` | default per-call deadline in seconds |
|
|
183
|
+
|
|
184
|
+
The child's stderr is captured and never written to your stderr. Each line goes to
|
|
185
|
+
`logging.getLogger('consol')` at DEBUG level; add `env={'RUST_LOG': 'debug'}` for more detail. A
|
|
186
|
+
`server_exited` error carries the last lines in its message and in `stderr_tail`.
|
|
187
|
+
|
|
188
|
+
## Types and stability
|
|
189
|
+
|
|
190
|
+
Exports: `create_vault`, `open_vault`, `Vault`, `Consol`, `http_status`, `ConsolError`,
|
|
191
|
+
`slice_utf8`, `Reply`, `Context` and the result `TypedDict`s (`RecallResult`, `ReadResult`,
|
|
192
|
+
`RememberResult`, `RemoveResult`, `ReflectResult`, `Decision`, `ToolError`, ...). Payloads may gain
|
|
193
|
+
fields in any release. Experimental, so they may change in a minor release: hub and promotion
|
|
194
|
+
work, leads and decisions, and every `jev`, `jev_triage` and `jev_advisory` field (plain dicts).
|
|
195
|
+
|
|
196
|
+
## Check
|
|
197
|
+
|
|
198
|
+
From the repository root:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
uv run --no-project python bindings/test_sdk.py # protocol tests: no binary, no database
|
|
202
|
+
CONSOL_BIN=/abs/consol CONSOL_DB_URL=postgres://... uv run --no-project python bindings/test_sdk.py real
|
|
203
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# keb-consol
|
|
2
|
+
|
|
3
|
+
Python SDK for the consol memory engine. Distribution `keb-consol`, import name `consol`; Python
|
|
4
|
+
>= 3.10, no dependencies, synchronous and thread-safe, typed payloads (`TypedDict`s, `py.typed`).
|
|
5
|
+
Three uses, one behaviour: a vault in your process, a client of a hosted backend, or your own
|
|
6
|
+
backend. Not published yet; install from a checkout with `uv pip install ./bindings/py`.
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
- **The `consol` binary.** The wheel does not bundle it. The SDK uses the first of: the
|
|
11
|
+
`binary_path` argument, the `CONSOL_BIN` environment variable, `consol` (`consol.exe`) on `PATH`.
|
|
12
|
+
An explicit path that points nowhere fails at once with `binary_not_found`.
|
|
13
|
+
- **A database** for local and backend use: PostgreSQL with pgvector, or CockroachDB. Pass
|
|
14
|
+
`db='postgres://...'` or set `CONSOL_DB_URL` (the one `CONSOL_*` variable the child inherits).
|
|
15
|
+
|
|
16
|
+
## 1. Local: a vault in your process
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from consol import create_vault
|
|
20
|
+
|
|
21
|
+
with create_vault('./memory', agent='assistant') as vault:
|
|
22
|
+
vault.remember(id='deploy-day', content='Deploys run on Fridays.')
|
|
23
|
+
hits = vault.recall(query='deploys run')
|
|
24
|
+
assert 'deploy-day' in [card['source']['id'] for card in hits['candidates']]
|
|
25
|
+
print(vault.read(id='deploy-day')['text']) # the complete Markdown file, frontmatter included
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`create_vault` starts `consol stdio --vault ./memory` and creates the vault (`vault_exists` if there
|
|
29
|
+
is one); `open_vault('./memory', agent=...)` opens an existing one (`vault_not_found`). Both check
|
|
30
|
+
the server version against the SDK's before they return. For an LLM tool loop, `call_tool(name,
|
|
31
|
+
args)` returns the whole reply and never raises for a tool error: send `reply['text']` back to the
|
|
32
|
+
model. `consol schema` prints the JSON Schema of the wire.
|
|
33
|
+
|
|
34
|
+
## 2. Hosted: a client of a backend
|
|
35
|
+
|
|
36
|
+
```py
|
|
37
|
+
from consol import open_vault
|
|
38
|
+
|
|
39
|
+
with open_vault(url='https://memory.example.com/v1/consol',
|
|
40
|
+
headers={'Authorization': 'Bearer <your key>'}, # whatever your backend checks
|
|
41
|
+
vault='acme', agent='alice') as vault:
|
|
42
|
+
vault.remember(id='deploy-day', content='Deploys run on Fridays.')
|
|
43
|
+
print(vault.recall(query='deploys run')['candidates'][0]['source']['id'])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The same methods over HTTP: each call POSTs `{"tool", "vault", "agent", "args"}` with your headers.
|
|
47
|
+
There is no create: backends create vaults. `vault` and `agent` may be left out when the backend
|
|
48
|
+
pins them. Redirects are never followed (they would carry your credentials). Transport failures are
|
|
49
|
+
`request_failed` (`status`; retryable for connect errors and 502/503/504) and `forbidden` for a 401
|
|
50
|
+
or 403 that is not a consol reply; a consol error body is a normal tool error whatever its status.
|
|
51
|
+
|
|
52
|
+
## 3. Backend: your own endpoint
|
|
53
|
+
|
|
54
|
+
```py
|
|
55
|
+
import asyncio
|
|
56
|
+
from consol import Consol, Context, http_status
|
|
57
|
+
from fastapi import FastAPI, Header, HTTPException, Request
|
|
58
|
+
from fastapi.responses import JSONResponse
|
|
59
|
+
|
|
60
|
+
consol = Consol.start(vault_root='./vaults') # or vault='./one-vault'; db= or CONSOL_DB_URL
|
|
61
|
+
app = FastAPI()
|
|
62
|
+
USERS = {'alice-token': ('acme', 'alice')} # YOUR auth: token -> (vault, agent)
|
|
63
|
+
|
|
64
|
+
@app.post('/v1/consol')
|
|
65
|
+
async def wire(request: Request, authorization: str = Header('')):
|
|
66
|
+
user = USERS.get(authorization.removeprefix('Bearer '))
|
|
67
|
+
if user is None:
|
|
68
|
+
raise HTTPException(401, 'unauthorized')
|
|
69
|
+
ctx = Context(vaults=[user[0]], agent=user[1]) # from your auth, NEVER from the request
|
|
70
|
+
reply = await asyncio.to_thread(consol.handle, await request.json(), ctx) # handle is sync
|
|
71
|
+
return JSONResponse(reply, status_code=http_status(reply))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`ctx` decides which vaults and which agent a request may use: build it from your own authentication,
|
|
75
|
+
never from the body or headers a client controls (`{'vaults': None, ...}` means every vault; write it
|
|
76
|
+
deliberately). `handle(body, ctx)` returns the reply dict as is (`version`, `text`, `result` or
|
|
77
|
+
`error`); a tool error is in the reply, never raised, and `http_status(reply)` is the status to
|
|
78
|
+
answer with. One child serves every thread. Creating a vault is the backend's job, never the wire's:
|
|
79
|
+
`consol.create_vault('acme')` (`vault_exists`), `consol.open_vault('acme')` (`vault_not_found`).
|
|
80
|
+
`Consol.start` takes `vault_root` (a directory or `s3://bucket/prefix`; vault `x` is `<root>/x`) or
|
|
81
|
+
`vault` (one vault, named `default`), plus `db`, `embedding`, `jev={'url', 'api_key', 'model',
|
|
82
|
+
'only', 'auto'}`, `binary_path`, `env` and `timeout`.
|
|
83
|
+
|
|
84
|
+
## Methods
|
|
85
|
+
|
|
86
|
+
Keyword-only arguments with the tool's wire names, plus `timeout=` in seconds; they return the
|
|
87
|
+
result payload as a plain dict and raise `ConsolError` on a tool error.
|
|
88
|
+
|
|
89
|
+
| Method | Arguments | Returns |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `recall` | `query`, `include_history`, `limit`, `agent` | `RecallResult`: `text` is the progressive disclosure to give a model (cards by tier, intent block, hints); `candidates` are the same cards as data. Cards are leads: read a note before relying on it |
|
|
92
|
+
| `read` | `id`, `continuation`, `agent` | `ReadResult`; `text` is the exact file |
|
|
93
|
+
| `remember` | `id`, `content`, `expected_version`, `summary`, `entities`, `facets`, `scope`, `team`, `valid_from`, `valid_to`, `reflection`, `agent` | `RememberResult` |
|
|
94
|
+
| `remove` | `id`, `expected_version`, `reflection`, `agent` | `RemoveResult`: the note is archived, its body stays readable |
|
|
95
|
+
| `reflect` | one mode: none (discovery), `ids`, `work_ids` or `decisions`; `limit`, `agent` | `ReflectResult`; delivered sources carry their `text` |
|
|
96
|
+
| `call_tool` | `(name, args)` with raw wire arguments | the whole `Reply`; a tool error is a reply with `error` |
|
|
97
|
+
| `close` | | final and idempotent |
|
|
98
|
+
|
|
99
|
+
- A call's `agent` goes into the envelope and overrides the vault's default.
|
|
100
|
+
- `remember` without `expected_version` is create-only. To update, pass `source.content_version`
|
|
101
|
+
from your last `read`; a stale one fails with `version_conflict`. `None` keeps a stored value,
|
|
102
|
+
`''` or `[]` clears it. `remember` and `remove` also take `resume=True` with `reflection`
|
|
103
|
+
(`{'work_id', 'basis_hash', 'action_key'}`) to resume a pending reflection-bound write.
|
|
104
|
+
- `byte_range` values are UTF-8 byte offsets: `slice_utf8(text, byte_range)` slices a `str` by them.
|
|
105
|
+
|
|
106
|
+
## Loading many notes
|
|
107
|
+
|
|
108
|
+
Each `remember` is independent, and the vault is safe to call from several threads, so load a
|
|
109
|
+
corpus in parallel: with a hosted embedding provider most of a write is waiting on it, and
|
|
110
|
+
parallel writes overlap that wait (8 writers loaded 3.8x faster than one in a measured run).
|
|
111
|
+
|
|
112
|
+
```py
|
|
113
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
114
|
+
|
|
115
|
+
with ThreadPoolExecutor(8) as pool:
|
|
116
|
+
list(pool.map(lambda doc: vault.remember(**doc), docs)) # docs: [{"id": ..., "content": ...}]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Errors
|
|
120
|
+
|
|
121
|
+
Every failure is a `ConsolError`. Branch on `code` and `retryable`, never on the message:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from consol import ConsolError, open_vault
|
|
125
|
+
|
|
126
|
+
with open_vault('./memory', agent='assistant') as vault:
|
|
127
|
+
try:
|
|
128
|
+
vault.remember(id='deploy-day', content='Deploys run on Mondays.') # create-only: the id exists
|
|
129
|
+
except ConsolError as error:
|
|
130
|
+
if error.code != 'already_exists':
|
|
131
|
+
raise
|
|
132
|
+
print(error.code, error.retryable, error.tool) # already_exists False remember
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `field` names the input at fault, relative to `args` (`limit`, `decisions[2].temporal`); `tool`
|
|
136
|
+
the tool; `result` keeps the raw reply of a failed call, including the durable state of a pending
|
|
137
|
+
write (`error.result['result']`).
|
|
138
|
+
- Engine codes (accept unknown future ones): `invalid_argument`, `forbidden`, `not_found`,
|
|
139
|
+
`already_exists`, `version_conflict`, `vault_exists`, `vault_not_found`, `team_not_attached`,
|
|
140
|
+
`secret_rejected`, `content_too_large`, `unsupported`, `work_not_found`, `basis_mismatch`,
|
|
141
|
+
`already_decided`, `unresolved_writes`, `postcondition_failed`, `index_unavailable`, `storage_unavailable`, `vault_busy`,
|
|
142
|
+
`internal`.
|
|
143
|
+
- SDK codes: `request_failed` (hosted), `incompatible_server` (server major.minor differs from the
|
|
144
|
+
SDK's), `binary_not_found`, `server_exited`, `timeout`, `closed`.
|
|
145
|
+
- `retryable: True` (`vault_busy`, `index_unavailable`, `storage_unavailable`, connect errors) means the identical request
|
|
146
|
+
may succeed later. The SDK never retries by itself.
|
|
147
|
+
|
|
148
|
+
## Lifecycle
|
|
149
|
+
|
|
150
|
+
- Use the vault (or `Consol`) as a context manager or call `close()`: final and idempotent.
|
|
151
|
+
In-flight and later calls fail with `closed`; a concurrent `close()` returns once the child exited.
|
|
152
|
+
- Calls may overlap; replies are matched by id. Every call has a deadline: 300 s by default,
|
|
153
|
+
`timeout=` per vault or per call, `0` or `None` disables it. A timeout only stops waiting: a write
|
|
154
|
+
may still complete, so read the note before you retry.
|
|
155
|
+
- When the child crashes, in-flight calls fail with `server_exited`. The next call starts it once
|
|
156
|
+
more (a local vault is re-opened first: deleted meanwhile means `vault_not_found`).
|
|
157
|
+
- Run one child per vault; a second process waits up to 5 s for the vault lock, then `vault_busy`.
|
|
158
|
+
|
|
159
|
+
## Configuration and logging
|
|
160
|
+
|
|
161
|
+
Arguments become the child's environment, never its argv. Inherited `CONSOL_*` and `RUST_LOG` are
|
|
162
|
+
dropped first (except `CONSOL_DB_URL`), so a stray host variable cannot change the agent or the vault.
|
|
163
|
+
|
|
164
|
+
| Argument | Meaning |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `path` / `vault_root` / `vault` | the vault location (made absolute) or, for `Consol`, the vaults' root |
|
|
167
|
+
| `agent` | default agent of every call; a call's `agent` overrides it |
|
|
168
|
+
| `db` | database URL (`CONSOL_DB_URL`, required unless the environment has it) |
|
|
169
|
+
| `embedding` | `{'url', 'model', 'api_key', 'dims', 'query_prefix', 'batch_size', 'timeout_ms'}` (`CONSOL_EMBED_*`; another key fails with `invalid_argument`); without it recall is lexical-only |
|
|
170
|
+
| `env` | any other child variable, applied last |
|
|
171
|
+
| `binary_path` | the binary, see above |
|
|
172
|
+
| `timeout` | default per-call deadline in seconds |
|
|
173
|
+
|
|
174
|
+
The child's stderr is captured and never written to your stderr. Each line goes to
|
|
175
|
+
`logging.getLogger('consol')` at DEBUG level; add `env={'RUST_LOG': 'debug'}` for more detail. A
|
|
176
|
+
`server_exited` error carries the last lines in its message and in `stderr_tail`.
|
|
177
|
+
|
|
178
|
+
## Types and stability
|
|
179
|
+
|
|
180
|
+
Exports: `create_vault`, `open_vault`, `Vault`, `Consol`, `http_status`, `ConsolError`,
|
|
181
|
+
`slice_utf8`, `Reply`, `Context` and the result `TypedDict`s (`RecallResult`, `ReadResult`,
|
|
182
|
+
`RememberResult`, `RemoveResult`, `ReflectResult`, `Decision`, `ToolError`, ...). Payloads may gain
|
|
183
|
+
fields in any release. Experimental, so they may change in a minor release: hub and promotion
|
|
184
|
+
work, leads and decisions, and every `jev`, `jev_triage` and `jev_advisory` field (plain dicts).
|
|
185
|
+
|
|
186
|
+
## Check
|
|
187
|
+
|
|
188
|
+
From the repository root:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
uv run --no-project python bindings/test_sdk.py # protocol tests: no binary, no database
|
|
192
|
+
CONSOL_BIN=/abs/consol CONSOL_DB_URL=postgres://... uv run --no-project python bindings/test_sdk.py real
|
|
193
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "keb-consol"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the consol memory engine: a local vault, a hosted client, and the backend API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Khas" }]
|
|
13
|
+
|
|
14
|
+
[tool.hatch.build.targets.wheel]
|
|
15
|
+
packages = ["src/consol"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from consol.client import (Consol, ConsolError, Vault, __version__, create_vault, http_status,
|
|
2
|
+
open_vault, slice_utf8)
|
|
3
|
+
from consol.types import (AffectedDoc, Context, Decision, DecisionReceipt, DeliveredSource,
|
|
4
|
+
EmbeddingConfig, EntityAffectedGroup, ReadResult,
|
|
5
|
+
RecallCard, RecallResult,
|
|
6
|
+
ReflectCounters, ReflectDelivery, ReflectDiscovery,
|
|
7
|
+
ReflectionContext, ReflectionWork,
|
|
8
|
+
ReflectResult, Relationship, RememberResult, RemoveResult,
|
|
9
|
+
Reply, SourceRef, ToolError,
|
|
10
|
+
WorkBasis, WriteOutcome)
|
|
11
|
+
|
|
12
|
+
__all__ = ["AffectedDoc", "Consol", "ConsolError", "Context", "Decision", "DecisionReceipt",
|
|
13
|
+
"DeliveredSource", "EmbeddingConfig", "EntityAffectedGroup", "ReadResult",
|
|
14
|
+
"RecallCard", "RecallResult", "ReflectCounters", "ReflectDelivery",
|
|
15
|
+
"ReflectDiscovery", "ReflectionContext", "ReflectionWork",
|
|
16
|
+
"ReflectResult", "Relationship", "RememberResult", "RemoveResult", "Reply",
|
|
17
|
+
"SourceRef", "ToolError", "Vault", "WorkBasis", "WriteOutcome", "__version__",
|
|
18
|
+
"create_vault", "http_status", "open_vault", "slice_utf8"]
|