@hviana/sema 0.1.5 → 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/AGENTS.md +6 -5
- package/CITATION.cff +49 -0
- package/HOW_IT_WORKS.md +11 -12
- package/README.md +7 -5
- package/dist/example/demo.js +25 -25
- package/dist/example/train_base.d.ts +19 -21
- package/dist/example/train_base.js +1700 -1952
- package/dist/src/alphabet.d.ts +3 -3
- package/dist/src/alphabet.js +24 -27
- package/dist/src/alu/src/alu.d.ts +169 -194
- package/dist/src/alu/src/alu.js +374 -414
- package/dist/src/alu/src/expr.d.ts +36 -43
- package/dist/src/alu/src/expr.js +252 -278
- package/dist/src/alu/src/index.d.ts +9 -98
- package/dist/src/alu/src/index.js +10 -64
- package/dist/src/alu/src/kernel-arith.d.ts +1 -5
- package/dist/src/alu/src/kernel-arith.js +195 -285
- package/dist/src/alu/src/kernel-bits.d.ts +1 -5
- package/dist/src/alu/src/kernel-bits.js +81 -115
- package/dist/src/alu/src/kernel-logic.js +33 -63
- package/dist/src/alu/src/kernel-nd.js +155 -195
- package/dist/src/alu/src/kernel-numeric.d.ts +16 -80
- package/dist/src/alu/src/kernel-numeric.js +259 -331
- package/dist/src/alu/src/operation.d.ts +112 -132
- package/dist/src/alu/src/operation.js +149 -163
- package/dist/src/alu/src/parser.d.ts +168 -175
- package/dist/src/alu/src/parser.js +382 -421
- package/dist/src/alu/src/resonance.d.ts +16 -25
- package/dist/src/alu/src/resonance.js +49 -56
- package/dist/src/alu/src/text.d.ts +3 -7
- package/dist/src/alu/src/text.js +31 -37
- package/dist/src/alu/src/value.d.ts +14 -14
- package/dist/src/alu/src/value.js +150 -160
- package/dist/src/alu/test/alu.test.js +543 -656
- package/dist/src/bytes.d.ts +1 -5
- package/dist/src/bytes.js +33 -40
- package/dist/src/config.d.ts +99 -102
- package/dist/src/config.js +78 -83
- package/dist/src/derive/src/deduction.d.ts +60 -62
- package/dist/src/derive/src/deduction.js +103 -109
- package/dist/src/derive/src/index.d.ts +1 -7
- package/dist/src/derive/src/priority-queue.d.ts +8 -8
- package/dist/src/derive/src/priority-queue.js +57 -61
- package/dist/src/derive/src/rewrite.d.ts +15 -18
- package/dist/src/derive/src/rewrite.js +59 -67
- package/dist/src/derive/src/trie.d.ts +56 -56
- package/dist/src/derive/src/trie.js +175 -179
- package/dist/src/derive/test/derive.test.js +96 -100
- package/dist/src/extension.d.ts +13 -16
- package/dist/src/geometry.d.ts +29 -43
- package/dist/src/geometry.js +324 -268
- package/dist/src/index.d.ts +2 -17
- package/dist/src/index.js +2 -12
- package/dist/src/ingest-cache.d.ts +25 -30
- package/dist/src/ingest-cache.js +108 -127
- package/dist/src/mind/articulation.d.ts +1 -5
- package/dist/src/mind/articulation.js +77 -112
- package/dist/src/mind/attention.d.ts +40 -108
- package/dist/src/mind/attention.js +754 -872
- package/dist/src/mind/canonical.d.ts +4 -19
- package/dist/src/mind/canonical.js +27 -31
- package/dist/src/mind/graph-search.d.ts +201 -225
- package/dist/src/mind/graph-search.js +742 -821
- package/dist/src/mind/index.d.ts +2 -10
- package/dist/src/mind/junction.d.ts +31 -58
- package/dist/src/mind/junction.js +172 -237
- package/dist/src/mind/learning.d.ts +16 -52
- package/dist/src/mind/learning.js +143 -165
- package/dist/src/mind/match.d.ts +20 -69
- package/dist/src/mind/match.js +259 -318
- package/dist/src/mind/mechanisms/alu.js +16 -16
- package/dist/src/mind/mechanisms/cast.d.ts +9 -13
- package/dist/src/mind/mechanisms/cast.js +363 -456
- package/dist/src/mind/mechanisms/confluence.d.ts +8 -12
- package/dist/src/mind/mechanisms/confluence.js +152 -183
- package/dist/src/mind/mechanisms/cover.d.ts +2 -8
- package/dist/src/mind/mechanisms/cover.js +148 -210
- package/dist/src/mind/mechanisms/extraction.d.ts +8 -34
- package/dist/src/mind/mechanisms/extraction.js +234 -288
- package/dist/src/mind/mechanisms/recall.d.ts +6 -10
- package/dist/src/mind/mechanisms/recall.js +141 -185
- package/dist/src/mind/mind.d.ts +194 -155
- package/dist/src/mind/mind.js +392 -289
- package/dist/src/mind/pipeline-mechanism.d.ts +112 -124
- package/dist/src/mind/pipeline-mechanism.js +161 -172
- package/dist/src/mind/pipeline.d.ts +4 -14
- package/dist/src/mind/pipeline.js +125 -189
- package/dist/src/mind/primitives.d.ts +20 -33
- package/dist/src/mind/primitives.js +124 -120
- package/dist/src/mind/rationale.d.ts +86 -98
- package/dist/src/mind/rationale.js +113 -121
- package/dist/src/mind/reasoning.d.ts +2 -13
- package/dist/src/mind/reasoning.js +129 -166
- package/dist/src/mind/recognition.d.ts +1 -4
- package/dist/src/mind/recognition.js +198 -210
- package/dist/src/mind/resonance.d.ts +5 -22
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/trace.d.ts +6 -25
- package/dist/src/mind/trace.js +50 -58
- package/dist/src/mind/traverse.d.ts +29 -58
- package/dist/src/mind/traverse.js +344 -356
- package/dist/src/mind/types.d.ts +122 -127
- package/dist/src/mind/types.js +60 -69
- package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
- package/dist/src/rabitq-ivf/src/database.js +201 -0
- package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
- package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.js +1 -3
- package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
- package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
- package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
- package/dist/src/rabitq-ivf/src/prng.js +36 -0
- package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
- package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
- package/dist/src/sema.d.ts +9 -13
- package/dist/src/sema.js +26 -40
- package/dist/src/store-sqlite.d.ts +171 -166
- package/dist/src/store-sqlite.js +768 -662
- package/dist/src/store.d.ts +597 -630
- package/dist/src/store.js +1434 -1581
- package/dist/src/vec.d.ts +5 -9
- package/dist/src/vec.js +61 -73
- package/example/train_base.ts +33 -15
- package/index.html +65 -0
- package/package.json +1 -1
- package/src/alu/README.md +1 -1
- package/src/alu/src/index.ts +1 -1
- package/src/config.ts +19 -27
- package/src/geometry.ts +93 -1
- package/src/index.ts +6 -11
- 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/rabitq-ivf/README.md +56 -0
- package/src/rabitq-ivf/src/database.ts +276 -0
- package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
- package/src/rabitq-ivf/src/ivf.ts +1330 -0
- package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
- package/src/store-sqlite.ts +204 -9
- package/src/store.ts +27 -34
- package/test/08-storage.test.mjs +3 -3
- package/test/13-conversation.test.mjs +190 -20
- package/test/14-scaling.test.mjs +2 -2
- package/test/35-ivf.test.mjs +263 -0
- package/test/36-bloom.test.mjs +123 -0
- package/dist/src/rabitq-hnsw/src/database.d.ts +0 -202
- package/dist/src/rabitq-hnsw/src/database.js +0 -405
- package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
- package/dist/src/rabitq-hnsw/src/heap.js +0 -94
- package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
- package/dist/src/rabitq-hnsw/src/hnsw.js +0 -500
- package/dist/src/rabitq-hnsw/src/index.d.ts +0 -15
- package/dist/src/rabitq-hnsw/src/prng.d.ts +0 -19
- package/dist/src/rabitq-hnsw/src/prng.js +0 -38
- package/dist/src/rabitq-hnsw/src/rabitq.d.ts +0 -95
- package/dist/src/rabitq-hnsw/src/rabitq.js +0 -299
- package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
- package/dist/src/rabitq-hnsw/src/store.js +0 -916
- package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
- package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -1197
- package/src/rabitq-hnsw/README.md +0 -303
- package/src/rabitq-hnsw/src/database.ts +0 -492
- package/src/rabitq-hnsw/src/heap.ts +0 -90
- package/src/rabitq-hnsw/src/hnsw.ts +0 -514
- package/src/rabitq-hnsw/src/store.ts +0 -994
- package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
- /package/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.ts +0 -0
package/src/index.ts
CHANGED
|
@@ -13,26 +13,21 @@ export * from "./store-sqlite.js";
|
|
|
13
13
|
export * from "./config.js";
|
|
14
14
|
export * from "./extension.js";
|
|
15
15
|
export * from "./ingest-cache.js";
|
|
16
|
-
// rabitq-
|
|
17
|
-
// `StoreConfig` are internal names that collide with sema's own top-level
|
|
18
|
-
// `store.js` / `config.js` exports, and sema code that needs the rabitq ones
|
|
19
|
-
// imports them from the subpath directly. Re-export the public vector-DB
|
|
20
|
-
// surface under the sema root, omitting the three colliding names.
|
|
16
|
+
// rabitq-ivf: the partitioned (IVF) vector index over 1-bit RaBitQ codes.
|
|
21
17
|
export {
|
|
22
|
-
|
|
23
|
-
HnswIndex,
|
|
18
|
+
IvfIndex,
|
|
24
19
|
Prng,
|
|
25
20
|
RaBitQuantizer,
|
|
26
21
|
VectorDatabase,
|
|
27
|
-
} from "./rabitq-
|
|
22
|
+
} from "./rabitq-ivf/src/index.js";
|
|
28
23
|
export type {
|
|
29
24
|
DatabaseOptions,
|
|
30
25
|
ExternalId,
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
IvfConfig,
|
|
27
|
+
IvfHit,
|
|
33
28
|
QueryContext,
|
|
34
29
|
QueryResult,
|
|
35
30
|
RaBitQOptions,
|
|
36
31
|
StorageStats,
|
|
37
|
-
} from "./rabitq-
|
|
32
|
+
} from "./rabitq-ivf/src/index.js";
|
|
38
33
|
export * from "./derive/src/index.js";
|
package/src/mind/attention.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
SaturationInfo,
|
|
28
28
|
} from "./types.js";
|
|
29
29
|
import { consensusFloor, dominates, estimatorNoise } from "../geometry.js";
|
|
30
|
-
import { foldTree, gistOf, perceive, read } from "./primitives.js";
|
|
30
|
+
import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
|
|
31
31
|
import { recognise } from "./recognition.js";
|
|
32
32
|
import { leafIdRun } from "./canonical.js";
|
|
33
33
|
import { corpusN, edgeAncestors } from "./traverse.js";
|
|
@@ -65,14 +65,19 @@ export async function climbAttentionAll(
|
|
|
65
65
|
k: number,
|
|
66
66
|
mode: DFMode = "inverse",
|
|
67
67
|
): Promise<AttentionRead> {
|
|
68
|
+
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
69
|
+
// respondTurn(). Skipped while tracing.
|
|
68
70
|
if (ctx.climbMemo && !ctx.trace) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const contentKey = latin1Key(query);
|
|
72
|
+
const modeKey = `${k}:${mode}`;
|
|
73
|
+
let byRead = ctx.climbMemo.get(contentKey);
|
|
74
|
+
if (byRead === undefined) {
|
|
75
|
+
ctx.climbMemo.set(contentKey, byRead = new Map());
|
|
76
|
+
}
|
|
77
|
+
const hit = byRead.get(modeKey);
|
|
73
78
|
if (hit !== undefined) return hit;
|
|
74
79
|
const read = await computeAttention(ctx, query, k, mode);
|
|
75
|
-
byRead.set(
|
|
80
|
+
byRead.set(modeKey, read);
|
|
76
81
|
return read;
|
|
77
82
|
}
|
|
78
83
|
return computeAttention(ctx, query, k, mode);
|
|
@@ -168,8 +168,21 @@ export async function recallByResonance(
|
|
|
168
168
|
// long answer sharing only scaffolding pass; the query-relative fraction
|
|
169
169
|
// measures exactly what the reach bar means: how much of THE QUERY the
|
|
170
170
|
// store accounts for.
|
|
171
|
+
// Chance similarity survives the length conversion AMPLIFIED: the same
|
|
172
|
+
// √(lenG/lenQ) factor that converts an honest shared fraction into a
|
|
173
|
+
// query-relative one multiplies the estimator/chance floor too, so a long
|
|
174
|
+
// stored form (√(lenG/lenQ) ≈ 10 at 100×) lifted a noise-level cosine past
|
|
175
|
+
// the reach bar and grounded pure gibberish (observed). Only the
|
|
176
|
+
// ABOVE-CHANCE part of the similarity is evidence of shared content —
|
|
177
|
+
// subtract the significance bar (3/√D, §8.3) before converting. Derived
|
|
178
|
+
// from the existing bars; never tuned.
|
|
179
|
+
const sig = significanceBar(ctx.store.D);
|
|
171
180
|
const fracOfQuery = (cos: number, otherLen: number): number =>
|
|
172
|
-
Math.min(
|
|
181
|
+
Math.min(
|
|
182
|
+
1,
|
|
183
|
+
Math.max(0, cos - sig) *
|
|
184
|
+
Math.sqrt(otherLen / Math.max(1, query.length)),
|
|
185
|
+
);
|
|
173
186
|
for (const h of whole) {
|
|
174
187
|
const g = await project(ctx, h.id, queryGist);
|
|
175
188
|
if (g) {
|
|
@@ -186,18 +199,21 @@ export async function recallByResonance(
|
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
201
|
}
|
|
189
|
-
// The refusal/echo decision
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// reach bar
|
|
194
|
-
//
|
|
195
|
-
//
|
|
202
|
+
// The refusal/echo decision. The echo returns a stored form's bytes AS
|
|
203
|
+
// the answer — a near-identity claim about the query — and identity-grade
|
|
204
|
+
// decisions are never made on an estimated score ("approximate scores may
|
|
205
|
+
// rank and propose; they may never decide", §6.2): the RaBitQ estimate
|
|
206
|
+
// overshooting the reach bar echoed a WRONG-entity neighbour ("capital of
|
|
207
|
+
// Zamunda?" echoed the Armenia fact, observed). The bytes are read
|
|
208
|
+
// anyway to be echoed, so the decision uses their EXACT fold: one river
|
|
209
|
+
// fold of the top hit, measured in the same query-relative,
|
|
210
|
+
// chance-corrected units as the tier above.
|
|
196
211
|
const reach = reachThreshold(ctx.space.maxGroup);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
const topBytes = read(ctx, top.id);
|
|
213
|
+
const exact = topBytes.length > 0
|
|
214
|
+
? cosine(queryGist, gistOf(ctx, topBytes))
|
|
215
|
+
: 0;
|
|
216
|
+
if (fracOfQuery(exact, topBytes.length) < reach) {
|
|
201
217
|
return ground(
|
|
202
218
|
null,
|
|
203
219
|
"below reach threshold — nothing in the store relates to this query",
|
|
@@ -207,7 +223,7 @@ export async function recallByResonance(
|
|
|
207
223
|
}
|
|
208
224
|
// Honest echo.
|
|
209
225
|
return ground(
|
|
210
|
-
|
|
226
|
+
topBytes,
|
|
211
227
|
"last resort: the nearest resonant form's own bytes (echo, not grounded)",
|
|
212
228
|
[],
|
|
213
229
|
0,
|
package/src/mind/mind.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { bindSeat, fold, Sema, Space } from "../sema.js";
|
|
|
14
14
|
import { Alphabet } from "../alphabet.js";
|
|
15
15
|
import {
|
|
16
16
|
bytesToTree,
|
|
17
|
+
bytesToTreePyramid,
|
|
18
|
+
type FoldPyramid,
|
|
17
19
|
Grid,
|
|
18
20
|
gridToTree,
|
|
19
21
|
hilbertBytes,
|
|
@@ -68,6 +70,53 @@ export interface Response {
|
|
|
68
70
|
provenance?: import("./pipeline.js").Provenance;
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/** Serializable state of a conversation — can be saved and restored across
|
|
74
|
+
* sessions. The Mind never interprets the bytes; it only tracks their
|
|
75
|
+
* cumulative lengths so the caller can reconstruct turn boundaries later
|
|
76
|
+
* without inspecting content. */
|
|
77
|
+
export interface ConversationState {
|
|
78
|
+
/** The accumulated context bytes — raw concatenation of every turn's
|
|
79
|
+
* bytes in order. No separator is inserted; the boundary offsets
|
|
80
|
+
* ({@link boundaries}) tell the caller where each turn ends. */
|
|
81
|
+
context: Uint8Array;
|
|
82
|
+
/** Cumulative byte length after each completed turn. Sorted, strictly
|
|
83
|
+
* increasing, each {@code < context.length}. The first turn's length
|
|
84
|
+
* is `boundaries[0]`; the second turn starts at that offset, and so
|
|
85
|
+
* on. Empty for a single-turn or new conversation. */
|
|
86
|
+
boundaries: number[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** An active conversation handle. Opaque — interact through the Mind's
|
|
90
|
+
* conversation methods ({@link Mind.beginConversation},
|
|
91
|
+
* {@link Mind.respondTurn}, {@link Mind.endConversation}). */
|
|
92
|
+
export interface Conversation {
|
|
93
|
+
readonly id: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Internal per-conversation state.
|
|
97
|
+
*
|
|
98
|
+
* The {@link pyramid} IS the conversation — the accumulated internal
|
|
99
|
+
* processing state. {@link bytes} is the raw accumulated input, kept
|
|
100
|
+
* in sync with the pyramid for O(1) concatenation.
|
|
101
|
+
*
|
|
102
|
+
* Memos persist across turns so the inference pipeline does not
|
|
103
|
+
* re-process the prefix. Content-keyed (latin1) — each turn's fresh
|
|
104
|
+
* {@code Uint8Array} would never hit an object-identity key.
|
|
105
|
+
*
|
|
106
|
+
* {@link resolvedSubtrees} caches foldTree resolutions at the Sema-node
|
|
107
|
+
* level. When the pyramid reuses prefix subtrees (identical objects),
|
|
108
|
+
* foldTree returns their ids immediately — O(suffix) instead of
|
|
109
|
+
* O(context) for every tree walk. */
|
|
110
|
+
interface ConversationData {
|
|
111
|
+
pyramid: FoldPyramid;
|
|
112
|
+
bytes: Uint8Array;
|
|
113
|
+
boundaries: number[];
|
|
114
|
+
perceiveMemo: Map<string, Sema>;
|
|
115
|
+
recogniseMemo: Map<string, Recognition>;
|
|
116
|
+
climbMemo: Map<string, Map<string, AttentionRead>>;
|
|
117
|
+
resolvedSubtrees: WeakMap<Sema, { id: number; len: number }>;
|
|
118
|
+
}
|
|
119
|
+
|
|
71
120
|
// Mind module imports
|
|
72
121
|
import type { AttentionRead, MindContext, Recognition } from "./types.js";
|
|
73
122
|
import { changedNodes, liftAnswer, spliceAll } from "./types.js";
|
|
@@ -75,6 +124,7 @@ import {
|
|
|
75
124
|
foldTree,
|
|
76
125
|
gistOf,
|
|
77
126
|
inputBytes,
|
|
127
|
+
latin1Key,
|
|
78
128
|
perceive as perceiveImpl,
|
|
79
129
|
read,
|
|
80
130
|
resolve as resolveImpl,
|
|
@@ -132,19 +182,23 @@ export class Mind implements MindContext {
|
|
|
132
182
|
/** The live rationale tracer for the inference currently in flight, or null. */
|
|
133
183
|
trace: Rationale | null = null;
|
|
134
184
|
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
* (every mechanism must emit its own steps), so a traced respond re-pays
|
|
138
|
-
* up to four consensus climbs plus repeat recognitions — that is where the
|
|
139
|
-
* traced-vs-untraced latency multiple comes from, by design. */
|
|
140
|
-
climbMemo: WeakMap<Uint8Array, Map<string, AttentionRead>> | null = null;
|
|
185
|
+
/** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
|
|
186
|
+
climbMemo: Map<string, Map<string, AttentionRead>> | null = null;
|
|
141
187
|
|
|
142
|
-
/**
|
|
143
|
-
recogniseMemo:
|
|
188
|
+
/** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
|
|
189
|
+
recogniseMemo: Map<string, Recognition> | null = null;
|
|
144
190
|
|
|
145
|
-
/**
|
|
191
|
+
/** Memo of perceive() — content-keyed. See {@link MindContext.perceiveMemo}. */
|
|
146
192
|
perceiveMemo: Map<string, import("../sema.js").Sema> | null = null;
|
|
147
193
|
|
|
194
|
+
/** Subtree-resolution cache. See {@link MindContext._resolvedSubtrees}. */
|
|
195
|
+
_resolvedSubtrees:
|
|
196
|
+
| WeakMap<
|
|
197
|
+
import("../sema.js").Sema,
|
|
198
|
+
{ id: number; len: number }
|
|
199
|
+
>
|
|
200
|
+
| null = null;
|
|
201
|
+
|
|
148
202
|
/** The perceived gist of the query currently being answered. Set by `think`
|
|
149
203
|
* before the graph search runs; `chooseNext` consults it as a gate (a null
|
|
150
204
|
* guide means no query is in flight, so structural walkers keep plain
|
|
@@ -176,6 +230,11 @@ export class Mind implements MindContext {
|
|
|
176
230
|
_depositLens = new Set<number>();
|
|
177
231
|
_internIds = new WeakMap<import("../sema.js").Sema, number>();
|
|
178
232
|
|
|
233
|
+
// ── Conversation state ──────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
private _nextConvId = 1;
|
|
236
|
+
private _conversations = new Map<number, ConversationData>();
|
|
237
|
+
|
|
179
238
|
// ── GraphSearchHost implementation ─────────────────────────────────────
|
|
180
239
|
|
|
181
240
|
/** Canonical node id of a byte span. Required by GraphSearchHost & MindContext. */
|
|
@@ -321,8 +380,8 @@ export class Mind implements MindContext {
|
|
|
321
380
|
* created, so adding a memo cannot forget its reset. */
|
|
322
381
|
private beginResponse(inspectRationale?: InspectRationale): void {
|
|
323
382
|
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
324
|
-
this.climbMemo = new
|
|
325
|
-
this.recogniseMemo = new
|
|
383
|
+
this.climbMemo = new Map();
|
|
384
|
+
this.recogniseMemo = new Map();
|
|
326
385
|
this.perceiveMemo = new Map();
|
|
327
386
|
}
|
|
328
387
|
|
|
@@ -337,24 +396,26 @@ export class Mind implements MindContext {
|
|
|
337
396
|
this._edgeChoice.clear();
|
|
338
397
|
}
|
|
339
398
|
|
|
340
|
-
|
|
341
|
-
|
|
399
|
+
/** Shared response core — the one path from bytes to voiced answer.
|
|
400
|
+
* `respond` calls this directly; `respondTurn` has its own path
|
|
401
|
+
* with conversation-persistent memos and incremental perception. */
|
|
402
|
+
private async _respondImpl(
|
|
403
|
+
queryBytes: Uint8Array,
|
|
342
404
|
inspectRationale?: InspectRationale,
|
|
405
|
+
traceLabel = "respond",
|
|
343
406
|
): Promise<Response> {
|
|
344
407
|
this.beginResponse(inspectRationale);
|
|
345
408
|
try {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
rItem(inBytes, "query"),
|
|
409
|
+
const top = this.trace?.enter(traceLabel, [
|
|
410
|
+
rItem(queryBytes, "query"),
|
|
349
411
|
]);
|
|
350
|
-
|
|
351
|
-
const thought = await think(this, inBytes, this.mechanisms);
|
|
412
|
+
const thought = await think(this, queryBytes, this.mechanisms);
|
|
352
413
|
if (thought === null) {
|
|
353
414
|
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
354
415
|
return { v: null, bytes: new Uint8Array(0) };
|
|
355
416
|
}
|
|
356
417
|
|
|
357
|
-
const voiced = await articulate(this, thought.bytes,
|
|
418
|
+
const voiced = await articulate(this, thought.bytes, queryBytes);
|
|
358
419
|
top?.done(
|
|
359
420
|
[rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
|
|
360
421
|
"the answer, re-voiced in the asker's words",
|
|
@@ -369,6 +430,13 @@ export class Mind implements MindContext {
|
|
|
369
430
|
}
|
|
370
431
|
}
|
|
371
432
|
|
|
433
|
+
async respond(
|
|
434
|
+
input: Input,
|
|
435
|
+
inspectRationale?: InspectRationale,
|
|
436
|
+
): Promise<Response> {
|
|
437
|
+
return this._respondImpl(inputBytes(this, input), inspectRationale);
|
|
438
|
+
}
|
|
439
|
+
|
|
372
440
|
/** Text view of {@link respond}. NUL bytes (0x00) are stripped before
|
|
373
441
|
* decoding — they are structural padding in text answers. LOSSY for a
|
|
374
442
|
* binary answer that legitimately contains NULs: use {@link respond} and
|
|
@@ -381,6 +449,169 @@ export class Mind implements MindContext {
|
|
|
381
449
|
return decodeText(r.bytes);
|
|
382
450
|
}
|
|
383
451
|
|
|
452
|
+
// ── Conversation API ────────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
/** Begin a new conversation, optionally restoring from a previously-saved
|
|
455
|
+
* {@link ConversationState}. The returned handle is required for
|
|
456
|
+
* {@link respondTurn} and {@link endConversation}.
|
|
457
|
+
*
|
|
458
|
+
* Conversations are independent — a Mind can manage several concurrently.
|
|
459
|
+
* Each tracks the fold pyramid (accumulated internal processing) and
|
|
460
|
+
* turn-boundary offsets; the geometry never inspects content to guess
|
|
461
|
+
* where one turn ends and the next begins. */
|
|
462
|
+
beginConversation(state?: ConversationState): Conversation {
|
|
463
|
+
const id = this._nextConvId++;
|
|
464
|
+
// Build the initial pyramid: from scratch for a new conversation,
|
|
465
|
+
// or from the saved context bytes when restoring.
|
|
466
|
+
const initBytes = state?.context ?? new Uint8Array(0);
|
|
467
|
+
const { pyramid } = bytesToTreePyramid(
|
|
468
|
+
this.space,
|
|
469
|
+
this.alphabet,
|
|
470
|
+
initBytes,
|
|
471
|
+
);
|
|
472
|
+
this._conversations.set(id, {
|
|
473
|
+
pyramid,
|
|
474
|
+
bytes: initBytes,
|
|
475
|
+
boundaries: state?.boundaries ? [...state.boundaries] : [],
|
|
476
|
+
perceiveMemo: new Map(),
|
|
477
|
+
recogniseMemo: new Map(),
|
|
478
|
+
climbMemo: new Map(),
|
|
479
|
+
resolvedSubtrees: new WeakMap(),
|
|
480
|
+
});
|
|
481
|
+
return { id };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** End a conversation, releasing its internal resources (accumulated
|
|
485
|
+
* context, boundary offsets, and the fold-pyramid cache). Idempotent. */
|
|
486
|
+
endConversation(conv: Conversation): void {
|
|
487
|
+
this._conversations.delete(conv.id);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** The current serialisable state of an active conversation. Save this
|
|
491
|
+
* to resume the conversation later via {@link beginConversation}. */
|
|
492
|
+
conversationState(conv: Conversation): ConversationState | null {
|
|
493
|
+
const data = this._conversations.get(conv.id);
|
|
494
|
+
if (!data) return null;
|
|
495
|
+
return {
|
|
496
|
+
context: data.bytes,
|
|
497
|
+
boundaries: [...data.boundaries],
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Process one turn of a conversation.
|
|
502
|
+
*
|
|
503
|
+
* `turn` is the raw input for the latest turn — its bytes are appended
|
|
504
|
+
* to the accumulated context directly (raw concatenation). The Mind
|
|
505
|
+
* tracks the byte offset where each turn ends; no separator is ever
|
|
506
|
+
* inserted or inspected.
|
|
507
|
+
*
|
|
508
|
+
* Returns the response AND the updated {@link ConversationState} so the
|
|
509
|
+
* caller can persist it. The conversation handle's internal state is
|
|
510
|
+
* updated in place — the returned state is a snapshot for storage. */
|
|
511
|
+
async respondTurn(
|
|
512
|
+
conv: Conversation,
|
|
513
|
+
turn: Input,
|
|
514
|
+
inspectRationale?: InspectRationale,
|
|
515
|
+
): Promise<{ response: Response; state: ConversationState }> {
|
|
516
|
+
const data = this._conversations.get(conv.id);
|
|
517
|
+
if (!data) throw new Error(`Conversation ${conv.id} not found`);
|
|
518
|
+
|
|
519
|
+
const turnBytes = inputBytes(this, turn);
|
|
520
|
+
const prevLen = data.pyramid.bytes;
|
|
521
|
+
|
|
522
|
+
const prevBytes = data.bytes;
|
|
523
|
+
const newContext = prevLen > 0 ? concat2(prevBytes, turnBytes) : turnBytes;
|
|
524
|
+
data.bytes = newContext;
|
|
525
|
+
|
|
526
|
+
if (prevLen > 0) data.boundaries.push(prevLen);
|
|
527
|
+
|
|
528
|
+
// Incremental perception — O(turn) instead of O(context).
|
|
529
|
+
const { tree, pyramid } = bytesToTreePyramid(
|
|
530
|
+
this.space,
|
|
531
|
+
this.alphabet,
|
|
532
|
+
newContext,
|
|
533
|
+
data.pyramid,
|
|
534
|
+
);
|
|
535
|
+
data.pyramid = pyramid;
|
|
536
|
+
|
|
537
|
+
// Swap in the conversation's persistent state so the inference
|
|
538
|
+
// pipeline does not re-process the prefix from scratch.
|
|
539
|
+
// perceiveMemo / recogniseMemo / climbMemo — content-keyed;
|
|
540
|
+
// the prefix's results from the previous turn are found by
|
|
541
|
+
// the current turn's sub-span calls.
|
|
542
|
+
// _resolvedSubtrees — Sema-node-keyed; when the pyramid reuses
|
|
543
|
+
// prefix subtrees (identical objects), foldTree returns their
|
|
544
|
+
// ids immediately — O(suffix) instead of O(context).
|
|
545
|
+
this.perceiveMemo = data.perceiveMemo;
|
|
546
|
+
this.recogniseMemo = data.recogniseMemo;
|
|
547
|
+
this.climbMemo = data.climbMemo;
|
|
548
|
+
this._resolvedSubtrees = data.resolvedSubtrees;
|
|
549
|
+
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
this.perceiveMemo.set(latin1Key(newContext), tree);
|
|
553
|
+
|
|
554
|
+
const top = this.trace?.enter("respondTurn", [
|
|
555
|
+
rItem(newContext, "query"),
|
|
556
|
+
]);
|
|
557
|
+
|
|
558
|
+
const thought = await think(this, newContext, this.mechanisms);
|
|
559
|
+
if (thought === null) {
|
|
560
|
+
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
561
|
+
return {
|
|
562
|
+
response: { v: null, bytes: new Uint8Array(0) },
|
|
563
|
+
state: this.conversationState(conv)!,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const voiced = await articulate(this, thought.bytes, newContext);
|
|
568
|
+
top?.done(
|
|
569
|
+
[rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
|
|
570
|
+
"the answer, re-voiced in the asker's words",
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
response: {
|
|
575
|
+
v: gistOf(this, voiced),
|
|
576
|
+
bytes: voiced,
|
|
577
|
+
provenance: thought.provenance,
|
|
578
|
+
},
|
|
579
|
+
state: this.conversationState(conv)!,
|
|
580
|
+
};
|
|
581
|
+
} finally {
|
|
582
|
+
// Save back to the conversation for the next turn.
|
|
583
|
+
data.perceiveMemo = this.perceiveMemo;
|
|
584
|
+
data.recogniseMemo = this.recogniseMemo;
|
|
585
|
+
data.climbMemo = this.climbMemo;
|
|
586
|
+
data.resolvedSubtrees = this._resolvedSubtrees!;
|
|
587
|
+
// Clear Mind references — non-conversation respond() calls get
|
|
588
|
+
// fresh per-response memos.
|
|
589
|
+
this.trace = null;
|
|
590
|
+
this.perceiveMemo = null;
|
|
591
|
+
this.recogniseMemo = null;
|
|
592
|
+
this.climbMemo = null;
|
|
593
|
+
this._resolvedSubtrees = null;
|
|
594
|
+
this._edgeGuide = null;
|
|
595
|
+
this._edgeChoice.clear();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Text view of {@link respondTurn}. See {@link respondText} for the
|
|
600
|
+
* NUL-stripping caveat. For binary or grid turns use {@link respondTurn}
|
|
601
|
+
* directly — this is a text-only convenience, like {@link respondText}. */
|
|
602
|
+
async respondTurnText(
|
|
603
|
+
conv: Conversation,
|
|
604
|
+
turn: string,
|
|
605
|
+
inspectRationale?: InspectRationale,
|
|
606
|
+
): Promise<{ response: string; state: ConversationState }> {
|
|
607
|
+
const { response, state } = await this.respondTurn(
|
|
608
|
+
conv,
|
|
609
|
+
turn,
|
|
610
|
+
inspectRationale,
|
|
611
|
+
);
|
|
612
|
+
return { response: decodeText(response.bytes), state };
|
|
613
|
+
}
|
|
614
|
+
|
|
384
615
|
async embedding(input: Input): Promise<Vec | null> {
|
|
385
616
|
return (await this.respond(input)).v;
|
|
386
617
|
}
|
package/src/mind/primitives.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
// Read — node → bytes (read)
|
|
5
5
|
|
|
6
6
|
import { Vec } from "../vec.js";
|
|
7
|
-
import { Sema
|
|
8
|
-
import { Alphabet } from "../alphabet.js";
|
|
7
|
+
import { Sema } from "../sema.js";
|
|
9
8
|
import {
|
|
10
9
|
bytesToTree,
|
|
11
10
|
bytesToTreePyramid,
|
|
@@ -24,7 +23,7 @@ import type { Input, MindContext } from "./types.js";
|
|
|
24
23
|
* collision-free encoding. Spans on the perception path are query-scale
|
|
25
24
|
* (windows, regions, candidate spans), so key construction is far cheaper
|
|
26
25
|
* than the river fold it deduplicates. */
|
|
27
|
-
function latin1Key(bytes: Uint8Array): string {
|
|
26
|
+
export function latin1Key(bytes: Uint8Array): string {
|
|
28
27
|
// Batched String.fromCharCode — avoids the O(n²) cost of repeated += on
|
|
29
28
|
// potentially-large query spans, and stays well under the ~65536 arg limit.
|
|
30
29
|
const n = bytes.length;
|
|
@@ -36,12 +35,19 @@ function latin1Key(bytes: Uint8Array): string {
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
/** Perceive input into a content-defined tree (the river fold).
|
|
39
|
-
* Deterministic — identical bytes always produce an identical tree.
|
|
38
|
+
* Deterministic — identical bytes always produce an identical tree.
|
|
39
|
+
*
|
|
40
|
+
* `boundaries` is an optional sorted list of proper byte offsets where the
|
|
41
|
+
* fold must split so that each prefix segment folds identically to how it
|
|
42
|
+
* folded when it was learned (§10.3 stable-prefix contract). Only the
|
|
43
|
+
* CALLER — who assembled the multi-turn context — knows where those
|
|
44
|
+
* boundaries are; the geometry never guesses them from the bytes. */
|
|
40
45
|
export function perceive(
|
|
41
46
|
ctx: MindContext,
|
|
42
47
|
input: Input,
|
|
43
48
|
leafAt?: (i: number) => number | null,
|
|
44
49
|
lookup?: (ids: number[]) => number | null,
|
|
50
|
+
boundaries?: readonly number[],
|
|
45
51
|
): Sema {
|
|
46
52
|
if (typeof input === "string" || input instanceof Uint8Array) {
|
|
47
53
|
const bytes = typeof input === "string"
|
|
@@ -57,11 +63,25 @@ export function perceive(
|
|
|
57
63
|
const key = latin1Key(bytes);
|
|
58
64
|
const hit = memo.get(key);
|
|
59
65
|
if (hit !== undefined) return hit;
|
|
60
|
-
const tree = bytesToTree(
|
|
66
|
+
const tree = bytesToTree(
|
|
67
|
+
ctx.space,
|
|
68
|
+
ctx.alphabet,
|
|
69
|
+
bytes,
|
|
70
|
+
undefined,
|
|
71
|
+
undefined,
|
|
72
|
+
boundaries,
|
|
73
|
+
);
|
|
61
74
|
memo.set(key, tree);
|
|
62
75
|
return tree;
|
|
63
76
|
}
|
|
64
|
-
return bytesToTree(
|
|
77
|
+
return bytesToTree(
|
|
78
|
+
ctx.space,
|
|
79
|
+
ctx.alphabet,
|
|
80
|
+
bytes,
|
|
81
|
+
undefined,
|
|
82
|
+
undefined,
|
|
83
|
+
boundaries,
|
|
84
|
+
);
|
|
65
85
|
}
|
|
66
86
|
return bytesToTree(ctx.space, ctx.alphabet, bytes, leafAt, lookup);
|
|
67
87
|
}
|
|
@@ -136,11 +156,25 @@ export function foldTree(
|
|
|
136
156
|
start: number,
|
|
137
157
|
visit?: (n: Sema, start: number, end: number, node: number | null) => void,
|
|
138
158
|
): { end: number; node: number | null } {
|
|
159
|
+
// Fast path: subtree already resolved (from a previous conversation turn
|
|
160
|
+
// or an earlier recognition pass). The pyramid reuses prefix subtrees as
|
|
161
|
+
// identical Sema objects, so this cache turns foldTree into O(suffix)
|
|
162
|
+
// instead of O(context) for multi-turn recognition.
|
|
163
|
+
const cached = ctx._resolvedSubtrees?.get(n);
|
|
164
|
+
if (cached !== undefined) {
|
|
165
|
+
const end = start + cached.len;
|
|
166
|
+
visit?.(n, start, end, cached.id);
|
|
167
|
+
return { end, node: cached.id };
|
|
168
|
+
}
|
|
169
|
+
|
|
139
170
|
if (n.kids === null) {
|
|
140
171
|
const b = n.leaf ?? new Uint8Array(0);
|
|
141
172
|
const end = start + b.length;
|
|
142
173
|
const node = ctx.store.findLeaf(b);
|
|
143
174
|
visit?.(n, start, end, node);
|
|
175
|
+
if (node !== null && ctx._resolvedSubtrees) {
|
|
176
|
+
ctx._resolvedSubtrees.set(n, { id: node, len: b.length });
|
|
177
|
+
}
|
|
144
178
|
return { end, node };
|
|
145
179
|
}
|
|
146
180
|
let pos = start;
|
|
@@ -154,6 +188,9 @@ export function foldTree(
|
|
|
154
188
|
}
|
|
155
189
|
const node = known ? ctx.store.findBranch(kids) : null;
|
|
156
190
|
visit?.(n, start, pos, node);
|
|
191
|
+
if (node !== null && ctx._resolvedSubtrees) {
|
|
192
|
+
ctx._resolvedSubtrees.set(n, { id: node, len: pos - start });
|
|
193
|
+
}
|
|
157
194
|
return { end: pos, node };
|
|
158
195
|
}
|
|
159
196
|
|
package/src/mind/recognition.ts
CHANGED
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
import { rItem } from "./trace.js";
|
|
8
8
|
|
|
9
9
|
import type { MindContext, Recognition, Segment } from "./types.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
foldTree,
|
|
12
|
+
gistOf,
|
|
13
|
+
latin1Key,
|
|
14
|
+
perceive,
|
|
15
|
+
resolve,
|
|
16
|
+
} from "./primitives.js";
|
|
17
|
+
import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
|
|
12
18
|
import { chainReach, leafIdAt } from "./canonical.js";
|
|
13
19
|
import { isChunk, type Sema } from "../sema.js";
|
|
14
20
|
import type { Leaf, Site } from "./graph-search.js";
|
|
@@ -26,15 +32,15 @@ import type { Leaf, Site } from "./graph-search.js";
|
|
|
26
32
|
*
|
|
27
33
|
* Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
|
|
28
34
|
export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// emits its rationale step.
|
|
35
|
+
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
36
|
+
// respondTurn() (where the map persists across calls). Skipped while
|
|
37
|
+
// tracing so every call still emits its rationale step.
|
|
33
38
|
if (ctx.recogniseMemo && !ctx.trace) {
|
|
34
|
-
const
|
|
39
|
+
const key = latin1Key(bytes);
|
|
40
|
+
const hit = ctx.recogniseMemo.get(key);
|
|
35
41
|
if (hit !== undefined) return hit;
|
|
36
42
|
const fresh = recogniseImpl(ctx, bytes);
|
|
37
|
-
ctx.recogniseMemo.set(
|
|
43
|
+
ctx.recogniseMemo.set(key, fresh);
|
|
38
44
|
return fresh;
|
|
39
45
|
}
|
|
40
46
|
return recogniseImpl(ctx, bytes);
|
|
@@ -64,7 +70,19 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
64
70
|
return id;
|
|
65
71
|
};
|
|
66
72
|
|
|
73
|
+
// Byte atoms (implicit negative-id single-byte leaves) are admitted as
|
|
74
|
+
// recognised sites only while atoms can still DISCRIMINATE at this corpus
|
|
75
|
+
// scale (see {@link atomIsHub}). On a small store a single-letter fact
|
|
76
|
+
// ("a" → "A") is genuine learnt content and its site is essential; on a
|
|
77
|
+
// large one every letter of every query would otherwise become a
|
|
78
|
+
// "recognised form" — the bridge then finds junction connectors between
|
|
79
|
+
// bare letters, cover follows edges hanging off them, and pure noise
|
|
80
|
+
// ("qq8f3kz9…") grounds to an arbitrary learnt sentence instead of
|
|
81
|
+
// silence. Atoms stay available as leaves (PASS-carried literals) and
|
|
82
|
+
// through exact tier-0 resolution regardless.
|
|
83
|
+
const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
|
|
67
84
|
const emit = (start: number, end: number, id: number) => {
|
|
85
|
+
if (id < 0 && atomsAreHubs) return;
|
|
68
86
|
if (leadsSomewhere(ctx, id)) {
|
|
69
87
|
sites.push({ start, end, payload: id });
|
|
70
88
|
}
|