@adam0white/human-framework 2.13.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 +38 -2
- package/dist/cognition/cognition.js +4 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/migrate.js +6 -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 +46 -2
- 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 +85 -1
- package/dist/types.js +1 -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) &&
|
|
@@ -479,11 +490,16 @@ function heldConviction(p, c) {
|
|
|
479
490
|
* keeps by the same rule, so a pull never leads to an activity that cannot keep the commitment.
|
|
480
491
|
*/
|
|
481
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
|
+
}
|
|
482
498
|
function goalAmount(g, aff) {
|
|
483
499
|
const own = g.advancedBy.find((a) => a.action === aff.action);
|
|
484
500
|
if (own)
|
|
485
501
|
return own.amount;
|
|
486
|
-
return aff.advances
|
|
502
|
+
return namesGoal(g, aff.advances) ? AGENDA_DEFAULTS.defaultAdvanceAmount : 0;
|
|
487
503
|
}
|
|
488
504
|
function deadlinePressure(g, now) {
|
|
489
505
|
if (g.deadline === undefined)
|
|
@@ -671,9 +687,14 @@ export function onFinished(p, outcome, startedAt = outcome.at, opts = {}) {
|
|
|
671
687
|
if (g.status !== 'active')
|
|
672
688
|
continue;
|
|
673
689
|
const own = g.advancedBy.find((a) => a.action === outcome.action)?.amount ?? 0;
|
|
674
|
-
const amount = own > 0 ? own : outcome.advances
|
|
690
|
+
const amount = own > 0 ? own : namesGoal(g, outcome.advances) ? AGENDA_DEFAULTS.defaultAdvanceAmount : 0;
|
|
675
691
|
if (amount <= 0)
|
|
676
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
|
+
}
|
|
677
698
|
g.progress = clamp01(g.progress + amount);
|
|
678
699
|
g.lastAdvancedAt = outcome.at;
|
|
679
700
|
if (g.baseImportance !== undefined)
|
|
@@ -704,6 +725,21 @@ export function adoptGoal(p, g, now) {
|
|
|
704
725
|
p.agenda.goals.push(goal);
|
|
705
726
|
return goal;
|
|
706
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
|
+
}
|
|
707
743
|
export function abandonGoal(p, id) {
|
|
708
744
|
const g = p.agenda.goals.find((x) => x.id === id);
|
|
709
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))
|
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.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
|
|
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,11 @@ 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
|
+
},
|
|
73
78
|
'2.12.0': {
|
|
74
79
|
to: '2.13.0',
|
|
75
80
|
note: 'suggestion reasons (Person.will.suggestionReasons) and firm self-rules (Commitment.firm) are optional and start absent; 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,6 +37,8 @@ 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';
|
|
@@ -162,8 +164,12 @@ export function createPerson(spec) {
|
|
|
162
164
|
enableFatigueStore(p);
|
|
163
165
|
if (on?.fullness)
|
|
164
166
|
enableFullness(p);
|
|
167
|
+
if (on?.projects)
|
|
168
|
+
enableProjects(p, on.projects === true ? {} : on.projects);
|
|
165
169
|
if (on?.curiosity)
|
|
166
170
|
enableCuriosity(p, on.curiosity === true ? {} : on.curiosity);
|
|
171
|
+
if (on?.persistence)
|
|
172
|
+
enablePersistence(p, on.persistence === true ? {} : on.persistence);
|
|
167
173
|
if (on?.planning)
|
|
168
174
|
enablePlanning(p, on.planning);
|
|
169
175
|
if (on?.promptings)
|
|
@@ -367,11 +373,15 @@ function recordKeptAbstentions(p, kept, now) {
|
|
|
367
373
|
* rounding (~1e-9) unless calls also fall on the grid, in which case replay is byte-identical.
|
|
368
374
|
*/
|
|
369
375
|
export function tick(p, now) {
|
|
376
|
+
if (p.pursuit)
|
|
377
|
+
releasePursuit(p);
|
|
370
378
|
if (now <= p.now)
|
|
371
379
|
return;
|
|
372
380
|
if (!p.body.alive) {
|
|
373
381
|
p.now = now;
|
|
374
382
|
pruneConsequences(p);
|
|
383
|
+
if (p.pursuit)
|
|
384
|
+
releasePursuit(p);
|
|
375
385
|
return;
|
|
376
386
|
}
|
|
377
387
|
const seg = PERSON_DEFAULTS.maxSegment;
|
|
@@ -386,15 +396,21 @@ export function tick(p, now) {
|
|
|
386
396
|
if (!(end > p.now)) {
|
|
387
397
|
p.now = now;
|
|
388
398
|
pruneConsequences(p);
|
|
399
|
+
if (p.pursuit)
|
|
400
|
+
releasePursuit(p);
|
|
389
401
|
return;
|
|
390
402
|
}
|
|
391
403
|
advanceSegment(p, end - p.now, load, active && act ? act.affordance : undefined);
|
|
392
404
|
p.now = end;
|
|
393
405
|
pruneConsequences(p);
|
|
406
|
+
if (p.pursuit)
|
|
407
|
+
releasePursuit(p);
|
|
394
408
|
if (!p.body.alive) {
|
|
395
409
|
p.activity = null;
|
|
396
410
|
p.now = now;
|
|
397
411
|
pruneConsequences(p);
|
|
412
|
+
if (p.pursuit)
|
|
413
|
+
releasePursuit(p);
|
|
398
414
|
return;
|
|
399
415
|
}
|
|
400
416
|
// Discrete bookkeeping runs only on grid boundaries, so it happens at the same minutes whatever the
|
|
@@ -454,6 +470,10 @@ export function tick(p, now) {
|
|
|
454
470
|
}
|
|
455
471
|
}
|
|
456
472
|
}
|
|
473
|
+
if (p.projects)
|
|
474
|
+
considerProjects(p, end);
|
|
475
|
+
if (p.pursuit)
|
|
476
|
+
releasePursuit(p);
|
|
457
477
|
}
|
|
458
478
|
}
|
|
459
479
|
/** The composite's side of the hourly crisis check: interrupt and remember an onset, remember a recovery. */
|
|
@@ -546,6 +566,8 @@ export function skip(p, to) {
|
|
|
546
566
|
skipBody(p, to);
|
|
547
567
|
p.now = to;
|
|
548
568
|
pruneConsequences(p);
|
|
569
|
+
if (p.pursuit)
|
|
570
|
+
releasePursuit(p);
|
|
549
571
|
skipAffect(p, to);
|
|
550
572
|
advanceAgenda(p, to);
|
|
551
573
|
// Lasting gists (1.8.0, opt-in): episodes now past the horizon fold into gists.
|
|
@@ -565,6 +587,8 @@ function socialKindOf(kind) {
|
|
|
565
587
|
}
|
|
566
588
|
function perceiveOne(p, pc) {
|
|
567
589
|
receiveConsequences(p, pc);
|
|
590
|
+
if (p.projects && pc.prospect)
|
|
591
|
+
perceiveProspect(p, pc.prospect, pc.at);
|
|
568
592
|
const at = Math.min(Math.max(pc.at, p.affect.lastUpdated), p.now);
|
|
569
593
|
const actor = pc.actorId;
|
|
570
594
|
const target = pc.targetId;
|
|
@@ -1021,6 +1045,20 @@ export function begin(p, aff, record, opts = {}) {
|
|
|
1021
1045
|
if (!p.body.alive)
|
|
1022
1046
|
return null;
|
|
1023
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
|
+
}
|
|
1024
1062
|
const duration = Number.isFinite(aff.duration) ? Math.max(1, Math.round(aff.duration)) : 1;
|
|
1025
1063
|
const mode = aff.mode ?? 'awake';
|
|
1026
1064
|
const load = { effort: clamp01(aff.effort), focus: clamp01(aff.focus ?? 0), mode };
|
|
@@ -1295,6 +1333,8 @@ function learnActivityVoices(p, act, after, now, felt, broken, breached) {
|
|
|
1295
1333
|
* relationships, trust in the advising voice, injuries and illness. Clears the activity.
|
|
1296
1334
|
*/
|
|
1297
1335
|
export function finish(p, outcome, opts = {}) {
|
|
1336
|
+
if (p.pursuit)
|
|
1337
|
+
releasePursuit(p);
|
|
1298
1338
|
const act = p.activity;
|
|
1299
1339
|
if (!act)
|
|
1300
1340
|
return null;
|
|
@@ -1409,6 +1449,10 @@ export function finish(p, outcome, opts = {}) {
|
|
|
1409
1449
|
...(act.necessity ? { necessity: true } : {}),
|
|
1410
1450
|
...(opts.catalog ? { catalog: opts.catalog } : {}),
|
|
1411
1451
|
});
|
|
1452
|
+
if (p.projects)
|
|
1453
|
+
noteProjectStatus(p, now);
|
|
1454
|
+
if (p.pursuit)
|
|
1455
|
+
releasePursuit(p);
|
|
1412
1456
|
// A completed violating action inside an open abstention's window broke it (the fast, e.g. "no work after the evening prayer").
|
|
1413
1457
|
if (broken.length > 0) {
|
|
1414
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
|
@@ -588,6 +588,11 @@ export interface Commitment {
|
|
|
588
588
|
recurUntil?: Minute;
|
|
589
589
|
}
|
|
590
590
|
export interface Goal {
|
|
591
|
+
/** Prospect identity; perceived projects receive progress only through perception. */
|
|
592
|
+
project?: {
|
|
593
|
+
prospectId: string;
|
|
594
|
+
perceivedProgress: boolean;
|
|
595
|
+
};
|
|
591
596
|
id: string;
|
|
592
597
|
label: string;
|
|
593
598
|
/** Which need/value the goal serves. */
|
|
@@ -889,7 +894,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
889
894
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
890
895
|
* `dutyReviewAt`). Person shape unchanged.
|
|
891
896
|
*/
|
|
892
|
-
export declare const ENGINE_VERSION = "2.
|
|
897
|
+
export declare const ENGINE_VERSION = "2.14.0";
|
|
893
898
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
894
899
|
export interface Pregnancy {
|
|
895
900
|
fatherId: PersonId;
|
|
@@ -1006,6 +1011,8 @@ export interface ReceivedConsequence {
|
|
|
1006
1011
|
effort: Unit;
|
|
1007
1012
|
focus?: Unit;
|
|
1008
1013
|
materialCost?: number;
|
|
1014
|
+
/** Goal or prospect ids this successor contributes to (bounded to 8). */
|
|
1015
|
+
advances?: string[];
|
|
1009
1016
|
};
|
|
1010
1017
|
availableFrom: Minute;
|
|
1011
1018
|
/** Latest permitted successor completion, inclusive. */
|
|
@@ -1071,6 +1078,10 @@ export interface Person {
|
|
|
1071
1078
|
promptings?: PromptingsState;
|
|
1072
1079
|
/** Intrinsic exploration and varied repetition. Owned by `curiosity/`; absent unless enabled. */
|
|
1073
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;
|
|
1074
1085
|
/** Evening self-review and its resolves (2.7.0, opt-in via `enableReview`). Owned by `review/`. */
|
|
1075
1086
|
review?: ReviewState;
|
|
1076
1087
|
/** Practice consolidation (1.8.0, opt-in via `enableSkillRetention`). Owned by `skills/`. */
|
|
@@ -1206,6 +1217,8 @@ export interface Affordance {
|
|
|
1206
1217
|
}
|
|
1207
1218
|
/** Something a person perceives. Hosts emit percepts; attention decides which are encoded. */
|
|
1208
1219
|
export interface Percept {
|
|
1220
|
+
/** Host-shown opportunity, retained only with projects enabled and when attended. */
|
|
1221
|
+
prospect?: Prospect;
|
|
1209
1222
|
at: Minute;
|
|
1210
1223
|
/**
|
|
1211
1224
|
* How it reached the person: seen, heard, told (testimony), felt (bodily), the result of an action
|
|
@@ -1529,6 +1542,10 @@ export interface PersonSpec {
|
|
|
1529
1542
|
planning?: PlanningSpec;
|
|
1530
1543
|
/** Evening self-review (2.7.0): the same as `enableReview(p, spec)` after creation. */
|
|
1531
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;
|
|
1532
1549
|
};
|
|
1533
1550
|
}
|
|
1534
1551
|
/** A known span of exertion after an act (see `Affordance.followedBy`). */
|
|
@@ -1957,3 +1974,70 @@ export interface CuriosityReading {
|
|
|
1957
1974
|
vary: Unit;
|
|
1958
1975
|
drive: number;
|
|
1959
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/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",
|