oracular 0.7.0 → 0.8.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/CHANGELOG.md +12 -0
- package/README.md +3 -2
- package/dist/index.cjs +73 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -10
- package/dist/index.d.ts +22 -10
- package/dist/index.js +73 -11
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.cts +4 -1
- package/dist/inspect.d.ts +4 -1
- package/dist/presets.d.cts +7 -0
- package/dist/presets.d.ts +7 -0
- package/package.json +3 -2
- package/schema/config.schema.json +5 -0
package/dist/index.d.cts
CHANGED
|
@@ -54,6 +54,13 @@ interface RevealOptions {
|
|
|
54
54
|
z?: number;
|
|
55
55
|
/** How long after `markRevealed` the intent stays revealed and `shouldReveal` is false. Default: the intent's `halfLife`. */
|
|
56
56
|
cooldown?: number;
|
|
57
|
+
/**
|
|
58
|
+
* How `lower` is computed. `'normal'` (default): p − z·sd, clamped to [0, 1]. `'exact'`: the
|
|
59
|
+
* Beta distribution's own quantile at the same one-sided level, Φ(−z), so `z` keeps its
|
|
60
|
+
* meaning. The two differ on thin evidence near the threshold, where the approximation is
|
|
61
|
+
* optimistic; under `'exact'`, `z: 0` is the median rather than the mean.
|
|
62
|
+
*/
|
|
63
|
+
bound?: 'normal' | 'exact';
|
|
57
64
|
}
|
|
58
65
|
/**
|
|
59
66
|
* How the engine learns from the user's own outcomes.
|
|
@@ -261,7 +268,10 @@ interface Prediction {
|
|
|
261
268
|
p: number;
|
|
262
269
|
/** Standard deviation of the Beta distribution. */
|
|
263
270
|
sd: number;
|
|
264
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* The lower confidence bound `shouldReveal` compares to the threshold: p − z·sd clamped to
|
|
273
|
+
* [0, 1], or with `bound: 'exact'` the Beta quantile at Φ(−z).
|
|
274
|
+
*/
|
|
265
275
|
lower: number;
|
|
266
276
|
/** 1 − 2·sd, clamped to [0, 1]. A heuristic: 1 when the estimate is sharp, 0 when it is as vague as possible. */
|
|
267
277
|
confidence: number;
|
|
@@ -515,8 +525,8 @@ interface PresetSource {
|
|
|
515
525
|
* @param state - The current state. Not mutated.
|
|
516
526
|
* @param intent - Name of an intent declared in the config.
|
|
517
527
|
* @param at - The time to evaluate at.
|
|
518
|
-
* @param opts - Reveal overrides (`threshold`, `z`, `cooldown`) and explain filters
|
|
519
|
-
* `limit`) in one object.
|
|
528
|
+
* @param opts - Reveal overrides (`threshold`, `z`, `cooldown`, `bound`) and explain filters
|
|
529
|
+
* (`nonZero`, `limit`) in one object.
|
|
520
530
|
* @returns `prediction` (as `predict`), `reveal` (as `shouldReveal`) and `explanation` (as
|
|
521
531
|
* `explain` with the same filters).
|
|
522
532
|
* @throws `RangeError` when `intent` is not declared in the config.
|
|
@@ -552,7 +562,8 @@ declare function inspect(state: EngineState, at: number, opts?: EvaluateOptions)
|
|
|
552
562
|
/**
|
|
553
563
|
* How much positive evidence an intent needs before {@link shouldReveal} first becomes true:
|
|
554
564
|
* the smallest pseudo-count `w` such that Beta(α₀ + w, β₀) clears both `p ≥ threshold` and
|
|
555
|
-
* `
|
|
565
|
+
* `lower ≥ threshold`, with `lower` as `bound` defines it (p − z·sd, or the exact Beta quantile).
|
|
566
|
+
* Divide by a signal's weight and round up for the number of full-weight
|
|
556
567
|
* occurrences that open the gate: distinct signals, or one occurrence per episode. Repeats within
|
|
557
568
|
* one episode add less (n of them add `w × log2(1 + n)`, see {@link IntentConfig.saturate}).
|
|
558
569
|
* Solved numerically to 1e-9.
|
|
@@ -560,8 +571,8 @@ declare function inspect(state: EngineState, at: number, opts?: EvaluateOptions)
|
|
|
560
571
|
* Cooldowns and decay are not part of the answer: this is the evidence needed at one instant.
|
|
561
572
|
*
|
|
562
573
|
* @param intent - One intent's config, validated as `createState` would.
|
|
563
|
-
* @param opts - `threshold` and `
|
|
564
|
-
* no engine config here). `cooldown` is accepted and ignored.
|
|
574
|
+
* @param opts - `threshold`, `z` and `bound` overrides, resolved as call → intent → defaults
|
|
575
|
+
* (there is no engine config here). `cooldown` is accepted and ignored.
|
|
565
576
|
* @returns The pseudo-count sum; 0 when the prior alone already clears the gate; `Infinity`
|
|
566
577
|
* when no finite amount of evidence can (threshold 1).
|
|
567
578
|
* @throws `TypeError` or `RangeError` when `intent` or `opts` is invalid.
|
|
@@ -679,9 +690,9 @@ declare function episodeClosed(prev: EngineState, next: EngineState): boolean;
|
|
|
679
690
|
* @param state - The current state. Not mutated.
|
|
680
691
|
* @param intent - Name of an intent declared in the config.
|
|
681
692
|
* @param at - The time to evaluate at, in the same unit as your durations.
|
|
682
|
-
* @param opts - Overrides for `threshold`, `z` and `
|
|
683
|
-
* engine → defaults. `z`
|
|
684
|
-
* `threshold` only matters to {@link shouldReveal}.
|
|
693
|
+
* @param opts - Overrides for `threshold`, `z`, `cooldown` and `bound`, resolved as call →
|
|
694
|
+
* intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets `revealed` and
|
|
695
|
+
* `suppressedUntil`; `threshold` only matters to {@link shouldReveal}.
|
|
685
696
|
* @returns The prediction. `p`, `sd`, `lower` and `confidence` are in [0, 1]; `evidence` is
|
|
686
697
|
* α + β.
|
|
687
698
|
* @throws `RangeError` when `intent` is not declared in the config.
|
|
@@ -696,7 +707,8 @@ declare function episodeClosed(prev: EngineState, next: EngineState): boolean;
|
|
|
696
707
|
declare function predict(state: EngineState, intent: string, at: number, opts?: RevealOptions): Prediction;
|
|
697
708
|
/**
|
|
698
709
|
* Whether to act on the intent now. True when p ≥ `threshold`, the lower confidence bound
|
|
699
|
-
* p − z·sd
|
|
710
|
+
* (p − z·sd, or the exact Beta quantile with `bound: 'exact'`) also clears `threshold`, and no
|
|
711
|
+
* cooldown is running: neither the one started by
|
|
700
712
|
* {@link markRevealed} nor the one started by the intent's outcome event.
|
|
701
713
|
*
|
|
702
714
|
* @param state - The current state. Not mutated.
|
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,13 @@ interface RevealOptions {
|
|
|
54
54
|
z?: number;
|
|
55
55
|
/** How long after `markRevealed` the intent stays revealed and `shouldReveal` is false. Default: the intent's `halfLife`. */
|
|
56
56
|
cooldown?: number;
|
|
57
|
+
/**
|
|
58
|
+
* How `lower` is computed. `'normal'` (default): p − z·sd, clamped to [0, 1]. `'exact'`: the
|
|
59
|
+
* Beta distribution's own quantile at the same one-sided level, Φ(−z), so `z` keeps its
|
|
60
|
+
* meaning. The two differ on thin evidence near the threshold, where the approximation is
|
|
61
|
+
* optimistic; under `'exact'`, `z: 0` is the median rather than the mean.
|
|
62
|
+
*/
|
|
63
|
+
bound?: 'normal' | 'exact';
|
|
57
64
|
}
|
|
58
65
|
/**
|
|
59
66
|
* How the engine learns from the user's own outcomes.
|
|
@@ -261,7 +268,10 @@ interface Prediction {
|
|
|
261
268
|
p: number;
|
|
262
269
|
/** Standard deviation of the Beta distribution. */
|
|
263
270
|
sd: number;
|
|
264
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* The lower confidence bound `shouldReveal` compares to the threshold: p − z·sd clamped to
|
|
273
|
+
* [0, 1], or with `bound: 'exact'` the Beta quantile at Φ(−z).
|
|
274
|
+
*/
|
|
265
275
|
lower: number;
|
|
266
276
|
/** 1 − 2·sd, clamped to [0, 1]. A heuristic: 1 when the estimate is sharp, 0 when it is as vague as possible. */
|
|
267
277
|
confidence: number;
|
|
@@ -515,8 +525,8 @@ interface PresetSource {
|
|
|
515
525
|
* @param state - The current state. Not mutated.
|
|
516
526
|
* @param intent - Name of an intent declared in the config.
|
|
517
527
|
* @param at - The time to evaluate at.
|
|
518
|
-
* @param opts - Reveal overrides (`threshold`, `z`, `cooldown`) and explain filters
|
|
519
|
-
* `limit`) in one object.
|
|
528
|
+
* @param opts - Reveal overrides (`threshold`, `z`, `cooldown`, `bound`) and explain filters
|
|
529
|
+
* (`nonZero`, `limit`) in one object.
|
|
520
530
|
* @returns `prediction` (as `predict`), `reveal` (as `shouldReveal`) and `explanation` (as
|
|
521
531
|
* `explain` with the same filters).
|
|
522
532
|
* @throws `RangeError` when `intent` is not declared in the config.
|
|
@@ -552,7 +562,8 @@ declare function inspect(state: EngineState, at: number, opts?: EvaluateOptions)
|
|
|
552
562
|
/**
|
|
553
563
|
* How much positive evidence an intent needs before {@link shouldReveal} first becomes true:
|
|
554
564
|
* the smallest pseudo-count `w` such that Beta(α₀ + w, β₀) clears both `p ≥ threshold` and
|
|
555
|
-
* `
|
|
565
|
+
* `lower ≥ threshold`, with `lower` as `bound` defines it (p − z·sd, or the exact Beta quantile).
|
|
566
|
+
* Divide by a signal's weight and round up for the number of full-weight
|
|
556
567
|
* occurrences that open the gate: distinct signals, or one occurrence per episode. Repeats within
|
|
557
568
|
* one episode add less (n of them add `w × log2(1 + n)`, see {@link IntentConfig.saturate}).
|
|
558
569
|
* Solved numerically to 1e-9.
|
|
@@ -560,8 +571,8 @@ declare function inspect(state: EngineState, at: number, opts?: EvaluateOptions)
|
|
|
560
571
|
* Cooldowns and decay are not part of the answer: this is the evidence needed at one instant.
|
|
561
572
|
*
|
|
562
573
|
* @param intent - One intent's config, validated as `createState` would.
|
|
563
|
-
* @param opts - `threshold` and `
|
|
564
|
-
* no engine config here). `cooldown` is accepted and ignored.
|
|
574
|
+
* @param opts - `threshold`, `z` and `bound` overrides, resolved as call → intent → defaults
|
|
575
|
+
* (there is no engine config here). `cooldown` is accepted and ignored.
|
|
565
576
|
* @returns The pseudo-count sum; 0 when the prior alone already clears the gate; `Infinity`
|
|
566
577
|
* when no finite amount of evidence can (threshold 1).
|
|
567
578
|
* @throws `TypeError` or `RangeError` when `intent` or `opts` is invalid.
|
|
@@ -679,9 +690,9 @@ declare function episodeClosed(prev: EngineState, next: EngineState): boolean;
|
|
|
679
690
|
* @param state - The current state. Not mutated.
|
|
680
691
|
* @param intent - Name of an intent declared in the config.
|
|
681
692
|
* @param at - The time to evaluate at, in the same unit as your durations.
|
|
682
|
-
* @param opts - Overrides for `threshold`, `z` and `
|
|
683
|
-
* engine → defaults. `z`
|
|
684
|
-
* `threshold` only matters to {@link shouldReveal}.
|
|
693
|
+
* @param opts - Overrides for `threshold`, `z`, `cooldown` and `bound`, resolved as call →
|
|
694
|
+
* intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets `revealed` and
|
|
695
|
+
* `suppressedUntil`; `threshold` only matters to {@link shouldReveal}.
|
|
685
696
|
* @returns The prediction. `p`, `sd`, `lower` and `confidence` are in [0, 1]; `evidence` is
|
|
686
697
|
* α + β.
|
|
687
698
|
* @throws `RangeError` when `intent` is not declared in the config.
|
|
@@ -696,7 +707,8 @@ declare function episodeClosed(prev: EngineState, next: EngineState): boolean;
|
|
|
696
707
|
declare function predict(state: EngineState, intent: string, at: number, opts?: RevealOptions): Prediction;
|
|
697
708
|
/**
|
|
698
709
|
* Whether to act on the intent now. True when p ≥ `threshold`, the lower confidence bound
|
|
699
|
-
* p − z·sd
|
|
710
|
+
* (p − z·sd, or the exact Beta quantile with `bound: 'exact'`) also clears `threshold`, and no
|
|
711
|
+
* cooldown is running: neither the one started by
|
|
700
712
|
* {@link markRevealed} nor the one started by the intent's outcome event.
|
|
701
713
|
*
|
|
702
714
|
* @param state - The current state. Not mutated.
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,62 @@ function clamp01(x) {
|
|
|
18
18
|
if (x > 1) return 1;
|
|
19
19
|
return x;
|
|
20
20
|
}
|
|
21
|
+
function normalTail(z) {
|
|
22
|
+
const x = z / Math.SQRT2;
|
|
23
|
+
const t = 1 / (1 + 0.3275911 * x);
|
|
24
|
+
const poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
|
|
25
|
+
return 0.5 * poly * Math.exp(-x * x);
|
|
26
|
+
}
|
|
27
|
+
function lgamma(x) {
|
|
28
|
+
let acc = 0;
|
|
29
|
+
let v = x;
|
|
30
|
+
while (v < 8) {
|
|
31
|
+
acc -= Math.log(v);
|
|
32
|
+
v += 1;
|
|
33
|
+
}
|
|
34
|
+
const r = 1 / v;
|
|
35
|
+
const r2 = r * r;
|
|
36
|
+
return acc + (v - 0.5) * Math.log(v) - v + 0.9189385332046728 + r * (1 / 12 - r2 * (1 / 360 - r2 * (1 / 1260 - r2 / 1680)));
|
|
37
|
+
}
|
|
38
|
+
function betacf(x, a, b) {
|
|
39
|
+
const tiny = 1e-300;
|
|
40
|
+
const guard = (v) => Math.abs(v) < tiny ? tiny : v;
|
|
41
|
+
let c = 1 / tiny;
|
|
42
|
+
let d = 1;
|
|
43
|
+
let h = 1;
|
|
44
|
+
for (let i = 1; i < 2e4; i++) {
|
|
45
|
+
const m = i >> 1;
|
|
46
|
+
const aa = (i & 1 ? -(a + m) * (a + b + m) : m * (b - m)) * x / ((a + i - 1) * (a + i));
|
|
47
|
+
d = 1 / guard(1 + aa * d);
|
|
48
|
+
c = guard(1 + aa / c);
|
|
49
|
+
const del = c * d;
|
|
50
|
+
h *= del;
|
|
51
|
+
if (Math.abs(del - 1) < 1e-12) break;
|
|
52
|
+
}
|
|
53
|
+
return h;
|
|
54
|
+
}
|
|
55
|
+
function incompleteBeta(x, a, b) {
|
|
56
|
+
if (x <= 0) return 0;
|
|
57
|
+
if (x >= 1) return 1;
|
|
58
|
+
const front = Math.exp(
|
|
59
|
+
lgamma(a + b) - lgamma(a) - lgamma(b) + a * Math.log(x) + b * Math.log(1 - x)
|
|
60
|
+
);
|
|
61
|
+
return x < (a + 1) / (a + b + 2) ? front * betacf(x, a, b) / a : 1 - front * betacf(1 - x, b, a) / b;
|
|
62
|
+
}
|
|
63
|
+
function betaQuantile(q, a, b) {
|
|
64
|
+
let lo = 0;
|
|
65
|
+
let hi = 1;
|
|
66
|
+
for (let i = 0; i < 40 && hi - lo > 1e-9; i++) {
|
|
67
|
+
const mid = (lo + hi) / 2;
|
|
68
|
+
if (incompleteBeta(mid, a, b) < q) lo = mid;
|
|
69
|
+
else hi = mid;
|
|
70
|
+
}
|
|
71
|
+
return (lo + hi) / 2;
|
|
72
|
+
}
|
|
73
|
+
function lowerBound(alpha, beta, z, bound) {
|
|
74
|
+
if (bound === "exact") return betaQuantile(normalTail(z), alpha, beta);
|
|
75
|
+
return clamp01(betaMean(alpha, beta) - z * betaSd(alpha, beta));
|
|
76
|
+
}
|
|
21
77
|
function repeatFactor(n) {
|
|
22
78
|
return Math.log1p(1 / n) / Math.LN2;
|
|
23
79
|
}
|
|
@@ -198,7 +254,7 @@ var INTENT_KEYS = [
|
|
|
198
254
|
"attribution",
|
|
199
255
|
"reveal"
|
|
200
256
|
];
|
|
201
|
-
var REVEAL_KEYS = ["threshold", "z", "cooldown"];
|
|
257
|
+
var REVEAL_KEYS = ["threshold", "z", "cooldown", "bound"];
|
|
202
258
|
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes"];
|
|
203
259
|
var EXPLAIN_KEYS = ["nonZero", "limit"];
|
|
204
260
|
function typeError(path, message) {
|
|
@@ -272,6 +328,12 @@ function normalizeRevealOptions(value, path) {
|
|
|
272
328
|
if (record2.cooldown !== void 0) {
|
|
273
329
|
out.cooldown = expectNonNegative(record2.cooldown, `${path}.cooldown`);
|
|
274
330
|
}
|
|
331
|
+
if (record2.bound !== void 0) {
|
|
332
|
+
if (record2.bound !== "normal" && record2.bound !== "exact") {
|
|
333
|
+
typeError(`${path}.bound`, 'must be "normal" or "exact"');
|
|
334
|
+
}
|
|
335
|
+
out.bound = record2.bound;
|
|
336
|
+
}
|
|
275
337
|
return out;
|
|
276
338
|
}
|
|
277
339
|
function normalizeLearning(value, path) {
|
|
@@ -392,7 +454,8 @@ function resolveReveal(config, intent, opts, normalized = false) {
|
|
|
392
454
|
return {
|
|
393
455
|
threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
|
|
394
456
|
z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
|
|
395
|
-
cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife
|
|
457
|
+
cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife,
|
|
458
|
+
bound: o.bound ?? intent.reveal?.bound ?? config?.reveal?.bound ?? "normal"
|
|
396
459
|
};
|
|
397
460
|
}
|
|
398
461
|
function resolveLearning(config) {
|
|
@@ -499,7 +562,7 @@ function buildPrediction(is, name, reveal, at) {
|
|
|
499
562
|
intent: name,
|
|
500
563
|
p,
|
|
501
564
|
sd,
|
|
502
|
-
lower:
|
|
565
|
+
lower: lowerBound(is.alpha, is.beta, reveal.z, reveal.bound),
|
|
503
566
|
confidence: clamp01(1 - 2 * sd),
|
|
504
567
|
evidence: is.alpha + is.beta,
|
|
505
568
|
revealed: revealEnd !== null,
|
|
@@ -567,13 +630,9 @@ function inspect(state, at, opts) {
|
|
|
567
630
|
// src/evidence.ts
|
|
568
631
|
function evidenceToReveal(intent, opts) {
|
|
569
632
|
const config = normalizeIntentConfig(intent, "intent");
|
|
570
|
-
const { threshold, z } = resolveReveal(void 0, config, opts);
|
|
633
|
+
const { threshold, z, bound } = resolveReveal(void 0, config, opts);
|
|
571
634
|
const { alpha0, beta0 } = priorCounts(config);
|
|
572
|
-
const open = (w) =>
|
|
573
|
-
const alpha = alpha0 + w;
|
|
574
|
-
const p = betaMean(alpha, beta0);
|
|
575
|
-
return p >= threshold && clamp01(p - z * betaSd(alpha, beta0)) >= threshold;
|
|
576
|
-
};
|
|
635
|
+
const open = (w) => betaMean(alpha0 + w, beta0) >= threshold && lowerBound(alpha0 + w, beta0, z, bound) >= threshold;
|
|
577
636
|
if (threshold >= 1) return Number.POSITIVE_INFINITY;
|
|
578
637
|
if (open(0)) return 0;
|
|
579
638
|
let lo = 0;
|
|
@@ -1009,8 +1068,11 @@ function reconcile(raw, config) {
|
|
|
1009
1068
|
keep(stored.contributions, intent.signals)
|
|
1010
1069
|
);
|
|
1011
1070
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1071
|
+
}
|
|
1072
|
+
if (raw.episode.startedAt !== null) {
|
|
1073
|
+
for (const [name, episode] of Object.entries(raw.episode.intents)) {
|
|
1074
|
+
const intent = getOwn(config.intents, name);
|
|
1075
|
+
if (intent === void 0) continue;
|
|
1014
1076
|
const seen = episode.seen.filter(
|
|
1015
1077
|
(s, i) => hasOwn(intent.signals, s) && episode.seen.indexOf(s) === i
|
|
1016
1078
|
);
|