@adam0white/human-framework 2.12.0 → 2.14.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 +42 -3
- package/dist/cognition/cognition.js +4 -0
- package/dist/conscience/conscience.js +10 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/migrate.js +11 -1
- package/dist/persistence/index.d.ts +1 -0
- package/dist/persistence/index.js +1 -0
- package/dist/persistence/persistence.d.ts +18 -0
- package/dist/persistence/persistence.js +190 -0
- package/dist/person.js +49 -3
- package/dist/projects/index.d.ts +1 -0
- package/dist/projects/index.js +1 -0
- package/dist/projects/projects.d.ts +23 -0
- package/dist/projects/projects.js +314 -0
- package/dist/prospection/prospection.js +5 -0
- package/dist/restore.js +6 -0
- package/dist/types.d.ts +97 -1
- 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
|
@@ -144,6 +144,17 @@ function copyArrays(c) {
|
|
|
144
144
|
*/
|
|
145
145
|
/** Copy valid temporal settings and drop invalid ones at every agenda entry point. */
|
|
146
146
|
function sanitizeGoalTemporal(g) {
|
|
147
|
+
const project = g.project;
|
|
148
|
+
if (project !== undefined) {
|
|
149
|
+
if (isObj(project) &&
|
|
150
|
+
typeof project.prospectId === 'string' &&
|
|
151
|
+
project.prospectId.length > 0 &&
|
|
152
|
+
project.prospectId.length <= 512 &&
|
|
153
|
+
typeof project.perceivedProgress === 'boolean')
|
|
154
|
+
g.project = { prospectId: project.prospectId, perceivedProgress: project.perceivedProgress };
|
|
155
|
+
else
|
|
156
|
+
delete g.project;
|
|
157
|
+
}
|
|
147
158
|
const t = g.temporal;
|
|
148
159
|
if (t !== undefined &&
|
|
149
160
|
!(isObj(t) &&
|
|
@@ -154,8 +165,11 @@ function sanitizeGoalTemporal(g) {
|
|
|
154
165
|
else if (g.temporal)
|
|
155
166
|
g.temporal = { halfLifeMinutes: g.temporal.halfLifeMinutes };
|
|
156
167
|
}
|
|
157
|
-
/** Restore-time check of
|
|
168
|
+
/** Restore-time check of optional lapse and firm flags; malformed values are dropped. @internal */
|
|
158
169
|
export function sanitizeAgenda(a) {
|
|
170
|
+
for (const c of a.commitments)
|
|
171
|
+
if (isObj(c) && (c.firm !== true || c.kind !== 'abstain'))
|
|
172
|
+
delete c.firm;
|
|
159
173
|
for (const g of a.goals)
|
|
160
174
|
if (isObj(g))
|
|
161
175
|
sanitizeGoalTemporal(g);
|
|
@@ -476,11 +490,16 @@ function heldConviction(p, c) {
|
|
|
476
490
|
* keeps by the same rule, so a pull never leads to an activity that cannot keep the commitment.
|
|
477
491
|
*/
|
|
478
492
|
export const spanMeetsWindow = (c, start, end) => start <= c.until && end >= c.from;
|
|
493
|
+
/** Explicit host progress can name either the adopted goal or its perceived prospect. */
|
|
494
|
+
function namesGoal(g, advances) {
|
|
495
|
+
return ((advances?.includes(g.id) ?? false) ||
|
|
496
|
+
(g.project !== undefined && (advances?.includes(g.project.prospectId) ?? false)));
|
|
497
|
+
}
|
|
479
498
|
function goalAmount(g, aff) {
|
|
480
499
|
const own = g.advancedBy.find((a) => a.action === aff.action);
|
|
481
500
|
if (own)
|
|
482
501
|
return own.amount;
|
|
483
|
-
return aff.advances
|
|
502
|
+
return namesGoal(g, aff.advances) ? AGENDA_DEFAULTS.defaultAdvanceAmount : 0;
|
|
484
503
|
}
|
|
485
504
|
function deadlinePressure(g, now) {
|
|
486
505
|
if (g.deadline === undefined)
|
|
@@ -668,9 +687,14 @@ export function onFinished(p, outcome, startedAt = outcome.at, opts = {}) {
|
|
|
668
687
|
if (g.status !== 'active')
|
|
669
688
|
continue;
|
|
670
689
|
const own = g.advancedBy.find((a) => a.action === outcome.action)?.amount ?? 0;
|
|
671
|
-
const amount = own > 0 ? own : outcome.advances
|
|
690
|
+
const amount = own > 0 ? own : namesGoal(g, outcome.advances) ? AGENDA_DEFAULTS.defaultAdvanceAmount : 0;
|
|
672
691
|
if (amount <= 0)
|
|
673
692
|
continue;
|
|
693
|
+
// Credit the contribution for narration/reward, but only perception sets this project's progress.
|
|
694
|
+
if (g.project?.perceivedProgress) {
|
|
695
|
+
advanced.push(g);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
674
698
|
g.progress = clamp01(g.progress + amount);
|
|
675
699
|
g.lastAdvancedAt = outcome.at;
|
|
676
700
|
if (g.baseImportance !== undefined)
|
|
@@ -701,6 +725,21 @@ export function adoptGoal(p, g, now) {
|
|
|
701
725
|
p.agenda.goals.push(goal);
|
|
702
726
|
return goal;
|
|
703
727
|
}
|
|
728
|
+
/** Attended host progress is authoritative for an active perceived-progress project. @internal */
|
|
729
|
+
export function perceiveProjectProgress(p, prospectId, progress, now) {
|
|
730
|
+
for (const g of p.agenda.goals) {
|
|
731
|
+
if (g.status !== 'active' || !g.project?.perceivedProgress || g.project.prospectId !== prospectId)
|
|
732
|
+
continue;
|
|
733
|
+
if (progress > g.progress) {
|
|
734
|
+
g.lastAdvancedAt = now;
|
|
735
|
+
if (g.baseImportance !== undefined)
|
|
736
|
+
g.importance = g.baseImportance;
|
|
737
|
+
}
|
|
738
|
+
g.progress = clamp01(progress);
|
|
739
|
+
if (g.progress >= 1)
|
|
740
|
+
g.status = 'achieved';
|
|
741
|
+
}
|
|
742
|
+
}
|
|
704
743
|
export function abandonGoal(p, id) {
|
|
705
744
|
const g = p.agenda.goals.find((x) => x.id === id);
|
|
706
745
|
if (g?.status !== 'active')
|
|
@@ -46,6 +46,7 @@ import { expectedEffect } from '../memory/index.js';
|
|
|
46
46
|
import { methodEvidence } from '../methods/index.js';
|
|
47
47
|
import { levelOf, needThreshold, urgencyOf } from '../needs/index.js';
|
|
48
48
|
import { partneringTerms } from '../partnering/index.js';
|
|
49
|
+
import { persistenceTerms } from '../persistence/index.js';
|
|
49
50
|
import { promptIn, promptingTerms } from '../promptings/index.js';
|
|
50
51
|
import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
|
|
51
52
|
import { reviewTerms } from '../review/index.js';
|
|
@@ -461,6 +462,9 @@ export function consider(p, aff, ctx) {
|
|
|
461
462
|
if (p.curiosity)
|
|
462
463
|
for (const t of curiosityTerms(p, aff, ctx))
|
|
463
464
|
push(t.source, t.value);
|
|
465
|
+
if (p.pursuit)
|
|
466
|
+
for (const t of persistenceTerms(p, terms, ctx))
|
|
467
|
+
push(t.source, t.value);
|
|
464
468
|
// Promptings (2.4.0, opt-in): a recurrence pull under `fault` and a practised turning-away from a noticed prompt.
|
|
465
469
|
if (p.promptings)
|
|
466
470
|
for (const t of promptingTerms(p, aff.action, terms, aff.tags, ctx.now, ctx.desperation))
|
|
@@ -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.14.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';
|
|
@@ -28,7 +28,9 @@ export { MIGRATIONS, migratableVersions, migrate } from './migrate.ts';
|
|
|
28
28
|
export * from './narrate/index.ts';
|
|
29
29
|
export * from './needs/index.ts';
|
|
30
30
|
export * from './partnering/index.ts';
|
|
31
|
+
export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.ts';
|
|
31
32
|
export * from './person.ts';
|
|
33
|
+
export { considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal } from './projects/index.ts';
|
|
32
34
|
export * from './promptings/index.ts';
|
|
33
35
|
export type { ConsequenceEvidence, PlanRoute, PlanStep } from './prospection/index.ts';
|
|
34
36
|
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/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.14.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';
|
|
@@ -25,7 +25,9 @@ export { MIGRATIONS, migratableVersions, migrate } from './migrate.js';
|
|
|
25
25
|
export * from './narrate/index.js';
|
|
26
26
|
export * from './needs/index.js';
|
|
27
27
|
export * from './partnering/index.js';
|
|
28
|
+
export { enablePersistence, PERSISTENCE_DEFAULTS, pursuitOf } from './persistence/index.js';
|
|
28
29
|
export * from './person.js';
|
|
30
|
+
export { considerProjects, enableProjects, PROJECTS_DEFAULTS, projectAppeal } from './projects/index.js';
|
|
29
31
|
export * from './promptings/index.js';
|
|
30
32
|
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, enablePlanning, FORECAST_DEFAULTS, forecastLine, forecastThresholds, PROSPECTION_DEFAULTS, seedConsequences, } from './prospection/index.js';
|
|
31
33
|
export { fingerprint, restore, snapshot } from './restore.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.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
|
|
21
|
+
* the earliest-ending one. 2.13.0 to 2.14.0 stamps the version (projects and persistence start absent; goals may carry `project`, received successors may carry `advances`). 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.13.0': {
|
|
74
|
+
to: '2.14.0',
|
|
75
|
+
note: 'projects (Person.projects) and persistence (Person.pursuit) are optional and start absent; Goal.project, Percept.prospect and received successor advances are optional; existing hosts retain exact behavior',
|
|
76
|
+
apply: stamp,
|
|
77
|
+
},
|
|
78
|
+
'2.12.0': {
|
|
79
|
+
to: '2.13.0',
|
|
80
|
+
note: 'suggestion reasons (Person.will.suggestionReasons) and firm self-rules (Commitment.firm) are optional and start absent; existing hosts retain exact behavior',
|
|
81
|
+
apply: stamp,
|
|
82
|
+
},
|
|
73
83
|
'2.11.0': {
|
|
74
84
|
to: '2.12.0',
|
|
75
85
|
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',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './persistence.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './persistence.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PersistenceSpec, Person, Pursuit, PursuitState } from '../types.ts';
|
|
2
|
+
/** Engineering defaults, relative to cognition's need scale 4, pleasure .6 and curiosity .8/.15.
|
|
3
|
+
* Continuation and switching together outweigh mild relaxed distraction; urgency and fear gate both.
|
|
4
|
+
* A pressing commitment has its full agenda term and pays no switching cost.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PERSISTENCE_DEFAULTS: {
|
|
7
|
+
readonly continueScale: 0.8;
|
|
8
|
+
readonly switchScale: 0.8;
|
|
9
|
+
readonly cap: 0.8;
|
|
10
|
+
readonly switchMargin: 0.15;
|
|
11
|
+
readonly slackMinutes: 20;
|
|
12
|
+
readonly maxMinutes: 240;
|
|
13
|
+
readonly restMinutes: 20;
|
|
14
|
+
};
|
|
15
|
+
/** Enable or reconfigure, retaining a current pursuit. Invalid overrides use defaults. */
|
|
16
|
+
export declare function enablePersistence(p: Person, spec?: PersistenceSpec): PursuitState;
|
|
17
|
+
/** A copy of the current pursuit, without pruning, mutation or RNG. */
|
|
18
|
+
export declare function pursuitOf(p: Person): Pursuit | undefined;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: opt-in persistence of a started goal, owned only in p.pursuit. Positive goal terms cue
|
|
3
|
+
* continued means (implementation intentions: Gollwitzer 1999; Gollwitzer & Sheeran 2006), while
|
|
4
|
+
* unrelated options pay a switching cost (goal shielding: Shah, Friedman & Kruglanski 2002;
|
|
5
|
+
* task switching: Monsell 2003). Unused activation expires (Altmann & Trafton 2002); short
|
|
6
|
+
* interruptions allow resumption (Ovsiankina 1928). See research/projects-and-persistence.md.
|
|
7
|
+
* Maximum-duration release rests that goal for one bounded interval before pursuit can form again.
|
|
8
|
+
* These are shapes, not fitted values; all coefficients and time bounds are engineering defaults.
|
|
9
|
+
* This is a bounded bias plus a switching cost, not a planner. It chooses no goals, executes no
|
|
10
|
+
* plans, reorders no steps and changes no agenda valuation. It imports no projects, uses no RNG,
|
|
11
|
+
* LLM or faith lens. Needs and fear gate both terms; commitment options never pay the switch cost.
|
|
12
|
+
* Without the slice, it adds no state or terms.
|
|
13
|
+
*/
|
|
14
|
+
import { emotionLevel } from '../affect/index.js';
|
|
15
|
+
import { clamp01, cmpStr, isNum, isObj, round } from '../core/index.js';
|
|
16
|
+
import { MAX_MINUTE, PHYSIOLOGICAL_NEEDS } from '../types.js';
|
|
17
|
+
/** Engineering defaults, relative to cognition's need scale 4, pleasure .6 and curiosity .8/.15.
|
|
18
|
+
* Continuation and switching together outweigh mild relaxed distraction; urgency and fear gate both.
|
|
19
|
+
* A pressing commitment has its full agenda term and pays no switching cost.
|
|
20
|
+
*/
|
|
21
|
+
export const PERSISTENCE_DEFAULTS = {
|
|
22
|
+
continueScale: 0.8,
|
|
23
|
+
switchScale: 0.8,
|
|
24
|
+
cap: 0.8,
|
|
25
|
+
switchMargin: 0.15,
|
|
26
|
+
slackMinutes: 20,
|
|
27
|
+
maxMinutes: 240,
|
|
28
|
+
restMinutes: 20,
|
|
29
|
+
};
|
|
30
|
+
const KEYS = [
|
|
31
|
+
'continueScale',
|
|
32
|
+
'switchScale',
|
|
33
|
+
'cap',
|
|
34
|
+
'switchMargin',
|
|
35
|
+
'slackMinutes',
|
|
36
|
+
'maxMinutes',
|
|
37
|
+
'restMinutes',
|
|
38
|
+
];
|
|
39
|
+
function cleanSpec(input) {
|
|
40
|
+
const out = {};
|
|
41
|
+
if (!isObj(input))
|
|
42
|
+
return out;
|
|
43
|
+
for (const key of KEYS) {
|
|
44
|
+
const x = input[key];
|
|
45
|
+
const max = key === 'slackMinutes' || key === 'maxMinutes' || key === 'restMinutes' ? 1440 : 1;
|
|
46
|
+
if (isNum(x) && x >= 0 && x <= max)
|
|
47
|
+
out[key] = x;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
const setting = (s, key) => s.spec?.[key] ?? PERSISTENCE_DEFAULTS[key];
|
|
52
|
+
const identifier = (x) => typeof x === 'string' && x.length > 0 && x.length <= 512;
|
|
53
|
+
const active = (p, id) => p.agenda.goals.find((g) => g.id === id && g.status === 'active');
|
|
54
|
+
/** Enable or reconfigure, retaining a current pursuit. Invalid overrides use defaults. */
|
|
55
|
+
export function enablePersistence(p, spec = {}) {
|
|
56
|
+
const out = {};
|
|
57
|
+
const clean = cleanSpec(spec);
|
|
58
|
+
if (Object.keys(clean).length)
|
|
59
|
+
out.spec = clean;
|
|
60
|
+
if (p.pursuit?.current)
|
|
61
|
+
out.current = { ...p.pursuit.current };
|
|
62
|
+
if (p.pursuit?.rested)
|
|
63
|
+
out.rested = { ...p.pursuit.rested };
|
|
64
|
+
p.pursuit = out;
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** A copy of the current pursuit, without pruning, mutation or RNG. */
|
|
68
|
+
export function pursuitOf(p) {
|
|
69
|
+
return p.pursuit?.current ? { ...p.pursuit.current } : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Drop finished, absent, neglected or overlong pursuits. Interruptions alone do not release. @internal */
|
|
72
|
+
export function releasePursuit(p) {
|
|
73
|
+
const s = p.pursuit;
|
|
74
|
+
if (!s)
|
|
75
|
+
return;
|
|
76
|
+
if (s.rested && p.now >= s.rested.until)
|
|
77
|
+
delete s.rested;
|
|
78
|
+
const c = s.current;
|
|
79
|
+
if (!c)
|
|
80
|
+
return;
|
|
81
|
+
if (!active(p, c.goalId) || p.now - c.lastServedAt >= setting(s, 'slackMinutes')) {
|
|
82
|
+
delete s.current;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (p.now - c.since >= setting(s, 'maxMinutes')) {
|
|
86
|
+
delete s.current;
|
|
87
|
+
s.rested = { goalId: c.goalId, until: Math.min(MAX_MINUTE, p.now + setting(s, 'restMinutes')) };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Form from the begun option's already-rounded goal terms, including route credit. @internal */
|
|
91
|
+
export function beginPursuit(p, terms) {
|
|
92
|
+
const s = p.pursuit;
|
|
93
|
+
if (!s)
|
|
94
|
+
return;
|
|
95
|
+
releasePursuit(p);
|
|
96
|
+
const candidates = p.agenda.goals
|
|
97
|
+
.filter((g) => g.status === 'active' && identifier(g.id) && g.id !== s.rested?.goalId)
|
|
98
|
+
.map((goal) => ({
|
|
99
|
+
goal,
|
|
100
|
+
value: terms
|
|
101
|
+
.filter((t) => t.source === `goal:${goal.id}` && t.value > 0)
|
|
102
|
+
.reduce((v, t) => v + t.value, 0),
|
|
103
|
+
}))
|
|
104
|
+
.filter((x) => x.value > 0)
|
|
105
|
+
.sort((a, b) => b.value - a.value || cmpStr(a.goal.id, b.goal.id));
|
|
106
|
+
const best = candidates[0];
|
|
107
|
+
if (!best)
|
|
108
|
+
return;
|
|
109
|
+
const current = s.current;
|
|
110
|
+
const currentValue = candidates.find((x) => x.goal.id === current?.goalId)?.value ?? 0;
|
|
111
|
+
if (current &&
|
|
112
|
+
best.goal.id !== current.goalId &&
|
|
113
|
+
round(best.value - currentValue) <= setting(s, 'switchMargin')) {
|
|
114
|
+
if (currentValue > 0)
|
|
115
|
+
current.lastServedAt = p.now;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
s.current = {
|
|
119
|
+
goalId: best.goal.id,
|
|
120
|
+
since: current?.goalId === best.goal.id ? current.since : p.now,
|
|
121
|
+
lastServedAt: p.now,
|
|
122
|
+
strength: clamp01(best.goal.importance * (0.4 + 0.6 * p.traits.conscientiousness)),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Read already-pushed goal/commitment terms under the actual perceived needs. @internal */
|
|
126
|
+
export function persistenceTerms(p, terms, ctx) {
|
|
127
|
+
const s = p.pursuit;
|
|
128
|
+
const c = s?.current;
|
|
129
|
+
if (!s || !c || !active(p, c.goalId))
|
|
130
|
+
return [];
|
|
131
|
+
let urgency = 0;
|
|
132
|
+
for (const n of ctx.needs)
|
|
133
|
+
if (n.id === 'safety' || PHYSIOLOGICAL_NEEDS.includes(n.id))
|
|
134
|
+
urgency = Math.max(urgency, n.urgency);
|
|
135
|
+
const gate = (1 - clamp01(urgency)) * (1 - emotionLevel(p, 'fear'));
|
|
136
|
+
if (terms.some((t) => t.source === `goal:${c.goalId}` && t.value > 0))
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
source: `pursuit:${c.goalId}`,
|
|
140
|
+
value: round(Math.min(setting(s, 'cap'), setting(s, 'continueScale') * c.strength * gate)),
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
if (terms.some((t) => t.source.startsWith('commitment:')))
|
|
144
|
+
return [];
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
source: 'pursuit:switch',
|
|
148
|
+
value: -round(Math.min(setting(s, 'cap'), setting(s, 'switchScale') * c.strength * gate)),
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
const minute = (x, now) => isNum(x) && Math.abs(x) <= MAX_MINUTE && x <= now;
|
|
153
|
+
/** Conservative restore: malformed settings or pursuit drop the optional slice. @internal */
|
|
154
|
+
export function sanitizePursuit(input, now) {
|
|
155
|
+
if (!isObj(input) || (input.spec !== undefined && !isObj(input.spec)))
|
|
156
|
+
return undefined;
|
|
157
|
+
const out = {};
|
|
158
|
+
if (input.spec !== undefined) {
|
|
159
|
+
const spec = cleanSpec(input.spec);
|
|
160
|
+
if (Object.keys(spec).length !== Object.keys(input.spec).length)
|
|
161
|
+
return undefined;
|
|
162
|
+
if (Object.keys(spec).length)
|
|
163
|
+
out.spec = spec;
|
|
164
|
+
}
|
|
165
|
+
const c = input.current;
|
|
166
|
+
if (c !== undefined) {
|
|
167
|
+
if (!isObj(c) ||
|
|
168
|
+
!identifier(c.goalId) ||
|
|
169
|
+
!minute(c.since, now) ||
|
|
170
|
+
!minute(c.lastServedAt, now) ||
|
|
171
|
+
c.lastServedAt < c.since ||
|
|
172
|
+
!isNum(c.strength) ||
|
|
173
|
+
c.strength < 0 ||
|
|
174
|
+
c.strength > 1)
|
|
175
|
+
return undefined;
|
|
176
|
+
out.current = { goalId: c.goalId, since: c.since, lastServedAt: c.lastServedAt, strength: c.strength };
|
|
177
|
+
}
|
|
178
|
+
const r = input.rested;
|
|
179
|
+
if (r !== undefined) {
|
|
180
|
+
if (!isObj(r) ||
|
|
181
|
+
!identifier(r.goalId) ||
|
|
182
|
+
!isNum(r.until) ||
|
|
183
|
+
Math.abs(r.until) > MAX_MINUTE ||
|
|
184
|
+
// A later reconfiguration may shorten restMinutes without shortening an existing interval.
|
|
185
|
+
r.until > now + 1440)
|
|
186
|
+
return undefined;
|
|
187
|
+
out.rested = { goalId: r.goalId, until: r.until };
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
package/dist/person.js
CHANGED
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
* long-horizon runs. Each hook calls the owning module; nothing here writes another module's slice.
|
|
21
21
|
*/
|
|
22
22
|
import { actionTendencies, advanceAffect, appraise, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, feel, readAffect, regulate, release, skipAffect, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
|
|
23
|
-
import { advanceAgenda, createAgenda, onFinished, promise, proposeGoals, revisePurposes, } from './agenda/index.js';
|
|
23
|
+
import { advanceAgenda, agendaTermsWithGoalEstimates, createAgenda, onFinished, promise, proposeGoals, revisePurposes, } from './agenda/index.js';
|
|
24
24
|
import { advanceBeliefs, applyContradiction, attend, believe, confirm, credence, noteProvenance, trustOf, } from './beliefs/index.js';
|
|
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
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
|
-
import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay } from './core/index.js';
|
|
30
|
+
import { clamp01, clampSigned, createRng, dayOf, isObj, minuteOfDay, round } from './core/index.js';
|
|
31
31
|
import { enableCuriosity, noteCuriosity } from './curiosity/index.js';
|
|
32
32
|
import { ambientBodyParams, ambientModifiers, ambientMood, ambientNeeds } from './environment/index.js';
|
|
33
33
|
import { aptitudeOf, createFamily, pregnancyModifiers } from './family/index.js';
|
|
@@ -37,13 +37,15 @@ import { advanceMemory, createMemory, enableGists, foldGists, learnOutcome, reca
|
|
|
37
37
|
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
|
+
import { beginPursuit, enablePersistence, releasePursuit } from './persistence/index.js';
|
|
41
|
+
import { considerProjects, enableProjects, noteProjectStatus, perceiveProspect } from './projects/index.js';
|
|
40
42
|
import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
|
|
41
43
|
import { bodyForecastFor, consumeConsequences, enablePlanning, forecastLine, forecastThresholds, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
42
44
|
import { enableReview, reviewIfDue } from './review/index.js';
|
|
43
45
|
import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
|
|
44
46
|
import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, noteKnownBy, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
|
|
45
47
|
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';
|
|
48
|
+
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
49
|
export const PERSON_DEFAULTS = {
|
|
48
50
|
/** Body load while idle and awake. */
|
|
49
51
|
idleLoad: { effort: 0.05, focus: 0.05, mode: 'awake' },
|
|
@@ -148,6 +150,8 @@ export function createPerson(spec) {
|
|
|
148
150
|
if (family)
|
|
149
151
|
p.family = family;
|
|
150
152
|
const on = spec.enable;
|
|
153
|
+
if (on?.suggestionReasons)
|
|
154
|
+
enableSuggestionReasons(p);
|
|
151
155
|
if (on?.gists)
|
|
152
156
|
enableGists(p);
|
|
153
157
|
if (on?.yearbook)
|
|
@@ -160,8 +164,12 @@ export function createPerson(spec) {
|
|
|
160
164
|
enableFatigueStore(p);
|
|
161
165
|
if (on?.fullness)
|
|
162
166
|
enableFullness(p);
|
|
167
|
+
if (on?.projects)
|
|
168
|
+
enableProjects(p, on.projects === true ? {} : on.projects);
|
|
163
169
|
if (on?.curiosity)
|
|
164
170
|
enableCuriosity(p, on.curiosity === true ? {} : on.curiosity);
|
|
171
|
+
if (on?.persistence)
|
|
172
|
+
enablePersistence(p, on.persistence === true ? {} : on.persistence);
|
|
165
173
|
if (on?.planning)
|
|
166
174
|
enablePlanning(p, on.planning);
|
|
167
175
|
if (on?.promptings)
|
|
@@ -365,11 +373,15 @@ function recordKeptAbstentions(p, kept, now) {
|
|
|
365
373
|
* rounding (~1e-9) unless calls also fall on the grid, in which case replay is byte-identical.
|
|
366
374
|
*/
|
|
367
375
|
export function tick(p, now) {
|
|
376
|
+
if (p.pursuit)
|
|
377
|
+
releasePursuit(p);
|
|
368
378
|
if (now <= p.now)
|
|
369
379
|
return;
|
|
370
380
|
if (!p.body.alive) {
|
|
371
381
|
p.now = now;
|
|
372
382
|
pruneConsequences(p);
|
|
383
|
+
if (p.pursuit)
|
|
384
|
+
releasePursuit(p);
|
|
373
385
|
return;
|
|
374
386
|
}
|
|
375
387
|
const seg = PERSON_DEFAULTS.maxSegment;
|
|
@@ -384,15 +396,21 @@ export function tick(p, now) {
|
|
|
384
396
|
if (!(end > p.now)) {
|
|
385
397
|
p.now = now;
|
|
386
398
|
pruneConsequences(p);
|
|
399
|
+
if (p.pursuit)
|
|
400
|
+
releasePursuit(p);
|
|
387
401
|
return;
|
|
388
402
|
}
|
|
389
403
|
advanceSegment(p, end - p.now, load, active && act ? act.affordance : undefined);
|
|
390
404
|
p.now = end;
|
|
391
405
|
pruneConsequences(p);
|
|
406
|
+
if (p.pursuit)
|
|
407
|
+
releasePursuit(p);
|
|
392
408
|
if (!p.body.alive) {
|
|
393
409
|
p.activity = null;
|
|
394
410
|
p.now = now;
|
|
395
411
|
pruneConsequences(p);
|
|
412
|
+
if (p.pursuit)
|
|
413
|
+
releasePursuit(p);
|
|
396
414
|
return;
|
|
397
415
|
}
|
|
398
416
|
// Discrete bookkeeping runs only on grid boundaries, so it happens at the same minutes whatever the
|
|
@@ -452,6 +470,10 @@ export function tick(p, now) {
|
|
|
452
470
|
}
|
|
453
471
|
}
|
|
454
472
|
}
|
|
473
|
+
if (p.projects)
|
|
474
|
+
considerProjects(p, end);
|
|
475
|
+
if (p.pursuit)
|
|
476
|
+
releasePursuit(p);
|
|
455
477
|
}
|
|
456
478
|
}
|
|
457
479
|
/** The composite's side of the hourly crisis check: interrupt and remember an onset, remember a recovery. */
|
|
@@ -544,6 +566,8 @@ export function skip(p, to) {
|
|
|
544
566
|
skipBody(p, to);
|
|
545
567
|
p.now = to;
|
|
546
568
|
pruneConsequences(p);
|
|
569
|
+
if (p.pursuit)
|
|
570
|
+
releasePursuit(p);
|
|
547
571
|
skipAffect(p, to);
|
|
548
572
|
advanceAgenda(p, to);
|
|
549
573
|
// Lasting gists (1.8.0, opt-in): episodes now past the horizon fold into gists.
|
|
@@ -563,6 +587,8 @@ function socialKindOf(kind) {
|
|
|
563
587
|
}
|
|
564
588
|
function perceiveOne(p, pc) {
|
|
565
589
|
receiveConsequences(p, pc);
|
|
590
|
+
if (p.projects && pc.prospect)
|
|
591
|
+
perceiveProspect(p, pc.prospect, pc.at);
|
|
566
592
|
const at = Math.min(Math.max(pc.at, p.affect.lastUpdated), p.now);
|
|
567
593
|
const actor = pc.actorId;
|
|
568
594
|
const target = pc.targetId;
|
|
@@ -1019,6 +1045,20 @@ export function begin(p, aff, record, opts = {}) {
|
|
|
1019
1045
|
if (!p.body.alive)
|
|
1020
1046
|
return null;
|
|
1021
1047
|
const now = p.now;
|
|
1048
|
+
if (p.pursuit) {
|
|
1049
|
+
const considered = record.considered.find((c) => c.affordanceId === aff.id);
|
|
1050
|
+
let terms = considered?.terms;
|
|
1051
|
+
if (!terms) {
|
|
1052
|
+
const { ctx } = decisionInputs(p, [aff], record.id, {});
|
|
1053
|
+
const route = prospectiveRoute(p, aff, ctx);
|
|
1054
|
+
const scale = 0.5 + 0.5 * clamp01(ctx.mods.maturity);
|
|
1055
|
+
terms = agendaTermsWithGoalEstimates(p, aff, now, route?.goals).map((t) => ({
|
|
1056
|
+
source: t.source,
|
|
1057
|
+
value: round(t.value * scale),
|
|
1058
|
+
}));
|
|
1059
|
+
}
|
|
1060
|
+
beginPursuit(p, terms);
|
|
1061
|
+
}
|
|
1022
1062
|
const duration = Number.isFinite(aff.duration) ? Math.max(1, Math.round(aff.duration)) : 1;
|
|
1023
1063
|
const mode = aff.mode ?? 'awake';
|
|
1024
1064
|
const load = { effort: clamp01(aff.effort), focus: clamp01(aff.focus ?? 0), mode };
|
|
@@ -1293,6 +1333,8 @@ function learnActivityVoices(p, act, after, now, felt, broken, breached) {
|
|
|
1293
1333
|
* relationships, trust in the advising voice, injuries and illness. Clears the activity.
|
|
1294
1334
|
*/
|
|
1295
1335
|
export function finish(p, outcome, opts = {}) {
|
|
1336
|
+
if (p.pursuit)
|
|
1337
|
+
releasePursuit(p);
|
|
1296
1338
|
const act = p.activity;
|
|
1297
1339
|
if (!act)
|
|
1298
1340
|
return null;
|
|
@@ -1407,6 +1449,10 @@ export function finish(p, outcome, opts = {}) {
|
|
|
1407
1449
|
...(act.necessity ? { necessity: true } : {}),
|
|
1408
1450
|
...(opts.catalog ? { catalog: opts.catalog } : {}),
|
|
1409
1451
|
});
|
|
1452
|
+
if (p.projects)
|
|
1453
|
+
noteProjectStatus(p, now);
|
|
1454
|
+
if (p.pursuit)
|
|
1455
|
+
releasePursuit(p);
|
|
1410
1456
|
// A completed violating action inside an open abstention's window broke it (the fast, e.g. "no work after the evening prayer").
|
|
1411
1457
|
if (broken.length > 0) {
|
|
1412
1458
|
recordMissed(p, broken, now, outcome.action);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './projects.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './projects.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Minute, Person, ProjectsSpec, ProjectsState, Prospect } from '../types.ts';
|
|
2
|
+
export declare const PROJECTS_DEFAULTS: {
|
|
3
|
+
readonly maxProspects: 8;
|
|
4
|
+
readonly maxHistory: 8;
|
|
5
|
+
readonly forgetMinutes: 1440;
|
|
6
|
+
readonly calmUrgency: 0.35;
|
|
7
|
+
readonly fearBelow: 0.35;
|
|
8
|
+
readonly adoptEveryMinutes: 30;
|
|
9
|
+
readonly cooldownMinutes: 60;
|
|
10
|
+
readonly adoptAppeal: 0.3;
|
|
11
|
+
readonly halfLifeMinutes: 120;
|
|
12
|
+
readonly staleMinutes: 180;
|
|
13
|
+
};
|
|
14
|
+
/** Enable or reconfigure without discarding retained prospects and history. */
|
|
15
|
+
export declare function enableProjects(p: Person, spec?: ProjectsSpec): ProjectsState;
|
|
16
|
+
/** Read-only, deterministic appeal. Thresholds close the relaxed gate before the bounded bonus. */
|
|
17
|
+
export declare function projectAppeal(p: Person, prospect: Prospect, now?: Minute): number;
|
|
18
|
+
/** Only the composite calls this, after attention. Newest event minute wins; equal-minute input order wins. */
|
|
19
|
+
export declare function perceiveProspect(p: Person, input: Prospect, at: Minute): void;
|
|
20
|
+
/** Called on each tick segment, including one-minute begin/tick/finish host loops. No RNG. */
|
|
21
|
+
export declare function considerProjects(p: Person, now: Minute): void;
|
|
22
|
+
/** Conservative restore: bounded fields, finite past timestamps, and no ambiguous duplicate ids. */
|
|
23
|
+
export declare function sanitizeProjects(input: unknown, now: Minute): ProjectsState | undefined;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: self-set, bounded pursuits from host-shown, attended prospects. Fit to temperament and
|
|
3
|
+
* relaxed needs chooses one project; perceived progress or completed acts advances it; neglect drops
|
|
4
|
+
* it. Shapes, not fitted values: Little's personal projects (1983), Locke & Latham's goal setting
|
|
5
|
+
* (2002), autonomy/competence in Deci & Ryan and Ryan & Deci (2000), constructive play (Smilansky
|
|
6
|
+
* 1968; Burghardt 2005), and interrupted-task resumption (Ovsiankina 1928). The resumption bonus
|
|
7
|
+
* models a pull to continue, not a Zeigarnik memory advantage. See research/projects-and-persistence.md.
|
|
8
|
+
* All thresholds and coefficients are engineering defaults. This owns only p.projects; agenda owns
|
|
9
|
+
* goals. All kinds serve competence and leisure: making is chosen as a free-time pursuit, not duty.
|
|
10
|
+
* Restore recovers one active project, keeping the newest (id breaks ties) and abandoning extras.
|
|
11
|
+
* No invented prospects, host task interpretation, world search, persistence, shielding, faith lens,
|
|
12
|
+
* LLM or RNG. Without the slice, perception and minute bookkeeping write nothing.
|
|
13
|
+
*/
|
|
14
|
+
import { emotionLevel } from '../affect/index.js';
|
|
15
|
+
import { AGENDA_DEFAULTS, abandonGoal, adoptGoal, perceiveProjectProgress } from '../agenda/index.js';
|
|
16
|
+
import { clamp01, cmpStr, isNum, isObj, round } from '../core/index.js';
|
|
17
|
+
import { curiosityOf } from '../curiosity/index.js';
|
|
18
|
+
import { readPerson } from '../person.js';
|
|
19
|
+
import { MAX_MINUTE, PHYSIOLOGICAL_NEEDS } from '../types.js';
|
|
20
|
+
export const PROJECTS_DEFAULTS = {
|
|
21
|
+
maxProspects: 8,
|
|
22
|
+
maxHistory: 8,
|
|
23
|
+
forgetMinutes: 1440,
|
|
24
|
+
calmUrgency: 0.35,
|
|
25
|
+
fearBelow: 0.35,
|
|
26
|
+
adoptEveryMinutes: 30,
|
|
27
|
+
cooldownMinutes: 60,
|
|
28
|
+
adoptAppeal: 0.3,
|
|
29
|
+
halfLifeMinutes: 120,
|
|
30
|
+
staleMinutes: 180,
|
|
31
|
+
};
|
|
32
|
+
const KEYS = [
|
|
33
|
+
'forgetMinutes',
|
|
34
|
+
'calmUrgency',
|
|
35
|
+
'fearBelow',
|
|
36
|
+
'adoptEveryMinutes',
|
|
37
|
+
'cooldownMinutes',
|
|
38
|
+
'adoptAppeal',
|
|
39
|
+
'halfLifeMinutes',
|
|
40
|
+
'staleMinutes',
|
|
41
|
+
];
|
|
42
|
+
function cleanSpec(input) {
|
|
43
|
+
const out = {};
|
|
44
|
+
if (!isObj(input))
|
|
45
|
+
return out;
|
|
46
|
+
for (const key of KEYS) {
|
|
47
|
+
const x = input[key];
|
|
48
|
+
const unit = key === 'calmUrgency' || key === 'fearBelow' || key === 'adoptAppeal';
|
|
49
|
+
if (isNum(x) && x >= (unit ? 0 : 1) && x <= (unit ? 1 : 525600))
|
|
50
|
+
out[key] = x;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
const setting = (s, key) => s.spec?.[key] ?? PROJECTS_DEFAULTS[key];
|
|
55
|
+
const str = (x) => typeof x === 'string' && x.length > 0 && x.length <= 512;
|
|
56
|
+
const unit = (x) => isNum(x) && x >= 0 && x <= 1;
|
|
57
|
+
const minute = (x, now) => isNum(x) && Math.abs(x) <= MAX_MINUTE && x <= now;
|
|
58
|
+
function cleanProspect(x) {
|
|
59
|
+
if (!isObj(x) ||
|
|
60
|
+
!str(x.id) ||
|
|
61
|
+
!str(x.label) ||
|
|
62
|
+
!['make', 'arrange', 'gather', 'finish'].includes(x.kind) ||
|
|
63
|
+
(x.placeId !== undefined && !str(x.placeId)) ||
|
|
64
|
+
(x.progress !== undefined && !unit(x.progress)) ||
|
|
65
|
+
(x.materialIds !== undefined &&
|
|
66
|
+
(!Array.isArray(x.materialIds) || x.materialIds.length > 256 || !x.materialIds.every(str))))
|
|
67
|
+
return undefined;
|
|
68
|
+
const p = { id: x.id, label: x.label, kind: x.kind };
|
|
69
|
+
if (x.placeId !== undefined)
|
|
70
|
+
p.placeId = x.placeId;
|
|
71
|
+
if (x.progress !== undefined)
|
|
72
|
+
p.progress = x.progress;
|
|
73
|
+
// A long list is kept, not rejected: the first eight distinct ids in the host's order (island rerun, 2026-10-20).
|
|
74
|
+
if (x.materialIds !== undefined)
|
|
75
|
+
p.materialIds = [...new Set(x.materialIds)].slice(0, 8);
|
|
76
|
+
return p;
|
|
77
|
+
}
|
|
78
|
+
/** Enable or reconfigure without discarding retained prospects and history. */
|
|
79
|
+
export function enableProjects(p, spec = {}) {
|
|
80
|
+
const state = p.projects ?? { prospects: [], history: [] };
|
|
81
|
+
const clean = cleanSpec(spec);
|
|
82
|
+
delete state.spec;
|
|
83
|
+
if (Object.keys(clean).length)
|
|
84
|
+
state.spec = clean;
|
|
85
|
+
p.projects = state;
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
88
|
+
function relaxed(p) {
|
|
89
|
+
const needs = readPerson(p).needs;
|
|
90
|
+
return {
|
|
91
|
+
urgency: Math.max(0, ...needs
|
|
92
|
+
.filter((n) => n.id === 'safety' || PHYSIOLOGICAL_NEEDS.includes(n.id))
|
|
93
|
+
.map((n) => n.urgency)),
|
|
94
|
+
fear: emotionLevel(p, 'fear'),
|
|
95
|
+
interest: Math.max(0, ...needs.filter((n) => ['leisure', 'competence', 'autonomy'].includes(n.id)).map((n) => n.urgency)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Read-only, deterministic appeal. Thresholds close the relaxed gate before the bounded bonus. */
|
|
99
|
+
export function projectAppeal(p, prospect, now = p.now) {
|
|
100
|
+
const s = p.projects;
|
|
101
|
+
if (!s ||
|
|
102
|
+
!cleanProspect(prospect) ||
|
|
103
|
+
prospect.progress === 1 ||
|
|
104
|
+
p.body.asleep ||
|
|
105
|
+
p.activity?.mode === 'sleep' ||
|
|
106
|
+
!p.body.alive)
|
|
107
|
+
return 0;
|
|
108
|
+
const { urgency, fear, interest } = relaxed(p);
|
|
109
|
+
if (urgency >= setting(s, 'calmUrgency') || fear >= setting(s, 'fearBelow'))
|
|
110
|
+
return 0;
|
|
111
|
+
const gate = (1 - urgency) * (1 - fear);
|
|
112
|
+
const fit = 0.25 + 0.75 * (prospect.kind === 'finish' ? p.traits.conscientiousness : p.traits.openness);
|
|
113
|
+
const resume = prospect.progress !== undefined && prospect.progress > 0 && prospect.progress < 1 ? 0.1 : 0;
|
|
114
|
+
const bonus = p.curiosity
|
|
115
|
+
? 0.05 *
|
|
116
|
+
clamp01(curiosityOf(p, {
|
|
117
|
+
id: prospect.id,
|
|
118
|
+
action: prospect.kind,
|
|
119
|
+
label: prospect.label,
|
|
120
|
+
duration: 1,
|
|
121
|
+
effort: 0,
|
|
122
|
+
advertises: {},
|
|
123
|
+
}, now).drive)
|
|
124
|
+
: 0;
|
|
125
|
+
return round(clamp01(gate * (fit * (0.5 + 0.5 * interest) + resume + bonus)));
|
|
126
|
+
}
|
|
127
|
+
function recordProjectStatus(p, g, now) {
|
|
128
|
+
const s = p.projects;
|
|
129
|
+
if (!s || !g.project || g.status === 'active')
|
|
130
|
+
return;
|
|
131
|
+
if (!s.history.some((h) => h.goalId === g.id))
|
|
132
|
+
s.history.push({
|
|
133
|
+
prospectId: g.project.prospectId,
|
|
134
|
+
goalId: g.id,
|
|
135
|
+
adoptedAt: g.adoptedAt,
|
|
136
|
+
outcome: g.status,
|
|
137
|
+
at: now,
|
|
138
|
+
});
|
|
139
|
+
if (g.status === 'abandoned')
|
|
140
|
+
s.lastAbandonedAt = now;
|
|
141
|
+
else {
|
|
142
|
+
// Completed event-progress projects have no host progress reading: retain the known completion.
|
|
143
|
+
const prospect = s.prospects.find((x) => x.id === g.project?.prospectId);
|
|
144
|
+
if (prospect)
|
|
145
|
+
prospect.progress = 1;
|
|
146
|
+
}
|
|
147
|
+
if (s.history.length > PROJECTS_DEFAULTS.maxHistory)
|
|
148
|
+
s.history.shift();
|
|
149
|
+
if (s.currentGoalId === g.id)
|
|
150
|
+
delete s.currentGoalId;
|
|
151
|
+
}
|
|
152
|
+
/** Record the adopted project's terminal state once, even after history eviction. @internal */
|
|
153
|
+
export function noteProjectStatus(p, now) {
|
|
154
|
+
const s = p.projects;
|
|
155
|
+
if (!s?.currentGoalId)
|
|
156
|
+
return;
|
|
157
|
+
const g = p.agenda.goals.find((g) => g.id === s.currentGoalId);
|
|
158
|
+
if (!g?.project)
|
|
159
|
+
delete s.currentGoalId;
|
|
160
|
+
else
|
|
161
|
+
recordProjectStatus(p, g, now);
|
|
162
|
+
}
|
|
163
|
+
/** Only the composite calls this, after attention. Newest event minute wins; equal-minute input order wins. */
|
|
164
|
+
export function perceiveProspect(p, input, at) {
|
|
165
|
+
const s = p.projects;
|
|
166
|
+
if (!s || !minute(at, p.now))
|
|
167
|
+
return;
|
|
168
|
+
const prospect = cleanProspect(input);
|
|
169
|
+
if (!prospect)
|
|
170
|
+
return;
|
|
171
|
+
const old = s.prospects.find((x) => x.id === prospect.id);
|
|
172
|
+
if (old && old.lastSeenAt > at)
|
|
173
|
+
return;
|
|
174
|
+
if (p.now - at > setting(s, 'forgetMinutes'))
|
|
175
|
+
return;
|
|
176
|
+
const held = { ...prospect, lastSeenAt: at };
|
|
177
|
+
if (old)
|
|
178
|
+
s.prospects[s.prospects.indexOf(old)] = held;
|
|
179
|
+
else {
|
|
180
|
+
if (s.prospects.length >= PROJECTS_DEFAULTS.maxProspects) {
|
|
181
|
+
let oldest = 0;
|
|
182
|
+
for (const [i, entry] of s.prospects.entries())
|
|
183
|
+
if (entry.lastSeenAt < (s.prospects[oldest]?.lastSeenAt ?? Infinity))
|
|
184
|
+
oldest = i;
|
|
185
|
+
s.prospects.splice(oldest, 1);
|
|
186
|
+
}
|
|
187
|
+
s.prospects.push(held);
|
|
188
|
+
}
|
|
189
|
+
if (prospect.progress !== undefined)
|
|
190
|
+
perceiveProjectProgress(p, prospect.id, prospect.progress, p.now);
|
|
191
|
+
noteProjectStatus(p, p.now);
|
|
192
|
+
}
|
|
193
|
+
/** Called on each tick segment, including one-minute begin/tick/finish host loops. No RNG. */
|
|
194
|
+
export function considerProjects(p, now) {
|
|
195
|
+
const s = p.projects;
|
|
196
|
+
if (!s || !minute(now, p.now))
|
|
197
|
+
return;
|
|
198
|
+
s.prospects = s.prospects.filter((x) => now - x.lastSeenAt <= setting(s, 'forgetMinutes'));
|
|
199
|
+
for (const g of p.agenda.goals)
|
|
200
|
+
if (g.project &&
|
|
201
|
+
g.status === 'active' &&
|
|
202
|
+
now - (g.lastAdvancedAt ?? g.adoptedAt) >= setting(s, 'staleMinutes')) {
|
|
203
|
+
abandonGoal(p, g.id);
|
|
204
|
+
recordProjectStatus(p, g, now);
|
|
205
|
+
}
|
|
206
|
+
noteProjectStatus(p, now);
|
|
207
|
+
const active = p.agenda.goals.filter((g) => g.status === 'active');
|
|
208
|
+
if (active.some((g) => g.project) ||
|
|
209
|
+
active.length >= AGENDA_DEFAULTS.maxActiveGoals ||
|
|
210
|
+
p.agenda.commitments.some((c) => c.status === 'pending' && c.from <= now && c.until >= now) ||
|
|
211
|
+
(s.lastAdoptedAt !== undefined && now - s.lastAdoptedAt < setting(s, 'adoptEveryMinutes')) ||
|
|
212
|
+
(s.lastAbandonedAt !== undefined && now - s.lastAbandonedAt < setting(s, 'cooldownMinutes')))
|
|
213
|
+
return;
|
|
214
|
+
const best = s.prospects
|
|
215
|
+
.map((prospect) => ({ prospect, appeal: projectAppeal(p, prospect, now) }))
|
|
216
|
+
.filter((x) => x.appeal > setting(s, 'adoptAppeal'))
|
|
217
|
+
.sort((a, b) => b.appeal - a.appeal || cmpStr(a.prospect.id, b.prospect.id))[0];
|
|
218
|
+
if (!best)
|
|
219
|
+
return;
|
|
220
|
+
const { prospect, appeal } = best;
|
|
221
|
+
const g = adoptGoal(p, {
|
|
222
|
+
label: prospect.label,
|
|
223
|
+
serves: ['competence', 'leisure'],
|
|
224
|
+
advancedBy: [],
|
|
225
|
+
importance: Math.max(0.3, Math.min(0.7, appeal)),
|
|
226
|
+
temporal: { halfLifeMinutes: setting(s, 'halfLifeMinutes') },
|
|
227
|
+
project: { prospectId: prospect.id, perceivedProgress: prospect.progress !== undefined },
|
|
228
|
+
}, now);
|
|
229
|
+
if (prospect.progress !== undefined)
|
|
230
|
+
perceiveProjectProgress(p, prospect.id, prospect.progress, now);
|
|
231
|
+
s.lastAdoptedAt = now;
|
|
232
|
+
s.currentGoalId = g.id;
|
|
233
|
+
}
|
|
234
|
+
/** Conservative restore: bounded fields, finite past timestamps, and no ambiguous duplicate ids. */
|
|
235
|
+
export function sanitizeProjects(input, now) {
|
|
236
|
+
if (!isObj(input) || !Array.isArray(input.prospects) || !Array.isArray(input.history))
|
|
237
|
+
return undefined;
|
|
238
|
+
const out = { prospects: [], history: [] };
|
|
239
|
+
if (input.spec !== undefined) {
|
|
240
|
+
if (!isObj(input.spec))
|
|
241
|
+
return undefined;
|
|
242
|
+
const spec = cleanSpec(input.spec);
|
|
243
|
+
if (Object.keys(spec).length !== Object.keys(input.spec).length)
|
|
244
|
+
return undefined;
|
|
245
|
+
if (Object.keys(spec).length)
|
|
246
|
+
out.spec = spec;
|
|
247
|
+
}
|
|
248
|
+
for (const key of ['lastAdoptedAt', 'lastAbandonedAt']) {
|
|
249
|
+
if (input[key] === undefined)
|
|
250
|
+
continue;
|
|
251
|
+
if (!minute(input[key], now))
|
|
252
|
+
return undefined;
|
|
253
|
+
out[key] = input[key];
|
|
254
|
+
}
|
|
255
|
+
if (input.currentGoalId !== undefined) {
|
|
256
|
+
if (!str(input.currentGoalId))
|
|
257
|
+
return undefined;
|
|
258
|
+
out.currentGoalId = input.currentGoalId;
|
|
259
|
+
}
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
for (const entry of input.prospects.slice(0, PROJECTS_DEFAULTS.maxProspects)) {
|
|
262
|
+
const prospect = cleanProspect(entry);
|
|
263
|
+
if (!prospect || !isObj(entry) || !minute(entry.lastSeenAt, now))
|
|
264
|
+
continue;
|
|
265
|
+
if (seen.has(prospect.id))
|
|
266
|
+
return undefined;
|
|
267
|
+
seen.add(prospect.id);
|
|
268
|
+
if (now - entry.lastSeenAt <= setting(out, 'forgetMinutes'))
|
|
269
|
+
out.prospects.push({ ...prospect, lastSeenAt: entry.lastSeenAt });
|
|
270
|
+
}
|
|
271
|
+
seen.clear();
|
|
272
|
+
for (const h of input.history.slice(-PROJECTS_DEFAULTS.maxHistory)) {
|
|
273
|
+
if (!isObj(h) ||
|
|
274
|
+
!str(h.prospectId) ||
|
|
275
|
+
!str(h.goalId) ||
|
|
276
|
+
!minute(h.at, now) ||
|
|
277
|
+
!minute(h.adoptedAt, h.at) ||
|
|
278
|
+
(h.outcome !== 'achieved' && h.outcome !== 'abandoned'))
|
|
279
|
+
continue;
|
|
280
|
+
if (seen.has(h.goalId))
|
|
281
|
+
return undefined;
|
|
282
|
+
seen.add(h.goalId);
|
|
283
|
+
out.history.push({
|
|
284
|
+
prospectId: h.prospectId,
|
|
285
|
+
goalId: h.goalId,
|
|
286
|
+
adoptedAt: h.adoptedAt,
|
|
287
|
+
outcome: h.outcome,
|
|
288
|
+
at: h.at,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
/** Sanitize and reconcile agenda ownership deterministically, without RNG. @internal */
|
|
294
|
+
export function restoreProjects(p) {
|
|
295
|
+
const s = sanitizeProjects(p.projects, p.now);
|
|
296
|
+
if (!s) {
|
|
297
|
+
delete p.projects;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
p.projects = s;
|
|
301
|
+
noteProjectStatus(p, p.now);
|
|
302
|
+
const active = p.agenda.goals
|
|
303
|
+
.filter((g) => g.project && g.status === 'active')
|
|
304
|
+
.sort((a, b) => b.adoptedAt - a.adoptedAt || cmpStr(a.id, b.id));
|
|
305
|
+
for (const g of active.slice(1)) {
|
|
306
|
+
abandonGoal(p, g.id);
|
|
307
|
+
recordProjectStatus(p, g, p.now);
|
|
308
|
+
}
|
|
309
|
+
const kept = active[0];
|
|
310
|
+
if (kept)
|
|
311
|
+
s.currentGoalId = kept.id;
|
|
312
|
+
else
|
|
313
|
+
delete s.currentGoalId;
|
|
314
|
+
}
|
|
@@ -88,6 +88,10 @@ function consequence(x, receivedAt) {
|
|
|
88
88
|
s.duration < 0 ||
|
|
89
89
|
s.duration > PROSPECTION_DEFAULTS.horizon ||
|
|
90
90
|
!unit(s.effort) ||
|
|
91
|
+
(s.advances !== undefined &&
|
|
92
|
+
(!Array.isArray(s.advances) ||
|
|
93
|
+
s.advances.length > 8 ||
|
|
94
|
+
!s.advances.every((x) => typeof x === 'string' && x.length > 0 && x.length <= 512))) ||
|
|
91
95
|
(s.focus !== undefined && !unit(s.focus)) ||
|
|
92
96
|
(s.materialCost !== undefined &&
|
|
93
97
|
(!isNum(s.materialCost) || s.materialCost < 0 || s.materialCost > PROSPECTION_DEFAULTS.maxMaterialCost)))
|
|
@@ -110,6 +114,7 @@ function consequence(x, receivedAt) {
|
|
|
110
114
|
...successorAction,
|
|
111
115
|
duration: s.duration,
|
|
112
116
|
effort: s.effort,
|
|
117
|
+
...(s.advances !== undefined ? { advances: [...s.advances] } : {}),
|
|
113
118
|
...(s.focus !== undefined ? { focus: s.focus } : {}),
|
|
114
119
|
...(s.materialCost !== undefined ? { materialCost: s.materialCost } : {}),
|
|
115
120
|
},
|
package/dist/restore.js
CHANGED
|
@@ -21,7 +21,9 @@ import { GIST_DEFAULTS, MEMORY_DEFAULTS, sanitizeGists } from './memory/index.js
|
|
|
21
21
|
import { sanitizeMethods } from './methods/index.js';
|
|
22
22
|
import { migrate } from './migrate.js';
|
|
23
23
|
import { sanitizeBonds } from './partnering/index.js';
|
|
24
|
+
import { sanitizePursuit } from './persistence/index.js';
|
|
24
25
|
import { cleanRetention, createPerson, traceLimit } from './person.js';
|
|
26
|
+
import { restoreProjects } from './projects/index.js';
|
|
25
27
|
import { sanitizePromptings } from './promptings/index.js';
|
|
26
28
|
import { sanitizePlanning, sanitizeProspection } from './prospection/index.js';
|
|
27
29
|
import { sanitizeReview } from './review/index.js';
|
|
@@ -88,6 +90,8 @@ const OPTIONAL_KEYS = new Set([
|
|
|
88
90
|
'planning',
|
|
89
91
|
'promptings',
|
|
90
92
|
'curiosity',
|
|
93
|
+
'projects',
|
|
94
|
+
'pursuit',
|
|
91
95
|
'review',
|
|
92
96
|
]);
|
|
93
97
|
/** Kind of a JSON value for the per-field type check in `restore`. */
|
|
@@ -205,6 +209,8 @@ export function restore(input) {
|
|
|
205
209
|
keepValid(out, 'planning', sanitizePlanning);
|
|
206
210
|
keepValid(out, 'prospection', (x) => sanitizeProspection(x, out.now, out.id, out.planning !== undefined));
|
|
207
211
|
keepValid(out, 'curiosity', (x) => sanitizeCuriosity(x, out.now));
|
|
212
|
+
restoreProjects(out);
|
|
213
|
+
keepValid(out, 'pursuit', (x) => sanitizePursuit(x, out.now));
|
|
208
214
|
keepValid(out, 'promptings', (x) => sanitizePromptings(x, out.now));
|
|
209
215
|
keepValid(out, 'review', (x) => sanitizeReview(x, out.now));
|
|
210
216
|
boundLists(out);
|
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
|
/**
|
|
@@ -586,6 +588,11 @@ export interface Commitment {
|
|
|
586
588
|
recurUntil?: Minute;
|
|
587
589
|
}
|
|
588
590
|
export interface Goal {
|
|
591
|
+
/** Prospect identity; perceived projects receive progress only through perception. */
|
|
592
|
+
project?: {
|
|
593
|
+
prospectId: string;
|
|
594
|
+
perceivedProgress: boolean;
|
|
595
|
+
};
|
|
589
596
|
id: string;
|
|
590
597
|
label: string;
|
|
591
598
|
/** Which need/value the goal serves. */
|
|
@@ -700,6 +707,8 @@ export interface VoiceTrustEvent {
|
|
|
700
707
|
* impulse emerges from competing terms, real fatigue costs, habits and precommitments.
|
|
701
708
|
*/
|
|
702
709
|
export interface WillState {
|
|
710
|
+
/** Opt-in score explanations on suggestion resolutions; absent preserves legacy JSON. */
|
|
711
|
+
suggestionReasons?: true;
|
|
703
712
|
voices: VoiceRelation[];
|
|
704
713
|
/**
|
|
705
714
|
* Precommitments the person made for themselves (e.g. "no work after Isha", "walk instead of eat when bored"):
|
|
@@ -885,7 +894,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
885
894
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
886
895
|
* `dutyReviewAt`). Person shape unchanged.
|
|
887
896
|
*/
|
|
888
|
-
export declare const ENGINE_VERSION = "2.
|
|
897
|
+
export declare const ENGINE_VERSION = "2.14.0";
|
|
889
898
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
890
899
|
export interface Pregnancy {
|
|
891
900
|
fatherId: PersonId;
|
|
@@ -1002,6 +1011,8 @@ export interface ReceivedConsequence {
|
|
|
1002
1011
|
effort: Unit;
|
|
1003
1012
|
focus?: Unit;
|
|
1004
1013
|
materialCost?: number;
|
|
1014
|
+
/** Goal or prospect ids this successor contributes to (bounded to 8). */
|
|
1015
|
+
advances?: string[];
|
|
1005
1016
|
};
|
|
1006
1017
|
availableFrom: Minute;
|
|
1007
1018
|
/** Latest permitted successor completion, inclusive. */
|
|
@@ -1067,6 +1078,10 @@ export interface Person {
|
|
|
1067
1078
|
promptings?: PromptingsState;
|
|
1068
1079
|
/** Intrinsic exploration and varied repetition. Owned by `curiosity/`; absent unless enabled. */
|
|
1069
1080
|
curiosity?: CuriosityState;
|
|
1081
|
+
/** Self-set projects from attended prospects; owned by projects/. */
|
|
1082
|
+
projects?: ProjectsState;
|
|
1083
|
+
/** Keeping a started goal active. Owned by persistence/; absent unless enabled. */
|
|
1084
|
+
pursuit?: PursuitState;
|
|
1070
1085
|
/** Evening self-review and its resolves (2.7.0, opt-in via `enableReview`). Owned by `review/`. */
|
|
1071
1086
|
review?: ReviewState;
|
|
1072
1087
|
/** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
|
|
@@ -1202,6 +1217,8 @@ export interface Affordance {
|
|
|
1202
1217
|
}
|
|
1203
1218
|
/** Something a person perceives. Hosts emit percepts; attention decides which are encoded. */
|
|
1204
1219
|
export interface Percept {
|
|
1220
|
+
/** Host-shown opportunity, retained only with projects enabled and when attended. */
|
|
1221
|
+
prospect?: Prospect;
|
|
1205
1222
|
at: Minute;
|
|
1206
1223
|
/**
|
|
1207
1224
|
* How it reached the person: seen, heard, told (testimony), felt (bodily), the result of an action
|
|
@@ -1374,6 +1391,12 @@ export type SuggestionVerdict = 'assented' | 'complied' | 'deferred' | 'modified
|
|
|
1374
1391
|
/** Refusal class for UI: grey = cannot, amber = notNow, red = willNot. */
|
|
1375
1392
|
export type RefusalKind = 'cannot' | 'notNow' | 'willNot';
|
|
1376
1393
|
export interface SuggestionResolution {
|
|
1394
|
+
/** Read-only score explanation, only with `enableSuggestionReasons`; margin includes activity inertia. */
|
|
1395
|
+
decidedBy?: {
|
|
1396
|
+
kind: 'inertia' | 'term';
|
|
1397
|
+
source?: string;
|
|
1398
|
+
margin: number;
|
|
1399
|
+
};
|
|
1377
1400
|
voiceId: EntityId;
|
|
1378
1401
|
verdict: SuggestionVerdict;
|
|
1379
1402
|
kind?: RefusalKind;
|
|
@@ -1501,6 +1524,8 @@ export interface PersonSpec {
|
|
|
1501
1524
|
* `enableCharacterChange` and `enableSkillRetention` right after `createPerson`. Each is off when absent.
|
|
1502
1525
|
*/
|
|
1503
1526
|
enable?: {
|
|
1527
|
+
/** Read-only suggestion score explanations, as with `enableSuggestionReasons(p)`. */
|
|
1528
|
+
suggestionReasons?: boolean;
|
|
1504
1529
|
gists?: boolean;
|
|
1505
1530
|
yearbook?: boolean;
|
|
1506
1531
|
character?: boolean;
|
|
@@ -1517,6 +1542,10 @@ export interface PersonSpec {
|
|
|
1517
1542
|
planning?: PlanningSpec;
|
|
1518
1543
|
/** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
|
|
1519
1544
|
review?: ReviewSpec;
|
|
1545
|
+
/** Self-set projects from what the person attends to. */
|
|
1546
|
+
projects?: boolean | ProjectsSpec;
|
|
1547
|
+
/** Keep started goal pursuits across decisions. */
|
|
1548
|
+
persistence?: boolean | PersistenceSpec;
|
|
1520
1549
|
};
|
|
1521
1550
|
}
|
|
1522
1551
|
/** A known span of exertion after an act (see `Affordance.followedBy`). */
|
|
@@ -1945,3 +1974,70 @@ export interface CuriosityReading {
|
|
|
1945
1974
|
vary: Unit;
|
|
1946
1975
|
drive: number;
|
|
1947
1976
|
}
|
|
1977
|
+
/** A bounded pursuit opportunity explicitly shown by the host. */
|
|
1978
|
+
export interface Prospect {
|
|
1979
|
+
id: string;
|
|
1980
|
+
label: string;
|
|
1981
|
+
kind: 'make' | 'arrange' | 'gather' | 'finish';
|
|
1982
|
+
placeId?: EntityId;
|
|
1983
|
+
/** Up to 256 accepted; the first eight distinct ids are retained. */
|
|
1984
|
+
materialIds?: EntityId[];
|
|
1985
|
+
progress?: Unit;
|
|
1986
|
+
}
|
|
1987
|
+
export interface ProjectsSpec {
|
|
1988
|
+
forgetMinutes?: number;
|
|
1989
|
+
calmUrgency?: Unit;
|
|
1990
|
+
fearBelow?: Unit;
|
|
1991
|
+
adoptEveryMinutes?: number;
|
|
1992
|
+
cooldownMinutes?: number;
|
|
1993
|
+
adoptAppeal?: Unit;
|
|
1994
|
+
halfLifeMinutes?: number;
|
|
1995
|
+
staleMinutes?: number;
|
|
1996
|
+
}
|
|
1997
|
+
export interface ProjectHistory {
|
|
1998
|
+
prospectId: string;
|
|
1999
|
+
goalId: string;
|
|
2000
|
+
adoptedAt: Minute;
|
|
2001
|
+
outcome: 'achieved' | 'abandoned';
|
|
2002
|
+
at: Minute;
|
|
2003
|
+
}
|
|
2004
|
+
/** Optional bounded JSON state owned by projects/. */
|
|
2005
|
+
export interface ProjectsState {
|
|
2006
|
+
spec?: ProjectsSpec;
|
|
2007
|
+
prospects: (Prospect & {
|
|
2008
|
+
lastSeenAt: Minute;
|
|
2009
|
+
})[];
|
|
2010
|
+
history: ProjectHistory[];
|
|
2011
|
+
lastAdoptedAt?: Minute;
|
|
2012
|
+
lastAbandonedAt?: Minute;
|
|
2013
|
+
/** The adopted goal awaiting its one terminal history entry. */
|
|
2014
|
+
currentGoalId?: string;
|
|
2015
|
+
}
|
|
2016
|
+
/** Bounded engineering overrides for persistence/. Invalid overrides use defaults. */
|
|
2017
|
+
export interface PersistenceSpec {
|
|
2018
|
+
continueScale?: number;
|
|
2019
|
+
switchScale?: number;
|
|
2020
|
+
cap?: number;
|
|
2021
|
+
switchMargin?: number;
|
|
2022
|
+
slackMinutes?: number;
|
|
2023
|
+
maxMinutes?: number;
|
|
2024
|
+
/** Refractory interval for the same goal after maximum-duration release (default 20 minutes). */
|
|
2025
|
+
restMinutes?: number;
|
|
2026
|
+
}
|
|
2027
|
+
/** A started goal retained across decisions. */
|
|
2028
|
+
export interface Pursuit {
|
|
2029
|
+
goalId: string;
|
|
2030
|
+
since: Minute;
|
|
2031
|
+
lastServedAt: Minute;
|
|
2032
|
+
strength: Unit;
|
|
2033
|
+
}
|
|
2034
|
+
/** Optional JSON state owned by persistence/: one current pursuit and one refractory goal. */
|
|
2035
|
+
export interface PursuitState {
|
|
2036
|
+
spec?: PersistenceSpec;
|
|
2037
|
+
current?: Pursuit;
|
|
2038
|
+
/** One goal withheld from pursuit formation until this minute; agenda terms remain unchanged. */
|
|
2039
|
+
rested?: {
|
|
2040
|
+
goalId: string;
|
|
2041
|
+
until: Minute;
|
|
2042
|
+
};
|
|
2043
|
+
}
|
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.14.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.14.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",
|