@seanhogg/builderforce-memory-engine 2026.9.28 → 2026.9.30

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 (112) hide show
  1. package/dist/evermind/adapt.d.ts +13 -0
  2. package/dist/evermind/adapt.d.ts.map +1 -1
  3. package/dist/evermind/adapt.js +13 -1
  4. package/dist/evermind/adapt.js.map +1 -1
  5. package/dist/evermind/index.d.ts +3 -3
  6. package/dist/evermind/index.d.ts.map +1 -1
  7. package/dist/evermind/index.js +2 -2
  8. package/dist/evermind/index.js.map +1 -1
  9. package/dist/evermind/package.d.ts +16 -0
  10. package/dist/evermind/package.d.ts.map +1 -1
  11. package/dist/evermind/package.js +40 -14
  12. package/dist/evermind/package.js.map +1 -1
  13. package/dist/experience/approval.d.ts +16 -0
  14. package/dist/experience/approval.d.ts.map +1 -0
  15. package/dist/experience/approval.js +40 -0
  16. package/dist/experience/approval.js.map +1 -0
  17. package/dist/experience/corpus.d.ts +24 -0
  18. package/dist/experience/corpus.d.ts.map +1 -0
  19. package/dist/experience/corpus.js +90 -0
  20. package/dist/experience/corpus.js.map +1 -0
  21. package/dist/experience/index.d.ts +18 -0
  22. package/dist/experience/index.d.ts.map +1 -0
  23. package/dist/experience/index.js +8 -0
  24. package/dist/experience/index.js.map +1 -0
  25. package/dist/experience/overview.d.ts +52 -0
  26. package/dist/experience/overview.d.ts.map +1 -0
  27. package/dist/experience/overview.js +92 -0
  28. package/dist/experience/overview.js.map +1 -0
  29. package/dist/experience/schedule.d.ts +10 -0
  30. package/dist/experience/schedule.d.ts.map +1 -0
  31. package/dist/experience/schedule.js +22 -0
  32. package/dist/experience/schedule.js.map +1 -0
  33. package/dist/experience/store.d.ts +88 -0
  34. package/dist/experience/store.d.ts.map +1 -0
  35. package/dist/experience/store.js +128 -0
  36. package/dist/experience/store.js.map +1 -0
  37. package/dist/experience/trainOnce.d.ts +18 -0
  38. package/dist/experience/trainOnce.d.ts.map +1 -0
  39. package/dist/experience/trainOnce.js +74 -0
  40. package/dist/experience/trainOnce.js.map +1 -0
  41. package/dist/experience/types.d.ts +171 -0
  42. package/dist/experience/types.d.ts.map +1 -0
  43. package/dist/experience/types.js +22 -0
  44. package/dist/experience/types.js.map +1 -0
  45. package/dist/export/onnx.d.ts.map +1 -1
  46. package/dist/export/onnx.js +0 -8
  47. package/dist/export/onnx.js.map +1 -1
  48. package/dist/import/foreign/falcon_mamba.d.ts +15 -2
  49. package/dist/import/foreign/falcon_mamba.d.ts.map +1 -1
  50. package/dist/import/foreign/falcon_mamba.js +53 -36
  51. package/dist/import/foreign/falcon_mamba.js.map +1 -1
  52. package/dist/import/foreign/index.d.ts +5 -2
  53. package/dist/import/foreign/index.d.ts.map +1 -1
  54. package/dist/import/foreign/index.js +4 -2
  55. package/dist/import/foreign/index.js.map +1 -1
  56. package/dist/import/foreign/infer.d.ts +20 -0
  57. package/dist/import/foreign/infer.d.ts.map +1 -0
  58. package/dist/import/foreign/infer.js +58 -0
  59. package/dist/import/foreign/infer.js.map +1 -0
  60. package/dist/import/foreign/registry.d.ts.map +1 -1
  61. package/dist/import/foreign/registry.js +2 -1
  62. package/dist/import/foreign/registry.js.map +1 -1
  63. package/dist/import/foreign/vocab.d.ts +19 -0
  64. package/dist/import/foreign/vocab.d.ts.map +1 -0
  65. package/dist/import/foreign/vocab.js +40 -0
  66. package/dist/import/foreign/vocab.js.map +1 -0
  67. package/dist/import/index.d.ts +2 -2
  68. package/dist/import/index.d.ts.map +1 -1
  69. package/dist/import/index.js +1 -1
  70. package/dist/import/index.js.map +1 -1
  71. package/dist/import/safetensors.d.ts +1 -1
  72. package/dist/import/safetensors.js +11 -2
  73. package/dist/import/safetensors.js.map +1 -1
  74. package/dist/index.d.ts +5 -4
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -3
  77. package/dist/index.js.map +1 -1
  78. package/dist/moe/moe_model.d.ts +2 -0
  79. package/dist/moe/moe_model.d.ts.map +1 -1
  80. package/dist/moe/moe_model.js +9 -7
  81. package/dist/moe/moe_model.js.map +1 -1
  82. package/dist/moe/moe_trainer.d.ts.map +1 -1
  83. package/dist/moe/moe_trainer.js +5 -8
  84. package/dist/moe/moe_trainer.js.map +1 -1
  85. package/dist/utils/rng.d.ts +7 -3
  86. package/dist/utils/rng.d.ts.map +1 -1
  87. package/dist/utils/rng.js +10 -6
  88. package/dist/utils/rng.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/evermind/adapt.ts +26 -1
  91. package/src/evermind/index.ts +3 -3
  92. package/src/evermind/package.ts +39 -12
  93. package/src/experience/approval.ts +49 -0
  94. package/src/experience/corpus.ts +103 -0
  95. package/src/experience/index.ts +35 -0
  96. package/src/experience/overview.ts +143 -0
  97. package/src/experience/schedule.ts +29 -0
  98. package/src/experience/store.ts +188 -0
  99. package/src/experience/trainOnce.ts +85 -0
  100. package/src/experience/types.ts +162 -0
  101. package/src/export/onnx.ts +0 -7
  102. package/src/import/foreign/falcon_mamba.ts +63 -40
  103. package/src/import/foreign/index.ts +5 -2
  104. package/src/import/foreign/infer.ts +62 -0
  105. package/src/import/foreign/registry.ts +2 -1
  106. package/src/import/foreign/vocab.ts +50 -0
  107. package/src/import/index.ts +5 -0
  108. package/src/import/safetensors.ts +9 -2
  109. package/src/index.ts +14 -1
  110. package/src/moe/moe_model.ts +9 -7
  111. package/src/moe/moe_trainer.ts +5 -8
  112. package/src/utils/rng.ts +10 -6
@@ -0,0 +1,49 @@
1
+ /**
2
+ * experience/approval.ts — which learned steps pause for the person: the ones that
3
+ * cannot be taken back. A click on "Send", "Pay", "Delete" or "Submit" is where a replay
4
+ * that drifted does real damage, so the gate is on by default there. Review can still
5
+ * turn it on or off per step.
6
+ */
7
+ import type { Action } from './types.js';
8
+
9
+ /** Words, in the five product languages, that name an action with consequences outside the app. */
10
+ const IRREVERSIBLE = new Set([
11
+ // en
12
+ 'send', 'submit', 'pay', 'purchase', 'buy', 'order', 'checkout', 'delete', 'remove', 'confirm', 'transfer',
13
+ 'publish', 'post', 'sign', 'approve', 'book', 'cancel', 'erase', 'wipe', 'format', 'uninstall',
14
+ // de
15
+ 'senden', 'absenden', 'bezahlen', 'kaufen', 'bestellen', 'löschen', 'entfernen', 'bestätigen', 'überweisen', 'veröffentlichen',
16
+ // es
17
+ 'enviar', 'pagar', 'comprar', 'pedido', 'eliminar', 'borrar', 'confirmar', 'transferir', 'publicar',
18
+ // fr
19
+ 'envoyer', 'payer', 'acheter', 'commander', 'supprimer', 'confirmer', 'virement', 'publier',
20
+ ]);
21
+
22
+ /** CJK has no word boundaries, so these are matched as substrings. */
23
+ const IRREVERSIBLE_CJK = ['发送', '提交', '支付', '付款', '购买', '下单', '删除', '确认', '转账', '发布'];
24
+
25
+ export function isIrreversibleLabel(label: string): boolean {
26
+ const lower = label.toLowerCase();
27
+ if (IRREVERSIBLE_CJK.some((w) => lower.includes(w))) return true;
28
+ return lower.split(/[^\p{L}\p{N}]+/u).some((w) => w.length > 0 && IRREVERSIBLE.has(w));
29
+ }
30
+
31
+ /** The label a person would use for an element: its name, else its automation id. */
32
+ export function elementLabel(target: { name: string; automationId: string } | null | undefined): string {
33
+ if (!target) return '';
34
+ return target.name.trim() || target.automationId.trim();
35
+ }
36
+
37
+ /** The default approval gate for a recorded action. */
38
+ export function needsApproval(action: Action): boolean {
39
+ switch (action.kind) {
40
+ case 'click':
41
+ return isIrreversibleLabel(elementLabel(action.target));
42
+ case 'keys':
43
+ // Enter can submit the form it is pressed in, but which button fires is not in the
44
+ // recording — so it is gated only when the element itself is labelled irreversible.
45
+ return action.keys.toLowerCase() === 'enter' && isIrreversibleLabel(elementLabel(action.target));
46
+ case 'setValue':
47
+ return false;
48
+ }
49
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * experience/corpus.ts — experience as text Evermind can learn from.
3
+ *
4
+ * Evermind already learns from text through ONE recipe (`evermind/adapt.ts`
5
+ * `adaptAndDiff`: fit a private copy of a version, diff it). Rather than a second,
6
+ * trajectory-shaped training path, each demonstration is rendered as the procedure a
7
+ * person would write down — the task, then numbered steps — and handed to that recipe.
8
+ * Values that became parameters are written as `<name>` placeholders, so the model
9
+ * learns the procedure rather than one day's numbers. Secrets never appear: a secret
10
+ * field's value was never recorded.
11
+ */
12
+ import { elementLabel } from './approval.js';
13
+ import type { Action, Episode, Skill, SkillAction } from './types.js';
14
+
15
+ function where(a: Action | SkillAction): string {
16
+ const w = 'target' in a && a.target ? a.target.windowTitle : '';
17
+ return w ? ` in “${w}”` : '';
18
+ }
19
+
20
+ function stepSentence(a: Action | SkillAction): string {
21
+ switch (a.kind) {
22
+ case 'click':
23
+ return `Click “${elementLabel(a.target)}”${where(a)}.`;
24
+ case 'keys':
25
+ return a.target ? `Press ${a.keys} in “${elementLabel(a.target)}”${where(a)}.` : `Press ${a.keys}.`;
26
+ case 'setValue': {
27
+ const field = `“${elementLabel(a.target)}”`;
28
+ if (typeof a.value === 'string') {
29
+ return 'secret' in a && a.secret ? `Enter the secret for ${field}${where(a)}.` : `Set ${field} to “${a.value}”${where(a)}.`;
30
+ }
31
+ return a.value.from === 'param' ? `Set ${field} to <${a.value.name}>${where(a)}.` : `Set ${field} to “${a.value.text}”${where(a)}.`;
32
+ }
33
+ }
34
+ }
35
+
36
+ function programName(program: string): string {
37
+ return program.split(/[\\/]/).pop() ?? program;
38
+ }
39
+
40
+ /** A skill as a written procedure (parameters as placeholders, approvals marked). */
41
+ export function skillText(skill: Skill): string {
42
+ const lines = [`Task: ${skill.name} (${programName(skill.program)})`];
43
+ skill.steps.forEach((s, i) => lines.push(`${i + 1}. ${stepSentence(s.action)}${s.requiresApproval ? ' Ask before doing this.' : ''}`));
44
+ return lines.join('\n');
45
+ }
46
+
47
+ /** A raw demonstration as a written procedure. */
48
+ export function episodeText(episode: Episode): string {
49
+ const lines = [`Task: ${episode.name} (${programName(episode.program)})`];
50
+ episode.steps.forEach((s, i) => lines.push(`${i + 1}. ${stepSentence(s.action)}`));
51
+ return lines.join('\n');
52
+ }
53
+
54
+ /**
55
+ * The corpus to learn from: every skill as a procedure, plus demonstrations that have
56
+ * not been compiled into one yet (a compiled skill supersedes its raw episode — the
57
+ * reviewed version is the one worth learning).
58
+ */
59
+ export function experienceCorpus(episodes: Episode[], skills: Skill[]): string {
60
+ return experienceDocuments(episodes, skills)
61
+ .map((d) => d.text)
62
+ .join('\n\n');
63
+ }
64
+
65
+ /** One learnable procedure and the experience item it came from. */
66
+ export interface ExperienceDocument {
67
+ id: string;
68
+ text: string;
69
+ }
70
+
71
+ /** The corpus as separate procedures, each tagged with its source item's id. */
72
+ export function experienceDocuments(episodes: Episode[], skills: Skill[]): ExperienceDocument[] {
73
+ const compiled = new Set(skills.map((s) => s.sourceEpisode));
74
+ return [
75
+ ...skills.map((s) => ({ id: s.id, text: skillText(s) })),
76
+ ...episodes.filter((e) => !compiled.has(e.id)).map((e) => ({ id: e.id, text: episodeText(e) })),
77
+ ];
78
+ }
79
+
80
+ /**
81
+ * Pack procedures into passes of at most `maxChars` (one adaptation pass reads that
82
+ * much), never splitting a procedure — one longer than a pass gets a pass of its own.
83
+ */
84
+ export function packDocuments(docs: ExperienceDocument[], maxChars: number): ExperienceDocument[][] {
85
+ const passes: ExperienceDocument[][] = [];
86
+ let cur: ExperienceDocument[] = [];
87
+ let len = 0;
88
+ for (const d of docs) {
89
+ const add = cur.length ? d.text.length + 2 : d.text.length;
90
+ if (cur.length && len + add > maxChars) {
91
+ passes.push(cur);
92
+ cur = [];
93
+ len = 0;
94
+ cur.push(d);
95
+ len = d.text.length;
96
+ continue;
97
+ }
98
+ cur.push(d);
99
+ len += add;
100
+ }
101
+ if (cur.length) passes.push(cur);
102
+ return passes;
103
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Experience — the learned items that are not facts or weights: demonstrations
3
+ * (episodes), the skills Train Once compiles from them, and the runs of those skills.
4
+ * Pure and storage-agnostic, like the rest of the engine.
5
+ */
6
+ export type {
7
+ ElementRef,
8
+ Action,
9
+ RecordedStep,
10
+ Episode,
11
+ EpisodeSummary,
12
+ SkillParam,
13
+ SkillValue,
14
+ SkillAction,
15
+ SkillStep,
16
+ Schedule,
17
+ SkillRoutine,
18
+ Skill,
19
+ RunStatus,
20
+ StepOutcome,
21
+ RunStepLog,
22
+ Run,
23
+ Adaptation,
24
+ } from './types.js';
25
+ export { newExperienceId } from './types.js';
26
+ export { isIrreversibleLabel, elementLabel, needsApproval } from './approval.js';
27
+ export { trainOnce, slug } from './trainOnce.js';
28
+ export type { ReviewEdits } from './trainOnce.js';
29
+ export { isDue, dueSkills } from './schedule.js';
30
+ export { experienceCorpus, experienceDocuments, packDocuments } from './corpus.js';
31
+ export type { ExperienceDocument } from './corpus.js';
32
+ export { experienceOverview } from './overview.js';
33
+ export type { ExperienceOverview, ExperienceRegion, ExperienceEvent, ExperienceDay, OverviewOptions } from './overview.js';
34
+ export { EXPERIENCE_SCHEMA, MAX_RUNS, MAX_ADAPTATIONS, emptySnapshot, parseSnapshot, InMemoryExperienceStore } from './store.js';
35
+ export type { ExperienceSnapshot, ExperienceStore } from './store.js';
@@ -0,0 +1,143 @@
1
+ /**
2
+ * experience/overview.ts — what experience says about the brain, for anything that draws
3
+ * it. Pure: the host passes the snapshot, which adaptation its model is at, the clock
4
+ * and the local UTC offset; every number comes from the snapshot, none is estimated.
5
+ *
6
+ * The regions are the parts of the brain experience lives in:
7
+ * - **hippocampus** — episodic memory: the demonstrations;
8
+ * - **basalGanglia** — action selection: the skills, and how their runs went;
9
+ * - **amygdala** — salience: the decisions made at irreversible steps;
10
+ * - **hypothalamus** — drives: the routines that start skills by themselves;
11
+ * - **neocortex** — the private model's weights: the procedures adapted into it.
12
+ * Facts (semantic memory) and attention belong to other stores; a host adds those.
13
+ */
14
+ import { experienceDocuments } from './corpus.js';
15
+ import type { ExperienceSnapshot } from './store.js';
16
+ import type { Adaptation, RunStatus } from './types.js';
17
+
18
+ export type ExperienceRegion = 'hippocampus' | 'basalGanglia' | 'amygdala' | 'hypothalamus' | 'neocortex';
19
+
20
+ /** One thing that happened, placed in the region it landed in. */
21
+ export interface ExperienceEvent {
22
+ region: ExperienceRegion;
23
+ kind: 'demonstration' | 'skill' | 'run' | 'approval' | 'adaptation';
24
+ /** The episode, skill, run or model version it concerns. */
25
+ id: string;
26
+ name: string;
27
+ at: number;
28
+ /** Run status, approval outcome, or the adaptation's loss — what happened. */
29
+ detail?: string;
30
+ }
31
+
32
+ export interface ExperienceDay {
33
+ /** Local calendar day, `YYYY-MM-DD`. */
34
+ day: string;
35
+ demonstrations: number;
36
+ skills: number;
37
+ runs: number;
38
+ learned: number;
39
+ }
40
+
41
+ export interface ExperienceOverview {
42
+ regions: Record<ExperienceRegion, number>;
43
+ /** Procedures the model at `modelIndex` has learned, and those it has not yet. */
44
+ learned: number;
45
+ pending: number;
46
+ runs: Record<RunStatus, number>;
47
+ approvals: { approved: number; denied: number };
48
+ /** Oldest first — the order a chart reads. */
49
+ adaptations: Adaptation[];
50
+ /** One entry per day of the window, oldest first, empty days included. */
51
+ days: ExperienceDay[];
52
+ /** Newest first. */
53
+ recent: ExperienceEvent[];
54
+ }
55
+
56
+ const DAY_MS = 86_400_000;
57
+
58
+ export interface OverviewOptions {
59
+ /** N of the loaded model's `+expN` version (0 for a base model or none). */
60
+ modelIndex: number;
61
+ now: number;
62
+ /** Local minus UTC, in minutes. */
63
+ utcOffsetMinutes: number;
64
+ /** Days of activity to chart. Default 30. */
65
+ days?: number;
66
+ /** Events to list. Default 40. */
67
+ recent?: number;
68
+ }
69
+
70
+ function localDay(at: number, offsetMinutes: number): string {
71
+ return new Date(at + offsetMinutes * 60_000).toISOString().slice(0, 10);
72
+ }
73
+
74
+ export function experienceOverview(snap: ExperienceSnapshot, opts: OverviewOptions): ExperienceOverview {
75
+ const known = (id: string) => {
76
+ const n = snap.learned[id];
77
+ return typeof n === 'number' && n >= 1 && n <= opts.modelIndex;
78
+ };
79
+ const docs = experienceDocuments(snap.episodes, snap.skills);
80
+ const learned = docs.filter((d) => known(d.id)).length;
81
+
82
+ const runs: Record<RunStatus, number> = { running: 0, succeeded: 0, failed: 0, stopped: 0, denied: 0 };
83
+ const approvals = { approved: 0, denied: 0 };
84
+ const events: ExperienceEvent[] = [];
85
+
86
+ for (const e of snap.episodes) {
87
+ events.push({ region: 'hippocampus', kind: 'demonstration', id: e.id, name: e.name, at: e.startedAt });
88
+ }
89
+ for (const s of snap.skills) {
90
+ events.push({ region: 'basalGanglia', kind: 'skill', id: s.id, name: s.name, at: s.createdAt });
91
+ }
92
+ for (const r of snap.runs) {
93
+ runs[r.status] = (runs[r.status] ?? 0) + 1;
94
+ events.push({ region: 'basalGanglia', kind: 'run', id: r.id, name: r.skillName, at: r.startedAt, detail: r.status });
95
+ for (const step of r.steps) {
96
+ if (step.outcome !== 'approved' && step.outcome !== 'denied') continue;
97
+ approvals[step.outcome]++;
98
+ events.push({ region: 'amygdala', kind: 'approval', id: r.id, name: r.skillName, at: step.at, detail: step.outcome });
99
+ }
100
+ }
101
+ // Only adaptations the loaded model descends from: a rolled-back model's discarded
102
+ // versions are not what it knows.
103
+ const adaptations = snap.adaptations.filter((a) => a.index <= opts.modelIndex).reverse();
104
+ for (const a of adaptations) {
105
+ events.push({ region: 'neocortex', kind: 'adaptation', id: a.version, name: a.version, at: a.at, detail: a.loss.toFixed(3) });
106
+ }
107
+
108
+ const windowDays = Math.max(1, opts.days ?? 30);
109
+ const days: ExperienceDay[] = [];
110
+ const index = new Map<string, ExperienceDay>();
111
+ for (let i = windowDays - 1; i >= 0; i--) {
112
+ const day = localDay(opts.now - i * DAY_MS, opts.utcOffsetMinutes);
113
+ if (index.has(day)) continue;
114
+ const entry = { day, demonstrations: 0, skills: 0, runs: 0, learned: 0 };
115
+ index.set(day, entry);
116
+ days.push(entry);
117
+ }
118
+ const bump = (at: number, field: Exclude<keyof ExperienceDay, 'day'>, by = 1) => {
119
+ const d = index.get(localDay(at, opts.utcOffsetMinutes));
120
+ if (d) d[field] += by;
121
+ };
122
+ for (const e of snap.episodes) bump(e.startedAt, 'demonstrations');
123
+ for (const s of snap.skills) bump(s.createdAt, 'skills');
124
+ for (const r of snap.runs) bump(r.startedAt, 'runs');
125
+ for (const a of adaptations) bump(a.at, 'learned', a.learned.length);
126
+
127
+ return {
128
+ regions: {
129
+ hippocampus: snap.episodes.length,
130
+ basalGanglia: snap.skills.length,
131
+ amygdala: approvals.approved + approvals.denied,
132
+ hypothalamus: snap.skills.filter((s) => s.routine?.enabled).length,
133
+ neocortex: learned,
134
+ },
135
+ learned,
136
+ pending: docs.length - learned,
137
+ runs,
138
+ approvals,
139
+ adaptations,
140
+ days,
141
+ recent: events.sort((a, b) => b.at - a.at).slice(0, Math.max(0, opts.recent ?? 40)),
142
+ };
143
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * experience/schedule.ts — when a skill's routine is due. Pure: the host supplies the
3
+ * clock and the local UTC offset, and ticks as often as it likes.
4
+ */
5
+ import type { Schedule, Skill } from './types.js';
6
+
7
+ const MINUTE = 60_000;
8
+ const DAY = 86_400_000;
9
+
10
+ /** `utcOffsetMinutes` is local minus UTC (e.g. -240 for UTC-4). */
11
+ export function isDue(schedule: Schedule, lastRunAt: number | null | undefined, now: number, utcOffsetMinutes: number): boolean {
12
+ if (schedule.every === 'minutes') {
13
+ const every = Math.max(1, schedule.minutes) * MINUTE;
14
+ return lastRunAt == null || now - lastRunAt >= every;
15
+ }
16
+ const offset = utcOffsetMinutes * MINUTE;
17
+ const localNow = now + offset;
18
+ const dayStart = localNow - (((localNow % DAY) + DAY) % DAY);
19
+ const target = dayStart + Math.min(23, Math.max(0, schedule.hour)) * 3_600_000 + Math.min(59, Math.max(0, schedule.minute)) * MINUTE;
20
+ // Due once the time of day has passed, unless it already ran since then.
21
+ return localNow >= target && (lastRunAt == null || lastRunAt + offset < target);
22
+ }
23
+
24
+ /** Skills whose enabled routine is due now, oldest-run first. */
25
+ export function dueSkills(skills: Skill[], now: number, utcOffsetMinutes: number): Skill[] {
26
+ return skills
27
+ .filter((s) => s.routine?.enabled && isDue(s.routine.schedule, s.routine.lastRunAt, now, utcOffsetMinutes))
28
+ .sort((a, b) => (a.routine?.lastRunAt ?? 0) - (b.routine?.lastRunAt ?? 0));
29
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * experience/store.ts — the persistence port for experience, and its snapshot format.
3
+ *
4
+ * The engine does no I/O (checkpoints are `ArrayBuffer`s the host stores); experience
5
+ * follows the same rule. `ExperienceStore` is the port every host implements over its own
6
+ * storage — a JSON file on disk (memory-mcp), IndexedDB, a KV. `InMemoryExperienceStore`
7
+ * is the reference implementation, and `ExperienceSnapshot` is the one serialized shape,
8
+ * so a store on one runtime can be read by another.
9
+ */
10
+ import type { Adaptation, Episode, EpisodeSummary, Run, RunStepLog, Skill } from './types.js';
11
+
12
+ export const EXPERIENCE_SCHEMA = 'evermind.experience/1';
13
+ /** Runs kept, newest first — the audit trail is recent history, not an archive. */
14
+ export const MAX_RUNS = 500;
15
+ /** Adaptations kept, newest first — enough history to chart how learning went. */
16
+ export const MAX_ADAPTATIONS = 200;
17
+
18
+ export interface ExperienceSnapshot {
19
+ schema: typeof EXPERIENCE_SCHEMA;
20
+ episodes: Episode[];
21
+ skills: Skill[];
22
+ runs: Run[];
23
+ /**
24
+ * Which episodes and skills the local Evermind has been adapted on, and by which
25
+ * adaptation (the N of the model's `+expN` version) — so training reads each
26
+ * procedure once, and a model rolled back to an earlier N re-learns what the
27
+ * discarded versions had learned.
28
+ */
29
+ learned: Record<string, number>;
30
+ /** Every adaptation's measurements, newest first (capped at {@link MAX_ADAPTATIONS}). */
31
+ adaptations: Adaptation[];
32
+ }
33
+
34
+ export interface ExperienceStore {
35
+ putEpisode(episode: Episode): Promise<void>;
36
+ getEpisode(id: string): Promise<Episode | undefined>;
37
+ listEpisodes(): Promise<EpisodeSummary[]>;
38
+ /** Forget a demonstration. Skills compiled from it keep working — they carry their own steps. */
39
+ deleteEpisode(id: string): Promise<boolean>;
40
+
41
+ putSkill(skill: Skill): Promise<void>;
42
+ getSkill(id: string): Promise<Skill | undefined>;
43
+ listSkills(): Promise<Skill[]>;
44
+ deleteSkill(id: string): Promise<boolean>;
45
+
46
+ putRun(run: Run): Promise<void>;
47
+ /** Append one audit line to a run; false when the run is unknown. */
48
+ appendRunStep(runId: string, step: RunStepLog): Promise<boolean>;
49
+ getRun(id: string): Promise<Run | undefined>;
50
+ listRuns(limit: number): Promise<Run[]>;
51
+
52
+ /**
53
+ * Record one adaptation: its items join the learned ledger under its index (the
54
+ * model's `+expN`) and its measurements join the history — one write, so the ledger
55
+ * and the chart can never disagree.
56
+ */
57
+ recordAdaptation(adaptation: Adaptation): Promise<void>;
58
+
59
+ snapshot(): Promise<ExperienceSnapshot>;
60
+ /** Delete every episode, skill and run. */
61
+ clear(): Promise<void>;
62
+ }
63
+
64
+ export function emptySnapshot(): ExperienceSnapshot {
65
+ return { schema: EXPERIENCE_SCHEMA, episodes: [], skills: [], runs: [], learned: {}, adaptations: [] };
66
+ }
67
+
68
+ /** Parse a snapshot, tolerating a missing or foreign file as empty. */
69
+ export function parseSnapshot(raw: unknown): ExperienceSnapshot {
70
+ const s = raw as Partial<ExperienceSnapshot> | null;
71
+ if (!s || s.schema !== EXPERIENCE_SCHEMA) return emptySnapshot();
72
+ return {
73
+ schema: EXPERIENCE_SCHEMA,
74
+ episodes: Array.isArray(s.episodes) ? s.episodes : [],
75
+ skills: Array.isArray(s.skills) ? s.skills : [],
76
+ runs: Array.isArray(s.runs) ? s.runs : [],
77
+ learned: s.learned && typeof s.learned === 'object' && !Array.isArray(s.learned) ? s.learned : {},
78
+ // Optional within schema 1: snapshots written before the history existed have none.
79
+ adaptations: Array.isArray(s.adaptations) ? s.adaptations : [],
80
+ };
81
+ }
82
+
83
+ function summary(e: Episode): EpisodeSummary {
84
+ return { id: e.id, name: e.name, program: e.program, startedAt: e.startedAt, steps: e.steps.length };
85
+ }
86
+
87
+ /**
88
+ * The reference store: the snapshot held in memory. Hosts that persist wrap it and
89
+ * write `snapshot()` after each change (see `onChange`).
90
+ */
91
+ export class InMemoryExperienceStore implements ExperienceStore {
92
+ private data: ExperienceSnapshot;
93
+
94
+ constructor(initial?: ExperienceSnapshot, private readonly onChange?: (s: ExperienceSnapshot) => void | Promise<void>) {
95
+ this.data = initial ? parseSnapshot(initial) : emptySnapshot();
96
+ }
97
+
98
+ /** Replace the contents (a host re-reading its file after another process wrote it). */
99
+ load(snapshot: ExperienceSnapshot): void {
100
+ this.data = parseSnapshot(snapshot);
101
+ }
102
+
103
+ private async changed(): Promise<void> {
104
+ await this.onChange?.(this.data);
105
+ }
106
+
107
+ private static upsert<T extends { id: string }>(list: T[], item: T): void {
108
+ const i = list.findIndex((x) => x.id === item.id);
109
+ if (i >= 0) list[i] = item;
110
+ else list.push(item);
111
+ }
112
+
113
+ private static remove<T extends { id: string }>(list: T[], id: string): boolean {
114
+ const i = list.findIndex((x) => x.id === id);
115
+ if (i < 0) return false;
116
+ list.splice(i, 1);
117
+ return true;
118
+ }
119
+
120
+ async putEpisode(episode: Episode): Promise<void> {
121
+ InMemoryExperienceStore.upsert(this.data.episodes, episode);
122
+ await this.changed();
123
+ }
124
+ async getEpisode(id: string): Promise<Episode | undefined> {
125
+ return this.data.episodes.find((e) => e.id === id);
126
+ }
127
+ async listEpisodes(): Promise<EpisodeSummary[]> {
128
+ return [...this.data.episodes].sort((a, b) => b.startedAt - a.startedAt).map(summary);
129
+ }
130
+ async deleteEpisode(id: string): Promise<boolean> {
131
+ const ok = InMemoryExperienceStore.remove(this.data.episodes, id);
132
+ delete this.data.learned[id];
133
+ if (ok) await this.changed();
134
+ return ok;
135
+ }
136
+
137
+ async putSkill(skill: Skill): Promise<void> {
138
+ InMemoryExperienceStore.upsert(this.data.skills, skill);
139
+ await this.changed();
140
+ }
141
+ async getSkill(id: string): Promise<Skill | undefined> {
142
+ return this.data.skills.find((s) => s.id === id);
143
+ }
144
+ async listSkills(): Promise<Skill[]> {
145
+ return [...this.data.skills].sort((a, b) => b.createdAt - a.createdAt);
146
+ }
147
+ async deleteSkill(id: string): Promise<boolean> {
148
+ const ok = InMemoryExperienceStore.remove(this.data.skills, id);
149
+ delete this.data.learned[id];
150
+ if (ok) await this.changed();
151
+ return ok;
152
+ }
153
+
154
+ async putRun(run: Run): Promise<void> {
155
+ InMemoryExperienceStore.upsert(this.data.runs, run);
156
+ this.data.runs.sort((a, b) => b.startedAt - a.startedAt);
157
+ this.data.runs.length = Math.min(this.data.runs.length, MAX_RUNS);
158
+ await this.changed();
159
+ }
160
+ async appendRunStep(runId: string, step: RunStepLog): Promise<boolean> {
161
+ const run = this.data.runs.find((r) => r.id === runId);
162
+ if (!run) return false;
163
+ run.steps.push(step);
164
+ await this.changed();
165
+ return true;
166
+ }
167
+ async getRun(id: string): Promise<Run | undefined> {
168
+ return this.data.runs.find((r) => r.id === id);
169
+ }
170
+ async listRuns(limit: number): Promise<Run[]> {
171
+ return this.data.runs.slice(0, Math.max(0, limit));
172
+ }
173
+
174
+ async recordAdaptation(adaptation: Adaptation): Promise<void> {
175
+ for (const id of adaptation.learned) this.data.learned[id] = adaptation.index;
176
+ this.data.adaptations.unshift(adaptation);
177
+ this.data.adaptations.length = Math.min(this.data.adaptations.length, MAX_ADAPTATIONS);
178
+ await this.changed();
179
+ }
180
+
181
+ async snapshot(): Promise<ExperienceSnapshot> {
182
+ return structuredClone(this.data);
183
+ }
184
+ async clear(): Promise<void> {
185
+ this.data = emptySnapshot();
186
+ await this.changed();
187
+ }
188
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * experience/trainOnce.ts — Train Once: one reviewed demonstration becomes a skill that
3
+ * runs again with new values.
4
+ *
5
+ * Deterministic, local, no model call. Every value the person typed becomes a named
6
+ * parameter (named after the field it went into), every secret field a secret parameter,
7
+ * and every irreversible click an approval gate. The review edits decide what stays.
8
+ */
9
+ import { elementLabel, needsApproval } from './approval.js';
10
+ import { newExperienceId, type Episode, type Skill, type SkillAction, type SkillParam, type SkillStep } from './types.js';
11
+
12
+ /** What the person changed in review, keyed by recorded step id. */
13
+ export interface ReviewEdits {
14
+ /** The skill's name (defaults to the episode's). */
15
+ name?: string | null;
16
+ /** Steps left out of the skill. */
17
+ removed?: string[];
18
+ /** Typed values kept exactly as recorded instead of asked for each run. */
19
+ fixed?: string[];
20
+ /** Approval gate per step, where the person overrode the default. */
21
+ approvals?: Record<string, boolean>;
22
+ /** Parameter names the person chose, per step. */
23
+ paramNames?: Record<string, string>;
24
+ }
25
+
26
+ export function trainOnce(episode: Episode, edits: ReviewEdits = {}, now: number = Date.now()): Skill {
27
+ const removed = new Set(edits.removed ?? []);
28
+ const fixed = new Set(edits.fixed ?? []);
29
+ const params: SkillParam[] = [];
30
+ const steps: SkillStep[] = [];
31
+ for (const rec of episode.steps) {
32
+ if (removed.has(rec.id)) continue;
33
+ const a = rec.action;
34
+ let action: SkillAction;
35
+ if (a.kind === 'setValue') {
36
+ if (!a.secret && fixed.has(rec.id)) {
37
+ action = { kind: 'setValue', target: a.target, value: { from: 'literal', text: a.value } };
38
+ } else {
39
+ const chosen = slug(edits.paramNames?.[rec.id] ?? '');
40
+ const base = chosen || slug(elementLabel(a.target)) || 'value';
41
+ const name = uniqueName(params, base);
42
+ params.push({ name, label: elementLabel(a.target), default: a.secret ? null : a.value, secret: !!a.secret });
43
+ action = { kind: 'setValue', target: a.target, value: { from: 'param', name } };
44
+ }
45
+ } else {
46
+ action = a;
47
+ }
48
+ steps.push({
49
+ id: rec.id,
50
+ action,
51
+ requiresApproval: edits.approvals?.[rec.id] ?? needsApproval(a),
52
+ screenshot: rec.screenshot ?? null,
53
+ });
54
+ }
55
+ return {
56
+ id: newExperienceId(),
57
+ name: edits.name?.trim() || episode.name,
58
+ program: episode.program,
59
+ args: [...episode.args],
60
+ params,
61
+ steps,
62
+ sourceEpisode: episode.id,
63
+ createdAt: now,
64
+ routine: null,
65
+ };
66
+ }
67
+
68
+ /** `Invoice Amount (USD)` → `invoice_amount_usd`. Letters in any script survive. */
69
+ export function slug(label: string): string {
70
+ return label
71
+ .toLowerCase()
72
+ .split(/[^\p{L}\p{N}]+/u)
73
+ .filter(Boolean)
74
+ .join('_')
75
+ .slice(0, 40)
76
+ .replace(/_+$/, '');
77
+ }
78
+
79
+ function uniqueName(params: SkillParam[], base: string): string {
80
+ if (!params.some((p) => p.name === base)) return base;
81
+ for (let i = 2; ; i++) {
82
+ const n = `${base}_${i}`;
83
+ if (!params.some((p) => p.name === n)) return n;
84
+ }
85
+ }