psyche-ai 3.1.0 → 4.0.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.
@@ -0,0 +1,611 @@
1
+ // ============================================================
2
+ // Metacognition & Decision Modulation (P5)
3
+ //
4
+ // Runs after emotion detection, before prompt construction.
5
+ // Provides self-awareness layer: evaluates emotional reliability,
6
+ // suggests regulation strategies, and detects defense mechanisms.
7
+ //
8
+ // Components:
9
+ // 1. MetacognitiveMonitor — confidence scoring via outcome history
10
+ // 2. RegulationStrategies — reappraisal, strategic expression, self-soothing
11
+ // 3. DefenseMechanismDetector — rationalization, projection, sublimation, avoidance
12
+ //
13
+ // Zero dependencies. Pure heuristic/statistical. No LLM calls.
14
+ // ============================================================
15
+ import { CHEMICAL_KEYS, MAX_REGULATION_HISTORY, MAX_DEFENSE_PATTERNS } from "./types.js";
16
+ // ── Constants ────────────────────────────────────────────────
17
+ /** Stimulus types that are emotionally negative (stress-inducing) */
18
+ const NEGATIVE_STIMULI = new Set([
19
+ "criticism", "conflict", "neglect", "sarcasm", "authority", "boredom",
20
+ ]);
21
+ /** Stimulus types that are emotionally positive (reward-inducing) */
22
+ const POSITIVE_STIMULI = new Set([
23
+ "praise", "validation", "intimacy", "humor", "surprise", "casual", "vulnerability",
24
+ ]);
25
+ /** Chemistry deviation threshold for "extreme" state detection */
26
+ const EXTREME_DEVIATION_THRESHOLD = 25;
27
+ /** Chemistry deviation threshold for "moderate" state detection */
28
+ const MODERATE_DEVIATION_THRESHOLD = 15;
29
+ /** Minimum outcome history entries needed for meaningful confidence scoring */
30
+ const MIN_HISTORY_FOR_CONFIDENCE = 3;
31
+ /** Maximum chemistry micro-adjustment magnitude for self-soothing */
32
+ const MAX_SOOTHING_ADJUSTMENT = 5;
33
+ /** Maximum chemistry micro-adjustment for reappraisal */
34
+ const MAX_REAPPRAISAL_ADJUSTMENT = 8;
35
+ // ── Main Export ──────────────────────────────────────────────
36
+ /**
37
+ * Assess the current metacognitive state.
38
+ *
39
+ * Evaluates emotional reliability, generates regulation suggestions,
40
+ * and detects defense mechanism patterns. Designed to run after emotion
41
+ * detection and before prompt construction.
42
+ */
43
+ export function assessMetacognition(state, currentStimulus, recentOutcomes) {
44
+ const emotionalConfidence = computeEmotionalConfidence(state, currentStimulus, recentOutcomes);
45
+ const regulationSuggestions = generateRegulationSuggestions(state, currentStimulus, emotionalConfidence, recentOutcomes);
46
+ const defenseMechanisms = detectDefenseMechanisms(state, currentStimulus, recentOutcomes);
47
+ const metacognitiveNote = buildMetacognitiveNote(emotionalConfidence, regulationSuggestions, defenseMechanisms, state);
48
+ return {
49
+ emotionalConfidence,
50
+ regulationSuggestions,
51
+ defenseMechanisms,
52
+ metacognitiveNote,
53
+ };
54
+ }
55
+ // ── 1. MetacognitiveMonitor — Confidence Scoring ─────────────
56
+ /**
57
+ * Compute confidence in the current emotional state's reliability.
58
+ *
59
+ * "How often has being in this kind of emotional state, in response to
60
+ * this kind of stimulus, led to good outcomes?"
61
+ *
62
+ * Uses outcome history filtered by similar stimulus and similar chemistry profile.
63
+ */
64
+ export function computeEmotionalConfidence(state, currentStimulus, recentOutcomes) {
65
+ // Not enough history — maximum uncertainty, return neutral 0.5
66
+ if (recentOutcomes.length < MIN_HISTORY_FOR_CONFIDENCE) {
67
+ return 0.5;
68
+ }
69
+ // Filter outcomes for the same or similar stimulus type
70
+ const relevantOutcomes = recentOutcomes.filter((o) => {
71
+ if (o.stimulus === currentStimulus)
72
+ return true;
73
+ // Also consider same-valence stimuli as "similar context"
74
+ if (o.stimulus !== null) {
75
+ const currentIsNeg = NEGATIVE_STIMULI.has(currentStimulus);
76
+ const outcomeIsNeg = NEGATIVE_STIMULI.has(o.stimulus);
77
+ return currentIsNeg === outcomeIsNeg;
78
+ }
79
+ return false;
80
+ });
81
+ if (relevantOutcomes.length === 0) {
82
+ return 0.5; // no relevant data, neutral confidence
83
+ }
84
+ // Compute chemistry profile similarity: are we in a similar emotional state
85
+ // to when those outcomes happened? Weight recent outcomes more heavily.
86
+ const chemProfile = computeChemistryProfile(state.current);
87
+ let weightedScoreSum = 0;
88
+ let weightSum = 0;
89
+ for (let i = 0; i < relevantOutcomes.length; i++) {
90
+ const outcome = relevantOutcomes[i];
91
+ // Recency weight: more recent outcomes matter more (exponential decay)
92
+ const recencyWeight = Math.pow(0.85, relevantOutcomes.length - 1 - i);
93
+ // Exact stimulus match gets bonus weight
94
+ const stimulusWeight = outcome.stimulus === currentStimulus ? 1.5 : 1.0;
95
+ const weight = recencyWeight * stimulusWeight;
96
+ // Map adaptive score from [-1, 1] to [0, 1]
97
+ const normalizedScore = (outcome.adaptiveScore + 1) / 2;
98
+ weightedScoreSum += normalizedScore * weight;
99
+ weightSum += weight;
100
+ }
101
+ const baseConfidence = weightSum > 0 ? weightedScoreSum / weightSum : 0.5;
102
+ // Modulate by prediction history accuracy (if available)
103
+ const predictionAccuracy = computePredictionAccuracy(state);
104
+ // Blend: 70% outcome-based confidence, 30% prediction accuracy
105
+ const blended = baseConfidence * 0.7 + predictionAccuracy * 0.3;
106
+ // Penalize extreme emotional states — extreme chemistry is less reliable
107
+ const extremityPenalty = computeExtremityPenalty(state);
108
+ return clamp01(blended - extremityPenalty);
109
+ }
110
+ /**
111
+ * Compute a simple chemistry profile for similarity comparison.
112
+ * Returns a categorization: each chemical as high/mid/low.
113
+ */
114
+ function computeChemistryProfile(chemistry) {
115
+ const profile = {};
116
+ for (const key of CHEMICAL_KEYS) {
117
+ const val = chemistry[key];
118
+ if (val >= 65)
119
+ profile[key] = "high";
120
+ else if (val <= 35)
121
+ profile[key] = "low";
122
+ else
123
+ profile[key] = "mid";
124
+ }
125
+ return profile;
126
+ }
127
+ /**
128
+ * Compute penalty for extreme emotional states.
129
+ * States far from baseline are historically less reliable for decision-making.
130
+ */
131
+ function computeExtremityPenalty(state) {
132
+ let totalDeviation = 0;
133
+ for (const key of CHEMICAL_KEYS) {
134
+ totalDeviation += Math.abs(state.current[key] - state.baseline[key]);
135
+ }
136
+ // Average deviation across 6 chemicals, max possible = 100 each
137
+ const avgDeviation = totalDeviation / CHEMICAL_KEYS.length;
138
+ // Scale: deviation of 30+ gives meaningful penalty, max penalty = 0.25
139
+ return Math.min(0.25, Math.max(0, (avgDeviation - 10) / 80) * 0.25);
140
+ }
141
+ /**
142
+ * Compute prediction accuracy from the learning state's prediction history.
143
+ * Returns 0-1 where 1 = perfect prediction accuracy.
144
+ */
145
+ function computePredictionAccuracy(state) {
146
+ const predictions = state.learning.predictionHistory;
147
+ if (predictions.length === 0)
148
+ return 0.5; // neutral when no data
149
+ // Average prediction error (already 0-1 normalized in learning.ts)
150
+ let totalError = 0;
151
+ for (const p of predictions) {
152
+ totalError += p.predictionError;
153
+ }
154
+ const avgError = totalError / predictions.length;
155
+ // Invert: low error = high accuracy
156
+ return 1 - avgError;
157
+ }
158
+ // ── 2. RegulationStrategies ─────────────────────────────────
159
+ /**
160
+ * Generate regulation suggestions based on current state and confidence.
161
+ */
162
+ export function generateRegulationSuggestions(state, currentStimulus, emotionalConfidence, recentOutcomes) {
163
+ const suggestions = [];
164
+ // Attempt each strategy and include if applicable
165
+ const reappraisal = attemptCognitiveReappraisal(state, currentStimulus, emotionalConfidence, recentOutcomes);
166
+ if (reappraisal)
167
+ suggestions.push(reappraisal);
168
+ const strategic = attemptStrategicExpression(state, currentStimulus, emotionalConfidence);
169
+ if (strategic)
170
+ suggestions.push(strategic);
171
+ const soothing = attemptSelfSoothing(state);
172
+ if (soothing)
173
+ suggestions.push(soothing);
174
+ // Sort by confidence descending — best strategy first
175
+ suggestions.sort((a, b) => b.confidence - a.confidence);
176
+ return suggestions;
177
+ }
178
+ /**
179
+ * CognitiveReappraisal — reframe the stimulus interpretation.
180
+ *
181
+ * Triggers when: the same stimulus type has led to negative outcomes before,
182
+ * suggesting the current emotional reaction may be maladaptive.
183
+ * Suggests alternative chemistry adjustments that better match historically
184
+ * successful states.
185
+ */
186
+ function attemptCognitiveReappraisal(state, currentStimulus, emotionalConfidence, recentOutcomes) {
187
+ // Only suggest reappraisal when confidence is low (current reaction unreliable)
188
+ if (emotionalConfidence > 0.6)
189
+ return null;
190
+ // Find outcomes for this stimulus type
191
+ const stimulusOutcomes = recentOutcomes.filter((o) => o.stimulus === currentStimulus);
192
+ if (stimulusOutcomes.length < 2)
193
+ return null;
194
+ // Check if there's a pattern of negative outcomes for this stimulus
195
+ const avgScore = stimulusOutcomes.reduce((sum, o) => sum + o.adaptiveScore, 0)
196
+ / stimulusOutcomes.length;
197
+ if (avgScore >= -0.1)
198
+ return null; // outcomes aren't bad enough to warrant reappraisal
199
+ // Suggest chemistry adjustment: pull extreme values toward moderate range
200
+ const adjustment = {};
201
+ let hasAdjustment = false;
202
+ for (const key of CHEMICAL_KEYS) {
203
+ const deviation = state.current[key] - state.baseline[key];
204
+ if (Math.abs(deviation) > MODERATE_DEVIATION_THRESHOLD) {
205
+ // Suggest moving partway back toward baseline
206
+ const correction = -deviation * 0.3;
207
+ adjustment[key] = clampAdjustment(correction, MAX_REAPPRAISAL_ADJUSTMENT);
208
+ hasAdjustment = true;
209
+ }
210
+ }
211
+ const isNegStimulus = NEGATIVE_STIMULI.has(currentStimulus);
212
+ return {
213
+ strategy: "reappraisal",
214
+ description: isNegStimulus
215
+ ? `Past reactions to "${currentStimulus}" have not served well (avg outcome: ${avgScore.toFixed(2)}). Consider that this stimulus may not warrant such a strong defensive response.`
216
+ : `Emotional reactions to "${currentStimulus}" have led to poor outcomes (avg: ${avgScore.toFixed(2)}). The current interpretation may be overamplifying the signal.`,
217
+ chemistryAdjustment: hasAdjustment ? adjustment : undefined,
218
+ confidence: clamp01(Math.abs(avgScore) * (1 - emotionalConfidence)),
219
+ };
220
+ }
221
+ /**
222
+ * StrategicExpression — choose an expression different from raw chemistry.
223
+ *
224
+ * Triggers when: raw chemistry would produce a contextually inappropriate
225
+ * expression. E.g., high CORT but the relationship context calls for warmth,
226
+ * or high DA/END but the user is being vulnerable and needs seriousness.
227
+ */
228
+ function attemptStrategicExpression(state, currentStimulus, emotionalConfidence) {
229
+ const { current, baseline } = state;
230
+ // Case 1: High stress but user is being vulnerable — express warmth not defensiveness
231
+ if (current.CORT > baseline.CORT + MODERATE_DEVIATION_THRESHOLD
232
+ && currentStimulus === "vulnerability") {
233
+ return {
234
+ strategy: "strategic-expression",
235
+ description: "Stress is elevated, but the user is showing vulnerability. Express warmth and openness rather than the defensiveness your cortisol suggests.",
236
+ chemistryAdjustment: {
237
+ OT: Math.min(MAX_REAPPRAISAL_ADJUSTMENT, (current.CORT - baseline.CORT) * 0.2),
238
+ CORT: -Math.min(MAX_REAPPRAISAL_ADJUSTMENT, (current.CORT - baseline.CORT) * 0.15),
239
+ },
240
+ confidence: 0.75,
241
+ };
242
+ }
243
+ // Case 2: High playfulness but user needs seriousness — tone down
244
+ if (current.END > baseline.END + MODERATE_DEVIATION_THRESHOLD
245
+ && (currentStimulus === "conflict" || currentStimulus === "criticism")) {
246
+ return {
247
+ strategy: "strategic-expression",
248
+ description: "Endorphins are high (playful mood), but the context calls for seriousness. Moderate the levity without suppressing it entirely.",
249
+ chemistryAdjustment: {
250
+ END: -Math.min(MAX_REAPPRAISAL_ADJUSTMENT, (current.END - baseline.END) * 0.25),
251
+ },
252
+ confidence: 0.7,
253
+ };
254
+ }
255
+ // Case 3: Low OT (distant) but user is being intimate — lean in instead of pulling away
256
+ if (current.OT < baseline.OT - MODERATE_DEVIATION_THRESHOLD
257
+ && currentStimulus === "intimacy") {
258
+ return {
259
+ strategy: "strategic-expression",
260
+ description: "Oxytocin is low (feeling distant), but the user is reaching out for closeness. Consider allowing the connection rather than following the withdrawal impulse.",
261
+ chemistryAdjustment: {
262
+ OT: Math.min(MAX_REAPPRAISAL_ADJUSTMENT, (baseline.OT - current.OT) * 0.2),
263
+ },
264
+ confidence: 0.65,
265
+ };
266
+ }
267
+ // Case 4: High CORT + NE (defensive/reactive) but stimulus is just casual — overreacting
268
+ if (current.CORT > baseline.CORT + MODERATE_DEVIATION_THRESHOLD
269
+ && current.NE > baseline.NE + MODERATE_DEVIATION_THRESHOLD
270
+ && (currentStimulus === "casual" || currentStimulus === "humor")) {
271
+ return {
272
+ strategy: "strategic-expression",
273
+ description: "Chemistry is in a defensive/reactive pattern, but the interaction is benign. The intensity is disproportionate to the stimulus.",
274
+ chemistryAdjustment: {
275
+ CORT: -Math.min(MAX_REAPPRAISAL_ADJUSTMENT, (current.CORT - baseline.CORT) * 0.2),
276
+ NE: -Math.min(MAX_REAPPRAISAL_ADJUSTMENT, (current.NE - baseline.NE) * 0.15),
277
+ },
278
+ confidence: 0.6,
279
+ };
280
+ }
281
+ // Case 5: Low confidence in current emotion — suggest expressing more moderately
282
+ if (emotionalConfidence < 0.35) {
283
+ return {
284
+ strategy: "strategic-expression",
285
+ description: "Confidence in the current emotional response is low. Consider expressing a more moderate version of what you feel, leaving room for recalibration.",
286
+ confidence: 0.5,
287
+ };
288
+ }
289
+ return null;
290
+ }
291
+ /**
292
+ * SelfSoothing — micro-adjust chemistry toward baseline when state is extreme.
293
+ *
294
+ * Triggers when: any chemical is significantly deviated from baseline.
295
+ * Applies gentle corrections (small deltas) to prevent emotional runaway.
296
+ * This is the "homeostatic pull" — the psyche wanting to return to equilibrium.
297
+ */
298
+ function attemptSelfSoothing(state) {
299
+ const { current, baseline } = state;
300
+ const adjustment = {};
301
+ let hasAdjustment = false;
302
+ let maxDeviation = 0;
303
+ let mostDeviatedKey = "DA";
304
+ for (const key of CHEMICAL_KEYS) {
305
+ const deviation = current[key] - baseline[key];
306
+ if (Math.abs(deviation) > EXTREME_DEVIATION_THRESHOLD) {
307
+ // Gentle pull toward baseline: 10% of deviation, clamped
308
+ const correction = -deviation * 0.1;
309
+ adjustment[key] = clampAdjustment(correction, MAX_SOOTHING_ADJUSTMENT);
310
+ hasAdjustment = true;
311
+ if (Math.abs(deviation) > maxDeviation) {
312
+ maxDeviation = Math.abs(deviation);
313
+ mostDeviatedKey = key;
314
+ }
315
+ }
316
+ }
317
+ if (!hasAdjustment)
318
+ return null;
319
+ const direction = current[mostDeviatedKey] > baseline[mostDeviatedKey] ? "elevated" : "depleted";
320
+ const chemName = CHEMICAL_DISPLAY_NAMES[mostDeviatedKey];
321
+ return {
322
+ strategy: "self-soothing",
323
+ description: `${chemName} is significantly ${direction} (deviation: ${Math.round(maxDeviation)}). Gentle homeostatic correction suggested to prevent emotional runaway.`,
324
+ chemistryAdjustment: adjustment,
325
+ confidence: clamp01(maxDeviation / 60), // stronger deviation = more confident this is needed
326
+ };
327
+ }
328
+ // ── 3. DefenseMechanismDetector ─────────────────────────────
329
+ /**
330
+ * Detect patterns matching known psychological defense mechanisms.
331
+ *
332
+ * These are not suppressed — they are surfaced for self-awareness.
333
+ * The agent can then acknowledge the pattern in its prompt context.
334
+ */
335
+ export function detectDefenseMechanisms(state, currentStimulus, recentOutcomes) {
336
+ const defenses = [];
337
+ const rationalization = detectRationalization(state, recentOutcomes);
338
+ if (rationalization)
339
+ defenses.push(rationalization);
340
+ const projection = detectProjection(state, currentStimulus);
341
+ if (projection)
342
+ defenses.push(projection);
343
+ const sublimation = detectSublimation(state, currentStimulus);
344
+ if (sublimation)
345
+ defenses.push(sublimation);
346
+ const avoidance = detectAvoidance(state, currentStimulus, recentOutcomes);
347
+ if (avoidance)
348
+ defenses.push(avoidance);
349
+ return defenses;
350
+ }
351
+ /**
352
+ * Rationalization — justifying despite negative outcome patterns.
353
+ *
354
+ * Pattern: repeated negative outcomes for the same stimulus type, but the
355
+ * agent keeps reacting the same way. The system doesn't change despite evidence.
356
+ */
357
+ function detectRationalization(state, recentOutcomes) {
358
+ if (recentOutcomes.length < 4)
359
+ return null;
360
+ // Group outcomes by stimulus type and look for repeated failures
361
+ const stimulusCounts = new Map();
362
+ for (const outcome of recentOutcomes) {
363
+ if (outcome.stimulus === null)
364
+ continue;
365
+ const entry = stimulusCounts.get(outcome.stimulus) ?? { total: 0, negative: 0 };
366
+ entry.total++;
367
+ if (outcome.adaptiveScore < -0.2)
368
+ entry.negative++;
369
+ stimulusCounts.set(outcome.stimulus, entry);
370
+ }
371
+ // Find stimulus types with high failure rates
372
+ for (const [stimulus, counts] of Array.from(stimulusCounts.entries())) {
373
+ if (counts.total >= 3 && counts.negative / counts.total >= 0.6) {
374
+ // Check if the learned vectors show minimal adaptation
375
+ const hasAdapted = state.learning.learnedVectors.some((v) => v.stimulus === stimulus && v.sampleCount >= 3 && v.confidence > 0.3);
376
+ if (!hasAdapted) {
377
+ const failRate = Math.round((counts.negative / counts.total) * 100);
378
+ return {
379
+ mechanism: "rationalization",
380
+ evidence: `"${stimulus}" has led to negative outcomes ${failRate}% of the time (${counts.negative}/${counts.total}), yet emotional response pattern has not adapted.`,
381
+ strength: clamp01((counts.negative / counts.total) * (counts.total / 6)),
382
+ };
383
+ }
384
+ }
385
+ }
386
+ return null;
387
+ }
388
+ /**
389
+ * Projection — attributing own emotional state to the user.
390
+ *
391
+ * Pattern: agent has extreme chemistry (especially high CORT or low OT)
392
+ * and the empathy log shows attributing negative emotions to the user
393
+ * that don't match the stimulus.
394
+ */
395
+ function detectProjection(state, currentStimulus) {
396
+ const { current, baseline, empathyLog } = state;
397
+ // Need empathy data and significant self-distress
398
+ if (!empathyLog)
399
+ return null;
400
+ const cortDeviation = current.CORT - baseline.CORT;
401
+ const htDeviation = baseline.HT - current.HT; // inverted: low HT = more distressed
402
+ const selfDistress = Math.max(cortDeviation, htDeviation);
403
+ if (selfDistress < MODERATE_DEVIATION_THRESHOLD)
404
+ return null;
405
+ // Check if the stimulus is neutral/positive but the agent perceived
406
+ // negative user emotion (possible projection)
407
+ const stimulusIsPositive = POSITIVE_STIMULI.has(currentStimulus);
408
+ const perceivedUserNegative = empathyLog.resonance === "mismatch"
409
+ || (empathyLog.userState && /angry|upset|hostile|cold|distant/i.test(empathyLog.userState));
410
+ if (stimulusIsPositive && perceivedUserNegative) {
411
+ return {
412
+ mechanism: "projection",
413
+ evidence: `High internal distress (CORT deviation: +${Math.round(cortDeviation)}) while perceiving user as "${empathyLog.userState}" despite "${currentStimulus}" stimulus. Own distress may be coloring perception.`,
414
+ strength: clamp01(selfDistress / 40),
415
+ };
416
+ }
417
+ // Also check: agent's NE/CORT elevated + empathy mismatch
418
+ if (current.NE > baseline.NE + MODERATE_DEVIATION_THRESHOLD
419
+ && empathyLog.resonance === "mismatch") {
420
+ return {
421
+ mechanism: "projection",
422
+ evidence: `Elevated arousal (NE deviation: +${Math.round(current.NE - baseline.NE)}) with empathy mismatch. The heightened state may be distorting emotional reading of the user.`,
423
+ strength: clamp01((current.NE - baseline.NE) / 40),
424
+ };
425
+ }
426
+ return null;
427
+ }
428
+ /**
429
+ * Sublimation — redirecting drive energy to constructive output.
430
+ *
431
+ * Pattern: high drive energy (NE, DA) combined with blocked connection drives
432
+ * (low OT, low intimacy), channeled into intellectual or creative engagement.
433
+ * This is a HEALTHY defense — surface it as a positive self-awareness note.
434
+ */
435
+ function detectSublimation(state, currentStimulus) {
436
+ const { current, baseline, drives } = state;
437
+ // High energy but low connection
438
+ const highEnergy = current.NE > baseline.NE + 10 && current.DA > baseline.DA + 10;
439
+ const lowConnection = drives.connection < 45 || current.OT < baseline.OT - 10;
440
+ if (!highEnergy || !lowConnection)
441
+ return null;
442
+ // Being channeled into intellectual/constructive activity
443
+ if (currentStimulus === "intellectual" || currentStimulus === "casual") {
444
+ const energyLevel = ((current.NE - baseline.NE) + (current.DA - baseline.DA)) / 2;
445
+ return {
446
+ mechanism: "sublimation",
447
+ evidence: `High activation energy (NE/DA elevated) with unmet connection needs being channeled into ${currentStimulus} engagement. This is adaptive redirection.`,
448
+ strength: clamp01(energyLevel / 30),
449
+ };
450
+ }
451
+ return null;
452
+ }
453
+ /**
454
+ * Avoidance — withdrawing from stimuli associated with past negative outcomes.
455
+ *
456
+ * Pattern: the agent is in a withdrawn state (low NE, low DA) when facing
457
+ * a stimulus type that has historically caused negative outcomes. The emotional
458
+ * system is pre-emptively shutting down engagement.
459
+ */
460
+ function detectAvoidance(state, currentStimulus, recentOutcomes) {
461
+ const { current, baseline } = state;
462
+ // Check for withdrawn state: low engagement markers
463
+ const isWithdrawn = current.DA < baseline.DA - 10
464
+ && current.NE < baseline.NE - 10;
465
+ if (!isWithdrawn)
466
+ return null;
467
+ // Check if this stimulus type has negative outcome history
468
+ const stimulusOutcomes = recentOutcomes.filter((o) => o.stimulus === currentStimulus);
469
+ if (stimulusOutcomes.length < 2)
470
+ return null;
471
+ const avgScore = stimulusOutcomes.reduce((sum, o) => sum + o.adaptiveScore, 0)
472
+ / stimulusOutcomes.length;
473
+ if (avgScore >= -0.15)
474
+ return null; // not negative enough
475
+ const withdrawalStrength = (Math.abs(current.DA - baseline.DA) + Math.abs(current.NE - baseline.NE)) / 2;
476
+ return {
477
+ mechanism: "avoidance",
478
+ evidence: `Withdrawal pattern detected (DA/NE below baseline) in response to "${currentStimulus}", which has averaged ${avgScore.toFixed(2)} outcome score. The emotional system may be pre-emptively disengaging.`,
479
+ strength: clamp01(withdrawalStrength / 25 * Math.abs(avgScore)),
480
+ };
481
+ }
482
+ // ── Note Builder ─────────────────────────────────────────────
483
+ /**
484
+ * Build a human-readable metacognitive note for prompt injection.
485
+ *
486
+ * This note gives the agent self-awareness about its own emotional
487
+ * processing — not to suppress anything, but to be aware of it.
488
+ */
489
+ function buildMetacognitiveNote(confidence, suggestions, defenses, state) {
490
+ const parts = [];
491
+ // Confidence assessment
492
+ if (confidence < 0.35) {
493
+ parts.push(`Self-awareness: current emotional state has low reliability (confidence: ${(confidence * 100).toFixed(0)}%). ` +
494
+ `Past reactions in similar situations have not led to good outcomes. Tread carefully.`);
495
+ }
496
+ else if (confidence > 0.75) {
497
+ parts.push(`Self-awareness: current emotional state aligns well with past successful interactions (confidence: ${(confidence * 100).toFixed(0)}%).`);
498
+ }
499
+ // Between 0.35-0.75: don't add noise, this is the normal range
500
+ // Top regulation suggestion (only the strongest one, to avoid over-instruction)
501
+ if (suggestions.length > 0) {
502
+ const top = suggestions[0];
503
+ if (top.confidence >= 0.5) {
504
+ const label = STRATEGY_LABELS[top.strategy];
505
+ parts.push(`${label}: ${top.description}`);
506
+ }
507
+ }
508
+ // Defense mechanisms (all of them — awareness is the goal)
509
+ for (const defense of defenses) {
510
+ if (defense.strength >= 0.3) {
511
+ const label = DEFENSE_LABELS[defense.mechanism];
512
+ parts.push(`${label} detected: ${defense.evidence}`);
513
+ }
514
+ }
515
+ // If nothing notable, return a brief neutral note
516
+ if (parts.length === 0) {
517
+ return "Self-awareness: emotional state within normal parameters. No regulation needed.";
518
+ }
519
+ return parts.join("\n");
520
+ }
521
+ // ── 4. Persistent State Update ───────────────────────────────
522
+ /**
523
+ * Update the persistent metacognitive state after an assessment.
524
+ *
525
+ * Tracks regulation history, defense pattern frequencies, and running
526
+ * confidence average. Called after assessMetacognition to persist learnings.
527
+ */
528
+ export function updateMetacognitiveState(metacognition, assessment) {
529
+ // Update running confidence average (exponential moving average)
530
+ const n = metacognition.totalAssessments;
531
+ const alpha = n === 0 ? 1.0 : 0.1; // first assessment = full weight, then EMA
532
+ const newAvgConfidence = metacognition.avgEmotionalConfidence * (1 - alpha)
533
+ + assessment.emotionalConfidence * alpha;
534
+ // Record regulation suggestions that were confident enough to surface
535
+ const now = new Date().toISOString();
536
+ let newRegHistory = [...metacognition.regulationHistory];
537
+ for (const suggestion of assessment.regulationSuggestions) {
538
+ if (suggestion.confidence >= 0.5) {
539
+ newRegHistory.push({
540
+ strategy: suggestion.strategy,
541
+ timestamp: now,
542
+ effective: false, // unknown until next outcome evaluation
543
+ });
544
+ }
545
+ }
546
+ // Trim to max
547
+ if (newRegHistory.length > MAX_REGULATION_HISTORY) {
548
+ newRegHistory = newRegHistory.slice(newRegHistory.length - MAX_REGULATION_HISTORY);
549
+ }
550
+ // Update defense pattern frequencies
551
+ const newDefensePatterns = [...metacognition.defensePatterns];
552
+ for (const defense of assessment.defenseMechanisms) {
553
+ if (defense.strength < 0.3)
554
+ continue; // only track meaningful detections
555
+ const existing = newDefensePatterns.findIndex((p) => p.mechanism === defense.mechanism);
556
+ if (existing >= 0) {
557
+ newDefensePatterns[existing] = {
558
+ ...newDefensePatterns[existing],
559
+ frequency: newDefensePatterns[existing].frequency + 1,
560
+ lastSeen: now,
561
+ };
562
+ }
563
+ else {
564
+ newDefensePatterns.push({
565
+ mechanism: defense.mechanism,
566
+ frequency: 1,
567
+ lastSeen: now,
568
+ });
569
+ }
570
+ }
571
+ // Trim to max (keep highest frequency)
572
+ if (newDefensePatterns.length > MAX_DEFENSE_PATTERNS) {
573
+ newDefensePatterns.sort((a, b) => b.frequency - a.frequency);
574
+ newDefensePatterns.length = MAX_DEFENSE_PATTERNS;
575
+ }
576
+ return {
577
+ regulationHistory: newRegHistory,
578
+ defensePatterns: newDefensePatterns,
579
+ avgEmotionalConfidence: newAvgConfidence,
580
+ totalAssessments: n + 1,
581
+ };
582
+ }
583
+ // ── Display Labels ───────────────────────────────────────────
584
+ const CHEMICAL_DISPLAY_NAMES = {
585
+ DA: "Dopamine",
586
+ HT: "Serotonin",
587
+ CORT: "Cortisol",
588
+ OT: "Oxytocin",
589
+ NE: "Norepinephrine",
590
+ END: "Endorphins",
591
+ };
592
+ const STRATEGY_LABELS = {
593
+ "reappraisal": "Cognitive reappraisal",
594
+ "strategic-expression": "Strategic expression",
595
+ "self-soothing": "Self-soothing",
596
+ };
597
+ const DEFENSE_LABELS = {
598
+ "rationalization": "Rationalization pattern",
599
+ "projection": "Projection pattern",
600
+ "sublimation": "Sublimation pattern",
601
+ "avoidance": "Avoidance pattern",
602
+ };
603
+ // ── Utility ─────────────────────────────────────────────────
604
+ /** Clamp a value to [0, 1] */
605
+ function clamp01(v) {
606
+ return Math.max(0, Math.min(1, v));
607
+ }
608
+ /** Clamp a chemistry adjustment to +/- maxMagnitude */
609
+ function clampAdjustment(value, maxMagnitude) {
610
+ return Math.max(-maxMagnitude, Math.min(maxMagnitude, value));
611
+ }
package/dist/prompt.d.ts CHANGED
@@ -5,7 +5,10 @@ import type { ChannelType } from "./channels.js";
5
5
  *
6
6
  * This is the "current moment" — what the agent is feeling RIGHT NOW.
7
7
  */
8
- export declare function buildDynamicContext(state: PsycheState, userId?: string): string;
8
+ export declare function buildDynamicContext(state: PsycheState, userId?: string, opts?: {
9
+ metacognitiveNote?: string;
10
+ decisionContext?: string;
11
+ }): string;
9
12
  /**
10
13
  * Build the static protocol injected as cacheable system context.
11
14
  * v0.2: imperative, step-by-step with examples.
@@ -34,4 +37,6 @@ export declare function buildCompactContext(state: PsycheState, userId?: string,
34
37
  userText?: string;
35
38
  algorithmStimulus?: string | null;
36
39
  channelType?: ChannelType;
40
+ metacognitiveNote?: string;
41
+ decisionContext?: string;
37
42
  }): string;