@seanhogg/builderforce-memory 2026.6.28 → 2026.6.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 +28 -0
- package/dist/diagnostics/index.d.ts +7 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +6 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/diagnostics/stack-diagnostic.d.ts +71 -0
- package/dist/diagnostics/stack-diagnostic.d.ts.map +1 -0
- package/dist/diagnostics/stack-diagnostic.js +43 -0
- package/dist/diagnostics/stack-diagnostic.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/workflow/index.d.ts +11 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +10 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/registry.d.ts +23 -0
- package/dist/workflow/registry.d.ts.map +1 -0
- package/dist/workflow/registry.js +47 -0
- package/dist/workflow/registry.js.map +1 -0
- package/dist/workflow/run.d.ts +26 -0
- package/dist/workflow/run.d.ts.map +1 -0
- package/dist/workflow/run.js +40 -0
- package/dist/workflow/run.js.map +1 -0
- package/dist/workflow/steps.d.ts +19 -0
- package/dist/workflow/steps.d.ts.map +1 -0
- package/dist/workflow/steps.js +318 -0
- package/dist/workflow/steps.js.map +1 -0
- package/dist/workflow/templates.d.ts +24 -0
- package/dist/workflow/templates.d.ts.map +1 -0
- package/dist/workflow/templates.js +80 -0
- package/dist/workflow/templates.js.map +1 -0
- package/dist/workflow/types.d.ts +38 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/types.js +11 -0
- package/dist/workflow/types.js.map +1 -0
- package/package.json +7 -5
- package/src/diagnostics/index.ts +14 -0
- package/src/diagnostics/stack-diagnostic.ts +103 -0
- package/src/index.ts +36 -0
- package/src/workflow/index.ts +24 -0
- package/src/workflow/registry.ts +55 -0
- package/src/workflow/run.ts +58 -0
- package/src/workflow/steps.ts +325 -0
- package/src/workflow/templates.ts +87 -0
- package/src/workflow/types.ts +42 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stack-diagnostic.ts — the generic workflow runner + execution-output types.
|
|
3
|
+
*
|
|
4
|
+
* A workflow is an ordered list of {@link StackStep}s. `runStackDiagnostic`
|
|
5
|
+
* executes them, capturing per-step pass/fail + timing + error and streaming a
|
|
6
|
+
* callback — so the result is an "execution output" timeline a UI renders: run a
|
|
7
|
+
* workflow, watch each step, see exactly which one breaks.
|
|
8
|
+
*
|
|
9
|
+
* The concrete steps (the 7 agent-stack layers, the LLM-creation pipeline) and
|
|
10
|
+
* the configurable workflow/template layer live in `../workflow/` and compile
|
|
11
|
+
* down to `StackStep[]` runnable here. Steps share a {@link StackContext}: `bag`
|
|
12
|
+
* is scratch threaded across steps; `artifacts` is the workflow's OUTPUT (e.g. a
|
|
13
|
+
* trained `.evermind` blob).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type StepStatus = "pass" | "fail" | "skip";
|
|
17
|
+
|
|
18
|
+
/** One step's result — the shape an execution-output timeline renders per row. */
|
|
19
|
+
export interface StackStepResult {
|
|
20
|
+
id: string;
|
|
21
|
+
/** Grouping tag, e.g. "L1".."L7" for the agent-stack layers, or "BUILD". */
|
|
22
|
+
layer: string;
|
|
23
|
+
label: string;
|
|
24
|
+
status: StepStatus;
|
|
25
|
+
ms: number;
|
|
26
|
+
/** Short human detail on success (e.g. "retrieved 3 chunks"). */
|
|
27
|
+
detail?: string;
|
|
28
|
+
/** Failure message when status === "fail". */
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Shared state threaded across steps. */
|
|
33
|
+
export interface StackContext {
|
|
34
|
+
/** Scratch populated/consumed by steps (e.g. the model L1 builds, L7 deploys). */
|
|
35
|
+
bag: Record<string, unknown>;
|
|
36
|
+
/** Workflow OUTPUTS — what the run produces (e.g. `artifacts.evermind` = blob). */
|
|
37
|
+
artifacts: Record<string, unknown>;
|
|
38
|
+
/** Prior step results so far — this IS the observability trace. */
|
|
39
|
+
trace: StackStepResult[];
|
|
40
|
+
/** Optional IDBFactory (Node/fake-indexeddb); browser uses global indexedDB. */
|
|
41
|
+
idbFactory?: unknown;
|
|
42
|
+
now: () => number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface StackStep {
|
|
46
|
+
id: string;
|
|
47
|
+
layer: string;
|
|
48
|
+
label: string;
|
|
49
|
+
/** Throw to fail the step; optionally return a human detail string. */
|
|
50
|
+
run: (ctx: StackContext) => Promise<string | void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RunStackOptions {
|
|
54
|
+
/** Streamed as each step settles — for live execution-output rendering. */
|
|
55
|
+
onStep?: (r: StackStepResult) => void;
|
|
56
|
+
/** Injectable clock for deterministic timing in tests. Default Date.now. */
|
|
57
|
+
now?: () => number;
|
|
58
|
+
/** IDBFactory for Node; omit in the browser. */
|
|
59
|
+
idbFactory?: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface StackDiagnosticResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
steps: StackStepResult[];
|
|
65
|
+
/** The first step that failed (the breaking point), if any. */
|
|
66
|
+
firstFailure?: StackStepResult;
|
|
67
|
+
/** Outputs produced by the run (e.g. `artifacts.evermind` for a build workflow). */
|
|
68
|
+
artifacts: Record<string, unknown>;
|
|
69
|
+
totalMs: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run the steps in order, capturing each result. Never throws — a step that
|
|
74
|
+
* throws becomes a `fail` row, and the run continues so the caller sees the full
|
|
75
|
+
* timeline (and exactly where it broke). Pass `onStep` to stream rows live.
|
|
76
|
+
*/
|
|
77
|
+
export async function runStackDiagnostic(
|
|
78
|
+
steps: StackStep[],
|
|
79
|
+
opts: RunStackOptions = {},
|
|
80
|
+
): Promise<StackDiagnosticResult> {
|
|
81
|
+
const now = opts.now ?? (() => Date.now());
|
|
82
|
+
const ctx: StackContext = { bag: {}, artifacts: {}, trace: [], idbFactory: opts.idbFactory, now };
|
|
83
|
+
const results: StackStepResult[] = [];
|
|
84
|
+
let firstFailure: StackStepResult | undefined;
|
|
85
|
+
const start = now();
|
|
86
|
+
|
|
87
|
+
for (const step of steps) {
|
|
88
|
+
const t0 = now();
|
|
89
|
+
let result: StackStepResult;
|
|
90
|
+
try {
|
|
91
|
+
const detail = await step.run(ctx);
|
|
92
|
+
result = { id: step.id, layer: step.layer, label: step.label, status: "pass", ms: now() - t0, ...(detail ? { detail } : {}) };
|
|
93
|
+
} catch (e) {
|
|
94
|
+
result = { id: step.id, layer: step.layer, label: step.label, status: "fail", ms: now() - t0, error: e instanceof Error ? e.message : String(e) };
|
|
95
|
+
firstFailure ??= result;
|
|
96
|
+
}
|
|
97
|
+
results.push(result);
|
|
98
|
+
ctx.trace.push(result);
|
|
99
|
+
opts.onStep?.(result);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { ok: !firstFailure, steps: results, ...(firstFailure ? { firstFailure } : {}), artifacts: ctx.artifacts, totalMs: now() - start };
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -182,6 +182,42 @@ export type {
|
|
|
182
182
|
Verdict,
|
|
183
183
|
} from './cognition/index.js';
|
|
184
184
|
|
|
185
|
+
// ── Diagnostics (execution-output runner) ──────────────────────────────────────
|
|
186
|
+
export { runStackDiagnostic } from './diagnostics/index.js';
|
|
187
|
+
export type {
|
|
188
|
+
StackStep,
|
|
189
|
+
StackStepResult,
|
|
190
|
+
StackContext,
|
|
191
|
+
StackDiagnosticResult,
|
|
192
|
+
RunStackOptions,
|
|
193
|
+
StepStatus,
|
|
194
|
+
} from './diagnostics/index.js';
|
|
195
|
+
|
|
196
|
+
// ── Workflows (configurable templates + step registry → run + .evermind output) ─
|
|
197
|
+
export {
|
|
198
|
+
runWorkflow,
|
|
199
|
+
compileWorkflow,
|
|
200
|
+
validateWorkflow,
|
|
201
|
+
buildEvermindStackSteps,
|
|
202
|
+
StepTypeRegistry,
|
|
203
|
+
createDefaultRegistry,
|
|
204
|
+
defaultStepRegistry,
|
|
205
|
+
BUILTIN_STEPS,
|
|
206
|
+
AGENTIC_SEVEN_LAYER,
|
|
207
|
+
TRAIN_LLM,
|
|
208
|
+
WORKFLOW_TEMPLATES,
|
|
209
|
+
getTemplate,
|
|
210
|
+
cloneTemplate,
|
|
211
|
+
} from './workflow/index.js';
|
|
212
|
+
export type {
|
|
213
|
+
WorkflowConfig,
|
|
214
|
+
WorkflowStepConfig,
|
|
215
|
+
StepTypeInfo,
|
|
216
|
+
StepFactory,
|
|
217
|
+
WorkflowValidationError,
|
|
218
|
+
RunWorkflowOptions,
|
|
219
|
+
} from './workflow/index.js';
|
|
220
|
+
|
|
185
221
|
// ── Distillation ──────────────────────────────────────────────────────────────
|
|
186
222
|
export { DistillationEngine } from './distillation/DistillationEngine.js';
|
|
187
223
|
export type {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurable agent/LLM workflows — templates, a step-type registry, and a
|
|
3
|
+
* runner that produces an execution-output timeline (+ a trained `.evermind`
|
|
4
|
+
* artifact for build workflows).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { WorkflowConfig, WorkflowStepConfig, StepTypeInfo, StepFactory } from "./types.js";
|
|
8
|
+
export { StepTypeRegistry, createDefaultRegistry, defaultStepRegistry } from "./registry.js";
|
|
9
|
+
export { BUILTIN_STEPS } from "./steps.js";
|
|
10
|
+
export {
|
|
11
|
+
AGENTIC_SEVEN_LAYER,
|
|
12
|
+
TRAIN_LLM,
|
|
13
|
+
WORKFLOW_TEMPLATES,
|
|
14
|
+
getTemplate,
|
|
15
|
+
cloneTemplate,
|
|
16
|
+
} from "./templates.js";
|
|
17
|
+
export {
|
|
18
|
+
compileWorkflow,
|
|
19
|
+
validateWorkflow,
|
|
20
|
+
runWorkflow,
|
|
21
|
+
buildEvermindStackSteps,
|
|
22
|
+
type WorkflowValidationError,
|
|
23
|
+
type RunWorkflowOptions,
|
|
24
|
+
} from "./run.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow/registry.ts — the step-type registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps a step `type` → its factory + metadata. Built-ins are pre-registered;
|
|
5
|
+
* callers (e.g. the agent runtime) can `register` real implementations for the
|
|
6
|
+
* layers they own. Unknown types compile to a failing step so a bad workflow is
|
|
7
|
+
* localized in the timeline rather than crashing the run.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BUILTIN_STEPS } from "./steps.js";
|
|
11
|
+
import type { StepFactory, StepTypeInfo, WorkflowStepConfig } from "./types.js";
|
|
12
|
+
import type { StackStep } from "../diagnostics/stack-diagnostic.js";
|
|
13
|
+
|
|
14
|
+
export class StepTypeRegistry {
|
|
15
|
+
private readonly map = new Map<string, { info: StepTypeInfo; factory: StepFactory }>();
|
|
16
|
+
|
|
17
|
+
register(info: StepTypeInfo, factory: StepFactory): this {
|
|
18
|
+
this.map.set(info.type, { info, factory });
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
has(type: string): boolean {
|
|
23
|
+
return this.map.has(type);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Compile one step config to a runnable step (failing step for unknown types). */
|
|
27
|
+
build(cfg: WorkflowStepConfig): StackStep {
|
|
28
|
+
const entry = this.map.get(cfg.type);
|
|
29
|
+
if (!entry) {
|
|
30
|
+
return {
|
|
31
|
+
id: cfg.id,
|
|
32
|
+
layer: "?",
|
|
33
|
+
label: cfg.label ?? cfg.type,
|
|
34
|
+
run: async () => {
|
|
35
|
+
throw new Error(`unknown step type "${cfg.type}"`);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return entry.factory(cfg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** All registered step types — a workflow-builder palette reads this. */
|
|
43
|
+
types(): StepTypeInfo[] {
|
|
44
|
+
return [...this.map.values()].map((e) => e.info);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createDefaultRegistry(): StepTypeRegistry {
|
|
49
|
+
const r = new StepTypeRegistry();
|
|
50
|
+
for (const { info, factory } of Object.values(BUILTIN_STEPS)) r.register(info, factory);
|
|
51
|
+
return r;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Shared registry seeded with the built-in step types. */
|
|
55
|
+
export const defaultStepRegistry = createDefaultRegistry();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow/run.ts — compile, validate, and run a workflow config.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
runStackDiagnostic,
|
|
7
|
+
type RunStackOptions,
|
|
8
|
+
type StackDiagnosticResult,
|
|
9
|
+
type StackStep,
|
|
10
|
+
} from "../diagnostics/stack-diagnostic.js";
|
|
11
|
+
import { defaultStepRegistry, StepTypeRegistry } from "./registry.js";
|
|
12
|
+
import { AGENTIC_SEVEN_LAYER } from "./templates.js";
|
|
13
|
+
import type { WorkflowConfig } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/** Compile a workflow config to runnable steps (unknown types → failing steps). */
|
|
16
|
+
export function compileWorkflow(config: WorkflowConfig, registry: StepTypeRegistry = defaultStepRegistry): StackStep[] {
|
|
17
|
+
return config.steps.map((s) => registry.build(s));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorkflowValidationError {
|
|
21
|
+
stepId: string;
|
|
22
|
+
type: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Static checks before running: non-empty, unique ids, known types. */
|
|
27
|
+
export function validateWorkflow(config: WorkflowConfig, registry: StepTypeRegistry = defaultStepRegistry): WorkflowValidationError[] {
|
|
28
|
+
const errors: WorkflowValidationError[] = [];
|
|
29
|
+
if (!config.steps || config.steps.length === 0) {
|
|
30
|
+
errors.push({ stepId: "", type: "", message: "workflow has no steps" });
|
|
31
|
+
return errors;
|
|
32
|
+
}
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
for (const s of config.steps) {
|
|
35
|
+
if (seen.has(s.id)) errors.push({ stepId: s.id, type: s.type, message: `duplicate step id "${s.id}"` });
|
|
36
|
+
seen.add(s.id);
|
|
37
|
+
if (!registry.has(s.type)) errors.push({ stepId: s.id, type: s.type, message: `unknown step type "${s.type}"` });
|
|
38
|
+
}
|
|
39
|
+
return errors;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RunWorkflowOptions extends RunStackOptions {
|
|
43
|
+
registry?: StepTypeRegistry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run a workflow config and return its execution-output timeline + artifacts
|
|
48
|
+
* (e.g. `result.artifacts.evermind` is the trained model for a build workflow).
|
|
49
|
+
*/
|
|
50
|
+
export function runWorkflow(config: WorkflowConfig, opts: RunWorkflowOptions = {}): Promise<StackDiagnosticResult> {
|
|
51
|
+
const { registry = defaultStepRegistry, ...runOpts } = opts;
|
|
52
|
+
return runStackDiagnostic(compileWorkflow(config, registry), runOpts);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Back-compat: the 7-layer template compiled to steps (consumed by the e2e test). */
|
|
56
|
+
export function buildEvermindStackSteps(): StackStep[] {
|
|
57
|
+
return compileWorkflow(AGENTIC_SEVEN_LAYER);
|
|
58
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow/steps.ts — the built-in step types.
|
|
3
|
+
*
|
|
4
|
+
* Two families, both compiling to a runnable {@link StackStep}:
|
|
5
|
+
* • Agent-stack layers (L1..L7) — the diagnostic checks (the 7-layer template).
|
|
6
|
+
* • LLM-creation pipeline (BUILD) — train-tokenizer → train-model → evaluate →
|
|
7
|
+
* package, which produces a trained `.evermind` artifact (a custom LLM) into
|
|
8
|
+
* `ctx.artifacts.evermind`.
|
|
9
|
+
*
|
|
10
|
+
* A custom workflow mixes these freely; new step types register the same way.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
EvermindLM,
|
|
15
|
+
EvermindLMTrainer,
|
|
16
|
+
BPETokenizer,
|
|
17
|
+
EvermindModelPackage,
|
|
18
|
+
} from "@seanhogg/builderforce-memory-engine";
|
|
19
|
+
import { EvermindCognition } from "../cognition/index.js";
|
|
20
|
+
import { hybridRetrieve, chunkText } from "../retrieval/index.js";
|
|
21
|
+
import { MemoryStore } from "../memory/MemoryStore.js";
|
|
22
|
+
import type { StackStep } from "../diagnostics/stack-diagnostic.js";
|
|
23
|
+
import type { StepFactory, StepTypeInfo, WorkflowStepConfig } from "./types.js";
|
|
24
|
+
|
|
25
|
+
const KNOWLEDGE =
|
|
26
|
+
"BuilderForce orchestrates many agents through a planning loop. " +
|
|
27
|
+
"The memory layer stores facts as SSM embeddings. " +
|
|
28
|
+
"Deployment runs on Cloudflare Workers and Durable Objects. " +
|
|
29
|
+
"Tools are gated by a capability registry.";
|
|
30
|
+
|
|
31
|
+
let dbSeq = 0;
|
|
32
|
+
|
|
33
|
+
// ── param readers (configurable per step) ──────────────────────────────────────
|
|
34
|
+
function pStr(cfg: WorkflowStepConfig, key: string, def: string): string {
|
|
35
|
+
const v = cfg.params?.[key];
|
|
36
|
+
return typeof v === "string" ? v : def;
|
|
37
|
+
}
|
|
38
|
+
function pNum(cfg: WorkflowStepConfig, key: string, def: number): number {
|
|
39
|
+
const v = cfg.params?.[key];
|
|
40
|
+
return typeof v === "number" && Number.isFinite(v) ? v : def;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mkStep(cfg: WorkflowStepConfig, layer: string, defaultLabel: string, run: StackStep["run"]): StackStep {
|
|
44
|
+
return { id: cfg.id, layer, label: cfg.label ?? defaultLabel, run };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Encode a corpus into next-token training sequences (one per sentence). */
|
|
48
|
+
function corpusToSequences(corpus: string, tok: BPETokenizer): number[][] {
|
|
49
|
+
return corpus
|
|
50
|
+
.split(/(?<=\.)\s+/)
|
|
51
|
+
.map((s) => tok.encode(s.trim()))
|
|
52
|
+
.filter((ids) => ids.length >= 2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Registered {
|
|
56
|
+
info: StepTypeInfo;
|
|
57
|
+
factory: StepFactory;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const BUILTIN_STEPS: Record<string, Registered> = {
|
|
61
|
+
// ── Agent-stack layers (the diagnostic / 7-layer template) ──────────────────
|
|
62
|
+
foundation: {
|
|
63
|
+
info: { type: "foundation", layer: "L1", label: "Foundation Model", description: "EvermindLM + tokenizer generate text" },
|
|
64
|
+
factory: (cfg) =>
|
|
65
|
+
mkStep(cfg, "L1", "Foundation Model — EvermindLM + tokenizer", async (ctx) => {
|
|
66
|
+
const tok = new BPETokenizer();
|
|
67
|
+
tok.train(KNOWLEDGE.repeat(4), { numMerges: 60 });
|
|
68
|
+
const model = new EvermindLM({ vocabSize: tok.vocabSize, dModel: 16, numLayers: 2, hiddenDim: 24, seed: 7 });
|
|
69
|
+
const out = model.generateText("Deployment", tok, { maxNewTokens: 4, temperature: 0 });
|
|
70
|
+
if (typeof out !== "string") throw new Error("model did not produce text");
|
|
71
|
+
if (tok.decode(tok.encode("Cloudflare Workers")) !== "Cloudflare Workers") throw new Error("tokenizer did not round-trip text");
|
|
72
|
+
ctx.bag.model = model;
|
|
73
|
+
ctx.bag.tokenizer = tok;
|
|
74
|
+
return `vocab ${tok.vocabSize}, generated ${out.length} chars`;
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
memory: {
|
|
78
|
+
info: { type: "memory", layer: "L3", label: "Memory", description: "Write-through cognition (replace-on-write)" },
|
|
79
|
+
factory: (cfg) =>
|
|
80
|
+
mkStep(cfg, "L3", "Memory — write-through cognition", async (ctx) => {
|
|
81
|
+
const store = new MemoryStore({ dbName: `wf-${ctx.now()}-${dbSeq++}`, idbFactory: ctx.idbFactory as never });
|
|
82
|
+
const cognition = new EvermindCognition({ store });
|
|
83
|
+
await cognition.commit({ subjectKey: "fact:deploy", content: "deploy target = unknown" });
|
|
84
|
+
const r = await cognition.commit({ subjectKey: "fact:deploy", content: "deploy target = Cloudflare" });
|
|
85
|
+
if (r.verdict !== "supersede") throw new Error(`expected supersede, got ${r.verdict}`);
|
|
86
|
+
ctx.bag.cognition = cognition;
|
|
87
|
+
return `verdict ${r.verdict}`;
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
rag: {
|
|
91
|
+
info: { type: "rag", layer: "L4", label: "RAG", description: "Chunk + hybrid (BM25 + dense) retrieve" },
|
|
92
|
+
factory: (cfg) =>
|
|
93
|
+
mkStep(cfg, "L4", "RAG — chunk + hybrid retrieve", async (ctx) => {
|
|
94
|
+
const candidates = chunkText(KNOWLEDGE, { chunkSize: 70, chunkOverlap: 0 }).map((c, i) => ({ id: `c${i}`, text: c.text }));
|
|
95
|
+
const hits = hybridRetrieve({ text: "where does deployment run" }, candidates, { topK: 2 });
|
|
96
|
+
if (hits.length === 0 || !hits[0]!.text.toLowerCase().includes("cloudflare")) throw new Error("retriever did not surface the deployment passage");
|
|
97
|
+
ctx.bag.candidates = candidates;
|
|
98
|
+
return `top hit: ${hits[0]!.text.slice(0, 40)}…`;
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
tools: {
|
|
102
|
+
info: { type: "tools", layer: "L5", label: "Tools", description: "Capability-gated tool registry" },
|
|
103
|
+
factory: (cfg) =>
|
|
104
|
+
mkStep(cfg, "L5", "Tools — capability-gated registry", async (ctx) => {
|
|
105
|
+
const tools = new Map<string, (a: string) => string>([["shout", (a) => a.toUpperCase()]]);
|
|
106
|
+
if (tools.get("shout")?.("deploy") !== "DEPLOY") throw new Error("tool invocation failed");
|
|
107
|
+
if (tools.get("missing")) throw new Error("ungated tool resolved");
|
|
108
|
+
ctx.bag.tools = tools;
|
|
109
|
+
return "1 tool registered + invoked";
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
orchestration: {
|
|
113
|
+
info: { type: "orchestration", layer: "L2", label: "Orchestration", description: "retrieve→recall→act→generate loop" },
|
|
114
|
+
factory: (cfg) =>
|
|
115
|
+
mkStep(cfg, "L2", "Orchestration — retrieve→recall→act→generate", async (ctx) => {
|
|
116
|
+
const model = ctx.bag.model as EvermindLM | undefined;
|
|
117
|
+
const tokenizer = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
118
|
+
const cognition = ctx.bag.cognition as EvermindCognition | undefined;
|
|
119
|
+
const candidates = ctx.bag.candidates as { id: string; text: string }[] | undefined;
|
|
120
|
+
const tools = ctx.bag.tools as Map<string, (a: string) => string> | undefined;
|
|
121
|
+
if (!model || !tokenizer || !cognition || !candidates || !tools) throw new Error("a prerequisite layer (L1/L3/L4/L5) did not complete");
|
|
122
|
+
const steps: string[] = [];
|
|
123
|
+
const hits = hybridRetrieve({ text: "deployment cloudflare" }, candidates, { topK: 1 });
|
|
124
|
+
steps.push("retrieve");
|
|
125
|
+
await cognition.recall("deployment", 3);
|
|
126
|
+
steps.push("recall");
|
|
127
|
+
tools.get("shout")!(hits[0]?.text ?? "deploy");
|
|
128
|
+
steps.push("tool");
|
|
129
|
+
model.generateText("Deployment", tokenizer, { maxNewTokens: 3, temperature: 0 });
|
|
130
|
+
steps.push("model");
|
|
131
|
+
if (steps.join(",") !== "retrieve,recall,tool,model") throw new Error("loop did not complete all stages");
|
|
132
|
+
return steps.join(" → ");
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
observability: {
|
|
136
|
+
info: { type: "observability", layer: "L6", label: "Observability", description: "Trace captured every prior step" },
|
|
137
|
+
factory: (cfg) =>
|
|
138
|
+
mkStep(cfg, "L6", "Observability — trace captured prior steps", async (ctx) => {
|
|
139
|
+
if (ctx.trace.length === 0) throw new Error("no spans captured");
|
|
140
|
+
for (const s of ctx.trace) {
|
|
141
|
+
if (typeof s.layer !== "string" || typeof s.status !== "string" || s.ms < 0) throw new Error(`malformed span: ${s.id}`);
|
|
142
|
+
}
|
|
143
|
+
return `${ctx.trace.length} spans recorded`;
|
|
144
|
+
}),
|
|
145
|
+
},
|
|
146
|
+
deployment: {
|
|
147
|
+
info: { type: "deployment", layer: "L7", label: "Deployment", description: ".evermind artifact ships + runs identically" },
|
|
148
|
+
factory: (cfg) =>
|
|
149
|
+
mkStep(cfg, "L7", "Deployment — .evermind ships + runs identically", async (ctx) => {
|
|
150
|
+
const model = ctx.bag.model as EvermindLM | undefined;
|
|
151
|
+
const tokenizer = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
152
|
+
if (!model || !tokenizer) throw new Error("no model from L1 to deploy");
|
|
153
|
+
const before = model.generateText("Deployment", tokenizer, { maxNewTokens: 4, temperature: 0 });
|
|
154
|
+
const blob = EvermindModelPackage.fromLM(model, { name: "stack-demo", version: "1.0.0", card: { description: "diagnostic" } }).toBlob();
|
|
155
|
+
const pkg = EvermindModelPackage.fromBlob(blob);
|
|
156
|
+
if (!pkg.validate().ok) throw new Error("packaged artifact failed validation");
|
|
157
|
+
const after = pkg.loadLM().generateText("Deployment", tokenizer, { maxNewTokens: 4, temperature: 0 });
|
|
158
|
+
if (after !== before) throw new Error("deployed instance produced different output");
|
|
159
|
+
return `artifact ${blob.byteLength} bytes, output matches`;
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// ── LLM-creation pipeline (BUILD) — produces a custom .evermind model ────────
|
|
164
|
+
"train-tokenizer": {
|
|
165
|
+
info: { type: "train-tokenizer", layer: "BUILD", label: "Train tokenizer", description: "Learn a BPE vocab from your corpus (params: corpus, numMerges)" },
|
|
166
|
+
factory: (cfg) =>
|
|
167
|
+
mkStep(cfg, "BUILD", "Train tokenizer (BPE)", async (ctx) => {
|
|
168
|
+
const corpus = pStr(cfg, "corpus", KNOWLEDGE.repeat(4));
|
|
169
|
+
const numMerges = pNum(cfg, "numMerges", 100);
|
|
170
|
+
const tok = new BPETokenizer();
|
|
171
|
+
tok.train(corpus, { numMerges });
|
|
172
|
+
ctx.bag.tokenizer = tok;
|
|
173
|
+
ctx.bag.corpus = corpus;
|
|
174
|
+
return `vocab ${tok.vocabSize} (${numMerges} merges)`;
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
"train-model": {
|
|
178
|
+
info: { type: "train-model", layer: "BUILD", label: "Train model", description: "Train an EvermindLM on the corpus (params: dModel, numLayers, hiddenDim, epochs, lr)" },
|
|
179
|
+
factory: (cfg) =>
|
|
180
|
+
mkStep(cfg, "BUILD", "Train model (EvermindLM)", async (ctx) => {
|
|
181
|
+
const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
182
|
+
if (!tok) throw new Error("no tokenizer — add a 'train-tokenizer' step first");
|
|
183
|
+
const corpus = pStr(cfg, "corpus", (ctx.bag.corpus as string) ?? KNOWLEDGE.repeat(4));
|
|
184
|
+
const sequences = corpusToSequences(corpus, tok);
|
|
185
|
+
if (sequences.length === 0) throw new Error("corpus produced no trainable sequences");
|
|
186
|
+
const model = new EvermindLM({
|
|
187
|
+
vocabSize: tok.vocabSize,
|
|
188
|
+
dModel: pNum(cfg, "dModel", 16),
|
|
189
|
+
numLayers: pNum(cfg, "numLayers", 2),
|
|
190
|
+
hiddenDim: pNum(cfg, "hiddenDim", 24),
|
|
191
|
+
seed: pNum(cfg, "seed", 7),
|
|
192
|
+
});
|
|
193
|
+
const epochs = pNum(cfg, "epochs", 40);
|
|
194
|
+
const history = new EvermindLMTrainer(model, { lr: pNum(cfg, "lr", 0.03), epochs }).fit(sequences);
|
|
195
|
+
ctx.bag.model = model;
|
|
196
|
+
// Surface the loss curve so a `convergence` diagnostic / observability can
|
|
197
|
+
// assert training actually worked (it was previously discarded).
|
|
198
|
+
ctx.bag.trainingHistory = history;
|
|
199
|
+
const last = history.at(-1) ?? 0;
|
|
200
|
+
return `trained ${epochs} epochs over ${sequences.length} seqs, loss ${last.toFixed(3)}`;
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
evaluate: {
|
|
204
|
+
info: { type: "evaluate", layer: "BUILD", label: "Evaluate", description: "Sanity-check the model generates text (params: prompt)" },
|
|
205
|
+
factory: (cfg) =>
|
|
206
|
+
mkStep(cfg, "BUILD", "Evaluate model", async (ctx) => {
|
|
207
|
+
const model = ctx.bag.model as EvermindLM | undefined;
|
|
208
|
+
const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
209
|
+
if (!model || !tok) throw new Error("no trained model — add 'train-model' first");
|
|
210
|
+
const prompt = pStr(cfg, "prompt", "The");
|
|
211
|
+
const out = model.generateText(prompt, tok, { maxNewTokens: pNum(cfg, "maxNewTokens", 6), temperature: 0 });
|
|
212
|
+
if (typeof out !== "string") throw new Error("evaluation generation failed");
|
|
213
|
+
ctx.bag.sample = out;
|
|
214
|
+
return `sample: "${out.slice(0, 40)}"`;
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
"dataset-quality": {
|
|
218
|
+
info: { type: "dataset-quality", layer: "BUILD", label: "Dataset quality", description: "Validate the corpus is trainable before spending epochs (params: minSequences, minWords, maxDuplicateRatio)" },
|
|
219
|
+
factory: (cfg) =>
|
|
220
|
+
mkStep(cfg, "BUILD", "Dataset quality gate", async (ctx) => {
|
|
221
|
+
const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
222
|
+
if (!tok) throw new Error("no tokenizer — add a 'train-tokenizer' step first");
|
|
223
|
+
const corpus = pStr(cfg, "corpus", (ctx.bag.corpus as string) ?? "");
|
|
224
|
+
const words = corpus.trim().split(/\s+/).filter(Boolean).length;
|
|
225
|
+
const sequences = corpusToSequences(corpus, tok);
|
|
226
|
+
const seqLens = sequences.map((s) => s.length);
|
|
227
|
+
const avgSeqLen = seqLens.length ? seqLens.reduce((a, b) => a + b, 0) / seqLens.length : 0;
|
|
228
|
+
const uniqueSeqs = new Set(sequences.map((s) => s.join(","))).size;
|
|
229
|
+
const duplicateRatio = sequences.length ? 1 - uniqueSeqs / sequences.length : 1;
|
|
230
|
+
|
|
231
|
+
const minSequences = pNum(cfg, "minSequences", 3);
|
|
232
|
+
const minWords = pNum(cfg, "minWords", 20);
|
|
233
|
+
const maxDuplicateRatio = pNum(cfg, "maxDuplicateRatio", 0.7);
|
|
234
|
+
|
|
235
|
+
const metrics = { words, sequences: sequences.length, avgSeqLen: Number(avgSeqLen.toFixed(1)), duplicateRatio: Number(duplicateRatio.toFixed(2)) };
|
|
236
|
+
ctx.bag.datasetMetrics = metrics;
|
|
237
|
+
|
|
238
|
+
if (words < minWords) throw new Error(`corpus too small: ${words} words < ${minWords}`);
|
|
239
|
+
if (sequences.length < minSequences) throw new Error(`too few trainable sequences: ${sequences.length} < ${minSequences} (need more sentences)`);
|
|
240
|
+
if (duplicateRatio > maxDuplicateRatio) throw new Error(`corpus too repetitive: ${(duplicateRatio * 100) | 0}% duplicate sequences > ${(maxDuplicateRatio * 100) | 0}%`);
|
|
241
|
+
return `${words} words, ${sequences.length} seqs (avg ${metrics.avgSeqLen} tok), ${(duplicateRatio * 100) | 0}% dup`;
|
|
242
|
+
}),
|
|
243
|
+
},
|
|
244
|
+
convergence: {
|
|
245
|
+
info: { type: "convergence", layer: "BUILD", label: "Convergence", description: "Assert the model actually learned — training loss decreased (params: minDrop)" },
|
|
246
|
+
factory: (cfg) =>
|
|
247
|
+
mkStep(cfg, "BUILD", "Training convergence", async (ctx) => {
|
|
248
|
+
const history = ctx.bag.trainingHistory as number[] | undefined;
|
|
249
|
+
if (!history || history.length === 0) throw new Error("no training history — run 'train-model' first");
|
|
250
|
+
const first = history[0]!;
|
|
251
|
+
const last = history.at(-1)!;
|
|
252
|
+
if (!Number.isFinite(first) || !Number.isFinite(last)) throw new Error(`loss is not finite (first=${first}, last=${last}) — training diverged`);
|
|
253
|
+
const minDrop = pNum(cfg, "minDrop", 0); // require at least this absolute loss drop
|
|
254
|
+
if (last >= first - minDrop) throw new Error(`loss did not decrease: ${first.toFixed(3)} → ${last.toFixed(3)} (model did not learn — raise epochs/lr)`);
|
|
255
|
+
ctx.bag.converged = true;
|
|
256
|
+
return `loss ${first.toFixed(3)} → ${last.toFixed(3)} (−${(first - last).toFixed(3)})`;
|
|
257
|
+
}),
|
|
258
|
+
},
|
|
259
|
+
"generate-check": {
|
|
260
|
+
info: { type: "generate-check", layer: "BUILD", label: "Generation check", description: "Validate the model generates non-empty, reproducible text (params: prompt, seed, temperature)" },
|
|
261
|
+
factory: (cfg) =>
|
|
262
|
+
mkStep(cfg, "BUILD", "Generation quality + determinism", async (ctx) => {
|
|
263
|
+
const model = ctx.bag.model as EvermindLM | undefined;
|
|
264
|
+
const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
265
|
+
if (!model || !tok) throw new Error("no trained model — add 'train-model' first");
|
|
266
|
+
const prompt = pStr(cfg, "prompt", "The");
|
|
267
|
+
const maxNewTokens = pNum(cfg, "maxNewTokens", 8);
|
|
268
|
+
const seed = pNum(cfg, "seed", 1234);
|
|
269
|
+
const temperature = pNum(cfg, "temperature", 0.8);
|
|
270
|
+
// Same seed + temperature MUST reproduce — serving relies on it.
|
|
271
|
+
const a = model.generateText(prompt, tok, { maxNewTokens, temperature, seed });
|
|
272
|
+
const b = model.generateText(prompt, tok, { maxNewTokens, temperature, seed });
|
|
273
|
+
if (a !== b) throw new Error("sampling is non-deterministic for a fixed seed — serving would be unreproducible");
|
|
274
|
+
const greedy = model.generateText(prompt, tok, { maxNewTokens, temperature: 0 });
|
|
275
|
+
if (typeof greedy !== "string" || greedy.trim().length === 0) throw new Error("model produced empty output");
|
|
276
|
+
ctx.bag.sample = greedy;
|
|
277
|
+
return `deterministic@seed ${seed}; sample "${greedy.slice(0, 40)}"`;
|
|
278
|
+
}),
|
|
279
|
+
},
|
|
280
|
+
roundtrip: {
|
|
281
|
+
info: { type: "roundtrip", layer: "BUILD", label: "Deploy round-trip", description: "Package the TRAINED model → load → generate; output must match (the real serve smoke test)" },
|
|
282
|
+
factory: (cfg) =>
|
|
283
|
+
mkStep(cfg, "BUILD", "Deploy round-trip (.evermind)", async (ctx) => {
|
|
284
|
+
const model = ctx.bag.model as EvermindLM | undefined;
|
|
285
|
+
const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
286
|
+
if (!model || !tok) throw new Error("no trained model — add 'train-model' first");
|
|
287
|
+
const prompt = pStr(cfg, "prompt", "The");
|
|
288
|
+
const maxNewTokens = pNum(cfg, "maxNewTokens", 6);
|
|
289
|
+
const before = model.generateText(prompt, tok, { maxNewTokens, temperature: 0 });
|
|
290
|
+
const blob = EvermindModelPackage.fromLM(model, {
|
|
291
|
+
name: pStr(cfg, "name", "custom-evermind"),
|
|
292
|
+
version: pStr(cfg, "version", "1.0.0"),
|
|
293
|
+
card: { description: pStr(cfg, "description", "round-trip validated model") },
|
|
294
|
+
}).toBlob();
|
|
295
|
+
const pkg = EvermindModelPackage.fromBlob(blob);
|
|
296
|
+
const v = pkg.validate();
|
|
297
|
+
if (!v.ok) throw new Error(`packaged artifact failed validation: ${v.errors.join("; ")}`);
|
|
298
|
+
const after = pkg.loadLM().generateText(prompt, tok, { maxNewTokens, temperature: 0 });
|
|
299
|
+
if (after !== before) throw new Error("served model produced different output than the trained model");
|
|
300
|
+
ctx.artifacts.evermind = blob;
|
|
301
|
+
ctx.artifacts.tokenizer = { vocab: Object.fromEntries(tok.vocab), merges: [...tok.merges.keys()] };
|
|
302
|
+
return `artifact ${blob.byteLength} bytes; trained vs served output matches`;
|
|
303
|
+
}),
|
|
304
|
+
},
|
|
305
|
+
package: {
|
|
306
|
+
info: { type: "package", layer: "BUILD", label: "Package", description: "Bundle the trained model into a portable .evermind artifact (params: name, version)" },
|
|
307
|
+
factory: (cfg) =>
|
|
308
|
+
mkStep(cfg, "BUILD", "Package .evermind artifact", async (ctx) => {
|
|
309
|
+
const model = ctx.bag.model as EvermindLM | undefined;
|
|
310
|
+
if (!model) throw new Error("no trained model — add 'train-model' first");
|
|
311
|
+
const blob = EvermindModelPackage.fromLM(model, {
|
|
312
|
+
name: pStr(cfg, "name", "custom-evermind"),
|
|
313
|
+
version: pStr(cfg, "version", "1.0.0"),
|
|
314
|
+
card: { description: pStr(cfg, "description", "workflow-built model") },
|
|
315
|
+
}).toBlob();
|
|
316
|
+
ctx.artifacts.evermind = blob;
|
|
317
|
+
// Ship the tokenizer alongside so the deployed model has text I/O.
|
|
318
|
+
const tok = ctx.bag.tokenizer as BPETokenizer | undefined;
|
|
319
|
+
if (tok) {
|
|
320
|
+
ctx.artifacts.tokenizer = { vocab: Object.fromEntries(tok.vocab), merges: [...tok.merges.keys()] };
|
|
321
|
+
}
|
|
322
|
+
return `packaged ${blob.byteLength} bytes${tok ? " + tokenizer" : ""}`;
|
|
323
|
+
}),
|
|
324
|
+
},
|
|
325
|
+
};
|