@hviana/sema 0.1.6 → 0.1.7
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/dist/example/train_base.js +18 -5
- package/dist/src/geometry.d.ts +13 -2
- package/dist/src/geometry.js +89 -2
- package/dist/src/mind/attention.js +11 -7
- package/dist/src/mind/mechanisms/recall.js +26 -11
- package/dist/src/mind/mind.d.ts +75 -11
- package/dist/src/mind/mind.js +151 -18
- package/dist/src/mind/primitives.d.ts +13 -2
- package/dist/src/mind/primitives.js +27 -5
- package/dist/src/mind/recognition.js +21 -8
- package/dist/src/mind/traverse.d.ts +13 -0
- package/dist/src/mind/traverse.js +41 -0
- package/dist/src/mind/types.d.ts +23 -21
- package/dist/src/store-sqlite.d.ts +2 -0
- package/dist/src/store-sqlite.js +6 -0
- package/dist/src/store.d.ts +11 -0
- package/dist/src/store.js +13 -2
- package/example/train_base.ts +20 -5
- package/index.html +65 -0
- package/package.json +1 -1
- package/src/geometry.ts +93 -1
- package/src/mind/attention.ts +11 -6
- package/src/mind/mechanisms/recall.ts +29 -13
- package/src/mind/mind.ts +250 -19
- package/src/mind/primitives.ts +43 -6
- package/src/mind/recognition.ts +26 -8
- package/src/mind/traverse.ts +47 -0
- package/src/mind/types.ts +20 -21
- package/src/store-sqlite.ts +8 -0
- package/src/store.ts +19 -2
- package/test/13-conversation.test.mjs +190 -20
|
@@ -1487,9 +1487,22 @@ async function main() {
|
|
|
1487
1487
|
const checkpoint = () => mind.save();
|
|
1488
1488
|
/** Run index maintenance: compact (remove garbage) then repair (fill gaps).
|
|
1489
1489
|
* Both are idempotent — running twice produces the same result as once.
|
|
1490
|
-
* Compaction frees index space first; repair then adds back
|
|
1491
|
-
* whose
|
|
1492
|
-
*
|
|
1490
|
+
* Compaction frees index space first; repair then adds back every
|
|
1491
|
+
* edge/halo-bearing node whose gist was evicted from the pending cache
|
|
1492
|
+
* before it reached the content index, completing the coverage that
|
|
1493
|
+
* incremental promotion alone cannot guarantee.
|
|
1494
|
+
*
|
|
1495
|
+
* repair runs with minParents = 0, NOT the library default of 2. The
|
|
1496
|
+
* default repairs only structural BRIDGES (≥2 parents), but this
|
|
1497
|
+
* trainer's fact deposits also leave answer-side DEPOSIT ROOTS with 0
|
|
1498
|
+
* structural parents ("The capital of France is Paris." as the dst of a
|
|
1499
|
+
* Q→A edge is a root of its own tree, contained in nothing). Those are
|
|
1500
|
+
* resonance targets recall depends on — a trained store shipped without
|
|
1501
|
+
* them cannot ground statement-shaped queries against its own answers
|
|
1502
|
+
* (observed: 33 such roots missing after a full curriculum, including
|
|
1503
|
+
* high-traffic conversation replies). minParents = 0 admits every
|
|
1504
|
+
* edge/halo bearer; the candidate set is still corpus-of-experiences-
|
|
1505
|
+
* sized, so the pass stays cheap.
|
|
1493
1506
|
*
|
|
1494
1507
|
* Logs the number of entries removed/added so a run that silently degrades
|
|
1495
1508
|
* (growing compaction count, or repair never recovering anything) is
|
|
@@ -1507,9 +1520,9 @@ async function main() {
|
|
|
1507
1520
|
progress.log(` ${YEL}⚠ index compact failed${R}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1508
1521
|
}
|
|
1509
1522
|
try {
|
|
1510
|
-
const added = await mind.repairContentIndex();
|
|
1523
|
+
const added = await mind.repairContentIndex(0);
|
|
1511
1524
|
if (added > 0) {
|
|
1512
|
-
progress.log(` ${GRN}index repair: added ${int(added)} missing
|
|
1525
|
+
progress.log(` ${GRN}index repair: added ${int(added)} missing resonance targets${R}`);
|
|
1513
1526
|
}
|
|
1514
1527
|
}
|
|
1515
1528
|
catch (err) {
|
package/dist/src/geometry.d.ts
CHANGED
|
@@ -103,8 +103,19 @@ export declare function knownPrefixLength(bytes: Uint8Array, leafAt: (i: number)
|
|
|
103
103
|
/** Bytes → Sema tree. `leafAt` and `lookup` are store capabilities for
|
|
104
104
|
* detecting previously-stored prefixes so the river can split at the
|
|
105
105
|
* correct boundary. Pass them through from `perceive`; the geometry
|
|
106
|
-
* computes the stable prefix internally.
|
|
107
|
-
|
|
106
|
+
* computes the stable prefix internally.
|
|
107
|
+
*
|
|
108
|
+
* `boundaries` is the CALLER-computed stable-prefix boundary set (§10.3):
|
|
109
|
+
* strictly-increasing proper byte offsets, each the length of a prefix that
|
|
110
|
+
* is already a stored whole-stream form. When given, the fold splits into
|
|
111
|
+
* the segments between consecutive boundaries — each folded independently,
|
|
112
|
+
* exactly as it folded when it was learned — and the segment roots join
|
|
113
|
+
* LEFT-NESTED (((s₀·s₁)·s₂)…), so every learnt cumulative-context root
|
|
114
|
+
* reappears as an identical subtree (and, by hash-consing, the very same
|
|
115
|
+
* node) inside the grown stream. This is what lets a conversation's next
|
|
116
|
+
* turn extend perception instead of refolding it: identical prefixes
|
|
117
|
+
* produce identical subtrees regardless of what follows them. */
|
|
118
|
+
export declare function bytesToTree(space: Space, alphabet: Alphabet, bytes: Uint8Array, leafAt?: (i: number) => number | null, lookup?: (leafIds: number[]) => number | null, boundaries?: readonly number[]): Sema;
|
|
108
119
|
/** The PLAIN fold's full level pyramid — every level's item list, bottom
|
|
109
120
|
* (leaves) to top (root). Left-grouped folding is RADIX-ALIGNED: the item
|
|
110
121
|
* at level L, index i, covers exactly bytes [i·mg^L, (i+1)·mg^L) whenever
|
package/dist/src/geometry.js
CHANGED
|
@@ -270,14 +270,101 @@ export function knownPrefixLength(bytes, leafAt, lookup) {
|
|
|
270
270
|
/** Bytes → Sema tree. `leafAt` and `lookup` are store capabilities for
|
|
271
271
|
* detecting previously-stored prefixes so the river can split at the
|
|
272
272
|
* correct boundary. Pass them through from `perceive`; the geometry
|
|
273
|
-
* computes the stable prefix internally.
|
|
274
|
-
|
|
273
|
+
* computes the stable prefix internally.
|
|
274
|
+
*
|
|
275
|
+
* `boundaries` is the CALLER-computed stable-prefix boundary set (§10.3):
|
|
276
|
+
* strictly-increasing proper byte offsets, each the length of a prefix that
|
|
277
|
+
* is already a stored whole-stream form. When given, the fold splits into
|
|
278
|
+
* the segments between consecutive boundaries — each folded independently,
|
|
279
|
+
* exactly as it folded when it was learned — and the segment roots join
|
|
280
|
+
* LEFT-NESTED (((s₀·s₁)·s₂)…), so every learnt cumulative-context root
|
|
281
|
+
* reappears as an identical subtree (and, by hash-consing, the very same
|
|
282
|
+
* node) inside the grown stream. This is what lets a conversation's next
|
|
283
|
+
* turn extend perception instead of refolding it: identical prefixes
|
|
284
|
+
* produce identical subtrees regardless of what follows them. */
|
|
285
|
+
export function bytesToTree(space, alphabet, bytes, leafAt, lookup, boundaries) {
|
|
275
286
|
if (bytes.length === 0) {
|
|
276
287
|
return sema(alphabet.vecs[0], new Uint8Array(0), null);
|
|
277
288
|
}
|
|
289
|
+
if (boundaries !== undefined && boundaries.length > 0) {
|
|
290
|
+
return stablePrefixFold(space, alphabet, bytes, boundaries);
|
|
291
|
+
}
|
|
278
292
|
const sb = (leafAt && lookup) ? knownPrefixLength(bytes, leafAt, lookup) : 0;
|
|
279
293
|
return riverFold(space, bytesToLeaves(alphabet, bytes), sb > 0 ? sb : bytes.length).tree;
|
|
280
294
|
}
|
|
295
|
+
/** The stable-prefix segmented fold (§10.3). Each segment between
|
|
296
|
+
* consecutive boundaries folds PLAINLY and independently; segment roots
|
|
297
|
+
* join left-nested, and only the final root is normalized (the linear-fold
|
|
298
|
+
* contract: one normalize per perception). A segment's own inner splits
|
|
299
|
+
* need no recursion here: a nested learnt prefix is itself an earlier
|
|
300
|
+
* boundary, so the left-nested join reproduces every intermediate learnt
|
|
301
|
+
* root ((s₀·s₁) IS the root the store learnt for the first two segments'
|
|
302
|
+
* bytes, and so on). */
|
|
303
|
+
function stablePrefixFold(space, alphabet, bytes, boundaries) {
|
|
304
|
+
const cuts = [];
|
|
305
|
+
let prev = 0;
|
|
306
|
+
for (const b of boundaries) {
|
|
307
|
+
if (b > prev && b < bytes.length) {
|
|
308
|
+
cuts.push(b);
|
|
309
|
+
prev = b;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (cuts.length === 0) {
|
|
313
|
+
return riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
|
|
314
|
+
}
|
|
315
|
+
const edges = [0, ...cuts, bytes.length];
|
|
316
|
+
const segs = [];
|
|
317
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
318
|
+
const seg = bytes.subarray(edges[i], edges[i + 1]);
|
|
319
|
+
segs.push(riverFoldRaw(space, bytesToLeaves(alphabet, seg)));
|
|
320
|
+
}
|
|
321
|
+
let cur = segs[0];
|
|
322
|
+
for (let i = 1; i < segs.length; i++)
|
|
323
|
+
cur = fold2(space, cur, segs[i]);
|
|
324
|
+
normalize(cur.tree.v);
|
|
325
|
+
return cur.tree;
|
|
326
|
+
}
|
|
327
|
+
/** Join two folded items as one 2-kid branch — the top-level join of the
|
|
328
|
+
* stable-prefix fold, identical FP ops to foldSlice's seat-bound
|
|
329
|
+
* accumulation over a group of two. Unnormalized (interior). */
|
|
330
|
+
function fold2(space, a, b) {
|
|
331
|
+
const D = space.D;
|
|
332
|
+
const gist = new Float32Array(D);
|
|
333
|
+
const kids = [a.tree, b.tree];
|
|
334
|
+
for (let k = 0; k < 2; k++) {
|
|
335
|
+
const seat = space.seats[k].fwd;
|
|
336
|
+
const v = kids[k].v;
|
|
337
|
+
for (let d = 0; d < D; d++)
|
|
338
|
+
gist[d] += v[seat[d]];
|
|
339
|
+
}
|
|
340
|
+
return { tree: sema(gist, null, kids), len: a.len + b.len };
|
|
341
|
+
}
|
|
342
|
+
/** Plain river fold WITHOUT the final root normalize — the segment-level
|
|
343
|
+
* building block of {@link stablePrefixFold} (interiors must keep their
|
|
344
|
+
* byte-proportional magnitude; only the whole perception's root is ever
|
|
345
|
+
* normalized). */
|
|
346
|
+
function riverFoldRaw(space, row) {
|
|
347
|
+
if (row.length === 0) {
|
|
348
|
+
const z = new Float32Array(space.D);
|
|
349
|
+
return { tree: sema(z, new Uint8Array(0), null), len: 0 };
|
|
350
|
+
}
|
|
351
|
+
if (row.length === 1)
|
|
352
|
+
return row[0];
|
|
353
|
+
let level = row;
|
|
354
|
+
while (level.length > 1) {
|
|
355
|
+
const next = [];
|
|
356
|
+
foldSlice(space, level, 0, level.length, next, false);
|
|
357
|
+
if (next.length === level.length) {
|
|
358
|
+
const forced = [];
|
|
359
|
+
foldSlice(space, next, 0, next.length, forced, true);
|
|
360
|
+
level = forced;
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
level = next;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return level[0];
|
|
367
|
+
}
|
|
281
368
|
/** Plain bytes→tree (identical to capability-less {@link bytesToTree}) that
|
|
282
369
|
* also RETURNS its pyramid, reusing `prev` — the pyramid of a PROPER
|
|
283
370
|
* prefix of `bytes` (caller guarantees content match and
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { isChunk } from "../sema.js";
|
|
10
10
|
import { lightestDerivation, } from "../derive/src/index.js";
|
|
11
11
|
import { consensusFloor, dominates, estimatorNoise } from "../geometry.js";
|
|
12
|
-
import { foldTree, gistOf, perceive, read } from "./primitives.js";
|
|
12
|
+
import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
|
|
13
13
|
import { recognise } from "./recognition.js";
|
|
14
14
|
import { leafIdRun } from "./canonical.js";
|
|
15
15
|
import { corpusN, edgeAncestors } from "./traverse.js";
|
|
@@ -27,16 +27,20 @@ export async function climbAttention(ctx, query, k, mode = "inverse") {
|
|
|
27
27
|
* attention) and the entire ranked list. Cached via ctx.climbMemo when
|
|
28
28
|
* ctx.trace is null. */
|
|
29
29
|
export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
|
|
30
|
+
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
31
|
+
// respondTurn(). Skipped while tracing.
|
|
30
32
|
if (ctx.climbMemo && !ctx.trace) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const contentKey = latin1Key(query);
|
|
34
|
+
const modeKey = `${k}:${mode}`;
|
|
35
|
+
let byRead = ctx.climbMemo.get(contentKey);
|
|
36
|
+
if (byRead === undefined) {
|
|
37
|
+
ctx.climbMemo.set(contentKey, byRead = new Map());
|
|
38
|
+
}
|
|
39
|
+
const hit = byRead.get(modeKey);
|
|
36
40
|
if (hit !== undefined)
|
|
37
41
|
return hit;
|
|
38
42
|
const read = await computeAttention(ctx, query, k, mode);
|
|
39
|
-
byRead.set(
|
|
43
|
+
byRead.set(modeKey, read);
|
|
40
44
|
return read;
|
|
41
45
|
}
|
|
42
46
|
return computeAttention(ctx, query, k, mode);
|
|
@@ -100,7 +100,17 @@ export async function recallByResonance(ctx, query, pre) {
|
|
|
100
100
|
// long answer sharing only scaffolding pass; the query-relative fraction
|
|
101
101
|
// measures exactly what the reach bar means: how much of THE QUERY the
|
|
102
102
|
// store accounts for.
|
|
103
|
-
|
|
103
|
+
// Chance similarity survives the length conversion AMPLIFIED: the same
|
|
104
|
+
// √(lenG/lenQ) factor that converts an honest shared fraction into a
|
|
105
|
+
// query-relative one multiplies the estimator/chance floor too, so a long
|
|
106
|
+
// stored form (√(lenG/lenQ) ≈ 10 at 100×) lifted a noise-level cosine past
|
|
107
|
+
// the reach bar and grounded pure gibberish (observed). Only the
|
|
108
|
+
// ABOVE-CHANCE part of the similarity is evidence of shared content —
|
|
109
|
+
// subtract the significance bar (3/√D, §8.3) before converting. Derived
|
|
110
|
+
// from the existing bars; never tuned.
|
|
111
|
+
const sig = significanceBar(ctx.store.D);
|
|
112
|
+
const fracOfQuery = (cos, otherLen) => Math.min(1, Math.max(0, cos - sig) *
|
|
113
|
+
Math.sqrt(otherLen / Math.max(1, query.length)));
|
|
104
114
|
for (const h of whole) {
|
|
105
115
|
const g = await project(ctx, h.id, queryGist);
|
|
106
116
|
if (g) {
|
|
@@ -110,20 +120,25 @@ export async function recallByResonance(ctx, query, pre) {
|
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
|
-
// The refusal/echo decision
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// reach bar
|
|
118
|
-
//
|
|
119
|
-
//
|
|
123
|
+
// The refusal/echo decision. The echo returns a stored form's bytes AS
|
|
124
|
+
// the answer — a near-identity claim about the query — and identity-grade
|
|
125
|
+
// decisions are never made on an estimated score ("approximate scores may
|
|
126
|
+
// rank and propose; they may never decide", §6.2): the RaBitQ estimate
|
|
127
|
+
// overshooting the reach bar echoed a WRONG-entity neighbour ("capital of
|
|
128
|
+
// Zamunda?" echoed the Armenia fact, observed). The bytes are read
|
|
129
|
+
// anyway to be echoed, so the decision uses their EXACT fold: one river
|
|
130
|
+
// fold of the top hit, measured in the same query-relative,
|
|
131
|
+
// chance-corrected units as the tier above.
|
|
120
132
|
const reach = reachThreshold(ctx.space.maxGroup);
|
|
121
|
-
const
|
|
122
|
-
|
|
133
|
+
const topBytes = read(ctx, top.id);
|
|
134
|
+
const exact = topBytes.length > 0
|
|
135
|
+
? cosine(queryGist, gistOf(ctx, topBytes))
|
|
136
|
+
: 0;
|
|
137
|
+
if (fracOfQuery(exact, topBytes.length) < reach) {
|
|
123
138
|
return ground(null, "below reach threshold — nothing in the store relates to this query", [], 0);
|
|
124
139
|
}
|
|
125
140
|
// Honest echo.
|
|
126
|
-
return ground(
|
|
141
|
+
return ground(topBytes, "last resort: the nearest resonant form's own bytes (echo, not grounded)", [], 0, true);
|
|
127
142
|
}
|
|
128
143
|
// ── Pipeline mechanism ──────────────────────────────────────────────────────
|
|
129
144
|
export const recallMechanism = {
|
package/dist/src/mind/mind.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Vec } from "../vec.js";
|
|
2
2
|
import { Sema, Space } from "../sema.js";
|
|
3
3
|
import { Alphabet } from "../alphabet.js";
|
|
4
|
-
import { Grid } from "../geometry.js";
|
|
4
|
+
import { type FoldPyramid, Grid } from "../geometry.js";
|
|
5
5
|
import { BoundedMap, type Store } from "../store.js";
|
|
6
6
|
import { type MindConfig } from "../config.js";
|
|
7
7
|
import { GraphSearch, type Leaf, type Site } from "./graph-search.js";
|
|
@@ -19,6 +19,27 @@ export interface Response {
|
|
|
19
19
|
* no answer. */
|
|
20
20
|
provenance?: import("./pipeline.js").Provenance;
|
|
21
21
|
}
|
|
22
|
+
/** Serializable state of a conversation — can be saved and restored across
|
|
23
|
+
* sessions. The Mind never interprets the bytes; it only tracks their
|
|
24
|
+
* cumulative lengths so the caller can reconstruct turn boundaries later
|
|
25
|
+
* without inspecting content. */
|
|
26
|
+
export interface ConversationState {
|
|
27
|
+
/** The accumulated context bytes — raw concatenation of every turn's
|
|
28
|
+
* bytes in order. No separator is inserted; the boundary offsets
|
|
29
|
+
* ({@link boundaries}) tell the caller where each turn ends. */
|
|
30
|
+
context: Uint8Array;
|
|
31
|
+
/** Cumulative byte length after each completed turn. Sorted, strictly
|
|
32
|
+
* increasing, each {@code < context.length}. The first turn's length
|
|
33
|
+
* is `boundaries[0]`; the second turn starts at that offset, and so
|
|
34
|
+
* on. Empty for a single-turn or new conversation. */
|
|
35
|
+
boundaries: number[];
|
|
36
|
+
}
|
|
37
|
+
/** An active conversation handle. Opaque — interact through the Mind's
|
|
38
|
+
* conversation methods ({@link Mind.beginConversation},
|
|
39
|
+
* {@link Mind.respondTurn}, {@link Mind.endConversation}). */
|
|
40
|
+
export interface Conversation {
|
|
41
|
+
readonly id: number;
|
|
42
|
+
}
|
|
22
43
|
import type { AttentionRead, MindContext, Recognition } from "./types.js";
|
|
23
44
|
export interface MindOptions {
|
|
24
45
|
seed?: number;
|
|
@@ -46,16 +67,17 @@ export declare class Mind implements MindContext {
|
|
|
46
67
|
readonly mechanisms: import("./pipeline-mechanism.js").PipelineMechanism[];
|
|
47
68
|
/** The live rationale tracer for the inference currently in flight, or null. */
|
|
48
69
|
trace: Rationale | null;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
climbMemo: WeakMap<Uint8Array, Map<string, AttentionRead>> | null;
|
|
55
|
-
/** Per-response memo of recognise() — see {@link MindContext.recogniseMemo}. */
|
|
56
|
-
recogniseMemo: WeakMap<Uint8Array, Recognition> | null;
|
|
57
|
-
/** Per-response memo of perceive() — see {@link MindContext.perceiveMemo}. */
|
|
70
|
+
/** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
|
|
71
|
+
climbMemo: Map<string, Map<string, AttentionRead>> | null;
|
|
72
|
+
/** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
|
|
73
|
+
recogniseMemo: Map<string, Recognition> | null;
|
|
74
|
+
/** Memo of perceive() — content-keyed. See {@link MindContext.perceiveMemo}. */
|
|
58
75
|
perceiveMemo: Map<string, import("../sema.js").Sema> | null;
|
|
76
|
+
/** Subtree-resolution cache. See {@link MindContext._resolvedSubtrees}. */
|
|
77
|
+
_resolvedSubtrees: WeakMap<import("../sema.js").Sema, {
|
|
78
|
+
id: number;
|
|
79
|
+
len: number;
|
|
80
|
+
}> | null;
|
|
59
81
|
/** The perceived gist of the query currently being answered. Set by `think`
|
|
60
82
|
* before the graph search runs; `chooseNext` consults it as a gate (a null
|
|
61
83
|
* guide means no query is in flight, so structural walkers keep plain
|
|
@@ -72,9 +94,11 @@ export declare class Mind implements MindContext {
|
|
|
72
94
|
* {@link MindContext._gistCache}. 32 MB ≈ 8K gists at D=1024; hub
|
|
73
95
|
* candidate sets (√N at most) fit comfortably and recur across queries. */
|
|
74
96
|
_gistCache: BoundedMap<number, Vec>;
|
|
75
|
-
_depositTrees: BoundedMap<string,
|
|
97
|
+
_depositTrees: BoundedMap<string, FoldPyramid>;
|
|
76
98
|
_depositLens: Set<number>;
|
|
77
99
|
_internIds: WeakMap<Sema, number>;
|
|
100
|
+
private _nextConvId;
|
|
101
|
+
private _conversations;
|
|
78
102
|
/** Canonical node id of a byte span. Required by GraphSearchHost & MindContext. */
|
|
79
103
|
resolve(bytes: Uint8Array): number | null;
|
|
80
104
|
recogniseSpan(bytes: Uint8Array): {
|
|
@@ -109,12 +133,52 @@ export declare class Mind implements MindContext {
|
|
|
109
133
|
/** Close one response's transient state — every per-response field, incl.
|
|
110
134
|
* the edge guide/choices `think` sets mid-flight. */
|
|
111
135
|
private endResponse;
|
|
136
|
+
/** Shared response core — the one path from bytes to voiced answer.
|
|
137
|
+
* `respond` calls this directly; `respondTurn` has its own path
|
|
138
|
+
* with conversation-persistent memos and incremental perception. */
|
|
139
|
+
private _respondImpl;
|
|
112
140
|
respond(input: Input, inspectRationale?: InspectRationale): Promise<Response>;
|
|
113
141
|
/** Text view of {@link respond}. NUL bytes (0x00) are stripped before
|
|
114
142
|
* decoding — they are structural padding in text answers. LOSSY for a
|
|
115
143
|
* binary answer that legitimately contains NULs: use {@link respond} and
|
|
116
144
|
* read `bytes` directly for binary/grid modalities. */
|
|
117
145
|
respondText(input: string, inspectRationale?: InspectRationale): Promise<string>;
|
|
146
|
+
/** Begin a new conversation, optionally restoring from a previously-saved
|
|
147
|
+
* {@link ConversationState}. The returned handle is required for
|
|
148
|
+
* {@link respondTurn} and {@link endConversation}.
|
|
149
|
+
*
|
|
150
|
+
* Conversations are independent — a Mind can manage several concurrently.
|
|
151
|
+
* Each tracks the fold pyramid (accumulated internal processing) and
|
|
152
|
+
* turn-boundary offsets; the geometry never inspects content to guess
|
|
153
|
+
* where one turn ends and the next begins. */
|
|
154
|
+
beginConversation(state?: ConversationState): Conversation;
|
|
155
|
+
/** End a conversation, releasing its internal resources (accumulated
|
|
156
|
+
* context, boundary offsets, and the fold-pyramid cache). Idempotent. */
|
|
157
|
+
endConversation(conv: Conversation): void;
|
|
158
|
+
/** The current serialisable state of an active conversation. Save this
|
|
159
|
+
* to resume the conversation later via {@link beginConversation}. */
|
|
160
|
+
conversationState(conv: Conversation): ConversationState | null;
|
|
161
|
+
/** Process one turn of a conversation.
|
|
162
|
+
*
|
|
163
|
+
* `turn` is the raw input for the latest turn — its bytes are appended
|
|
164
|
+
* to the accumulated context directly (raw concatenation). The Mind
|
|
165
|
+
* tracks the byte offset where each turn ends; no separator is ever
|
|
166
|
+
* inserted or inspected.
|
|
167
|
+
*
|
|
168
|
+
* Returns the response AND the updated {@link ConversationState} so the
|
|
169
|
+
* caller can persist it. The conversation handle's internal state is
|
|
170
|
+
* updated in place — the returned state is a snapshot for storage. */
|
|
171
|
+
respondTurn(conv: Conversation, turn: Input, inspectRationale?: InspectRationale): Promise<{
|
|
172
|
+
response: Response;
|
|
173
|
+
state: ConversationState;
|
|
174
|
+
}>;
|
|
175
|
+
/** Text view of {@link respondTurn}. See {@link respondText} for the
|
|
176
|
+
* NUL-stripping caveat. For binary or grid turns use {@link respondTurn}
|
|
177
|
+
* directly — this is a text-only convenience, like {@link respondText}. */
|
|
178
|
+
respondTurnText(conv: Conversation, turn: string, inspectRationale?: InspectRationale): Promise<{
|
|
179
|
+
response: string;
|
|
180
|
+
state: ConversationState;
|
|
181
|
+
}>;
|
|
118
182
|
embedding(input: Input): Promise<Vec | null>;
|
|
119
183
|
/** Kinship note: the vector arm below is a miniature of recall's tier 3
|
|
120
184
|
* (resonate → reach gate → read out the nearest form's bytes) — the
|
package/dist/src/mind/mind.js
CHANGED
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
// Implementation split across src/mind/*.ts — this file assembles the Mind class.
|
|
11
11
|
import { makeKeyring, rng, setVecConfig } from "../vec.js";
|
|
12
12
|
import { Alphabet } from "../alphabet.js";
|
|
13
|
-
import { reachThreshold, } from "../geometry.js";
|
|
13
|
+
import { bytesToTreePyramid, reachThreshold, } from "../geometry.js";
|
|
14
14
|
import { BoundedMap } from "../store.js";
|
|
15
15
|
import { SQliteStore } from "../store-sqlite.js";
|
|
16
16
|
import { resolveConfig } from "../config.js";
|
|
17
|
-
import { bytesEqual } from "../bytes.js";
|
|
17
|
+
import { bytesEqual, concat2 } from "../bytes.js";
|
|
18
18
|
import { GraphSearch, } from "./graph-search.js";
|
|
19
19
|
import { Alu } from "../alu/src/index.js";
|
|
20
20
|
import { decodeText, Rationale, } from "./rationale.js";
|
|
21
|
-
import { gistOf, inputBytes, perceive as perceiveImpl, resolve as resolveImpl, } from "./primitives.js";
|
|
21
|
+
import { gistOf, inputBytes, latin1Key, perceive as perceiveImpl, resolve as resolveImpl, } from "./primitives.js";
|
|
22
22
|
import { chooseNext, edgeAncestors as edgeAncestorsFn } from "./traverse.js";
|
|
23
23
|
import { follow } from "./match.js";
|
|
24
24
|
import { recognise, segment } from "./recognition.js";
|
|
@@ -42,16 +42,14 @@ export class Mind {
|
|
|
42
42
|
mechanisms = [];
|
|
43
43
|
/** The live rationale tracer for the inference currently in flight, or null. */
|
|
44
44
|
trace = null;
|
|
45
|
-
/**
|
|
46
|
-
* {@link recogniseMemo} are BYPASSED while a rationale trace is attached
|
|
47
|
-
* (every mechanism must emit its own steps), so a traced respond re-pays
|
|
48
|
-
* up to four consensus climbs plus repeat recognitions — that is where the
|
|
49
|
-
* traced-vs-untraced latency multiple comes from, by design. */
|
|
45
|
+
/** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
|
|
50
46
|
climbMemo = null;
|
|
51
|
-
/**
|
|
47
|
+
/** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
|
|
52
48
|
recogniseMemo = null;
|
|
53
|
-
/**
|
|
49
|
+
/** Memo of perceive() — content-keyed. See {@link MindContext.perceiveMemo}. */
|
|
54
50
|
perceiveMemo = null;
|
|
51
|
+
/** Subtree-resolution cache. See {@link MindContext._resolvedSubtrees}. */
|
|
52
|
+
_resolvedSubtrees = null;
|
|
55
53
|
/** The perceived gist of the query currently being answered. Set by `think`
|
|
56
54
|
* before the graph search runs; `chooseNext` consults it as a gate (a null
|
|
57
55
|
* guide means no query is in flight, so structural walkers keep plain
|
|
@@ -75,6 +73,9 @@ export class Mind {
|
|
|
75
73
|
_depositTrees = new BoundedMap(8);
|
|
76
74
|
_depositLens = new Set();
|
|
77
75
|
_internIds = new WeakMap();
|
|
76
|
+
// ── Conversation state ──────────────────────────────────────────────────
|
|
77
|
+
_nextConvId = 1;
|
|
78
|
+
_conversations = new Map();
|
|
78
79
|
// ── GraphSearchHost implementation ─────────────────────────────────────
|
|
79
80
|
/** Canonical node id of a byte span. Required by GraphSearchHost & MindContext. */
|
|
80
81
|
resolve(bytes) {
|
|
@@ -167,8 +168,8 @@ export class Mind {
|
|
|
167
168
|
* created, so adding a memo cannot forget its reset. */
|
|
168
169
|
beginResponse(inspectRationale) {
|
|
169
170
|
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
170
|
-
this.climbMemo = new
|
|
171
|
-
this.recogniseMemo = new
|
|
171
|
+
this.climbMemo = new Map();
|
|
172
|
+
this.recogniseMemo = new Map();
|
|
172
173
|
this.perceiveMemo = new Map();
|
|
173
174
|
}
|
|
174
175
|
/** Close one response's transient state — every per-response field, incl.
|
|
@@ -181,19 +182,21 @@ export class Mind {
|
|
|
181
182
|
this._edgeGuide = null;
|
|
182
183
|
this._edgeChoice.clear();
|
|
183
184
|
}
|
|
184
|
-
|
|
185
|
+
/** Shared response core — the one path from bytes to voiced answer.
|
|
186
|
+
* `respond` calls this directly; `respondTurn` has its own path
|
|
187
|
+
* with conversation-persistent memos and incremental perception. */
|
|
188
|
+
async _respondImpl(queryBytes, inspectRationale, traceLabel = "respond") {
|
|
185
189
|
this.beginResponse(inspectRationale);
|
|
186
190
|
try {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
rItem(inBytes, "query"),
|
|
191
|
+
const top = this.trace?.enter(traceLabel, [
|
|
192
|
+
rItem(queryBytes, "query"),
|
|
190
193
|
]);
|
|
191
|
-
const thought = await think(this,
|
|
194
|
+
const thought = await think(this, queryBytes, this.mechanisms);
|
|
192
195
|
if (thought === null) {
|
|
193
196
|
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
194
197
|
return { v: null, bytes: new Uint8Array(0) };
|
|
195
198
|
}
|
|
196
|
-
const voiced = await articulate(this, thought.bytes,
|
|
199
|
+
const voiced = await articulate(this, thought.bytes, queryBytes);
|
|
197
200
|
top?.done([rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)], "the answer, re-voiced in the asker's words");
|
|
198
201
|
return {
|
|
199
202
|
v: gistOf(this, voiced),
|
|
@@ -205,6 +208,9 @@ export class Mind {
|
|
|
205
208
|
this.endResponse();
|
|
206
209
|
}
|
|
207
210
|
}
|
|
211
|
+
async respond(input, inspectRationale) {
|
|
212
|
+
return this._respondImpl(inputBytes(this, input), inspectRationale);
|
|
213
|
+
}
|
|
208
214
|
/** Text view of {@link respond}. NUL bytes (0x00) are stripped before
|
|
209
215
|
* decoding — they are structural padding in text answers. LOSSY for a
|
|
210
216
|
* binary answer that legitimately contains NULs: use {@link respond} and
|
|
@@ -213,6 +219,133 @@ export class Mind {
|
|
|
213
219
|
const r = await this.respond(input, inspectRationale);
|
|
214
220
|
return decodeText(r.bytes);
|
|
215
221
|
}
|
|
222
|
+
// ── Conversation API ────────────────────────────────────────────────────
|
|
223
|
+
/** Begin a new conversation, optionally restoring from a previously-saved
|
|
224
|
+
* {@link ConversationState}. The returned handle is required for
|
|
225
|
+
* {@link respondTurn} and {@link endConversation}.
|
|
226
|
+
*
|
|
227
|
+
* Conversations are independent — a Mind can manage several concurrently.
|
|
228
|
+
* Each tracks the fold pyramid (accumulated internal processing) and
|
|
229
|
+
* turn-boundary offsets; the geometry never inspects content to guess
|
|
230
|
+
* where one turn ends and the next begins. */
|
|
231
|
+
beginConversation(state) {
|
|
232
|
+
const id = this._nextConvId++;
|
|
233
|
+
// Build the initial pyramid: from scratch for a new conversation,
|
|
234
|
+
// or from the saved context bytes when restoring.
|
|
235
|
+
const initBytes = state?.context ?? new Uint8Array(0);
|
|
236
|
+
const { pyramid } = bytesToTreePyramid(this.space, this.alphabet, initBytes);
|
|
237
|
+
this._conversations.set(id, {
|
|
238
|
+
pyramid,
|
|
239
|
+
bytes: initBytes,
|
|
240
|
+
boundaries: state?.boundaries ? [...state.boundaries] : [],
|
|
241
|
+
perceiveMemo: new Map(),
|
|
242
|
+
recogniseMemo: new Map(),
|
|
243
|
+
climbMemo: new Map(),
|
|
244
|
+
resolvedSubtrees: new WeakMap(),
|
|
245
|
+
});
|
|
246
|
+
return { id };
|
|
247
|
+
}
|
|
248
|
+
/** End a conversation, releasing its internal resources (accumulated
|
|
249
|
+
* context, boundary offsets, and the fold-pyramid cache). Idempotent. */
|
|
250
|
+
endConversation(conv) {
|
|
251
|
+
this._conversations.delete(conv.id);
|
|
252
|
+
}
|
|
253
|
+
/** The current serialisable state of an active conversation. Save this
|
|
254
|
+
* to resume the conversation later via {@link beginConversation}. */
|
|
255
|
+
conversationState(conv) {
|
|
256
|
+
const data = this._conversations.get(conv.id);
|
|
257
|
+
if (!data)
|
|
258
|
+
return null;
|
|
259
|
+
return {
|
|
260
|
+
context: data.bytes,
|
|
261
|
+
boundaries: [...data.boundaries],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Process one turn of a conversation.
|
|
265
|
+
*
|
|
266
|
+
* `turn` is the raw input for the latest turn — its bytes are appended
|
|
267
|
+
* to the accumulated context directly (raw concatenation). The Mind
|
|
268
|
+
* tracks the byte offset where each turn ends; no separator is ever
|
|
269
|
+
* inserted or inspected.
|
|
270
|
+
*
|
|
271
|
+
* Returns the response AND the updated {@link ConversationState} so the
|
|
272
|
+
* caller can persist it. The conversation handle's internal state is
|
|
273
|
+
* updated in place — the returned state is a snapshot for storage. */
|
|
274
|
+
async respondTurn(conv, turn, inspectRationale) {
|
|
275
|
+
const data = this._conversations.get(conv.id);
|
|
276
|
+
if (!data)
|
|
277
|
+
throw new Error(`Conversation ${conv.id} not found`);
|
|
278
|
+
const turnBytes = inputBytes(this, turn);
|
|
279
|
+
const prevLen = data.pyramid.bytes;
|
|
280
|
+
const prevBytes = data.bytes;
|
|
281
|
+
const newContext = prevLen > 0 ? concat2(prevBytes, turnBytes) : turnBytes;
|
|
282
|
+
data.bytes = newContext;
|
|
283
|
+
if (prevLen > 0)
|
|
284
|
+
data.boundaries.push(prevLen);
|
|
285
|
+
// Incremental perception — O(turn) instead of O(context).
|
|
286
|
+
const { tree, pyramid } = bytesToTreePyramid(this.space, this.alphabet, newContext, data.pyramid);
|
|
287
|
+
data.pyramid = pyramid;
|
|
288
|
+
// Swap in the conversation's persistent state so the inference
|
|
289
|
+
// pipeline does not re-process the prefix from scratch.
|
|
290
|
+
// perceiveMemo / recogniseMemo / climbMemo — content-keyed;
|
|
291
|
+
// the prefix's results from the previous turn are found by
|
|
292
|
+
// the current turn's sub-span calls.
|
|
293
|
+
// _resolvedSubtrees — Sema-node-keyed; when the pyramid reuses
|
|
294
|
+
// prefix subtrees (identical objects), foldTree returns their
|
|
295
|
+
// ids immediately — O(suffix) instead of O(context).
|
|
296
|
+
this.perceiveMemo = data.perceiveMemo;
|
|
297
|
+
this.recogniseMemo = data.recogniseMemo;
|
|
298
|
+
this.climbMemo = data.climbMemo;
|
|
299
|
+
this._resolvedSubtrees = data.resolvedSubtrees;
|
|
300
|
+
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
301
|
+
try {
|
|
302
|
+
this.perceiveMemo.set(latin1Key(newContext), tree);
|
|
303
|
+
const top = this.trace?.enter("respondTurn", [
|
|
304
|
+
rItem(newContext, "query"),
|
|
305
|
+
]);
|
|
306
|
+
const thought = await think(this, newContext, this.mechanisms);
|
|
307
|
+
if (thought === null) {
|
|
308
|
+
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
309
|
+
return {
|
|
310
|
+
response: { v: null, bytes: new Uint8Array(0) },
|
|
311
|
+
state: this.conversationState(conv),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const voiced = await articulate(this, thought.bytes, newContext);
|
|
315
|
+
top?.done([rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)], "the answer, re-voiced in the asker's words");
|
|
316
|
+
return {
|
|
317
|
+
response: {
|
|
318
|
+
v: gistOf(this, voiced),
|
|
319
|
+
bytes: voiced,
|
|
320
|
+
provenance: thought.provenance,
|
|
321
|
+
},
|
|
322
|
+
state: this.conversationState(conv),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
// Save back to the conversation for the next turn.
|
|
327
|
+
data.perceiveMemo = this.perceiveMemo;
|
|
328
|
+
data.recogniseMemo = this.recogniseMemo;
|
|
329
|
+
data.climbMemo = this.climbMemo;
|
|
330
|
+
data.resolvedSubtrees = this._resolvedSubtrees;
|
|
331
|
+
// Clear Mind references — non-conversation respond() calls get
|
|
332
|
+
// fresh per-response memos.
|
|
333
|
+
this.trace = null;
|
|
334
|
+
this.perceiveMemo = null;
|
|
335
|
+
this.recogniseMemo = null;
|
|
336
|
+
this.climbMemo = null;
|
|
337
|
+
this._resolvedSubtrees = null;
|
|
338
|
+
this._edgeGuide = null;
|
|
339
|
+
this._edgeChoice.clear();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/** Text view of {@link respondTurn}. See {@link respondText} for the
|
|
343
|
+
* NUL-stripping caveat. For binary or grid turns use {@link respondTurn}
|
|
344
|
+
* directly — this is a text-only convenience, like {@link respondText}. */
|
|
345
|
+
async respondTurnText(conv, turn, inspectRationale) {
|
|
346
|
+
const { response, state } = await this.respondTurn(conv, turn, inspectRationale);
|
|
347
|
+
return { response: decodeText(response.bytes), state };
|
|
348
|
+
}
|
|
216
349
|
async embedding(input) {
|
|
217
350
|
return (await this.respond(input)).v;
|
|
218
351
|
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { Vec } from "../vec.js";
|
|
2
2
|
import { Sema } from "../sema.js";
|
|
3
3
|
import type { Input, MindContext } from "./types.js";
|
|
4
|
+
/** The content key of a byte span — one latin1 char per byte, an exact,
|
|
5
|
+
* collision-free encoding. Spans on the perception path are query-scale
|
|
6
|
+
* (windows, regions, candidate spans), so key construction is far cheaper
|
|
7
|
+
* than the river fold it deduplicates. */
|
|
8
|
+
export declare function latin1Key(bytes: Uint8Array): string;
|
|
4
9
|
/** Perceive input into a content-defined tree (the river fold).
|
|
5
|
-
* Deterministic — identical bytes always produce an identical tree.
|
|
6
|
-
|
|
10
|
+
* Deterministic — identical bytes always produce an identical tree.
|
|
11
|
+
*
|
|
12
|
+
* `boundaries` is an optional sorted list of proper byte offsets where the
|
|
13
|
+
* fold must split so that each prefix segment folds identically to how it
|
|
14
|
+
* folded when it was learned (§10.3 stable-prefix contract). Only the
|
|
15
|
+
* CALLER — who assembled the multi-turn context — knows where those
|
|
16
|
+
* boundaries are; the geometry never guesses them from the bytes. */
|
|
17
|
+
export declare function perceive(ctx: MindContext, input: Input, leafAt?: (i: number) => number | null, lookup?: (ids: number[]) => number | null, boundaries?: readonly number[]): Sema;
|
|
7
18
|
/** The DEPOSIT-shaped perceive: the PLAIN fold (bit-identical to inference
|
|
8
19
|
* perception — that structural train/inference agreement is load-bearing
|
|
9
20
|
* for exact recall), computed INCREMENTALLY via the fold's level pyramid
|