@hviana/sema 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/AGENTS.md +43 -4
  2. package/DATASETS.md +160 -0
  3. package/README.md +12 -0
  4. package/dist/example/train_base/cache.d.ts +35 -0
  5. package/dist/example/train_base/cache.js +211 -0
  6. package/dist/example/train_base/config.d.ts +21 -0
  7. package/dist/example/train_base/config.js +94 -0
  8. package/dist/example/train_base/corpora/aya.d.ts +19 -0
  9. package/dist/example/train_base/corpora/aya.js +76 -0
  10. package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
  11. package/dist/example/train_base/corpora/converted-parquet.js +44 -0
  12. package/dist/example/train_base/corpora/genknow.d.ts +14 -0
  13. package/dist/example/train_base/corpora/genknow.js +83 -0
  14. package/dist/example/train_base/corpora/index.d.ts +29 -0
  15. package/dist/example/train_base/corpora/index.js +81 -0
  16. package/dist/example/train_base/corpora/massive.d.ts +7 -0
  17. package/dist/example/train_base/corpora/massive.js +98 -0
  18. package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
  19. package/dist/example/train_base/corpora/oasst2.js +120 -0
  20. package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
  21. package/dist/example/train_base/corpora/smolsent.js +156 -0
  22. package/dist/example/train_base/corpora/soda.d.ts +12 -0
  23. package/dist/example/train_base/corpora/soda.js +113 -0
  24. package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
  25. package/dist/example/train_base/corpora/taskmaster.js +144 -0
  26. package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
  27. package/dist/example/train_base/corpora/wiki2.js +132 -0
  28. package/dist/example/train_base/corpus.d.ts +88 -0
  29. package/dist/example/train_base/corpus.js +65 -0
  30. package/dist/example/train_base/discovery.d.ts +48 -0
  31. package/dist/example/train_base/discovery.js +143 -0
  32. package/dist/example/train_base/http.d.ts +82 -0
  33. package/dist/example/train_base/http.js +219 -0
  34. package/dist/example/train_base/items.d.ts +46 -0
  35. package/dist/example/train_base/items.js +98 -0
  36. package/dist/example/train_base/main.d.ts +4 -0
  37. package/dist/example/train_base/main.js +207 -0
  38. package/dist/example/train_base/progress.d.ts +34 -0
  39. package/dist/example/train_base/progress.js +114 -0
  40. package/dist/example/train_base/readers.d.ts +125 -0
  41. package/dist/example/train_base/readers.js +391 -0
  42. package/dist/example/train_base/runtime.d.ts +115 -0
  43. package/dist/example/train_base/runtime.js +637 -0
  44. package/dist/example/train_base/stage.d.ts +3 -0
  45. package/dist/example/train_base/stage.js +246 -0
  46. package/dist/example/train_base/ui.d.ts +88 -0
  47. package/dist/example/train_base/ui.js +272 -0
  48. package/dist/src/geometry.d.ts +20 -0
  49. package/dist/src/geometry.js +22 -0
  50. package/dist/src/mind/attention.d.ts +6 -0
  51. package/dist/src/mind/attention.js +44 -4
  52. package/dist/src/mind/learning.js +134 -50
  53. package/dist/src/mind/mechanisms/cast.js +45 -1
  54. package/dist/src/mind/mind.d.ts +7 -2
  55. package/dist/src/mind/mind.js +15 -3
  56. package/dist/src/mind/reasoning.js +59 -5
  57. package/dist/src/mind/recognition.js +29 -3
  58. package/dist/src/mind/traverse.d.ts +16 -0
  59. package/dist/src/mind/traverse.js +18 -0
  60. package/dist/src/store-sqlite.d.ts +4 -0
  61. package/dist/src/store-sqlite.js +47 -0
  62. package/dist/src/store.d.ts +7 -0
  63. package/example/train_base/cache.ts +251 -0
  64. package/example/train_base/config.ts +128 -0
  65. package/example/train_base/corpora/aya.ts +106 -0
  66. package/example/train_base/corpora/converted-parquet.ts +64 -0
  67. package/example/train_base/corpora/genknow.ts +114 -0
  68. package/example/train_base/corpora/index.ts +88 -0
  69. package/example/train_base/corpora/massive.ts +111 -0
  70. package/example/train_base/corpora/oasst2.ts +163 -0
  71. package/example/train_base/corpora/smolsent.ts +203 -0
  72. package/example/train_base/corpora/soda.ts +130 -0
  73. package/example/train_base/corpora/taskmaster.ts +217 -0
  74. package/example/train_base/corpora/wiki2.ts +190 -0
  75. package/example/train_base/corpus.ts +150 -0
  76. package/example/train_base/discovery.ts +203 -0
  77. package/example/train_base/http.ts +284 -0
  78. package/example/train_base/items.ts +118 -0
  79. package/example/train_base/main.ts +240 -0
  80. package/example/train_base/progress.ts +149 -0
  81. package/example/train_base/readers.ts +505 -0
  82. package/example/train_base/runtime.ts +894 -0
  83. package/example/train_base/stage.ts +276 -0
  84. package/example/train_base/ui.ts +333 -0
  85. package/jsr.json +1 -1
  86. package/package.json +2 -4
  87. package/src/geometry.ts +23 -0
  88. package/src/mind/attention.ts +54 -1
  89. package/src/mind/learning.ts +137 -43
  90. package/src/mind/mechanisms/cast.ts +48 -1
  91. package/src/mind/mind.ts +13 -2
  92. package/src/mind/reasoning.ts +64 -5
  93. package/src/mind/recognition.ts +29 -3
  94. package/src/mind/traverse.ts +19 -0
  95. package/src/store-sqlite.ts +53 -0
  96. package/src/store.ts +28 -0
  97. package/test/13-conversation.test.mjs +1 -1
  98. package/test/29-counterfactual.test.mjs +43 -6
  99. package/test/77-company-saturation.test.mjs +302 -0
  100. package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
  101. package/test/84-composed-answer-honesty.test.mjs +137 -0
  102. package/test/85-answered-directly.test.mjs +126 -0
  103. package/test/86-cast-voices-committed.test.mjs +164 -0
  104. package/test/87-codominant-commitment.test.mjs +250 -0
  105. package/test/88-dependency-footprint.test.mjs +99 -0
  106. package/dist/example/train_base.d.ts +0 -93
  107. package/dist/example/train_base.js +0 -2269
  108. package/example/train_base.ts +0 -2735
@@ -0,0 +1,284 @@
1
+ // train_base/http.ts — the shared network policy: wait out throttling, retry
2
+ // what is transient, give up at once on what is not.
3
+ //
4
+ // A leaf module by construction: everything it needs to cancel (the run's
5
+ // AbortSignal) and everything it needs to report (a throttle notice) arrives
6
+ // as a parameter. There is no module-level shutdown handle and no module-level
7
+ // log hook — those were globals precisely because this code used to live in the
8
+ // same file as the run that owned them.
9
+
10
+ import { DOWNLOAD_TRIES } from "./config.js";
11
+
12
+ /** Sleep `ms`, but wake early if `signal` fires — so a long back-off (e.g. a
13
+ * rate-limit wait) never swallows Ctrl+C. Resolves either way. */
14
+ export const waitMs = (ms: number, signal: AbortSignal): Promise<void> =>
15
+ new Promise((resolve) => {
16
+ if (signal.aborted) return resolve();
17
+ // NOTE: the timer is deliberately NOT unref'd — an unref'd timer does not
18
+ // keep the event loop alive, so a pending wait (e.g. the pace between page
19
+ // requests, or a rate-limit back-off) would let Node exit early and the run
20
+ // would "do nothing and close". The listener lets a shutdown wake it early.
21
+ const t = setTimeout(done, ms);
22
+ function done() {
23
+ clearTimeout(t);
24
+ signal.removeEventListener("abort", done);
25
+ resolve();
26
+ }
27
+ signal.addEventListener("abort", done, { once: true });
28
+ });
29
+
30
+ /** Resolve `p`, but reject with a TimeoutError if it takes longer than `ms`.
31
+ * The underlying promise is left to settle on its own (we just stop waiting),
32
+ * so a slow black-box call can never wedge the caller. */
33
+ export function withTimeout<T>(
34
+ p: Promise<T>,
35
+ ms: number,
36
+ label = "operation",
37
+ ): Promise<T> {
38
+ return new Promise<T>((resolve, reject) => {
39
+ const t = setTimeout(() => {
40
+ const e: Error & { name: string } = new Error(
41
+ `${label} timed out after ${ms}ms`,
42
+ );
43
+ e.name = "TimeoutError";
44
+ reject(e);
45
+ }, ms);
46
+ if (typeof (t as any).unref === "function") (t as any).unref();
47
+ p.then(
48
+ (v) => {
49
+ clearTimeout(t);
50
+ resolve(v);
51
+ },
52
+ (e) => {
53
+ clearTimeout(t);
54
+ reject(e);
55
+ },
56
+ );
57
+ });
58
+ }
59
+
60
+ /** An HTTP error the caller tagged as transient. `.fatal` skips all retries;
61
+ * `.throttle` (a 429/503 rate-limit or overload) is retried indefinitely and
62
+ * does NOT consume the bounded attempt budget — the server told us to wait, not
63
+ * to give up. `.retryAfterMs` carries a server-suggested delay when present. */
64
+ export type HttpError = Error & {
65
+ fatal?: boolean;
66
+ throttle?: boolean;
67
+ retryAfterMs?: number;
68
+ };
69
+
70
+ /** What every network call in this trainer needs from its caller: how to be
71
+ * cancelled, and (optionally) where to report a rate-limit wait. */
72
+ export interface HttpOptions {
73
+ signal: AbortSignal;
74
+ /** Called after each throttle wait, so a 429 back-off reads as "waiting"
75
+ * rather than a silent hang. Omitted ⇒ the wait is silent. */
76
+ onThrottle?: (waitMsAmount: number, label: string) => void;
77
+ }
78
+
79
+ /** Wrap a throttle notice so a STORM of 429s logs at most one notice every
80
+ * `minGapMs`. Without the gap a busy server produces a wall of identical
81
+ * "waiting…" lines that pushes the real log out of the scrollback. */
82
+ export function throttleNotifier(
83
+ fn: (waitMsAmount: number, label: string) => void,
84
+ minGapMs = 3000,
85
+ ): (waitMsAmount: number, label: string) => void {
86
+ let last = 0;
87
+ return (ms, label) => {
88
+ const now = Date.now();
89
+ if (now - last <= minGapMs) return;
90
+ last = now;
91
+ fn(ms, label);
92
+ };
93
+ }
94
+
95
+ /** Retry `fn` with exponential backoff.
96
+ *
97
+ * Three error classes:
98
+ * • `.fatal` / AbortError → rethrown immediately (never retried).
99
+ * • `.throttle` (429/503) → the server is rate-limiting/overloaded. We are
100
+ * NOT failing — we WAIT (honouring Retry-After, else capped exponential
101
+ * back-off with jitter) and retry WITHOUT consuming an attempt, so a
102
+ * throttled request holds on until it succeeds rather than being dropped.
103
+ * Only a shutdown breaks this loop.
104
+ * • anything else → a genuine transient error, retried up to `tries`
105
+ * with exponential back-off before giving up.
106
+ *
107
+ * `onFail` is called after each non-throttle failed attempt; `onThrottle` after
108
+ * each throttle wait (for a "waiting…" notice). */
109
+ export async function retry<T>(
110
+ label: string,
111
+ fn: () => Promise<T>,
112
+ tries: number,
113
+ opts: HttpOptions & { onFail?: (attempt: number, err: Error) => void },
114
+ ): Promise<T> {
115
+ const { signal, onFail, onThrottle } = opts;
116
+ let wait = 1000, last = "", throttleWait = 1000;
117
+ for (let attempt = 1; attempt <= tries;) {
118
+ if (signal.aborted) {
119
+ const e: HttpError = new Error("aborted");
120
+ e.fatal = true;
121
+ throw e;
122
+ }
123
+ try {
124
+ return await fn();
125
+ } catch (e) {
126
+ const err = e as HttpError;
127
+ if (err.name === "AbortError" || err.fatal) throw err;
128
+
129
+ // Rate-limited / overloaded: wait it out. Does NOT advance `attempt`, so a
130
+ // busy server can never exhaust the retry budget and drop the request.
131
+ if (err.throttle && !signal.aborted) {
132
+ // Honour Retry-After when the server sent one; else exponential back-off
133
+ // with jitter, capped, so a fleet of requests does not resynchronise.
134
+ const base = err.retryAfterMs && err.retryAfterMs > 0
135
+ ? err.retryAfterMs
136
+ : throttleWait;
137
+ const ms = Math.min(base, 60_000) +
138
+ Math.floor(base * 0.25 * Math.random());
139
+ onThrottle?.(ms, label);
140
+ await waitMs(ms, signal);
141
+ throttleWait = Math.min(throttleWait * 2, 60_000);
142
+ continue;
143
+ }
144
+
145
+ last = err.message;
146
+ onFail?.(attempt, err);
147
+ attempt++;
148
+ if (attempt <= tries) {
149
+ await waitMs(wait, signal);
150
+ wait = Math.min(wait * 2, 30_000);
151
+ }
152
+ }
153
+ }
154
+ throw new Error(`${label} failed after ${tries} attempts: ${last}`);
155
+ }
156
+
157
+ /** Classify a non-OK HTTP response into an {@link HttpError} for {@link retry}:
158
+ * • 429 / 503 → THROTTLE (rate-limited / overloaded): retried indefinitely,
159
+ * honouring a Retry-After header (seconds or an HTTP-date) when present.
160
+ * • other 5xx → transient: retried up to the caller's attempt budget.
161
+ * • other 4xx → FATAL: a real client error (404, 401, …) — not retried.
162
+ * Never throttles forever silently: the wait is interruptible by shutdown. */
163
+ export function httpError(res: Response): HttpError {
164
+ const err: HttpError = new Error(`HTTP ${res.status}`);
165
+ if (res.status === 429 || res.status === 503) {
166
+ err.throttle = true;
167
+ const ra = res.headers.get("retry-after");
168
+ if (ra) {
169
+ const secs = Number(ra);
170
+ if (Number.isFinite(secs)) err.retryAfterMs = Math.max(0, secs * 1000);
171
+ else {
172
+ const when = Date.parse(ra);
173
+ if (Number.isFinite(when)) {
174
+ err.retryAfterMs = Math.max(0, when - Date.now());
175
+ }
176
+ }
177
+ }
178
+ } else if (res.status < 500) {
179
+ err.fatal = true; // genuine client error — do not retry
180
+ } // other 5xx: neither fatal nor throttle → ordinary bounded retry
181
+ return err;
182
+ }
183
+
184
+ /** GET a URL and parse JSON, with the shared retry policy: rate-limits (429/503)
185
+ * WAIT indefinitely (surfaced through `opts.onThrottle`), other 4xx is fatal,
186
+ * other 5xx retried up to DOWNLOAD_TRIES. Used by every dataset LISTING call so
187
+ * all share the same never-drop-on-throttle behaviour. */
188
+ export async function getJson(
189
+ url: string,
190
+ label: string,
191
+ opts: HttpOptions,
192
+ ): Promise<any> {
193
+ return retry(
194
+ label,
195
+ async () => {
196
+ const res = await fetch(url, { signal: opts.signal });
197
+ if (res.ok) return res.json();
198
+ throw httpError(res);
199
+ },
200
+ DOWNLOAD_TRIES,
201
+ opts,
202
+ );
203
+ }
204
+
205
+ /** The `rel="next"` URL of an RFC 5988 Link header, or null. */
206
+ export function nextLink(header: string | null): string | null {
207
+ if (!header) return null;
208
+ for (const part of header.split(",")) {
209
+ const m = part.match(/<([^>]+)>\s*;\s*rel\s*=\s*"?next"?/i);
210
+ if (m) return m[1];
211
+ }
212
+ return null;
213
+ }
214
+
215
+ /** GET a paginated JSON ARRAY, following `Link: rel="next"` to the end.
216
+ *
217
+ * A LISTING THAT STOPS EARLY IS INVISIBLE, and that is why this exists.
218
+ * Hugging Face caps a tree listing at 1,000 entries and hands back a next
219
+ * link (verified: allenai/c4 returns exactly 1,000 plus a link). A caller that
220
+ * ignores it gets a work-list silently missing everything past the first page,
221
+ * trains it, marks those units complete, and thereafter reports the corpus
222
+ * "already trained". No error at any point. Following the links is the only
223
+ * way the work-list can be trusted to be the whole work-list.
224
+ *
225
+ * `maxPages` is a runaway guard, not a limit anyone should hit; exceeding it
226
+ * throws rather than returning a partial list, for exactly the reason above. */
227
+ export async function getJsonPaged(
228
+ url: string,
229
+ label: string,
230
+ opts: HttpOptions,
231
+ maxPages = 500,
232
+ ): Promise<unknown[]> {
233
+ const out: unknown[] = [];
234
+ let next: string | null = url;
235
+ let pages = 0;
236
+ while (next !== null) {
237
+ const at: string = next;
238
+ const { body, link } = await retry(
239
+ label,
240
+ async () => {
241
+ const res = await fetch(at, { signal: opts.signal });
242
+ if (!res.ok) throw httpError(res);
243
+ return { body: await res.json(), link: res.headers.get("link") };
244
+ },
245
+ DOWNLOAD_TRIES,
246
+ opts,
247
+ );
248
+ if (!Array.isArray(body)) break; // not a listing — nothing to page through
249
+ out.push(...body);
250
+ next = nextLink(link);
251
+ if (++pages >= maxPages && next) {
252
+ throw new Error(
253
+ `${label}: more than ${maxPages} pages of listing — refusing to ` +
254
+ `continue with a work-list that may be incomplete`,
255
+ );
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+
261
+ /** Advertised transfer size of `url`, used only to reserve cache room. Like any
262
+ * `content-length` this is the ON-THE-WIRE size, so for a content-coded source
263
+ * (GitHub raw gzips JSON ~14x) it UNDER-estimates the file that lands on disk.
264
+ * That is tolerable here because the cache ceiling is a budget, not a
265
+ * correctness property — a run may overshoot MAX_CACHE_GB by the compression
266
+ * ratio of one in-flight file, and each file is deleted as soon as it is
267
+ * consumed. It must NOT be reused as an integrity check; see downloadFile.
268
+ *
269
+ * Rate-limits wait; other 4xx is fatal; total failure → the caller's catch. */
270
+ export async function headSize(
271
+ url: string,
272
+ opts: HttpOptions,
273
+ ): Promise<number> {
274
+ return retry(
275
+ `HEAD ${url}`,
276
+ async () => {
277
+ const res = await fetch(url, { method: "HEAD", signal: opts.signal });
278
+ if (res.ok) return Number(res.headers.get("content-length")) || 0;
279
+ throw httpError(res);
280
+ },
281
+ 4,
282
+ opts,
283
+ );
284
+ }
@@ -0,0 +1,118 @@
1
+ // train_base/items.ts — the REPRESENTATION core: what a training item is, and
2
+ // the three shapes a corpus datum may take.
3
+ //
4
+ // REPRESENTATION POLICY (one datum → one form; no replication):
5
+ // • FACTS are the default. A datum that is a RELATION (translation pair,
6
+ // question → answer) is emitted as a (context → continuation) edge SEMA
7
+ // points at and, by example across the corpus, generalizes from (cf.
8
+ // example/demo.ts).
9
+ // • EXPERIENCES (bare statements) are used only when a fact is NOT possible —
10
+ // content with no natural relational split.
11
+ // • CUMULATIVE CONTINUOUS CONTEXT is used only when truly necessary — genuine
12
+ // MULTI-TURN dialogue, where a turn follows from the whole conversation so
13
+ // far. The fact stages do NOT synthesize a multi-turn walk, which would just
14
+ // replicate the facts (repetition SEMA avoids).
15
+ //
16
+ // Nothing here reads the environment or touches I/O: these are the pure
17
+ // functions every corpus adapter is built out of.
18
+
19
+ export interface Episode {
20
+ context: string;
21
+ continuation: string;
22
+ }
23
+ export type TrainingItem = string | Episode;
24
+
25
+ export const isEpisode = (it: TrainingItem): it is Episode =>
26
+ typeof it !== "string";
27
+
28
+ /** One turn of a dialogue, attributed to a speaker. The speaker is only ever
29
+ * used to decide MERGING (see `mergeSpeakerTurns`); it is never deposited. */
30
+ export interface SpeakerTurn {
31
+ // Upper-cased by the adapters, so USER/ASSISTANT (TM-1/2) and user/assistant
32
+ // (TM-3/4) compare equal.
33
+ speaker: string;
34
+ text: string;
35
+ }
36
+
37
+ /** Build the accumulated-context episodes of a turn sequence: each successive
38
+ * turn is the continuation of ALL the turns before it joined together. This is
39
+ * the same cumulative-context shape a multi-turn conversation deposits, so the
40
+ * store learns to continue a growing context.
41
+ *
42
+ * The "\n" below is a CORPUS choice, not a protocol. oasst2 turns are
43
+ * paragraphs, and reading them back with the newlines kept is how this corpus
44
+ * reads naturally; a different corpus may join with nothing, and
45
+ * test/13-conversation.test.mjs does exactly that. Neither has to match the
46
+ * other, because Sema never scans content for turn boundaries — those are
47
+ * offsets the Conversation API carries beside the bytes (see Mind.addTurn's
48
+ * "ON SEPARATORS" note). The newline here is simply part of the text this
49
+ * store learnt, so anything replaying this corpus feeds it back as part of
50
+ * the turn: `addTurn(conv, "\n" + turnText)`. It is not a convention the
51
+ * engine, the API, or the tests have to agree on. */
52
+ export function accumulate(turns: string[]): Episode[] {
53
+ const out: Episode[] = [];
54
+ for (let i = 1; i < turns.length; i++) {
55
+ out.push({ context: turns.slice(0, i).join("\n"), continuation: turns[i] });
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /** Collapse consecutive same-speaker turns into one, joining with a space, and
61
+ * return the bare texts in order. A turn with no speaker never merges with its
62
+ * neighbour: an unlabelled row is of unknown origin, and joining two of them
63
+ * would invent a contribution that may span two speakers.
64
+ *
65
+ * Load-bearing for corpora that split one contribution across several indexed
66
+ * utterances (an artifact of the collection UI). Left unmerged, the cumulative
67
+ * walk deposits a turn boundary in the middle of one speaker's contribution
68
+ * and teaches it as a hand-off. Measured share of turns absorbed by merging:
69
+ * TM-1 17.7%, TM-2 11.9%, TM-3 0.8%, TM-4 0.0%. */
70
+ export function mergeSpeakerTurns(turns: SpeakerTurn[]): string[] {
71
+ const out: string[] = [];
72
+ let prev = "";
73
+ for (const t of turns) {
74
+ if (out.length > 0 && t.speaker !== "" && t.speaker === prev) {
75
+ out[out.length - 1] += " " + t.text;
76
+ } else {
77
+ out.push(t.text);
78
+ }
79
+ prev = t.speaker;
80
+ }
81
+ return out;
82
+ }
83
+
84
+ /** Dedup + trim a concept's items: drop empty/degenerate pairs and exact
85
+ * repeats so a concept never deposits the same form twice. */
86
+ export function refineItems(items: TrainingItem[]): TrainingItem[] {
87
+ const out: TrainingItem[] = [];
88
+ const seen = new Set<string>();
89
+ for (const it of items) {
90
+ if (!isEpisode(it)) {
91
+ const exp = it.trim();
92
+ const key = "E:" + exp;
93
+ if (exp && !seen.has(key)) {
94
+ seen.add(key);
95
+ out.push(exp);
96
+ }
97
+ continue;
98
+ }
99
+ const ctx = it.context.trim();
100
+ const cont = it.continuation.trim();
101
+ if (!ctx || !cont || ctx === cont) continue;
102
+ const key = "P:" + ctx + "\u0000" + cont;
103
+ if (seen.has(key)) continue;
104
+ seen.add(key);
105
+ out.push({ context: ctx, continuation: cont });
106
+ }
107
+ return out;
108
+ }
109
+
110
+ const ENC = new TextEncoder();
111
+
112
+ /** Content size of a training item in UTF-8 bytes — the same quantity the
113
+ * scaling suite (14-scaling.test.mjs) measures as KB/s: for an episode the
114
+ * context plus the continuation, for a bare experience its own text. */
115
+ export const itemBytes = (it: TrainingItem): number =>
116
+ isEpisode(it)
117
+ ? ENC.encode(it.context).length + ENC.encode(it.continuation).length
118
+ : ENC.encode(it).length;
@@ -0,0 +1,240 @@
1
+ //This file uses the Google SMOL dataset, made available under the CC BY 4.0 license.
2
+ //This file uses Aya and oasst2 datasets, made available under the apache-2.0 license.
3
+ //
4
+ //A trained Sema store retains its training text VERBATIM, so distributing a
5
+ //store distributes these corpora and every upstream licence applies to it in
6
+ //full. Read DATASETS.md before adding a corpus here or publishing a store:
7
+ //it carries the per-corpus attribution a distributed store is required to
8
+ //travel with, and the two rules a candidate corpus must pass (no NonCommercial
9
+ //term, no ShareAlike term — checked against what the corpus was BUILT FROM,
10
+ //not merely against the repository's licence tag).
11
+
12
+ //This file is a more appropriate training example for Sema.
13
+ //Sema does not learn through repetition;
14
+ //it does not require a massive database.
15
+ //It needs fundamental datasets that teach basic cognitive concepts such as conversation, logic, relationships, behaviors and feelings.
16
+ //The focus is on covering fundamental patterns, not repetition.
17
+ //Tip: ontology-based adapted training datasets could be an interesting path.
18
+
19
+ // train_base/main.ts — streaming trainer for the SmolSent + Aya + oasst2 +
20
+ // Taskmaster + 2Wiki + SODA base.
21
+ //
22
+ // Training IS deposition: every source datum is translated into SEMA facts (or,
23
+ // for genuine dialogue, accumulated-context episodes), then stored in one pass.
24
+ // There are no gradients or epochs, and there is no LLM in the loop — the only
25
+ // "model" is the SEMA store itself.
26
+ //
27
+ // Every source here is commercially licensable (cc-by-4.0 / apache-2.0).
28
+ //
29
+ // WHERE THINGS LIVE (this file is the folder's entry point and nothing else):
30
+ //
31
+ // config.ts RUN-level knobs — store, checkpoint cadence, cache ceiling,
32
+ // MAX_MB, the Parquet read budget.
33
+ // items.ts the REPRESENTATION core: what a training item is (fact /
34
+ // experience / accumulated walk) and the policy governing
35
+ // which shape a datum may take.
36
+ // http.ts network policy: wait out throttling, retry what is
37
+ // transient, give up at once on what is not.
38
+ // cache.ts the durable disk cache and the atomic download sink — the
39
+ // sole irreducible Node dependency.
40
+ // readers.ts container formats: newline-delimited JSON (plain or
41
+ // gzipped), JSON arrays, Parquet.
42
+ // discovery.ts where a work-list comes from: an HF repo tree, the
43
+ // auto-converted Parquet branch, a GitHub directory, a local
44
+ // directory.
45
+ // progress.ts the resume record inside the store, and the index passes
46
+ // that keep a checkpoint queryable.
47
+ // ui.ts the live panel, the formatters, the recall box.
48
+ // runtime.ts the RUN: counters, the deposit gate, file acquisition,
49
+ // checkpointing, shutdown.
50
+ // corpus.ts WHAT A CORPUS IS — the contract a corpus declares, and the
51
+ // one rule that turns it into a resume id.
52
+ // stage.ts ONE loop, run once per corpus.
53
+ // corpora/ ONE FILE PER CORPUS: its knobs, its row adapter, its stage
54
+ // descriptor, and the evidence that fixed each default.
55
+ // corpora/index.ts is the curriculum, in order.
56
+ //
57
+ // Adding a corpus is therefore one new file in corpora/ plus its import, its
58
+ // place in CURRICULUM and its re-export in corpora/index.ts — and no change at
59
+ // all to the loop, the readers, or the run.
60
+ //
61
+ // Every source is DOWNLOADED as a file and streamed from disk (never paged
62
+ // row-by-row over an HTTP API — that was slow and rate-limited). Resume is
63
+ // per-file: a fully-consumed file is marked complete; an interrupted one
64
+ // re-reads from the top (re-deposition is idempotent). LOCAL_PATH may hold
65
+ // pre-downloaded files.
66
+ //
67
+ // The store IS the model: memories, training metadata, and the config snapshot
68
+ // all live in {DB_PATH}.sqlite, so a run resumes from the store alone.
69
+ //
70
+ // Built on web standards. All I/O except the durable disk cache uses platform
71
+ // primitives — fetch, WHATWG ReadableStream/WritableStream/TransformStream,
72
+ // DecompressionStream, TextDecoderStream, Blob, AbortController. The sole
73
+ // third-party code is hyparquet (+ its Snappy codec): a DEV dependency of this
74
+ // example, never of the library, and loaded by a dynamic import inside
75
+ // readers.ts the first time a Parquet corpus is read — so a curriculum with no
76
+ // Parquet stage needs it not at all. Consistency guarantees:
77
+ // • Resume from the store alone — completed stage-units, example count,
78
+ // learned-content bytes, and processed-byte total are persisted in
79
+ // {DB_PATH}.sqlite and reloaded.
80
+ // • Atomic cache — a download streams to "<file>.part", is fsync'd, then
81
+ // renamed into place; a file at its final path is, by construction,
82
+ // complete, so an interrupted download can never be mistaken for a cached
83
+ // one.
84
+ // • Bounded cache — a download blocks under the MAX_CACHE_GB ceiling and the
85
+ // fully-processed file is deleted immediately.
86
+ // • Interruptible — Ctrl+C (SIGINT/SIGTERM) aborts in-flight network at once,
87
+ // stops at the next item boundary, writes a final checkpoint, and exits; an
88
+ // un-finished stage-unit is NOT marked complete, so resume re-reads it (re-
89
+ // deposition is idempotent). A second Ctrl+C, or a 60s watchdog, force-exits.
90
+ //
91
+ // Run:
92
+ // npx tsc && node dist/example/train_base/main.js
93
+ // MAX_MB=500 node dist/example/train_base/main.js
94
+ // CHECKPOINT_MB=250 node dist/example/train_base/main.js
95
+ // SMOLSENT_PAIRS=ha_en,zu_en node dist/example/train_base/main.js # a subset of pairs
96
+ // SMOLSENT_DIRECTIONS=both node dist/example/train_base/main.js # also English->foreign
97
+ // SMOLSENT=0 node dist/example/train_base/main.js # skip SmolSent stage
98
+ // AYA=0 node dist/example/train_base/main.js # skip Aya stage
99
+ // OASST=0 node dist/example/train_base/main.js # skip oasst2 stage
100
+ // OASST_MIN_TURNS=6 node dist/example/train_base/main.js # deeper multi-turn only
101
+ // GENKNOW=1 node dist/example/train_base/main.js # General-Knowledge (see DATASETS.md §3.2)
102
+ // PARQUET_BATCH_MB=8 node dist/example/train_base/main.js # smaller Parquet reads on a tight host
103
+ // TASKMASTER=0 node dist/example/train_base/main.js # skip Taskmaster stage
104
+ // TASKMASTER_SETS=TM-3-2020 node dist/example/train_base/main.js # one Taskmaster set
105
+ // WIKI2=0 node dist/example/train_base/main.js # skip 2Wiki triples stage
106
+ // SODA=0 node dist/example/train_base/main.js # skip the SODA stage
107
+ // MASSIVE=1 node dist/example/train_base/main.js # enable MASSIVE (off by default)
108
+ // SODA_MAX_DIALOGS=0 node dist/example/train_base/main.js # lift the SODA budget
109
+ // WIKI2_MAX_ROWS=50000 node dist/example/train_base/main.js # budget the 2Wiki stage
110
+ // LOCAL_PATH=./base node dist/example/train_base/main.js # offline: *.jsonl/.parquet/.jsonl.gz/.json
111
+ // DB_PATH=./data/sema node dist/example/train_base/main.js
112
+
113
+ import { CachedIngest, Mind, SQliteStore } from "../../src/index.js";
114
+ import {
115
+ D,
116
+ DB_PATH,
117
+ SEED,
118
+ SQLITE_CACHE_MB,
119
+ VECTOR_CACHE_MB,
120
+ } from "./config.js";
121
+ import { createRuntime } from "./runtime.js";
122
+ import { runStage } from "./stage.js";
123
+ import { sweepPartials } from "./cache.js";
124
+ import { CURRICULUM, enabledLabels, storedLabels } from "./corpora/index.js";
125
+ import { bytes, DIM, dur, num, R, RED, SHOW } from "./ui.js";
126
+
127
+ // The parser/representation surface this module used to define itself. Kept
128
+ // exported from here so importing `example/train_base/main.js` still reaches every
129
+ // row adapter (toSmolSentRow, wikiTriplesToItems, …) and the shapes they build.
130
+ export * from "./items.js";
131
+ export * from "./corpora/index.js";
132
+ export { parquetBatchRows } from "./readers.js";
133
+ // The name this helper had while it lived here. It is `mergeSpeakerTurns` now,
134
+ // because SODA merges by the same rule and the Taskmaster-specific name was a
135
+ // lie — but the old name stays reachable so nothing importing it breaks.
136
+ export { mergeSpeakerTurns as mergeTaskmasterTurns } from "./items.js";
137
+
138
+ async function main(): Promise<void> {
139
+ const store = new SQliteStore({
140
+ path: DB_PATH,
141
+ D,
142
+ vectorCacheMb: VECTOR_CACHE_MB,
143
+ sqliteCacheMb: SQLITE_CACHE_MB,
144
+ });
145
+
146
+ // The store IS the model: memories, progress, and metadata all persist in
147
+ // it, so a resumed run just reopens the same store and continues. Guard
148
+ // against a changed D/SEED by comparing against what a previous run recorded.
149
+ const mind = new Mind({ seed: SEED, store });
150
+
151
+ // Pre-fill the vector indices' RAM caches with sequential scans (bounded by
152
+ // VECTOR_CACHE_MB). A resumed run over a large store otherwise spends its
153
+ // first minutes warming those caches through random point reads — the
154
+ // ingest hot path is cache-miss bound until then. Seconds, once, up front.
155
+ if (VECTOR_CACHE_MB > 0) {
156
+ const t = Date.now();
157
+ const warmed = await store.warmVectorCaches();
158
+ if (warmed > 0) {
159
+ process.stderr.write(
160
+ ` warmed vector caches: ${num(warmed)} rows in ${
161
+ dur((Date.now() - t) / 1000)
162
+ }\n`,
163
+ );
164
+ }
165
+ }
166
+ const ci = new CachedIngest(mind);
167
+ const prevD = await store.getMeta("train.D");
168
+ const prevSeed = await store.getMeta("train.seed");
169
+ if (
170
+ (prevD && Number(prevD) !== D) || (prevSeed && Number(prevSeed) !== SEED)
171
+ ) {
172
+ process.stderr.write(
173
+ `fatal: D/SEED changed (store has D=${prevD} seed=${prevSeed}, ` +
174
+ `requested D=${D} seed=${SEED}). Delete ${DB_PATH}.sqlite ` +
175
+ `to start fresh.\n`,
176
+ );
177
+ process.exit(1);
178
+ }
179
+
180
+ const dataset = enabledLabels();
181
+ await store.setMeta("train.D", String(D));
182
+ await store.setMeta("train.seed", String(SEED));
183
+ // createdAt is when this STORE was first trained into, so a resume must not
184
+ // overwrite it — the previous behaviour left every store claiming it was
185
+ // created at its most recent resume. When the current run started is a
186
+ // separate, also-useful fact, so it gets its own key rather than displacing
187
+ // this one.
188
+ if ((await store.getMeta("train.createdAt")) === null) {
189
+ await store.setMeta("train.createdAt", new Date().toISOString());
190
+ }
191
+ await store.setMeta("train.startedAt", new Date().toISOString());
192
+
193
+ const ctx = createRuntime({ store, mind, ci, title: dataset });
194
+ ctx.tick(true);
195
+
196
+ // Reclaim the debris of any download killed mid-transfer. It occupies cache
197
+ // ceiling that nothing else would ever free (see cache.ts sweepPartials).
198
+ const swept = sweepPartials();
199
+ if (swept.files > 0) {
200
+ ctx.progress.log(
201
+ ` ${DIM}· swept ${swept.files} interrupted download(s), ` +
202
+ `${bytes(swept.bytes)} reclaimed${R}`,
203
+ );
204
+ }
205
+
206
+ // ── resume — restore counters and the per-source tally from the store ──
207
+ await ctx.restore();
208
+
209
+ // What the STORE contains, which is only knowable after the resume: this
210
+ // run's stages plus whatever earlier runs deposited. The panel header above
211
+ // names this RUN's stages, which is a different and also-true fact.
212
+ await store.setMeta(
213
+ "train.dataset",
214
+ storedLabels(Object.keys(ctx.counters.langTally)),
215
+ );
216
+
217
+ // Walk the curriculum. Each stage skips itself on a resume that already
218
+ // finished it, and the walk stops at the first requested stop.
219
+ for (const corpus of CURRICULUM) {
220
+ if (ctx.stopRequested) break;
221
+ await runStage(ctx, corpus);
222
+ }
223
+
224
+ // Nothing is in flight any more; each stage announces itself as it starts, so
225
+ // this is the one transition that has no next stage to correct it.
226
+ ctx.state.activity = "idle";
227
+ await ctx.finish(ctx.stopRequested ? ctx.stopReason : "done");
228
+ }
229
+
230
+ // Only run when invoked directly, so importing the row adapters above (e.g.
231
+ // for a fixture check) never starts training.
232
+ const isMain = import.meta.url === `file://${process.argv[1]}` ||
233
+ process.argv[1]?.endsWith("train_base/main.js");
234
+ if (isMain) {
235
+ main().catch((e) => {
236
+ process.stderr.write(SHOW);
237
+ console.error(`\n${RED}fatal:${R}`, e);
238
+ process.exit(1);
239
+ });
240
+ }