oracular 0.13.0 → 0.15.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 +16 -0
- package/README.md +7 -5
- package/dist/index.cjs +20 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.cts +2 -2
- package/dist/inspect.d.ts +2 -2
- package/dist/presets.d.cts +19 -0
- package/dist/presets.d.ts +19 -0
- package/package.json +1 -1
- package/schema/config.schema.json +10 -1
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 long after the intent's outcome event `shouldReveal` stays false. Default: `cooldown`.
|
|
59
|
+
* 0 is allowed: the outcome then pauses nothing, while a reveal still starts its cooldown, for
|
|
60
|
+
* an intent whose outcome recurs within minutes, or a "stuck" intent whose help should be back
|
|
61
|
+
* at the next attempt.
|
|
62
|
+
*/
|
|
63
|
+
outcomeCooldown?: number;
|
|
57
64
|
/**
|
|
58
65
|
* How `lower` is computed. `'normal'` (default): p − z·sd, clamped to [0, 1]. `'exact'`: the
|
|
59
66
|
* Beta distribution's own quantile at the same one-sided level, Φ(−z), so `z` keeps its
|
|
@@ -131,6 +138,18 @@ interface IntentConfig {
|
|
|
131
138
|
* weight.
|
|
132
139
|
*/
|
|
133
140
|
saturate?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Each outcome is an episode of its own for this intent, rather than the session. After the
|
|
143
|
+
* outcome arrives, the intent's bookkeeping starts over: a later outcome in the same session
|
|
144
|
+
* counts as another episode even with nothing seen since, and signals seen after an outcome
|
|
145
|
+
* take a miss at the session's close (or wait under `attribution`), as a first attempt would.
|
|
146
|
+
* Default false: the episode is the session, a repeated outcome with nothing seen since counts
|
|
147
|
+
* nothing, and signals seen after an outcome are neutral at the close. For intents shaped as
|
|
148
|
+
* attempts (an upload that fails, then completes, then completes again), where the second
|
|
149
|
+
* attempt is the contrast learning needs. With it set, a duplicated outcome event counts twice:
|
|
150
|
+
* dedupe such events in the app.
|
|
151
|
+
*/
|
|
152
|
+
episodePerOutcome?: boolean;
|
|
134
153
|
/**
|
|
135
154
|
* How long a closed episode's signals wait for the outcome before they take a miss, in the same
|
|
136
155
|
* unit as `at`. With it set, an episode that ends without the outcome is held instead of
|
|
@@ -307,8 +326,8 @@ interface Prediction {
|
|
|
307
326
|
revealedAt: number | null;
|
|
308
327
|
/**
|
|
309
328
|
* When the current suppression ends: the later of the reveal cooldown (after `markRevealed`)
|
|
310
|
-
* and the outcome cooldown (after the outcome event) still running at `at`,
|
|
311
|
-
* neither is. `shouldReveal` is false while this is non-null.
|
|
329
|
+
* and the outcome cooldown (`outcomeCooldown` after the outcome event) still running at `at`,
|
|
330
|
+
* or null when neither is. `shouldReveal` is false while this is non-null.
|
|
312
331
|
*/
|
|
313
332
|
suppressedUntil: number | null;
|
|
314
333
|
}
|
|
@@ -650,7 +669,8 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
|
|
|
650
669
|
* it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
|
|
651
670
|
* outcome of: give the signals remembered since the episode started or the outcome last arrived a
|
|
652
671
|
* hit and count the episode toward the base rate (a repeated outcome with nothing seen since
|
|
653
|
-
* counts nothing
|
|
672
|
+
* counts nothing, unless `IntentConfig.episodePerOutcome` makes every outcome an episode of its
|
|
673
|
+
* own for the intent), credit
|
|
654
674
|
* the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
|
|
655
675
|
* each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
|
|
656
676
|
* or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
|
|
@@ -730,9 +750,10 @@ declare function episodeClosed(prev: EngineState, next: EngineState): boolean;
|
|
|
730
750
|
* @param state - The current state. Not mutated.
|
|
731
751
|
* @param intent - Name of an intent declared in the config.
|
|
732
752
|
* @param at - The time to evaluate at, in the same unit as your durations.
|
|
733
|
-
* @param opts - Overrides for `threshold`, `z`, `cooldown` and `bound`,
|
|
734
|
-
* intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets
|
|
735
|
-
* `
|
|
753
|
+
* @param opts - Overrides for `threshold`, `z`, `cooldown`, `outcomeCooldown` and `bound`,
|
|
754
|
+
* resolved as call → intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets
|
|
755
|
+
* `revealed` and, with `outcomeCooldown` (default: `cooldown`), `suppressedUntil`;
|
|
756
|
+
* `threshold` only matters to {@link shouldReveal}.
|
|
736
757
|
* @returns The prediction. `p`, `sd`, `lower` and `confidence` are in [0, 1]; `evidence` is
|
|
737
758
|
* α + β.
|
|
738
759
|
* @throws `RangeError` when `intent` is not declared in the config.
|
|
@@ -755,7 +776,8 @@ declare function predict(state: EngineState, intent: string, at: number, opts?:
|
|
|
755
776
|
* @param intent - Name of an intent declared in the config.
|
|
756
777
|
* @param at - The time to evaluate at.
|
|
757
778
|
* @param opts - Overrides for `threshold` (default 0.5), `z` (default 1.28, a one-sided 90%
|
|
758
|
-
* bound)
|
|
779
|
+
* bound), `cooldown` (default: the intent's `halfLife`) and `outcomeCooldown` (default:
|
|
780
|
+
* `cooldown`; 0 lets the outcome pause nothing).
|
|
759
781
|
* @returns True when the app should show, offer or reveal whatever this intent gates.
|
|
760
782
|
* @throws `RangeError` when `intent` is not declared in the config.
|
|
761
783
|
* @throws `TypeError` when `state` is not an engine state, `at` is not finite, or `opts` is
|
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 long after the intent's outcome event `shouldReveal` stays false. Default: `cooldown`.
|
|
59
|
+
* 0 is allowed: the outcome then pauses nothing, while a reveal still starts its cooldown, for
|
|
60
|
+
* an intent whose outcome recurs within minutes, or a "stuck" intent whose help should be back
|
|
61
|
+
* at the next attempt.
|
|
62
|
+
*/
|
|
63
|
+
outcomeCooldown?: number;
|
|
57
64
|
/**
|
|
58
65
|
* How `lower` is computed. `'normal'` (default): p − z·sd, clamped to [0, 1]. `'exact'`: the
|
|
59
66
|
* Beta distribution's own quantile at the same one-sided level, Φ(−z), so `z` keeps its
|
|
@@ -131,6 +138,18 @@ interface IntentConfig {
|
|
|
131
138
|
* weight.
|
|
132
139
|
*/
|
|
133
140
|
saturate?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Each outcome is an episode of its own for this intent, rather than the session. After the
|
|
143
|
+
* outcome arrives, the intent's bookkeeping starts over: a later outcome in the same session
|
|
144
|
+
* counts as another episode even with nothing seen since, and signals seen after an outcome
|
|
145
|
+
* take a miss at the session's close (or wait under `attribution`), as a first attempt would.
|
|
146
|
+
* Default false: the episode is the session, a repeated outcome with nothing seen since counts
|
|
147
|
+
* nothing, and signals seen after an outcome are neutral at the close. For intents shaped as
|
|
148
|
+
* attempts (an upload that fails, then completes, then completes again), where the second
|
|
149
|
+
* attempt is the contrast learning needs. With it set, a duplicated outcome event counts twice:
|
|
150
|
+
* dedupe such events in the app.
|
|
151
|
+
*/
|
|
152
|
+
episodePerOutcome?: boolean;
|
|
134
153
|
/**
|
|
135
154
|
* How long a closed episode's signals wait for the outcome before they take a miss, in the same
|
|
136
155
|
* unit as `at`. With it set, an episode that ends without the outcome is held instead of
|
|
@@ -307,8 +326,8 @@ interface Prediction {
|
|
|
307
326
|
revealedAt: number | null;
|
|
308
327
|
/**
|
|
309
328
|
* When the current suppression ends: the later of the reveal cooldown (after `markRevealed`)
|
|
310
|
-
* and the outcome cooldown (after the outcome event) still running at `at`,
|
|
311
|
-
* neither is. `shouldReveal` is false while this is non-null.
|
|
329
|
+
* and the outcome cooldown (`outcomeCooldown` after the outcome event) still running at `at`,
|
|
330
|
+
* or null when neither is. `shouldReveal` is false while this is non-null.
|
|
312
331
|
*/
|
|
313
332
|
suppressedUntil: number | null;
|
|
314
333
|
}
|
|
@@ -650,7 +669,8 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
|
|
|
650
669
|
* it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
|
|
651
670
|
* outcome of: give the signals remembered since the episode started or the outcome last arrived a
|
|
652
671
|
* hit and count the episode toward the base rate (a repeated outcome with nothing seen since
|
|
653
|
-
* counts nothing
|
|
672
|
+
* counts nothing, unless `IntentConfig.episodePerOutcome` makes every outcome an episode of its
|
|
673
|
+
* own for the intent), credit
|
|
654
674
|
* the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
|
|
655
675
|
* each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
|
|
656
676
|
* or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
|
|
@@ -730,9 +750,10 @@ declare function episodeClosed(prev: EngineState, next: EngineState): boolean;
|
|
|
730
750
|
* @param state - The current state. Not mutated.
|
|
731
751
|
* @param intent - Name of an intent declared in the config.
|
|
732
752
|
* @param at - The time to evaluate at, in the same unit as your durations.
|
|
733
|
-
* @param opts - Overrides for `threshold`, `z`, `cooldown` and `bound`,
|
|
734
|
-
* intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets
|
|
735
|
-
* `
|
|
753
|
+
* @param opts - Overrides for `threshold`, `z`, `cooldown`, `outcomeCooldown` and `bound`,
|
|
754
|
+
* resolved as call → intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets
|
|
755
|
+
* `revealed` and, with `outcomeCooldown` (default: `cooldown`), `suppressedUntil`;
|
|
756
|
+
* `threshold` only matters to {@link shouldReveal}.
|
|
736
757
|
* @returns The prediction. `p`, `sd`, `lower` and `confidence` are in [0, 1]; `evidence` is
|
|
737
758
|
* α + β.
|
|
738
759
|
* @throws `RangeError` when `intent` is not declared in the config.
|
|
@@ -755,7 +776,8 @@ declare function predict(state: EngineState, intent: string, at: number, opts?:
|
|
|
755
776
|
* @param intent - Name of an intent declared in the config.
|
|
756
777
|
* @param at - The time to evaluate at.
|
|
757
778
|
* @param opts - Overrides for `threshold` (default 0.5), `z` (default 1.28, a one-sided 90%
|
|
758
|
-
* bound)
|
|
779
|
+
* bound), `cooldown` (default: the intent's `halfLife`) and `outcomeCooldown` (default:
|
|
780
|
+
* `cooldown`; 0 lets the outcome pause nothing).
|
|
759
781
|
* @returns True when the app should show, offer or reveal whatever this intent gates.
|
|
760
782
|
* @throws `RangeError` when `intent` is not declared in the config.
|
|
761
783
|
* @throws `TypeError` when `state` is not an engine state, `at` is not finite, or `opts` is
|
package/dist/index.js
CHANGED
|
@@ -284,10 +284,11 @@ var INTENT_KEYS = [
|
|
|
284
284
|
"outcome",
|
|
285
285
|
"resetOnOutcome",
|
|
286
286
|
"saturate",
|
|
287
|
+
"episodePerOutcome",
|
|
287
288
|
"attribution",
|
|
288
289
|
"reveal"
|
|
289
290
|
];
|
|
290
|
-
var REVEAL_KEYS = ["threshold", "z", "cooldown", "bound"];
|
|
291
|
+
var REVEAL_KEYS = ["threshold", "z", "cooldown", "outcomeCooldown", "bound"];
|
|
291
292
|
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes", "lift"];
|
|
292
293
|
var LIFT_TYPES = [
|
|
293
294
|
"absolute-difference",
|
|
@@ -373,6 +374,9 @@ function normalizeRevealOptions(value, path, strict = true) {
|
|
|
373
374
|
if (record2.cooldown !== void 0) {
|
|
374
375
|
out.cooldown = expectNonNegative(record2.cooldown, `${path}.cooldown`);
|
|
375
376
|
}
|
|
377
|
+
if (record2.outcomeCooldown !== void 0) {
|
|
378
|
+
out.outcomeCooldown = expectNonNegative(record2.outcomeCooldown, `${path}.outcomeCooldown`);
|
|
379
|
+
}
|
|
376
380
|
if (record2.bound !== void 0) {
|
|
377
381
|
if (record2.bound !== "normal" && record2.bound !== "exact") {
|
|
378
382
|
typeError(`${path}.bound`, 'must be "normal" or "exact"');
|
|
@@ -438,6 +442,12 @@ function normalizeIntentConfig(value, path, strict = true) {
|
|
|
438
442
|
if (typeof record2.saturate !== "boolean") typeError(`${path}.saturate`, "must be a boolean");
|
|
439
443
|
out.saturate = record2.saturate;
|
|
440
444
|
}
|
|
445
|
+
if (record2.episodePerOutcome !== void 0) {
|
|
446
|
+
if (typeof record2.episodePerOutcome !== "boolean") {
|
|
447
|
+
typeError(`${path}.episodePerOutcome`, "must be a boolean");
|
|
448
|
+
}
|
|
449
|
+
out.episodePerOutcome = record2.episodePerOutcome;
|
|
450
|
+
}
|
|
441
451
|
if (record2.attribution !== void 0) {
|
|
442
452
|
out.attribution = expectPositive(record2.attribution, `${path}.attribution`);
|
|
443
453
|
}
|
|
@@ -511,10 +521,12 @@ function splitEvaluateOptions(value, path) {
|
|
|
511
521
|
}
|
|
512
522
|
function resolveReveal(config, intent, opts, normalized = false) {
|
|
513
523
|
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options", false);
|
|
524
|
+
const cooldown = o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife;
|
|
514
525
|
return {
|
|
515
526
|
threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
|
|
516
527
|
z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
|
|
517
|
-
cooldown
|
|
528
|
+
cooldown,
|
|
529
|
+
outcomeCooldown: o.outcomeCooldown ?? intent.reveal?.outcomeCooldown ?? config?.reveal?.outcomeCooldown ?? cooldown,
|
|
518
530
|
bound: o.bound ?? intent.reveal?.bound ?? config?.reveal?.bound ?? "normal"
|
|
519
531
|
};
|
|
520
532
|
}
|
|
@@ -619,7 +631,7 @@ function buildPrediction(is, name, reveal, at) {
|
|
|
619
631
|
const p = betaMean(is.alpha, is.beta);
|
|
620
632
|
const sd = betaSd(is.alpha, is.beta);
|
|
621
633
|
const revealEnd = cooldownEnd(is.lastRevealedAt, reveal.cooldown, at);
|
|
622
|
-
const outcomeEnd = cooldownEnd(is.lastOutcomeAt, reveal.
|
|
634
|
+
const outcomeEnd = cooldownEnd(is.lastOutcomeAt, reveal.outcomeCooldown, at);
|
|
623
635
|
const suppressedUntil = revealEnd === null ? outcomeEnd : outcomeEnd === null ? revealEnd : Math.max(revealEnd, outcomeEnd);
|
|
624
636
|
return {
|
|
625
637
|
intent: name,
|
|
@@ -752,7 +764,9 @@ function observe(state, event) {
|
|
|
752
764
|
}
|
|
753
765
|
if (isOutcome) {
|
|
754
766
|
const record2 = episodeRecord(episode, intentName);
|
|
755
|
-
if (!record2.resolved || record2.seen.length > 0
|
|
767
|
+
if (!record2.resolved || record2.seen.length > 0 || intent.episodePerOutcome === true) {
|
|
768
|
+
is = recordEpisode(is, record2.seen, 1);
|
|
769
|
+
}
|
|
756
770
|
is = creditPending(is, intent, at);
|
|
757
771
|
episode = markResolved(episode, intentName);
|
|
758
772
|
is = { ...is, lastOutcomeAt: at };
|
|
@@ -805,7 +819,8 @@ function closeEpisode(state, at) {
|
|
|
805
819
|
const intents = { ...state.intents };
|
|
806
820
|
for (const [intentName, intent] of Object.entries(state.config.intents)) {
|
|
807
821
|
const record2 = episodeRecord(state.episode, intentName);
|
|
808
|
-
if (record2.resolved)
|
|
822
|
+
if (record2.resolved && (intent.episodePerOutcome !== true || record2.seen.length === 0))
|
|
823
|
+
continue;
|
|
809
824
|
let is = getIntentState(state, intentName, intent);
|
|
810
825
|
is = settlePending(is, intent, learning, at);
|
|
811
826
|
is = intent.attribution === void 0 ? recordEpisode(is, record2.seen, 0) : deferEpisode(is, record2.seen, at);
|