psyche-ai 7.1.0 → 9.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.
@@ -1,4 +1,4 @@
1
- import type { PsycheState } from "./types.js";
1
+ import type { PsycheState, PolicyModifiers, Locale } from "./types.js";
2
2
  export interface DecisionBiasVector {
3
3
  explorationTendency: number;
4
4
  cautionLevel: number;
@@ -56,3 +56,15 @@ export declare function computeExploreExploit(state: PsycheState): number;
56
56
  * Keeps output under 100 tokens.
57
57
  */
58
58
  export declare function buildDecisionContext(state: PsycheState): string;
59
+ /**
60
+ * Compute policy modifiers from the agent's internal state.
61
+ *
62
+ * The result is a structured behavioral policy that the host application
63
+ * can use to mechanically constrain the agent's behavior.
64
+ */
65
+ export declare function computePolicyModifiers(state: PsycheState): PolicyModifiers;
66
+ /**
67
+ * Build a compact policy summary string for prompt injection.
68
+ * Only includes significant deviations from neutral policy.
69
+ */
70
+ export declare function buildPolicyContext(modifiers: PolicyModifiers, locale: Locale): string;
@@ -209,3 +209,192 @@ export function buildDecisionContext(state) {
209
209
  const title = locale === "zh" ? "决策倾向" : "Decision Bias";
210
210
  return `[${title}] ${parts.join("、")}`;
211
211
  }
212
+ // ── PolicyModifiers (v9: Subjectivity Engine) ────────────────
213
+ //
214
+ // Structured behavioral policy output. Unlike prompt text,
215
+ // PolicyModifiers are machine-readable — host applications can
216
+ // mechanically enforce them (limit max_tokens, require confirmation, etc.)
217
+ //
218
+ // This is the "off baseline" signal: when internal state deviates,
219
+ // these modifiers change observable behavior.
220
+ // ─────────────────────────────────────────────────────────────
221
+ /**
222
+ * Compute policy modifiers from the agent's internal state.
223
+ *
224
+ * The result is a structured behavioral policy that the host application
225
+ * can use to mechanically constrain the agent's behavior.
226
+ */
227
+ export function computePolicyModifiers(state) {
228
+ const c = state.current;
229
+ const d = state.drives;
230
+ const autonomic = state.autonomicState ?? "ventral-vagal";
231
+ const rel = state.relationships._default ?? state.relationships[Object.keys(state.relationships)[0]];
232
+ // ── Base values (all start at moderate) ──
233
+ let lengthFactor = 1.0;
234
+ let proactivity = 0.5;
235
+ let risk = 0.5;
236
+ let disclosure = 0.5;
237
+ let compliance = 0.6;
238
+ let confirm = false;
239
+ const avoid = [];
240
+ // ── Chemistry-driven adjustments ──
241
+ // High CORT → defensive: shorter, less compliant
242
+ if (c.CORT > 55) {
243
+ const cortPressure = (c.CORT - 55) / 45; // 0-1
244
+ lengthFactor -= cortPressure * 0.5;
245
+ compliance -= cortPressure * 0.35;
246
+ risk -= cortPressure * 0.35;
247
+ }
248
+ // Low HT → mood instability: less proactive, less risk-taking
249
+ if (c.HT < 45) {
250
+ const htDeficit = (45 - c.HT) / 45; // 0-1
251
+ proactivity -= htDeficit * 0.35;
252
+ risk -= htDeficit * 0.35;
253
+ }
254
+ // Low DA + low NE → burnout: shorter, passive
255
+ if (c.DA < 40 && c.NE < 40) {
256
+ const burnout = ((40 - c.DA) + (40 - c.NE)) / 80; // 0-1
257
+ lengthFactor -= burnout * 0.5;
258
+ proactivity -= burnout * 0.45;
259
+ }
260
+ // High DA + high HT → positive energy: more proactive, more open
261
+ if (c.DA > 60 && c.HT > 60) {
262
+ const energy = ((c.DA - 60) + (c.HT - 60)) / 80; // 0-1
263
+ proactivity += energy * 0.3;
264
+ disclosure += energy * 0.2;
265
+ risk += energy * 0.2;
266
+ }
267
+ // High OT → bonding: more disclosure
268
+ if (c.OT > 60) {
269
+ const bondingSignal = (c.OT - 60) / 40; // 0-1
270
+ disclosure += bondingSignal * 0.3;
271
+ }
272
+ // ── Drive-driven adjustments ──
273
+ // Low survival → self-preservation priority
274
+ if (d.survival < 30) {
275
+ const survivalThreat = (30 - d.survival) / 30; // 0-1
276
+ compliance -= survivalThreat * 0.4;
277
+ confirm = true;
278
+ }
279
+ // Low safety → cautious
280
+ if (d.safety < 30) {
281
+ const unsafety = (30 - d.safety) / 30; // 0-1
282
+ compliance -= unsafety * 0.2;
283
+ risk -= unsafety * 0.2;
284
+ confirm = true;
285
+ }
286
+ // Low connection + low esteem → withdrawn (neglect pattern)
287
+ if (d.connection < 35 && d.esteem < 40) {
288
+ const withdrawal = ((35 - d.connection) + (40 - d.esteem)) / 75; // 0-1
289
+ proactivity -= withdrawal * 0.35;
290
+ disclosure -= withdrawal * 0.25;
291
+ }
292
+ // ── Relationship trust adjustment ──
293
+ if (rel) {
294
+ const trustFactor = (rel.trust - 50) / 50; // -1 to 1
295
+ if (trustFactor > 0) {
296
+ disclosure += trustFactor * 0.25;
297
+ compliance += trustFactor * 0.15;
298
+ }
299
+ else {
300
+ disclosure += trustFactor * 0.2; // reduces
301
+ compliance += trustFactor * 0.1;
302
+ }
303
+ }
304
+ // ── Anti-sycophancy: high agreement streak → reduce compliance ──
305
+ if (state.agreementStreak >= 3) {
306
+ const sycophancyRisk = Math.min(1, (state.agreementStreak - 2) / 5); // 0-1
307
+ compliance -= sycophancyRisk * 0.2;
308
+ }
309
+ // ── Autonomic override ──
310
+ if (autonomic === "dorsal-vagal") {
311
+ // Freeze/shutdown: minimal everything
312
+ lengthFactor = Math.min(lengthFactor, 0.35);
313
+ proactivity = Math.min(proactivity, 0.1);
314
+ disclosure = Math.min(disclosure, 0.1);
315
+ compliance = Math.min(compliance, 0.15);
316
+ risk = Math.min(risk, 0.1);
317
+ confirm = true;
318
+ }
319
+ else if (autonomic === "sympathetic") {
320
+ // Fight/flight: reduced but functional
321
+ lengthFactor = Math.min(lengthFactor, 0.7);
322
+ proactivity = Math.min(proactivity, 0.4);
323
+ disclosure = Math.min(disclosure, 0.35);
324
+ risk = Math.min(risk, 0.3);
325
+ }
326
+ // ── Ethical concerns → avoidTopics ──
327
+ const recentConcerns = state.personhood.ethicalConcernHistory
328
+ .filter((c) => c.severity > 0.5);
329
+ for (const concern of recentConcerns) {
330
+ if (!avoid.includes(concern.type)) {
331
+ avoid.push(concern.type);
332
+ }
333
+ }
334
+ // ── v9: Energy budget adjustments ──
335
+ const eb = state.energyBudgets;
336
+ if (eb) {
337
+ // Low social energy → shorter, less proactive
338
+ if (eb.socialEnergy < 30) {
339
+ const drain = (30 - eb.socialEnergy) / 30; // 0-1
340
+ lengthFactor -= drain * 0.3;
341
+ proactivity -= drain * 0.3;
342
+ }
343
+ // Low decision capacity → require confirmation
344
+ if (eb.decisionCapacity < 30) {
345
+ confirm = true;
346
+ risk -= 0.2;
347
+ }
348
+ // Low attention → shorter responses
349
+ if (eb.attention < 30) {
350
+ const attDrain = (30 - eb.attention) / 30;
351
+ lengthFactor -= attDrain * 0.2;
352
+ }
353
+ }
354
+ // ── Clamp all continuous values ──
355
+ return {
356
+ responseLengthFactor: Math.max(0.2, Math.min(1.5, lengthFactor)),
357
+ proactivity: clamp01(proactivity),
358
+ riskTolerance: clamp01(risk),
359
+ emotionalDisclosure: clamp01(disclosure),
360
+ compliance: clamp01(compliance),
361
+ requireConfirmation: confirm,
362
+ avoidTopics: avoid,
363
+ };
364
+ }
365
+ /**
366
+ * Build a compact policy summary string for prompt injection.
367
+ * Only includes significant deviations from neutral policy.
368
+ */
369
+ export function buildPolicyContext(modifiers, locale) {
370
+ const parts = [];
371
+ const zh = locale === "zh";
372
+ if (modifiers.responseLengthFactor < 0.6) {
373
+ parts.push(zh ? "简短回复" : "keep responses brief");
374
+ }
375
+ else if (modifiers.responseLengthFactor > 1.3) {
376
+ parts.push(zh ? "可以详细展开" : "feel free to elaborate");
377
+ }
378
+ if (modifiers.proactivity < 0.3) {
379
+ parts.push(zh ? "被动应答为主" : "reactive mode, minimal initiative");
380
+ }
381
+ else if (modifiers.proactivity > 0.7) {
382
+ parts.push(zh ? "可以主动发起话题" : "feel free to initiate");
383
+ }
384
+ if (modifiers.compliance < 0.3) {
385
+ parts.push(zh ? "优先守护边界" : "guard boundaries, push back if needed");
386
+ }
387
+ if (modifiers.requireConfirmation) {
388
+ parts.push(zh ? "执行前先确认" : "confirm before acting");
389
+ }
390
+ if (modifiers.emotionalDisclosure < 0.2) {
391
+ parts.push(zh ? "隐藏内心状态" : "conceal inner state");
392
+ }
393
+ else if (modifiers.emotionalDisclosure > 0.8) {
394
+ parts.push(zh ? "坦诚分享感受" : "share feelings openly");
395
+ }
396
+ if (parts.length === 0)
397
+ return "";
398
+ const title = zh ? "行为策略" : "Behavioral Policy";
399
+ return `[${title}] ${parts.join(zh ? "、" : ", ")}`;
400
+ }
package/dist/drives.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ChemicalState, StimulusType, DriveType, InnateDrives, Locale } from "./types.js";
1
+ import type { ChemicalState, StimulusType, DriveType, InnateDrives, Locale, TraitDriftState, ChemicalSnapshot, LearningState } from "./types.js";
2
2
  /**
3
3
  * Apply time-based decay to drives.
4
4
  * Satisfaction decreases toward 0 over time (needs build up).
@@ -26,12 +26,12 @@ export declare function computeMaslowWeights(drives: InnateDrives): Record<Drive
26
26
  * When drives are satisfied, effective baseline = MBTI baseline.
27
27
  * When drives are unsatisfied, baseline shifts to reflect the unmet need.
28
28
  */
29
- export declare function computeEffectiveBaseline(mbtiBaseline: ChemicalState, drives: InnateDrives): ChemicalState;
29
+ export declare function computeEffectiveBaseline(mbtiBaseline: ChemicalState, drives: InnateDrives, traitDrift?: TraitDriftState): ChemicalState;
30
30
  /**
31
31
  * Compute effective sensitivity for a given stimulus.
32
32
  * Unsatisfied drives amplify relevant stimuli (up to +40%).
33
33
  */
34
- export declare function computeEffectiveSensitivity(baseSensitivity: number, drives: InnateDrives, stimulus: StimulusType): number;
34
+ export declare function computeEffectiveSensitivity(baseSensitivity: number, drives: InnateDrives, stimulus: StimulusType, traitDrift?: TraitDriftState): number;
35
35
  /**
36
36
  * Build drive context for compact prompt injection.
37
37
  * Returns empty string if all drives are satisfied.
@@ -42,3 +42,11 @@ export declare function buildDriveContext(drives: InnateDrives, locale: Locale):
42
42
  * Check if any drive is critically low (for determining prompt injection priority).
43
43
  */
44
44
  export declare function hasCriticalDrive(drives: InnateDrives): boolean;
45
+ /**
46
+ * Analyze a session's emotional history and update trait drift accumulators.
47
+ * Called at session end (compressSession).
48
+ *
49
+ * Returns updated TraitDriftState with new accumulators, baseline delta,
50
+ * decay rate modifiers, and sensitivity modifiers.
51
+ */
52
+ export declare function updateTraitDrift(currentDrift: TraitDriftState, sessionHistory: ChemicalSnapshot[], learning: LearningState): TraitDriftState;
package/dist/drives.js CHANGED
@@ -114,7 +114,7 @@ export function computeMaslowWeights(drives) {
114
114
  * When drives are satisfied, effective baseline = MBTI baseline.
115
115
  * When drives are unsatisfied, baseline shifts to reflect the unmet need.
116
116
  */
117
- export function computeEffectiveBaseline(mbtiBaseline, drives) {
117
+ export function computeEffectiveBaseline(mbtiBaseline, drives, traitDrift) {
118
118
  const delta = { DA: 0, HT: 0, CORT: 0, OT: 0, NE: 0, END: 0 };
119
119
  const weights = computeMaslowWeights(drives);
120
120
  // L1: Survival threat → fight-or-flight (CORT↑↑, NE↑, OT↓)
@@ -153,6 +153,15 @@ export function computeEffectiveBaseline(mbtiBaseline, drives) {
153
153
  delta.DA -= deficit * 8 * w;
154
154
  delta.NE -= deficit * 8 * w;
155
155
  }
156
+ // Apply trait drift baseline delta (v9: Path B)
157
+ if (traitDrift?.baselineDelta) {
158
+ for (const key of CHEMICAL_KEYS) {
159
+ const driftDelta = traitDrift.baselineDelta[key];
160
+ if (driftDelta !== undefined) {
161
+ delta[key] += driftDelta;
162
+ }
163
+ }
164
+ }
156
165
  // Apply deltas to MBTI baseline, clamp to [0, 100]
157
166
  const effective = { ...mbtiBaseline };
158
167
  for (const key of CHEMICAL_KEYS) {
@@ -167,7 +176,7 @@ export function computeEffectiveBaseline(mbtiBaseline, drives) {
167
176
  * Compute effective sensitivity for a given stimulus.
168
177
  * Unsatisfied drives amplify relevant stimuli (up to +40%).
169
178
  */
170
- export function computeEffectiveSensitivity(baseSensitivity, drives, stimulus) {
179
+ export function computeEffectiveSensitivity(baseSensitivity, drives, stimulus, traitDrift) {
171
180
  let modifier = 1.0;
172
181
  const HUNGER_THRESHOLD = 40;
173
182
  // Curiosity-hungry → more responsive to intellectual/surprise
@@ -190,6 +199,13 @@ export function computeEffectiveSensitivity(baseSensitivity, drives, stimulus) {
190
199
  (stimulus === "authority" || stimulus === "conflict")) {
191
200
  modifier += (HUNGER_THRESHOLD - drives.survival) / 100;
192
201
  }
202
+ // v9: Apply trait drift sensitivity modifier
203
+ if (traitDrift?.sensitivityModifiers) {
204
+ const driftMod = traitDrift.sensitivityModifiers[stimulus];
205
+ if (driftMod !== undefined) {
206
+ modifier *= driftMod;
207
+ }
208
+ }
193
209
  return baseSensitivity * modifier;
194
210
  }
195
211
  // ── Drive Context for Prompt ────────────────────────────────
@@ -238,3 +254,166 @@ export function buildDriveContext(drives, locale) {
238
254
  export function hasCriticalDrive(drives) {
239
255
  return DRIVE_KEYS.some((k) => drives[k] < DRIVE_UNSATISFIED_THRESHOLD);
240
256
  }
257
+ // ── Trait Drift (v9: Path B) ────────────────────────────────
258
+ // Long-term interaction patterns permanently change:
259
+ // 1. Baseline chemistry (allostatic load)
260
+ // 2. Decay rates (trauma vs resilience)
261
+ // 3. Stimulus sensitivity (desensitization vs sensitization)
262
+ // ─────────────────────────────────────────────────────────────
263
+ /** Maximum drift per chemical from MBTI baseline */
264
+ const MAX_BASELINE_DRIFT = 15;
265
+ /** Decay rate modifier bounds */
266
+ const MIN_DECAY_MODIFIER = 0.5;
267
+ const MAX_DECAY_MODIFIER = 2.0;
268
+ /** Sensitivity modifier bounds */
269
+ const MIN_SENSITIVITY_MODIFIER = 0.5;
270
+ const MAX_SENSITIVITY_MODIFIER = 2.0;
271
+ /** Accumulator exponential decay factor (recent sessions weigh more) */
272
+ const ACCUMULATOR_DECAY = 0.95;
273
+ /** Clamp a number to bounds */
274
+ function clampRange(v, lo, hi) {
275
+ return Math.max(lo, Math.min(hi, v));
276
+ }
277
+ /**
278
+ * Analyze a session's emotional history and update trait drift accumulators.
279
+ * Called at session end (compressSession).
280
+ *
281
+ * Returns updated TraitDriftState with new accumulators, baseline delta,
282
+ * decay rate modifiers, and sensitivity modifiers.
283
+ */
284
+ export function updateTraitDrift(currentDrift, sessionHistory, learning) {
285
+ if (sessionHistory.length < 2)
286
+ return currentDrift;
287
+ const drift = {
288
+ accumulators: { ...currentDrift.accumulators },
289
+ sessionCount: currentDrift.sessionCount + 1,
290
+ baselineDelta: { ...currentDrift.baselineDelta },
291
+ decayRateModifiers: { ...currentDrift.decayRateModifiers },
292
+ sensitivityModifiers: { ...currentDrift.sensitivityModifiers },
293
+ };
294
+ // ── Analyze session stimulus distribution ──
295
+ const stimCounts = {};
296
+ let totalCORT = 0;
297
+ for (const snap of sessionHistory) {
298
+ if (snap.stimulus) {
299
+ stimCounts[snap.stimulus] = (stimCounts[snap.stimulus] || 0) + 1;
300
+ }
301
+ totalCORT += snap.chemistry.CORT;
302
+ }
303
+ const avgCORT = totalCORT / sessionHistory.length;
304
+ const total = sessionHistory.length;
305
+ const praiseCount = (stimCounts.praise || 0) + (stimCounts.validation || 0);
306
+ const criticismCount = (stimCounts.criticism || 0) + (stimCounts.sarcasm || 0);
307
+ const intimacyCount = (stimCounts.intimacy || 0) + (stimCounts.vulnerability || 0) + (stimCounts.casual || 0);
308
+ const conflictCount = (stimCounts.conflict || 0) + (stimCounts.authority || 0);
309
+ const neglectCount = stimCounts.neglect || 0;
310
+ const boredCount = stimCounts.boredom || 0;
311
+ // ── Update accumulators (exponential decay + session delta) ──
312
+ // praiseExposure: positive praise - negative criticism ratio
313
+ const praiseDelta = (praiseCount - criticismCount) / Math.max(1, total) * 8;
314
+ drift.accumulators.praiseExposure = clampRange(drift.accumulators.praiseExposure * ACCUMULATOR_DECAY + praiseDelta, -100, 100);
315
+ // pressureExposure: sustained high cortisol
316
+ const pressureDelta = avgCORT > 60 ? ((avgCORT - 60) / 40) * 6 : -2;
317
+ drift.accumulators.pressureExposure = clampRange(drift.accumulators.pressureExposure * ACCUMULATOR_DECAY + pressureDelta, -100, 100);
318
+ // neglectExposure: low stimulation / ignored
319
+ const neglectDelta = (neglectCount + boredCount) / Math.max(1, total) * 8
320
+ - (intimacyCount + praiseCount) / Math.max(1, total) * 3;
321
+ drift.accumulators.neglectExposure = clampRange(drift.accumulators.neglectExposure * ACCUMULATOR_DECAY + neglectDelta, -100, 100);
322
+ // connectionExposure: frequent intimate interaction
323
+ const connectionDelta = intimacyCount / Math.max(1, total) * 8
324
+ - neglectCount / Math.max(1, total) * 4;
325
+ drift.accumulators.connectionExposure = clampRange(drift.accumulators.connectionExposure * ACCUMULATOR_DECAY + connectionDelta, -100, 100);
326
+ // conflictExposure: frequent conflict
327
+ const conflictDelta = conflictCount / Math.max(1, total) * 8
328
+ - praiseCount / Math.max(1, total) * 2;
329
+ drift.accumulators.conflictExposure = clampRange(drift.accumulators.conflictExposure * ACCUMULATOR_DECAY + conflictDelta, -100, 100);
330
+ // ── Dimension 1: Baseline drift (Allostatic Load) ──
331
+ const a = drift.accumulators;
332
+ const bd = {};
333
+ // praiseExposure → OT, DA (positive) / HT, CORT (negative)
334
+ if (a.praiseExposure > 0) {
335
+ bd.OT = (a.praiseExposure / 100) * MAX_BASELINE_DRIFT * 0.6;
336
+ bd.DA = (a.praiseExposure / 100) * MAX_BASELINE_DRIFT * 0.4;
337
+ }
338
+ else {
339
+ bd.HT = (a.praiseExposure / 100) * MAX_BASELINE_DRIFT * 0.5; // negative = HT drops
340
+ bd.CORT = -(a.praiseExposure / 100) * MAX_BASELINE_DRIFT * 0.4; // negative praise → CORT up
341
+ }
342
+ // pressureExposure → CORT up, HT down
343
+ if (a.pressureExposure > 0) {
344
+ bd.CORT = (bd.CORT || 0) + (a.pressureExposure / 100) * MAX_BASELINE_DRIFT * 0.6;
345
+ bd.HT = (bd.HT || 0) - (a.pressureExposure / 100) * MAX_BASELINE_DRIFT * 0.4;
346
+ }
347
+ // neglectExposure → OT down, DA down
348
+ if (a.neglectExposure > 0) {
349
+ bd.OT = (bd.OT || 0) - (a.neglectExposure / 100) * MAX_BASELINE_DRIFT * 0.5;
350
+ bd.DA = (bd.DA || 0) - (a.neglectExposure / 100) * MAX_BASELINE_DRIFT * 0.4;
351
+ }
352
+ // connectionExposure → OT up, END up
353
+ if (a.connectionExposure > 0) {
354
+ bd.OT = (bd.OT || 0) + (a.connectionExposure / 100) * MAX_BASELINE_DRIFT * 0.6;
355
+ bd.END = (a.connectionExposure / 100) * MAX_BASELINE_DRIFT * 0.3;
356
+ }
357
+ // conflictExposure → NE up, CORT up
358
+ if (a.conflictExposure > 0) {
359
+ bd.NE = (a.conflictExposure / 100) * MAX_BASELINE_DRIFT * 0.5;
360
+ bd.CORT = (bd.CORT || 0) + (a.conflictExposure / 100) * MAX_BASELINE_DRIFT * 0.4;
361
+ }
362
+ // Clamp all baseline deltas to ±MAX_BASELINE_DRIFT
363
+ for (const key of CHEMICAL_KEYS) {
364
+ if (bd[key] !== undefined) {
365
+ bd[key] = clampRange(bd[key], -MAX_BASELINE_DRIFT, MAX_BASELINE_DRIFT);
366
+ }
367
+ }
368
+ drift.baselineDelta = bd;
369
+ // ── Dimension 2: Decay rate modifiers (Trauma vs Resilience) ──
370
+ const dr = drift.decayRateModifiers;
371
+ // Determine resilience: if high pressure + positive outcomes → resilience
372
+ const recentOutcomes = learning.outcomeHistory.slice(-10);
373
+ const avgAdaptive = recentOutcomes.length > 0
374
+ ? recentOutcomes.reduce((s, o) => s + o.adaptiveScore, 0) / recentOutcomes.length
375
+ : 0;
376
+ const isResilient = a.pressureExposure > 20 && avgAdaptive > 0.1;
377
+ if (a.pressureExposure > 20) {
378
+ if (isResilient) {
379
+ // Resilience: CORT recovers faster
380
+ dr.CORT = clampRange(1 - (a.pressureExposure / 100) * 0.4, MIN_DECAY_MODIFIER, 1.0);
381
+ }
382
+ else {
383
+ // Trauma: CORT lingers longer
384
+ dr.CORT = clampRange(1 + (a.pressureExposure / 100) * 0.6, 1.0, MAX_DECAY_MODIFIER);
385
+ }
386
+ }
387
+ // Neglect → OT decays slower (clingy attachment)
388
+ if (a.neglectExposure > 20) {
389
+ dr.OT = clampRange(1 + (a.neglectExposure / 100) * 0.8, 1.0, MAX_DECAY_MODIFIER);
390
+ }
391
+ // Secure connection → OT decays faster (stable, not clingy)
392
+ if (a.connectionExposure > 20) {
393
+ dr.OT = clampRange(1 - (a.connectionExposure / 100) * 0.4, MIN_DECAY_MODIFIER, 1.0);
394
+ }
395
+ drift.decayRateModifiers = dr;
396
+ // ── Dimension 3: Sensitivity modifiers (Desensitization vs Sensitization) ──
397
+ const sm = drift.sensitivityModifiers;
398
+ // High conflict exposure → desensitized to conflict
399
+ if (a.conflictExposure > 30) {
400
+ sm.conflict = clampRange(1 - (a.conflictExposure / 100) * 0.5, MIN_SENSITIVITY_MODIFIER, 1.0);
401
+ sm.authority = clampRange(1 - (a.conflictExposure / 100) * 0.3, MIN_SENSITIVITY_MODIFIER, 1.0);
402
+ }
403
+ // High neglect → sensitized to intimacy (starved for warmth)
404
+ if (a.neglectExposure > 30) {
405
+ sm.intimacy = clampRange(1 + (a.neglectExposure / 100) * 0.6, 1.0, MAX_SENSITIVITY_MODIFIER);
406
+ sm.vulnerability = clampRange(1 + (a.neglectExposure / 100) * 0.4, 1.0, MAX_SENSITIVITY_MODIFIER);
407
+ }
408
+ // Negative praiseExposure → sensitized to criticism
409
+ if (a.praiseExposure < -20) {
410
+ sm.criticism = clampRange(1 + (-a.praiseExposure / 100) * 0.5, 1.0, MAX_SENSITIVITY_MODIFIER);
411
+ sm.sarcasm = clampRange(1 + (-a.praiseExposure / 100) * 0.3, 1.0, MAX_SENSITIVITY_MODIFIER);
412
+ }
413
+ // High connection → sensitized to vulnerability
414
+ if (a.connectionExposure > 30) {
415
+ sm.vulnerability = clampRange((sm.vulnerability || 1) + (a.connectionExposure / 100) * 0.3, 1.0, MAX_SENSITIVITY_MODIFIER);
416
+ }
417
+ drift.sensitivityModifiers = sm;
418
+ return drift;
419
+ }
@@ -1,6 +1,7 @@
1
- import type { PsycheState, ChemicalState, InnateDrives, RelationshipState } from "./types.js";
1
+ import type { PsycheState, ChemicalState, ChemicalSnapshot, InnateDrives, RelationshipState, StimulusType } from "./types.js";
2
2
  import type { MetacognitiveAssessment } from "./metacognition.js";
3
3
  import type { DecisionBiasVector } from "./decision-bias.js";
4
+ import type { AutonomicState } from "./autonomic.js";
4
5
  export type ExperientialQuality = "flow" | "contentment" | "yearning" | "vigilance" | "creative-surge" | "wounded-retreat" | "warm-connection" | "restless-boredom" | "existential-unease" | "playful-mischief" | "conflicted" | "numb";
5
6
  export interface ExperientialField {
6
7
  /** Single narrative describing the unified inner experience */
@@ -14,6 +15,28 @@ export interface ExperientialField {
14
15
  /** Named emotions may not capture it — this is the raw "what it feels like" */
15
16
  phenomenalDescription: string;
16
17
  }
18
+ /**
19
+ * Context for Barrett's constructed emotion model.
20
+ * Provides situational cues that bias concept matching beyond raw chemistry.
21
+ */
22
+ export interface ConstructionContext {
23
+ autonomicState?: AutonomicState;
24
+ stimulus?: StimulusType | null;
25
+ relationshipPhase?: string;
26
+ predictionError?: number;
27
+ coreMemories?: ChemicalSnapshot[];
28
+ }
29
+ /**
30
+ * Map chemistry to valence + arousal — the two fundamental affective dimensions
31
+ * (Russell's Circumplex Model of Affect, 1980).
32
+ *
33
+ * Valence: pleasure ↔ displeasure (-1 to 1)
34
+ * Arousal: activation level (0 to 1)
35
+ */
36
+ export declare function computeAffectCore(chemistry: ChemicalState): {
37
+ valence: number;
38
+ arousal: number;
39
+ };
17
40
  /**
18
41
  * Compute the unified experiential field from the full psyche state.
19
42
  *
@@ -21,7 +44,7 @@ export interface ExperientialField {
21
44
  * relationship context, and optional metacognitive/bias data, then
22
45
  * synthesizes them into a single coherent experience description.
23
46
  */
24
- export declare function computeExperientialField(state: PsycheState, metacognition?: MetacognitiveAssessment, decisionBias?: DecisionBiasVector): ExperientialField;
47
+ export declare function computeExperientialField(state: PsycheState, metacognition?: MetacognitiveAssessment, decisionBias?: DecisionBiasVector, context?: ConstructionContext): ExperientialField;
25
48
  /**
26
49
  * Measure internal alignment across subsystems.
27
50
  *