@keb-org/consol 0.0.0-stage → 0.1.0

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.
package/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.
package/README.md CHANGED
@@ -1,3 +1,266 @@
1
- # Temporary Holding Version
1
+ # @keb-org/consol
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TypeScript SDK for the consol memory engine, for three uses:
4
+
5
+ 1. **Local**: a vault on disk (or S3) in your own process. The SDK starts `consol stdio` as a child.
6
+ 2. **Hosted client**: the same `Vault` methods against a backend over HTTP.
7
+ 3. **Backend**: `Consol.handle` behind your own HTTP route, so clients of any language can use it.
8
+
9
+ Node >= 22 or Bun, no dependencies; `import` and `require` share one implementation. The behaviour
10
+ is the same in every mode.
11
+
12
+ ## The server binary and database
13
+
14
+ The package does not build the server. The SDK uses the first of these that exists:
15
+
16
+ 1. the `binaryPath` option,
17
+ 2. the `CONSOL_BIN` environment variable,
18
+ 3. the platform package `@keb-org/consol-<platform>-<arch>`, resolved from the SDK's own location,
19
+ 4. `consol` (`consol.exe` on Windows) on `PATH`.
20
+
21
+ A `binaryPath` or `CONSOL_BIN` that points nowhere fails at once with `binary_not_found` instead
22
+ of falling through to the next place. Build the binary from the repository with
23
+ `cargo build --release -p consol-cli`.
24
+
25
+ The engine needs a database, PostgreSQL with pgvector or CockroachDB: pass `db` or set
26
+ `CONSOL_DB_URL` in your process (the one `CONSOL_*` variable the SDK takes from its host).
27
+
28
+ ## 1. Local
29
+
30
+ ```ts
31
+ import assert from 'node:assert/strict';
32
+ import { createVault } from '@keb-org/consol';
33
+
34
+ const vault = await createVault({ path: './memory', agent: 'assistant' });
35
+ try {
36
+ await vault.remember({ id: 'deploy-day', content: 'Deploys run on Fridays.' });
37
+ const hits = await vault.recall({ query: 'deploys run' });
38
+ assert(hits.candidates.some((card) => card.source.id === 'deploy-day'));
39
+ const note = await vault.read({ id: 'deploy-day' });
40
+ console.log(note.text); // the complete Markdown file, frontmatter included
41
+ } finally {
42
+ await vault.close();
43
+ }
44
+ ```
45
+
46
+ `createVault` fails with `vault_exists` when the directory already holds a vault, `openVault`
47
+ (same options) with `vault_not_found` when it does not. Both start the server and check that its
48
+ version matches the SDK's major.minor before they return.
49
+
50
+ ## 2. Hosted client
51
+
52
+ ```typescript
53
+ import { openVault } from '@keb-org/consol';
54
+
55
+ // A backend running `Consol.handle` (use 3) answers at `url`; `headers` are your auth with it.
56
+ const vault = await openVault({
57
+ url: 'https://api.acme.dev/v1/consol',
58
+ headers: { Authorization: `Bearer ${process.env.ACME_KEY}` },
59
+ vault: 'demo', // the backend's name for the vault
60
+ agent: 'assistant',
61
+ });
62
+ try {
63
+ await vault.remember({ id: 'pref-1', content: 'The user prefers short answers.' });
64
+ const { candidates } = await vault.recall({ query: 'how long should answers be?' });
65
+ console.log(candidates.map((card) => card.source.id));
66
+ } finally {
67
+ await vault.close();
68
+ }
69
+ ```
70
+
71
+ Backends create vaults, clients only open them: there is no `createVault` for a URL. Opening makes
72
+ no request, so a missing vault is `vault_not_found` on the first call. Every method, error and
73
+ deadline works as in the local use. A backend that refuses the vault or agent answers `forbidden`;
74
+ a reply that is not a consol reply is `request_failed` (`status`; retryable on connect errors and
75
+ 502/503/504; a 401 or 403 is `forbidden`).
76
+
77
+ ## 3. Backend
78
+
79
+ ```typescript
80
+ import express from 'express';
81
+ import { Consol, httpStatus } from '@keb-org/consol';
82
+
83
+ const consol = await Consol.start({ vaultRoot: './vaults' }); // or s3://bucket/prefix; db is $CONSOL_DB_URL
84
+ await consol.createVault('acme'); // admin, once per tenant: `vault_exists` if it is there
85
+
86
+ const app = express();
87
+ app.use(express.json());
88
+ app.post('/v1/consol', async (req, res) => {
89
+ const user = await authenticate(req); // YOUR auth: a session, a token, an API key
90
+ // ctx limits the request. It comes from your auth, NEVER from req.body or anything the caller sets.
91
+ const reply = await consol.handle(req.body, { vaults: [user.tenant], agent: user.id });
92
+ res.status(httpStatus(reply)).json(reply); // a tool error is a reply, not an exception
93
+ });
94
+ ```
95
+
96
+ - `handle(body, ctx)` takes `{ tool, vault?, agent?, args }` (the five tools), returns the reply
97
+ `{ version, text?, result?, error? }` as is, and is safe to call concurrently. `httpStatus` maps
98
+ it to 200, 400, 403, 404, 409, 413, 422, 503 or 500.
99
+ - `ctx.vaults` lists the vaults the caller may use (`null` is any vault, on purpose; with one entry
100
+ the body may omit `vault`). `ctx.agent` pins the agent: a body naming another is `forbidden`.
101
+ - `createVault(name)` / `openVault(name)` never go over the network (`vault_exists` /
102
+ `vault_not_found`). `Consol.start({ vault: path })` serves one vault, named `default`. Call
103
+ `close()` on shutdown. Engine options (`db`, `embedding`, ...) are listed below.
104
+
105
+ ## Methods
106
+
107
+ Each method takes one object with the tool's wire argument names, plus optional
108
+ `{ timeoutMs, signal }`, and returns the payload, not the wire reply.
109
+
110
+ | Method | Arguments | Returns |
111
+ |---|---|---|
112
+ | `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 |
113
+ | `read` | `{ id, continuation?, agent? }` | `ReadResult`; `text` is the exact file |
114
+ | `remember` | `{ id, content?, expected_version?, summary?, entities?, facets?, scope?, team?, valid_from?, valid_to?, reflection?, agent? }` | `RememberResult` |
115
+ | `remove` | `{ id, expected_version, reflection?, agent? }` | `RemoveResult`: the note is archived, its body stays readable |
116
+ | `reflect` | one mode: `{}` discovery, `{ ids }`, `{ work_ids }` or `{ decisions }`; `limit?`, `agent?` | `ReflectResult`; delivered sources carry their `text` |
117
+ | `callTool` | `(name, args)` with raw wire arguments | the raw `Reply`; tool errors come back as `reply.error` |
118
+ | `close` | | final and idempotent |
119
+
120
+ - `remember` without `expected_version` is create-only. To update, pass `source.content_version`
121
+ from your last `read`; a stale one fails with `version_conflict`. Omitted or `null` fields keep
122
+ their stored value, `""` or `[]` clears them.
123
+ - `remember` and `remove` also take `{ resume: true, reflection, agent? }` to resume a pending
124
+ reflection-bound write. `reflection` is `{ work_id, basis_hash, action_key }`.
125
+ - `byte_range` values are UTF-8 byte offsets: `sliceUtf8(text, start, end)` slices a JS string by them.
126
+ - `vault.serverVersion` is the server version of the last reply. An agent exists from its first
127
+ `remember`; reading as an agent that wrote nothing finds nothing.
128
+
129
+ ## LLM tool loop
130
+
131
+ ```ts
132
+ import { openVault } from '@keb-org/consol';
133
+
134
+ const vault = await openVault({ path: './memory', agent: 'assistant' });
135
+ const call = { name: 'recall', input: { query: 'deploys run' } }; // a tool call from your model
136
+
137
+ const reply = await vault.callTool(call.name, call.input); // run it
138
+ const toolResult = { content: JSON.stringify(reply), isError: reply.error !== undefined }; // hand it back
139
+
140
+ console.log(toolResult.isError);
141
+ await vault.close();
142
+ ```
143
+
144
+ `callTool` never throws for a tool error, so the model sees the structured error and can fix its
145
+ call. In the reply, `text` is guidance written for the model and `result` the payload, with note
146
+ Markdown inline.
147
+
148
+ The tool list is not on the wire. `consol schema` prints its JSON Schema: `oneOf` holds one
149
+ envelope per tool, with the `args` schema and the description the model reads. An MCP client can
150
+ use `consol mcp` instead.
151
+
152
+ ## Loading many notes
153
+
154
+ Each `remember` is independent, so load a corpus with several writes in flight: with a hosted
155
+ embedding provider most of a write is waiting on it, and concurrent writes overlap that wait
156
+ (8 writers loaded 3.8x faster than one in a measured run).
157
+
158
+ ```typescript
159
+ const queue = [...docs]; // docs: { id, content }[]
160
+ await Promise.all(Array.from({ length: 8 }, async () => {
161
+ for (let doc; (doc = queue.shift()); ) await vault.remember(doc);
162
+ }));
163
+ ```
164
+
165
+ ## Errors
166
+
167
+ Every failure is a `ConsolError`. Branch on `code` and `retryable`, never on the message:
168
+
169
+ ```ts
170
+ import { ConsolError, openVault } from '@keb-org/consol';
171
+
172
+ const vault = await openVault({ path: './memory', agent: 'assistant' });
173
+ try {
174
+ await vault.remember({ id: 'deploy-day', content: 'Deploys run on Mondays.' }); // create-only: the id exists
175
+ } catch (error) {
176
+ if (!ConsolError.is(error) || error.code !== 'already_exists') throw error;
177
+ console.log(error.code, error.retryable, error.tool); // already_exists false remember
178
+ } finally {
179
+ await vault.close();
180
+ }
181
+ ```
182
+
183
+ - `ConsolError.is(error)` also works across duplicate copies of the package, where `instanceof` fails.
184
+ - `field` names the input at fault, relative to the call's arguments (`limit`,
185
+ `decisions[2].temporal`), `tool` the tool, and `result` keeps the raw `Reply` of a failed tool
186
+ call, including the durable state of a pending write (`error.result.result`).
187
+ - Tool codes (contract section 4; accept unknown future ones): `invalid_argument`, `forbidden`,
188
+ `not_found`, `already_exists`, `version_conflict`, `team_not_attached`, `secret_rejected`,
189
+ `content_too_large`, `unsupported`, `work_not_found`, `basis_mismatch`, `already_decided`,
190
+ `unresolved_writes`, `postcondition_failed`, `index_unavailable`, `storage_unavailable`, `vault_busy`, `internal`.
191
+ Vault codes: `vault_exists`, `vault_not_found`.
192
+ - SDK codes: `request_failed` (`status?`), `binary_not_found`, `incompatible_server` (server
193
+ major.minor differs from the SDK's), `server_exited`, `timeout`, `aborted`, `closed`.
194
+ - `retryable: true` (`vault_busy`, `index_unavailable`, `storage_unavailable`, `request_failed` on a connect error or
195
+ 502/503/504) means the identical request may succeed later. The SDK never retries by itself.
196
+
197
+ ## Lifecycle
198
+
199
+ - `close()` is final and idempotent: in-flight and later calls fail with `closed`, and concurrent
200
+ calls share one completion. `await using vault = await openVault(...)` closes it for you.
201
+ - An idle local vault does not keep Node or Bun alive: a script that forgets `close()` still
202
+ exits, and its server exits with it.
203
+ - Every call has a deadline: 300 s by default, `timeoutMs` per vault or per call, `0` disables it.
204
+ `signal` aborts a call. Both only stop waiting: a write may still complete, so read the note
205
+ before you retry.
206
+ - When a local server crashes, in-flight calls fail with `server_exited`. The next call starts the
207
+ server once more and opens the vault again (`vault_not_found` if it was deleted meanwhile).
208
+ - Servers on one vault take turns on its lock, a lease in the database; a wait over 5 s fails with
209
+ `vault_busy`.
210
+
211
+ ## Configuration and logging
212
+
213
+ Options of `createVault`, `openVault` (local) and `Consol.start` become the server's environment,
214
+ never its argv. Inherited `CONSOL_*` and `RUST_LOG` variables are dropped first (except
215
+ `CONSOL_DB_URL` as the default for `db`), so a stray host variable cannot change the agent or the
216
+ vault.
217
+
218
+ | Option | Meaning |
219
+ |---|---|
220
+ | `path` | local vault directory or `s3://bucket[/prefix]/<name]`, made absolute (`createVault`, `openVault`) |
221
+ | `vaultRoot` / `vault` | `Consol.start`: a directory or `s3://` prefix of many vaults, or one vault |
222
+ | `agent` | default agent of every call; a call's `agent` argument overrides it |
223
+ | `db` | database URL (`CONSOL_DB_URL`): PostgreSQL with pgvector, or CockroachDB |
224
+ | `embedding` | `{ url, model, apiKey?, dims?, queryPrefix?, batchSize?, timeoutMs? }` (`CONSOL_EMBED_*`); without it recall is lexical-only |
225
+ | `jev` | `{ url?, apiKey?, model?, auto? }` (`CONSOL_JEV_*`); experimental advice |
226
+ | `env` | any other server variable, applied last |
227
+ | `binaryPath` | server binary, see above |
228
+ | `timeoutMs` | default per-call deadline in ms |
229
+ | `onLog` | `(line) => void`, receives each server stderr line |
230
+
231
+ The server's stderr is captured and never written to your process's stderr. Pass `onLog` to see
232
+ it, and `env: { RUST_LOG: 'debug' }` for more detail. A `server_exited` error carries the server's
233
+ last lines in its message and in `stderrTail`.
234
+
235
+ ## Types and stability
236
+
237
+ Exports: `createVault`, `openVault`, `Vault`, `Consol`, `httpStatus`, `ConsolError`, `sliceUtf8` and
238
+ the payload and argument types (`Reply`, `Context`, `RecallResult`, `ReadResult`, `RememberResult`,
239
+ `RemoveResult`, `ReflectResult`, `Decision`, `ToolError`, ...). Payloads may gain fields in any
240
+ release. Experimental, so they may change in a minor release: hub and promotion work, leads and
241
+ decisions, and every `jev`, `jev_triage` and `jev_advisory` field (typed as plain records).
242
+
243
+ ## Development
244
+
245
+ ```bash
246
+ bun install
247
+ bun run build # dist/index.js (CJS), dist/index.mjs (ESM wrapper), dist/*.d.ts
248
+ bun run typecheck # tsc over src
249
+ # the rest needs the real binary and a database:
250
+ export CONSOL_BIN=/abs/consol CONSOL_DB_URL=postgres://...
251
+ bun run test # pure checks, hosted-client stub, real-binary suite on Node, strict tsc incl. conformance
252
+ bun run test:consumers # packed tarballs in Bun, Node ESM, Node CJS and platform-package projects
253
+ node test/capture.mjs # recapture test/fixtures from the real server (without CONSOL_BIN it only regenerates the types check)
254
+ ```
255
+
256
+ `bun run test`, not `bun test` (that is Bun's own runner). Consumer logs go to
257
+ `consumers/<project>.log`.
258
+
259
+ ## Publishing platform binaries
260
+
261
+ 1. `bun run build`
262
+ 2. `bun scripts/make-platform-package.mjs <binary> <platform> <arch> <outDir>` per target
263
+ (win32-x64, darwin-arm64, darwin-x64, linux-x64, linux-arm64); the binary is stored as
264
+ `bin/consol[.exe]` whatever its file name
265
+ 3. Publish each platform package, then `bun scripts/inject-optional-deps.mjs <publish-copy-of-package.json>`
266
+ 4. `bun publish` the main package (`publishConfig.access` is `public`)
@@ -0,0 +1,36 @@
1
+ import { type EngineOptions } from "./stdio";
2
+ import type { CallOptions, Context } from "./transport";
3
+ import type { Reply } from "./types";
4
+ /** Exactly one of `vaultRoot` / `vault`, plus the engine settings (`db`, `embedding`, `jev`, ...). */
5
+ export interface ConsolOptions extends EngineOptions {
6
+ /** Directory or `s3://bucket[/prefix]` holding the vaults: vault `x` is `<root>/x`. */
7
+ vaultRoot?: string;
8
+ /** One vault (directory or `s3://bucket[/prefix]/<name>`), named `default`. */
9
+ vault?: string;
10
+ }
11
+ export declare function httpStatus(reply: Reply): number;
12
+ /**
13
+ * The engine in your own backend: one `consol stdio` child behind `handle`. A backend authorizes
14
+ * the request itself, then passes the result as `ctx`.
15
+ *
16
+ * SECURITY: `ctx` (the vaults the caller may use, the agent they are) must come from YOUR auth,
17
+ * for example the session of the signed-in user. NEVER build it from the request body, query or
18
+ * headers: the body is untrusted input, `ctx` is what limits it.
19
+ */
20
+ export declare class Consol {
21
+ private readonly transport;
22
+ private constructor();
23
+ static start(options: ConsolOptions): Promise<Consol>;
24
+ /**
25
+ * Runs one envelope (`{tool, vault?, agent?, args}`, usually `req.body`) under `ctx` and returns
26
+ * the reply as is. A tool error is a reply with `error`, not an exception: answer with
27
+ * `res.status(httpStatus(reply)).json(reply)`. Safe to call concurrently.
28
+ */
29
+ handle(body: unknown, ctx: Context, options?: CallOptions): Promise<Reply>;
30
+ /** Rejects with `vault_exists` when the vault is there. A vault is created here, never over the wire. */
31
+ createVault(name: string, options?: CallOptions): Promise<void>;
32
+ /** Rejects with `vault_not_found` when there is no such vault. */
33
+ openVault(name: string, options?: CallOptions): Promise<void>;
34
+ /** Final and idempotent: in-flight and later calls reject with `closed`. */
35
+ close(): Promise<void>;
36
+ }
@@ -0,0 +1,29 @@
1
+ import type { Reply } from "./types";
2
+ /** Branchable error codes: wire tool codes (contract section 4) plus the SDK codes of section 6. */
3
+ export type ConsolErrorCode = "invalid_argument" | "not_found" | "already_exists" | "version_conflict" | "team_not_attached" | "secret_rejected" | "content_too_large" | "unsupported" | "work_not_found" | "basis_mismatch" | "already_decided" | "unresolved_writes" | "postcondition_failed" | "index_unavailable" | "storage_unavailable" | "vault_busy" | "forbidden" | "vault_exists" | "vault_not_found" | "internal" | "request_failed" | "binary_not_found" | "incompatible_server" | "server_exited" | "timeout" | "aborted" | "closed" | (string & {});
4
+ export interface ConsolErrorDetails {
5
+ retryable?: boolean;
6
+ field?: string;
7
+ tool?: string;
8
+ /** HTTP status of a `request_failed` / `forbidden` that came from the transport, not the engine. */
9
+ status?: number;
10
+ stderrTail?: string;
11
+ result?: Reply;
12
+ cause?: unknown;
13
+ }
14
+ /** One error class; branch on `code`, never on the message. */
15
+ export declare class ConsolError extends Error {
16
+ readonly code: ConsolErrorCode;
17
+ readonly retryable: boolean;
18
+ readonly field?: string;
19
+ readonly tool?: string;
20
+ readonly status?: number;
21
+ readonly stderrTail?: string;
22
+ /** Raw reply of a failed typed call: durable write state (`result.result`) survives the throw. */
23
+ readonly result?: Reply;
24
+ constructor(code: ConsolErrorCode, message: string, details?: ConsolErrorDetails);
25
+ /** instanceof breaks across duplicate package copies; the string brand does not. */
26
+ static is(error: unknown): error is ConsolError;
27
+ }
28
+ /** The ConsolError for an error reply, or undefined for a result reply. */
29
+ export declare function replyError(reply: Reply, tool?: string): ConsolError | undefined;
@@ -0,0 +1,6 @@
1
+ export { ConsolError, type ConsolErrorCode } from "./errors";
2
+ export { Consol, httpStatus, type ConsolOptions } from "./consol";
3
+ export type { CallOptions, Context } from "./transport";
4
+ export type { EmbeddingOptions, EngineOptions, JevOptions } from "./stdio";
5
+ export { Vault, createVault, openVault, sliceUtf8, type VaultOptions, type HostedOptions, type RecallArgs, type ReadArgs, type RememberArgs, type RemoveArgs, type ResumeArgs, type ReflectArgs, type ReflectionContext, type Decision, type InspectedSource, type InspectedSpan, type TemporalAssessment, type PairAssertion, } from "./vault";
6
+ export type { Reply, ToolError, Unavailable, Status, ByteRange, Scope, SourceRef, RelationshipKind, Relationship, RecallRetrieval, SourceExcerpt, RecallCard, RecallResult, ReadResult, RememberResult, RemoveResult, ReflectResult, AffectedDoc, EntityAffectedGroup, WriteState, WriteFailureCode, WriteReceipt, WriteResolution, WriteOutcome, ReflectionWork, SourceWork, PairWork, HubWork, PromotionWork, DeliveredSource, CompleteDelivery, FailedDelivery, ReflectDelivery, DecisionReceipt, SourceDisposition, PairDisposition, } from "./types";