oracular 0.9.0 → 0.10.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 +18 -0
- package/README.md +3 -3
- package/dist/cli.js +56 -27
- package/dist/index.cjs +48 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -7
- package/dist/index.d.ts +16 -7
- package/dist/index.js +48 -27
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +11 -4
- package/dist/presets.cjs.map +1 -1
- package/dist/presets.d.cts +0 -17
- package/dist/presets.d.ts +0 -17
- package/dist/presets.js +11 -4
- package/dist/presets.js.map +1 -1
- package/package.json +1 -1
- package/schema/config.schema.json +4 -3
package/dist/index.d.cts
CHANGED
|
@@ -629,7 +629,9 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
|
|
|
629
629
|
* `w × log2(1 + 1/n)`, counted since the episode started or the intent's outcome last arrived.
|
|
630
630
|
* That is the default when `config.episodeTimeout` is set, and `IntentConfig.saturate` overrides
|
|
631
631
|
* it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
|
|
632
|
-
* outcome of: give the
|
|
632
|
+
* outcome of: give the signals remembered since the episode started or the outcome last arrived a
|
|
633
|
+
* hit and count the episode toward the base rate (a repeated outcome with nothing seen since
|
|
634
|
+
* counts nothing), credit
|
|
633
635
|
* the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
|
|
634
636
|
* each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
|
|
635
637
|
* or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
|
|
@@ -824,7 +826,9 @@ declare function serialize(state: EngineState): string;
|
|
|
824
826
|
/**
|
|
825
827
|
* Restores a state saved with {@link serialize}.
|
|
826
828
|
*
|
|
827
|
-
* Always pass the app's current `config`: it is validated and replaces the stored snapshot
|
|
829
|
+
* Always pass the app's current `config`: it is validated and replaces the stored snapshot (the
|
|
830
|
+
* stored one is read without the checks that refuse a config that cannot work, so a state saved
|
|
831
|
+
* before those checks existed still loads),
|
|
828
832
|
* intents and signals no longer declared are dropped (α and β are recomputed from the kept
|
|
829
833
|
* contributions), and newly declared intents start at their prior. States saved by oracular
|
|
830
834
|
* 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
|
|
@@ -863,10 +867,13 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
|
|
|
863
867
|
* @param config - Intents with their priors and signals, plus engine-wide options. Validated
|
|
864
868
|
* strictly: unknown keys, non-finite numbers and out-of-range values throw.
|
|
865
869
|
* @returns A new, JSON-serializable state. `config` is not mutated.
|
|
866
|
-
* @throws `TypeError` when a value has the wrong type
|
|
870
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
871
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on (the reset would wipe what
|
|
872
|
+
* the signal just added).
|
|
867
873
|
* @throws `RangeError` when a number is out of range, for example a `prior` outside (0, 1), a
|
|
868
|
-
* signal weight of 0,
|
|
869
|
-
* `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
874
|
+
* signal weight of 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight,
|
|
875
|
+
* `priorStrength`, `learning.gain` or `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
876
|
+
* overflow the arithmetic).
|
|
870
877
|
* @example
|
|
871
878
|
* ```ts
|
|
872
879
|
* const state = createState({
|
|
@@ -929,9 +936,11 @@ declare const ms: Units;
|
|
|
929
936
|
*
|
|
930
937
|
* @param config - The config to check. Typed `unknown` so parsed JSON can be passed as it is.
|
|
931
938
|
* @returns A fresh, normalized {@link EngineConfig}. `config` is not mutated.
|
|
932
|
-
* @throws `TypeError` when a value has the wrong type
|
|
939
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
940
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on.
|
|
933
941
|
* @throws `RangeError` when a number is out of range: a `prior` outside (0, 1), a signal weight of
|
|
934
|
-
* 0,
|
|
942
|
+
* 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight, `priorStrength`,
|
|
943
|
+
* `learning.gain` or `learning.priorEpisodes` beyond 1e6.
|
|
935
944
|
* @example
|
|
936
945
|
* ```ts
|
|
937
946
|
* import { readFileSync } from 'node:fs';
|
package/dist/index.d.ts
CHANGED
|
@@ -629,7 +629,9 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
|
|
|
629
629
|
* `w × log2(1 + 1/n)`, counted since the episode started or the intent's outcome last arrived.
|
|
630
630
|
* That is the default when `config.episodeTimeout` is set, and `IntentConfig.saturate` overrides
|
|
631
631
|
* it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
|
|
632
|
-
* outcome of: give the
|
|
632
|
+
* outcome of: give the signals remembered since the episode started or the outcome last arrived a
|
|
633
|
+
* hit and count the episode toward the base rate (a repeated outcome with nothing seen since
|
|
634
|
+
* counts nothing), credit
|
|
633
635
|
* the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
|
|
634
636
|
* each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
|
|
635
637
|
* or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
|
|
@@ -824,7 +826,9 @@ declare function serialize(state: EngineState): string;
|
|
|
824
826
|
/**
|
|
825
827
|
* Restores a state saved with {@link serialize}.
|
|
826
828
|
*
|
|
827
|
-
* Always pass the app's current `config`: it is validated and replaces the stored snapshot
|
|
829
|
+
* Always pass the app's current `config`: it is validated and replaces the stored snapshot (the
|
|
830
|
+
* stored one is read without the checks that refuse a config that cannot work, so a state saved
|
|
831
|
+
* before those checks existed still loads),
|
|
828
832
|
* intents and signals no longer declared are dropped (α and β are recomputed from the kept
|
|
829
833
|
* contributions), and newly declared intents start at their prior. States saved by oracular
|
|
830
834
|
* 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
|
|
@@ -863,10 +867,13 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
|
|
|
863
867
|
* @param config - Intents with their priors and signals, plus engine-wide options. Validated
|
|
864
868
|
* strictly: unknown keys, non-finite numbers and out-of-range values throw.
|
|
865
869
|
* @returns A new, JSON-serializable state. `config` is not mutated.
|
|
866
|
-
* @throws `TypeError` when a value has the wrong type
|
|
870
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
871
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on (the reset would wipe what
|
|
872
|
+
* the signal just added).
|
|
867
873
|
* @throws `RangeError` when a number is out of range, for example a `prior` outside (0, 1), a
|
|
868
|
-
* signal weight of 0,
|
|
869
|
-
* `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
874
|
+
* signal weight of 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight,
|
|
875
|
+
* `priorStrength`, `learning.gain` or `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
876
|
+
* overflow the arithmetic).
|
|
870
877
|
* @example
|
|
871
878
|
* ```ts
|
|
872
879
|
* const state = createState({
|
|
@@ -929,9 +936,11 @@ declare const ms: Units;
|
|
|
929
936
|
*
|
|
930
937
|
* @param config - The config to check. Typed `unknown` so parsed JSON can be passed as it is.
|
|
931
938
|
* @returns A fresh, normalized {@link EngineConfig}. `config` is not mutated.
|
|
932
|
-
* @throws `TypeError` when a value has the wrong type
|
|
939
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
940
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on.
|
|
933
941
|
* @throws `RangeError` when a number is out of range: a `prior` outside (0, 1), a signal weight of
|
|
934
|
-
* 0,
|
|
942
|
+
* 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight, `priorStrength`,
|
|
943
|
+
* `learning.gain` or `learning.priorEpisodes` beyond 1e6.
|
|
935
944
|
* @example
|
|
936
945
|
* ```ts
|
|
937
946
|
* import { readFileSync } from 'node:fs';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/math.ts
|
|
2
2
|
function decayFactor(elapsed, halfLife) {
|
|
3
|
-
if (!(elapsed > 0)
|
|
3
|
+
if (!(elapsed > 0)) return 1;
|
|
4
4
|
return 2 ** (-elapsed / halfLife);
|
|
5
5
|
}
|
|
6
6
|
function betaMean(alpha, beta) {
|
|
@@ -136,12 +136,11 @@ function mapRecord(record2, fn) {
|
|
|
136
136
|
var NONE = { hits: 0, misses: 0 };
|
|
137
137
|
function decayLearned(state, learning, at) {
|
|
138
138
|
if (state.learnedAt === null) return { ...state, learnedAt: at };
|
|
139
|
-
const learnedAt = Math.max(state.learnedAt, at);
|
|
140
139
|
const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
|
|
141
|
-
if (d === 1) return
|
|
140
|
+
if (d === 1) return at === state.learnedAt ? state : { ...state, learnedAt: at };
|
|
142
141
|
return {
|
|
143
142
|
...state,
|
|
144
|
-
learnedAt,
|
|
143
|
+
learnedAt: at,
|
|
145
144
|
learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
|
|
146
145
|
episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
|
|
147
146
|
};
|
|
@@ -250,13 +249,12 @@ function withContributions(state, intent, contributions) {
|
|
|
250
249
|
function decayIntent(state, intent, at) {
|
|
251
250
|
if (state.updatedAt === null) return { ...state, updatedAt: at };
|
|
252
251
|
const d = decayFactor(at - state.updatedAt, intent.halfLife);
|
|
253
|
-
|
|
254
|
-
if (d === 1) return updatedAt === state.updatedAt ? state : { ...state, updatedAt };
|
|
252
|
+
if (d === 1) return at === state.updatedAt ? state : { ...state, updatedAt: at };
|
|
255
253
|
const contributions = mapRecord(state.contributions, (c) => ({
|
|
256
254
|
alpha: c.alpha * d,
|
|
257
255
|
beta: c.beta * d
|
|
258
256
|
}));
|
|
259
|
-
return withContributions({ ...state, updatedAt }, intent, contributions);
|
|
257
|
+
return withContributions({ ...state, updatedAt: at }, intent, contributions);
|
|
260
258
|
}
|
|
261
259
|
function applySignal(state, intent, signal, weight) {
|
|
262
260
|
const prev = getOwn(state.contributions, signal) ?? { alpha: 0, beta: 0 };
|
|
@@ -273,6 +271,7 @@ var DEFAULT_Z = 1.28;
|
|
|
273
271
|
var DEFAULT_GAIN = 2;
|
|
274
272
|
var DEFAULT_PRIOR_EPISODES = 10;
|
|
275
273
|
var MAX_MAGNITUDE = 1e6;
|
|
274
|
+
var MAX_Z = 10;
|
|
276
275
|
var CONFIG_KEYS = ["intents", "episodeTimeout", "learning", "reveal"];
|
|
277
276
|
var INTENT_KEYS = [
|
|
278
277
|
"prior",
|
|
@@ -357,14 +356,17 @@ function expectKeyName(name, path) {
|
|
|
357
356
|
if (name === "__proto__") typeError(path, 'must not use the reserved name "__proto__"');
|
|
358
357
|
return name;
|
|
359
358
|
}
|
|
360
|
-
function normalizeRevealOptions(value, path) {
|
|
359
|
+
function normalizeRevealOptions(value, path, strict = true) {
|
|
361
360
|
const record2 = expectRecord(value, path);
|
|
362
361
|
expectKeys(record2, REVEAL_KEYS, path);
|
|
363
362
|
const out = {};
|
|
364
363
|
if (record2.threshold !== void 0) {
|
|
365
|
-
out.threshold = expectClosedUnit(record2.threshold, `${path}.threshold`);
|
|
364
|
+
out.threshold = strict ? expectOpenUnit(record2.threshold, `${path}.threshold`) : expectClosedUnit(record2.threshold, `${path}.threshold`);
|
|
365
|
+
}
|
|
366
|
+
if (record2.z !== void 0) {
|
|
367
|
+
out.z = expectNonNegative(record2.z, `${path}.z`);
|
|
368
|
+
if (strict && out.z > MAX_Z) rangeError(`${path}.z`, `must be at most ${MAX_Z}`);
|
|
366
369
|
}
|
|
367
|
-
if (record2.z !== void 0) out.z = expectNonNegative(record2.z, `${path}.z`);
|
|
368
370
|
if (record2.cooldown !== void 0) {
|
|
369
371
|
out.cooldown = expectNonNegative(record2.cooldown, `${path}.cooldown`);
|
|
370
372
|
}
|
|
@@ -394,7 +396,7 @@ function normalizeLearning(value, path) {
|
|
|
394
396
|
}
|
|
395
397
|
return out;
|
|
396
398
|
}
|
|
397
|
-
function normalizeIntentConfig(value, path) {
|
|
399
|
+
function normalizeIntentConfig(value, path, strict = true) {
|
|
398
400
|
const record2 = expectRecord(value, path);
|
|
399
401
|
expectKeys(record2, INTENT_KEYS, path);
|
|
400
402
|
const signalsRecord = expectRecord(record2.signals, `${path}.signals`);
|
|
@@ -436,11 +438,18 @@ function normalizeIntentConfig(value, path) {
|
|
|
436
438
|
if (record2.attribution !== void 0) {
|
|
437
439
|
out.attribution = expectPositive(record2.attribution, `${path}.attribution`);
|
|
438
440
|
}
|
|
439
|
-
if (record2.reveal !== void 0)
|
|
440
|
-
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal
|
|
441
|
+
if (record2.reveal !== void 0) {
|
|
442
|
+
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal`, strict);
|
|
443
|
+
}
|
|
444
|
+
if (strict && out.outcome !== void 0 && hasOwn(signals, out.outcome) && out.resetOnOutcome !== false) {
|
|
445
|
+
typeError(
|
|
446
|
+
`${path}.outcome`,
|
|
447
|
+
"is also one of its signals: the reset on outcome would wipe what it adds; set resetOnOutcome: false to keep it"
|
|
448
|
+
);
|
|
449
|
+
}
|
|
441
450
|
return out;
|
|
442
451
|
}
|
|
443
|
-
function normalizeConfig(value) {
|
|
452
|
+
function normalizeConfig(value, strict = true) {
|
|
444
453
|
const record2 = expectRecord(value, "config");
|
|
445
454
|
expectKeys(record2, CONFIG_KEYS, "config");
|
|
446
455
|
const intentsRecord = expectRecord(record2.intents, "config.intents");
|
|
@@ -451,7 +460,8 @@ function normalizeConfig(value) {
|
|
|
451
460
|
expectKeyName(name, "config.intents");
|
|
452
461
|
intents[name] = normalizeIntentConfig(
|
|
453
462
|
intentsRecord[name],
|
|
454
|
-
`config.intents[${JSON.stringify(name)}]
|
|
463
|
+
`config.intents[${JSON.stringify(name)}]`,
|
|
464
|
+
strict
|
|
455
465
|
);
|
|
456
466
|
}
|
|
457
467
|
const out = { intents };
|
|
@@ -460,8 +470,9 @@ function normalizeConfig(value) {
|
|
|
460
470
|
}
|
|
461
471
|
if (record2.learning !== void 0)
|
|
462
472
|
out.learning = normalizeLearning(record2.learning, "config.learning");
|
|
463
|
-
if (record2.reveal !== void 0)
|
|
464
|
-
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal");
|
|
473
|
+
if (record2.reveal !== void 0) {
|
|
474
|
+
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal", strict);
|
|
475
|
+
}
|
|
465
476
|
return out;
|
|
466
477
|
}
|
|
467
478
|
function normalizeExplainOptions(value, path) {
|
|
@@ -491,12 +502,12 @@ function splitEvaluateOptions(value, path) {
|
|
|
491
502
|
else explain2[key] = v;
|
|
492
503
|
}
|
|
493
504
|
return {
|
|
494
|
-
reveal: normalizeRevealOptions(reveal, path),
|
|
505
|
+
reveal: normalizeRevealOptions(reveal, path, false),
|
|
495
506
|
explain: normalizeExplainOptions(explain2, path)
|
|
496
507
|
};
|
|
497
508
|
}
|
|
498
509
|
function resolveReveal(config, intent, opts, normalized = false) {
|
|
499
|
-
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options");
|
|
510
|
+
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options", false);
|
|
500
511
|
return {
|
|
501
512
|
threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
|
|
502
513
|
z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
|
|
@@ -534,11 +545,7 @@ function timedOutAt(episode, timeout, at) {
|
|
|
534
545
|
return at - episode.lastEventAt > timeout ? episode.lastEventAt + timeout : null;
|
|
535
546
|
}
|
|
536
547
|
function extendEpisode(episode, at) {
|
|
537
|
-
return {
|
|
538
|
-
startedAt: episode.startedAt ?? at,
|
|
539
|
-
lastEventAt: episode.lastEventAt === null ? at : Math.max(episode.lastEventAt, at),
|
|
540
|
-
intents: episode.intents
|
|
541
|
-
};
|
|
548
|
+
return { startedAt: episode.startedAt ?? at, lastEventAt: at, intents: episode.intents };
|
|
542
549
|
}
|
|
543
550
|
function episodeRecord(episode, intent) {
|
|
544
551
|
return getOwn(episode.intents, intent) ?? { seen: [], counts: {}, resolved: false };
|
|
@@ -596,7 +603,7 @@ function assessIntent(state, name, intent, learning, at) {
|
|
|
596
603
|
}
|
|
597
604
|
function cooldownEnd(since, cooldown, at) {
|
|
598
605
|
if (since === null) return null;
|
|
599
|
-
const end = since + cooldown;
|
|
606
|
+
const end = Math.min(since, at) + cooldown;
|
|
600
607
|
return at < end ? end : null;
|
|
601
608
|
}
|
|
602
609
|
function buildPrediction(is, name, reveal, at) {
|
|
@@ -724,6 +731,7 @@ function observe(state, event) {
|
|
|
724
731
|
let is = getIntentState(s, intentName, intent);
|
|
725
732
|
is = decayIntent(is, intent, at);
|
|
726
733
|
is = settlePending(is, intent, learning, at);
|
|
734
|
+
is = pinAnchors(is, at);
|
|
727
735
|
if (isSignal) {
|
|
728
736
|
const { effective } = signalWeights(is, intent, learning, name);
|
|
729
737
|
const n = occurrences(episode, intentName, name) + 1;
|
|
@@ -733,7 +741,8 @@ function observe(state, event) {
|
|
|
733
741
|
if (!isOutcome) episode = markSeen(episode, intentName, name);
|
|
734
742
|
}
|
|
735
743
|
if (isOutcome) {
|
|
736
|
-
|
|
744
|
+
const record2 = episodeRecord(episode, intentName);
|
|
745
|
+
if (!record2.resolved || record2.seen.length > 0) is = recordEpisode(is, record2.seen, 1);
|
|
737
746
|
is = creditPending(is, intent, at);
|
|
738
747
|
episode = markResolved(episode, intentName);
|
|
739
748
|
is = { ...is, lastOutcomeAt: at };
|
|
@@ -743,6 +752,16 @@ function observe(state, event) {
|
|
|
743
752
|
}
|
|
744
753
|
return { ...s, intents, episode };
|
|
745
754
|
}
|
|
755
|
+
function pinAnchors(is, at) {
|
|
756
|
+
const reveal = is.lastRevealedAt !== null && is.lastRevealedAt > at;
|
|
757
|
+
const outcome = is.lastOutcomeAt !== null && is.lastOutcomeAt > at;
|
|
758
|
+
if (!reveal && !outcome) return is;
|
|
759
|
+
return {
|
|
760
|
+
...is,
|
|
761
|
+
lastRevealedAt: reveal ? at : is.lastRevealedAt,
|
|
762
|
+
lastOutcomeAt: outcome ? at : is.lastOutcomeAt
|
|
763
|
+
};
|
|
764
|
+
}
|
|
746
765
|
function endEpisode(state, at) {
|
|
747
766
|
const s = requireState(state);
|
|
748
767
|
const t = expectTimestamp(at, "at");
|
|
@@ -1060,7 +1079,9 @@ function parseState(value) {
|
|
|
1060
1079
|
}
|
|
1061
1080
|
return {
|
|
1062
1081
|
version: STATE_VERSION,
|
|
1063
|
-
|
|
1082
|
+
// Without the strict checks: a config this release would refuse at createState still loads
|
|
1083
|
+
// when it is only stored, and the config passed in replaces it anyway.
|
|
1084
|
+
config: normalizeConfig(r.config, false),
|
|
1064
1085
|
intents: mapOwn(r.intents, "state.intents", intentState),
|
|
1065
1086
|
episode: episodeState(r.episode, "state.episode")
|
|
1066
1087
|
};
|