@mindot/will 0.1.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/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/faculties/goal.manager.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GoalManager — maintains the active goal hierarchy.
|
|
7
|
+
*
|
|
8
|
+
* Manages:
|
|
9
|
+
* - Goal activation and deactivation based on drives and context
|
|
10
|
+
* - Goal priority ordering (conflict resolution)
|
|
11
|
+
* - Progress tracking
|
|
12
|
+
* - Goal completion and abandonment
|
|
13
|
+
* - Sub-goal decomposition
|
|
14
|
+
*
|
|
15
|
+
* Goals are entities in the state manager. The GoalManager reads
|
|
16
|
+
* drive signals from regulatory engines and perceptual context to
|
|
17
|
+
* determine which goals should be active, then updates their
|
|
18
|
+
* priorities and tracks progress.
|
|
19
|
+
*
|
|
20
|
+
* Part of Shard 3 (Executive Layer) — runs every tick, synchronous.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
Duration,
|
|
25
|
+
Tick,
|
|
26
|
+
SimulationContext,
|
|
27
|
+
ReadonlySimulationState,
|
|
28
|
+
StateCommands,
|
|
29
|
+
SimulationEvent,
|
|
30
|
+
} from '#core/types'
|
|
31
|
+
import type { SimulationEngine, EngineResult, CognitiveEngine } from '#cognition/types'
|
|
32
|
+
import type { CognitiveEventSchema } from '#cognition/schema.registry'
|
|
33
|
+
import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
|
|
34
|
+
import type { SessionLogger } from '#stem/tracts/session.logger'
|
|
35
|
+
import { readEffectiveParams } from '#cognition/persona.prior'
|
|
36
|
+
import { GenerativeModel } from '#cognition/generative.model'
|
|
37
|
+
|
|
38
|
+
export interface GoalManagerConfig {
|
|
39
|
+
/** Maximum active goals at once */
|
|
40
|
+
maxActiveGoals?: number
|
|
41
|
+
/** How quickly goal priority decays without reinforcement */
|
|
42
|
+
priorityDecayRate?: number
|
|
43
|
+
/** Minimum priority before a goal is deactivated */
|
|
44
|
+
deactivationThreshold?: number
|
|
45
|
+
/**
|
|
46
|
+
* Resilience / grit. A goal whose priority ≥ gritPriority is NEVER auto-abandoned
|
|
47
|
+
* by staleness — the mind only lets it go by a deliberate executive decision.
|
|
48
|
+
* Below that, the staleness patience window scales with priority by
|
|
49
|
+
* gritPatienceScale (important stuck goals are pursued much longer).
|
|
50
|
+
*/
|
|
51
|
+
gritPriority?: number
|
|
52
|
+
gritPatienceScale?: number
|
|
53
|
+
/** Frustration tolerance [0–1]: how much the frustration emotion is allowed to
|
|
54
|
+
* compress the patience window. High tolerance → setbacks don't make the mind
|
|
55
|
+
* give up faster. A personality dimension (seeded by PMA, developed by metacog). */
|
|
56
|
+
frustrationTolerance?: number
|
|
57
|
+
/**
|
|
58
|
+
* How many new beliefs must form after a goal activates for it to be
|
|
59
|
+
* considered complete. Applies only to epistemic (non-drive) goals.
|
|
60
|
+
*/
|
|
61
|
+
epistemicBeliefThreshold?: number
|
|
62
|
+
bus?: CognitiveBus
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Drive-related tags — used to recognise goals spawned by drive signals. */
|
|
66
|
+
const DRIVE_TAGS = new Set([ 'energy', 'sleep', 'stress', 'survival', 'wellbeing', 'external', 'demand' ])
|
|
67
|
+
|
|
68
|
+
// Task-persistence commitment (Channel A). The goal task.switcher is focused on gets a
|
|
69
|
+
// bounded priority boost that BUILDS the longer it's been focused, is AMPLIFIED by sunk
|
|
70
|
+
// cost in an in-progress plan (planning.engine), and SCALES with the switch cost (= the
|
|
71
|
+
// conscientiousness-developable baseSwitchCost, #28). Recomputed fresh each tick from
|
|
72
|
+
// basePriority — never accumulates. This is what makes the focus mechanically "stick":
|
|
73
|
+
// the focused goal stays top for goal selection, the executive, and planning.
|
|
74
|
+
const FOCUS_COMMITMENT_RAMP = 30 // ticks of focus to reach full commitment weight
|
|
75
|
+
const COMMITMENT_GAIN = 0.3 // scales switchCost × commitment × plan-sunk-cost
|
|
76
|
+
const MAX_COMMITMENT_BOOST = 0.2 // cap — a clearly higher-priority goal still wins
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
export interface GoalState {
|
|
80
|
+
id: string
|
|
81
|
+
description: string
|
|
82
|
+
priority: number // 0-1: current priority
|
|
83
|
+
basePriority: number // 0-1: priority from drives (stable)
|
|
84
|
+
progress: number // 0-1
|
|
85
|
+
status: 'active' | 'blocked' | 'completed' | 'abandoned' | 'pending' | 'pending_verification'
|
|
86
|
+
parentGoalId?: string
|
|
87
|
+
subGoals: string[]
|
|
88
|
+
activatedAt: Tick
|
|
89
|
+
deadline?: Tick
|
|
90
|
+
tags: string[]
|
|
91
|
+
/** Snapshot of memory.beliefs_total when this goal was activated.
|
|
92
|
+
* Used to compute epistemic progress: (currentBeliefs - baseline) / threshold. */
|
|
93
|
+
beliefsAtActivation: number
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* How this goal knows it is done — set at creation time, not inferred from tags.
|
|
97
|
+
*
|
|
98
|
+
* 'metric' — a measurable state crosses a threshold (completionCondition).
|
|
99
|
+
* 'action' — a real-world outcome must occur; stays 0% if impossible and
|
|
100
|
+
* is abandoned through frustration rather than belief formation.
|
|
101
|
+
* 'epistemic' — resolved through understanding: belief formation about the
|
|
102
|
+
* situation, the self, or the world.
|
|
103
|
+
*/
|
|
104
|
+
completionType: 'metric' | 'action' | 'epistemic' | 'pending_verification'
|
|
105
|
+
|
|
106
|
+
/** For 'metric' goals: e.g. "stress.load < 40" or "energy.level > 80".
|
|
107
|
+
* Parsed and evaluated each tick to compute smooth progress. */
|
|
108
|
+
completionCondition?: string
|
|
109
|
+
|
|
110
|
+
/** Tick of the most recent action.outcome that matched this goal's tags.
|
|
111
|
+
* Lets the executive see "I tried this N ticks ago" without scanning decision.records. */
|
|
112
|
+
lastActionAttemptTick?: number
|
|
113
|
+
/** ActionType of the most recent attempt (e.g. 'text', 'talk', 'observe'). */
|
|
114
|
+
lastActionType?: string
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Causal link back to the entity whose message triggered this goal.
|
|
118
|
+
* Populated when a conversation escalation creates the goal (AuditionEngine → executive).
|
|
119
|
+
* Used by PlanningEngine to tag plan bus events so the activity SSE stream can
|
|
120
|
+
* filter and forward them to the correct requesting entity.
|
|
121
|
+
*/
|
|
122
|
+
requestingEntityId?: string
|
|
123
|
+
/** Thread ID of the conversation turn that triggered this goal (for reply correlation). */
|
|
124
|
+
requestingThreadId?: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class GoalManager implements SimulationEngine, CognitiveEngine {
|
|
128
|
+
readonly name = 'goal-manager'
|
|
129
|
+
|
|
130
|
+
private _maxActiveGoals: number
|
|
131
|
+
private _priorityDecayRate: number
|
|
132
|
+
private _basePriorityDecayRate: number
|
|
133
|
+
private _deactivationThreshold: number
|
|
134
|
+
private _gritPriority: number
|
|
135
|
+
private _gritPatienceScale: number
|
|
136
|
+
private _frustrationTolerance: number
|
|
137
|
+
private _epistemicBeliefThreshold: number
|
|
138
|
+
|
|
139
|
+
private _goals = new Map<string, GoalState>()
|
|
140
|
+
private _goalCounter = 0
|
|
141
|
+
/** Updated every tick — used as the baseline for newly created goals. */
|
|
142
|
+
private _currentBeliefCount = 0
|
|
143
|
+
/** IDs of goals for which goal.achieved has already been published, so we
|
|
144
|
+
* don't re-fire the event on every subsequent tick. */
|
|
145
|
+
private _achievedGoalIds = new Set<string>()
|
|
146
|
+
/** IDs of goals for which goal.completed has already been emitted. Same
|
|
147
|
+
* once-per-goal guard as _achievedGoalIds — completedGoals accumulates across
|
|
148
|
+
* ticks, so without this the event re-fires every tick for every completed
|
|
149
|
+
* goal (the goal-completion churn). */
|
|
150
|
+
private _completedEmittedIds = new Set<string>()
|
|
151
|
+
|
|
152
|
+
// ── Blocked-goal detection ─────────────────────────────────
|
|
153
|
+
private _goalLastProgress = new Map<string, number>() // goalId → last progress
|
|
154
|
+
private _goalStuckSince = new Map<string, Tick>() // goalId → tick stuck began
|
|
155
|
+
|
|
156
|
+
private _energyLevel: number = 100
|
|
157
|
+
private _sleepPressure: number = 0
|
|
158
|
+
private _stressLoad: number = 0
|
|
159
|
+
private _executiveGoalConfidence: number = 0.5
|
|
160
|
+
|
|
161
|
+
/** Tracks current simulation tick so addGoal() can stamp activatedAt correctly. */
|
|
162
|
+
private _currentTick: Tick = 0
|
|
163
|
+
|
|
164
|
+
private _bus: CognitiveBus | null = null
|
|
165
|
+
private _sessionLogger: SessionLogger | null = null
|
|
166
|
+
|
|
167
|
+
private readonly _model = new GenerativeModel()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
attachSessionLogger( logger: SessionLogger | null ): void {
|
|
171
|
+
this._sessionLogger = logger
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
constructor( config: GoalManagerConfig = {} ){
|
|
175
|
+
this._bus = config.bus ?? null
|
|
176
|
+
this._maxActiveGoals = config.maxActiveGoals ?? 5
|
|
177
|
+
this._priorityDecayRate = config.priorityDecayRate ?? 0.005
|
|
178
|
+
this._basePriorityDecayRate = this._priorityDecayRate
|
|
179
|
+
this._deactivationThreshold = config.deactivationThreshold ?? 0.1
|
|
180
|
+
this._gritPriority = config.gritPriority ?? 0.8
|
|
181
|
+
this._gritPatienceScale = config.gritPatienceScale ?? 2
|
|
182
|
+
this._frustrationTolerance = config.frustrationTolerance ?? 0.5
|
|
183
|
+
this._epistemicBeliefThreshold = config.epistemicBeliefThreshold ?? 8
|
|
184
|
+
}
|
|
185
|
+
attachBus( bus: CognitiveBus ): void { this._bus = bus }
|
|
186
|
+
|
|
187
|
+
// ── Engine interface ─────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
private readonly _STUCK_THRESHOLD = 20 // ticks without progress before "blocked"
|
|
190
|
+
|
|
191
|
+
subscribes(): string[] {
|
|
192
|
+
return [
|
|
193
|
+
'energy.state.changed',
|
|
194
|
+
'sleep.state.changed',
|
|
195
|
+
'stress.state.changed',
|
|
196
|
+
'belief.updated',
|
|
197
|
+
'executive.goal.proposed',
|
|
198
|
+
'executive.prediction.formed',
|
|
199
|
+
'executive.facet.progress',
|
|
200
|
+
'plan.completed',
|
|
201
|
+
'action.outcome', // 4.1: advance action-type goals when matching outcomes fire
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
publishes(): CognitiveEventSchema[] {
|
|
205
|
+
return [
|
|
206
|
+
{ type: 'goal.blocked', version: 1, validate: () => null },
|
|
207
|
+
{ type: 'goal.achieved', version: 1, validate: () => null },
|
|
208
|
+
{ type: 'goal.abandoned', version: 1, validate: () => null },
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
onCognitiveEvent( e: CognitiveEvent ): StateCommands | void {
|
|
213
|
+
this._model.observe( e.type, e.salience )
|
|
214
|
+
switch( e.type ){
|
|
215
|
+
case 'energy.state.changed':
|
|
216
|
+
this._energyLevel = (e.payload as Record<string,number>)['level'] ?? this._energyLevel
|
|
217
|
+
break
|
|
218
|
+
case 'sleep.state.changed':
|
|
219
|
+
this._sleepPressure = (e.payload as Record<string,number>)['pressure'] ?? this._sleepPressure
|
|
220
|
+
break
|
|
221
|
+
case 'stress.state.changed':
|
|
222
|
+
this._stressLoad = (e.payload as Record<string,number>)['load'] ?? this._stressLoad
|
|
223
|
+
break
|
|
224
|
+
case 'belief.updated':
|
|
225
|
+
// Salience model only — authoritative count always read from state.metrics.
|
|
226
|
+
this._model.observe( 'belief.count', ( e.payload as Record<string,number> )['total'] ?? 0 )
|
|
227
|
+
break
|
|
228
|
+
case 'executive.goal.proposed':
|
|
229
|
+
// Track that executive proposed goals — prioritize executive-sourced goals slightly higher
|
|
230
|
+
this._executiveGoalConfidence = (e.payload as Record<string,number>)['confidence'] ?? 0.5
|
|
231
|
+
break
|
|
232
|
+
case 'executive.prediction.formed': {
|
|
233
|
+
const p = e.payload as { predictedDomains: string[]; confidence: number }
|
|
234
|
+
if (p.predictedDomains.includes('executive'))
|
|
235
|
+
this._model.setPrecision('goal.active_count', 1.0 + p.confidence * 0.5)
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
case 'executive.facet.progress': {
|
|
239
|
+
const payload = e.payload as {
|
|
240
|
+
facetId?: string
|
|
241
|
+
planId?: string
|
|
242
|
+
goalId?: string
|
|
243
|
+
goalProgress?: number
|
|
244
|
+
newGoals?: Array<{
|
|
245
|
+
description: string; priority: number; tags: string[]
|
|
246
|
+
completionType: string; completionCondition?: string
|
|
247
|
+
}>
|
|
248
|
+
goalsToAbandon?: Array<{ goalId: string; reason: string }>
|
|
249
|
+
newBeliefs?: Array<{
|
|
250
|
+
statement: string; category: string; confidence: number
|
|
251
|
+
evidence: 'single_observation' | 'recurring_pattern' | 'strong_pattern'; tags: string[]
|
|
252
|
+
}>
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Forward new goals to GoalManager
|
|
256
|
+
if( payload.newGoals )
|
|
257
|
+
for( const goal of payload.newGoals )
|
|
258
|
+
this.addGoal(
|
|
259
|
+
goal.description,
|
|
260
|
+
goal.priority,
|
|
261
|
+
goal.tags,
|
|
262
|
+
undefined,
|
|
263
|
+
undefined,
|
|
264
|
+
goal.completionType as 'action',
|
|
265
|
+
goal.completionCondition
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
// Forward goal abandonments
|
|
269
|
+
if( payload.goalsToAbandon )
|
|
270
|
+
for( const ga of payload.goalsToAbandon )
|
|
271
|
+
this.abandonGoal( ga.goalId, ga.reason )
|
|
272
|
+
|
|
273
|
+
if( payload.goalId && payload.goalProgress !== undefined ){
|
|
274
|
+
const goal = this._goals.get( payload.goalId )
|
|
275
|
+
if( goal?.status === 'active' ){
|
|
276
|
+
goal.progress = Math.max( goal.progress, payload.goalProgress ) // forward-only
|
|
277
|
+
if( goal.progress >= 0.95 )
|
|
278
|
+
goal.status = 'pending_verification'
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
case 'plan.completed': {
|
|
285
|
+
const
|
|
286
|
+
p = e.payload as { goalId: string },
|
|
287
|
+
goal = this._goals.get( p.goalId )
|
|
288
|
+
|
|
289
|
+
if( goal?.status === 'active' )
|
|
290
|
+
goal.status = 'pending_verification' // queued for condition eval next tick
|
|
291
|
+
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
case 'action.outcome': {
|
|
295
|
+
// 4.1: advance action-type goals when a real outcome matches their domain/tags.
|
|
296
|
+
// Epistemic/metric goals are advanced by their own mechanisms (_updateProgress).
|
|
297
|
+
const p = e.payload as { actionType: string; domain: string; outcomeQuality: number }
|
|
298
|
+
this._nudgeActionGoals( p.domain, p.actionType, p.outcomeQuality )
|
|
299
|
+
break
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
snapshot(): Record<string, unknown> {
|
|
305
|
+
return {
|
|
306
|
+
energyLevel: this._energyLevel,
|
|
307
|
+
sleepPressure: this._sleepPressure,
|
|
308
|
+
stressLoad: this._stressLoad,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async react(
|
|
313
|
+
_delta: Duration,
|
|
314
|
+
tick: Tick,
|
|
315
|
+
state: ReadonlySimulationState,
|
|
316
|
+
context: SimulationContext
|
|
317
|
+
): Promise<EngineResult> {
|
|
318
|
+
const
|
|
319
|
+
events: Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> = [],
|
|
320
|
+
commands: StateCommands = { set: [], delete: [], metrics: [] }
|
|
321
|
+
|
|
322
|
+
// Refresh personality dispositions from the engine-config mirror (base ⊕
|
|
323
|
+
// persona-prior) so grit/persistence reflect both the PMA seed and metacognitive
|
|
324
|
+
// self-tuning over time — not hardcoded constants.
|
|
325
|
+
this._readConfigFromState( state )
|
|
326
|
+
|
|
327
|
+
// 0a. Snapshot current tick and belief count.
|
|
328
|
+
// Belief count is read directly from state.metrics — the only reliable source.
|
|
329
|
+
// The bus event (memory.state.changed) used to carry this but the payload field
|
|
330
|
+
// never matched; we now read the metric directly to avoid the stale-zero problem.
|
|
331
|
+
this._currentTick = tick
|
|
332
|
+
this._currentBeliefCount = state.metrics.get('memory.beliefs_total') ?? 0
|
|
333
|
+
|
|
334
|
+
// 0b. Sync goals seeded into state (e.g. bootstrap goals or LLM-generated goals)
|
|
335
|
+
this._syncFromStateGoals( state, tick )
|
|
336
|
+
|
|
337
|
+
// 1. Scan drives for new goal activation
|
|
338
|
+
this._activateFromDrives( state, tick )
|
|
339
|
+
|
|
340
|
+
// 2. Scan percepts for externally triggered goals
|
|
341
|
+
this._activateFromPercepts( state, tick )
|
|
342
|
+
|
|
343
|
+
// 3. Update priorities based on drive intensity and context
|
|
344
|
+
this._updatePriorities( state )
|
|
345
|
+
|
|
346
|
+
// 4. Check progress from state changes
|
|
347
|
+
this._updateProgress( state, tick )
|
|
348
|
+
|
|
349
|
+
// 5. Resolve goal conflicts (highest priority wins)
|
|
350
|
+
this._resolveConflicts()
|
|
351
|
+
|
|
352
|
+
// 6. Deactivate goals below threshold
|
|
353
|
+
this._deactivateStale( tick, state )
|
|
354
|
+
|
|
355
|
+
// 7. Persist goals as entities
|
|
356
|
+
this._persistGoals( commands, tick )
|
|
357
|
+
|
|
358
|
+
// 8. Metrics
|
|
359
|
+
const
|
|
360
|
+
activeGoals = Array.from( this._goals.values() ).filter( g => g.status === 'active'),
|
|
361
|
+
completedGoals = Array.from( this._goals.values() ).filter( g => g.status === 'completed')
|
|
362
|
+
|
|
363
|
+
const avgProgress = activeGoals.length > 0
|
|
364
|
+
? activeGoals.reduce( ( s, g ) => s + g.progress, 0 ) / activeGoals.length
|
|
365
|
+
: 0
|
|
366
|
+
|
|
367
|
+
commands.metrics!.push(
|
|
368
|
+
[ 'goals.active', activeGoals.length ],
|
|
369
|
+
[ 'goals.completed_total', completedGoals.length ],
|
|
370
|
+
[ 'goals.total', this._goals.size ],
|
|
371
|
+
[ 'goals.top_priority', activeGoals[0]?.priority ?? 0 ],
|
|
372
|
+
[ 'goals.avg_progress', avgProgress ],
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
// Goal completion events — once per goal, on the completion transition.
|
|
376
|
+
// completedGoals accumulates across ticks; the _completedEmittedIds guard
|
|
377
|
+
// stops this re-firing every tick for every completed goal.
|
|
378
|
+
for( const goal of completedGoals )
|
|
379
|
+
if( goal.progress >= 1 && !this._completedEmittedIds.has( goal.id ) ){
|
|
380
|
+
this._completedEmittedIds.add( goal.id )
|
|
381
|
+
events.push({
|
|
382
|
+
type: 'goal.completed',
|
|
383
|
+
source: this.name,
|
|
384
|
+
payload: { goalId: goal.id, description: goal.description },
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Phase C: publish cognitive event
|
|
389
|
+
const _bus = this._bus
|
|
390
|
+
if( _bus && activeGoals.length >= 0 )
|
|
391
|
+
_bus.publish({
|
|
392
|
+
type: 'goal.state.changed',
|
|
393
|
+
version: 1,
|
|
394
|
+
sourceEngine: this.name,
|
|
395
|
+
salience: Math.max( 0.4, this._model.observe( 'goal.active_count', activeGoals.length ).salience ),
|
|
396
|
+
payload: { activeCount: activeGoals.length }
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// Publish goal.achieved once per newly completed goal.
|
|
400
|
+
// Uses _achievedGoalIds to ensure the event fires exactly once per goal
|
|
401
|
+
// (completedGoals accumulates across ticks and would otherwise re-fire every tick).
|
|
402
|
+
if( _bus )
|
|
403
|
+
for( const goal of completedGoals )
|
|
404
|
+
if( !this._achievedGoalIds.has( goal.id ) ){
|
|
405
|
+
this._achievedGoalIds.add( goal.id )
|
|
406
|
+
const timeToComplete = tick - goal.activatedAt
|
|
407
|
+
|
|
408
|
+
_bus.publish({
|
|
409
|
+
type: 'goal.achieved', version: 1, sourceEngine: this.name,
|
|
410
|
+
salience: Math.min( 1, 0.5 + goal.priority * 0.5 ),
|
|
411
|
+
payload: { goalId: goal.id, description: goal.description, priority: goal.priority, timeToComplete },
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Detect newly-blocked goals and fire one event per goal on the transition tick
|
|
416
|
+
_bus && this._detectBlockedGoals( tick, activeGoals, _bus )
|
|
417
|
+
|
|
418
|
+
return { events: events.length > 0 ? events : undefined, commands }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private _detectBlockedGoals( tick: Tick, activeGoals: GoalState[], bus: CognitiveBus ): void {
|
|
422
|
+
for( const goal of activeGoals ){
|
|
423
|
+
const lastProgress = this._goalLastProgress.get( goal.id )
|
|
424
|
+
|
|
425
|
+
if( lastProgress !== undefined && Math.abs( goal.progress - lastProgress ) < 0.001 ){
|
|
426
|
+
// No progress change — increment stuck counter
|
|
427
|
+
if( !this._goalStuckSince.has( goal.id ) )
|
|
428
|
+
this._goalStuckSince.set( goal.id, tick )
|
|
429
|
+
|
|
430
|
+
const
|
|
431
|
+
stuckSince = this._goalStuckSince.get( goal.id )!,
|
|
432
|
+
ticksStuck = tick - stuckSince
|
|
433
|
+
|
|
434
|
+
// Fire on every _STUCK_THRESHOLD crossing so subscribers get
|
|
435
|
+
// escalating ticksStuck values (20, 40, 60...) and can detect resolution
|
|
436
|
+
// when events stop arriving.
|
|
437
|
+
if( ticksStuck > 0 && ticksStuck % this._STUCK_THRESHOLD === 0 )
|
|
438
|
+
bus.publish({
|
|
439
|
+
type: 'goal.blocked', version: 1, sourceEngine: this.name,
|
|
440
|
+
salience: Math.min( 1, 0.5 + goal.priority * 0.5 ),
|
|
441
|
+
payload: { goalId: goal.id, ticksStuck, priority: goal.priority },
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
// Progress moved — clear stuck state and log significant shifts
|
|
445
|
+
else {
|
|
446
|
+
this._goalStuckSince.delete( goal.id )
|
|
447
|
+
if( lastProgress !== undefined && Math.abs( goal.progress - lastProgress ) >= 0.05 ){
|
|
448
|
+
this._sessionLogger?.write({
|
|
449
|
+
type: 'goal.progress',
|
|
450
|
+
tick: tick as unknown as number,
|
|
451
|
+
goalId: goal.id,
|
|
452
|
+
description: goal.description,
|
|
453
|
+
previousProgress: lastProgress,
|
|
454
|
+
newProgress: goal.progress,
|
|
455
|
+
delta: goal.progress - lastProgress,
|
|
456
|
+
} as any)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this._goalLastProgress.set( goal.id, goal.progress )
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Clean up trackers for goals that are no longer active
|
|
464
|
+
for( const id of this._goalStuckSince.keys() )
|
|
465
|
+
if( !activeGoals.find( g => g.id === id ) ){
|
|
466
|
+
this._goalStuckSince.delete( id )
|
|
467
|
+
this._goalLastProgress.delete( id )
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── Public API ───────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Add a goal to the manager.
|
|
475
|
+
*/
|
|
476
|
+
addGoal(
|
|
477
|
+
description: string,
|
|
478
|
+
basePriority: number,
|
|
479
|
+
tags: string[] = [],
|
|
480
|
+
parentGoalId?: string,
|
|
481
|
+
deadline?: Tick,
|
|
482
|
+
completionType: GoalState['completionType'] = 'epistemic',
|
|
483
|
+
completionCondition?: string,
|
|
484
|
+
id?: string,
|
|
485
|
+
requestingEntityId?: string,
|
|
486
|
+
requestingThreadId?: string,
|
|
487
|
+
): string {
|
|
488
|
+
this._goalCounter++
|
|
489
|
+
const goalId = id ?? `goal-${this._goalCounter}`
|
|
490
|
+
|
|
491
|
+
this._goals.set( goalId, {
|
|
492
|
+
id: goalId,
|
|
493
|
+
description,
|
|
494
|
+
priority: basePriority,
|
|
495
|
+
basePriority,
|
|
496
|
+
progress: 0,
|
|
497
|
+
status: 'active',
|
|
498
|
+
parentGoalId,
|
|
499
|
+
subGoals: [],
|
|
500
|
+
activatedAt: this._currentTick,
|
|
501
|
+
deadline,
|
|
502
|
+
tags,
|
|
503
|
+
beliefsAtActivation: this._currentBeliefCount,
|
|
504
|
+
completionType,
|
|
505
|
+
completionCondition,
|
|
506
|
+
requestingEntityId,
|
|
507
|
+
requestingThreadId
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
if( parentGoalId ){
|
|
511
|
+
const parent = this._goals.get( parentGoalId )
|
|
512
|
+
if( parent ) parent.subGoals.push( goalId )
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return goalId
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Get all active goals sorted by priority.
|
|
520
|
+
*/
|
|
521
|
+
getActiveGoals(): GoalState[] {
|
|
522
|
+
return Array.from( this._goals.values() )
|
|
523
|
+
.filter( g => g.status === 'active')
|
|
524
|
+
.sort( ( a, b ) => b.priority - a.priority )
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
getGoal( id: string ): GoalState | undefined {
|
|
528
|
+
return this._goals.get( id )
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Mark a goal as completed.
|
|
533
|
+
*/
|
|
534
|
+
completeGoal( goalId: string ): void {
|
|
535
|
+
const goal = this._goals.get( goalId )
|
|
536
|
+
if( goal ){
|
|
537
|
+
goal.status = 'completed'
|
|
538
|
+
goal.progress = 1
|
|
539
|
+
|
|
540
|
+
this._sessionLogger?.write({
|
|
541
|
+
type: 'goal.achieved',
|
|
542
|
+
tick: this._currentTick as unknown as number,
|
|
543
|
+
goalId: goal.id,
|
|
544
|
+
description: goal.description,
|
|
545
|
+
priority: goal.priority,
|
|
546
|
+
activatedAt: goal.activatedAt as unknown as number,
|
|
547
|
+
age: ( this._currentTick as unknown as number ) - ( goal.activatedAt as unknown as number ),
|
|
548
|
+
} as any)
|
|
549
|
+
|
|
550
|
+
// Cascade completion to parent
|
|
551
|
+
if( goal.parentGoalId ){
|
|
552
|
+
const parent = this._goals.get( goal.parentGoalId )
|
|
553
|
+
if( parent && parent.subGoals.every( sgId => this._goals.get( sgId )?.status === 'completed') ){
|
|
554
|
+
parent.progress = 1
|
|
555
|
+
parent.status = 'completed'
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Abandon a goal with an optional reason.
|
|
563
|
+
*/
|
|
564
|
+
abandonGoal( goalId: string, reason?: string ): void {
|
|
565
|
+
const goal = this._goals.get( goalId )
|
|
566
|
+
if( goal && goal.status === 'active' ){
|
|
567
|
+
goal.status = 'abandoned'
|
|
568
|
+
// Optionally store the reason (if GoalState had a reason field; we can add it or store in tags)
|
|
569
|
+
reason && goal.tags.push(`abandoned:${reason.slice(0,50)}`)
|
|
570
|
+
|
|
571
|
+
this._sessionLogger?.write({
|
|
572
|
+
type: 'goal.abandoned',
|
|
573
|
+
tick: this._currentTick as unknown as number,
|
|
574
|
+
goalId: goal.id,
|
|
575
|
+
description: goal.description,
|
|
576
|
+
priority: goal.priority,
|
|
577
|
+
progress: goal.progress,
|
|
578
|
+
reason: reason?.slice( 0, 200 ),
|
|
579
|
+
age: ( this._currentTick as unknown as number ) - ( goal.activatedAt as unknown as number ),
|
|
580
|
+
} as any)
|
|
581
|
+
|
|
582
|
+
// Bus signal so the PlanningEngine can cancel any plans pursuing this goal
|
|
583
|
+
// (previously abandonment was session-log only — plans ran on regardless).
|
|
584
|
+
this._bus?.publish({
|
|
585
|
+
type: 'goal.abandoned', version: 1, sourceEngine: this.name,
|
|
586
|
+
salience: 0.55,
|
|
587
|
+
payload: { goalId: goal.id, reason: reason?.slice( 0, 200 ) },
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Update a goal's priority.
|
|
594
|
+
*/
|
|
595
|
+
updateGoalPriority( goalId: string, newPriority: number ): void {
|
|
596
|
+
const goal = this._goals.get( goalId )
|
|
597
|
+
if( goal && goal.status === 'active')
|
|
598
|
+
goal.priority = Math.max( 0, Math.min( 1, newPriority ) )
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ── Internal: activation ─────────────────────────────────
|
|
602
|
+
|
|
603
|
+
private _syncFromStateGoals( state: ReadonlySimulationState, tick: Tick ): void {
|
|
604
|
+
for( const entity of state.entities.values() ){
|
|
605
|
+
if( entity.type !== 'goal') continue
|
|
606
|
+
if( this._goals.has( entity.id ) ) continue
|
|
607
|
+
|
|
608
|
+
const meta = entity.metadata ?? {}
|
|
609
|
+
|
|
610
|
+
const status = ( meta.status as GoalState['status'] ) ?? 'active'
|
|
611
|
+
if( status === 'completed' || status === 'abandoned') continue
|
|
612
|
+
|
|
613
|
+
this._goals.set( entity.id, {
|
|
614
|
+
id: entity.id,
|
|
615
|
+
description: ( meta.description as string ) ?? entity.id,
|
|
616
|
+
priority: ( meta.priority as number ) ?? 0.5,
|
|
617
|
+
basePriority: ( meta.basePriority as number ) ?? ( meta.priority as number ) ?? 0.5,
|
|
618
|
+
progress: ( meta.progress as number ) ?? 0,
|
|
619
|
+
status,
|
|
620
|
+
parentGoalId: meta.parentGoalId as string | undefined,
|
|
621
|
+
subGoals: ( meta.subGoals as string[] ) ?? [],
|
|
622
|
+
activatedAt: ( meta.activatedAt as Tick ) ?? tick,
|
|
623
|
+
deadline: meta.deadline as Tick | undefined,
|
|
624
|
+
tags: ( meta.tags as string[] ) ?? [],
|
|
625
|
+
beliefsAtActivation: ( meta.beliefsAtActivation as number ) ?? this._currentBeliefCount,
|
|
626
|
+
completionType: ( meta.completionType as GoalState['completionType'] ) ?? 'epistemic',
|
|
627
|
+
completionCondition: meta.completionCondition as string | undefined,
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private _activateFromDrives( state: ReadonlySimulationState, _tick: Tick ): void {
|
|
633
|
+
const driveMappings: Array<{
|
|
634
|
+
drive: string; threshold: number
|
|
635
|
+
goalDesc: string; priority: number; tags: string[]
|
|
636
|
+
completionType?: GoalState['completionType'] // defaults to 'metric'
|
|
637
|
+
completionCondition?: string // metric goals only
|
|
638
|
+
}> = [
|
|
639
|
+
{ drive: 'drive.energy', threshold: 0.4, goalDesc: 'Replenish energy', priority: 0.7, tags: [ 'survival', 'energy' ], completionCondition: 'energy.level > 70' },
|
|
640
|
+
{ drive: 'drive.sleep', threshold: 0.5, goalDesc: 'Find rest opportunity', priority: 0.8, tags: [ 'survival', 'sleep' ], completionCondition: 'sleep.pressure < 20' },
|
|
641
|
+
{ drive: 'drive.stress_reduction', threshold: 0.4, goalDesc: 'Reduce stress load', priority: 0.6, tags: [ 'wellbeing', 'stress' ], completionCondition: 'stress.load < 40' },
|
|
642
|
+
// `emotion.boredom` is a 0–1 metric (aesthetic.evaluator); the drive fires when it is
|
|
643
|
+
// sustained above 0.6, so the goal resolves once it falls back under 0.4 — a real
|
|
644
|
+
// hysteresis band, not the old `< 40` (always-true on a 0–1 scale → born-done loop).
|
|
645
|
+
{ drive: 'drive.seek_engagement', threshold: 0.5, goalDesc: 'Seek stimulating engagement — reach out, explore, create, or learn something new to break the monotony', priority: 0.55, tags: [ 'engagement', 'boredom' ], completionCondition: 'emotion.boredom < 0.4' },
|
|
646
|
+
// Curiosity-to-resolve (Phase 3.a): a familiar-yet-unknown someone the Will keeps
|
|
647
|
+
// meeting generates an epistemic pull — "get to know them". Epistemic, so it resolves
|
|
648
|
+
// as the Will *learns* (belief formation, incl. keid-tagged beliefs from conversation),
|
|
649
|
+
// rather than against a metric threshold.
|
|
650
|
+
{ drive: 'drive.curiosity_resolve', threshold: 0.4, goalDesc: 'Get to know the people I keep encountering but barely know', priority: 0.45, tags: [ 'curiosity', 'knowledge' ], completionType: 'epistemic' },
|
|
651
|
+
]
|
|
652
|
+
|
|
653
|
+
for( const mapping of driveMappings ){
|
|
654
|
+
const driveIntensity = state.metrics.get( mapping.drive ) ?? 0
|
|
655
|
+
if( driveIntensity < mapping.threshold ) continue
|
|
656
|
+
|
|
657
|
+
const existing = Array.from( this._goals.values() )
|
|
658
|
+
.find( g => {
|
|
659
|
+
return g.tags.includes( mapping.tags[0]! )
|
|
660
|
+
&& g.tags.includes( mapping.tags[1]! )
|
|
661
|
+
&& g.status === 'active'
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
// Never spawn a goal that is *already satisfied* — it would complete on its
|
|
665
|
+
// creation tick and respawn the next, forever (a goal born done). Only create when
|
|
666
|
+
// there is genuine work toward the completion condition. (Guards a real drive/metric
|
|
667
|
+
// threshold mismatch found in a session log: seek-engagement spawned while
|
|
668
|
+
// `emotion.boredom < 40` was already true, producing one age-0 "achievement" per tick.)
|
|
669
|
+
if( !existing && !this._isConditionMet( mapping.completionCondition, state ) )
|
|
670
|
+
this.addGoal( mapping.goalDesc, mapping.priority, mapping.tags, undefined, undefined, mapping.completionType ?? 'metric', mapping.completionCondition )
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private _activateFromPercepts( state: ReadonlySimulationState, _tick: Tick ): void {
|
|
675
|
+
for( const entity of state.entities.values() )
|
|
676
|
+
if( entity.type === 'attention.demand' && entity.metadata?.generatesGoal ){
|
|
677
|
+
const m = entity.metadata
|
|
678
|
+
const desc = ( m.goalDescription as string ) ?? 'Respond to demand'
|
|
679
|
+
// A demand may carry its own goal shape (tags / completion) — e.g. the per-entity
|
|
680
|
+
// curiosity pull from known.entity.tracker, an epistemic/metric goal keyed to one
|
|
681
|
+
// referent. Defaults preserve the original external-action-demand behaviour.
|
|
682
|
+
const tags = ( m.goalTags as string[] ) ?? [ 'external', 'demand' ]
|
|
683
|
+
const cType = ( m.goalCompletionType as GoalState['completionType'] ) ?? 'action'
|
|
684
|
+
const cCond = m.goalCompletionCondition as string | undefined
|
|
685
|
+
// Dedup by the referent (keid: tag) when present, else by description.
|
|
686
|
+
const keidTag = tags.find( t => t.startsWith( 'keid:' ) )
|
|
687
|
+
const existing = Array.from( this._goals.values() ).find( g =>
|
|
688
|
+
g.status === 'active' && ( keidTag ? g.tags.includes( keidTag ) : g.description === desc ) )
|
|
689
|
+
|
|
690
|
+
// Don't spawn a goal that's already satisfied (born-done guard — see _activateFromDrives).
|
|
691
|
+
if( !existing && !this._isConditionMet( cCond, state ) )
|
|
692
|
+
this.addGoal( desc, ( m.goalPriority as number ) ?? 0.5, tags, undefined, undefined, cType, cCond )
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ── Internal: priority ───────────────────────────────────
|
|
697
|
+
|
|
698
|
+
private _updatePriorities( state: ReadonlySimulationState ): void {
|
|
699
|
+
// Task-persistence: which goal the Will is focused on + how committed (task.switcher)
|
|
700
|
+
// and the (developable) cost of switching away. Drives the commitment boost below.
|
|
701
|
+
const focusMeta = state.entities.get( 'task-switch-focus' )?.metadata as Record<string, unknown> | undefined
|
|
702
|
+
const focusedGoalId = focusMeta?.goalId as string | undefined
|
|
703
|
+
const focusTicks = ( focusMeta?.focusTicks as number ) ?? 0
|
|
704
|
+
const switchCost = state.metrics.get( 'task_switch.switch_cost' ) ?? 0
|
|
705
|
+
|
|
706
|
+
for( const goal of this._goals.values() ){
|
|
707
|
+
if( goal.status !== 'active') continue
|
|
708
|
+
|
|
709
|
+
// Commitment boost — only the focused goal, bounded, never accumulating. Builds with
|
|
710
|
+
// focus duration, amplified by sunk cost in an in-progress plan, scaled by switch cost.
|
|
711
|
+
let commitmentBoost = 0
|
|
712
|
+
if( goal.id === focusedGoalId && focusTicks > 0 && switchCost > 0 ){
|
|
713
|
+
const commitment = Math.min( 1, focusTicks / FOCUS_COMMITMENT_RAMP )
|
|
714
|
+
const planProgress = this._focusedPlanProgress( state, goal.id )
|
|
715
|
+
commitmentBoost = Math.min(
|
|
716
|
+
MAX_COMMITMENT_BOOST,
|
|
717
|
+
switchCost * commitment * ( 1 + planProgress ) * COMMITMENT_GAIN
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Drive-aligned goals get priority boost from drive intensity
|
|
722
|
+
let driveBoost = 0
|
|
723
|
+
if( goal.tags.includes('energy') )
|
|
724
|
+
driveBoost += ( state.metrics.get('drive.energy') ?? 0 ) * 0.3
|
|
725
|
+
|
|
726
|
+
if( goal.tags.includes('sleep') )
|
|
727
|
+
driveBoost += ( state.metrics.get('drive.sleep') ?? 0 ) * 0.3
|
|
728
|
+
|
|
729
|
+
if( goal.tags.includes('engagement') )
|
|
730
|
+
driveBoost += ( state.metrics.get('drive.seek_engagement') ?? 0 ) * 0.25
|
|
731
|
+
|
|
732
|
+
// Deadline pressure: boost before deadline, decay faster after
|
|
733
|
+
let deadlineBoost = 0
|
|
734
|
+
if( goal.deadline ){
|
|
735
|
+
const ticksUntilDeadline = ( goal.deadline as unknown as number ) - ( this._currentTick as unknown as number )
|
|
736
|
+
if( ticksUntilDeadline > 0 ){
|
|
737
|
+
// Urgency ramps linearly in the last 50 ticks before deadline
|
|
738
|
+
const urgency = Math.max( 0, 1 - ticksUntilDeadline / 50 )
|
|
739
|
+
deadlineBoost = urgency * 0.4
|
|
740
|
+
} else {
|
|
741
|
+
// Overdue: decay priority 3× faster — stale goals should yield to fresh ones
|
|
742
|
+
deadlineBoost = -this._priorityDecayRate * 2
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Decay priority toward base
|
|
747
|
+
goal.priority = goal.basePriority
|
|
748
|
+
+ driveBoost
|
|
749
|
+
+ deadlineBoost
|
|
750
|
+
+ commitmentBoost
|
|
751
|
+
- this._priorityDecayRate
|
|
752
|
+
|
|
753
|
+
goal.priority = Math.max( 0, Math.min( 1, goal.priority ) )
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Plan completion (0..1) for a goal, read from the live `plan` entity (planning.engine).
|
|
759
|
+
* Sunk cost: a goal with a half-finished plan is costlier to abandon, so it amplifies the
|
|
760
|
+
* task-persistence commitment boost. 0 when the goal has no plan or an empty one.
|
|
761
|
+
*/
|
|
762
|
+
private _focusedPlanProgress( state: ReadonlySimulationState, goalId: string ): number {
|
|
763
|
+
for( const e of state.entities.values() ){
|
|
764
|
+
if( e.type !== 'plan' || e.metadata?.goalId !== goalId ) continue
|
|
765
|
+
const steps = ( e.metadata?.steps as Array<{ status?: string }> | undefined ) ?? []
|
|
766
|
+
if( steps.length === 0 ) continue
|
|
767
|
+
const done = steps.filter( s => s.status === 'completed' || s.status === 'skipped' ).length
|
|
768
|
+
return done / steps.length
|
|
769
|
+
}
|
|
770
|
+
return 0
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private _resolveConflicts(): void {
|
|
774
|
+
const active = this.getActiveGoals()
|
|
775
|
+
|
|
776
|
+
// Deactivate lowest-priority goals if over capacity
|
|
777
|
+
while( active.length > this._maxActiveGoals ){
|
|
778
|
+
const lowest = active.pop()
|
|
779
|
+
if( lowest ) lowest.status = 'pending'
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ── Internal: progress ───────────────────────────────────
|
|
784
|
+
|
|
785
|
+
private _updateProgress( state: ReadonlySimulationState, tick: Tick ): void {
|
|
786
|
+
// Read from state.metrics — authoritative, updated every tick by semantic integrator.
|
|
787
|
+
const currentBeliefs = state.metrics.get('memory.beliefs_total') ?? 0
|
|
788
|
+
|
|
789
|
+
for( const goal of this._goals.values() ){
|
|
790
|
+
// A goal cannot be born and die in the same tick. Give it at least one tick to be
|
|
791
|
+
// worked on before it can complete — this kills age-0 "achievements" universally
|
|
792
|
+
// (defence-in-depth beyond the already-satisfied creation guard in _activateFromDrives).
|
|
793
|
+
if( goal.activatedAt === tick ) continue
|
|
794
|
+
|
|
795
|
+
if( goal.status === 'pending_verification' ){
|
|
796
|
+
const met = this._evaluateMetricProgress( goal, state ) >= 1
|
|
797
|
+
met ? this.completeGoal( goal.id ) : ( goal.status = 'active' )
|
|
798
|
+
|
|
799
|
+
continue // skip completionType switch entirely
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if( goal.status !== 'active' && goal.status !== 'blocked' ) continue
|
|
803
|
+
|
|
804
|
+
switch( goal.completionType ){
|
|
805
|
+
case 'metric':
|
|
806
|
+
goal.progress = this._evaluateMetricProgress( goal, state )
|
|
807
|
+
break
|
|
808
|
+
|
|
809
|
+
case 'action':
|
|
810
|
+
// Progress is advanced externally via onCognitiveEvent('action.outcome')
|
|
811
|
+
// → _nudgeActionGoals(). _updateProgress() only checks for completion here.
|
|
812
|
+
// _deactivateStale abandons it if stuck for too long under frustration.
|
|
813
|
+
break
|
|
814
|
+
|
|
815
|
+
case 'epistemic':
|
|
816
|
+
default: {
|
|
817
|
+
const newBeliefs = currentBeliefs - goal.beliefsAtActivation
|
|
818
|
+
goal.progress = Math.min( 1, newBeliefs / this._epistemicBeliefThreshold )
|
|
819
|
+
break
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
goal.progress >= 1 && this.completeGoal( goal.id )
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Evaluate progress for a metric goal.
|
|
829
|
+
*
|
|
830
|
+
* If the goal has a completionCondition (e.g. "stress.load < 40"), parse and
|
|
831
|
+
* compute smooth 0-1 progress toward it. Falls back to tag-based heuristics
|
|
832
|
+
* for the three built-in drive metrics when no condition is specified.
|
|
833
|
+
*/
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* 4.1: Nudge progress on `action`-type goals when an action.outcome event fires
|
|
837
|
+
* whose domain or actionType overlaps with the goal's tags.
|
|
838
|
+
*
|
|
839
|
+
* Progress is incremented by `outcomeQuality × 0.12` per matched action, so
|
|
840
|
+
* a goal with a single matching tag needs ~9 successful actions (at full quality)
|
|
841
|
+
* to complete — a realistic bar for discrete, real-world tasks.
|
|
842
|
+
*
|
|
843
|
+
* Substring matching (both directions) handles common mismatches between
|
|
844
|
+
* effector names and goal tags (e.g. "communicate" ↔ "communication",
|
|
845
|
+
* "learn" ↔ "learning").
|
|
846
|
+
*/
|
|
847
|
+
private _nudgeActionGoals(
|
|
848
|
+
domain: string,
|
|
849
|
+
actionType: string,
|
|
850
|
+
outcomeQuality: number
|
|
851
|
+
): void {
|
|
852
|
+
const dLow = domain.toLowerCase()
|
|
853
|
+
const aLow = actionType.toLowerCase()
|
|
854
|
+
|
|
855
|
+
// Communication actions — 'talk' and 'text' are always treated as
|
|
856
|
+
// matching the 'communication' and 'reply' tag families so goals created
|
|
857
|
+
// in response to incoming messages get their progress nudged.
|
|
858
|
+
const isCommunicationAction = aLow === 'talk' || aLow === 'text'
|
|
859
|
+
|| dLow === 'communication'
|
|
860
|
+
|
|
861
|
+
for( const goal of this._goals.values() ){
|
|
862
|
+
if( goal.status !== 'active' && goal.status !== 'blocked' ) continue
|
|
863
|
+
if( goal.completionType !== 'action' ) continue
|
|
864
|
+
|
|
865
|
+
const hasMatch = goal.tags.some( tag => {
|
|
866
|
+
const t = tag.toLowerCase()
|
|
867
|
+
return t === dLow
|
|
868
|
+
|| t === aLow
|
|
869
|
+
|| dLow.includes( t ) || t.includes( dLow )
|
|
870
|
+
|| aLow.includes( t ) || t.includes( aLow )
|
|
871
|
+
}) || ( isCommunicationAction && goal.tags.some( t =>
|
|
872
|
+
t === 'communication' || t === 'reply' || t === 'conversation'
|
|
873
|
+
) )
|
|
874
|
+
|
|
875
|
+
if( hasMatch ){
|
|
876
|
+
goal.progress = Math.min( 1, goal.progress + outcomeQuality * 0.12 )
|
|
877
|
+
goal.lastActionAttemptTick = this._currentTick
|
|
878
|
+
goal.lastActionType = actionType
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** True when a metric completionCondition (e.g. "emotion.boredom < 40") is already met. */
|
|
884
|
+
private _isConditionMet( condition: string | undefined, state: ReadonlySimulationState ): boolean {
|
|
885
|
+
if( !condition ) return false
|
|
886
|
+
const m = condition.match( /^([\w.]+)\s*([<>]=?)\s*([\d.]+)$/ )
|
|
887
|
+
if( !m ) return false
|
|
888
|
+
const [ , key, op, raw ] = m
|
|
889
|
+
const current = state.metrics.get( key! ) ?? 0
|
|
890
|
+
const threshold = parseFloat( raw! )
|
|
891
|
+
return op === '<' ? current < threshold
|
|
892
|
+
: op === '<=' ? current <= threshold
|
|
893
|
+
: op === '>' ? current > threshold
|
|
894
|
+
: current >= threshold
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private _evaluateMetricProgress(
|
|
898
|
+
goal: GoalState,
|
|
899
|
+
state: ReadonlySimulationState
|
|
900
|
+
): number {
|
|
901
|
+
if( goal.completionCondition ){
|
|
902
|
+
const m = goal.completionCondition.match( /^([\w.]+)\s*([<>]=?)\s*([\d.]+)$/ )
|
|
903
|
+
if( m ){
|
|
904
|
+
const [ , key, op, rawThreshold ] = m
|
|
905
|
+
const current = state.metrics.get( key! ) ?? 0
|
|
906
|
+
const threshold = parseFloat( rawThreshold! )
|
|
907
|
+
|
|
908
|
+
const met = op === '<' ? current < threshold
|
|
909
|
+
: op === '<=' ? current <= threshold
|
|
910
|
+
: op === '>' ? current > threshold
|
|
911
|
+
: current >= threshold
|
|
912
|
+
|
|
913
|
+
if( met ) return 1
|
|
914
|
+
|
|
915
|
+
// Smooth gradient toward the threshold. Infer the metric's range from the threshold:
|
|
916
|
+
// ≤ 1 ⇒ a 0–1 metric (e.g. emotion.boredom), otherwise the 0–100 default. Clamped.
|
|
917
|
+
const scale = threshold <= 1 ? 1 : 100
|
|
918
|
+
return op === '<' || op === '<='
|
|
919
|
+
? Math.max( 0, Math.min( 1, ( scale - current ) / ( scale - threshold ) ) )
|
|
920
|
+
: Math.max( 0, Math.min( 1, current / threshold ) )
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Tag-based fallback for the three built-in drive metrics
|
|
925
|
+
if( goal.tags.includes('energy') )
|
|
926
|
+
return Math.min( 1, this._energyLevel / 100 )
|
|
927
|
+
|
|
928
|
+
if( goal.tags.includes('sleep') )
|
|
929
|
+
return Math.max( 0, 1 - ( this._sleepPressure ) / 100 )
|
|
930
|
+
|
|
931
|
+
if( goal.tags.includes('stress') )
|
|
932
|
+
return Math.max( 0, 1 - this._stressLoad / 100 )
|
|
933
|
+
|
|
934
|
+
return goal.progress // no update if no condition and no known tag
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Refresh personality-derived dispositions from `engine-config-goal-manager`
|
|
939
|
+
* (base params ⊕ persona-prior deltas). Grit/persistence is a per-Will trait
|
|
940
|
+
* seeded by the PMA and developed by the metacognition cycle — not a constant.
|
|
941
|
+
*/
|
|
942
|
+
private _readConfigFromState( state: ReadonlySimulationState ): void {
|
|
943
|
+
const p = readEffectiveParams( state, 'engine-config-goal-manager' )
|
|
944
|
+
if( p.gritPriority != null ) this._gritPriority = p.gritPriority
|
|
945
|
+
if( p.gritPatienceScale != null ) this._gritPatienceScale = p.gritPatienceScale
|
|
946
|
+
if( p.frustrationTolerance != null ) this._frustrationTolerance = p.frustrationTolerance
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private _deactivateStale( tick: Tick, state: ReadonlySimulationState ): void {
|
|
950
|
+
// Frustration compresses the patience window — but frustrationTolerance (a
|
|
951
|
+
// personality trait) dampens that, so a resilient mind doesn't give up faster
|
|
952
|
+
// just because it's frustrated.
|
|
953
|
+
const tol = Math.max( 0, Math.min( 1, this._frustrationTolerance ) )
|
|
954
|
+
const frustration = ( state.metrics.get('emotion.frustration') ?? 0 ) * ( 1 - tol )
|
|
955
|
+
const patienceTicks = frustration > 0.6
|
|
956
|
+
? 80 // gave up quickly under high (tolerance-adjusted) frustration
|
|
957
|
+
: frustration > 0.3
|
|
958
|
+
? 130 // moderate impatience
|
|
959
|
+
: 200 // default — full patience window
|
|
960
|
+
|
|
961
|
+
for( const goal of this._goals.values() ){
|
|
962
|
+
if( goal.status !== 'active') continue
|
|
963
|
+
if( goal.priority < this._deactivationThreshold ){
|
|
964
|
+
goal.status = 'abandoned'
|
|
965
|
+
continue
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Resilience / grit: importance buys persistence. A sufficiently important goal
|
|
969
|
+
// is NEVER auto-abandoned by staleness — the mind only lets it go by a deliberate
|
|
970
|
+
// executive decision (goalsToAbandon). What matters isn't dropped by a timer.
|
|
971
|
+
if( goal.priority >= this._gritPriority ) continue
|
|
972
|
+
|
|
973
|
+
// Below the grit threshold, patience scales with priority — a more-important
|
|
974
|
+
// stuck goal is pursued much longer before the mind gives up on it.
|
|
975
|
+
const scaledPatience = patienceTicks * ( 1 + goal.priority * this._gritPatienceScale )
|
|
976
|
+
const age = tick - goal.activatedAt
|
|
977
|
+
if( age > scaledPatience && goal.progress < 0.1 )
|
|
978
|
+
goal.status = 'abandoned'
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ── Internal: persistence ────────────────────────────────
|
|
983
|
+
|
|
984
|
+
private _persistGoals( commands: StateCommands, tick: Tick ): void {
|
|
985
|
+
for( const goal of this._goals.values() )
|
|
986
|
+
commands.set!.push({
|
|
987
|
+
id: goal.id,
|
|
988
|
+
type: 'goal',
|
|
989
|
+
updatedAt: tick,
|
|
990
|
+
metadata: {
|
|
991
|
+
description: goal.description,
|
|
992
|
+
priority: goal.priority,
|
|
993
|
+
basePriority: goal.basePriority,
|
|
994
|
+
progress: goal.progress,
|
|
995
|
+
status: goal.status,
|
|
996
|
+
parentGoalId: goal.parentGoalId,
|
|
997
|
+
subGoals: goal.subGoals,
|
|
998
|
+
deadline: goal.deadline,
|
|
999
|
+
tags: goal.tags,
|
|
1000
|
+
beliefsAtActivation: goal.beliefsAtActivation,
|
|
1001
|
+
completionType: goal.completionType,
|
|
1002
|
+
completionCondition: goal.completionCondition,
|
|
1003
|
+
activatedAt: goal.activatedAt,
|
|
1004
|
+
tick,
|
|
1005
|
+
},
|
|
1006
|
+
})
|
|
1007
|
+
}
|
|
1008
|
+
}
|