@knolo/core 3.2.2 → 3.2.4
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/README.md +68 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/indexer.d.ts +5 -4
- package/dist/indexer.js +6 -5
- package/dist/memory/consolidate.d.ts +15 -0
- package/dist/memory/consolidate.js +71 -0
- package/dist/memory/cortex.d.ts +43 -0
- package/dist/memory/cortex.js +93 -0
- package/dist/memory/engram.d.ts +48 -0
- package/dist/memory/engram.js +90 -0
- package/dist/memory/graph_adapter.d.ts +3 -0
- package/dist/memory/graph_adapter.js +65 -0
- package/dist/memory/index.d.ts +13 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/label.d.ts +4 -0
- package/dist/memory/label.js +53 -0
- package/dist/memory/log.d.ts +36 -0
- package/dist/memory/log.js +241 -0
- package/dist/memory/recall.d.ts +23 -0
- package/dist/memory/recall.js +167 -0
- package/dist/query.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -309,6 +309,74 @@ Properties:
|
|
|
309
309
|
|
|
310
310
|
---
|
|
311
311
|
|
|
312
|
+
# 🧠 Knolo Cortex
|
|
313
|
+
|
|
314
|
+
Knolo Cortex is a local-first overlay memory layer for `.knolo` packs.
|
|
315
|
+
|
|
316
|
+
It gives you:
|
|
317
|
+
|
|
318
|
+
* Deterministic append-only memory writes
|
|
319
|
+
* Lexical-first recall with label and namespace filters
|
|
320
|
+
* Portable memory logs you can serialize and replay
|
|
321
|
+
* Consolidation back into pack docs without mutating the pack itself
|
|
322
|
+
* Deterministic graph export via `memoryToClaimOps()`
|
|
323
|
+
|
|
324
|
+
## Example
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import {
|
|
328
|
+
buildPack,
|
|
329
|
+
consolidateMemories,
|
|
330
|
+
createCortex,
|
|
331
|
+
mountPack,
|
|
332
|
+
recall,
|
|
333
|
+
remember,
|
|
334
|
+
} from "@knolo/core";
|
|
335
|
+
|
|
336
|
+
const cortex = createCortex({ actor: "notes-app" });
|
|
337
|
+
const { cortex: next, memory } = remember(cortex, {
|
|
338
|
+
kind: "note",
|
|
339
|
+
text: "Project alpha uses a local-first memory overlay.",
|
|
340
|
+
labels: ["project.alpha"],
|
|
341
|
+
namespace: "project.alpha",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const hits = recall(next, "project alpha");
|
|
345
|
+
const docs = consolidateMemories(next, { namespacePrefix: "memory" });
|
|
346
|
+
const bytes = await buildPack(docs);
|
|
347
|
+
const pack = await mountPack({ src: bytes });
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
If you need to load a local file in Node, use `@knolo/core/node` or read the bytes first and pass a `Uint8Array` into `mountPack()`.
|
|
351
|
+
|
|
352
|
+
## Cortex API
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
import {
|
|
356
|
+
createCortex,
|
|
357
|
+
remember,
|
|
358
|
+
forget,
|
|
359
|
+
labelMemory,
|
|
360
|
+
linkMemories,
|
|
361
|
+
recall,
|
|
362
|
+
consolidateMemories,
|
|
363
|
+
memoryToClaimOps,
|
|
364
|
+
} from "@knolo/core";
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
* `createCortex({ actor?, now?, log? })` creates an immutable memory runtime
|
|
368
|
+
* `remember()` appends a new memory entry
|
|
369
|
+
* `forget()` tombstones a memory
|
|
370
|
+
* `labelMemory()` adds labels without mutating the original cortex
|
|
371
|
+
* `linkMemories()` records deterministic memory relationships
|
|
372
|
+
* `recall()` ranks memories with lexical-first scoring
|
|
373
|
+
* `consolidateMemories()` converts selected memories back into `BuildInputDoc[]`
|
|
374
|
+
* `memoryToClaimOps()` emits deterministic ClaimGraph ops for memory nodes, labels, and links
|
|
375
|
+
|
|
376
|
+
The full example lives in [`examples/memory-overlay/README.md`](../../examples/memory-overlay/README.md).
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
312
380
|
# 🗺 Roadmap
|
|
313
381
|
|
|
314
382
|
* Incremental pack updates
|
|
@@ -388,4 +456,3 @@ Runtimes that ignore unknown trailing bytes remain compatible.
|
|
|
388
456
|
# 📄 License
|
|
389
457
|
|
|
390
458
|
Apache-2.0
|
|
391
|
-
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { getClaimGraph, validateClaimGraph, } from './graph/claim_graph.js';
|
|
|
12
12
|
export { buildClaimGraph } from './graph/build_claim_graph.js';
|
|
13
13
|
export { createGraphLog, appendOp, applyClaimGraphLog, mergeClaimGraphLogs, serializeClaimGraphLog, deserializeClaimGraphLog, } from './graph/log.js';
|
|
14
14
|
export { expandQueryWithGraph } from './graph/query_expand.js';
|
|
15
|
+
export * from './memory/index.js';
|
|
15
16
|
export type { MountOptions, PackMeta, Pack } from './pack.runtime.js';
|
|
16
17
|
export type { QueryOptions, Hit } from './query.js';
|
|
17
18
|
export type { EmbeddingProvider, SemanticSidecar, SemanticQueryOptions, RetrievalEvidence } from './semantic/types.js';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { getClaimGraph, validateClaimGraph, } from './graph/claim_graph.js';
|
|
|
13
13
|
export { buildClaimGraph } from './graph/build_claim_graph.js';
|
|
14
14
|
export { createGraphLog, appendOp, applyClaimGraphLog, mergeClaimGraphLogs, serializeClaimGraphLog, deserializeClaimGraphLog, } from './graph/log.js';
|
|
15
15
|
export { expandQueryWithGraph } from './graph/query_expand.js';
|
|
16
|
+
export * from './memory/index.js';
|
|
16
17
|
export { parseToolCallV1FromText } from './tool_parse.js';
|
|
17
18
|
export { nowIso, createTrace } from './trace.js';
|
|
18
19
|
export { assertToolCallAllowed } from './tool_gate.js';
|
package/dist/indexer.d.ts
CHANGED
|
@@ -13,11 +13,12 @@ export type IndexBuildResult = {
|
|
|
13
13
|
* sequences of blockId and positions for that term, with zeros as delimiters.
|
|
14
14
|
* The structure looks like:
|
|
15
15
|
*
|
|
16
|
-
* [termId, blockId+1, pos, pos, 0, blockId+1, pos, 0, 0, termId, ...]
|
|
16
|
+
* [termId, blockId+1, pos+1, pos+1, 0, blockId+1, pos+1, 0, 0, termId, ...]
|
|
17
17
|
*
|
|
18
18
|
* Block IDs are stored as bid+1 so that 0 can remain a sentinel delimiter.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Positions are also stored as pos+1 for the same reason. Each block section
|
|
20
|
+
* ends with a 0, and each term section ends with a 0. The entire array can be
|
|
21
|
+
* streamed sequentially without needing to know the sizes of individual lists
|
|
22
|
+
* ahead of time.
|
|
22
23
|
*/
|
|
23
24
|
export declare function buildIndex(blocks: Block[]): IndexBuildResult;
|
package/dist/indexer.js
CHANGED
|
@@ -14,12 +14,13 @@ import { tokenize } from "./tokenize.js";
|
|
|
14
14
|
* sequences of blockId and positions for that term, with zeros as delimiters.
|
|
15
15
|
* The structure looks like:
|
|
16
16
|
*
|
|
17
|
-
* [termId, blockId+1, pos, pos, 0, blockId+1, pos, 0, 0, termId, ...]
|
|
17
|
+
* [termId, blockId+1, pos+1, pos+1, 0, blockId+1, pos+1, 0, 0, termId, ...]
|
|
18
18
|
*
|
|
19
19
|
* Block IDs are stored as bid+1 so that 0 can remain a sentinel delimiter.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Positions are also stored as pos+1 for the same reason. Each block section
|
|
21
|
+
* ends with a 0, and each term section ends with a 0. The entire array can be
|
|
22
|
+
* streamed sequentially without needing to know the sizes of individual lists
|
|
23
|
+
* ahead of time.
|
|
23
24
|
*/
|
|
24
25
|
export function buildIndex(blocks) {
|
|
25
26
|
// Map term to termId and interim map of termId -> blockId -> positions
|
|
@@ -61,7 +62,7 @@ export function buildIndex(blocks) {
|
|
|
61
62
|
for (const [tid, blockMap] of termBlockPositions) {
|
|
62
63
|
postings.push(tid);
|
|
63
64
|
for (const [bid, positions] of blockMap) {
|
|
64
|
-
postings.push(bid + 1, ...positions, 0);
|
|
65
|
+
postings.push(bid + 1, ...positions.map((pos) => pos + 1), 0);
|
|
65
66
|
}
|
|
66
67
|
postings.push(0); // end of term
|
|
67
68
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BuildInputDoc } from '../builder.js';
|
|
2
|
+
import type { CortexV1 } from './cortex.js';
|
|
3
|
+
import type { MemoryEngramV1 } from './engram.js';
|
|
4
|
+
export type ConsolidateMemoriesOptionsV1 = {
|
|
5
|
+
namespacePrefix?: string;
|
|
6
|
+
kind?: string | readonly string[];
|
|
7
|
+
labels?: string | readonly string[];
|
|
8
|
+
namespace?: string | readonly string[];
|
|
9
|
+
minImportance?: number;
|
|
10
|
+
minConfidence?: number;
|
|
11
|
+
minAgeMs?: number;
|
|
12
|
+
maxAgeMs?: number;
|
|
13
|
+
now?: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function consolidateMemories(cortexOrMemories: CortexV1 | readonly MemoryEngramV1[], opts?: ConsolidateMemoriesOptionsV1): BuildInputDoc[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { matchesMemoryLabels, validateMemoryLabels } from './label.js';
|
|
2
|
+
import { normalizeMemoryLabel } from './label.js';
|
|
3
|
+
import { normalize } from '../tokenize.js';
|
|
4
|
+
export function consolidateMemories(cortexOrMemories, opts = {}) {
|
|
5
|
+
const memories = isCortex(cortexOrMemories)
|
|
6
|
+
? cortexOrMemories.memories
|
|
7
|
+
: cortexOrMemories;
|
|
8
|
+
const now = opts.now ?? (isCortex(cortexOrMemories) ? cortexOrMemories.now() : Date.now());
|
|
9
|
+
const namespacePrefix = normalizeNamespacePrefix(opts.namespacePrefix);
|
|
10
|
+
const kindFilters = normalizeKindFilters(opts.kind);
|
|
11
|
+
const labelsFilter = validateMemoryLabels(opts.labels);
|
|
12
|
+
const namespaceFilters = normalizeNamespaceFilters(opts.namespace);
|
|
13
|
+
return memories
|
|
14
|
+
.filter((memory) => {
|
|
15
|
+
if (kindFilters.length > 0 && !kindFilters.includes(normalizeKind(memory.kind)))
|
|
16
|
+
return false;
|
|
17
|
+
if (labelsFilter.length > 0 && !matchesMemoryLabels(memory.labels, labelsFilter))
|
|
18
|
+
return false;
|
|
19
|
+
if (namespaceFilters.length > 0 && !matchesNamespace(memory.namespace, namespaceFilters))
|
|
20
|
+
return false;
|
|
21
|
+
if (opts.minImportance !== undefined && valueOrDefault(memory.importance, 0.5) < opts.minImportance)
|
|
22
|
+
return false;
|
|
23
|
+
if (opts.minConfidence !== undefined && valueOrDefault(memory.confidence, 0.5) < opts.minConfidence)
|
|
24
|
+
return false;
|
|
25
|
+
const ageMs = now - memory.ts;
|
|
26
|
+
if (opts.minAgeMs !== undefined && ageMs < opts.minAgeMs)
|
|
27
|
+
return false;
|
|
28
|
+
if (opts.maxAgeMs !== undefined && ageMs > opts.maxAgeMs)
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
})
|
|
32
|
+
.slice()
|
|
33
|
+
.sort((a, b) => a.ts - b.ts || a.id.localeCompare(b.id))
|
|
34
|
+
.map((memory) => ({
|
|
35
|
+
id: memory.id,
|
|
36
|
+
heading: `${memory.kind}: ${memory.labels.join('/')}`,
|
|
37
|
+
namespace: `${namespacePrefix}.${memory.kind}`,
|
|
38
|
+
text: memory.text,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function isCortex(input) {
|
|
42
|
+
return !Array.isArray(input) && typeof input === 'object' && input !== null && 'memories' in input;
|
|
43
|
+
}
|
|
44
|
+
function normalizeNamespacePrefix(value) {
|
|
45
|
+
const normalized = normalizeMemoryLabel(value ?? 'memory');
|
|
46
|
+
return normalized || 'memory';
|
|
47
|
+
}
|
|
48
|
+
function normalizeKindFilters(kind) {
|
|
49
|
+
if (kind === undefined)
|
|
50
|
+
return [];
|
|
51
|
+
const values = Array.isArray(kind) ? kind : [kind];
|
|
52
|
+
return [...new Set(values.map(normalizeKind).filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
53
|
+
}
|
|
54
|
+
function normalizeNamespaceFilters(value) {
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return [];
|
|
57
|
+
const values = Array.isArray(value) ? value : [value];
|
|
58
|
+
return [...new Set(values.map((entry) => normalizeMemoryLabel(entry)).filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
59
|
+
}
|
|
60
|
+
function normalizeKind(kind) {
|
|
61
|
+
return normalize(kind).replace(/\s+/g, ' ').trim();
|
|
62
|
+
}
|
|
63
|
+
function matchesNamespace(value, filters) {
|
|
64
|
+
if (!value)
|
|
65
|
+
return false;
|
|
66
|
+
const normalized = normalizeMemoryLabel(value);
|
|
67
|
+
return filters.some((filter) => normalized === filter || normalized.startsWith(`${filter}.`));
|
|
68
|
+
}
|
|
69
|
+
function valueOrDefault(value, fallback) {
|
|
70
|
+
return Number.isFinite(value) ? value : fallback;
|
|
71
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MemoryEngramV1, MemoryInputV1, MemoryLinkV1 } from './engram.js';
|
|
2
|
+
import { type MemoryLogV1, type MemoryOpV1 } from './log.js';
|
|
3
|
+
export type CortexV1 = {
|
|
4
|
+
version: 1;
|
|
5
|
+
actor: string;
|
|
6
|
+
now: () => number;
|
|
7
|
+
log: MemoryLogV1;
|
|
8
|
+
memories: MemoryEngramV1[];
|
|
9
|
+
};
|
|
10
|
+
export type CortexWriteResult<T extends object> = T & {
|
|
11
|
+
cortex: CortexV1;
|
|
12
|
+
};
|
|
13
|
+
export declare function createCortex(opts?: {
|
|
14
|
+
actor?: string;
|
|
15
|
+
now?: () => number;
|
|
16
|
+
log?: MemoryLogV1;
|
|
17
|
+
}): CortexV1;
|
|
18
|
+
export declare function remember(cortex: CortexV1, input: MemoryInputV1): CortexWriteResult<{
|
|
19
|
+
memory: MemoryEngramV1;
|
|
20
|
+
op: MemoryOpV1;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function forget(cortex: CortexV1, id: string, provenance?: {
|
|
23
|
+
ts?: number;
|
|
24
|
+
actor?: string;
|
|
25
|
+
}): CortexWriteResult<{
|
|
26
|
+
memoryId: string;
|
|
27
|
+
op: MemoryOpV1;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function labelMemory(cortex: CortexV1, id: string, labels: string | readonly string[], provenance?: {
|
|
30
|
+
ts?: number;
|
|
31
|
+
actor?: string;
|
|
32
|
+
}): CortexWriteResult<{
|
|
33
|
+
memory?: MemoryEngramV1;
|
|
34
|
+
op: MemoryOpV1;
|
|
35
|
+
}>;
|
|
36
|
+
export declare function linkMemories(cortex: CortexV1, from: string, to: string, relation: string, provenance?: {
|
|
37
|
+
ts?: number;
|
|
38
|
+
actor?: string;
|
|
39
|
+
confidence?: number;
|
|
40
|
+
}): CortexWriteResult<{
|
|
41
|
+
link: MemoryLinkV1;
|
|
42
|
+
op: MemoryOpV1;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createMemoryId, normalizeMemoryActor, normalizeMemoryInput, normalizeMemoryKind, normalizeMemoryTimestamp, } from './engram.js';
|
|
2
|
+
import { appendMemoryOp, applyMemoryLog, createMemoryLog, } from './log.js';
|
|
3
|
+
import { validateMemoryLabels } from './label.js';
|
|
4
|
+
export function createCortex(opts = {}) {
|
|
5
|
+
const log = opts.log ? { version: 1, ops: [...opts.log.ops] } : createMemoryLog();
|
|
6
|
+
return {
|
|
7
|
+
version: 1,
|
|
8
|
+
actor: normalizeMemoryActor(opts.actor ?? 'cortex'),
|
|
9
|
+
now: opts.now ?? (() => Date.now()),
|
|
10
|
+
log,
|
|
11
|
+
memories: applyMemoryLog(log),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function remember(cortex, input) {
|
|
15
|
+
const memory = normalizeMemoryInput(input, {
|
|
16
|
+
actor: cortex.actor,
|
|
17
|
+
ts: cortex.now(),
|
|
18
|
+
});
|
|
19
|
+
const op = {
|
|
20
|
+
op: 'remember',
|
|
21
|
+
ts: memory.ts,
|
|
22
|
+
actor: memory.actor,
|
|
23
|
+
memory,
|
|
24
|
+
};
|
|
25
|
+
return applyWrite(cortex, op, { memory });
|
|
26
|
+
}
|
|
27
|
+
export function forget(cortex, id, provenance = {}) {
|
|
28
|
+
const op = {
|
|
29
|
+
op: 'forget',
|
|
30
|
+
id,
|
|
31
|
+
ts: normalizeMemoryTimestamp(provenance.ts ?? cortex.now()),
|
|
32
|
+
actor: normalizeMemoryActor(provenance.actor ?? cortex.actor),
|
|
33
|
+
};
|
|
34
|
+
return applyWrite(cortex, op, { memoryId: id });
|
|
35
|
+
}
|
|
36
|
+
export function labelMemory(cortex, id, labels, provenance = {}) {
|
|
37
|
+
const normalizedLabels = validateMemoryLabels(labels);
|
|
38
|
+
const op = {
|
|
39
|
+
op: 'label',
|
|
40
|
+
id,
|
|
41
|
+
labels: normalizedLabels,
|
|
42
|
+
ts: normalizeMemoryTimestamp(provenance.ts ?? cortex.now()),
|
|
43
|
+
actor: normalizeMemoryActor(provenance.actor ?? cortex.actor),
|
|
44
|
+
};
|
|
45
|
+
return applyWrite(cortex, op, {
|
|
46
|
+
memory: cortex.memories.find((memory) => memory.id === id),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function linkMemories(cortex, from, to, relation, provenance = {}) {
|
|
50
|
+
const normalizedRelation = normalizeMemoryKind(relation);
|
|
51
|
+
const ts = normalizeMemoryTimestamp(provenance.ts ?? cortex.now());
|
|
52
|
+
const actor = normalizeMemoryActor(provenance.actor ?? cortex.actor);
|
|
53
|
+
const link = {
|
|
54
|
+
version: 1,
|
|
55
|
+
id: createMemoryId({
|
|
56
|
+
kind: 'link',
|
|
57
|
+
text: [from, normalizedRelation, to, provenance.confidence ?? ''].join('\u0001'),
|
|
58
|
+
ts,
|
|
59
|
+
actor,
|
|
60
|
+
}),
|
|
61
|
+
from,
|
|
62
|
+
to,
|
|
63
|
+
relation: normalizedRelation,
|
|
64
|
+
ts,
|
|
65
|
+
actor,
|
|
66
|
+
confidence: provenance.confidence,
|
|
67
|
+
};
|
|
68
|
+
const op = {
|
|
69
|
+
op: 'link',
|
|
70
|
+
from,
|
|
71
|
+
to,
|
|
72
|
+
relation: normalizedRelation,
|
|
73
|
+
confidence: provenance.confidence,
|
|
74
|
+
ts,
|
|
75
|
+
actor,
|
|
76
|
+
};
|
|
77
|
+
return applyWrite(cortex, op, { link });
|
|
78
|
+
}
|
|
79
|
+
function applyWrite(cortex, op, extra) {
|
|
80
|
+
const log = appendMemoryOp(cortex.log, op);
|
|
81
|
+
const memories = applyMemoryLog(log);
|
|
82
|
+
return {
|
|
83
|
+
...extra,
|
|
84
|
+
op,
|
|
85
|
+
cortex: {
|
|
86
|
+
version: 1,
|
|
87
|
+
actor: cortex.actor,
|
|
88
|
+
now: cortex.now,
|
|
89
|
+
log,
|
|
90
|
+
memories,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type MemoryKind = string;
|
|
2
|
+
export type MemoryLinkV1 = {
|
|
3
|
+
version: 1;
|
|
4
|
+
id: string;
|
|
5
|
+
from: string;
|
|
6
|
+
to: string;
|
|
7
|
+
relation: string;
|
|
8
|
+
ts: number;
|
|
9
|
+
actor: string;
|
|
10
|
+
confidence?: number;
|
|
11
|
+
};
|
|
12
|
+
export type MemoryEngramV1 = {
|
|
13
|
+
version: 1;
|
|
14
|
+
id: string;
|
|
15
|
+
kind: MemoryKind;
|
|
16
|
+
text: string;
|
|
17
|
+
labels: string[];
|
|
18
|
+
namespace?: string;
|
|
19
|
+
source?: string;
|
|
20
|
+
importance?: number;
|
|
21
|
+
confidence?: number;
|
|
22
|
+
ts: number;
|
|
23
|
+
actor: string;
|
|
24
|
+
links: MemoryLinkV1[];
|
|
25
|
+
};
|
|
26
|
+
export type MemoryInputV1 = {
|
|
27
|
+
kind: MemoryKind;
|
|
28
|
+
text: string;
|
|
29
|
+
labels?: string | readonly string[];
|
|
30
|
+
namespace?: string;
|
|
31
|
+
source?: string;
|
|
32
|
+
importance?: number;
|
|
33
|
+
confidence?: number;
|
|
34
|
+
ts?: number;
|
|
35
|
+
actor?: string;
|
|
36
|
+
};
|
|
37
|
+
export type MemoryProvenanceV1 = {
|
|
38
|
+
ts?: number;
|
|
39
|
+
actor?: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function createMemoryId(input: Pick<MemoryEngramV1, 'kind' | 'text' | 'ts' | 'actor'>): string;
|
|
42
|
+
export declare function normalizeMemoryInput(input: MemoryInputV1, provenance?: MemoryProvenanceV1): MemoryEngramV1;
|
|
43
|
+
export declare function normalizeMemoryKind(kind: string): string;
|
|
44
|
+
export declare function normalizeMemoryText(text: string): string;
|
|
45
|
+
export declare function normalizeMemoryNamespace(namespace?: string): string | undefined;
|
|
46
|
+
export declare function normalizeMemorySource(source?: string): string | undefined;
|
|
47
|
+
export declare function normalizeMemoryActor(actor: string): string;
|
|
48
|
+
export declare function normalizeMemoryTimestamp(ts: number): number;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { normalize } from '../tokenize.js';
|
|
2
|
+
import { normalizeMemoryLabel, validateMemoryLabels } from './label.js';
|
|
3
|
+
export function createMemoryId(input) {
|
|
4
|
+
const payload = [
|
|
5
|
+
normalizeMemoryKind(input.kind),
|
|
6
|
+
normalizeMemoryTextForId(input.text),
|
|
7
|
+
normalizeMemoryTimestamp(input.ts),
|
|
8
|
+
normalizeMemoryActor(input.actor),
|
|
9
|
+
].join('\u0001');
|
|
10
|
+
return `mem_${hash64Hex(payload)}`;
|
|
11
|
+
}
|
|
12
|
+
export function normalizeMemoryInput(input, provenance = {}) {
|
|
13
|
+
const kind = normalizeMemoryKind(input.kind);
|
|
14
|
+
if (!kind) {
|
|
15
|
+
throw new Error('Memory kind must be a non-empty string.');
|
|
16
|
+
}
|
|
17
|
+
const text = normalizeMemoryText(input.text);
|
|
18
|
+
if (!text) {
|
|
19
|
+
throw new Error('Memory text must be a non-empty string.');
|
|
20
|
+
}
|
|
21
|
+
const ts = normalizeMemoryTimestamp(input.ts ?? provenance.ts ?? 0);
|
|
22
|
+
const actor = normalizeMemoryActor(input.actor ?? provenance.actor ?? 'cortex');
|
|
23
|
+
const labels = validateMemoryLabels(input.labels);
|
|
24
|
+
const namespace = normalizeMemoryNamespace(input.namespace);
|
|
25
|
+
const source = normalizeMemorySource(input.source);
|
|
26
|
+
const importance = normalizeMemoryRatio(input.importance, 'importance');
|
|
27
|
+
const confidence = normalizeMemoryRatio(input.confidence, 'confidence');
|
|
28
|
+
return {
|
|
29
|
+
version: 1,
|
|
30
|
+
id: createMemoryId({ kind, text, ts, actor }),
|
|
31
|
+
kind,
|
|
32
|
+
text,
|
|
33
|
+
labels,
|
|
34
|
+
namespace,
|
|
35
|
+
source,
|
|
36
|
+
importance,
|
|
37
|
+
confidence,
|
|
38
|
+
ts,
|
|
39
|
+
actor,
|
|
40
|
+
links: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function normalizeMemoryKind(kind) {
|
|
44
|
+
return normalize(String(kind ?? '')).replace(/\s+/g, ' ').trim();
|
|
45
|
+
}
|
|
46
|
+
export function normalizeMemoryText(text) {
|
|
47
|
+
return String(text ?? '').trim();
|
|
48
|
+
}
|
|
49
|
+
export function normalizeMemoryNamespace(namespace) {
|
|
50
|
+
const normalized = normalizeMemoryLabel(namespace ?? '');
|
|
51
|
+
return normalized || undefined;
|
|
52
|
+
}
|
|
53
|
+
export function normalizeMemorySource(source) {
|
|
54
|
+
const normalized = normalize(String(source ?? '')).replace(/\s+/g, ' ').trim();
|
|
55
|
+
return normalized || undefined;
|
|
56
|
+
}
|
|
57
|
+
export function normalizeMemoryActor(actor) {
|
|
58
|
+
return String(actor ?? '').replace(/\s+/g, ' ').trim();
|
|
59
|
+
}
|
|
60
|
+
export function normalizeMemoryTimestamp(ts) {
|
|
61
|
+
const value = Number(ts);
|
|
62
|
+
if (!Number.isFinite(value)) {
|
|
63
|
+
throw new Error('Memory timestamp must be a finite number.');
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function normalizeMemoryRatio(value, name) {
|
|
68
|
+
if (value === undefined || value === null)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (!Number.isFinite(value)) {
|
|
71
|
+
throw new Error(`Memory ${name} must be a finite number.`);
|
|
72
|
+
}
|
|
73
|
+
if (value < 0)
|
|
74
|
+
return 0;
|
|
75
|
+
if (value > 1)
|
|
76
|
+
return 1;
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function normalizeMemoryTextForId(text) {
|
|
80
|
+
return normalize(String(text ?? '')).replace(/\s+/g, ' ').trim();
|
|
81
|
+
}
|
|
82
|
+
function hash64Hex(input) {
|
|
83
|
+
let hash = 0xcbf29ce484222325n;
|
|
84
|
+
const prime = 0x100000001b3n;
|
|
85
|
+
for (const char of input) {
|
|
86
|
+
hash ^= BigInt(char.codePointAt(0) ?? 0);
|
|
87
|
+
hash = (hash * prime) & 0xffffffffffffffffn;
|
|
88
|
+
}
|
|
89
|
+
return hash.toString(16).padStart(16, '0');
|
|
90
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { computeEdgeId, computeNodeId, normalizeClaimLabel } from '../graph/claim_graph.js';
|
|
2
|
+
import { validateMemoryLabels } from './label.js';
|
|
3
|
+
export function memoryToClaimOps(memory) {
|
|
4
|
+
const nodeOps = new Map();
|
|
5
|
+
const edgeOps = new Map();
|
|
6
|
+
const memoryLabel = memoryNodeLabel(memory.id);
|
|
7
|
+
upsertNode(nodeOps, memoryLabel, memory.ts, memory.actor);
|
|
8
|
+
for (const label of validateMemoryLabels(memory.labels)) {
|
|
9
|
+
const entityLabel = normalizeClaimLabel(label);
|
|
10
|
+
if (!entityLabel)
|
|
11
|
+
continue;
|
|
12
|
+
upsertNode(nodeOps, entityLabel, memory.ts, memory.actor);
|
|
13
|
+
upsertEdge(edgeOps, memoryLabel, 'mentions', entityLabel, memory.ts, memory.actor);
|
|
14
|
+
}
|
|
15
|
+
for (const link of sortedLinks(memory.links)) {
|
|
16
|
+
const relation = normalizeClaimLabel(link.relation);
|
|
17
|
+
if (!relation)
|
|
18
|
+
continue;
|
|
19
|
+
const targetLabel = memoryNodeLabel(link.to);
|
|
20
|
+
upsertNode(nodeOps, targetLabel, link.ts, link.actor);
|
|
21
|
+
upsertEdge(edgeOps, memoryLabel, relation, targetLabel, link.ts, link.actor);
|
|
22
|
+
}
|
|
23
|
+
return [...nodeOps.values(), ...edgeOps.values()];
|
|
24
|
+
}
|
|
25
|
+
function memoryNodeLabel(id) {
|
|
26
|
+
return normalizeClaimLabel(`memory ${id}`);
|
|
27
|
+
}
|
|
28
|
+
function upsertNode(nodeOps, label, ts, actor) {
|
|
29
|
+
if (!label)
|
|
30
|
+
return;
|
|
31
|
+
const id = computeNodeId(label);
|
|
32
|
+
if (nodeOps.has(id))
|
|
33
|
+
return;
|
|
34
|
+
nodeOps.set(id, {
|
|
35
|
+
op: 'upsert_node',
|
|
36
|
+
id,
|
|
37
|
+
label,
|
|
38
|
+
ts,
|
|
39
|
+
actor,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function upsertEdge(edgeOps, fromLabel, relation, toLabel, ts, actor) {
|
|
43
|
+
if (!fromLabel || !relation || !toLabel)
|
|
44
|
+
return;
|
|
45
|
+
const from = computeNodeId(fromLabel);
|
|
46
|
+
const to = computeNodeId(toLabel);
|
|
47
|
+
const id = computeEdgeId(from, relation, to);
|
|
48
|
+
if (edgeOps.has(id))
|
|
49
|
+
return;
|
|
50
|
+
edgeOps.set(id, {
|
|
51
|
+
op: 'add_edge',
|
|
52
|
+
from,
|
|
53
|
+
p: relation,
|
|
54
|
+
to,
|
|
55
|
+
ts,
|
|
56
|
+
actor,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function sortedLinks(memoryLinks) {
|
|
60
|
+
return [...memoryLinks].sort((a, b) => a.ts - b.ts ||
|
|
61
|
+
a.actor.localeCompare(b.actor) ||
|
|
62
|
+
a.relation.localeCompare(b.relation) ||
|
|
63
|
+
a.to.localeCompare(b.to) ||
|
|
64
|
+
a.id.localeCompare(b.id));
|
|
65
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { normalizeMemoryLabel, validateMemoryLabels, matchesMemoryLabels, normalizeMemoryLabelTokens, } from './label.js';
|
|
2
|
+
export type { MemoryKind, MemoryLinkV1, MemoryLinkV1 as MemoryLink, MemoryEngramV1, MemoryEngramV1 as MemoryEngram, MemoryInputV1, MemoryInputV1 as MemoryInput, MemoryProvenanceV1, MemoryProvenanceV1 as MemoryProvenance, } from './engram.js';
|
|
3
|
+
export { createMemoryId, normalizeMemoryInput, normalizeMemoryKind, normalizeMemoryText, normalizeMemoryNamespace, normalizeMemorySource, normalizeMemoryActor, normalizeMemoryTimestamp, } from './engram.js';
|
|
4
|
+
export type { MemoryLogV1, MemoryOpV1 } from './log.js';
|
|
5
|
+
export type { MemoryLogV1 as MemoryLog, MemoryOpV1 as MemoryOp, } from './log.js';
|
|
6
|
+
export { createMemoryLog, appendMemoryOp, mergeMemoryLogs, serializeMemoryLog, deserializeMemoryLog, applyMemoryLog, } from './log.js';
|
|
7
|
+
export type { CortexV1, CortexV1 as Cortex, CortexWriteResult, } from './cortex.js';
|
|
8
|
+
export { createCortex, remember, forget, labelMemory, linkMemories } from './cortex.js';
|
|
9
|
+
export type { RecallOptionsV1, RecallOptionsV1 as RecallOptions, MemoryRecallHitV1, MemoryRecallHitV1 as MemoryRecallHit, } from './recall.js';
|
|
10
|
+
export { recall } from './recall.js';
|
|
11
|
+
export type { ConsolidateMemoriesOptionsV1, ConsolidateMemoriesOptionsV1 as ConsolidateMemoriesOptions, } from './consolidate.js';
|
|
12
|
+
export { consolidateMemories } from './consolidate.js';
|
|
13
|
+
export { memoryToClaimOps } from './graph_adapter.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { normalizeMemoryLabel, validateMemoryLabels, matchesMemoryLabels, normalizeMemoryLabelTokens, } from './label.js';
|
|
2
|
+
export { createMemoryId, normalizeMemoryInput, normalizeMemoryKind, normalizeMemoryText, normalizeMemoryNamespace, normalizeMemorySource, normalizeMemoryActor, normalizeMemoryTimestamp, } from './engram.js';
|
|
3
|
+
export { createMemoryLog, appendMemoryOp, mergeMemoryLogs, serializeMemoryLog, deserializeMemoryLog, applyMemoryLog, } from './log.js';
|
|
4
|
+
export { createCortex, remember, forget, labelMemory, linkMemories } from './cortex.js';
|
|
5
|
+
export { recall } from './recall.js';
|
|
6
|
+
export { consolidateMemories } from './consolidate.js';
|
|
7
|
+
export { memoryToClaimOps } from './graph_adapter.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function normalizeMemoryLabel(label: string): string;
|
|
2
|
+
export declare function validateMemoryLabels(labels?: string | readonly string[] | null): string[];
|
|
3
|
+
export declare function matchesMemoryLabels(memoryLabels?: string[] | string | null, requestedLabels?: string | readonly string[] | null): boolean;
|
|
4
|
+
export declare function normalizeMemoryLabelTokens(label: string): string[];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { normalize } from '../tokenize.js';
|
|
2
|
+
export function normalizeMemoryLabel(label) {
|
|
3
|
+
if (typeof label !== 'string')
|
|
4
|
+
return '';
|
|
5
|
+
const lowered = label.normalize('NFKD').replace(/\p{M}+/gu, '').toLowerCase().trim();
|
|
6
|
+
if (!lowered)
|
|
7
|
+
return '';
|
|
8
|
+
const dotted = lowered
|
|
9
|
+
.replace(/\s+/g, '.')
|
|
10
|
+
.replace(/[\u2010-\u2015/\\:;|>]+/gu, '.')
|
|
11
|
+
.replace(/[^\p{L}\p{N}._-]+/gu, '.')
|
|
12
|
+
.replace(/\.{2,}/g, '.')
|
|
13
|
+
.replace(/^\.|\.$/g, '');
|
|
14
|
+
return dotted
|
|
15
|
+
.split('.')
|
|
16
|
+
.map((segment) => segment.trim())
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join('.');
|
|
19
|
+
}
|
|
20
|
+
export function validateMemoryLabels(labels) {
|
|
21
|
+
if (labels === undefined || labels === null)
|
|
22
|
+
return [];
|
|
23
|
+
const values = typeof labels === 'string' ? [labels] : labels;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const label of values) {
|
|
27
|
+
if (typeof label !== 'string') {
|
|
28
|
+
throw new Error('Memory labels must be strings.');
|
|
29
|
+
}
|
|
30
|
+
const normalized = normalizeMemoryLabel(label);
|
|
31
|
+
if (!normalized || seen.has(normalized))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(normalized);
|
|
34
|
+
out.push(normalized);
|
|
35
|
+
}
|
|
36
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
export function matchesMemoryLabels(memoryLabels, requestedLabels) {
|
|
40
|
+
const filters = validateMemoryLabels(requestedLabels);
|
|
41
|
+
if (filters.length === 0)
|
|
42
|
+
return true;
|
|
43
|
+
const labels = validateMemoryLabels(memoryLabels);
|
|
44
|
+
if (labels.length === 0)
|
|
45
|
+
return false;
|
|
46
|
+
return filters.some((filter) => labels.some((label) => label === filter || label.startsWith(`${filter}.`)));
|
|
47
|
+
}
|
|
48
|
+
export function normalizeMemoryLabelTokens(label) {
|
|
49
|
+
return normalizeMemoryLabel(label)
|
|
50
|
+
.split('.')
|
|
51
|
+
.map((part) => normalize(part).replace(/\s+/g, ' ').trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MemoryEngramV1 } from './engram.js';
|
|
2
|
+
export type MemoryOpV1 = {
|
|
3
|
+
op: 'remember';
|
|
4
|
+
ts: number;
|
|
5
|
+
actor: string;
|
|
6
|
+
memory: MemoryEngramV1;
|
|
7
|
+
} | {
|
|
8
|
+
op: 'forget';
|
|
9
|
+
ts: number;
|
|
10
|
+
actor: string;
|
|
11
|
+
id: string;
|
|
12
|
+
} | {
|
|
13
|
+
op: 'label';
|
|
14
|
+
ts: number;
|
|
15
|
+
actor: string;
|
|
16
|
+
id: string;
|
|
17
|
+
labels: string[];
|
|
18
|
+
} | {
|
|
19
|
+
op: 'link';
|
|
20
|
+
ts: number;
|
|
21
|
+
actor: string;
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
relation: string;
|
|
25
|
+
confidence?: number;
|
|
26
|
+
};
|
|
27
|
+
export type MemoryLogV1 = {
|
|
28
|
+
version: 1;
|
|
29
|
+
ops: MemoryOpV1[];
|
|
30
|
+
};
|
|
31
|
+
export declare function createMemoryLog(): MemoryLogV1;
|
|
32
|
+
export declare function appendMemoryOp(log: MemoryLogV1, op: MemoryOpV1): MemoryLogV1;
|
|
33
|
+
export declare function mergeMemoryLogs(a: MemoryLogV1, b: MemoryLogV1): MemoryLogV1;
|
|
34
|
+
export declare function serializeMemoryLog(log: MemoryLogV1): Uint8Array;
|
|
35
|
+
export declare function deserializeMemoryLog(data: Uint8Array): MemoryLogV1;
|
|
36
|
+
export declare function applyMemoryLog(log: MemoryLogV1): MemoryEngramV1[];
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { getTextDecoder, getTextEncoder } from '../utils/utf8.js';
|
|
2
|
+
import { createMemoryId, normalizeMemoryActor, normalizeMemoryInput, normalizeMemoryKind, normalizeMemoryNamespace, normalizeMemorySource, normalizeMemoryTimestamp, } from './engram.js';
|
|
3
|
+
import { matchesMemoryLabels, validateMemoryLabels } from './label.js';
|
|
4
|
+
export function createMemoryLog() {
|
|
5
|
+
return { version: 1, ops: [] };
|
|
6
|
+
}
|
|
7
|
+
export function appendMemoryOp(log, op) {
|
|
8
|
+
return { version: 1, ops: [...log.ops, op] };
|
|
9
|
+
}
|
|
10
|
+
export function mergeMemoryLogs(a, b) {
|
|
11
|
+
return { version: 1, ops: [...a.ops, ...b.ops].sort(compareMemoryOps) };
|
|
12
|
+
}
|
|
13
|
+
export function serializeMemoryLog(log) {
|
|
14
|
+
const enc = getTextEncoder();
|
|
15
|
+
return enc.encode(JSON.stringify(normalizeMemoryLog(log)));
|
|
16
|
+
}
|
|
17
|
+
export function deserializeMemoryLog(data) {
|
|
18
|
+
const dec = getTextDecoder();
|
|
19
|
+
const parsed = JSON.parse(dec.decode(data));
|
|
20
|
+
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.ops)) {
|
|
21
|
+
throw new Error('Invalid MemoryLog payload');
|
|
22
|
+
}
|
|
23
|
+
return normalizeMemoryLog({ version: 1, ops: parsed.ops });
|
|
24
|
+
}
|
|
25
|
+
export function applyMemoryLog(log) {
|
|
26
|
+
const memories = new Map();
|
|
27
|
+
const tombstones = new Set();
|
|
28
|
+
const pendingLabels = new Map();
|
|
29
|
+
const pendingLinks = new Map();
|
|
30
|
+
for (const op of [...log.ops].sort(compareMemoryOps)) {
|
|
31
|
+
if (op.op === 'remember') {
|
|
32
|
+
if (tombstones.has(op.memory.id))
|
|
33
|
+
continue;
|
|
34
|
+
const memory = normalizeMemoryInput(op.memory, {
|
|
35
|
+
actor: op.actor,
|
|
36
|
+
ts: op.ts,
|
|
37
|
+
});
|
|
38
|
+
const mergedLabels = mergeLabels(memory.labels, pendingLabels.get(memory.id));
|
|
39
|
+
const mergedLinks = mergeLinks(memory.id, memory.links, pendingLinks.get(memory.id));
|
|
40
|
+
memories.set(memory.id, {
|
|
41
|
+
...memory,
|
|
42
|
+
labels: mergedLabels,
|
|
43
|
+
links: mergedLinks,
|
|
44
|
+
});
|
|
45
|
+
pendingLabels.delete(memory.id);
|
|
46
|
+
pendingLinks.delete(memory.id);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (op.op === 'forget') {
|
|
50
|
+
memories.delete(op.id);
|
|
51
|
+
tombstones.add(op.id);
|
|
52
|
+
pendingLabels.delete(op.id);
|
|
53
|
+
pendingLinks.delete(op.id);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (op.op === 'label') {
|
|
57
|
+
if (tombstones.has(op.id))
|
|
58
|
+
continue;
|
|
59
|
+
const labels = validateMemoryLabels(op.labels);
|
|
60
|
+
const memory = memories.get(op.id);
|
|
61
|
+
if (memory) {
|
|
62
|
+
memories.set(op.id, {
|
|
63
|
+
...memory,
|
|
64
|
+
labels: validateMemoryLabels([...memory.labels, ...labels]),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else if (labels.length > 0) {
|
|
68
|
+
const pending = pendingLabels.get(op.id) ?? new Set();
|
|
69
|
+
for (const label of labels)
|
|
70
|
+
pending.add(label);
|
|
71
|
+
pendingLabels.set(op.id, pending);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (tombstones.has(op.from))
|
|
76
|
+
continue;
|
|
77
|
+
const link = createLink(op);
|
|
78
|
+
const memory = memories.get(op.from);
|
|
79
|
+
if (memory) {
|
|
80
|
+
memories.set(op.from, {
|
|
81
|
+
...memory,
|
|
82
|
+
links: mergeLinks(memory.id, memory.links, new Map([[link.id, link]])),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const pending = pendingLinks.get(op.from) ?? new Map();
|
|
87
|
+
pending.set(link.id, link);
|
|
88
|
+
pendingLinks.set(op.from, pending);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [...memories.values()]
|
|
92
|
+
.map((memory) => ({
|
|
93
|
+
...memory,
|
|
94
|
+
labels: [...memory.labels].sort((a, b) => a.localeCompare(b)),
|
|
95
|
+
links: [...memory.links].sort((a, b) => a.id.localeCompare(b.id)),
|
|
96
|
+
}))
|
|
97
|
+
.sort((a, b) => a.ts - b.ts || a.id.localeCompare(b.id));
|
|
98
|
+
}
|
|
99
|
+
function normalizeMemoryLog(log) {
|
|
100
|
+
return {
|
|
101
|
+
version: 1,
|
|
102
|
+
ops: [...log.ops].sort(compareMemoryOps).map((op) => normalizeMemoryOp(op)),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function normalizeMemoryOp(op) {
|
|
106
|
+
if (op.op === 'remember') {
|
|
107
|
+
const memory = normalizeMemoryInput(op.memory, {
|
|
108
|
+
actor: op.actor,
|
|
109
|
+
ts: op.ts,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
op: 'remember',
|
|
113
|
+
ts: normalizeMemoryTimestamp(op.ts),
|
|
114
|
+
actor: normalizeMemoryActor(op.actor),
|
|
115
|
+
memory,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (op.op === 'forget') {
|
|
119
|
+
return {
|
|
120
|
+
op: 'forget',
|
|
121
|
+
ts: normalizeMemoryTimestamp(op.ts),
|
|
122
|
+
actor: normalizeMemoryActor(op.actor),
|
|
123
|
+
id: String(op.id),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (op.op === 'label') {
|
|
127
|
+
return {
|
|
128
|
+
op: 'label',
|
|
129
|
+
ts: normalizeMemoryTimestamp(op.ts),
|
|
130
|
+
actor: normalizeMemoryActor(op.actor),
|
|
131
|
+
id: String(op.id),
|
|
132
|
+
labels: validateMemoryLabels(op.labels),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
op: 'link',
|
|
137
|
+
ts: normalizeMemoryTimestamp(op.ts),
|
|
138
|
+
actor: normalizeMemoryActor(op.actor),
|
|
139
|
+
from: String(op.from),
|
|
140
|
+
to: String(op.to),
|
|
141
|
+
relation: normalizeMemoryKind(op.relation),
|
|
142
|
+
confidence: op.confidence === undefined ? undefined : clamp01(op.confidence),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function compareMemoryOps(a, b) {
|
|
146
|
+
const tsA = a.ts;
|
|
147
|
+
const tsB = b.ts;
|
|
148
|
+
if (tsA !== tsB)
|
|
149
|
+
return tsA - tsB;
|
|
150
|
+
const actorCmp = a.actor.localeCompare(b.actor);
|
|
151
|
+
if (actorCmp !== 0)
|
|
152
|
+
return actorCmp;
|
|
153
|
+
const rankA = memoryOpRank(a.op);
|
|
154
|
+
const rankB = memoryOpRank(b.op);
|
|
155
|
+
if (rankA !== rankB)
|
|
156
|
+
return rankA - rankB;
|
|
157
|
+
return stableSerializeMemoryOp(a).localeCompare(stableSerializeMemoryOp(b));
|
|
158
|
+
}
|
|
159
|
+
function memoryOpRank(op) {
|
|
160
|
+
switch (op) {
|
|
161
|
+
case 'remember':
|
|
162
|
+
return 0;
|
|
163
|
+
case 'label':
|
|
164
|
+
return 1;
|
|
165
|
+
case 'link':
|
|
166
|
+
return 2;
|
|
167
|
+
case 'forget':
|
|
168
|
+
return 3;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function stableSerializeMemoryOp(op) {
|
|
172
|
+
if (op.op === 'remember') {
|
|
173
|
+
const memory = op.memory;
|
|
174
|
+
return [
|
|
175
|
+
'remember',
|
|
176
|
+
memory.id,
|
|
177
|
+
memory.kind,
|
|
178
|
+
memory.text,
|
|
179
|
+
memory.labels.join(','),
|
|
180
|
+
memory.namespace ?? '',
|
|
181
|
+
memory.source ?? '',
|
|
182
|
+
memory.importance ?? '',
|
|
183
|
+
memory.confidence ?? '',
|
|
184
|
+
].join('|');
|
|
185
|
+
}
|
|
186
|
+
if (op.op === 'forget') {
|
|
187
|
+
return ['forget', op.id].join('|');
|
|
188
|
+
}
|
|
189
|
+
if (op.op === 'label') {
|
|
190
|
+
return ['label', op.id, op.labels.join(',')].join('|');
|
|
191
|
+
}
|
|
192
|
+
return [
|
|
193
|
+
'link',
|
|
194
|
+
op.from,
|
|
195
|
+
op.to,
|
|
196
|
+
op.relation,
|
|
197
|
+
op.confidence ?? '',
|
|
198
|
+
].join('|');
|
|
199
|
+
}
|
|
200
|
+
function mergeLabels(labels, pending) {
|
|
201
|
+
if (!pending || pending.size === 0)
|
|
202
|
+
return [...labels].sort((a, b) => a.localeCompare(b));
|
|
203
|
+
return validateMemoryLabels([...labels, ...pending]);
|
|
204
|
+
}
|
|
205
|
+
function mergeLinks(fromId, links, pending) {
|
|
206
|
+
const map = new Map(links.map((link) => [link.id, link]));
|
|
207
|
+
if (pending) {
|
|
208
|
+
for (const [id, link] of pending) {
|
|
209
|
+
if (link.from === fromId) {
|
|
210
|
+
map.set(id, link);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return [...map.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
215
|
+
}
|
|
216
|
+
function createLink(op) {
|
|
217
|
+
return {
|
|
218
|
+
version: 1,
|
|
219
|
+
id: createMemoryId({
|
|
220
|
+
kind: 'link',
|
|
221
|
+
text: [op.from, op.relation, op.to, op.confidence ?? ''].join('\u0001'),
|
|
222
|
+
ts: op.ts,
|
|
223
|
+
actor: op.actor,
|
|
224
|
+
}),
|
|
225
|
+
from: op.from,
|
|
226
|
+
to: op.to,
|
|
227
|
+
relation: op.relation,
|
|
228
|
+
ts: op.ts,
|
|
229
|
+
actor: op.actor,
|
|
230
|
+
confidence: op.confidence,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function clamp01(value) {
|
|
234
|
+
if (!Number.isFinite(value))
|
|
235
|
+
return 0;
|
|
236
|
+
if (value < 0)
|
|
237
|
+
return 0;
|
|
238
|
+
if (value > 1)
|
|
239
|
+
return 1;
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CortexV1 } from './cortex.js';
|
|
2
|
+
import type { MemoryEngramV1 } from './engram.js';
|
|
3
|
+
export type RecallOptionsV1 = {
|
|
4
|
+
topK?: number;
|
|
5
|
+
kind?: string | readonly string[];
|
|
6
|
+
labels?: string | readonly string[];
|
|
7
|
+
namespace?: string | readonly string[];
|
|
8
|
+
source?: string | readonly string[];
|
|
9
|
+
since?: number;
|
|
10
|
+
until?: number;
|
|
11
|
+
minImportance?: number;
|
|
12
|
+
minConfidence?: number;
|
|
13
|
+
};
|
|
14
|
+
export type MemoryRecallHitV1 = MemoryEngramV1 & {
|
|
15
|
+
score: number;
|
|
16
|
+
lexicalScore: number;
|
|
17
|
+
metadataScore: number;
|
|
18
|
+
kindScore: number;
|
|
19
|
+
labelScore: number;
|
|
20
|
+
namespaceScore: number;
|
|
21
|
+
sourceScore: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function recall(cortexOrMemories: CortexV1 | readonly MemoryEngramV1[], query: string, opts?: RecallOptionsV1): MemoryRecallHitV1[];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { tokenize, normalize } from '../tokenize.js';
|
|
2
|
+
import { matchesMemoryLabels, validateMemoryLabels } from './label.js';
|
|
3
|
+
import { normalizeMemoryLabel } from './label.js';
|
|
4
|
+
export function recall(cortexOrMemories, query, opts = {}) {
|
|
5
|
+
const memories = isCortex(cortexOrMemories)
|
|
6
|
+
? cortexOrMemories.memories
|
|
7
|
+
: cortexOrMemories;
|
|
8
|
+
const queryTerms = tokenize(query).map((token) => token.term);
|
|
9
|
+
const hasQuery = queryTerms.length > 0;
|
|
10
|
+
const hasFilters = Boolean(opts.kind ||
|
|
11
|
+
opts.labels ||
|
|
12
|
+
opts.namespace ||
|
|
13
|
+
opts.source ||
|
|
14
|
+
opts.since !== undefined ||
|
|
15
|
+
opts.until !== undefined ||
|
|
16
|
+
opts.minImportance !== undefined ||
|
|
17
|
+
opts.minConfidence !== undefined);
|
|
18
|
+
if (!hasQuery && !hasFilters)
|
|
19
|
+
return [];
|
|
20
|
+
const kindFilters = normalizeKinds(opts.kind);
|
|
21
|
+
const namespaceFilters = normalizeHierarchicalFilters(opts.namespace);
|
|
22
|
+
const sourceFilters = normalizeSourceFilters(opts.source);
|
|
23
|
+
const labelFilters = validateMemoryLabels(opts.labels);
|
|
24
|
+
const hits = memories
|
|
25
|
+
.filter((memory) => passesFilters(memory, {
|
|
26
|
+
kindFilters,
|
|
27
|
+
labelFilters,
|
|
28
|
+
namespaceFilters,
|
|
29
|
+
sourceFilters,
|
|
30
|
+
since: opts.since,
|
|
31
|
+
until: opts.until,
|
|
32
|
+
minImportance: opts.minImportance,
|
|
33
|
+
minConfidence: opts.minConfidence,
|
|
34
|
+
}))
|
|
35
|
+
.map((memory) => scoreMemory(memory, queryTerms, {
|
|
36
|
+
kindFilters,
|
|
37
|
+
labelFilters,
|
|
38
|
+
namespaceFilters,
|
|
39
|
+
sourceFilters,
|
|
40
|
+
}))
|
|
41
|
+
.sort((a, b) => b.score - a.score || b.ts - a.ts || a.id.localeCompare(b.id));
|
|
42
|
+
const topK = Number.isInteger(opts.topK) && opts.topK > 0 ? opts.topK : 10;
|
|
43
|
+
const visibleHits = hasQuery ? hits.filter((hit) => hit.score > 0) : hits;
|
|
44
|
+
return visibleHits.slice(0, topK);
|
|
45
|
+
}
|
|
46
|
+
function passesFilters(memory, filters) {
|
|
47
|
+
if (filters.kindFilters.length > 0 && !filters.kindFilters.includes(normalizeKind(memory.kind))) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (filters.labelFilters.length > 0 && !matchesMemoryLabels(memory.labels, filters.labelFilters)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (filters.namespaceFilters.length > 0 && !matchesHierarchicalValue(memory.namespace, filters.namespaceFilters)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (filters.sourceFilters.length > 0 && !matchesSource(memory.source, filters.sourceFilters)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (filters.since !== undefined && memory.ts < filters.since)
|
|
60
|
+
return false;
|
|
61
|
+
if (filters.until !== undefined && memory.ts > filters.until)
|
|
62
|
+
return false;
|
|
63
|
+
if (filters.minImportance !== undefined && valueOrDefault(memory.importance, 0.5) < filters.minImportance)
|
|
64
|
+
return false;
|
|
65
|
+
if (filters.minConfidence !== undefined && valueOrDefault(memory.confidence, 0.5) < filters.minConfidence)
|
|
66
|
+
return false;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function scoreMemory(memory, queryTerms, filters) {
|
|
70
|
+
const lexicalScore = scoreTokenOverlap(queryTerms, tokenize(memory.text).map((token) => token.term));
|
|
71
|
+
const kindScore = scoreKind(memory, queryTerms, filters.kindFilters);
|
|
72
|
+
const labelScore = scoreLabelSignal(memory, queryTerms, filters.labelFilters);
|
|
73
|
+
const namespaceScore = scoreNamespaceSignal(memory, queryTerms, filters.namespaceFilters);
|
|
74
|
+
const sourceScore = scoreSourceSignal(memory, queryTerms, filters.sourceFilters);
|
|
75
|
+
const metadataScore = kindScore * 0.2 + labelScore * 0.1 + namespaceScore * 0.15 + sourceScore * 0.1;
|
|
76
|
+
const score = lexicalScore * 0.45 + metadataScore;
|
|
77
|
+
return {
|
|
78
|
+
...memory,
|
|
79
|
+
score,
|
|
80
|
+
lexicalScore,
|
|
81
|
+
metadataScore,
|
|
82
|
+
kindScore,
|
|
83
|
+
labelScore,
|
|
84
|
+
namespaceScore,
|
|
85
|
+
sourceScore,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function scoreKind(memory, queryTerms, kindFilters) {
|
|
89
|
+
const kind = normalizeKind(memory.kind);
|
|
90
|
+
if (kindFilters.length > 0) {
|
|
91
|
+
return kindFilters.includes(kind) ? 1 : 0;
|
|
92
|
+
}
|
|
93
|
+
return scoreTokenOverlap(queryTerms, tokenize(kind).map((token) => token.term));
|
|
94
|
+
}
|
|
95
|
+
function scoreLabelSignal(memory, queryTerms, labelFilters) {
|
|
96
|
+
if (labelFilters.length > 0) {
|
|
97
|
+
const matched = labelFilters.filter((filter) => memory.labels.some((label) => label === filter || label.startsWith(`${filter}.`)));
|
|
98
|
+
return matched.length / labelFilters.length;
|
|
99
|
+
}
|
|
100
|
+
return scoreTokenOverlap(queryTerms, tokenize(memory.labels.join(' ')).map((token) => token.term));
|
|
101
|
+
}
|
|
102
|
+
function scoreNamespaceSignal(memory, queryTerms, namespaceFilters) {
|
|
103
|
+
if (namespaceFilters.length > 0) {
|
|
104
|
+
return matchesHierarchicalValue(memory.namespace, namespaceFilters) ? 1 : 0;
|
|
105
|
+
}
|
|
106
|
+
return scoreTokenOverlap(queryTerms, tokenize(String(memory.namespace ?? '')).map((token) => token.term));
|
|
107
|
+
}
|
|
108
|
+
function scoreSourceSignal(memory, queryTerms, sourceFilters) {
|
|
109
|
+
if (sourceFilters.length > 0) {
|
|
110
|
+
return matchesSource(memory.source, sourceFilters) ? 1 : 0;
|
|
111
|
+
}
|
|
112
|
+
return scoreTokenOverlap(queryTerms, tokenize(String(memory.source ?? '')).map((token) => token.term));
|
|
113
|
+
}
|
|
114
|
+
function scoreTokenOverlap(queryTerms, fieldTerms) {
|
|
115
|
+
const q = new Set(queryTerms.filter(Boolean));
|
|
116
|
+
const f = new Set(fieldTerms.filter(Boolean));
|
|
117
|
+
if (q.size === 0 || f.size === 0)
|
|
118
|
+
return 0;
|
|
119
|
+
let matched = 0;
|
|
120
|
+
for (const term of q) {
|
|
121
|
+
if (f.has(term))
|
|
122
|
+
matched++;
|
|
123
|
+
}
|
|
124
|
+
return matched / q.size;
|
|
125
|
+
}
|
|
126
|
+
function normalizeKinds(kind) {
|
|
127
|
+
if (kind === undefined)
|
|
128
|
+
return [];
|
|
129
|
+
return uniqueSorted((Array.isArray(kind) ? kind : [kind]).map(normalizeKind).filter(Boolean));
|
|
130
|
+
}
|
|
131
|
+
function normalizeHierarchicalFilters(value) {
|
|
132
|
+
if (value === undefined)
|
|
133
|
+
return [];
|
|
134
|
+
return uniqueSorted((Array.isArray(value) ? value : [value]).map(normalizeMemoryLabel).filter(Boolean));
|
|
135
|
+
}
|
|
136
|
+
function normalizeSourceFilters(value) {
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
return [];
|
|
139
|
+
return uniqueSorted((Array.isArray(value) ? value : [value]).map(normalizeSource).filter(Boolean));
|
|
140
|
+
}
|
|
141
|
+
function normalizeKind(kind) {
|
|
142
|
+
return normalize(kind).replace(/\s+/g, ' ').trim();
|
|
143
|
+
}
|
|
144
|
+
function normalizeSource(source) {
|
|
145
|
+
return normalize(String(source ?? '')).replace(/\s+/g, ' ').trim();
|
|
146
|
+
}
|
|
147
|
+
function matchesHierarchicalValue(value, filters) {
|
|
148
|
+
if (!value)
|
|
149
|
+
return false;
|
|
150
|
+
const normalized = normalizeMemoryLabel(value);
|
|
151
|
+
return filters.some((filter) => normalized === filter || normalized.startsWith(`${filter}.`));
|
|
152
|
+
}
|
|
153
|
+
function matchesSource(value, filters) {
|
|
154
|
+
if (!value)
|
|
155
|
+
return false;
|
|
156
|
+
const normalized = normalizeSource(value);
|
|
157
|
+
return filters.includes(normalized);
|
|
158
|
+
}
|
|
159
|
+
function uniqueSorted(values) {
|
|
160
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
161
|
+
}
|
|
162
|
+
function valueOrDefault(value, fallback) {
|
|
163
|
+
return Number.isFinite(value) ? value : fallback;
|
|
164
|
+
}
|
|
165
|
+
function isCortex(input) {
|
|
166
|
+
return !Array.isArray(input) && typeof input === 'object' && input !== null && 'memories' in input;
|
|
167
|
+
}
|
package/dist/query.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knolo/core",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local-first knowledge packs for small LLMs.",
|
|
6
6
|
"keywords": [
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"build": "tsc -p tsconfig.json",
|
|
35
35
|
"prepublishOnly": "npm run build",
|
|
36
36
|
"smoke": "node scripts/smoke.mjs",
|
|
37
|
-
"test": "npm run build && node scripts/check-runtime-no-node.mjs && node scripts/test.mjs",
|
|
37
|
+
"test": "npm run build && node scripts/check-runtime-no-node.mjs && node --test test/*.test.mjs && node scripts/test.mjs",
|
|
38
38
|
"format": "prettier --write src/agent.ts src/pack.ts src/pack.runtime.ts src/pack.node.ts src/node.ts src/builder.ts src/index.ts scripts/test.mjs scripts/check-runtime-no-node.mjs ../../README.md README.md",
|
|
39
39
|
"format:check": "prettier --check src/agent.ts src/pack.ts src/pack.runtime.ts src/pack.node.ts src/node.ts src/builder.ts src/index.ts scripts/test.mjs scripts/check-runtime-no-node.mjs ../../README.md README.md",
|
|
40
40
|
"check:runtime-no-node": "node scripts/check-runtime-no-node.mjs"
|