decidekit 0.0.0-stage → 0.1.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.
@@ -0,0 +1,103 @@
1
+ import { type Trainable } from "./fallback.ts";
2
+ import type { Choices, Decision, Head } from "./index.ts";
3
+ type Json = string | number | boolean | null | readonly Json[] | {
4
+ readonly [key: string]: Json;
5
+ };
6
+ type Question = {
7
+ readonly type: "bool";
8
+ readonly instructions: string;
9
+ readonly criteria: {
10
+ readonly true: string;
11
+ readonly false: string;
12
+ };
13
+ } | {
14
+ readonly type: "choice";
15
+ readonly instructions: string;
16
+ readonly criteria: Readonly<Record<string, string>>;
17
+ } | {
18
+ readonly type: "score";
19
+ readonly instructions: string;
20
+ readonly criteria: readonly string[];
21
+ };
22
+ type Answer = {
23
+ readonly type: "bool";
24
+ readonly probability: number;
25
+ } | {
26
+ readonly type: "choice";
27
+ readonly choice: string;
28
+ readonly probabilities: Readonly<Record<string, number>>;
29
+ readonly confidence: number;
30
+ } | {
31
+ readonly type: "score";
32
+ readonly score: number;
33
+ readonly confidence: number;
34
+ };
35
+ /** switch's decision request: the state the model reads and the questions, by id. */
36
+ export interface SwitchRequest {
37
+ readonly decisionState: {
38
+ readonly [key: string]: Json;
39
+ };
40
+ readonly decisionImages?: readonly unknown[];
41
+ readonly decisionQuestions: Readonly<Record<string, Question>>;
42
+ }
43
+ /** switch's decision result, with the fields switch reads. */
44
+ export interface SwitchResult {
45
+ readonly decisionAnswers: Readonly<Record<string, Answer>>;
46
+ readonly decisionUsage?: {
47
+ input: number;
48
+ output: number;
49
+ cacheRead: number;
50
+ cacheWrite: number;
51
+ totalTokens: number;
52
+ cost: {
53
+ input: number;
54
+ output: number;
55
+ cacheRead: number;
56
+ cacheWrite: number;
57
+ total: number;
58
+ };
59
+ };
60
+ readonly api: string;
61
+ readonly provider: string;
62
+ readonly model: string;
63
+ readonly stopReason: "stop" | "error" | "aborted";
64
+ readonly errorMessage?: string;
65
+ readonly timestamp: number;
66
+ readonly tooLong: boolean;
67
+ readonly milliseconds: number;
68
+ }
69
+ /** switch's `Decider`. */
70
+ export interface SwitchDecider {
71
+ readonly model: string;
72
+ readonly window: number;
73
+ readonly charsPerToken: number;
74
+ decide(request: SwitchRequest): Promise<SwitchResult>;
75
+ }
76
+ type Local = Trainable & {
77
+ model: string;
78
+ decide<const L extends string>(text: string, using: Choices<L>): Promise<Decision<L>>;
79
+ };
80
+ export interface SwitchOptions {
81
+ /** Trained heads for questions that repeat, by question id. Questions without one are decided zero-shot. */
82
+ heads?: Readonly<Record<string, Head>>;
83
+ /**
84
+ * A stronger decider to ask when decidekit isn't sure, such as Jev through switch's pi-ai decider. Its answers to
85
+ * choice questions train a head for that question, so a question that repeats asks it less and less (see `withFallback`).
86
+ */
87
+ teacher?: SwitchDecider;
88
+ /** Below this confidence, choice questions go to `teacher`. Default 0.9. */
89
+ minConfidence?: number;
90
+ /**
91
+ * What decidekit reads for a question. Its encoder reads about 256 tokens and drops the rest, so a long state or long
92
+ * instructions can push out what matters; pick it here, such as the target of a search. Default: `textOf`.
93
+ */
94
+ text?: (state: SwitchRequest["decisionState"], question: Question, id: string) => string;
95
+ }
96
+ /** The text decidekit reads for a question: its instructions, then the state as plain lines rather than JSON. */
97
+ export declare function textOf(instructions: string, state: Json): string;
98
+ /**
99
+ * Wraps a decidekit decider as a switch decider. Choice, bool and score questions become decidekit decisions over the
100
+ * question's criteria; images are ignored. Every call is local, so usage costs nothing.
101
+ */
102
+ export declare function switchDecider(local: Local, options?: SwitchOptions): SwitchDecider;
103
+ export type { Question as SwitchQuestion, Answer as SwitchAnswer };
package/dist/switch.js ADDED
@@ -0,0 +1,94 @@
1
+ // decidekit as a decision model for switch (github.com/osolmaz/switch), a harness that runs a decision model through a
2
+ // task by asking it typed questions. The types below copy the parts of switch's `Decider` that this needs, so decidekit
3
+ // doesn't depend on switch: pass the result wherever switch takes a decider.
4
+ import { withFallback } from "./fallback.js";
5
+ /** The text decidekit reads for a question: its instructions, then the state as plain lines rather than JSON. */
6
+ export function textOf(instructions, state) {
7
+ const lines = [];
8
+ const walk = (v, key) => {
9
+ if (v === null || typeof v !== "object")
10
+ lines.push(key ? `${key}: ${v}` : String(v));
11
+ else if (Array.isArray(v) && v.every((x) => x === null || typeof x !== "object"))
12
+ lines.push(key ? `${key}: ${v.join(", ")}` : v.join(", "));
13
+ else
14
+ for (const [k, x] of Object.entries(v))
15
+ walk(x, Array.isArray(v) ? key : k);
16
+ };
17
+ walk(state, "");
18
+ return [instructions, ...lines].filter(Boolean).join("\n");
19
+ }
20
+ /**
21
+ * Wraps a decidekit decider as a switch decider. Choice, bool and score questions become decidekit decisions over the
22
+ * question's criteria; images are ignored. Every call is local, so usage costs nothing.
23
+ */
24
+ export function switchDecider(local, options = {}) {
25
+ const { heads = {}, teacher, minConfidence = 0.9, text: textFor = (state, q) => textOf(q.instructions, state) } = options;
26
+ // One router per repeating choice question, keyed by its instructions and options.
27
+ const routers = new Map();
28
+ // The state each pending text came from, so the teacher reads the same request.
29
+ const states = new Map();
30
+ function routerFor(id, q) {
31
+ const key = JSON.stringify([q.instructions, q.criteria]);
32
+ let r = routers.get(key);
33
+ if (!r) {
34
+ r = withFallback(local, heads[id] ?? q.criteria, {
35
+ minConfidence,
36
+ retrainEvery: 8,
37
+ ask: async (text) => {
38
+ const res = await teacher.decide({ decisionState: states.get(text), decisionQuestions: { [id]: q } });
39
+ const a = res.decisionAnswers[id];
40
+ return a?.type === "choice" ? a.choice : null;
41
+ },
42
+ });
43
+ routers.set(key, r);
44
+ }
45
+ return r;
46
+ }
47
+ async function answer(id, q, state) {
48
+ const text = textFor(state, q, id);
49
+ if (q.type === "choice") {
50
+ const keys = Object.keys(q.criteria);
51
+ // decidekit compares two or more options; a single one is the answer.
52
+ if (keys.length === 1)
53
+ return { type: "choice", choice: keys[0], probabilities: { [keys[0]]: 1 }, confidence: 1 };
54
+ if (teacher)
55
+ states.set(text, state);
56
+ const d = teacher ? await routerFor(id, q)(text).finally(() => states.delete(text)) : await local.decide(text, heads[id] ?? q.criteria);
57
+ const choice = d.choice ?? Object.entries(d.probabilities).sort((a, b) => b[1] - a[1])[0][0];
58
+ return { type: "choice", choice, probabilities: d.probabilities, confidence: d.confidence };
59
+ }
60
+ if (q.type === "bool") {
61
+ const d = await local.decide(text, heads[id] ?? { true: q.criteria.true, false: q.criteria.false });
62
+ return { type: "bool", probability: d.probabilities.true };
63
+ }
64
+ const levels = Object.fromEntries(q.criteria.map((c, i) => [String(i), c]));
65
+ const d = await local.decide(text, heads[id] ?? levels);
66
+ return { type: "score", score: Number(d.choice), confidence: d.confidence };
67
+ }
68
+ return {
69
+ model: `decidekit/${local.model}`,
70
+ // The encoder reads about 256 tokens and cuts the rest, so a long state still fits; tell switch the window is big
71
+ // so it doesn't trim the choices to fit.
72
+ window: 1_000_000,
73
+ charsPerToken: 4,
74
+ async decide(request) {
75
+ const started = performance.now();
76
+ const base = { api: "decidekit", provider: "decidekit", model: `decidekit/${local.model}`, timestamp: Date.now(), tooLong: false };
77
+ try {
78
+ const entries = await Promise.all(Object.entries(request.decisionQuestions).map(async ([id, q]) => [id, await answer(id, q, request.decisionState)]));
79
+ const input = Math.round(JSON.stringify(request.decisionState).length / 4);
80
+ const zero = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
81
+ return {
82
+ ...base,
83
+ decisionAnswers: Object.fromEntries(entries),
84
+ decisionUsage: { input, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: input, cost: zero },
85
+ stopReason: "stop",
86
+ milliseconds: performance.now() - started,
87
+ };
88
+ }
89
+ catch (e) {
90
+ return { ...base, decisionAnswers: {}, stopReason: "error", errorMessage: e instanceof Error ? e.message : String(e), milliseconds: performance.now() - started };
91
+ }
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,36 @@
1
+ import type { Choices, Decision, Head } from "./index.ts";
2
+ /** What this layer needs from a decider: `createDecider()` and `createWorkerDecider()` both fit. */
3
+ export interface Decides {
4
+ decide<const L extends string>(text: string, using: Choices<L>): Promise<Decision<L>>;
5
+ }
6
+ export interface ChoiceQuestion<L extends string = string> {
7
+ type: "choice";
8
+ /** Kept for compatibility and ignored: decidekit compares the state with the options, it doesn't read a question. */
9
+ instructions: string;
10
+ /** Option names to descriptions (`null` for none), as in TypeSafe's API, or a trained head. */
11
+ criteria: Readonly<Record<L, string | null>> | Head<L>;
12
+ }
13
+ export declare function choice<const L extends string>(instructions: string, criteria: Readonly<Record<L, string | null>> | Head<L>): ChoiceQuestion<L>;
14
+ export declare const noul: (instructions: string) => never;
15
+ export declare const score: (instructions: string, levels: unknown) => never;
16
+ export interface ChoiceAnswer<L extends string> {
17
+ type: "choice";
18
+ choice: L;
19
+ /** TypeSafe's measure: 1 with all probability on one option, 0 when evenly spread, `(n * top - 1) / (n - 1)`. */
20
+ confidence: number;
21
+ probabilities: Record<L, number>;
22
+ }
23
+ /**
24
+ * Wraps a decider in TypeSafe's client shape. `confidence` follows TypeSafe's formula, so it reads lower than
25
+ * `decide()`'s: 0.61 on the top of three options is 0.42 here. `Math.max(...probabilities)` is the calibrated chance of
26
+ * being right. Thresholds carry over from neither model to the other.
27
+ */
28
+ export declare function typesafeClient(decider: Decides): {
29
+ systemOne<const Q extends Record<string, ChoiceQuestion<string>>>({ state, questions }: {
30
+ state: string | Readonly<Record<string, unknown>>;
31
+ questions: Q;
32
+ }): Promise<{
33
+ model: string;
34
+ answers: { [K in keyof Q]: Q[K] extends ChoiceQuestion<infer L> ? ChoiceAnswer<L> : never; };
35
+ }>;
36
+ };
@@ -0,0 +1,47 @@
1
+ export function choice(instructions, criteria) {
2
+ return { type: "choice", instructions, criteria };
3
+ }
4
+ const unsupported = (type) => () => {
5
+ throw new Error(`decidekit: ${type} questions aren't supported locally, only choice. Ask a yes/no as a choice between two described options.`);
6
+ };
7
+ export const noul = unsupported("noul");
8
+ export const score = unsupported("score");
9
+ /** A string, or an object whose string values are joined: braces and keys only add noise for an encoder. */
10
+ function stateText(state) {
11
+ if (typeof state === "string")
12
+ return state;
13
+ return Object.values(state).map((v) => {
14
+ if (typeof v === "string")
15
+ return v;
16
+ if (typeof v === "number" || typeof v === "boolean")
17
+ return String(v);
18
+ throw new Error("decidekit: state values must be text, numbers or booleans");
19
+ }).join("\n");
20
+ }
21
+ function options(criteria) {
22
+ if ("weights" in criteria)
23
+ return criteria;
24
+ const entries = Object.entries(criteria);
25
+ if (entries.every(([, d]) => d === null))
26
+ return entries.map(([l]) => l);
27
+ // Same as a plain label: an option with no description is compared by its readable name.
28
+ return Object.fromEntries(entries.map(([l, d]) => [l, d ?? l.replace(/[_-]+/g, " ")]));
29
+ }
30
+ /**
31
+ * Wraps a decider in TypeSafe's client shape. `confidence` follows TypeSafe's formula, so it reads lower than
32
+ * `decide()`'s: 0.61 on the top of three options is 0.42 here. `Math.max(...probabilities)` is the calibrated chance of
33
+ * being right. Thresholds carry over from neither model to the other.
34
+ */
35
+ export function typesafeClient(decider) {
36
+ return {
37
+ async systemOne({ state, questions }) {
38
+ const text = stateText(state), answers = {};
39
+ for (const [id, q] of Object.entries(questions)) {
40
+ const { choice, confidence, probabilities } = await decider.decide(text, options(q.criteria));
41
+ const n = Object.keys(probabilities).length;
42
+ answers[id] = { type: "choice", choice, confidence: Math.max(0, (n * confidence - 1) / (n - 1)), probabilities };
43
+ }
44
+ return { model: "decidekit", answers: answers };
45
+ },
46
+ };
47
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/worker.js ADDED
@@ -0,0 +1,26 @@
1
+ // Runs a decider off the main thread. The encoder's wasm work never gives the page a turn (measured: a 171 s freeze
2
+ // while training in Chrome), so in a browser it belongs here.
3
+ import { createDecider } from "./index.js";
4
+ let decider;
5
+ self.onmessage = async ({ data }) => {
6
+ try {
7
+ let value;
8
+ if (data.op === "init")
9
+ decider = await createDecider({ ...data.options, onProgress: (progress) => self.postMessage({ id: data.id, progress }) });
10
+ else if (!decider)
11
+ throw new Error("decider not initialised");
12
+ else if (data.op === "decideMany")
13
+ value = await decider.decideMany(data.args[0], data.args[1], data.args[2] ?? {});
14
+ else if (data.op === "decide")
15
+ value = await decider.decide(data.args[0], data.args[1], data.args[2] ?? {});
16
+ else
17
+ value = await decider.train(data.args[0], {
18
+ ...data.args[1],
19
+ onEpoch: (epoch, accuracy) => self.postMessage({ id: data.id, progress: { epoch, accuracy } }),
20
+ });
21
+ self.postMessage({ id: data.id, value });
22
+ }
23
+ catch (e) {
24
+ self.postMessage({ id: data.id, error: e instanceof Error ? e.message : String(e) });
25
+ }
26
+ };
package/package.json CHANGED
@@ -1,6 +1,93 @@
1
- {
2
- "name": "decidekit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "decidekit",
3
+ "version": "0.1.0",
4
+ "description": "A System One model in the browser: typed, calibrated text decisions on the user's device. Learns from an LLM when it isn't sure.",
5
+ "license": "MIT",
6
+ "repository": "github:debuffjp/decidekit",
7
+ "keywords": [
8
+ "decision model",
9
+ "system one",
10
+ "classification",
11
+ "intent",
12
+ "zero-shot",
13
+ "calibration",
14
+ "llm",
15
+ "embeddings",
16
+ "browser",
17
+ "typescript",
18
+ "text classification",
19
+ "transformers.js",
20
+ "on-device",
21
+ "llm fallback"
22
+ ],
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./browser": {
30
+ "types": "./dist/browser.d.ts",
31
+ "default": "./dist/browser.js"
32
+ },
33
+ "./react": {
34
+ "types": "./dist/react.d.ts",
35
+ "default": "./dist/react.js"
36
+ },
37
+ "./switch": {
38
+ "types": "./dist/switch.d.ts",
39
+ "default": "./dist/switch.js"
40
+ },
41
+ "./typesafe": {
42
+ "types": "./dist/typesafe.d.ts",
43
+ "default": "./dist/typesafe.js"
44
+ }
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "CHANGELOG.md"
49
+ ],
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.build.json",
52
+ "prepack": "npm run build",
53
+ "typecheck": "tsc --noEmit",
54
+ "eval": "node --experimental-strip-types eval/banking77.ts",
55
+ "bench": "esbuild src/worker.ts src/browser.ts --bundle --format=esm --platform=browser --outdir=bench --alias:onnxruntime-web/webgpu=onnxruntime-web/wasm && node bench/serve.mjs",
56
+ "playground": "node playground/build.mjs",
57
+ "heads": "node --experimental-strip-types heads/build.ts",
58
+ "bench:run": "node bench/run.mjs",
59
+ "eval:suite": "node --experimental-strip-types eval/suite.ts",
60
+ "eval:hill": "node --experimental-strip-types eval/hill.ts",
61
+ "test": "node --test --experimental-strip-types \"test/*.test.ts\"",
62
+ "lint:prose": "node scripts/lint-prose.mjs",
63
+ "smoke": "node scripts/smoke.mjs"
64
+ },
65
+ "dependencies": {
66
+ "@huggingface/transformers": "4.2.0"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "22.15.3",
70
+ "@types/react": "19.3.0",
71
+ "esbuild": "0.25.10",
72
+ "playwright-core": "1.64.1",
73
+ "react": "19.3.1",
74
+ "spryte": "0.11.0",
75
+ "typescript": "5.9.3"
76
+ },
77
+ "engines": {
78
+ "node": ">=22.18"
79
+ },
80
+ "homepage": "https://debuff.jp/decidekit/",
81
+ "bugs": "https://github.com/debuffjp/decidekit/issues",
82
+ "bin": {
83
+ "decidekit": "./dist/cli.js"
84
+ },
85
+ "peerDependencies": {
86
+ "react": ">=18"
87
+ },
88
+ "peerDependenciesMeta": {
89
+ "react": {
90
+ "optional": true
91
+ }
92
+ }
93
+ }