@adam0white/human-framework 2.11.0 → 2.13.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/dist/agenda/agenda.js +4 -1
- package/dist/cognition/cognition.js +8 -3
- package/dist/conscience/conscience.js +10 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/migrate.js +11 -1
- package/dist/person.d.ts +6 -0
- package/dist/person.js +17 -3
- package/dist/prospection/prospection.d.ts +17 -1
- package/dist/prospection/prospection.js +118 -35
- package/dist/restore.js +4 -2
- package/dist/types.d.ts +30 -3
- package/dist/types.js +1 -1
- package/dist/will/will.d.ts +7 -0
- package/dist/will/will.js +47 -1
- package/package.json +1 -1
package/dist/agenda/agenda.js
CHANGED
|
@@ -154,8 +154,11 @@ function sanitizeGoalTemporal(g) {
|
|
|
154
154
|
else if (g.temporal)
|
|
155
155
|
g.temporal = { halfLifeMinutes: g.temporal.halfLifeMinutes };
|
|
156
156
|
}
|
|
157
|
-
/** Restore-time check of
|
|
157
|
+
/** Restore-time check of optional lapse and firm flags; malformed values are dropped. @internal */
|
|
158
158
|
export function sanitizeAgenda(a) {
|
|
159
|
+
for (const c of a.commitments)
|
|
160
|
+
if (isObj(c) && (c.firm !== true || c.kind !== 'abstain'))
|
|
161
|
+
delete c.firm;
|
|
159
162
|
for (const g of a.goals)
|
|
160
163
|
if (isObj(g))
|
|
161
164
|
sanitizeGoalTemporal(g);
|
|
@@ -248,6 +248,12 @@ function materialValue(p, material, scarcity) {
|
|
|
248
248
|
const sat = Math.sign(material) * (1 - dexp(-Math.abs(material) / scale));
|
|
249
249
|
return K.materialWeight * sat * 2 * weight * (1 + K.scarcityMaterialGain * scarcity);
|
|
250
250
|
}
|
|
251
|
+
/** Shared whole-route valuation for scoring and the read-only plan readout. @internal */
|
|
252
|
+
export function prospectiveRoute(p, aff, ctx) {
|
|
253
|
+
const scarcity = clamp01(Number.isFinite(ctx.scarcity) ? ctx.scarcity : 0);
|
|
254
|
+
return receivedGoalRoute(p, aff, ctx.now, 0.5 + 0.5 * clamp01(ctx.mods.maturity), (successor) => effortValue(p, successor, ctx) +
|
|
255
|
+
(successor.material ? materialValue(p, successor.material, scarcity) : 0));
|
|
256
|
+
}
|
|
251
257
|
/** Needs whose consumption can overfill (2.8.0 fullness): what `body.consume` feeds. */
|
|
252
258
|
const FULL_NEEDS = ['food', 'water', 'relief'];
|
|
253
259
|
/** Score one option. Pure apart from reads; terms are rounded for stable snapshots. */
|
|
@@ -328,12 +334,11 @@ export function consider(p, aff, ctx) {
|
|
|
328
334
|
}
|
|
329
335
|
const maturity = clamp01(ctx.mods.maturity);
|
|
330
336
|
const maturityScale = 0.5 + 0.5 * maturity;
|
|
331
|
-
const received =
|
|
332
|
-
(successor.material ? materialValue(p, successor.material, scarcity) : 0));
|
|
337
|
+
const received = prospectiveRoute(p, aff, ctx);
|
|
333
338
|
for (const t of agendaTermsWithGoalEstimates(p, aff, ctx.now, received?.goals))
|
|
334
339
|
push(t.source, t.value * maturityScale);
|
|
335
340
|
if (received)
|
|
336
|
-
push(received.cost.source, received.cost.value);
|
|
341
|
+
push(received.cost.source, received.cost.value, received.cost.ref);
|
|
337
342
|
// Habit: cue-matched automaticity (cued by the option's own place), stronger before planning matures.
|
|
338
343
|
const habit = p.habits.length === 0
|
|
339
344
|
? 0
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* desperation threshold, and only for acts meeting a bodily need; this is not the fast's own exemption for
|
|
23
23
|
* illness and travel (2:184-185, applied in the agenda) and not the 2:173 necessity exception for forbidden
|
|
24
24
|
* food. Below the threshold the abstention acts only as a negative term (agenda), so a weakly convinced person
|
|
25
|
-
* can be tempted.
|
|
25
|
+
* can be tempted. An opt-in `firm: true` self abstention without a linked obligatory norm instead vetoes
|
|
26
|
+
* as `rule:<label>` (id fallback). It uses the same open-window, exemption and bodily survival checks,
|
|
27
|
+
* including for self choice and insistence. It models a held rule, not a new religious ruling.
|
|
28
|
+
* It does not model the person's later judgment of a broken fast beyond the recorded breach.
|
|
26
29
|
*/
|
|
27
30
|
import { clamp01 } from '../core/index.js';
|
|
28
31
|
import { DEFAULT_NORMS, NORM_SCOPE } from './catalog.js';
|
|
@@ -251,11 +254,15 @@ export function abstentionVeto(p, aff, now, capacityLifted = false) {
|
|
|
251
254
|
return undefined;
|
|
252
255
|
const end = now + Math.max(0, aff.duration);
|
|
253
256
|
for (const c of p.agenda?.commitments ?? []) {
|
|
254
|
-
if (c.kind !== 'abstain' || c.status !== 'pending' || c.exempt !== undefined
|
|
257
|
+
if (c.kind !== 'abstain' || c.status !== 'pending' || c.exempt !== undefined)
|
|
255
258
|
continue;
|
|
256
259
|
if (!(c.violatedBy?.includes(aff.action) ?? false) || now >= c.until || end <= c.from)
|
|
257
260
|
continue;
|
|
258
|
-
const held = heldNorm(p, c.normId);
|
|
261
|
+
const held = c.normId === undefined ? undefined : heldNorm(p, c.normId);
|
|
262
|
+
if (c.firm === true &&
|
|
263
|
+
(c.toId === undefined || c.toId === 'self' || c.toId === p.id) &&
|
|
264
|
+
held?.standing !== 'obligatory')
|
|
265
|
+
return { kind: 'willNot', reason: `rule:${c.label ?? c.id}` };
|
|
259
266
|
if (held?.standing !== 'obligatory' || held.conviction < CONSCIENCE_DEFAULTS.abstainVetoConviction)
|
|
260
267
|
continue;
|
|
261
268
|
return { kind: 'willNot', reason: `duty:${c.normId}` };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export declare const FRAMEWORK_VERSION = "2.
|
|
2
|
+
export declare const FRAMEWORK_VERSION = "2.13.0";
|
|
3
3
|
export type { CrisisEvent, PracticeKind } from './affect/index.ts';
|
|
4
4
|
export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.ts';
|
|
5
5
|
export type { GoalTemplate, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
|
|
@@ -30,8 +30,8 @@ export * from './needs/index.ts';
|
|
|
30
30
|
export * from './partnering/index.ts';
|
|
31
31
|
export * from './person.ts';
|
|
32
32
|
export * from './promptings/index.ts';
|
|
33
|
-
export type { ConsequenceEvidence } from './prospection/index.ts';
|
|
34
|
-
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
|
|
33
|
+
export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
|
|
34
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.ts';
|
|
35
35
|
export { fingerprint, restore, snapshot } from './restore.ts';
|
|
36
36
|
export * from './review/index.ts';
|
|
37
37
|
export * from './scenarios/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
2
|
+
export const FRAMEWORK_VERSION = '2.13.0';
|
|
3
3
|
export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
|
|
4
4
|
// `agenda.release` collides with `affect.release`; the agenda one is re-exported under a clearer name.
|
|
5
5
|
export { AGENDA_DEFAULTS, abandonGoal, adoptGoal, advanceAgenda, agendaTerms, applyExemptions, CARE_DEFAULTS, calendarRetimer, careDuty, commitmentPressure, createAgenda, DEFAULT_PRAYER_TIMES, EID_PRAYER_IMPORTANCE, EXEMPTION_DEFAULTS, eidPrayer, eidWindow, fastWindow, iftarWindow, inMakruhTime, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
|
|
@@ -27,7 +27,7 @@ export * from './needs/index.js';
|
|
|
27
27
|
export * from './partnering/index.js';
|
|
28
28
|
export * from './person.js';
|
|
29
29
|
export * from './promptings/index.js';
|
|
30
|
-
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, } from './prospection/index.js';
|
|
30
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.js';
|
|
31
31
|
export { fingerprint, restore, snapshot } from './restore.js';
|
|
32
32
|
export * from './review/index.js';
|
|
33
33
|
export * from './scenarios/index.js';
|
package/dist/migrate.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* version; fullness stays absent unless enabled, and alongside partners and reconsideration are opt-in.
|
|
19
19
|
* 2.8.0 to 2.9.0 stamps the version; prompting cues and practice half-lives stay absent unless supplied,
|
|
20
20
|
* review terms may carry refs, and an outcome naming several commitments now keeps each rather than only
|
|
21
|
-
* the earliest-ending one. 2.10.0 to 2.11.0 stamps the version (curiosity starts absent). 2.9.0 to 2.10.0 stamps the version; belief provenance, noted contradictions and
|
|
21
|
+
* the earliest-ending one. 2.12.0 to 2.13.0 stamps the version (suggestion reasons and firm rules start absent). 2.11.0 to 2.12.0 stamps the version (planning starts absent). 2.10.0 to 2.11.0 stamps the version (curiosity starts absent). 2.9.0 to 2.10.0 stamps the version; belief provenance, noted contradictions and
|
|
22
22
|
* believed regard stay absent unless a host supplies them;
|
|
23
23
|
* `regard` becomes a reserved impression cue key. A migrated save continues under the current engine's rules: it restores and runs,
|
|
24
24
|
* but it does not reproduce what the old engine would have done next where the rules changed (1.4.0 to 1.5.0
|
|
@@ -70,6 +70,16 @@ export const MIGRATIONS = {
|
|
|
70
70
|
note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
|
|
71
71
|
apply: renameAmbient,
|
|
72
72
|
},
|
|
73
|
+
'2.12.0': {
|
|
74
|
+
to: '2.13.0',
|
|
75
|
+
note: 'suggestion reasons (Person.will.suggestionReasons) and firm self-rules (Commitment.firm) are optional and start absent; existing hosts retain exact behavior',
|
|
76
|
+
apply: stamp,
|
|
77
|
+
},
|
|
78
|
+
'2.11.0': {
|
|
79
|
+
to: '2.12.0',
|
|
80
|
+
note: 'the planning slice (Person.planning) is optional and starts absent; retained consequences may carry channel known only when seeded; existing hosts retain exact behavior',
|
|
81
|
+
apply: stamp,
|
|
82
|
+
},
|
|
73
83
|
'2.10.0': {
|
|
74
84
|
to: '2.11.0',
|
|
75
85
|
note: 'the curiosity slice (Person.curiosity) is optional and starts absent; existing hosts retain exact behavior',
|
package/dist/person.d.ts
CHANGED
|
@@ -136,6 +136,12 @@ export declare function answerNow(p: Person, affordances: readonly Affordance[],
|
|
|
136
136
|
resolution: SuggestionResolution;
|
|
137
137
|
booked: boolean;
|
|
138
138
|
};
|
|
139
|
+
/**
|
|
140
|
+
* Read the best retained-claim route using ordinary scoring costs (scarcity absent).
|
|
141
|
+
* Does not mutate, advance time or consume RNG. `now` values timing against the held state.
|
|
142
|
+
* Returns undefined when no route improves on direct goal value.
|
|
143
|
+
*/
|
|
144
|
+
export declare function planRoute(p: Person, aff: Affordance, now?: Minute): import("./index.ts").PlanRoute | undefined;
|
|
139
145
|
/** Every resolution of a decision record (all voices), oldest API first. */
|
|
140
146
|
export declare const resolutionsOf: (r: {
|
|
141
147
|
suggestion?: SuggestionResolution;
|
package/dist/person.js
CHANGED
|
@@ -25,7 +25,7 @@ import { advanceBeliefs, applyContradiction, attend, believe, confirm, credence,
|
|
|
25
25
|
import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, enableFullness, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
|
|
26
26
|
import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSocial, } from './character/index.js';
|
|
27
27
|
import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
|
|
28
|
-
import { decide as cognitionDecide, desperationOf, scoreAll } from './cognition/index.js';
|
|
28
|
+
import { decide as cognitionDecide, desperationOf, prospectiveRoute, scoreAll } from './cognition/index.js';
|
|
29
29
|
import { createConscience, guiltLoad, heldNorms, normVeto, recordDeed, recordRepair, repent, } from './conscience/index.js';
|
|
30
30
|
import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay } from './core/index.js';
|
|
31
31
|
import { enableCuriosity, noteCuriosity } from './curiosity/index.js';
|
|
@@ -38,12 +38,12 @@ import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
|
|
|
38
38
|
import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
|
|
39
39
|
import { widow } from './partnering/index.js';
|
|
40
40
|
import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
|
|
41
|
-
import { bodyForecastFor, consumeConsequences, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
41
|
+
import { bodyForecastFor, consumeConsequences, enablePlanning, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
42
42
|
import { enableReview, reviewIfDue } from './review/index.js';
|
|
43
43
|
import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
|
|
44
44
|
import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, noteKnownBy, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
|
|
45
45
|
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.js';
|
|
46
|
-
import { advanceWill, answerSuggestion, chargeCommand, commandTargets, createWill, creditedVoices, dischargeAdvice, dutyReviewAt, endCommand, learnFromVoice, noteCommandOutcome, predictResponse, rememberAdvice, resolveCommand, takeCommand, voicesIn, WILL_DEFAULTS, wakeReviewAt, } from './will/index.js';
|
|
46
|
+
import { advanceWill, answerSuggestion, chargeCommand, commandTargets, createWill, creditedVoices, dischargeAdvice, dutyReviewAt, enableSuggestionReasons, endCommand, learnFromVoice, noteCommandOutcome, predictResponse, rememberAdvice, resolveCommand, takeCommand, voicesIn, WILL_DEFAULTS, wakeReviewAt, } from './will/index.js';
|
|
47
47
|
export const PERSON_DEFAULTS = {
|
|
48
48
|
/** Body load while idle and awake. */
|
|
49
49
|
idleLoad: { effort: 0.05, focus: 0.05, mode: 'awake' },
|
|
@@ -148,6 +148,8 @@ export function createPerson(spec) {
|
|
|
148
148
|
if (family)
|
|
149
149
|
p.family = family;
|
|
150
150
|
const on = spec.enable;
|
|
151
|
+
if (on?.suggestionReasons)
|
|
152
|
+
enableSuggestionReasons(p);
|
|
151
153
|
if (on?.gists)
|
|
152
154
|
enableGists(p);
|
|
153
155
|
if (on?.yearbook)
|
|
@@ -162,6 +164,8 @@ export function createPerson(spec) {
|
|
|
162
164
|
enableFullness(p);
|
|
163
165
|
if (on?.curiosity)
|
|
164
166
|
enableCuriosity(p, on.curiosity === true ? {} : on.curiosity);
|
|
167
|
+
if (on?.planning)
|
|
168
|
+
enablePlanning(p, on.planning);
|
|
165
169
|
if (on?.promptings)
|
|
166
170
|
enablePromptings(p, on.promptings);
|
|
167
171
|
if (on?.review)
|
|
@@ -852,6 +856,16 @@ export function answerNow(p, affordances, suggestion, opts = {}) {
|
|
|
852
856
|
}
|
|
853
857
|
return out;
|
|
854
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Read the best retained-claim route using ordinary scoring costs (scarcity absent).
|
|
861
|
+
* Does not mutate, advance time or consume RNG. `now` values timing against the held state.
|
|
862
|
+
* Returns undefined when no route improves on direct goal value.
|
|
863
|
+
*/
|
|
864
|
+
export function planRoute(p, aff, now = p.now) {
|
|
865
|
+
const held = now === p.now ? p : { ...p, now };
|
|
866
|
+
const { ctx } = decisionInputs(held, [aff], 'plan-readout', {});
|
|
867
|
+
return prospectiveRoute(held, aff, ctx)?.route;
|
|
868
|
+
}
|
|
855
869
|
/** Every resolution of a decision record (all voices), oldest API first. */
|
|
856
870
|
export const resolutionsOf = (r) => r.suggestions ?? (r.suggestion ? [r.suggestion] : []);
|
|
857
871
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Affordance, ConsequenceRevision, Minute, Person, RetainedConsequence } from '../types.ts';
|
|
1
|
+
import type { Affordance, ConsequenceRevision, Minute, Person, PlanningSpec, ReceivedConsequence, RetainedConsequence } from '../types.ts';
|
|
2
2
|
export declare const PROSPECTION_DEFAULTS: {
|
|
3
3
|
maxEntries: number;
|
|
4
4
|
maxRevisions: number;
|
|
@@ -7,6 +7,10 @@ export declare const PROSPECTION_DEFAULTS: {
|
|
|
7
7
|
horizon: number;
|
|
8
8
|
maxMaterialCost: number;
|
|
9
9
|
};
|
|
10
|
+
/** Enable bounded planning. Invalid configuration throws without changing the person. */
|
|
11
|
+
export declare function enablePlanning(p: Person, spec?: PlanningSpec): void;
|
|
12
|
+
/** Seed what the person already knows. Uses claim confidence/chance, without source trust or attention. */
|
|
13
|
+
export declare function seedConsequences(p: Person, sourceId: string, claims: readonly ReceivedConsequence[]): boolean;
|
|
10
14
|
export interface ConsequenceEvidence {
|
|
11
15
|
entry: RetainedConsequence;
|
|
12
16
|
matches: boolean;
|
|
@@ -21,3 +25,15 @@ export declare function consequenceEvidence(p: Person, aff: Affordance, now?: Mi
|
|
|
21
25
|
entries: ConsequenceEvidence[];
|
|
22
26
|
revisions: ConsequenceRevision[];
|
|
23
27
|
};
|
|
28
|
+
/** One successor in a selected route. Times and access are unrounded estimates. */
|
|
29
|
+
export interface PlanStep {
|
|
30
|
+
successor: ReceivedConsequence['successor'];
|
|
31
|
+
start: Minute;
|
|
32
|
+
end: Minute;
|
|
33
|
+
access: number;
|
|
34
|
+
}
|
|
35
|
+
/** Plain JSON readout of the best whole route; goals are unscaled per-goal values. */
|
|
36
|
+
export interface PlanRoute {
|
|
37
|
+
steps: PlanStep[];
|
|
38
|
+
goals: Record<string, number>;
|
|
39
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SCOPE:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* bounds, not fitted psychological estimates.
|
|
2
|
+
* SCOPE: retained current-action → successor claims, acquired through attended external
|
|
3
|
+
* testimony or host-seeded prior knowledge. Knowledge is person-owned, source-labelled,
|
|
4
|
+
* bounded and saved. Opt-in planning searches chains of retained claims, depth first in
|
|
5
|
+
* stable source/id order, with total depth 2..5 and 64 feasible node expansions.
|
|
6
|
+
* Shapes: depth limits and pruning (Huys et al. 2012; Keramati et al. 2016), resource-rational
|
|
7
|
+
* planning (Callaway et al. 2022), quasi-hyperbolic present bias (Laibson 1997;
|
|
8
|
+
* O'Donoghue and Rabin 1999); see research/curiosity-and-play.md §Planning.
|
|
9
|
+
* Parameters are engineering bounds, not fitted psychological estimates.
|
|
10
|
+
* Without planning, only one successor is considered. One coherent route contributes;
|
|
11
|
+
* it never executes actions, grants consent or advances goals. Actual completed triggers
|
|
12
|
+
* consume opportunities. This is not a general planner: it does not search the host's
|
|
13
|
+
* world, infer procedures, execute plans or model another person's agency, future bodies
|
|
14
|
+
* or waiting opportunity cost. Revision memory is bounded, not an audit.
|
|
10
15
|
*/
|
|
11
16
|
import { temporalGoalValue } from '../agenda/agenda.js';
|
|
12
17
|
import { trustOf } from '../beliefs/index.js';
|
|
@@ -21,6 +26,32 @@ export const PROSPECTION_DEFAULTS = {
|
|
|
21
26
|
horizon: 7 * MINUTES_PER_DAY,
|
|
22
27
|
maxMaterialCost: 1_000_000,
|
|
23
28
|
};
|
|
29
|
+
/** Validate the optional planning configuration. Malformed saved state is dropped. @internal */
|
|
30
|
+
export function sanitizePlanning(input) {
|
|
31
|
+
if (!isObj(input) ||
|
|
32
|
+
!Number.isInteger(input.depth) ||
|
|
33
|
+
!isNum(input.depth) ||
|
|
34
|
+
input.depth < 2 ||
|
|
35
|
+
input.depth > 5)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (input.presentBias !== undefined &&
|
|
38
|
+
(!isNum(input.presentBias) || input.presentBias <= 0 || input.presentBias > 1))
|
|
39
|
+
return undefined;
|
|
40
|
+
return {
|
|
41
|
+
depth: input.depth,
|
|
42
|
+
...(input.presentBias !== undefined ? { presentBias: input.presentBias } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Enable bounded planning. Invalid configuration throws without changing the person. */
|
|
46
|
+
export function enablePlanning(p, spec = {}) {
|
|
47
|
+
const state = sanitizePlanning({ ...spec, depth: spec.depth ?? 4 });
|
|
48
|
+
if (!state)
|
|
49
|
+
throw new RangeError('Planning requires depth 2..5 and presentBias in (0, 1]');
|
|
50
|
+
p.planning = state;
|
|
51
|
+
}
|
|
52
|
+
const maxEntries = (planning) => (planning ? 64 : PROSPECTION_DEFAULTS.maxEntries);
|
|
53
|
+
const maxBatch = (p) => (p.planning ? 16 : PROSPECTION_DEFAULTS.maxBatch);
|
|
54
|
+
const reliability = (p, e) => e.channel === 'known' ? 1 : trustOf(p, e.sourceId);
|
|
24
55
|
const id = (x) => typeof x === 'string' &&
|
|
25
56
|
x.length > 0 &&
|
|
26
57
|
x.length <= PROSPECTION_DEFAULTS.maxIdentifierLength &&
|
|
@@ -90,7 +121,11 @@ function consequence(x, receivedAt) {
|
|
|
90
121
|
};
|
|
91
122
|
}
|
|
92
123
|
function retained(x, now) {
|
|
93
|
-
if (!isObj(x) ||
|
|
124
|
+
if (!isObj(x) ||
|
|
125
|
+
!id(x.sourceId) ||
|
|
126
|
+
(x.channel !== 'told' && x.channel !== 'known') ||
|
|
127
|
+
!minute(x.receivedAt) ||
|
|
128
|
+
x.receivedAt > now)
|
|
94
129
|
return undefined;
|
|
95
130
|
const c = consequence(x, x.receivedAt);
|
|
96
131
|
if (!c ||
|
|
@@ -100,7 +135,7 @@ function retained(x, now) {
|
|
|
100
135
|
return {
|
|
101
136
|
...c,
|
|
102
137
|
sourceId: x.sourceId,
|
|
103
|
-
channel:
|
|
138
|
+
channel: x.channel,
|
|
104
139
|
receivedAt: x.receivedAt,
|
|
105
140
|
...(x.triggeredAt !== undefined ? { triggeredAt: x.triggeredAt } : {}),
|
|
106
141
|
};
|
|
@@ -132,11 +167,18 @@ export function receiveConsequences(p, pc) {
|
|
|
132
167
|
pc.actorId === p.id ||
|
|
133
168
|
(pc.targetId !== undefined && pc.targetId !== p.id))
|
|
134
169
|
return false;
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
return acceptConsequences(p, pc.actorId, pc.consequences ?? [], pc.withdrawConsequences ?? [], 'told');
|
|
171
|
+
}
|
|
172
|
+
/** Seed what the person already knows. Uses claim confidence/chance, without source trust or attention. */
|
|
173
|
+
export function seedConsequences(p, sourceId, claims) {
|
|
174
|
+
if (!id(sourceId) || sourceId === p.id)
|
|
175
|
+
return false;
|
|
176
|
+
return acceptConsequences(p, sourceId, claims, [], 'known');
|
|
177
|
+
}
|
|
178
|
+
function acceptConsequences(p, sourceId, adds, removes, channel) {
|
|
137
179
|
if (!Array.isArray(adds) ||
|
|
138
180
|
!Array.isArray(removes) ||
|
|
139
|
-
adds.length + removes.length >
|
|
181
|
+
adds.length + removes.length > maxBatch(p) ||
|
|
140
182
|
adds.length + removes.length === 0)
|
|
141
183
|
return false;
|
|
142
184
|
const additions = adds.map((x) => consequence(x, p.now));
|
|
@@ -148,7 +190,6 @@ export function receiveConsequences(p, pc) {
|
|
|
148
190
|
if (new Set(batch.map((x) => x.id)).size !== batch.length)
|
|
149
191
|
return false;
|
|
150
192
|
const state = cleaned(structuredClone(p.prospection ?? { entries: [], revisions: [] }), p.now);
|
|
151
|
-
const sourceId = pc.actorId;
|
|
152
193
|
const updates = batch.filter((b) => !state.revisions.some((r) => r.sourceId === sourceId && r.id === b.id && r.revision >= (b.revision ?? 0)));
|
|
153
194
|
const newIds = updates.filter((b) => !state.revisions.some((r) => r.sourceId === sourceId && r.id === b.id));
|
|
154
195
|
// Never evict a still-valid tombstone to admit a batch; dedup holds within the stated horizon.
|
|
@@ -159,13 +200,13 @@ export function receiveConsequences(p, pc) {
|
|
|
159
200
|
state.revisions = state.revisions.filter((r) => !(r.sourceId === sourceId && r.id === b.id));
|
|
160
201
|
const addition = additions.find((a) => a?.id === b.id);
|
|
161
202
|
if (addition)
|
|
162
|
-
state.entries.push({ ...addition, sourceId, channel
|
|
203
|
+
state.entries.push({ ...addition, sourceId, channel, receivedAt: p.now });
|
|
163
204
|
state.revisions.push({ sourceId, id: b.id, revision: b.revision, receivedAt: p.now });
|
|
164
205
|
}
|
|
165
|
-
if (state.entries.length >
|
|
206
|
+
if (state.entries.length > maxEntries(p.planning !== undefined)) {
|
|
166
207
|
const oldest = [...state.entries]
|
|
167
208
|
.sort((a, b) => Number(b.triggeredAt !== undefined) - Number(a.triggeredAt !== undefined) || order(a, b))
|
|
168
|
-
.slice(0, state.entries.length -
|
|
209
|
+
.slice(0, state.entries.length - maxEntries(p.planning !== undefined));
|
|
169
210
|
state.entries = state.entries.filter((e) => !oldest.includes(e));
|
|
170
211
|
}
|
|
171
212
|
store(p, state);
|
|
@@ -190,7 +231,7 @@ export function consequenceEvidence(p, aff, now = p.now) {
|
|
|
190
231
|
now <= entry.expiresAt &&
|
|
191
232
|
end <= entry.availableUntil &&
|
|
192
233
|
Number.isFinite(end);
|
|
193
|
-
const accessProbability = expectedEffect(p, aff).successRate * entry.chance * entry.confidence *
|
|
234
|
+
const accessProbability = expectedEffect(p, aff).successRate * entry.chance * entry.confidence * reliability(p, entry);
|
|
194
235
|
return {
|
|
195
236
|
entry: structuredClone(entry),
|
|
196
237
|
matches: match,
|
|
@@ -223,30 +264,72 @@ export function receivedGoalRoute(p, aff, now, maturityScale, costs) {
|
|
|
223
264
|
const baseline = direct.reduce((sum, value) => sum + round(value * maturityScale), 0);
|
|
224
265
|
let bestScore = baseline;
|
|
225
266
|
let best;
|
|
226
|
-
const entries =
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
267
|
+
const entries = [...p.prospection.entries].sort((a, b) => cmpStr(a.sourceId, b.sourceId) || cmpStr(a.id, b.id));
|
|
268
|
+
const beta = p.planning?.presentBias ?? 1;
|
|
269
|
+
let remaining = p.planning ? 64 : Infinity;
|
|
270
|
+
function visit(previous, previousEnd, access, path, steps, values, accumulatedCost, done) {
|
|
271
|
+
if (path.length >= (p.planning?.depth ?? 2) - 1 || remaining <= 0)
|
|
272
|
+
return;
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
if (remaining <= 0)
|
|
275
|
+
return;
|
|
276
|
+
// Completing an action consumes every open opportunity it triggers, so a
|
|
277
|
+
// link whose trigger an earlier simulated step already performed is gone.
|
|
278
|
+
if (path.includes(entry) ||
|
|
279
|
+
!matches(entry.trigger, previous) ||
|
|
280
|
+
done.some((a) => matches(entry.trigger, a)) ||
|
|
281
|
+
entry.triggeredAt !== undefined ||
|
|
282
|
+
now > entry.expiresAt)
|
|
283
|
+
continue;
|
|
284
|
+
const start = Math.max(previousEnd, entry.availableFrom);
|
|
285
|
+
const end = start + entry.successor.duration;
|
|
286
|
+
if (!Number.isFinite(end) || end > entry.availableUntil)
|
|
287
|
+
continue;
|
|
288
|
+
remaining -= 1;
|
|
289
|
+
const nextAccess = access *
|
|
290
|
+
expectedEffect(p, previous).successRate *
|
|
291
|
+
entry.chance *
|
|
292
|
+
entry.confidence *
|
|
293
|
+
reliability(p, entry);
|
|
294
|
+
const successor = successorOffer(entry);
|
|
295
|
+
const nextValues = goals.map((g, i) => Math.max(values[i] ?? 0, nextAccess * beta * temporalGoalValue(p, g, successor, now, end)));
|
|
296
|
+
const nextCost = accumulatedCost + beta * nextAccess * Math.min(0, costs(successor));
|
|
297
|
+
const cost = round(nextCost);
|
|
298
|
+
const total = nextValues.reduce((sum, value) => sum + round(value * maturityScale), 0) + cost;
|
|
299
|
+
const nextPath = [...path, entry];
|
|
300
|
+
const nextSteps = [
|
|
301
|
+
...steps,
|
|
302
|
+
{ successor: structuredClone(entry.successor), start, end, access: nextAccess },
|
|
303
|
+
];
|
|
304
|
+
if (round(total - bestScore) > 0) {
|
|
305
|
+
bestScore = total;
|
|
306
|
+
// Keep the original root's source string; the ref explains longer routes.
|
|
307
|
+
const root = nextPath[0];
|
|
308
|
+
best = {
|
|
309
|
+
goals: new Map(goals.map((g, i) => [g.id, nextValues[i] ?? 0])),
|
|
310
|
+
cost: {
|
|
311
|
+
source: `prospection:cost:${root.sourceId}:${root.id}`,
|
|
312
|
+
value: cost,
|
|
313
|
+
...(nextPath.length > 1 ? { ref: nextPath.map((e) => e.id).join('>') } : {}),
|
|
314
|
+
},
|
|
315
|
+
route: {
|
|
316
|
+
steps: nextSteps,
|
|
317
|
+
goals: Object.fromEntries(goals.map((g, i) => [g.id, nextValues[i] ?? 0])),
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
visit(successor, end, nextAccess, nextPath, nextSteps, nextValues, nextCost, [...done, previous]);
|
|
322
|
+
}
|
|
241
323
|
}
|
|
324
|
+
visit(aff, now + Math.max(0, aff.duration), 1, [], [], direct, 0, []);
|
|
242
325
|
return best;
|
|
243
326
|
}
|
|
244
327
|
/** Conservative optional-slice restore; ambiguous identities drop the slice. @internal */
|
|
245
|
-
export function sanitizeProspection(input, now, personId) {
|
|
328
|
+
export function sanitizeProspection(input, now, personId, planning = false) {
|
|
246
329
|
if (!isObj(input) ||
|
|
247
330
|
!Array.isArray(input.entries) ||
|
|
248
331
|
!Array.isArray(input.revisions) ||
|
|
249
|
-
input.entries.length >
|
|
332
|
+
input.entries.length > maxEntries(planning) ||
|
|
250
333
|
input.revisions.length > PROSPECTION_DEFAULTS.maxRevisions)
|
|
251
334
|
return undefined;
|
|
252
335
|
const entries = input.entries.map((x) => retained(x, now)).filter((e) => e?.sourceId !== personId);
|
package/dist/restore.js
CHANGED
|
@@ -23,7 +23,7 @@ import { migrate } from './migrate.js';
|
|
|
23
23
|
import { sanitizeBonds } from './partnering/index.js';
|
|
24
24
|
import { cleanRetention, createPerson, traceLimit } from './person.js';
|
|
25
25
|
import { sanitizePromptings } from './promptings/index.js';
|
|
26
|
-
import { sanitizeProspection } from './prospection/index.js';
|
|
26
|
+
import { sanitizePlanning, sanitizeProspection } from './prospection/index.js';
|
|
27
27
|
import { sanitizeReview } from './review/index.js';
|
|
28
28
|
import { sanitizeSkillRetention } from './skills/index.js';
|
|
29
29
|
import { sanitizeGroups, sanitizeImpressions } from './social/index.js';
|
|
@@ -85,6 +85,7 @@ const OPTIONAL_KEYS = new Set([
|
|
|
85
85
|
'retention',
|
|
86
86
|
'methods',
|
|
87
87
|
'prospection',
|
|
88
|
+
'planning',
|
|
88
89
|
'promptings',
|
|
89
90
|
'curiosity',
|
|
90
91
|
'review',
|
|
@@ -201,7 +202,8 @@ export function restore(input) {
|
|
|
201
202
|
keepValid(out, 'ambient', sanitizeAmbient);
|
|
202
203
|
keepValid(out, 'retention', cleanRetention);
|
|
203
204
|
keepValid(out, 'methods', sanitizeMethods);
|
|
204
|
-
keepValid(out, '
|
|
205
|
+
keepValid(out, 'planning', sanitizePlanning);
|
|
206
|
+
keepValid(out, 'prospection', (x) => sanitizeProspection(x, out.now, out.id, out.planning !== undefined));
|
|
205
207
|
keepValid(out, 'curiosity', (x) => sanitizeCuriosity(x, out.now));
|
|
206
208
|
keepValid(out, 'promptings', (x) => sanitizePromptings(x, out.now));
|
|
207
209
|
keepValid(out, 'review', (x) => sanitizeReview(x, out.now));
|
package/dist/types.d.ts
CHANGED
|
@@ -563,6 +563,8 @@ export interface Commitment {
|
|
|
563
563
|
label?: string;
|
|
564
564
|
/** For kind 'abstain': action ids whose completion inside the window breaks it. */
|
|
565
565
|
violatedBy?: string[];
|
|
566
|
+
/** Firm self abstention: a will-not, lifted for bodily survival like a norm abstention. */
|
|
567
|
+
firm?: true;
|
|
566
568
|
/** For kind 'abstain': catalogued conditions under which the person may not hold it that day. */
|
|
567
569
|
exemptWhen?: ('illness' | 'travel')[];
|
|
568
570
|
/**
|
|
@@ -700,6 +702,8 @@ export interface VoiceTrustEvent {
|
|
|
700
702
|
* impulse emerges from competing terms, real fatigue costs, habits and precommitments.
|
|
701
703
|
*/
|
|
702
704
|
export interface WillState {
|
|
705
|
+
/** Opt-in score explanations on suggestion resolutions; absent preserves legacy JSON. */
|
|
706
|
+
suggestionReasons?: true;
|
|
703
707
|
voices: VoiceRelation[];
|
|
704
708
|
/**
|
|
705
709
|
* Precommitments the person made for themselves (e.g. "no work after Isha", "walk instead of eat when bored"):
|
|
@@ -885,7 +889,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
885
889
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
886
890
|
* `dutyReviewAt`). Person shape unchanged.
|
|
887
891
|
*/
|
|
888
|
-
export declare const ENGINE_VERSION = "2.
|
|
892
|
+
export declare const ENGINE_VERSION = "2.13.0";
|
|
889
893
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
890
894
|
export interface Pregnancy {
|
|
891
895
|
fatherId: PersonId;
|
|
@@ -1019,7 +1023,7 @@ export interface ConsequenceRevision {
|
|
|
1019
1023
|
}
|
|
1020
1024
|
export interface RetainedConsequence extends ReceivedConsequence {
|
|
1021
1025
|
sourceId: EntityId;
|
|
1022
|
-
channel: 'told';
|
|
1026
|
+
channel: 'told' | 'known';
|
|
1023
1027
|
receivedAt: Minute;
|
|
1024
1028
|
/** Own completed trigger; does not establish access or goal progress. */
|
|
1025
1029
|
triggeredAt?: Minute;
|
|
@@ -1029,6 +1033,17 @@ export interface ProspectionState {
|
|
|
1029
1033
|
entries: RetainedConsequence[];
|
|
1030
1034
|
revisions: ConsequenceRevision[];
|
|
1031
1035
|
}
|
|
1036
|
+
/** Bounded retained-claim search, owned by `prospection/`. */
|
|
1037
|
+
export interface PlanningSpec {
|
|
1038
|
+
/** Total steps including the current offer: integer 2..5, default 4. */
|
|
1039
|
+
depth?: number;
|
|
1040
|
+
/** Quasi-hyperbolic beta in (0, 1]; absent means no present bias. */
|
|
1041
|
+
presentBias?: Unit;
|
|
1042
|
+
}
|
|
1043
|
+
export interface PlanningState {
|
|
1044
|
+
depth: number;
|
|
1045
|
+
presentBias?: Unit;
|
|
1046
|
+
}
|
|
1032
1047
|
export interface Person {
|
|
1033
1048
|
schema: typeof PERSON_SCHEMA;
|
|
1034
1049
|
/** Engine version that last wrote this state. */
|
|
@@ -1048,8 +1063,10 @@ export interface Person {
|
|
|
1048
1063
|
skills: Record<string, Skill>;
|
|
1049
1064
|
/** Acquired conditional method evidence and current received cues; absent until used. */
|
|
1050
1065
|
methods?: MethodsState;
|
|
1051
|
-
/** Received future consequences, absent until attended testimony supplies them. */
|
|
1066
|
+
/** Received future consequences, absent until attended testimony or `seedConsequences` supplies them. */
|
|
1052
1067
|
prospection?: ProspectionState;
|
|
1068
|
+
/** Opt-in bounded planning over retained consequences. Owned by `prospection/`. */
|
|
1069
|
+
planning?: PlanningState;
|
|
1053
1070
|
/** Noticed promptings and practised turnings-away (2.4.0, opt-in via `enablePromptings`). Owned by `promptings/`. */
|
|
1054
1071
|
promptings?: PromptingsState;
|
|
1055
1072
|
/** Intrinsic exploration and varied repetition. Owned by `curiosity/`; absent unless enabled. */
|
|
@@ -1361,6 +1378,12 @@ export type SuggestionVerdict = 'assented' | 'complied' | 'deferred' | 'modified
|
|
|
1361
1378
|
/** Refusal class for UI: grey = cannot, amber = notNow, red = willNot. */
|
|
1362
1379
|
export type RefusalKind = 'cannot' | 'notNow' | 'willNot';
|
|
1363
1380
|
export interface SuggestionResolution {
|
|
1381
|
+
/** Read-only score explanation, only with `enableSuggestionReasons`; margin includes activity inertia. */
|
|
1382
|
+
decidedBy?: {
|
|
1383
|
+
kind: 'inertia' | 'term';
|
|
1384
|
+
source?: string;
|
|
1385
|
+
margin: number;
|
|
1386
|
+
};
|
|
1364
1387
|
voiceId: EntityId;
|
|
1365
1388
|
verdict: SuggestionVerdict;
|
|
1366
1389
|
kind?: RefusalKind;
|
|
@@ -1488,6 +1511,8 @@ export interface PersonSpec {
|
|
|
1488
1511
|
* `enableCharacterChange` and `enableSkillRetention` right after `createPerson`. Each is off when absent.
|
|
1489
1512
|
*/
|
|
1490
1513
|
enable?: {
|
|
1514
|
+
/** Read-only suggestion score explanations, as with `enableSuggestionReasons(p)`. */
|
|
1515
|
+
suggestionReasons?: boolean;
|
|
1491
1516
|
gists?: boolean;
|
|
1492
1517
|
yearbook?: boolean;
|
|
1493
1518
|
character?: boolean;
|
|
@@ -1500,6 +1525,8 @@ export interface PersonSpec {
|
|
|
1500
1525
|
promptings?: PromptingsSpec;
|
|
1501
1526
|
/** Enable intrinsic exploration and play, using engineering defaults or bounded overrides. */
|
|
1502
1527
|
curiosity?: boolean | CuriositySpec;
|
|
1528
|
+
/** Bounded planning: the same as `enablePlanning(p, spec)` after creation. */
|
|
1529
|
+
planning?: PlanningSpec;
|
|
1503
1530
|
/** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
|
|
1504
1531
|
review?: ReviewSpec;
|
|
1505
1532
|
};
|
package/dist/types.js
CHANGED
|
@@ -120,4 +120,4 @@ export const PERSON_SCHEMA = 'human/person@1';
|
|
|
120
120
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
121
121
|
* `dutyReviewAt`). Person shape unchanged.
|
|
122
122
|
*/
|
|
123
|
-
export const ENGINE_VERSION = '2.
|
|
123
|
+
export const ENGINE_VERSION = '2.13.0';
|
package/dist/will/will.d.ts
CHANGED
|
@@ -266,6 +266,13 @@ export declare function dominantTerm(c: Considered | undefined): string;
|
|
|
266
266
|
* sorted by voice id and bounded by `maxVoices`, so the outcome never depends on the host's argument order.
|
|
267
267
|
*/
|
|
268
268
|
export declare function voicesIn(single: Suggestion | undefined, list: readonly Suggestion[] | undefined): Suggestion[];
|
|
269
|
+
/**
|
|
270
|
+
* SCOPE (suggestion reasons): opt-in read-only explanations of score-based deferrals and modifications.
|
|
271
|
+
* The gap includes running-activity inertia; source differences aggregate like-labelled terms and tie by
|
|
272
|
+
* source string. This does not change selection, voice wording, counters or RNG, explain vetoes, or claim
|
|
273
|
+
* a score caused a lower-scoring stochastic choice. Legacy resolutions have no additional keys.
|
|
274
|
+
*/
|
|
275
|
+
export declare function enableSuggestionReasons(p: Person): void;
|
|
269
276
|
/** Whether a command names offer `aff` (by affordance id, or by action class when it names no affordance). */
|
|
270
277
|
export declare const commandTargets: (cmd: Command, aff: Affordance) => boolean;
|
|
271
278
|
/**
|
package/dist/will/will.js
CHANGED
|
@@ -36,7 +36,7 @@ import { breakAllows, inBreak } from '../affect/index.js';
|
|
|
36
36
|
import { commitmentPressure, isUnderWay, pressureReachedAt } from '../agenda/index.js';
|
|
37
37
|
import { downedAllows, readCapacities } from '../body/index.js';
|
|
38
38
|
import { CONSCIENCE_DEFAULTS, normVeto } from '../conscience/index.js';
|
|
39
|
-
import { clamp01, cmpStr, decay, dexp, dpow, hasNumbers, isObj, random } from '../core/index.js';
|
|
39
|
+
import { clamp01, cmpStr, decay, dexp, dpow, hasNumbers, isObj, random, round } from '../core/index.js';
|
|
40
40
|
import { skillLevel } from '../skills/index.js';
|
|
41
41
|
import { MINUTES_PER_DAY, MINUTES_PER_HOUR } from '../types.js';
|
|
42
42
|
export const WILL_DEFAULTS = {
|
|
@@ -160,6 +160,8 @@ export const WILL_DEFAULTS = {
|
|
|
160
160
|
* or `lastCommand` (1.6.0, 1.7.0) are dropped (absent means none). A save the engine wrote is unchanged. @internal
|
|
161
161
|
*/
|
|
162
162
|
export function sanitizeWill(w) {
|
|
163
|
+
if (w.suggestionReasons !== true)
|
|
164
|
+
delete w.suggestionReasons;
|
|
163
165
|
if (w.advice !== undefined && !Array.isArray(w.advice))
|
|
164
166
|
delete w.advice;
|
|
165
167
|
const cmd = w.command;
|
|
@@ -721,6 +723,43 @@ function arbitrateCommand(p, ctx, cmd, suggestions, prepared, selection, ev) {
|
|
|
721
723
|
}
|
|
722
724
|
return false;
|
|
723
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* SCOPE (suggestion reasons): opt-in read-only explanations of score-based deferrals and modifications.
|
|
728
|
+
* The gap includes running-activity inertia; source differences aggregate like-labelled terms and tie by
|
|
729
|
+
* source string. This does not change selection, voice wording, counters or RNG, explain vetoes, or claim
|
|
730
|
+
* a score caused a lower-scoring stochastic choice. Legacy resolutions have no additional keys.
|
|
731
|
+
*/
|
|
732
|
+
export function enableSuggestionReasons(p) {
|
|
733
|
+
p.will.suggestionReasons = true;
|
|
734
|
+
}
|
|
735
|
+
function scoreReason(selection, live, targetIds) {
|
|
736
|
+
const { winner, ranked, score } = selection;
|
|
737
|
+
if (!winner)
|
|
738
|
+
return undefined;
|
|
739
|
+
// A stochastic draw below the best live score was not decided by the scores.
|
|
740
|
+
if (ranked.some((c) => !c.vetoed && score(c) > score(winner)))
|
|
741
|
+
return undefined;
|
|
742
|
+
// Compare against the closest suggested option by the same adjusted score the winner was chosen on.
|
|
743
|
+
const suggested = [...live].sort((a, b) => score(b) - score(a) || cmpStr(a.affordanceId, b.affordanceId))[0];
|
|
744
|
+
if (!suggested)
|
|
745
|
+
return undefined;
|
|
746
|
+
const gap = score(winner) - score(suggested);
|
|
747
|
+
if (!Number.isFinite(gap) || gap < 0)
|
|
748
|
+
return undefined;
|
|
749
|
+
const margin = round(gap);
|
|
750
|
+
const withoutInertia = [...ranked].sort(byUtilityThenId)[0];
|
|
751
|
+
if (score(winner) > winner.utility && withoutInertia && targetIds.has(withoutInertia.affordanceId))
|
|
752
|
+
return { kind: 'inertia', margin };
|
|
753
|
+
const differences = new Map();
|
|
754
|
+
for (const t of winner.terms)
|
|
755
|
+
differences.set(t.source, (differences.get(t.source) ?? 0) + t.value);
|
|
756
|
+
for (const t of suggested.terms)
|
|
757
|
+
differences.set(t.source, (differences.get(t.source) ?? 0) - t.value);
|
|
758
|
+
const term = [...differences]
|
|
759
|
+
.filter(([, value]) => value > 0)
|
|
760
|
+
.sort((a, b) => b[1] - a[1] || cmpStr(a[0], b[0]))[0];
|
|
761
|
+
return { kind: 'term', ...(term ? { source: term[0] } : {}), margin };
|
|
762
|
+
}
|
|
724
763
|
/** Judge each voice against the same selected option before resolving competing insistence. */
|
|
725
764
|
function judgeVoices(p, ctx, suggestions, prepared, selection) {
|
|
726
765
|
const W = WILL_DEFAULTS;
|
|
@@ -829,17 +868,21 @@ function judgeVoices(p, ctx, suggestions, prepared, selection) {
|
|
|
829
868
|
}
|
|
830
869
|
side.pressure = W.pressurePush * clamp01(s.strength);
|
|
831
870
|
side.refused = 1;
|
|
871
|
+
const decidedBy = p.will.suggestionReasons ? scoreReason(selection, liveSuggested, targetIds) : undefined;
|
|
872
|
+
const explanation = decidedBy ? { decidedBy } : {};
|
|
832
873
|
const near = winnerAff !== undefined &&
|
|
833
874
|
suggestedAff !== undefined &&
|
|
834
875
|
(winnerAff.action === suggestedAff.action ||
|
|
835
876
|
(mainAim(suggestedAff) !== undefined && mainAim(suggestedAff) === mainAim(winnerAff)));
|
|
836
877
|
if (near && winnerAff)
|
|
837
878
|
return out(resolution('modified', reason, {
|
|
879
|
+
...explanation,
|
|
838
880
|
kind: 'notNow',
|
|
839
881
|
insteadAffordanceId: winnerAff.id,
|
|
840
882
|
counterOffer: { affordanceId: winnerAff.id, label: winnerAff.label },
|
|
841
883
|
}));
|
|
842
884
|
return out(resolution('deferred', reason, {
|
|
885
|
+
...explanation,
|
|
843
886
|
kind: 'notNow',
|
|
844
887
|
insteadAffordanceId: winnerAff?.id,
|
|
845
888
|
counterOffer: winnerAff
|
|
@@ -865,6 +908,9 @@ function arbitrateInsistence(p, judged, considered, affById, ev) {
|
|
|
865
908
|
forcer = j;
|
|
866
909
|
}
|
|
867
910
|
if (forcer?.comply) {
|
|
911
|
+
// The final option is imposed by insistence, not selected by scores.
|
|
912
|
+
for (const j of judged)
|
|
913
|
+
delete j.res.decidedBy;
|
|
868
914
|
const forced = forcer.comply.target.affordanceId;
|
|
869
915
|
const forcedAff = affById.get(forced);
|
|
870
916
|
ev.chosenId = forced;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Deterministic, explainable simulated people: body, needs, affect, memory, beliefs, skills, habits, relationships, values and a will that can refuse a suggestion",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"simulation",
|