@hviana/sema 0.1.6 → 0.1.8
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 +42 -9
- package/dist/src/alu/src/parser.d.ts +9 -0
- package/dist/src/alu/src/parser.js +110 -2
- package/dist/src/canon.d.ts +26 -0
- package/dist/src/canon.js +57 -0
- package/dist/src/geometry.d.ts +13 -2
- package/dist/src/geometry.js +101 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/mind/attention.js +11 -7
- package/dist/src/mind/learning.js +33 -1
- package/dist/src/mind/match.d.ts +4 -2
- package/dist/src/mind/match.js +30 -6
- package/dist/src/mind/mechanisms/cast.js +18 -4
- package/dist/src/mind/mechanisms/confluence.js +13 -1
- package/dist/src/mind/mechanisms/recall.js +115 -31
- package/dist/src/mind/mind.d.ts +138 -12
- package/dist/src/mind/mind.js +284 -22
- package/dist/src/mind/primitives.d.ts +22 -2
- package/dist/src/mind/primitives.js +95 -6
- package/dist/src/mind/recognition.js +31 -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 +33 -21
- package/dist/src/store-sqlite.d.ts +10 -0
- package/dist/src/store-sqlite.js +58 -0
- package/dist/src/store.d.ts +23 -0
- package/dist/src/store.js +13 -2
- package/example/train_base.ts +49 -9
- package/index.html +65 -0
- package/package.json +1 -1
- package/src/alu/src/parser.ts +105 -4
- package/src/canon.ts +65 -0
- package/src/geometry.ts +105 -1
- package/src/index.ts +1 -0
- package/src/mind/attention.ts +11 -6
- package/src/mind/learning.ts +39 -1
- package/src/mind/match.ts +29 -6
- package/src/mind/mechanisms/cast.ts +21 -6
- package/src/mind/mechanisms/confluence.ts +15 -1
- package/src/mind/mechanisms/recall.ts +132 -41
- package/src/mind/mind.ts +396 -22
- package/src/mind/primitives.ts +109 -7
- package/src/mind/recognition.ts +36 -8
- package/src/mind/traverse.ts +47 -0
- package/src/mind/types.ts +30 -21
- package/src/store-sqlite.ts +76 -0
- package/src/store.ts +46 -2
- package/test/13-conversation.test.mjs +240 -20
- package/test/35-prefix-edge.test.mjs +86 -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,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
import { BoundedMap, type Store } from "../store.js";
|
|
24
26
|
import { SQliteStore } from "../store-sqlite.js";
|
|
25
27
|
import { type MindConfig, resolveConfig } from "../config.js";
|
|
28
|
+
import { type Canon, canonHash, textCanon } from "../canon.js";
|
|
26
29
|
import {
|
|
27
30
|
type CandidateSpan,
|
|
28
31
|
coverSequence,
|
|
@@ -68,6 +71,53 @@ export interface Response {
|
|
|
68
71
|
provenance?: import("./pipeline.js").Provenance;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
/** Serializable state of a conversation — can be saved and restored across
|
|
75
|
+
* sessions. The Mind never interprets the bytes; it only tracks their
|
|
76
|
+
* cumulative lengths so the caller can reconstruct turn boundaries later
|
|
77
|
+
* without inspecting content. */
|
|
78
|
+
export interface ConversationState {
|
|
79
|
+
/** The accumulated context bytes — raw concatenation of every turn's
|
|
80
|
+
* bytes in order. No separator is inserted; the boundary offsets
|
|
81
|
+
* ({@link boundaries}) tell the caller where each turn ends. */
|
|
82
|
+
context: Uint8Array;
|
|
83
|
+
/** Cumulative byte length after each completed turn. Sorted, strictly
|
|
84
|
+
* increasing, each {@code < context.length}. The first turn's length
|
|
85
|
+
* is `boundaries[0]`; the second turn starts at that offset, and so
|
|
86
|
+
* on. Empty for a single-turn or new conversation. */
|
|
87
|
+
boundaries: number[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** An active conversation handle. Opaque — interact through the Mind's
|
|
91
|
+
* conversation methods ({@link Mind.beginConversation},
|
|
92
|
+
* {@link Mind.respondTurn}, {@link Mind.endConversation}). */
|
|
93
|
+
export interface Conversation {
|
|
94
|
+
readonly id: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Internal per-conversation state.
|
|
98
|
+
*
|
|
99
|
+
* The {@link pyramid} IS the conversation — the accumulated internal
|
|
100
|
+
* processing state. {@link bytes} is the raw accumulated input, kept
|
|
101
|
+
* in sync with the pyramid for O(1) concatenation.
|
|
102
|
+
*
|
|
103
|
+
* Memos persist across turns so the inference pipeline does not
|
|
104
|
+
* re-process the prefix. Content-keyed (latin1) — each turn's fresh
|
|
105
|
+
* {@code Uint8Array} would never hit an object-identity key.
|
|
106
|
+
*
|
|
107
|
+
* {@link resolvedSubtrees} caches foldTree resolutions at the Sema-node
|
|
108
|
+
* level. When the pyramid reuses prefix subtrees (identical objects),
|
|
109
|
+
* foldTree returns their ids immediately — O(suffix) instead of
|
|
110
|
+
* O(context) for every tree walk. */
|
|
111
|
+
interface ConversationData {
|
|
112
|
+
pyramid: FoldPyramid;
|
|
113
|
+
bytes: Uint8Array;
|
|
114
|
+
boundaries: number[];
|
|
115
|
+
perceiveMemo: Map<string, Sema>;
|
|
116
|
+
recogniseMemo: Map<string, Recognition>;
|
|
117
|
+
climbMemo: Map<string, Map<string, AttentionRead>>;
|
|
118
|
+
resolvedSubtrees: WeakMap<Sema, { id: number; len: number }>;
|
|
119
|
+
}
|
|
120
|
+
|
|
71
121
|
// Mind module imports
|
|
72
122
|
import type { AttentionRead, MindContext, Recognition } from "./types.js";
|
|
73
123
|
import { changedNodes, liftAnswer, spliceAll } from "./types.js";
|
|
@@ -75,6 +125,7 @@ import {
|
|
|
75
125
|
foldTree,
|
|
76
126
|
gistOf,
|
|
77
127
|
inputBytes,
|
|
128
|
+
latin1Key,
|
|
78
129
|
perceive as perceiveImpl,
|
|
79
130
|
read,
|
|
80
131
|
resolve as resolveImpl,
|
|
@@ -110,6 +161,12 @@ export interface MindOptions {
|
|
|
110
161
|
mechanismFactories?: ((
|
|
111
162
|
host: import("../extension.js").ExtensionHost,
|
|
112
163
|
) => import("./pipeline-mechanism.js").PipelineMechanism)[];
|
|
164
|
+
/** Content canonicalizer applied to EVERY response (any modality) for
|
|
165
|
+
* equivalence-class resolution — see src/canon.ts. Text entry points
|
|
166
|
+
* ({@link Mind.respondText}, {@link Mind.respondTurnText}) inject the
|
|
167
|
+
* Unicode text canonicalizer automatically when this is unset; pass
|
|
168
|
+
* `false` to disable canonical resolution everywhere. */
|
|
169
|
+
canon?: Canon | false;
|
|
113
170
|
}
|
|
114
171
|
|
|
115
172
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -132,19 +189,36 @@ export class Mind implements MindContext {
|
|
|
132
189
|
/** The live rationale tracer for the inference currently in flight, or null. */
|
|
133
190
|
trace: Rationale | null = null;
|
|
134
191
|
|
|
135
|
-
/**
|
|
136
|
-
* {@link
|
|
137
|
-
*
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
192
|
+
/** The content canonicalizer for the response in flight — see
|
|
193
|
+
* {@link MindContext.canon}. Injected per response by the modality entry
|
|
194
|
+
* point; null when the response carries no equivalence. */
|
|
195
|
+
canon: Canon | null = null;
|
|
196
|
+
|
|
197
|
+
/** Per-response canonical-resolution memo — see {@link MindContext.canonMemo}. */
|
|
198
|
+
canonMemo: Map<string, number | null> | null = null;
|
|
199
|
+
|
|
200
|
+
/** The Mind-level canon option: a canonicalizer to use for EVERY response,
|
|
201
|
+
* `false` to disable canonical resolution, or null to let each entry
|
|
202
|
+
* point decide (text entry points inject {@link textCanon}). */
|
|
203
|
+
private _canonOpt: Canon | false | null = null;
|
|
204
|
+
|
|
205
|
+
/** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
|
|
206
|
+
climbMemo: Map<string, Map<string, AttentionRead>> | null = null;
|
|
141
207
|
|
|
142
|
-
/**
|
|
143
|
-
recogniseMemo:
|
|
208
|
+
/** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
|
|
209
|
+
recogniseMemo: Map<string, Recognition> | null = null;
|
|
144
210
|
|
|
145
|
-
/**
|
|
211
|
+
/** Memo of perceive() — content-keyed. See {@link MindContext.perceiveMemo}. */
|
|
146
212
|
perceiveMemo: Map<string, import("../sema.js").Sema> | null = null;
|
|
147
213
|
|
|
214
|
+
/** Subtree-resolution cache. See {@link MindContext._resolvedSubtrees}. */
|
|
215
|
+
_resolvedSubtrees:
|
|
216
|
+
| WeakMap<
|
|
217
|
+
import("../sema.js").Sema,
|
|
218
|
+
{ id: number; len: number }
|
|
219
|
+
>
|
|
220
|
+
| null = null;
|
|
221
|
+
|
|
148
222
|
/** The perceived gist of the query currently being answered. Set by `think`
|
|
149
223
|
* before the graph search runs; `chooseNext` consults it as a gate (a null
|
|
150
224
|
* guide means no query is in flight, so structural walkers keep plain
|
|
@@ -176,6 +250,11 @@ export class Mind implements MindContext {
|
|
|
176
250
|
_depositLens = new Set<number>();
|
|
177
251
|
_internIds = new WeakMap<import("../sema.js").Sema, number>();
|
|
178
252
|
|
|
253
|
+
// ── Conversation state ──────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
private _nextConvId = 1;
|
|
256
|
+
private _conversations = new Map<number, ConversationData>();
|
|
257
|
+
|
|
179
258
|
// ── GraphSearchHost implementation ─────────────────────────────────────
|
|
180
259
|
|
|
181
260
|
/** Canonical node id of a byte span. Required by GraphSearchHost & MindContext. */
|
|
@@ -226,8 +305,10 @@ export class Mind implements MindContext {
|
|
|
226
305
|
store: optsStore,
|
|
227
306
|
mechanisms: userMechs,
|
|
228
307
|
mechanismFactories: userFacts,
|
|
308
|
+
canon: optsCanon,
|
|
229
309
|
...rest
|
|
230
310
|
} = (optsOrCfg ?? {}) as MindOptions;
|
|
311
|
+
this._canonOpt = optsCanon ?? null;
|
|
231
312
|
this.cfg = resolveConfig(rest as Partial<MindConfig>);
|
|
232
313
|
this.store = optsStore ?? new SQliteStore({
|
|
233
314
|
maxGroup: this.cfg.geometry.maxGroup,
|
|
@@ -319,11 +400,24 @@ export class Mind implements MindContext {
|
|
|
319
400
|
/** Open one response's transient state — the tracer and the per-response
|
|
320
401
|
* memos. Paired with {@link endResponse}; the ONE place this state is
|
|
321
402
|
* created, so adding a memo cannot forget its reset. */
|
|
322
|
-
private beginResponse(
|
|
403
|
+
private beginResponse(
|
|
404
|
+
inspectRationale?: InspectRationale,
|
|
405
|
+
canon?: Canon | null,
|
|
406
|
+
): void {
|
|
323
407
|
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
324
|
-
this.climbMemo = new
|
|
325
|
-
this.recogniseMemo = new
|
|
408
|
+
this.climbMemo = new Map();
|
|
409
|
+
this.recogniseMemo = new Map();
|
|
326
410
|
this.perceiveMemo = new Map();
|
|
411
|
+
this.canon = canon ?? null;
|
|
412
|
+
this.canonMemo = canon ? new Map() : null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The canonicalizer a response should carry: the Mind-level option when
|
|
416
|
+
* set (or none when explicitly disabled), else the entry point's own
|
|
417
|
+
* default — text entry points pass {@link textCanon}, binary ones null. */
|
|
418
|
+
private _canonFor(entryDefault: Canon | null): Canon | null {
|
|
419
|
+
if (this._canonOpt === false) return null;
|
|
420
|
+
return this._canonOpt ?? entryDefault;
|
|
327
421
|
}
|
|
328
422
|
|
|
329
423
|
/** Close one response's transient state — every per-response field, incl.
|
|
@@ -333,28 +427,33 @@ export class Mind implements MindContext {
|
|
|
333
427
|
this.climbMemo = null;
|
|
334
428
|
this.recogniseMemo = null;
|
|
335
429
|
this.perceiveMemo = null;
|
|
430
|
+
this.canon = null;
|
|
431
|
+
this.canonMemo = null;
|
|
336
432
|
this._edgeGuide = null;
|
|
337
433
|
this._edgeChoice.clear();
|
|
338
434
|
}
|
|
339
435
|
|
|
340
|
-
|
|
341
|
-
|
|
436
|
+
/** Shared response core — the one path from bytes to voiced answer.
|
|
437
|
+
* `respond` calls this directly; `respondTurn` has its own path
|
|
438
|
+
* with conversation-persistent memos and incremental perception. */
|
|
439
|
+
private async _respondImpl(
|
|
440
|
+
queryBytes: Uint8Array,
|
|
342
441
|
inspectRationale?: InspectRationale,
|
|
442
|
+
traceLabel = "respond",
|
|
443
|
+
canon: Canon | null = null,
|
|
343
444
|
): Promise<Response> {
|
|
344
|
-
this.beginResponse(inspectRationale);
|
|
445
|
+
this.beginResponse(inspectRationale, canon);
|
|
345
446
|
try {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
rItem(inBytes, "query"),
|
|
447
|
+
const top = this.trace?.enter(traceLabel, [
|
|
448
|
+
rItem(queryBytes, "query"),
|
|
349
449
|
]);
|
|
350
|
-
|
|
351
|
-
const thought = await think(this, inBytes, this.mechanisms);
|
|
450
|
+
const thought = await think(this, queryBytes, this.mechanisms);
|
|
352
451
|
if (thought === null) {
|
|
353
452
|
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
354
453
|
return { v: null, bytes: new Uint8Array(0) };
|
|
355
454
|
}
|
|
356
455
|
|
|
357
|
-
const voiced = await articulate(this, thought.bytes,
|
|
456
|
+
const voiced = await articulate(this, thought.bytes, queryBytes);
|
|
358
457
|
top?.done(
|
|
359
458
|
[rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
|
|
360
459
|
"the answer, re-voiced in the asker's words",
|
|
@@ -369,10 +468,31 @@ export class Mind implements MindContext {
|
|
|
369
468
|
}
|
|
370
469
|
}
|
|
371
470
|
|
|
471
|
+
async respond(
|
|
472
|
+
input: Input,
|
|
473
|
+
inspectRationale?: InspectRationale,
|
|
474
|
+
): Promise<Response> {
|
|
475
|
+
// A STRING input is text by nature: it carries the text equivalence even
|
|
476
|
+
// through the generic entry point. Raw bytes / grids carry only the
|
|
477
|
+
// Mind-level canon option, if any.
|
|
478
|
+
const canon = this._canonFor(typeof input === "string" ? textCanon : null);
|
|
479
|
+
return this._respondImpl(
|
|
480
|
+
inputBytes(this, input),
|
|
481
|
+
inspectRationale,
|
|
482
|
+
"respond",
|
|
483
|
+
canon,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
372
487
|
/** Text view of {@link respond}. NUL bytes (0x00) are stripped before
|
|
373
488
|
* decoding — they are structural padding in text answers. LOSSY for a
|
|
374
489
|
* binary answer that legitimately contains NULs: use {@link respond} and
|
|
375
|
-
* read `bytes` directly for binary/grid modalities.
|
|
490
|
+
* read `bytes` directly for binary/grid modalities.
|
|
491
|
+
*
|
|
492
|
+
* Injects the TEXT canonicalizer (src/canon.ts) so resolution treats
|
|
493
|
+
* every character variation of the same text — case, width, whitespace —
|
|
494
|
+
* as one form, provided the store's canon index is built
|
|
495
|
+
* ({@link buildCanonIndex}). */
|
|
376
496
|
async respondText(
|
|
377
497
|
input: string,
|
|
378
498
|
inspectRationale?: InspectRationale,
|
|
@@ -381,6 +501,220 @@ export class Mind implements MindContext {
|
|
|
381
501
|
return decodeText(r.bytes);
|
|
382
502
|
}
|
|
383
503
|
|
|
504
|
+
// ── Conversation API ────────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
/** Begin a new conversation, optionally restoring from a previously-saved
|
|
507
|
+
* {@link ConversationState}. The returned handle is required for
|
|
508
|
+
* {@link respondTurn} and {@link endConversation}.
|
|
509
|
+
*
|
|
510
|
+
* Conversations are independent — a Mind can manage several concurrently.
|
|
511
|
+
* Each tracks the fold pyramid (accumulated internal processing) and
|
|
512
|
+
* turn-boundary offsets; the geometry never inspects content to guess
|
|
513
|
+
* where one turn ends and the next begins. */
|
|
514
|
+
beginConversation(state?: ConversationState): Conversation {
|
|
515
|
+
const id = this._nextConvId++;
|
|
516
|
+
// Build the initial pyramid: from scratch for a new conversation,
|
|
517
|
+
// or from the saved context bytes when restoring.
|
|
518
|
+
const initBytes = state?.context ?? new Uint8Array(0);
|
|
519
|
+
const { pyramid } = bytesToTreePyramid(
|
|
520
|
+
this.space,
|
|
521
|
+
this.alphabet,
|
|
522
|
+
initBytes,
|
|
523
|
+
);
|
|
524
|
+
this._conversations.set(id, {
|
|
525
|
+
pyramid,
|
|
526
|
+
bytes: initBytes,
|
|
527
|
+
boundaries: state?.boundaries ? [...state.boundaries] : [],
|
|
528
|
+
perceiveMemo: new Map(),
|
|
529
|
+
recogniseMemo: new Map(),
|
|
530
|
+
climbMemo: new Map(),
|
|
531
|
+
resolvedSubtrees: new WeakMap(),
|
|
532
|
+
});
|
|
533
|
+
return { id };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** End a conversation, releasing its internal resources (accumulated
|
|
537
|
+
* context, boundary offsets, and the fold-pyramid cache). Idempotent. */
|
|
538
|
+
endConversation(conv: Conversation): void {
|
|
539
|
+
this._conversations.delete(conv.id);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** The current serialisable state of an active conversation. Save this
|
|
543
|
+
* to resume the conversation later via {@link beginConversation}. */
|
|
544
|
+
conversationState(conv: Conversation): ConversationState | null {
|
|
545
|
+
const data = this._conversations.get(conv.id);
|
|
546
|
+
if (!data) return null;
|
|
547
|
+
return {
|
|
548
|
+
context: data.bytes,
|
|
549
|
+
boundaries: [...data.boundaries],
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Append a turn to a conversation's accumulated context WITHOUT
|
|
554
|
+
* responding — raw byte append plus a boundary offset, never a
|
|
555
|
+
* separator; the fold pyramid advances by O(turn).
|
|
556
|
+
*
|
|
557
|
+
* This is the primitive for turns the Mind should hear but not answer:
|
|
558
|
+
* replaying a transcript, feeding the OTHER speaker's line in a
|
|
559
|
+
* prediction harness, or restoring context piecewise. {@link
|
|
560
|
+
* respondTurn} = addTurn + think + its own reply appended the same way. */
|
|
561
|
+
addTurn(conv: Conversation, turn: Input): ConversationState {
|
|
562
|
+
const data = this._conversations.get(conv.id);
|
|
563
|
+
if (!data) throw new Error(`Conversation ${conv.id} not found`);
|
|
564
|
+
const turnBytes = inputBytes(this, turn);
|
|
565
|
+
this._growContext(data, turnBytes);
|
|
566
|
+
return this.conversationState(conv)!;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Grow a conversation's accumulated context by one turn's bytes — raw
|
|
570
|
+
* append plus a boundary offset, pyramid advanced by O(turn), the grown
|
|
571
|
+
* context's tree seeded into the conversation's perceive memo. The ONE
|
|
572
|
+
* place a context grows ({@link addTurn} and {@link respondTurn} both
|
|
573
|
+
* come through here), so the append semantics cannot drift. */
|
|
574
|
+
private _growContext(data: ConversationData, turnBytes: Uint8Array): Sema {
|
|
575
|
+
const prevLen = data.pyramid.bytes;
|
|
576
|
+
// An empty turn neither grows the context nor marks a boundary —
|
|
577
|
+
// boundaries are documented strictly increasing, and a zero-length
|
|
578
|
+
// "turn" is no turn. The pyramid's zero-growth shortcut makes the
|
|
579
|
+
// refold below O(1), so the existing tree is returned unchanged.
|
|
580
|
+
const grow = turnBytes.length > 0;
|
|
581
|
+
const grown = !grow
|
|
582
|
+
? data.bytes
|
|
583
|
+
: prevLen > 0
|
|
584
|
+
? concat2(data.bytes, turnBytes)
|
|
585
|
+
: turnBytes;
|
|
586
|
+
if (grow && prevLen > 0) data.boundaries.push(prevLen);
|
|
587
|
+
const { tree, pyramid } = bytesToTreePyramid(
|
|
588
|
+
this.space,
|
|
589
|
+
this.alphabet,
|
|
590
|
+
grown,
|
|
591
|
+
data.pyramid,
|
|
592
|
+
);
|
|
593
|
+
data.pyramid = pyramid;
|
|
594
|
+
data.bytes = grown;
|
|
595
|
+
data.perceiveMemo.set(latin1Key(grown), tree);
|
|
596
|
+
return tree;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Process one turn of a conversation.
|
|
600
|
+
*
|
|
601
|
+
* `turn` is the raw input for the latest turn — its bytes are appended
|
|
602
|
+
* to the accumulated context directly (raw concatenation). The Mind
|
|
603
|
+
* tracks the byte offset where each turn ends; no separator is ever
|
|
604
|
+
* inserted or inspected.
|
|
605
|
+
*
|
|
606
|
+
* Returns the response AND the updated {@link ConversationState} so the
|
|
607
|
+
* caller can persist it. The conversation handle's internal state is
|
|
608
|
+
* updated in place — the returned state is a snapshot for storage.
|
|
609
|
+
*
|
|
610
|
+
* SINGLE FLIGHT: at most one respondTurn may be in flight per Mind. The
|
|
611
|
+
* conversation's memo caches are swapped into the Mind-level per-response
|
|
612
|
+
* pointers for the duration of the turn, so a concurrently-running
|
|
613
|
+
* respond()/respondTurn() on the SAME Mind would interleave state.
|
|
614
|
+
* Different Minds (or sequential awaits, as in every test) are safe. */
|
|
615
|
+
async respondTurn(
|
|
616
|
+
conv: Conversation,
|
|
617
|
+
turn: Input,
|
|
618
|
+
inspectRationale?: InspectRationale,
|
|
619
|
+
): Promise<{ response: Response; state: ConversationState }> {
|
|
620
|
+
const data = this._conversations.get(conv.id);
|
|
621
|
+
if (!data) throw new Error(`Conversation ${conv.id} not found`);
|
|
622
|
+
|
|
623
|
+
const turnBytes = inputBytes(this, turn);
|
|
624
|
+
// Incremental perception — O(turn) instead of O(context).
|
|
625
|
+
const tree = this._growContext(data, turnBytes);
|
|
626
|
+
const newContext = data.bytes;
|
|
627
|
+
|
|
628
|
+
// Swap in the conversation's persistent state so the inference
|
|
629
|
+
// pipeline does not re-process the prefix from scratch.
|
|
630
|
+
// perceiveMemo / recogniseMemo / climbMemo — content-keyed;
|
|
631
|
+
// the prefix's results from the previous turn are found by
|
|
632
|
+
// the current turn's sub-span calls.
|
|
633
|
+
// _resolvedSubtrees — Sema-node-keyed; when the pyramid reuses
|
|
634
|
+
// prefix subtrees (identical objects), foldTree returns their
|
|
635
|
+
// ids immediately — O(suffix) instead of O(context).
|
|
636
|
+
this.perceiveMemo = data.perceiveMemo;
|
|
637
|
+
this.recogniseMemo = data.recogniseMemo;
|
|
638
|
+
this.climbMemo = data.climbMemo;
|
|
639
|
+
this._resolvedSubtrees = data.resolvedSubtrees;
|
|
640
|
+
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
641
|
+
// A string turn is text by nature — carry the text equivalence, same as
|
|
642
|
+
// respond() (see _canonFor).
|
|
643
|
+
this.canon = this._canonFor(typeof turn === "string" ? textCanon : null);
|
|
644
|
+
this.canonMemo = this.canon ? new Map() : null;
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const top = this.trace?.enter("respondTurn", [
|
|
648
|
+
rItem(newContext, "query"),
|
|
649
|
+
]);
|
|
650
|
+
|
|
651
|
+
const thought = await think(this, newContext, this.mechanisms);
|
|
652
|
+
if (thought === null) {
|
|
653
|
+
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
654
|
+
return {
|
|
655
|
+
response: { v: null, bytes: new Uint8Array(0) },
|
|
656
|
+
state: this.conversationState(conv)!,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const voiced = await articulate(this, thought.bytes, newContext);
|
|
661
|
+
top?.done(
|
|
662
|
+
[rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
|
|
663
|
+
"the answer, re-voiced in the asker's words",
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// The REPLY joins the accumulated context the same way a turn does
|
|
667
|
+
// ({@link addTurn}): raw byte append plus a boundary offset — never a
|
|
668
|
+
// separator. A conversation's context is the full exchange, exactly
|
|
669
|
+
// the cumulative continuous shape multi-turn training deposits, so a
|
|
670
|
+
// later turn can refer to what was ANSWERED ("which of those two…"),
|
|
671
|
+
// not only to what was asked.
|
|
672
|
+
if (voiced.length > 0) this.addTurn(conv, voiced);
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
response: {
|
|
676
|
+
v: gistOf(this, voiced),
|
|
677
|
+
bytes: voiced,
|
|
678
|
+
provenance: thought.provenance,
|
|
679
|
+
},
|
|
680
|
+
state: this.conversationState(conv)!,
|
|
681
|
+
};
|
|
682
|
+
} finally {
|
|
683
|
+
// No save-back: the conversation's memo MAPS were mutated in place —
|
|
684
|
+
// `data.*` still points at them. Re-assigning from the Mind-level
|
|
685
|
+
// pointers here was a no-op in the single-flight case and, under a
|
|
686
|
+
// concurrently-started respond() (which swaps its own fresh maps into
|
|
687
|
+
// those pointers), would inject a FOREIGN response's memos into this
|
|
688
|
+
// conversation. Clear Mind references — non-conversation respond()
|
|
689
|
+
// calls get fresh per-response memos.
|
|
690
|
+
this.trace = null;
|
|
691
|
+
this.perceiveMemo = null;
|
|
692
|
+
this.recogniseMemo = null;
|
|
693
|
+
this.climbMemo = null;
|
|
694
|
+
this.canon = null;
|
|
695
|
+
this.canonMemo = null;
|
|
696
|
+
this._resolvedSubtrees = null;
|
|
697
|
+
this._edgeGuide = null;
|
|
698
|
+
this._edgeChoice.clear();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** Text view of {@link respondTurn}. See {@link respondText} for the
|
|
703
|
+
* NUL-stripping caveat. For binary or grid turns use {@link respondTurn}
|
|
704
|
+
* directly — this is a text-only convenience, like {@link respondText}. */
|
|
705
|
+
async respondTurnText(
|
|
706
|
+
conv: Conversation,
|
|
707
|
+
turn: string,
|
|
708
|
+
inspectRationale?: InspectRationale,
|
|
709
|
+
): Promise<{ response: string; state: ConversationState }> {
|
|
710
|
+
const { response, state } = await this.respondTurn(
|
|
711
|
+
conv,
|
|
712
|
+
turn,
|
|
713
|
+
inspectRationale,
|
|
714
|
+
);
|
|
715
|
+
return { response: decodeText(response.bytes), state };
|
|
716
|
+
}
|
|
717
|
+
|
|
384
718
|
async embedding(input: Input): Promise<Vec | null> {
|
|
385
719
|
return (await this.respond(input)).v;
|
|
386
720
|
}
|
|
@@ -463,6 +797,46 @@ export class Mind implements MindContext {
|
|
|
463
797
|
);
|
|
464
798
|
}
|
|
465
799
|
|
|
800
|
+
// ── Canonical-form index ───────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
/** Build (or incrementally refresh) the store's canonical-form index: for
|
|
803
|
+
* every content-bearing node, record the hash of its CANONICAL key so
|
|
804
|
+
* resolution can find stored forms across surface variation (case, width,
|
|
805
|
+
* whitespace — whatever `canon` equates; see src/canon.ts).
|
|
806
|
+
*
|
|
807
|
+
* Incremental and idempotent: the last indexed node id is remembered in
|
|
808
|
+
* store meta (`canon.upto`), so a refresh after further training scans
|
|
809
|
+
* only the new rows. Run once after training, and again after ingests —
|
|
810
|
+
* the same operational shape as {@link repairContentIndex}.
|
|
811
|
+
*
|
|
812
|
+
* @param canon the canonicalizer to index under — MUST be the same one
|
|
813
|
+
* queries will carry (text queries carry {@link textCanon}
|
|
814
|
+
* unless the Mind was constructed with its own)
|
|
815
|
+
* @returns number of index rows added */
|
|
816
|
+
async buildCanonIndex(canon?: Canon): Promise<number> {
|
|
817
|
+
const c = canon ?? this._canonFor(textCanon);
|
|
818
|
+
const store = this.store;
|
|
819
|
+
if (c === null || !store.canonAdd || !store.eachContent) return 0;
|
|
820
|
+
const from = Number(await store.getMeta("canon.upto") ?? 0);
|
|
821
|
+
let added = 0;
|
|
822
|
+
let maxId = from - 1;
|
|
823
|
+
store.eachContent((id, bytes) => {
|
|
824
|
+
if (id > maxId) maxId = id;
|
|
825
|
+
const key = c(bytes);
|
|
826
|
+
if (key.length === 0) return;
|
|
827
|
+
// Only index content whose canonical key DIFFERS from its raw bytes —
|
|
828
|
+
// an already-canonical span is found by the exact lookup (and by the
|
|
829
|
+
// fallback's own exact probe of the canonical bytes), so indexing it
|
|
830
|
+
// would only add rows.
|
|
831
|
+
if (bytesEqual(key, bytes)) return;
|
|
832
|
+
store.canonAdd!(canonHash(key), id);
|
|
833
|
+
added++;
|
|
834
|
+
}, from);
|
|
835
|
+
await store.setMeta("canon.upto", String(maxId + 1));
|
|
836
|
+
store.commit();
|
|
837
|
+
return added;
|
|
838
|
+
}
|
|
839
|
+
|
|
466
840
|
// ── Persistence ──────────────────────────────────────────────────────────
|
|
467
841
|
|
|
468
842
|
async save(): Promise<Uint8Array> {
|