@seanhogg/builderforce-memory-mcp 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.
- package/README.md +25 -0
- package/dist/backends/memory-store.d.ts.map +1 -1
- package/dist/backends/memory-store.js +15 -68
- package/dist/backends/memory-store.js.map +1 -1
- package/dist/bin/http.d.ts +1 -0
- package/dist/bin/http.d.ts.map +1 -1
- package/dist/bin/http.js +9 -0
- package/dist/bin/http.js.map +1 -1
- package/dist/bin/stdio.d.ts +4 -0
- package/dist/bin/stdio.d.ts.map +1 -1
- package/dist/bin/stdio.js +7 -0
- package/dist/bin/stdio.js.map +1 -1
- package/dist/dynamic-import.d.ts +10 -0
- package/dist/dynamic-import.d.ts.map +1 -0
- package/dist/dynamic-import.js +13 -0
- package/dist/dynamic-import.js.map +1 -0
- package/dist/embedding/evermind-embedder.d.ts +0 -1
- package/dist/embedding/evermind-embedder.d.ts.map +1 -1
- package/dist/embedding/evermind-embedder.js +8 -44
- package/dist/embedding/evermind-embedder.js.map +1 -1
- package/dist/experience/file-store.d.ts +52 -0
- package/dist/experience/file-store.d.ts.map +1 -0
- package/dist/experience/file-store.js +119 -0
- package/dist/experience/file-store.js.map +1 -0
- package/dist/experience/host.d.ts +46 -0
- package/dist/experience/host.d.ts.map +1 -0
- package/dist/experience/host.js +57 -0
- package/dist/experience/host.js.map +1 -0
- package/dist/experience/tools.d.ts +16 -0
- package/dist/experience/tools.d.ts.map +1 -0
- package/dist/experience/tools.js +279 -0
- package/dist/experience/tools.js.map +1 -0
- package/dist/experience/train.d.ts +48 -0
- package/dist/experience/train.d.ts.map +1 -0
- package/dist/experience/train.js +72 -0
- package/dist/experience/train.js.map +1 -0
- package/dist/gateway-tools.d.ts +8 -0
- package/dist/gateway-tools.d.ts.map +1 -0
- package/dist/gateway-tools.js +71 -0
- package/dist/gateway-tools.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/install/install.d.ts.map +1 -1
- package/dist/install/install.js +2 -2
- package/dist/install/install.js.map +1 -1
- package/dist/model/evermind-package.d.ts +55 -0
- package/dist/model/evermind-package.d.ts.map +1 -0
- package/dist/model/evermind-package.js +86 -0
- package/dist/model/evermind-package.js.map +1 -0
- package/dist/persistence/shared-json-file.d.ts +65 -0
- package/dist/persistence/shared-json-file.d.ts.map +1 -0
- package/dist/persistence/shared-json-file.js +110 -0
- package/dist/persistence/shared-json-file.js.map +1 -0
- package/dist/tool-core.d.ts +28 -0
- package/dist/tool-core.d.ts.map +1 -0
- package/dist/tool-core.js +19 -0
- package/dist/tool-core.js.map +1 -0
- package/dist/tools.d.ts +9 -17
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -73
- package/dist/tools.js.map +1 -1
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +8 -1
- package/dist/transports/http.js.map +1 -1
- package/dist/transports/sdk.d.ts.map +1 -1
- package/dist/transports/sdk.js +2 -4
- package/dist/transports/sdk.js.map +1 -1
- package/package.json +6 -6
- package/server.json +3 -3
- package/src/backends/memory-store.ts +16 -85
- package/src/bin/http.ts +12 -0
- package/src/bin/stdio.ts +7 -0
- package/src/dynamic-import.ts +12 -0
- package/src/embedding/evermind-embedder.ts +11 -61
- package/src/experience/file-store.ts +143 -0
- package/src/experience/host.ts +113 -0
- package/src/experience/tools.ts +363 -0
- package/src/experience/train.ts +96 -0
- package/src/gateway-tools.ts +97 -0
- package/src/index.ts +8 -1
- package/src/install/install.ts +2 -2
- package/src/model/evermind-package.ts +127 -0
- package/src/persistence/shared-json-file.ts +131 -0
- package/src/tool-core.ts +39 -0
- package/src/tools.ts +13 -116
- package/src/transports/http.ts +8 -1
- package/src/transports/sdk.ts +2 -5
package/src/bin/stdio.ts
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
* BUILDERFORCE_GATEWAY_URL Gateway base URL (default https://api.builderforce.ai).
|
|
24
24
|
* BUILDERFORCE_API_KEY `bfk_*` tenant key. When set, exposes the cost tools
|
|
25
25
|
* (token_usage, model_efficiency).
|
|
26
|
+
* BUILDERFORCE_MEMORY_EXPERIENCE '1' to expose the experience tools (episodes, Train
|
|
27
|
+
* Once skills, runs, experience_train). Kept beside the
|
|
28
|
+
* snapshot in experience.json + episodes/; trains the
|
|
29
|
+
* BUILDERFORCE_MEMORY_MODEL package.
|
|
26
30
|
*
|
|
27
31
|
* With no model configured recall ranks lexically (BM25) and the tool result SAYS
|
|
28
32
|
* so ("Ranked by: lexical recall (fallback …)"), so a degrade is never silent.
|
|
@@ -30,6 +34,7 @@
|
|
|
30
34
|
|
|
31
35
|
import { createLocalMemoryStoreBackend } from "../backends/memory-store.js";
|
|
32
36
|
import { createRecallEmbedder } from "../embedding/index.js";
|
|
37
|
+
import { createExperienceHost } from "../experience/host.js";
|
|
33
38
|
import { resolveMemoryFile } from "../install/server-spec.js";
|
|
34
39
|
import { runStdio } from "../transports/stdio.js";
|
|
35
40
|
|
|
@@ -49,4 +54,6 @@ await runStdio(backend, {
|
|
|
49
54
|
// exposed when an API key is present; URL defaults to the public gateway.
|
|
50
55
|
gatewayUrl: process.env["BUILDERFORCE_GATEWAY_URL"] ?? "https://api.builderforce.ai",
|
|
51
56
|
gatewayApiKey: process.env["BUILDERFORCE_API_KEY"],
|
|
57
|
+
// Null unless BUILDERFORCE_MEMORY_EXPERIENCE=1 — Synapse's opt-in.
|
|
58
|
+
experience: await createExperienceHost({ memoryFile }),
|
|
52
59
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a module by name at run time without the bundler or tsc resolving it.
|
|
3
|
+
*
|
|
4
|
+
* The optional peers (the memory runtime, the engine, fake-indexeddb, the Claude
|
|
5
|
+
* Agent SDK) and Node builtins are reached through this, so a consumer that never
|
|
6
|
+
* takes those paths — a browser bundle, a custom backend, the HTTP thin client —
|
|
7
|
+
* never has to install or ship them.
|
|
8
|
+
*/
|
|
9
|
+
export function dynamicImport(m: string): Promise<unknown> {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
11
|
+
return new Function("m", "return import(m)")(m) as Promise<unknown>;
|
|
12
|
+
}
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* absent file (drop it and recompute — a cache is never the source of truth).
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { dynamicImport } from "../dynamic-import.js";
|
|
25
|
+
import { loadEvermindPackage } from "../model/evermind-package.js";
|
|
26
|
+
|
|
24
27
|
/** The seam `MemoryStore.recallRanked(query, k, runtime)` consumes. */
|
|
25
28
|
export interface TextEmbedderLike {
|
|
26
29
|
embed(text: string): Promise<Float32Array>;
|
|
@@ -66,25 +69,8 @@ export interface EvermindEmbedderOptions {
|
|
|
66
69
|
flushDelayMs?: number;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
/** Minimal structural view of the optional engine package. */
|
|
70
|
-
interface EmbedEngine {
|
|
71
|
-
EvermindModelPackage: {
|
|
72
|
-
fromBlob(blob: ArrayBuffer): {
|
|
73
|
-
manifest: { modelType?: string; tokenizerFormat?: string };
|
|
74
|
-
loadLM(): unknown;
|
|
75
|
-
/** Present on engines that support the embedded-tokenizer section. */
|
|
76
|
-
loadTokenizer?(): { encode(t: string): number[] } | null;
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
BPETokenizer: {
|
|
80
|
-
new (): { loadFromSpec(spec: unknown): void; loadHuggingFace(spec: unknown): void; encode(t: string): number[] };
|
|
81
|
-
};
|
|
82
|
-
EvermindTextEmbedder: new (model: unknown, codec: unknown) => TextEmbedderLike;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
72
|
type FsLike = {
|
|
86
73
|
readFileSync(path: string, enc: "utf8"): string;
|
|
87
|
-
readFileSync(path: string): Uint8Array;
|
|
88
74
|
writeFileSync(path: string, data: string): void;
|
|
89
75
|
existsSync(path: string): boolean;
|
|
90
76
|
};
|
|
@@ -100,24 +86,6 @@ interface VectorCacheFile {
|
|
|
100
86
|
vectors: Array<[string, number[]]>;
|
|
101
87
|
}
|
|
102
88
|
|
|
103
|
-
/** Indirect import so the optional engine peer is never statically resolved. */
|
|
104
|
-
function dynamicImport(m: string): Promise<unknown> {
|
|
105
|
-
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
106
|
-
return new Function("m", "return import(m)")(m) as Promise<unknown>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Load the tokenizer spec from `file`, accepting either `BPETokenizer.toObject()`
|
|
111
|
-
* output or a Hugging Face `tokenizer.json`.
|
|
112
|
-
*/
|
|
113
|
-
function loadCodec(engine: EmbedEngine, fs: FsLike, file: string): { encode(t: string): number[] } {
|
|
114
|
-
const spec = JSON.parse(fs.readFileSync(file, "utf8")) as { vocab?: unknown; merges?: unknown; model?: unknown };
|
|
115
|
-
const tok = new engine.BPETokenizer();
|
|
116
|
-
if (spec.vocab && spec.merges) tok.loadFromSpec(spec);
|
|
117
|
-
else tok.loadHuggingFace(spec);
|
|
118
|
-
return tok;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
89
|
/**
|
|
122
90
|
* Build a CPU text embedder from a `.evermind` package + its tokenizer, wrapped in
|
|
123
91
|
* a persistent vector cache.
|
|
@@ -132,32 +100,14 @@ export async function createEvermindEmbedder(
|
|
|
132
100
|
opts: EvermindEmbedderOptions,
|
|
133
101
|
): Promise<PersistentTextEmbedder | null> {
|
|
134
102
|
try {
|
|
135
|
-
const
|
|
136
|
-
if (!
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
const pkg = (engine as EmbedEngine).EvermindModelPackage.fromBlob(blob);
|
|
144
|
-
if (pkg.manifest.modelType !== "evermind-lm") return null;
|
|
145
|
-
|
|
146
|
-
// Prefer the package's OWN tokenizer. It is checksummed and cross-checked
|
|
147
|
-
// against the checkpoint's vocab size at package time, so it cannot be the
|
|
148
|
-
// wrong vocabulary — which a separate file silently can be. Falling back to a
|
|
149
|
-
// file keeps every package written before the section existed working.
|
|
150
|
-
const embedded = pkg.loadTokenizer?.() ?? null;
|
|
151
|
-
let codec: { encode(t: string): number[] };
|
|
152
|
-
if (embedded) {
|
|
153
|
-
codec = embedded;
|
|
154
|
-
} else {
|
|
155
|
-
const tokenizerFile = opts.tokenizerFile ?? `${opts.modelFile}.tokenizer.json`;
|
|
156
|
-
if (!fs.existsSync(tokenizerFile)) return null;
|
|
157
|
-
codec = loadCodec(engine as EmbedEngine, fs, tokenizerFile);
|
|
158
|
-
}
|
|
159
|
-
const inner = new (engine as EmbedEngine).EvermindTextEmbedder(pkg.loadLM(), codec);
|
|
160
|
-
return new CachedTextEmbedder(inner, fs, opts);
|
|
103
|
+
const loaded = await loadEvermindPackage(opts.modelFile, opts.tokenizerFile);
|
|
104
|
+
if (!loaded) return null;
|
|
105
|
+
const engine = (await dynamicImport("@seanhogg/builderforce-memory-engine")) as {
|
|
106
|
+
EvermindTextEmbedder?: new (model: unknown, codec: unknown) => TextEmbedderLike;
|
|
107
|
+
};
|
|
108
|
+
if (!engine?.EvermindTextEmbedder) return null;
|
|
109
|
+
const inner = new engine.EvermindTextEmbedder(loaded.pkg.loadLM(), loaded.codec);
|
|
110
|
+
return new CachedTextEmbedder(inner, loaded.fs, opts);
|
|
161
111
|
} catch {
|
|
162
112
|
// Any failure to stand the model up is a degrade, not a fault.
|
|
163
113
|
return null;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine's experience store, made durable on disk.
|
|
3
|
+
*
|
|
4
|
+
* The engine owns experience — the types, Train Once, the snapshot format and the
|
|
5
|
+
* reference `InMemoryExperienceStore`. This host adds only what a pure package
|
|
6
|
+
* cannot: the snapshot lives in `experience.json` beside `memory.json`, shared by
|
|
7
|
+
* every process on the machine the same way memories are (one {@link SharedJsonFile}
|
|
8
|
+
* implementation, so the freshness check and the loop guard are the memory store's),
|
|
9
|
+
* and each episode's screenshots live in `episodes/<id>/`, removed when it is forgotten.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
Adaptation,
|
|
14
|
+
Episode,
|
|
15
|
+
EpisodeSummary,
|
|
16
|
+
ExperienceSnapshot,
|
|
17
|
+
ExperienceStore,
|
|
18
|
+
InMemoryExperienceStore,
|
|
19
|
+
Run,
|
|
20
|
+
RunStepLog,
|
|
21
|
+
Skill,
|
|
22
|
+
} from "@seanhogg/builderforce-memory-engine";
|
|
23
|
+
import type { SharedJsonFile } from "../persistence/shared-json-file.js";
|
|
24
|
+
|
|
25
|
+
/** The filesystem calls the store makes besides the snapshot's own. */
|
|
26
|
+
export interface EpisodeFolders {
|
|
27
|
+
/**
|
|
28
|
+
* Delete one episode's screenshot folder, if it exists. Only ever called with a safe
|
|
29
|
+
* id. Forgetting everything calls this once per KNOWN episode — never a recursive
|
|
30
|
+
* delete of the parent, which sits beside a user-chosen memory file.
|
|
31
|
+
*/
|
|
32
|
+
remove(episodeId: string): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Episode ids are also folder names — anything that could leave the folder is refused. */
|
|
36
|
+
export function isSafeExperienceId(id: string): boolean {
|
|
37
|
+
return /^[A-Za-z0-9_-]{1,64}$/.test(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class FileExperienceStore implements ExperienceStore {
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly inner: InMemoryExperienceStore,
|
|
43
|
+
private readonly file: SharedJsonFile,
|
|
44
|
+
private readonly parse: (raw: unknown) => ExperienceSnapshot,
|
|
45
|
+
private readonly folders: EpisodeFolders,
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Absorb the file when another process rewrote it. Also the boot hydration. A file
|
|
50
|
+
* that does not parse is left unabsorbed (a half-written snapshot): the store keeps
|
|
51
|
+
* what it has and the next call looks again.
|
|
52
|
+
*/
|
|
53
|
+
private fresh(): void {
|
|
54
|
+
const changed = this.file.readIfChanged();
|
|
55
|
+
if (!changed) return;
|
|
56
|
+
let raw: unknown;
|
|
57
|
+
try {
|
|
58
|
+
raw = JSON.parse(changed.text);
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const snapshot = this.parse(raw);
|
|
63
|
+
// A snapshot in a schema this server does not know (a newer writer) parses to
|
|
64
|
+
// empty — absorbing it would let the next write destroy it. Refuse instead.
|
|
65
|
+
const schema = (raw as { schema?: unknown } | null)?.schema;
|
|
66
|
+
if (typeof schema === "string" && schema !== snapshot.schema) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`${this.file.path} is in schema "${schema}", which this server cannot read (it writes "${snapshot.schema}"). Upgrade the memory server; the file was left untouched.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
this.inner.load(snapshot);
|
|
72
|
+
changed.accept();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async putEpisode(episode: Episode): Promise<void> {
|
|
76
|
+
this.fresh();
|
|
77
|
+
await this.inner.putEpisode(episode);
|
|
78
|
+
}
|
|
79
|
+
async getEpisode(id: string): Promise<Episode | undefined> {
|
|
80
|
+
this.fresh();
|
|
81
|
+
return this.inner.getEpisode(id);
|
|
82
|
+
}
|
|
83
|
+
async listEpisodes(): Promise<EpisodeSummary[]> {
|
|
84
|
+
this.fresh();
|
|
85
|
+
return this.inner.listEpisodes();
|
|
86
|
+
}
|
|
87
|
+
async deleteEpisode(id: string): Promise<boolean> {
|
|
88
|
+
this.fresh();
|
|
89
|
+
const ok = await this.inner.deleteEpisode(id);
|
|
90
|
+
if (isSafeExperienceId(id)) this.folders.remove(id);
|
|
91
|
+
return ok;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async putSkill(skill: Skill): Promise<void> {
|
|
95
|
+
this.fresh();
|
|
96
|
+
await this.inner.putSkill(skill);
|
|
97
|
+
}
|
|
98
|
+
async getSkill(id: string): Promise<Skill | undefined> {
|
|
99
|
+
this.fresh();
|
|
100
|
+
return this.inner.getSkill(id);
|
|
101
|
+
}
|
|
102
|
+
async listSkills(): Promise<Skill[]> {
|
|
103
|
+
this.fresh();
|
|
104
|
+
return this.inner.listSkills();
|
|
105
|
+
}
|
|
106
|
+
async deleteSkill(id: string): Promise<boolean> {
|
|
107
|
+
this.fresh();
|
|
108
|
+
return this.inner.deleteSkill(id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async putRun(run: Run): Promise<void> {
|
|
112
|
+
this.fresh();
|
|
113
|
+
await this.inner.putRun(run);
|
|
114
|
+
}
|
|
115
|
+
async appendRunStep(runId: string, step: RunStepLog): Promise<boolean> {
|
|
116
|
+
this.fresh();
|
|
117
|
+
return this.inner.appendRunStep(runId, step);
|
|
118
|
+
}
|
|
119
|
+
async getRun(id: string): Promise<Run | undefined> {
|
|
120
|
+
this.fresh();
|
|
121
|
+
return this.inner.getRun(id);
|
|
122
|
+
}
|
|
123
|
+
async listRuns(limit: number): Promise<Run[]> {
|
|
124
|
+
this.fresh();
|
|
125
|
+
return this.inner.listRuns(limit);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async recordAdaptation(adaptation: Adaptation): Promise<void> {
|
|
129
|
+
this.fresh();
|
|
130
|
+
await this.inner.recordAdaptation(adaptation);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async snapshot(): Promise<ExperienceSnapshot> {
|
|
134
|
+
this.fresh();
|
|
135
|
+
return this.inner.snapshot();
|
|
136
|
+
}
|
|
137
|
+
async clear(): Promise<void> {
|
|
138
|
+
this.fresh();
|
|
139
|
+
const known = (await this.inner.snapshot()).episodes.map((e) => e.id).filter(isSafeExperienceId);
|
|
140
|
+
await this.inner.clear();
|
|
141
|
+
for (const id of known) this.folders.remove(id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stand up experience for a headless server: the engine, the durable store and the
|
|
3
|
+
* private model it trains — or nothing, when experience is not switched on.
|
|
4
|
+
*
|
|
5
|
+
* Opt-in by design, like the Synapse feature it serves: a plain memory server (the
|
|
6
|
+
* one every coding agent installs) never pays for experience tools in its tool list.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ExperienceStore,
|
|
11
|
+
InMemoryExperienceStore,
|
|
12
|
+
ExperienceSnapshot,
|
|
13
|
+
dueSkills,
|
|
14
|
+
experienceOverview,
|
|
15
|
+
newExperienceId,
|
|
16
|
+
trainOnce,
|
|
17
|
+
} from "@seanhogg/builderforce-memory-engine";
|
|
18
|
+
import { dynamicImport } from "../dynamic-import.js";
|
|
19
|
+
import { MODEL_FILE_ENV, TOKENIZER_FILE_ENV } from "../embedding/index.js";
|
|
20
|
+
import { loadEvermindPackage, readEvermindVersion, type LoadedEvermind } from "../model/evermind-package.js";
|
|
21
|
+
import { SharedJsonFile, type SharedFileFs } from "../persistence/shared-json-file.js";
|
|
22
|
+
import { FileExperienceStore } from "./file-store.js";
|
|
23
|
+
import type { TrainEngine } from "./train.js";
|
|
24
|
+
|
|
25
|
+
/** '1' turns the experience tools on. */
|
|
26
|
+
export const EXPERIENCE_ENV = "BUILDERFORCE_MEMORY_EXPERIENCE";
|
|
27
|
+
|
|
28
|
+
/** The engine surface experience uses, passed around so the peer stays optional. */
|
|
29
|
+
export interface ExperienceEngine extends TrainEngine {
|
|
30
|
+
trainOnce: typeof trainOnce;
|
|
31
|
+
dueSkills: typeof dueSkills;
|
|
32
|
+
newExperienceId: typeof newExperienceId;
|
|
33
|
+
experienceOverview: typeof experienceOverview;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ExperienceHost {
|
|
37
|
+
store: ExperienceStore;
|
|
38
|
+
engine: ExperienceEngine;
|
|
39
|
+
/** The memory snapshot experience sits beside (the facts). */
|
|
40
|
+
memoryFile: string;
|
|
41
|
+
/** The experience snapshot file. */
|
|
42
|
+
file: string;
|
|
43
|
+
/** Where each episode's screenshots go: `<episodesDir>/<episodeId>/`. */
|
|
44
|
+
episodesDir: string;
|
|
45
|
+
/** The private model experience trains, when one is configured. */
|
|
46
|
+
modelFile?: string;
|
|
47
|
+
/** Load the model fresh from disk (training rewrites it, so it is never cached). */
|
|
48
|
+
loadModel(): Promise<LoadedEvermind | null>;
|
|
49
|
+
/** The model's version from its manifest alone, or null without a readable one. */
|
|
50
|
+
modelVersion(): Promise<string | null>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type HostFs = SharedFileFs & {
|
|
54
|
+
mkdirSync(path: string, opts: { recursive: boolean }): void;
|
|
55
|
+
rmSync(path: string, opts: { recursive: boolean; force: boolean }): void;
|
|
56
|
+
};
|
|
57
|
+
type HostPath = { dirname(p: string): string; join(...p: string[]): string };
|
|
58
|
+
|
|
59
|
+
export interface ExperienceHostOptions {
|
|
60
|
+
env?: Record<string, string | undefined>;
|
|
61
|
+
/** The memory snapshot in effect — experience lives beside it. */
|
|
62
|
+
memoryFile: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The experience host, or `null` when experience is off or the engine peer is
|
|
67
|
+
* absent. Never throws: a memory server must start without it.
|
|
68
|
+
*/
|
|
69
|
+
export async function createExperienceHost(opts: ExperienceHostOptions): Promise<ExperienceHost | null> {
|
|
70
|
+
const env = opts.env ?? process.env;
|
|
71
|
+
if (env[EXPERIENCE_ENV] !== "1") return null;
|
|
72
|
+
try {
|
|
73
|
+
const engine = (await dynamicImport("@seanhogg/builderforce-memory-engine")) as Partial<
|
|
74
|
+
ExperienceEngine & {
|
|
75
|
+
InMemoryExperienceStore: new (
|
|
76
|
+
initial?: ExperienceSnapshot,
|
|
77
|
+
onChange?: (s: ExperienceSnapshot) => void,
|
|
78
|
+
) => InMemoryExperienceStore;
|
|
79
|
+
parseSnapshot: (raw: unknown) => ExperienceSnapshot;
|
|
80
|
+
}
|
|
81
|
+
>;
|
|
82
|
+
if (!engine?.InMemoryExperienceStore || !engine.parseSnapshot || !engine.trainOnce || !engine.adaptPackage) return null;
|
|
83
|
+
|
|
84
|
+
const fs = (await dynamicImport("node:fs")) as HostFs;
|
|
85
|
+
const path = (await dynamicImport("node:path")) as HostPath;
|
|
86
|
+
const dir = path.dirname(opts.memoryFile);
|
|
87
|
+
const file = path.join(dir, "experience.json");
|
|
88
|
+
const episodesDir = path.join(dir, "episodes");
|
|
89
|
+
fs.mkdirSync(episodesDir, { recursive: true });
|
|
90
|
+
|
|
91
|
+
const shared = new SharedJsonFile(file, fs);
|
|
92
|
+
const inner = new engine.InMemoryExperienceStore(undefined, (s) => shared.write(JSON.stringify(s, null, 2)));
|
|
93
|
+
const store = new FileExperienceStore(inner, shared, engine.parseSnapshot, {
|
|
94
|
+
remove: (id) => fs.rmSync(path.join(episodesDir, id), { recursive: true, force: true }),
|
|
95
|
+
});
|
|
96
|
+
const modelFile = env[MODEL_FILE_ENV] || undefined;
|
|
97
|
+
const tokenizerFile = env[TOKENIZER_FILE_ENV] || undefined;
|
|
98
|
+
const loadModel = async () => (modelFile ? loadEvermindPackage(modelFile, tokenizerFile) : null);
|
|
99
|
+
const modelVersion = async () => (modelFile ? readEvermindVersion(modelFile) : null);
|
|
100
|
+
return {
|
|
101
|
+
store,
|
|
102
|
+
engine: engine as ExperienceEngine,
|
|
103
|
+
memoryFile: opts.memoryFile,
|
|
104
|
+
file,
|
|
105
|
+
episodesDir,
|
|
106
|
+
modelFile,
|
|
107
|
+
loadModel,
|
|
108
|
+
modelVersion,
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|