@adam0white/human-framework 2.10.0 → 2.12.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.
@@ -40,6 +40,7 @@ import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
40
40
  import { trustOf } from '../beliefs/index.js';
41
41
  import { normTerms } from '../conscience/index.js';
42
42
  import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
43
+ import { curiosityTerms } from '../curiosity/index.js';
43
44
  import { habitEase, habitPull } from '../habits/index.js';
44
45
  import { expectedEffect } from '../memory/index.js';
45
46
  import { methodEvidence } from '../methods/index.js';
@@ -247,6 +248,12 @@ function materialValue(p, material, scarcity) {
247
248
  const sat = Math.sign(material) * (1 - dexp(-Math.abs(material) / scale));
248
249
  return K.materialWeight * sat * 2 * weight * (1 + K.scarcityMaterialGain * scarcity);
249
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
+ }
250
257
  /** Needs whose consumption can overfill (2.8.0 fullness): what `body.consume` feeds. */
251
258
  const FULL_NEEDS = ['food', 'water', 'relief'];
252
259
  /** Score one option. Pure apart from reads; terms are rounded for stable snapshots. */
@@ -327,12 +334,11 @@ export function consider(p, aff, ctx) {
327
334
  }
328
335
  const maturity = clamp01(ctx.mods.maturity);
329
336
  const maturityScale = 0.5 + 0.5 * maturity;
330
- const received = receivedGoalRoute(p, aff, ctx.now, maturityScale, (successor) => effortValue(p, successor, ctx) +
331
- (successor.material ? materialValue(p, successor.material, scarcity) : 0));
337
+ const received = prospectiveRoute(p, aff, ctx);
332
338
  for (const t of agendaTermsWithGoalEstimates(p, aff, ctx.now, received?.goals))
333
339
  push(t.source, t.value * maturityScale);
334
340
  if (received)
335
- push(received.cost.source, received.cost.value);
341
+ push(received.cost.source, received.cost.value, received.cost.ref);
336
342
  // Habit: cue-matched automaticity (cued by the option's own place), stronger before planning matures.
337
343
  const habit = p.habits.length === 0
338
344
  ? 0
@@ -452,6 +458,9 @@ export function consider(p, aff, ctx) {
452
458
  if (!inBreak(p))
453
459
  for (const t of rememberedTerms(p, aff, ctx.now, voices))
454
460
  push(t.source, t.value);
461
+ if (p.curiosity)
462
+ for (const t of curiosityTerms(p, aff, ctx))
463
+ push(t.source, t.value);
455
464
  // Promptings (2.4.0, opt-in): a recurrence pull under `fault` and a practised turning-away from a noticed prompt.
456
465
  if (p.promptings)
457
466
  for (const t of promptingTerms(p, aff.action, terms, aff.tags, ctx.now, ctx.desperation))
@@ -0,0 +1,28 @@
1
+ import type { ConsiderContext } from '../cognition/index.ts';
2
+ import type { Affordance, CuriosityReading, CuriositySpec, CuriosityState, Minute, Outcome, Person, Term } from '../types.ts';
3
+ /** Engineering defaults: unknown .8 exceeds mild effort (.1 base) and learned pleasure (.6 scale).
4
+ * Need relief uses cognition's scale 4; urgency and fear also suppress this entire drive.
5
+ */
6
+ export declare const CURIOSITY_DEFAULTS: {
7
+ readonly unknownScale: 0.8;
8
+ readonly progressScale: 2;
9
+ readonly varyScale: 0.15;
10
+ readonly recentMinutes: 60;
11
+ readonly rewardMax: 0.03;
12
+ readonly fastAlpha: 0.5;
13
+ readonly slowAlpha: 0.15;
14
+ readonly maxKeys: 32;
15
+ readonly maxRecent: 8;
16
+ };
17
+ /** Enable or reconfigure; retain the bounded history. Invalid overrides use defaults. */
18
+ export declare function enableCuriosity(p: Person, spec?: CuriositySpec): CuriosityState;
19
+ type Context = Pick<ConsiderContext, 'needs' | 'mods' | 'now'>;
20
+ /** Read raw signals and drive without mutation or RNG. `now` changes only the recent-act window. */
21
+ export declare function curiosityOf(p: Person, aff: Affordance, now?: Minute): CuriosityReading;
22
+ /** Cognition uses the actual decision context, including perceived needs. Fixed term order. */
23
+ export declare function curiosityTerms(p: Person, aff: Affordance, ctx: Context): Term[];
24
+ /** Called before memory learns this outcome. Needs owner applies the small, bounded reward. */
25
+ export declare function noteCuriosity(p: Person, aff: Affordance, outcome: Outcome, felt: number): void;
26
+ /** Conservative restore: drop malformed or ambiguous slices; skip malformed entries and bound lists. */
27
+ export declare function sanitizeCuriosity(input: unknown, now: Minute): CuriosityState | undefined;
28
+ export {};
@@ -0,0 +1,190 @@
1
+ /**
2
+ * SCOPE: opt-in intrinsic exploration and play. Uncertainty draws a relaxed person toward unfamiliar
3
+ * offers; falling prediction error (learning progress) pulls and rewards; recent action kinds invite variation in target or
4
+ * place. Shapes, not fitted values: learning progress (Oudeyer, Kaplan & Hafner 2007), information gaps
5
+ * (Loewenstein 1994; Kidd & Hayden 2015; Gottlieb et al. 2013), effectance and competence (White 1959;
6
+ * Deci & Ryan), relaxed repetition with variation (Burghardt 2005), openness (DeYoung), childhood
7
+ * exploration (Gopnik 2020). See research/curiosity-and-play.md. All coefficients are engineering defaults.
8
+ * This owns only p.curiosity. It neither adopts projects nor plans, reads no host novelty tags, and
9
+ * implements no faith lens, LLM or RNG. Without the slice it adds no state, terms or reward.
10
+ */
11
+ import { emotionLevel } from '../affect/index.js';
12
+ import { clamp01, isNum, isObj, round } from '../core/index.js';
13
+ import { lifeModifiers } from '../lifecourse/index.js';
14
+ import { expectationKey, expectedEffect } from '../memory/index.js';
15
+ import { satisfy } from '../needs/index.js';
16
+ import { readPerson } from '../person.js';
17
+ import { MAX_MINUTE, PHYSIOLOGICAL_NEEDS } from '../types.js';
18
+ /** Engineering defaults: unknown .8 exceeds mild effort (.1 base) and learned pleasure (.6 scale).
19
+ * Need relief uses cognition's scale 4; urgency and fear also suppress this entire drive.
20
+ */
21
+ export const CURIOSITY_DEFAULTS = {
22
+ unknownScale: 0.8,
23
+ progressScale: 2,
24
+ varyScale: 0.15,
25
+ recentMinutes: 60,
26
+ rewardMax: 0.03,
27
+ fastAlpha: 0.5,
28
+ slowAlpha: 0.15,
29
+ maxKeys: 32,
30
+ maxRecent: 8,
31
+ };
32
+ const SPEC_KEYS = ['unknownScale', 'progressScale', 'varyScale', 'recentMinutes', 'rewardMax'];
33
+ function cleanSpec(input) {
34
+ const out = {};
35
+ if (!isObj(input))
36
+ return out;
37
+ for (const key of SPEC_KEYS) {
38
+ const x = input[key];
39
+ const max = key === 'recentMinutes' ? 1440 : CURIOSITY_DEFAULTS[key];
40
+ if (isNum(x) && x >= 0 && x <= max)
41
+ out[key] = x;
42
+ }
43
+ return out;
44
+ }
45
+ const setting = (s, key) => s.spec?.[key] ?? CURIOSITY_DEFAULTS[key];
46
+ /** Enable or reconfigure; retain the bounded history. Invalid overrides use defaults. */
47
+ export function enableCuriosity(p, spec = {}) {
48
+ const out = { errors: p.curiosity?.errors ?? [], recent: p.curiosity?.recent ?? [] };
49
+ const clean = cleanSpec(spec);
50
+ if (Object.keys(clean).length)
51
+ out.spec = clean;
52
+ p.curiosity = out;
53
+ return out;
54
+ }
55
+ /** Identifiers the slice records and restores; longer ones are not tracked, so a save round-trips. */
56
+ const str = (x) => typeof x === 'string' && x.length > 0 && x.length <= 512;
57
+ function reading(p, aff, ctx) {
58
+ const s = p.curiosity;
59
+ if (!s)
60
+ return { unknown: 0, progress: 0, vary: 0, drive: 0 };
61
+ let urgency = 0;
62
+ for (const n of ctx.needs)
63
+ if (n.id === 'safety' || PHYSIOLOGICAL_NEEDS.includes(n.id))
64
+ urgency = Math.max(urgency, n.urgency);
65
+ const drive = (1 - clamp01(urgency)) *
66
+ (1 - emotionLevel(p, 'fear')) *
67
+ (0.25 + 0.75 * p.traits.openness) *
68
+ (1.25 - 0.5 * ctx.mods.maturity);
69
+ const error = s.errors.find((e) => e.key === expectationKey(aff));
70
+ const recent = s.recent.filter((r) => r.at <= ctx.now && ctx.now - r.at <= setting(s, 'recentMinutes') && r.action === aff.action);
71
+ const identical = recent.some((r) => r.targetId === aff.targetId && r.placeId === aff.placeId);
72
+ return {
73
+ unknown: clamp01(1 - expectedEffect(p, aff).confidence),
74
+ progress: error ? clamp01(error.slow - error.fast) : 0,
75
+ vary: !identical && recent.length > 0 ? 1 : 0,
76
+ drive,
77
+ };
78
+ }
79
+ /** Read raw signals and drive without mutation or RNG. `now` changes only the recent-act window. */
80
+ export function curiosityOf(p, aff, now = p.now) {
81
+ if (!p.curiosity)
82
+ return { unknown: 0, progress: 0, vary: 0, drive: 0 };
83
+ return reading(p, aff, { now, needs: readPerson(p).needs, mods: lifeModifiers(p) });
84
+ }
85
+ /** Cognition uses the actual decision context, including perceived needs. Fixed term order. */
86
+ export function curiosityTerms(p, aff, ctx) {
87
+ const s = p.curiosity;
88
+ if (!s)
89
+ return [];
90
+ const r = reading(p, aff, ctx);
91
+ return ['unknown', 'progress', 'vary'].map((key) => ({
92
+ source: `curiosity:${key}`,
93
+ value: round(Math.min(setting(s, `${key}Scale`), setting(s, `${key}Scale`) * r[key] * r.drive)),
94
+ }));
95
+ }
96
+ /** Called before memory learns this outcome. Needs owner applies the small, bounded reward. */
97
+ export function noteCuriosity(p, aff, outcome, felt) {
98
+ const s = p.curiosity;
99
+ if (!s || outcome.status === 'interrupted')
100
+ return;
101
+ const before = curiosityOf(p, aff);
102
+ const expected = expectedEffect(p, aff);
103
+ const error = clamp01((Math.abs((outcome.status === 'completed' ? 1 : 0) - expected.successRate) +
104
+ Math.abs(felt - expected.valence) / 2) /
105
+ 2);
106
+ const key = expectationKey(aff);
107
+ if (!str(key) ||
108
+ !str(aff.action) ||
109
+ (aff.targetId !== undefined && !str(aff.targetId)) ||
110
+ (aff.placeId !== undefined && !str(aff.placeId)))
111
+ return;
112
+ let entry = s.errors.find((e) => e.key === key);
113
+ if (!entry) {
114
+ if (s.errors.length >= CURIOSITY_DEFAULTS.maxKeys) {
115
+ let oldest = 0;
116
+ let oldestAt = s.errors[0]?.lastAt ?? p.now;
117
+ for (const [i, held] of s.errors.entries()) {
118
+ if (held.lastAt < oldestAt) {
119
+ oldest = i;
120
+ oldestAt = held.lastAt;
121
+ }
122
+ }
123
+ s.errors.splice(oldest, 1);
124
+ }
125
+ entry = { key, fast: error, slow: error, n: 1, lastAt: p.now };
126
+ s.errors.push(entry);
127
+ }
128
+ else {
129
+ entry.fast = clamp01(entry.fast + CURIOSITY_DEFAULTS.fastAlpha * (error - entry.fast));
130
+ entry.slow = clamp01(entry.slow + CURIOSITY_DEFAULTS.slowAlpha * (error - entry.slow));
131
+ entry.n = Math.min(Number.MAX_SAFE_INTEGER, entry.n + 1);
132
+ entry.lastAt = p.now;
133
+ }
134
+ const act = { action: aff.action, at: p.now };
135
+ if (aff.targetId !== undefined)
136
+ act.targetId = aff.targetId;
137
+ if (aff.placeId !== undefined)
138
+ act.placeId = aff.placeId;
139
+ s.recent.push(act);
140
+ if (s.recent.length > CURIOSITY_DEFAULTS.maxRecent)
141
+ s.recent.shift();
142
+ const progress = Math.max(0, entry.slow - entry.fast);
143
+ const reward = Math.min(setting(s, 'rewardMax'), setting(s, 'rewardMax') * progress * before.drive);
144
+ satisfy(p, { leisure: reward / 2, competence: reward / 2 });
145
+ }
146
+ const unit = (x) => isNum(x) && x >= 0 && x <= 1;
147
+ const minute = (x, now) => isNum(x) && Math.abs(x) <= MAX_MINUTE && x <= now;
148
+ /** Conservative restore: drop malformed or ambiguous slices; skip malformed entries and bound lists. */
149
+ export function sanitizeCuriosity(input, now) {
150
+ if (!isObj(input) || !Array.isArray(input.errors) || !Array.isArray(input.recent))
151
+ return undefined;
152
+ if (input.spec !== undefined && !isObj(input.spec))
153
+ return undefined;
154
+ const out = { errors: [], recent: [] };
155
+ if (input.spec !== undefined) {
156
+ const spec = cleanSpec(input.spec);
157
+ if (Object.keys(spec).length !== Object.keys(input.spec).length)
158
+ return undefined;
159
+ if (Object.keys(spec).length)
160
+ out.spec = spec;
161
+ }
162
+ const seen = new Set();
163
+ for (const e of input.errors.slice(0, CURIOSITY_DEFAULTS.maxKeys)) {
164
+ if (!isObj(e) ||
165
+ !str(e.key) ||
166
+ !unit(e.fast) ||
167
+ !unit(e.slow) ||
168
+ !minute(e.lastAt, now) ||
169
+ !Number.isSafeInteger(e.n) ||
170
+ e.n < 1)
171
+ continue;
172
+ if (seen.has(e.key))
173
+ return undefined;
174
+ seen.add(e.key);
175
+ out.errors.push({ key: e.key, fast: e.fast, slow: e.slow, n: e.n, lastAt: e.lastAt });
176
+ }
177
+ for (const r of input.recent.slice(-CURIOSITY_DEFAULTS.maxRecent)) {
178
+ if (!isObj(r) || !str(r.action) || !minute(r.at, now))
179
+ continue;
180
+ if ((r.targetId !== undefined && !str(r.targetId)) || (r.placeId !== undefined && !str(r.placeId)))
181
+ continue;
182
+ const act = { action: r.action, at: r.at };
183
+ if (r.targetId !== undefined)
184
+ act.targetId = r.targetId;
185
+ if (r.placeId !== undefined)
186
+ act.placeId = r.placeId;
187
+ out.recent.push(act);
188
+ }
189
+ return out;
190
+ }
@@ -0,0 +1 @@
1
+ export * from './curiosity.ts';
@@ -0,0 +1 @@
1
+ export * from './curiosity.js';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.10.0";
2
+ export declare const FRAMEWORK_VERSION = "2.12.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';
@@ -13,6 +13,7 @@ export { COGNITION_DEFAULTS, consider, decide as scoreAndResolve, desperationOf,
13
13
  export * from './conscience/index.ts';
14
14
  export * from './conversation/index.ts';
15
15
  export * from './core/index.ts';
16
+ export { CURIOSITY_DEFAULTS, curiosityOf, enableCuriosity } from './curiosity/index.ts';
16
17
  export * from './environment/index.ts';
17
18
  export * from './family/index.ts';
18
19
  export * from './habits/index.ts';
@@ -29,8 +30,8 @@ export * from './needs/index.ts';
29
30
  export * from './partnering/index.ts';
30
31
  export * from './person.ts';
31
32
  export * from './promptings/index.ts';
32
- export type { ConsequenceEvidence } from './prospection/index.ts';
33
- 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';
34
35
  export { fingerprint, restore, snapshot } from './restore.ts';
35
36
  export * from './review/index.ts';
36
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.10.0';
2
+ export const FRAMEWORK_VERSION = '2.12.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';
@@ -12,6 +12,7 @@ export { COGNITION_DEFAULTS, consider, decide as scoreAndResolve, desperationOf,
12
12
  export * from './conscience/index.js';
13
13
  export * from './conversation/index.js';
14
14
  export * from './core/index.js';
15
+ export { CURIOSITY_DEFAULTS, curiosityOf, enableCuriosity } from './curiosity/index.js';
15
16
  export * from './environment/index.js';
16
17
  export * from './family/index.js';
17
18
  export * from './habits/index.js';
@@ -26,7 +27,7 @@ export * from './needs/index.js';
26
27
  export * from './partnering/index.js';
27
28
  export * from './person.js';
28
29
  export * from './promptings/index.js';
29
- 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';
30
31
  export { fingerprint, restore, snapshot } from './restore.js';
31
32
  export * from './review/index.js';
32
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.9.0 to 2.10.0 stamps the version; belief provenance, noted contradictions and
21
+ * the earliest-ending one. 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.11.0': {
74
+ to: '2.12.0',
75
+ 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',
76
+ apply: stamp,
77
+ },
78
+ '2.10.0': {
79
+ to: '2.11.0',
80
+ note: 'the curiosity slice (Person.curiosity) is optional and starts absent; existing hosts retain exact behavior',
81
+ apply: stamp,
82
+ },
73
83
  '2.9.0': {
74
84
  to: '2.10.0',
75
85
  note: 'belief provenance (Belief.provenance), noted contradictions (memory.contradictions) and the believed-regard impression cue are optional and start 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,9 +25,10 @@ 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
+ import { enableCuriosity, noteCuriosity } from './curiosity/index.js';
31
32
  import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
32
33
  import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
33
34
  import { advanceHabits, reinforce, withholdCued } from './habits/index.js';
@@ -37,7 +38,7 @@ import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
37
38
  import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
38
39
  import { widow } from './partnering/index.js';
39
40
  import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
40
- import { bodyForecastFor, consumeConsequences, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
41
+ import { bodyForecastFor, consumeConsequences, enablePlanning, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
41
42
  import { enableReview, reviewIfDue } from './review/index.js';
42
43
  import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
43
44
  import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, noteKnownBy, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
@@ -159,6 +160,10 @@ export function createPerson(spec) {
159
160
  enableFatigueStore(p);
160
161
  if (on?.fullness)
161
162
  enableFullness(p);
163
+ if (on?.curiosity)
164
+ enableCuriosity(p, on.curiosity === true ? {} : on.curiosity);
165
+ if (on?.planning)
166
+ enablePlanning(p, on.planning);
162
167
  if (on?.promptings)
163
168
  enablePromptings(p, on.promptings);
164
169
  if (on?.review)
@@ -849,6 +854,16 @@ export function answerNow(p, affordances, suggestion, opts = {}) {
849
854
  }
850
855
  return out;
851
856
  }
857
+ /**
858
+ * Read the best retained-claim route using ordinary scoring costs (scarcity absent).
859
+ * Does not mutate, advance time or consume RNG. `now` values timing against the held state.
860
+ * Returns undefined when no route improves on direct goal value.
861
+ */
862
+ export function planRoute(p, aff, now = p.now) {
863
+ const held = now === p.now ? p : { ...p, now };
864
+ const { ctx } = decisionInputs(held, [aff], 'plan-readout', {});
865
+ return prospectiveRoute(held, aff, ctx)?.route;
866
+ }
852
867
  /** Every resolution of a decision record (all voices), oldest API first. */
853
868
  export const resolutionsOf = (r) => r.suggestions ?? (r.suggestion ? [r.suggestion] : []);
854
869
  /**
@@ -1360,6 +1375,8 @@ export function finish(p, outcome, opts = {}) {
1360
1375
  dischargeAdvice(p, aff.action);
1361
1376
  }
1362
1377
  // Memory.
1378
+ if (p.curiosity)
1379
+ noteCuriosity(p, aff, outcome, felt);
1363
1380
  learnOutcome(p, aff, outcome, realized, felt);
1364
1381
  const episode = {
1365
1382
  at: now,
@@ -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: one received current-action → successor consequence, acquired only through
3
- * attended external testimony for this person or an untargeted broadcast. Knowledge is person-owned, source-labelled, bounded and saved.
4
- * Temporal goals value expected completion using time, success and source reliability.
5
- * One coherent route may contribute; it never executes actions, grants consent or
6
- * advances a goal. Own completed triggers consume their received opportunity.
7
- * This is not graph search, inferred procedure learning or a model of future bodies,
8
- * waiting opportunity cost or another person's agency. Parameters are engineering
9
- * bounds, not fitted psychological estimates. Revision memory is bounded, not an audit.
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) || !id(x.sourceId) || x.channel !== 'told' || !minute(x.receivedAt) || x.receivedAt > now)
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: 'told',
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
- const adds = pc.consequences ?? [];
136
- const removes = pc.withdrawConsequences ?? [];
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 > PROSPECTION_DEFAULTS.maxBatch ||
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: 'told', receivedAt: p.now });
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 > PROSPECTION_DEFAULTS.maxEntries) {
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 - PROSPECTION_DEFAULTS.maxEntries);
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 * trustOf(p, entry.sourceId);
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 = consequenceEvidence(p, aff, now).entries.sort((a, b) => cmpStr(a.entry.sourceId, b.entry.sourceId) || cmpStr(a.entry.id, b.entry.id));
227
- for (const e of entries) {
228
- if (!e.feasible)
229
- continue;
230
- const successor = successorOffer(e.entry);
231
- const values = goals.map((g, i) => Math.max(direct[i] ?? 0, e.accessProbability * temporalGoalValue(p, g, successor, now, e.successorEndsAt)));
232
- const cost = round(e.accessProbability * Math.min(0, costs(successor)));
233
- const total = values.reduce((sum, value) => sum + round(value * maturityScale), 0) + cost;
234
- if (!(round(total - bestScore) > 0))
235
- continue;
236
- bestScore = total;
237
- best = {
238
- goals: new Map(goals.map((g, i) => [g.id, values[i] ?? 0])),
239
- cost: { source: `prospection:cost:${e.entry.sourceId}:${e.entry.id}`, value: cost },
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 > PROSPECTION_DEFAULTS.maxEntries ||
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.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ /**
2
+ * SCOPE: saving and loading a person. `snapshot` copies a person as plain JSON; `restore` validates a saved person,
3
+ * upgrades a save from an earlier supported engine (`migrate`), fills defaults for missing or mistyped core fields,
4
+ * hands every optional slice to its owning module's sanitizer (the chronicle, its day accumulator and the lexicon
5
+ * need only a type check, done here), and holds every list to the bound the live code keeps.
6
+ * A save the engine wrote restores to exactly what was saved. It does not repair a save's meaning (a plausible but
7
+ * wrong value stays), merge two saves, or read any format but the person JSON.
8
+ */
1
9
  import type { Person } from './types.ts';
2
10
  /** A deep copy of the person as plain JSON (what a host saves). */
3
11
  export declare function snapshot(p: Person): Person;
package/dist/restore.js CHANGED
@@ -13,6 +13,7 @@ import { sanitizeBody, sanitizeInjuries } from './body/index.js';
13
13
  import { sanitizeCharacter } from './character/index.js';
14
14
  import { CHRONICLE_DEFAULTS, sanitizeYears } from './chronicle/index.js';
15
15
  import { isNum } from './core/index.js';
16
+ import { sanitizeCuriosity } from './curiosity/index.js';
16
17
  import { sanitizeAmbient } from './environment/index.js';
17
18
  import { sanitizeFamily } from './family/index.js';
18
19
  import { HABIT_DEFAULTS } from './habits/index.js';
@@ -22,7 +23,7 @@ import { migrate } from './migrate.js';
22
23
  import { sanitizeBonds } from './partnering/index.js';
23
24
  import { cleanRetention, createPerson, traceLimit } from './person.js';
24
25
  import { sanitizePromptings } from './promptings/index.js';
25
- import { sanitizeProspection } from './prospection/index.js';
26
+ import { sanitizePlanning, sanitizeProspection } from './prospection/index.js';
26
27
  import { sanitizeReview } from './review/index.js';
27
28
  import { sanitizeSkillRetention } from './skills/index.js';
28
29
  import { sanitizeGroups, sanitizeImpressions } from './social/index.js';
@@ -84,7 +85,9 @@ const OPTIONAL_KEYS = new Set([
84
85
  'retention',
85
86
  'methods',
86
87
  'prospection',
88
+ 'planning',
87
89
  'promptings',
90
+ 'curiosity',
88
91
  'review',
89
92
  ]);
90
93
  /** Kind of a JSON value for the per-field type check in `restore`. */
@@ -199,7 +202,9 @@ export function restore(input) {
199
202
  keepValid(out, 'ambient', sanitizeAmbient);
200
203
  keepValid(out, 'retention', cleanRetention);
201
204
  keepValid(out, 'methods', sanitizeMethods);
202
- keepValid(out, 'prospection', (x) => sanitizeProspection(x, out.now, out.id));
205
+ keepValid(out, 'planning', sanitizePlanning);
206
+ keepValid(out, 'prospection', (x) => sanitizeProspection(x, out.now, out.id, out.planning !== undefined));
207
+ keepValid(out, 'curiosity', (x) => sanitizeCuriosity(x, out.now));
203
208
  keepValid(out, 'promptings', (x) => sanitizePromptings(x, out.now));
204
209
  keepValid(out, 'review', (x) => sanitizeReview(x, out.now));
205
210
  boundLists(out);
package/dist/types.d.ts CHANGED
@@ -885,7 +885,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
885
885
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
886
886
  * `dutyReviewAt`). Person shape unchanged.
887
887
  */
888
- export declare const ENGINE_VERSION = "2.10.0";
888
+ export declare const ENGINE_VERSION = "2.12.0";
889
889
  /** A pregnancy carried by this person (host-driven; see `family/`). */
890
890
  export interface Pregnancy {
891
891
  fatherId: PersonId;
@@ -1019,7 +1019,7 @@ export interface ConsequenceRevision {
1019
1019
  }
1020
1020
  export interface RetainedConsequence extends ReceivedConsequence {
1021
1021
  sourceId: EntityId;
1022
- channel: 'told';
1022
+ channel: 'told' | 'known';
1023
1023
  receivedAt: Minute;
1024
1024
  /** Own completed trigger; does not establish access or goal progress. */
1025
1025
  triggeredAt?: Minute;
@@ -1029,6 +1029,17 @@ export interface ProspectionState {
1029
1029
  entries: RetainedConsequence[];
1030
1030
  revisions: ConsequenceRevision[];
1031
1031
  }
1032
+ /** Bounded retained-claim search, owned by `prospection/`. */
1033
+ export interface PlanningSpec {
1034
+ /** Total steps including the current offer: integer 2..5, default 4. */
1035
+ depth?: number;
1036
+ /** Quasi-hyperbolic beta in (0, 1]; absent means no present bias. */
1037
+ presentBias?: Unit;
1038
+ }
1039
+ export interface PlanningState {
1040
+ depth: number;
1041
+ presentBias?: Unit;
1042
+ }
1032
1043
  export interface Person {
1033
1044
  schema: typeof PERSON_SCHEMA;
1034
1045
  /** Engine version that last wrote this state. */
@@ -1048,10 +1059,14 @@ export interface Person {
1048
1059
  skills: Record<string, Skill>;
1049
1060
  /** Acquired conditional method evidence and current received cues; absent until used. */
1050
1061
  methods?: MethodsState;
1051
- /** Received future consequences, absent until attended testimony supplies them. */
1062
+ /** Received future consequences, absent until attended testimony or `seedConsequences` supplies them. */
1052
1063
  prospection?: ProspectionState;
1064
+ /** Opt-in bounded planning over retained consequences. Owned by `prospection/`. */
1065
+ planning?: PlanningState;
1053
1066
  /** Noticed promptings and practised turnings-away (2.4.0, opt-in via `enablePromptings`). Owned by `promptings/`. */
1054
1067
  promptings?: PromptingsState;
1068
+ /** Intrinsic exploration and varied repetition. Owned by `curiosity/`; absent unless enabled. */
1069
+ curiosity?: CuriosityState;
1055
1070
  /** Evening self-review and its resolves (2.7.0, opt-in via `enableReview`). Owned by `review/`. */
1056
1071
  review?: ReviewState;
1057
1072
  /** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
@@ -1496,6 +1511,10 @@ export interface PersonSpec {
1496
1511
  fullness?: boolean;
1497
1512
  /** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
1498
1513
  promptings?: PromptingsSpec;
1514
+ /** Enable intrinsic exploration and play, using engineering defaults or bounded overrides. */
1515
+ curiosity?: boolean | CuriositySpec;
1516
+ /** Bounded planning: the same as `enablePlanning(p, spec)` after creation. */
1517
+ planning?: PlanningSpec;
1499
1518
  /** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
1500
1519
  review?: ReviewSpec;
1501
1520
  };
@@ -1890,3 +1909,39 @@ export interface DayRecord {
1890
1909
  */
1891
1910
  state?: ChronicleState;
1892
1911
  }
1912
+ /** Engineering overrides for intrinsic motivation; scales are capped at their defaults. */
1913
+ export interface CuriositySpec {
1914
+ unknownScale?: number;
1915
+ progressScale?: number;
1916
+ varyScale?: number;
1917
+ recentMinutes?: number;
1918
+ rewardMax?: number;
1919
+ }
1920
+ /** Prediction-error history for the same key used by memory expectations. */
1921
+ export interface CuriosityError {
1922
+ key: string;
1923
+ fast: Unit;
1924
+ slow: Unit;
1925
+ n: number;
1926
+ lastAt: Minute;
1927
+ }
1928
+ /** A completed or failed act, retained only for bounded repetition with variation. */
1929
+ export interface CuriosityAct {
1930
+ action: string;
1931
+ targetId?: EntityId;
1932
+ placeId?: EntityId;
1933
+ at: Minute;
1934
+ }
1935
+ /** Optional JSON state owned by curiosity/. */
1936
+ export interface CuriosityState {
1937
+ spec?: CuriositySpec;
1938
+ errors: CuriosityError[];
1939
+ recent: CuriosityAct[];
1940
+ }
1941
+ /** Raw bounded signals and their common drive; no RNG or mutation. */
1942
+ export interface CuriosityReading {
1943
+ unknown: Unit;
1944
+ progress: Unit;
1945
+ vary: Unit;
1946
+ drive: number;
1947
+ }
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.10.0';
123
+ export const ENGINE_VERSION = '2.12.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.10.0",
3
+ "version": "2.12.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",