@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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experience tools — what Evermind has DONE, over MCP.
|
|
3
|
+
*
|
|
4
|
+
* The same framework-neutral {@link MemoryTool}s as the memory tools, registered
|
|
5
|
+
* beside them when experience is on. Every result is JSON: Synapse drives these as a
|
|
6
|
+
* program (record → review → compile → run → audit), and a model reads JSON as well.
|
|
7
|
+
* The domain rules — Train Once, approval gates, routines, the corpus — are the
|
|
8
|
+
* engine's; these handlers only validate input and move data through the store.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import type { Episode, ReviewEdits, Run, RunStatus, SkillRoutine, StepOutcome } from "@seanhogg/builderforce-memory-engine";
|
|
13
|
+
import { fail, okJson, type MemoryTool, type ToolResult } from "../tool-core.js";
|
|
14
|
+
import { isSafeExperienceId } from "./file-store.js";
|
|
15
|
+
import type { ExperienceHost } from "./host.js";
|
|
16
|
+
import { experienceIndex, pendingDocuments, trainExperience } from "./train.js";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_RUNS = 50;
|
|
19
|
+
const MAX_RUNS_RETURNED = 500;
|
|
20
|
+
|
|
21
|
+
const editsShape = z
|
|
22
|
+
.object({
|
|
23
|
+
name: z.string().nullable().optional(),
|
|
24
|
+
removed: z.array(z.string()).optional(),
|
|
25
|
+
fixed: z.array(z.string()).optional(),
|
|
26
|
+
approvals: z.record(z.boolean()).optional(),
|
|
27
|
+
paramNames: z.record(z.string()).optional(),
|
|
28
|
+
})
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Review edits keyed by recorded step id.");
|
|
31
|
+
|
|
32
|
+
const scheduleShape = z.union([
|
|
33
|
+
z.object({ every: z.literal("minutes"), minutes: z.number().int().min(1).max(10_080) }),
|
|
34
|
+
z.object({ every: z.literal("day"), hour: z.number().int().min(0).max(23), minute: z.number().int().min(0).max(59) }),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const ACTION_KINDS = new Set(["click", "setValue", "keys"]);
|
|
38
|
+
|
|
39
|
+
const isTarget = (t: unknown): boolean =>
|
|
40
|
+
!!t && typeof t === "object" && typeof (t as { name?: unknown }).name === "string";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Why one recorded action is unusable, or null. Every field the corpus and replay read
|
|
44
|
+
* is checked here, so one malformed step is refused at save time instead of making
|
|
45
|
+
* every later `experience_train` throw.
|
|
46
|
+
*/
|
|
47
|
+
function actionProblem(a: Record<string, unknown>): string | null {
|
|
48
|
+
switch (a["kind"]) {
|
|
49
|
+
case "click":
|
|
50
|
+
return isTarget(a["target"]) ? null : "a click needs a target element";
|
|
51
|
+
case "setValue":
|
|
52
|
+
if (!isTarget(a["target"])) return "a setValue needs a target element";
|
|
53
|
+
return typeof a["value"] === "string" ? null : "a setValue needs a string value (empty for a secret)";
|
|
54
|
+
case "keys":
|
|
55
|
+
if (typeof a["keys"] !== "string" || !a["keys"]) return "a keys action needs its keys";
|
|
56
|
+
return a["target"] == null || isTarget(a["target"]) ? null : "a keys target must be an element";
|
|
57
|
+
default:
|
|
58
|
+
return "the action kind must be click, setValue or keys";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Accept a recorder's episode, or say what is wrong with it. */
|
|
63
|
+
export function asEpisode(raw: unknown, newId: () => string): Episode | string {
|
|
64
|
+
const e = raw as Partial<Episode> | null;
|
|
65
|
+
if (!e || typeof e !== "object") return "episode must be an object.";
|
|
66
|
+
if (typeof e.name !== "string" || !e.name.trim()) return "episode.name is required.";
|
|
67
|
+
if (typeof e.program !== "string" || !e.program) return "episode.program is required.";
|
|
68
|
+
if (!Array.isArray(e.steps)) return "episode.steps must be an array.";
|
|
69
|
+
for (const [i, s] of e.steps.entries()) {
|
|
70
|
+
if (!s || typeof s.id !== "string" || !s.action || !ACTION_KINDS.has(s.action.kind)) {
|
|
71
|
+
return `episode.steps[${i}] needs an id and an action of kind click, setValue or keys.`;
|
|
72
|
+
}
|
|
73
|
+
const problem = actionProblem(s.action as unknown as Record<string, unknown>);
|
|
74
|
+
if (problem) return `episode.steps[${i}]: ${problem}.`;
|
|
75
|
+
}
|
|
76
|
+
// A secret field records THAT it was filled, never the value: whatever a recorder
|
|
77
|
+
// sent, a secret value is not stored (and so never returned by episode_get).
|
|
78
|
+
const steps = e.steps.map((s) =>
|
|
79
|
+
s.action.kind === "setValue" && s.action.secret ? { ...s, action: { ...s.action, value: "" } } : s,
|
|
80
|
+
);
|
|
81
|
+
const id = typeof e.id === "string" && e.id ? e.id : newId();
|
|
82
|
+
if (!isSafeExperienceId(id)) return "episode.id may use letters, digits, '-' and '_' only.";
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
name: e.name.trim(),
|
|
86
|
+
program: e.program,
|
|
87
|
+
args: Array.isArray(e.args) ? e.args.map(String) : [],
|
|
88
|
+
startedAt: typeof e.startedAt === "number" ? e.startedAt : Date.now(),
|
|
89
|
+
endedAt: typeof e.endedAt === "number" ? e.endedAt : null,
|
|
90
|
+
steps,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A tool whose thrown errors come back as tool errors named after it. */
|
|
95
|
+
function tool(
|
|
96
|
+
name: string,
|
|
97
|
+
description: string,
|
|
98
|
+
inputSchema: z.ZodRawShape,
|
|
99
|
+
run: (args: Record<string, unknown>) => Promise<ToolResult>,
|
|
100
|
+
): MemoryTool {
|
|
101
|
+
return {
|
|
102
|
+
name,
|
|
103
|
+
description,
|
|
104
|
+
inputSchema,
|
|
105
|
+
handler: async (args) => {
|
|
106
|
+
try {
|
|
107
|
+
return await run(args);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return fail(`${name} failed: ${String(err)}`);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
116
|
+
|
|
117
|
+
/** This machine's time zone as minutes east of UTC — what the engine's routines expect. */
|
|
118
|
+
function localOffsetMinutes(): number {
|
|
119
|
+
return -new Date().getTimezoneOffset();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function buildExperienceTools(host: ExperienceHost): MemoryTool[] {
|
|
123
|
+
const { store, engine } = host;
|
|
124
|
+
|
|
125
|
+
const episodeTools = [
|
|
126
|
+
tool(
|
|
127
|
+
"experience_info",
|
|
128
|
+
"Where experience is kept and how much there is: the memory and experience snapshot files, the screenshot folder, the private model it trains, and counts.",
|
|
129
|
+
{},
|
|
130
|
+
async () => {
|
|
131
|
+
const snap = await store.snapshot();
|
|
132
|
+
return okJson({
|
|
133
|
+
memoryFile: host.memoryFile,
|
|
134
|
+
file: host.file,
|
|
135
|
+
episodesDir: host.episodesDir,
|
|
136
|
+
modelFile: host.modelFile ?? null,
|
|
137
|
+
episodes: snap.episodes.length,
|
|
138
|
+
skills: snap.skills.length,
|
|
139
|
+
runs: snap.runs.length,
|
|
140
|
+
learned: Object.keys(snap.learned).length,
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
),
|
|
144
|
+
tool(
|
|
145
|
+
"experience_overview",
|
|
146
|
+
"What experience says about the brain, for drawing it: counts per region (hippocampus = demonstrations, basalGanglia = skills, amygdala = approval decisions, hypothalamus = routines, neocortex = procedures the private model has learned), learned vs pending, run outcomes, every adaptation's measured loss, activity per local day, and the newest events.",
|
|
147
|
+
{ days: z.number().int().min(1).max(365).optional() },
|
|
148
|
+
async (args) => {
|
|
149
|
+
const version = await host.modelVersion();
|
|
150
|
+
const overview = engine.experienceOverview(await store.snapshot(), {
|
|
151
|
+
modelIndex: version ? experienceIndex(version) : 0,
|
|
152
|
+
now: Date.now(),
|
|
153
|
+
utcOffsetMinutes: localOffsetMinutes(),
|
|
154
|
+
days: typeof args["days"] === "number" ? args["days"] : undefined,
|
|
155
|
+
});
|
|
156
|
+
return okJson({ modelVersion: version, ...overview });
|
|
157
|
+
},
|
|
158
|
+
),
|
|
159
|
+
tool(
|
|
160
|
+
"episode_save",
|
|
161
|
+
"Save a recorded demonstration (an episode: a program, and the UI steps a person took in it). Returns its id. Screenshots belong in <episodesDir>/<id>/.",
|
|
162
|
+
{ episode: z.record(z.unknown()).describe("The episode, in the engine's Episode shape.") },
|
|
163
|
+
async (args) => {
|
|
164
|
+
const episode = asEpisode(args["episode"], engine.newExperienceId);
|
|
165
|
+
if (typeof episode === "string") return fail(episode);
|
|
166
|
+
await store.putEpisode(episode);
|
|
167
|
+
return okJson({ id: episode.id });
|
|
168
|
+
},
|
|
169
|
+
),
|
|
170
|
+
tool("episode_list", "List recorded demonstrations, newest first (summaries only).", {}, async () =>
|
|
171
|
+
okJson(await store.listEpisodes()),
|
|
172
|
+
),
|
|
173
|
+
tool("episode_get", "Fetch one demonstration with all of its steps.", { id: z.string() }, async (args) => {
|
|
174
|
+
const e = await store.getEpisode(str(args["id"]));
|
|
175
|
+
return e ? okJson(e) : fail(`No episode "${str(args["id"])}".`);
|
|
176
|
+
}),
|
|
177
|
+
tool(
|
|
178
|
+
"episode_forget",
|
|
179
|
+
"Delete a demonstration and its screenshots. Skills compiled from it keep working.",
|
|
180
|
+
{ id: z.string() },
|
|
181
|
+
async (args) => okJson({ forgotten: await store.deleteEpisode(str(args["id"])) }),
|
|
182
|
+
),
|
|
183
|
+
tool(
|
|
184
|
+
"experience_forget_all",
|
|
185
|
+
"Delete every demonstration, screenshot, skill and run. Facts (memories) and the model are not touched. Requires confirm: true.",
|
|
186
|
+
{ confirm: z.literal(true) },
|
|
187
|
+
async () => {
|
|
188
|
+
await store.clear();
|
|
189
|
+
return okJson({ forgotten: true });
|
|
190
|
+
},
|
|
191
|
+
),
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
/** Train Once over a stored episode — ONE path for preview (no save) and compile (save). */
|
|
195
|
+
async function compile(args: Record<string, unknown>, save: boolean): Promise<ToolResult> {
|
|
196
|
+
const episode = await store.getEpisode(str(args["episodeId"]));
|
|
197
|
+
if (!episode) return fail(`No episode "${str(args["episodeId"])}".`);
|
|
198
|
+
const skill = engine.trainOnce(episode, (args["edits"] as ReviewEdits | undefined) ?? {});
|
|
199
|
+
if (save) await store.putSkill(skill);
|
|
200
|
+
return okJson(skill);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const skillTools = [
|
|
204
|
+
tool(
|
|
205
|
+
"skill_preview",
|
|
206
|
+
"Show the skill Train Once would compile from a demonstration with these review edits, without saving it.",
|
|
207
|
+
{ episodeId: z.string(), edits: editsShape },
|
|
208
|
+
(a) => compile(a, false),
|
|
209
|
+
),
|
|
210
|
+
tool(
|
|
211
|
+
"skill_compile",
|
|
212
|
+
"Compile a demonstration into a runnable skill (Train Once) and save it.",
|
|
213
|
+
{ episodeId: z.string(), edits: editsShape },
|
|
214
|
+
(a) => compile(a, true),
|
|
215
|
+
),
|
|
216
|
+
tool("skill_list", "List skills, newest first.", {}, async () => okJson(await store.listSkills())),
|
|
217
|
+
tool("skill_get", "Fetch one skill.", { id: z.string() }, async (args) => {
|
|
218
|
+
const s = await store.getSkill(str(args["id"]));
|
|
219
|
+
return s ? okJson(s) : fail(`No skill "${str(args["id"])}".`);
|
|
220
|
+
}),
|
|
221
|
+
tool("skill_forget", "Delete a skill (its run history stays).", { id: z.string() }, async (args) =>
|
|
222
|
+
okJson({ forgotten: await store.deleteSkill(str(args["id"])) }),
|
|
223
|
+
),
|
|
224
|
+
tool(
|
|
225
|
+
"skill_schedule",
|
|
226
|
+
"Give a skill a routine (every N minutes, or daily at a local time, with values for its non-secret parameters), or pass null to remove it.",
|
|
227
|
+
{
|
|
228
|
+
id: z.string(),
|
|
229
|
+
routine: z
|
|
230
|
+
.object({
|
|
231
|
+
schedule: scheduleShape,
|
|
232
|
+
values: z.record(z.string()).default({}),
|
|
233
|
+
enabled: z.boolean().default(true),
|
|
234
|
+
})
|
|
235
|
+
.nullable(),
|
|
236
|
+
},
|
|
237
|
+
async (args) => {
|
|
238
|
+
const skill = await store.getSkill(str(args["id"]));
|
|
239
|
+
if (!skill) return fail(`No skill "${str(args["id"])}".`);
|
|
240
|
+
const r = args["routine"] as Omit<SkillRoutine, "lastRunAt"> | null;
|
|
241
|
+
// Routine values are stored in plain JSON, so a secret parameter's value is
|
|
242
|
+
// never kept here — secrets come from the host's vault at run time.
|
|
243
|
+
const secret = new Set(skill.params.filter((p) => p.secret).map((p) => p.name));
|
|
244
|
+
const values = r ? Object.fromEntries(Object.entries(r.values).filter(([k]) => !secret.has(k))) : {};
|
|
245
|
+
skill.routine = r ? { ...r, values, lastRunAt: skill.routine?.lastRunAt ?? null } : null;
|
|
246
|
+
await store.putSkill(skill);
|
|
247
|
+
return okJson(skill);
|
|
248
|
+
},
|
|
249
|
+
),
|
|
250
|
+
tool(
|
|
251
|
+
"skills_due",
|
|
252
|
+
"Skills whose routine is due now, least recently run first.",
|
|
253
|
+
{
|
|
254
|
+
tzOffsetMinutes: z
|
|
255
|
+
.number()
|
|
256
|
+
.int()
|
|
257
|
+
.min(-840)
|
|
258
|
+
.max(840)
|
|
259
|
+
.optional()
|
|
260
|
+
.describe("Local offset east of UTC; defaults to this machine's."),
|
|
261
|
+
},
|
|
262
|
+
async (args) => {
|
|
263
|
+
const tz = args["tzOffsetMinutes"];
|
|
264
|
+
const offset = typeof tz === "number" ? tz : localOffsetMinutes();
|
|
265
|
+
return okJson(engine.dueSkills(await store.listSkills(), Date.now(), offset));
|
|
266
|
+
},
|
|
267
|
+
),
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const runTools = [
|
|
271
|
+
tool(
|
|
272
|
+
"run_start",
|
|
273
|
+
"Start a run of a skill. A routine run also marks the routine as run, so it is not due again until its next slot.",
|
|
274
|
+
{ skillId: z.string(), trigger: z.enum(["manual", "routine"]) },
|
|
275
|
+
async (args) => {
|
|
276
|
+
const skill = await store.getSkill(str(args["skillId"]));
|
|
277
|
+
if (!skill) return fail(`No skill "${str(args["skillId"])}".`);
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
const run: Run = {
|
|
280
|
+
id: engine.newExperienceId(),
|
|
281
|
+
skillId: skill.id,
|
|
282
|
+
skillName: skill.name,
|
|
283
|
+
trigger: str(args["trigger"]),
|
|
284
|
+
status: "running",
|
|
285
|
+
startedAt: now,
|
|
286
|
+
steps: [],
|
|
287
|
+
};
|
|
288
|
+
await store.putRun(run);
|
|
289
|
+
if (run.trigger === "routine" && skill.routine) {
|
|
290
|
+
skill.routine.lastRunAt = now;
|
|
291
|
+
await store.putSkill(skill);
|
|
292
|
+
}
|
|
293
|
+
return okJson(run);
|
|
294
|
+
},
|
|
295
|
+
),
|
|
296
|
+
tool(
|
|
297
|
+
"run_step",
|
|
298
|
+
"Append one line to a run's audit trail.",
|
|
299
|
+
{
|
|
300
|
+
runId: z.string(),
|
|
301
|
+
idx: z.number().int().min(0),
|
|
302
|
+
stepId: z.string(),
|
|
303
|
+
outcome: z.enum(["ok", "fallback", "failed", "approved", "denied"]),
|
|
304
|
+
detail: z.string().nullable().optional(),
|
|
305
|
+
},
|
|
306
|
+
async (args) => {
|
|
307
|
+
const logged = await store.appendRunStep(str(args["runId"]), {
|
|
308
|
+
idx: Number(args["idx"]),
|
|
309
|
+
stepId: str(args["stepId"]),
|
|
310
|
+
outcome: args["outcome"] as StepOutcome,
|
|
311
|
+
detail: typeof args["detail"] === "string" ? args["detail"] : null,
|
|
312
|
+
at: Date.now(),
|
|
313
|
+
});
|
|
314
|
+
return logged ? okJson({ logged }) : fail(`No run "${str(args["runId"])}".`);
|
|
315
|
+
},
|
|
316
|
+
),
|
|
317
|
+
tool(
|
|
318
|
+
"run_finish",
|
|
319
|
+
"Close a run with its final status.",
|
|
320
|
+
{ runId: z.string(), status: z.enum(["succeeded", "failed", "stopped", "denied"]), error: z.string().nullable().optional() },
|
|
321
|
+
async (args) => {
|
|
322
|
+
const run = await store.getRun(str(args["runId"]));
|
|
323
|
+
if (!run) return fail(`No run "${str(args["runId"])}".`);
|
|
324
|
+
run.status = args["status"] as RunStatus;
|
|
325
|
+
run.endedAt = Date.now();
|
|
326
|
+
run.error = typeof args["error"] === "string" ? args["error"] : null;
|
|
327
|
+
await store.putRun(run);
|
|
328
|
+
return okJson(run);
|
|
329
|
+
},
|
|
330
|
+
),
|
|
331
|
+
tool(
|
|
332
|
+
"run_list",
|
|
333
|
+
"Recent runs, newest first.",
|
|
334
|
+
{ limit: z.number().int().min(1).max(MAX_RUNS_RETURNED).optional() },
|
|
335
|
+
async (args) => okJson(await store.listRuns(Number(args["limit"] ?? DEFAULT_RUNS))),
|
|
336
|
+
),
|
|
337
|
+
tool("run_get", "Fetch one run with its audit trail.", { id: z.string() }, async (args) => {
|
|
338
|
+
const r = await store.getRun(str(args["id"]));
|
|
339
|
+
return r ? okJson(r) : fail(`No run "${str(args["id"])}".`);
|
|
340
|
+
}),
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
const learnTools = [
|
|
344
|
+
tool(
|
|
345
|
+
"experience_train",
|
|
346
|
+
"Teach the private Evermind model the procedures it has not learned yet (each skill, and each demonstration not yet compiled into one). Pass dryRun to only count them. The previous model is kept beside the new one.",
|
|
347
|
+
{ dryRun: z.boolean().optional() },
|
|
348
|
+
async (args) => {
|
|
349
|
+
if (!host.modelFile) return fail("No private Evermind model is configured (set BUILDERFORCE_MEMORY_MODEL).");
|
|
350
|
+
const model = await host.loadModel();
|
|
351
|
+
if (!model) return fail(`Could not load an evermind-lm package and its tokenizer from ${host.modelFile}.`);
|
|
352
|
+
if (args["dryRun"] === true) {
|
|
353
|
+
const version = model.pkg.manifest.version ?? "0";
|
|
354
|
+
const pending = (await pendingDocuments(store, engine, version)).length;
|
|
355
|
+
return okJson({ status: "dry_run", pending, version });
|
|
356
|
+
}
|
|
357
|
+
return okJson(await trainExperience(store, engine, model, host.modelFile));
|
|
358
|
+
},
|
|
359
|
+
),
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
return [...episodeTools, ...skillTools, ...runTools, ...learnTools];
|
|
363
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teach the person's private Evermind what they have done.
|
|
3
|
+
*
|
|
4
|
+
* Experience becomes text through the engine (`experienceDocuments`: each skill or
|
|
5
|
+
* uncompiled demonstration as a written procedure), and the text becomes weights
|
|
6
|
+
* through the engine's ONE adaptation recipe (`adaptPackage`). This module only
|
|
7
|
+
* decides what is new, runs the passes over the local model file and writes the
|
|
8
|
+
* result back, keeping the previous model beside it.
|
|
9
|
+
*
|
|
10
|
+
* "What is new" is the store's learned ledger. Every adaptation writes `<base>+expN`
|
|
11
|
+
* and records N against each procedure it learned, so a loaded model trusts exactly
|
|
12
|
+
* the entries at or below its own N: rolling back to `<model>.prev` (+exp N-1)
|
|
13
|
+
* re-teaches what the discarded version had learned, and a fresh base (no suffix,
|
|
14
|
+
* N = 0) — a newly downloaded Evermind — gets every procedure again.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { adaptPackage, experienceDocuments, packDocuments, ExperienceStore } from "@seanhogg/builderforce-memory-engine";
|
|
18
|
+
import type { LoadedEvermind } from "../model/evermind-package.js";
|
|
19
|
+
|
|
20
|
+
/** The engine functions training needs, passed in so the peer stays optional. */
|
|
21
|
+
export interface TrainEngine {
|
|
22
|
+
experienceDocuments: typeof experienceDocuments;
|
|
23
|
+
packDocuments: typeof packDocuments;
|
|
24
|
+
adaptPackage: typeof adaptPackage;
|
|
25
|
+
EVERMIND_ADAPT_MAX_CHARS: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type TrainOutcome =
|
|
29
|
+
| { status: "nothing_new"; pending: 0; version: string }
|
|
30
|
+
| { status: "no_window"; pending: number; version: string }
|
|
31
|
+
| { status: "trained"; learned: number; passes: number; loss: number; version: string; previousModel: string };
|
|
32
|
+
|
|
33
|
+
const EXP_SUFFIX = /\+exp(\d+)$/;
|
|
34
|
+
|
|
35
|
+
/** The version an adaptation writes: `<base>+exp1`, then `+exp2`, … */
|
|
36
|
+
export function nextExperienceVersion(version: string): string {
|
|
37
|
+
const m = EXP_SUFFIX.exec(version);
|
|
38
|
+
return m ? version.replace(EXP_SUFFIX, `+exp${Number(m[1]) + 1}`) : `${version}+exp1`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Which adaptation produced this model: N for `+expN`, 0 for a base that has none. */
|
|
42
|
+
export function experienceIndex(version: string): number {
|
|
43
|
+
const m = EXP_SUFFIX.exec(version);
|
|
44
|
+
return m ? Number(m[1]) : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Procedures the model has not learned yet. */
|
|
48
|
+
export async function pendingDocuments(store: ExperienceStore, engine: TrainEngine, modelVersion: string) {
|
|
49
|
+
const snap = await store.snapshot();
|
|
50
|
+
const current = experienceIndex(modelVersion);
|
|
51
|
+
const known = (id: string) => {
|
|
52
|
+
const n = snap.learned[id];
|
|
53
|
+
return typeof n === "number" && n >= 1 && n <= current;
|
|
54
|
+
};
|
|
55
|
+
return engine.experienceDocuments(snap.episodes, snap.skills).filter((d) => !known(d.id));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function trainExperience(
|
|
59
|
+
store: ExperienceStore,
|
|
60
|
+
engine: TrainEngine,
|
|
61
|
+
model: LoadedEvermind,
|
|
62
|
+
modelFile: string,
|
|
63
|
+
): Promise<TrainOutcome> {
|
|
64
|
+
const version = model.pkg.manifest.version ?? "0";
|
|
65
|
+
const docs = await pendingDocuments(store, engine, version);
|
|
66
|
+
if (docs.length === 0) return { status: "nothing_new", pending: 0, version };
|
|
67
|
+
|
|
68
|
+
const next = nextExperienceVersion(version);
|
|
69
|
+
// The engine's package type; the structural view is what was loaded from disk.
|
|
70
|
+
let pkg = model.pkg as unknown as Parameters<typeof adaptPackage>[0];
|
|
71
|
+
const learned: string[] = [];
|
|
72
|
+
let lossSum = 0;
|
|
73
|
+
let passes = 0;
|
|
74
|
+
for (const pass of engine.packDocuments(docs, engine.EVERMIND_ADAPT_MAX_CHARS)) {
|
|
75
|
+
const r = engine.adaptPackage(pkg, model.codec, pass.map((d) => d.text).join("\n\n"), next);
|
|
76
|
+
if (!r) continue;
|
|
77
|
+
pkg = r.pkg;
|
|
78
|
+
lossSum += r.loss;
|
|
79
|
+
passes++;
|
|
80
|
+
learned.push(...pass.map((d) => d.id));
|
|
81
|
+
}
|
|
82
|
+
if (passes === 0) return { status: "no_window", pending: docs.length, version };
|
|
83
|
+
|
|
84
|
+
const loss = lossSum / passes;
|
|
85
|
+
// Keep the model this pass started from, so one bad adaptation is one rename away
|
|
86
|
+
// from undone. The new model is written beside the old one and renamed into place,
|
|
87
|
+
// so a crash mid-write never leaves a truncated model. The ledger is written only
|
|
88
|
+
// after the new model is on disk.
|
|
89
|
+
const previousModel = `${modelFile}.prev`;
|
|
90
|
+
model.fs.copyFileSync(modelFile, previousModel);
|
|
91
|
+
const staged = `${modelFile}.next`;
|
|
92
|
+
model.fs.writeFileSync(staged, new Uint8Array(pkg.toBlob()));
|
|
93
|
+
model.fs.renameSync(staged, modelFile);
|
|
94
|
+
await store.recordAdaptation({ index: experienceIndex(next), version: next, learned, passes, loss, at: Date.now() });
|
|
95
|
+
return { status: "trained", learned: learned.length, passes, loss, version: next, previousModel };
|
|
96
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway-backed cost tools (token_usage, model_efficiency). Registered only when a
|
|
3
|
+
* gateway URL and a `bfk_*` key are both configured, so memory-only deployments
|
|
4
|
+
* never see them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { fail, ok, type MemoryTool } from "./tool-core.js";
|
|
8
|
+
|
|
9
|
+
/** Snapshot shape returned by GET /llm/v1/builder-insights. */
|
|
10
|
+
interface BuilderInsightsSnapshot {
|
|
11
|
+
windowLabel?: string;
|
|
12
|
+
todayTokens?: number;
|
|
13
|
+
todayCostUsd?: number;
|
|
14
|
+
dailyCapTokens?: number | null;
|
|
15
|
+
pctOfDailyCap?: number | null;
|
|
16
|
+
topModel?: { model: string; tokens: number } | null;
|
|
17
|
+
costPerMergedPrUsd?: number | null;
|
|
18
|
+
tip?: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Ranking shape returned by GET /llm/v1/model-analytics. */
|
|
22
|
+
interface ModelAnalytics {
|
|
23
|
+
byAction?: Array<{
|
|
24
|
+
actionType?: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
models?: Array<{
|
|
27
|
+
model: string;
|
|
28
|
+
samples?: number;
|
|
29
|
+
avgScore?: number;
|
|
30
|
+
mergeRate?: number;
|
|
31
|
+
avgCostMillicents?: number;
|
|
32
|
+
}>;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function gatewayGet<T>(gatewayUrl: string, path: string, apiKey: string): Promise<T> {
|
|
37
|
+
const res = await fetch(`${gatewayUrl}${path}`, {
|
|
38
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) throw new Error(`gateway ${path} → HTTP ${res.status}`);
|
|
41
|
+
return (await res.json()) as T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function appendGatewayCostTools(tools: MemoryTool[], gatewayUrl: string, apiKey: string): void {
|
|
45
|
+
tools.push({
|
|
46
|
+
name: "token_usage",
|
|
47
|
+
description: "Current token spend + budget for the workspace (today).",
|
|
48
|
+
inputSchema: {},
|
|
49
|
+
handler: async () => {
|
|
50
|
+
try {
|
|
51
|
+
const s = await gatewayGet<BuilderInsightsSnapshot>(gatewayUrl, "/llm/v1/builder-insights", apiKey);
|
|
52
|
+
const lines = [
|
|
53
|
+
`Token usage (${s.windowLabel ?? "today"}):`,
|
|
54
|
+
`• Tokens: ${(s.todayTokens ?? 0).toLocaleString()}`,
|
|
55
|
+
`• Cost: $${(s.todayCostUsd ?? 0).toFixed(2)}`,
|
|
56
|
+
`• % of daily cap: ${
|
|
57
|
+
s.pctOfDailyCap == null
|
|
58
|
+
? "no cap"
|
|
59
|
+
: `${s.pctOfDailyCap}%${s.dailyCapTokens ? ` of ${s.dailyCapTokens.toLocaleString()}` : ""}`
|
|
60
|
+
}`,
|
|
61
|
+
`• Top model: ${s.topModel ? `${s.topModel.model} (${s.topModel.tokens.toLocaleString()} tok)` : "—"}`,
|
|
62
|
+
];
|
|
63
|
+
if (s.costPerMergedPrUsd != null) lines.push(`• Cost / merged PR: $${s.costPerMergedPrUsd.toFixed(2)}`);
|
|
64
|
+
if (s.tip) lines.push(`• Tip: ${s.tip}`);
|
|
65
|
+
return ok(lines.join("\n"));
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return fail(`token_usage failed: ${String(err)}`);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
tools.push({
|
|
73
|
+
name: "model_efficiency",
|
|
74
|
+
description: "Which models performed best/cheapest for this workspace's recent work.",
|
|
75
|
+
inputSchema: {},
|
|
76
|
+
handler: async () => {
|
|
77
|
+
try {
|
|
78
|
+
const a = await gatewayGet<ModelAnalytics>(gatewayUrl, "/llm/v1/model-analytics", apiKey);
|
|
79
|
+
const groups = a.byAction ?? [];
|
|
80
|
+
if (groups.length === 0) return ok("No model efficiency data yet for this workspace.");
|
|
81
|
+
const out: string[] = ["Model efficiency by action type (best first):"];
|
|
82
|
+
for (const g of groups) {
|
|
83
|
+
out.push(`\n${g.label ?? g.actionType ?? "action"}:`);
|
|
84
|
+
for (const m of (g.models ?? []).slice(0, 3)) {
|
|
85
|
+
const cost = m.avgCostMillicents != null ? ` $${(m.avgCostMillicents / 100_000).toFixed(4)}/call` : "";
|
|
86
|
+
const merge = m.mergeRate != null ? ` merge=${Math.round(m.mergeRate * 100)}%` : "";
|
|
87
|
+
const score = m.avgScore != null ? ` score=${m.avgScore}` : "";
|
|
88
|
+
out.push(` • ${m.model}${score}${merge}${cost} (n=${m.samples ?? 0})`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return ok(out.join("\n"));
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return fail(`model_efficiency failed: ${String(err)}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,14 @@ export type { LocalBackendOptions } from "./backends/memory-store.js";
|
|
|
23
23
|
|
|
24
24
|
// ── Tool core ─────────────────────────────────────────────────────────────────
|
|
25
25
|
export { buildMemoryTools } from "./tools.js";
|
|
26
|
-
export type {
|
|
26
|
+
export type { MemoryToolsOptions } from "./tools.js";
|
|
27
|
+
export type { MemoryTool, ToolResult } from "./tool-core.js";
|
|
28
|
+
|
|
29
|
+
// ── Experience (demonstrations → Train Once skills → runs → the private model) ─
|
|
30
|
+
// The domain is the engine's; this is its durable host and its tools.
|
|
31
|
+
export { createExperienceHost, EXPERIENCE_ENV } from "./experience/host.js";
|
|
32
|
+
export type { ExperienceHost, ExperienceHostOptions, ExperienceEngine } from "./experience/host.js";
|
|
33
|
+
export { nextExperienceVersion } from "./experience/train.js";
|
|
27
34
|
|
|
28
35
|
// ── Compaction (absorbed fact → one-line pointer stub) ───────────────────────
|
|
29
36
|
// Exported so an EXTERNAL compactor (the BuilderForce VS Code extension rewrites
|
package/src/install/install.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import nodeFs from "node:fs";
|
|
11
11
|
import nodeOs from "node:os";
|
|
12
12
|
import nodePath from "node:path";
|
|
13
|
-
import { HOSTS, SERVER_KEY, type HostAdapter, type HostEnv } from "./hosts.js";
|
|
13
|
+
import { HOSTS, SERVER_KEY, findHost, type HostAdapter, type HostEnv } from "./hosts.js";
|
|
14
14
|
import { buildServerSpec, type ServerSpecOptions, type StdioServerSpec } from "./server-spec.js";
|
|
15
15
|
|
|
16
16
|
export interface FsLike {
|
|
@@ -132,7 +132,7 @@ function installToml(fs: FsLike, file: string, spec: StdioServerSpec): InstallSt
|
|
|
132
132
|
function resolveHosts(selector: HostSelector, fs: FsLike, hostEnv: HostEnv): HostAdapter[] {
|
|
133
133
|
if (Array.isArray(selector)) {
|
|
134
134
|
return selector.map((id) => {
|
|
135
|
-
const h =
|
|
135
|
+
const h = findHost(id);
|
|
136
136
|
if (!h) throw new Error(`unknown host "${id}" (known: ${HOSTS.map((x) => x.id).join(", ")})`);
|
|
137
137
|
return h;
|
|
138
138
|
});
|