jev-web 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bekko.mjs ADDED
@@ -0,0 +1,391 @@
1
+ /**
2
+ * bekko.mjs — Bekko System One v0 family (Ettin reranker cross-encoder).
3
+ *
4
+ * Mirrors the reference browser runtime (hotchpotch/bekko-system-one, MIT,
5
+ * browser/src/core.js + browser/src/decision.js):
6
+ *
7
+ * query: [cls] Instruction: {instruction}\nState: {state} [sep]
8
+ * (instruction and state split the query budget 50/50, with
9
+ * unused capacity spilling over to the other side)
10
+ * candidates: "Candidate: {id}: {description}" (+ [sep]), one per option
11
+ * graph: prefix_ids, prefix_mask, doc_ids, doc_mask, owners
12
+ * -> logits [N, T] (one column per task: choice, noul, score)
13
+ * decode: softmax over the task column's candidate logits.
14
+ *
15
+ * The browser export used here is hotchpotch/bekko-system-one-v0-17m
16
+ * (onnx_browser/, pinned by revision) — 29 MB, the smallest of the family.
17
+ * Larger siblings: v0-68m and v0-400m.
18
+ */
19
+
20
+ import { normalizeQuestions } from "./questions.mjs";
21
+ import { fetchCachedBytes } from "./laya.mjs";
22
+
23
+ export const BEKKO_DEFAULT_MODEL = "hotchpotch/bekko-system-one-v0-17m";
24
+ export const BEKKO_DEFAULT_REVISION = "b886a1f9b91f4e8368d7830080d52d955e2c8dfa";
25
+ export const BEKKO_DEFAULT_SUBDIR = "onnx_browser";
26
+ export const BEKKO_68M_MODEL = "hotchpotch/bekko-system-one-v0-68m";
27
+ export const BEKKO_68M_REVISION = "ab7685f23e5edbc1acb12ced4f2c4e12591efa69";
28
+ export const BEKKO_400M_MODEL = "hotchpotch/bekko-system-one-v0-400m";
29
+ export const BEKKO_400M_REVISION = "1960df5602bd93cc8d336fdebd9fb68a30926e13";
30
+
31
+ // Candidate batching heuristic from the reference runtime: bound the
32
+ // candidate-attention growth per forward; not a total RAM limit.
33
+ export const BEKKO_ATTENTION_BUDGET = 1_048_576;
34
+
35
+ export const BEKKO_DEFAULT_YES = "Yes, the condition in the question holds.";
36
+ export const BEKKO_DEFAULT_NO = "No, the condition in the question does not hold.";
37
+ // Rendered on both sides of a noul so the two candidates read like the
38
+ // reference runtime's defaults.
39
+ export const BEKKO_NOUL_DESCRIPTIONS = { true: BEKKO_DEFAULT_YES, false: BEKKO_DEFAULT_NO };
40
+
41
+ /** Flatten a state or instruction into text, stably. */
42
+ export function renderBekkoContent(content) {
43
+ if (typeof content === "string") return content.trim();
44
+ return JSON.stringify(content);
45
+ }
46
+
47
+ /**
48
+ * Render one normalized question to a Bekko request (mirrors decision.js
49
+ * renderDecision): the task, the instruction, the JSON-encoded state and the
50
+ * candidate list. Also returns the metadata jev-web needs to map the answer
51
+ * back: `labels` (answer keys in candidate order) and `kind`.
52
+ */
53
+ export function renderBekkoRequest(question, stateText) {
54
+ const instruction = renderBekkoContent(question.instructions);
55
+ const kind = question.type;
56
+ if (kind === "choice") {
57
+ const criteria = question.criteria ?? {};
58
+ const candidates = question.options.map((label) => {
59
+ const desc = String(criteria[label] ?? label);
60
+ return { id: label, text: `Candidate: ${label}: ${desc}` };
61
+ });
62
+ return {
63
+ task: "choice",
64
+ instruction,
65
+ state: JSON.stringify(stateText),
66
+ layout: "instruction_state",
67
+ candidates,
68
+ labels: [...question.options],
69
+ kind,
70
+ };
71
+ }
72
+ if (kind === "noul") {
73
+ const criteria = question.criteria ?? {};
74
+ const yesDesc = String(criteria.true ?? criteria.yes ?? BEKKO_NOUL_DESCRIPTIONS.true);
75
+ const noDesc = String(criteria.false ?? criteria.no ?? BEKKO_NOUL_DESCRIPTIONS.false);
76
+ return {
77
+ task: "noul",
78
+ instruction,
79
+ state: JSON.stringify({ noul: { yes: yesDesc, no: noDesc }, state: stateText }),
80
+ layout: "instruction_state",
81
+ candidates: [
82
+ { id: "true", text: `Candidate: true: ${yesDesc}` },
83
+ { id: "false", text: `Candidate: false: ${noDesc}` },
84
+ ],
85
+ // answer keys parallel to candidates: candidate 0 ("true") is the
86
+ // question's "yes" option, candidate 1 ("false") is the "no" option.
87
+ labels: [question.options[1], question.options[0]],
88
+ kind,
89
+ };
90
+ }
91
+ if (kind === "score") {
92
+ const candidates = question.options.map((rubric, i) => ({
93
+ id: String(i),
94
+ text: `Candidate: ${i}: ${rubric}`,
95
+ value: i,
96
+ }));
97
+ return {
98
+ task: "score",
99
+ instruction,
100
+ state: JSON.stringify(stateText),
101
+ layout: "instruction_state",
102
+ candidates,
103
+ labels: question.options.map((_, i) => String(i)),
104
+ kind,
105
+ };
106
+ }
107
+ throw new TypeError(`bekko: unsupported question type ${question.type}`);
108
+ }
109
+
110
+ /**
111
+ * Tokenise a Bekko request (mirrors core.js tokenize): the shared query
112
+ * prefix plus one tokenised candidate per option. Returns the raw id arrays
113
+ * and whether the instruction/state were truncated to the query budget.
114
+ */
115
+ export function tokenizeBekkoRequest(request, encode, manifest, system = "") {
116
+ const max = manifest.query_length;
117
+ const systemIds = encode(system.trim() ? `${system}\n\n` : "").slice(0, max);
118
+ const instructionIds = encode(request.instruction).slice(0, max);
119
+ const stateIds = encode(request.state).slice(0, max);
120
+ const im = encode("Instruction: ");
121
+ const sm = encode("State: ");
122
+ const sep = encode("\n");
123
+ const budget = max - 2 - systemIds.length - im.length - sm.length - sep.length;
124
+ if (budget < 2) {
125
+ throw new RangeError("bekko: system prompt leaves fewer than two content tokens");
126
+ }
127
+ let ni = Math.min(instructionIds.length, Math.ceil(budget / 2));
128
+ let ns = Math.min(stateIds.length, Math.floor(budget / 2));
129
+ ni += Math.min(instructionIds.length - ni, budget - ni - ns);
130
+ ns += Math.min(stateIds.length - ns, budget - ni - ns);
131
+ const truncated = ni < instructionIds.length || ns < stateIds.length;
132
+ const ins = [...im, ...instructionIds.slice(0, ni)];
133
+ const ctx = [...sm, ...stateIds.slice(0, ns)];
134
+ const body = request.layout === "state_instruction"
135
+ ? [...ctx, ...sep, ...ins]
136
+ : [...ins, ...sep, ...ctx];
137
+ return {
138
+ prefixIds: [manifest.cls_token_id, ...systemIds, ...body, manifest.sep_token_id],
139
+ docIds: request.candidates.map(
140
+ (c) => [...encode(c.text).slice(0, manifest.document_length - 1), manifest.sep_token_id],
141
+ ),
142
+ truncated,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Build the ONNX feeds for one question (mirrors core.js feedsFor): the
148
+ * shared prefix plus a batch of candidate documents.
149
+ */
150
+ export function collateBekkoDocs(prefixIds, docIds, manifest, ort) {
151
+ const pad = BigInt(manifest.pad_token_id);
152
+ const width = Math.max(...docIds.map((row) => row.length));
153
+ const ids = new BigInt64Array(docIds.length * width).fill(pad);
154
+ const mask = new Uint8Array(ids.length);
155
+ docIds.forEach((row, i) => row.forEach((id, j) => {
156
+ ids[i * width + j] = BigInt(id);
157
+ mask[i * width + j] = 1;
158
+ }));
159
+ const pIds = new BigInt64Array(prefixIds.length);
160
+ const pMask = new Uint8Array(prefixIds.length);
161
+ prefixIds.forEach((id, j) => { pIds[j] = BigInt(id); pMask[j] = 1; });
162
+ return {
163
+ prefix_ids: new ort.Tensor("int64", pIds, [1, prefixIds.length]),
164
+ prefix_mask: new ort.Tensor("bool", pMask, [1, prefixIds.length]),
165
+ doc_ids: new ort.Tensor("int64", ids, [docIds.length, width]),
166
+ doc_mask: new ort.Tensor("bool", mask, [docIds.length, width]),
167
+ owners: new ort.Tensor("int64", new BigInt64Array(docIds.length), [docIds.length]),
168
+ };
169
+ }
170
+
171
+ /** Softmax over candidate logits (mirrors core.js interpret). */
172
+ export function bekkoSoftmax(logits) {
173
+ if (!Array.isArray(logits) || logits.length === 0) {
174
+ throw new TypeError("bekkoSoftmax: logits must be a non-empty array");
175
+ }
176
+ const max = Math.max(...logits);
177
+ const exps = logits.map((x) => Math.exp(Number(x) - max));
178
+ const sum = exps.reduce((a, b) => a + b, 0);
179
+ return exps.map((e) => e / sum);
180
+ }
181
+
182
+ /**
183
+ * Decode one question's task-column logits into a jev-web answer
184
+ * (mirrors core.js interpret, mapped onto the shared answer shapes).
185
+ *
186
+ * @param {{logits:ArrayLike<number>, task:string, request:object}} spec
187
+ */
188
+ export function bekkoAnswerFromLogits({ logits, task, request }) {
189
+ const probs = bekkoSoftmax(Array.from(logits, Number));
190
+ if (probs.length !== request.candidates.length) {
191
+ throw new TypeError(
192
+ `bekko: got ${probs.length} logits for ${request.candidates.length} candidates`);
193
+ }
194
+ const probabilities = Object.fromEntries(request.labels.map((label, i) => [label, probs[i]]));
195
+ const best = probs.indexOf(Math.max(...probs));
196
+
197
+ if (task === "choice") {
198
+ return {
199
+ type: "choice",
200
+ choice: request.labels[best],
201
+ index: best,
202
+ probabilities,
203
+ confidence: Math.max(...probs),
204
+ };
205
+ }
206
+ if (task === "noul") {
207
+ const yes = probs[request.candidates.findIndex((c) => c.id === "true")];
208
+ // labels are [yesOption, noOption] (candidate order); re-key in the
209
+ // question's option order for the shared answer contract.
210
+ const [yesLabel, noLabel] = request.labels;
211
+ return {
212
+ type: "noul",
213
+ noul: yes,
214
+ probabilities: { [noLabel]: 1 - yes, [yesLabel]: yes },
215
+ confidence: Math.max(yes, 1 - yes),
216
+ };
217
+ }
218
+ // score: expected value over the numeric candidate values.
219
+ const values = request.candidates.map((c) => c.value);
220
+ const score = probs.reduce((acc, p, i) => acc + p * values[i], 0);
221
+ const lo = Math.min(...values);
222
+ const hi = Math.max(...values);
223
+ return {
224
+ type: "score",
225
+ score,
226
+ level: best,
227
+ normalizedScore: hi === lo ? 0 : (score - lo) / (hi - lo),
228
+ probabilities,
229
+ confidence: Math.max(...probs),
230
+ };
231
+ }
232
+
233
+ const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
234
+
235
+ const hfUrl = (model, revision, ...parts) =>
236
+ `https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${parts.join("/")}`;
237
+
238
+ /**
239
+ * Create a Bekko System One decider.
240
+ *
241
+ * @param {object} [opts]
242
+ * model/revision/subdir — pinned browser ONNX export (or your own mirror)
243
+ * manifestUrl/modelUrl — override the resolved file URLs
244
+ * ort — injected onnxruntime-web (lazy import otherwise)
245
+ * transformers — injected @huggingface/transformers (tokenizer only)
246
+ * tokenizerModel/tokenizerRevision — override the tokenizer source
247
+ * wasmPaths — ORT wasm/loader directory (self-hosted builds)
248
+ * device — "auto" | "wasm" | "webgpu"
249
+ * sessionFactory — (url, sessionOptions) => Promise<InferenceSession>
250
+ * attentionBudget — candidate batching budget (reference: 1M pairs)
251
+ */
252
+ export async function createBekkoDecider({
253
+ model = BEKKO_DEFAULT_MODEL,
254
+ revision = BEKKO_DEFAULT_REVISION,
255
+ subdir = BEKKO_DEFAULT_SUBDIR,
256
+ manifestUrl = null,
257
+ modelUrl = null,
258
+ ort = null,
259
+ transformers = null,
260
+ fetchImpl = fetch,
261
+ tokenizerModel = null,
262
+ tokenizerRevision = null,
263
+ wasmPaths = null,
264
+ device = "auto",
265
+ sessionFactory = null,
266
+ onProgress = null,
267
+ system = "",
268
+ attentionBudget = BEKKO_ATTENTION_BUDGET,
269
+ scope = globalThis,
270
+ } = {}) {
271
+ const ortMod = ort ?? (await import("onnxruntime-web"));
272
+ const tf = transformers ?? (await import("@huggingface/transformers"));
273
+ if (wasmPaths) {
274
+ try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
275
+ }
276
+
277
+ const resolvedDevice = device !== "auto"
278
+ ? device
279
+ : (scope?.navigator?.gpu ? "webgpu" : "wasm");
280
+ const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
281
+
282
+ const report = (phase) => (p) =>
283
+ onProgress?.({ phase, file: p?.file ?? null, loaded: p?.loaded ?? null, total: p?.total ?? null });
284
+
285
+ const manifestRes = await fetchImpl(manifestUrl ?? hfUrl(model, revision, subdir, "manifest.json"));
286
+ if (!manifestRes.ok) {
287
+ throw new Error(`bekko: manifest fetch failed (${manifestRes.status})`);
288
+ }
289
+ const manifest = await manifestRes.json();
290
+ const taskColumns = manifest.tasks;
291
+
292
+ const tok = await tf.AutoTokenizer.from_pretrained(tokenizerModel ?? model, {
293
+ revision: tokenizerRevision ?? (tokenizerModel ? undefined : revision),
294
+ progress_callback: report("tokenizer"),
295
+ });
296
+ const encode = (text) =>
297
+ Array.from(tok(text, { add_special_tokens: false }).input_ids.data, Number);
298
+
299
+ const url = modelUrl ?? hfUrl(model, revision, subdir, manifest.model_file ?? "model.onnx");
300
+ const createSession = sessionFactory
301
+ ?? (async (u, opts) => {
302
+ const modelBytes = await fetchCachedBytes(u, {
303
+ fetchImpl, onProgress: (p) => onProgress?.(p), persist: true,
304
+ });
305
+ return ortMod.InferenceSession.create(modelBytes, opts);
306
+ });
307
+ let session;
308
+ let activeDevice = resolvedDevice;
309
+ try {
310
+ session = await createSession(url, {
311
+ executionProviders,
312
+ graphOptimizationLevel: "all",
313
+ });
314
+ } catch (e) {
315
+ if (resolvedDevice === "webgpu") {
316
+ onProgress?.({ phase: `webgpu unavailable (${e?.message ?? e}) — retrying on wasm` });
317
+ session = await createSession(url, { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
318
+ activeDevice = "wasm";
319
+ } else {
320
+ throw e;
321
+ }
322
+ }
323
+
324
+ return {
325
+ info: {
326
+ family: "bekko",
327
+ model,
328
+ revision,
329
+ device: activeDevice,
330
+ tasks: [...taskColumns],
331
+ queryLength: manifest.query_length,
332
+ documentLength: manifest.document_length,
333
+ },
334
+ async decide(state, questions) {
335
+ const t0 = now();
336
+ const normalized = normalizeQuestions(questions);
337
+ const stateText = renderBekkoContent(state);
338
+ const answers = [];
339
+ const prompts = [];
340
+ let truncated = false;
341
+ let length = 0;
342
+ let runMs = 0;
343
+
344
+ for (const q of normalized) {
345
+ const request = renderBekkoRequest(q, stateText);
346
+ const column = taskColumns.indexOf(request.task);
347
+ if (column < 0) throw new Error(`bekko: task ${request.task} absent from this model`);
348
+ const tokens = tokenizeBekkoRequest(request, encode, manifest, system);
349
+ truncated = truncated || tokens.truncated;
350
+ length += tokens.prefixIds.length + tokens.docIds.reduce((a, d) => a + d.length, 0);
351
+ prompts.push({
352
+ query: request.instruction,
353
+ candidates: request.candidates.map((c) => c.text),
354
+ });
355
+
356
+ // Batch candidates by the reference attention budget.
357
+ const docLen = Math.max(...tokens.docIds.map((d) => d.length));
358
+ const attentionPairs = docLen * (tokens.prefixIds.length + docLen);
359
+ const batchSize = Math.max(1, Math.floor(attentionBudget / attentionPairs));
360
+ const logits = [];
361
+ for (let offset = 0; offset < tokens.docIds.length; offset += batchSize) {
362
+ const docs = tokens.docIds.slice(offset, offset + batchSize);
363
+ const feeds = collateBekkoDocs(tokens.prefixIds, docs, manifest, ortMod);
364
+ const tRun = now();
365
+ const out = await session.run(feeds);
366
+ runMs += now() - tRun;
367
+ const outLogits = out.logits ?? out[Object.keys(out)[0]];
368
+ const T = taskColumns.length;
369
+ if (outLogits.dims.length !== 2 || outLogits.dims[0] !== docs.length || outLogits.dims[1] !== T) {
370
+ throw new Error("bekko: invalid model logits shape");
371
+ }
372
+ for (let r = 0; r < docs.length; r++) {
373
+ logits.push(Number(outLogits.data[r * T + column]));
374
+ }
375
+ }
376
+ answers.push(bekkoAnswerFromLogits({ logits, task: request.task, request }));
377
+ }
378
+
379
+ return {
380
+ answers,
381
+ truncated,
382
+ length,
383
+ prompts,
384
+ timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
385
+ };
386
+ },
387
+ async dispose() {
388
+ try { await session.release?.(); } catch { /* optional */ }
389
+ },
390
+ };
391
+ }
package/src/index.mjs CHANGED
@@ -28,6 +28,47 @@ export {
28
28
  LAYA_TEMP_MIN,
29
29
  LAYA_TEMP_MAX,
30
30
  } from "./laya.mjs";
31
+ export {
32
+ createStrandsDecider,
33
+ renderStrandsQuestion,
34
+ renderStrandsState,
35
+ renderStrandsContent,
36
+ buildStrandsBatch,
37
+ collateStrandsItems,
38
+ strandsOptionTokenIndices,
39
+ strandsAnswersFromLogits,
40
+ strandsChoiceConfidence,
41
+ strandsScoreConfidence,
42
+ STRANDS_DEFAULT_MODEL,
43
+ STRANDS_DEFAULT_REVISION,
44
+ STRANDS_DEFAULT_FILE,
45
+ STRANDS_TEMPERATURE,
46
+ STRANDS_TEMPERATURE_BY_KIND,
47
+ STRANDS_ORDINAL_SMOOTHING,
48
+ STRANDS_DEFAULT_MAX_LEN,
49
+ STRANDS_MAX_QUESTION_FRACTION,
50
+ STRANDS_KIND,
51
+ } from "./strands.mjs";
52
+ export {
53
+ createBekkoDecider,
54
+ renderBekkoContent,
55
+ renderBekkoRequest,
56
+ tokenizeBekkoRequest,
57
+ collateBekkoDocs,
58
+ bekkoSoftmax,
59
+ bekkoAnswerFromLogits,
60
+ BEKKO_DEFAULT_MODEL,
61
+ BEKKO_DEFAULT_REVISION,
62
+ BEKKO_DEFAULT_SUBDIR,
63
+ BEKKO_68M_MODEL,
64
+ BEKKO_68M_REVISION,
65
+ BEKKO_400M_MODEL,
66
+ BEKKO_400M_REVISION,
67
+ BEKKO_ATTENTION_BUDGET,
68
+ BEKKO_DEFAULT_YES,
69
+ BEKKO_DEFAULT_NO,
70
+ BEKKO_NOUL_DESCRIPTIONS,
71
+ } from "./bekko.mjs";
31
72
  export {
32
73
  DEFAULT_MODEL,
33
74
  DEFAULT_REVISION,
package/src/laya.mjs CHANGED
@@ -412,17 +412,22 @@ async function readBytesWithProgress(res, url, onProgress) {
412
412
  * has no external data (404) or the fetch fails.
413
413
  */
414
414
  export async function fetchOnnxExternalData(url, { fetchImpl = fetch, onProgress = null, force = false } = {}) {
415
- const dataUrl = `${url}.data`;
416
- const path = `${String(url).split("/").pop()}.data`;
417
- try {
418
- // Probe cheaply first: a 404 means the model has no external data.
419
- const head = await fetchImpl(dataUrl, { method: "HEAD" }).catch(() => null);
420
- if (head && !head.ok) return null;
421
- const data = await fetchCachedBytes(dataUrl, { fetchImpl, onProgress, force, persist: true });
422
- return { path, data };
423
- } catch {
424
- return null;
415
+ // Probe `<model>.onnx.data` (laya web-q8 layout) then `<model>.onnx_data`
416
+ // (onnx-community layout); a 404 on both means the model has no external data.
417
+ for (const suffix of [".data", "_data"]) {
418
+ const dataUrl = `${url}${suffix}`;
419
+ const path = `${String(url).split("/").pop()}${suffix}`;
420
+ try {
421
+ // Probe cheaply first: a 404 means "try the next suffix".
422
+ const head = await fetchImpl(dataUrl, { method: "HEAD" }).catch(() => null);
423
+ if (head && !head.ok) continue;
424
+ const data = await fetchCachedBytes(dataUrl, { fetchImpl, onProgress, force, persist: true });
425
+ return { path, data };
426
+ } catch {
427
+ // fall through to the next suffix
428
+ }
425
429
  }
430
+ return null;
426
431
  }
427
432
 
428
433
  /**
package/src/questions.mjs CHANGED
@@ -34,6 +34,6 @@ export function normalizeQuestions(questions) {
34
34
  throw new TypeError(`questions[${i}]: score needs 2-10 ordered levels`);
35
35
  }
36
36
  }
37
- return { type, instructions, options };
37
+ return { type, instructions, options, ...(q?.criteria !== undefined ? { criteria: q.criteria } : {}) };
38
38
  });
39
39
  }
package/src/registry.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * registry.mjs — multi-family runtime for JEV-style typed-decision models.
3
3
  *
4
- * jev-web ships two families today (open-jev's fused DeBERTa graph and Laya's
5
- * ModernBERT encoder + typed head) and any third party can add its own without
6
- * touching core:
4
+ * jev-web ships three families today (open-jev's fused DeBERTa graph, Laya's
5
+ * ModernBERT encoder + typed head, Strands Decider 2B's Qwen torso + pointer
6
+ * head, and Bekko System One v0's cross-encoder) and any third party can add
7
+ * its own without touching core:
7
8
  *
8
9
  * registerDecisionFamily("my-model", {
9
10
  * defaults: { model: "org/my-typed-decision-ONNX", revision: "main" },
@@ -27,6 +28,16 @@ import {
27
28
  LAYA_DEFAULT_REVISION,
28
29
  LAYA_DEFAULT_SUBFOLDER,
29
30
  } from "./laya.mjs";
31
+ import {
32
+ createStrandsDecider,
33
+ STRANDS_DEFAULT_MODEL,
34
+ STRANDS_DEFAULT_REVISION,
35
+ } from "./strands.mjs";
36
+ import {
37
+ createBekkoDecider,
38
+ BEKKO_DEFAULT_MODEL,
39
+ BEKKO_DEFAULT_REVISION,
40
+ } from "./bekko.mjs";
30
41
 
31
42
  const families = new Map();
32
43
 
@@ -67,6 +78,22 @@ registerDecisionFamily("laya", {
67
78
  create: createLayaDecider,
68
79
  });
69
80
 
81
+ registerDecisionFamily("strands-decider", {
82
+ defaults: {
83
+ model: STRANDS_DEFAULT_MODEL,
84
+ revision: STRANDS_DEFAULT_REVISION,
85
+ },
86
+ create: createStrandsDecider,
87
+ });
88
+
89
+ registerDecisionFamily("bekko", {
90
+ defaults: {
91
+ model: BEKKO_DEFAULT_MODEL,
92
+ revision: BEKKO_DEFAULT_REVISION,
93
+ },
94
+ create: createBekkoDecider,
95
+ });
96
+
70
97
  /**
71
98
  * Create a decision runtime, preferring `family` and falling back in order.
72
99
  * @param {{family?:string, fallback?:string[], onFallback?:Function}} [opts]