psyche-ai 3.0.0 → 3.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/README.md CHANGED
@@ -163,6 +163,8 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
163
163
  - **自定义人格** — 超越 MBTI 预设,完全自定义 baseline/敏感度/气质
164
164
  - **情绪学习** — 从交互结果中学习,调整情绪反应参数(躯体标记假说)
165
165
  - **上下文分类** — 关系/驱力/历史感知的刺激分类,超越简单正则
166
+ - **时间意识** — 预期、惊喜/失望、遗憾(马尔可夫预测+反事实分析)
167
+ - **依恋动力学** — 4种依恋风格(安全/焦虑/回避/混乱),分离焦虑,重逢效应
166
168
  - **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
167
169
 
168
170
  架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
@@ -172,7 +174,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
172
174
  ```bash
173
175
  npm install
174
176
  npm run build
175
- npm test # 525 tests
177
+ npm test # 568 tests
176
178
  npm run typecheck # strict mode
177
179
  ```
178
180
 
@@ -0,0 +1,30 @@
1
+ import type { ChemicalState, StimulusType } from "./types.js";
2
+ export type AttachmentStyle = "secure" | "anxious" | "avoidant" | "disorganized";
3
+ export interface AttachmentState {
4
+ style: AttachmentStyle;
5
+ strength: number;
6
+ securityScore: number;
7
+ anxietyScore: number;
8
+ avoidanceScore: number;
9
+ lastInteractionAt: string;
10
+ interactionCount: number;
11
+ }
12
+ export interface SeparationEffect {
13
+ chemistryDelta: Partial<ChemicalState>;
14
+ description: string;
15
+ intensity: number;
16
+ }
17
+ export declare const DEFAULT_ATTACHMENT: AttachmentState;
18
+ /**
19
+ * Update attachment based on interaction outcome.
20
+ */
21
+ export declare function updateAttachment(attachment: AttachmentState, stimulus: StimulusType | null, outcomeScore: number): AttachmentState;
22
+ /**
23
+ * Compute chemistry effects of absence based on attachment.
24
+ * Called when time since last interaction is significant.
25
+ */
26
+ export declare function computeSeparationEffect(attachment: AttachmentState, minutesSinceLastInteraction: number): SeparationEffect | null;
27
+ /**
28
+ * Compute chemistry effects when reuniting after absence.
29
+ */
30
+ export declare function computeReunionEffect(attachment: AttachmentState, minutesSinceLastInteraction: number): Partial<ChemicalState> | null;
@@ -0,0 +1,241 @@
1
+ // ============================================================
2
+ // Attachment Dynamics — Bowlby-inspired attachment formation
3
+ //
4
+ // Models relationship attachment through interaction patterns:
5
+ // 1. AttachmentModel — style classification + strength tracking
6
+ // 2. SeparationAnxiety — absence effects on chemistry
7
+ // 3. ReunionEffect — return effects on chemistry
8
+ //
9
+ // Attachment style emerges from interaction history, not from
10
+ // configuration. Consistent positive interaction → secure.
11
+ // Inconsistency → anxious. Rejection/neglect → avoidant.
12
+ // ============================================================
13
+ // ── Defaults ─────────────────────────────────────────────────
14
+ export const DEFAULT_ATTACHMENT = {
15
+ style: "secure",
16
+ strength: 0,
17
+ securityScore: 50,
18
+ anxietyScore: 50,
19
+ avoidanceScore: 50,
20
+ lastInteractionAt: new Date().toISOString(),
21
+ interactionCount: 0,
22
+ };
23
+ // ── Stimulus Classification ──────────────────────────────────
24
+ const POSITIVE_STIMULI = new Set([
25
+ "praise", "validation", "intimacy", "humor",
26
+ ]);
27
+ const NEGATIVE_STIMULI = new Set([
28
+ "criticism", "conflict", "neglect", "sarcasm", "authority",
29
+ ]);
30
+ const REJECTION_STIMULI = new Set([
31
+ "neglect", "authority", "boredom",
32
+ ]);
33
+ // EMA smoothing factor: weight given to new observation
34
+ const EMA_ALPHA = 0.15;
35
+ // ── 1. AttachmentModel ──────────────────────────────────────
36
+ /**
37
+ * Determine attachment style from scores.
38
+ */
39
+ function determineStyle(securityScore, anxietyScore, avoidanceScore) {
40
+ // Disorganized: both anxiety and avoidance elevated
41
+ if (anxietyScore > 50 && avoidanceScore > 50) {
42
+ return "disorganized";
43
+ }
44
+ // Anxious: high anxiety
45
+ if (anxietyScore > 60) {
46
+ return "anxious";
47
+ }
48
+ // Avoidant: high avoidance
49
+ if (avoidanceScore > 60) {
50
+ return "avoidant";
51
+ }
52
+ // Secure: high security, low anxiety, low avoidance
53
+ if (securityScore > 60 && anxietyScore < 40 && avoidanceScore < 40) {
54
+ return "secure";
55
+ }
56
+ // Default to current trajectory — mild insecurity stays secure
57
+ if (securityScore >= 50)
58
+ return "secure";
59
+ if (anxietyScore > avoidanceScore)
60
+ return "anxious";
61
+ return "avoidant";
62
+ }
63
+ /**
64
+ * Update attachment based on interaction outcome.
65
+ */
66
+ export function updateAttachment(attachment, stimulus, outcomeScore) {
67
+ const result = { ...attachment };
68
+ // Strength increases slowly with interaction count
69
+ result.interactionCount = attachment.interactionCount + 1;
70
+ result.strength = Math.min(100, attachment.strength + 1);
71
+ result.lastInteractionAt = new Date().toISOString();
72
+ if (stimulus === null) {
73
+ // No stimulus — just update count/strength, reclassify
74
+ result.style = determineStyle(result.securityScore, result.anxietyScore, result.avoidanceScore);
75
+ return result;
76
+ }
77
+ // SecurityScore: EMA with positive/negative stimuli
78
+ const isPositive = POSITIVE_STIMULI.has(stimulus);
79
+ const isNegative = NEGATIVE_STIMULI.has(stimulus);
80
+ if (isPositive) {
81
+ const target = Math.min(100, result.securityScore + 5);
82
+ result.securityScore = result.securityScore * (1 - EMA_ALPHA) + target * EMA_ALPHA;
83
+ }
84
+ else if (isNegative) {
85
+ const target = Math.max(0, result.securityScore - 5);
86
+ result.securityScore = result.securityScore * (1 - EMA_ALPHA) + target * EMA_ALPHA;
87
+ }
88
+ // AnxietyScore: increases with inconsistency (rapid alternation between positive and negative)
89
+ // We detect inconsistency by checking if outcomeScore diverges from the security trend
90
+ const expectedDirection = result.securityScore > 50 ? 1 : -1;
91
+ const actualDirection = outcomeScore >= 0 ? 1 : -1;
92
+ const isInconsistent = expectedDirection !== actualDirection;
93
+ if (isInconsistent) {
94
+ // Inconsistency → anxiety rises
95
+ const anxietyTarget = Math.min(100, result.anxietyScore + 8);
96
+ result.anxietyScore = result.anxietyScore * (1 - EMA_ALPHA) + anxietyTarget * EMA_ALPHA;
97
+ }
98
+ else {
99
+ // Consistency → anxiety decreases
100
+ const anxietyTarget = Math.max(0, result.anxietyScore - 3);
101
+ result.anxietyScore = result.anxietyScore * (1 - EMA_ALPHA) + anxietyTarget * EMA_ALPHA;
102
+ }
103
+ // AvoidanceScore: increases with rejection/neglect stimuli
104
+ if (REJECTION_STIMULI.has(stimulus)) {
105
+ const avoidTarget = Math.min(100, result.avoidanceScore + 6);
106
+ result.avoidanceScore = result.avoidanceScore * (1 - EMA_ALPHA) + avoidTarget * EMA_ALPHA;
107
+ }
108
+ else if (isPositive) {
109
+ // Positive interactions reduce avoidance
110
+ const avoidTarget = Math.max(0, result.avoidanceScore - 3);
111
+ result.avoidanceScore = result.avoidanceScore * (1 - EMA_ALPHA) + avoidTarget * EMA_ALPHA;
112
+ }
113
+ // Clamp all scores
114
+ result.securityScore = Math.max(0, Math.min(100, result.securityScore));
115
+ result.anxietyScore = Math.max(0, Math.min(100, result.anxietyScore));
116
+ result.avoidanceScore = Math.max(0, Math.min(100, result.avoidanceScore));
117
+ // Determine style
118
+ result.style = determineStyle(result.securityScore, result.anxietyScore, result.avoidanceScore);
119
+ return result;
120
+ }
121
+ // ── 2. SeparationAnxiety ────────────────────────────────────
122
+ /**
123
+ * Compute chemistry effects of absence based on attachment.
124
+ * Called when time since last interaction is significant.
125
+ */
126
+ export function computeSeparationEffect(attachment, minutesSinceLastInteraction) {
127
+ // No effect for short absence or weak attachment
128
+ if (minutesSinceLastInteraction < 60 || attachment.strength < 20) {
129
+ return null;
130
+ }
131
+ const hours = minutesSinceLastInteraction / 60;
132
+ // Intensity scales with attachment strength and time (logarithmic growth, capped at 1)
133
+ const baseIntensity = (attachment.strength / 100) * Math.min(1, Math.log2(hours + 1) / 5);
134
+ switch (attachment.style) {
135
+ case "secure": {
136
+ // Mild longing after 24h
137
+ if (hours < 24)
138
+ return null;
139
+ const intensity = baseIntensity * 0.5;
140
+ return {
141
+ chemistryDelta: {
142
+ OT: -5 * intensity,
143
+ DA: -3 * intensity,
144
+ },
145
+ description: "gentle longing from sustained absence",
146
+ intensity: Math.min(1, intensity),
147
+ };
148
+ }
149
+ case "anxious": {
150
+ // Distress after 4h, grows with time
151
+ if (hours < 4)
152
+ return null;
153
+ const intensity = baseIntensity * 1.5;
154
+ // OT oscillation: represented as net negative with anxiety
155
+ return {
156
+ chemistryDelta: {
157
+ CORT: 10 * intensity,
158
+ OT: -5 * intensity,
159
+ NE: 8 * intensity,
160
+ DA: -3 * intensity,
161
+ },
162
+ description: "anxious distress from absence — fear of abandonment",
163
+ intensity: Math.min(1, intensity),
164
+ };
165
+ }
166
+ case "avoidant": {
167
+ // Relief initially, discomfort after 48h
168
+ if (hours < 48)
169
+ return null;
170
+ const intensity = baseIntensity * 0.4;
171
+ return {
172
+ chemistryDelta: {
173
+ OT: -3 * intensity,
174
+ },
175
+ description: "subtle discomfort surfacing through avoidant defense",
176
+ intensity: Math.min(1, intensity),
177
+ };
178
+ }
179
+ case "disorganized": {
180
+ // Conflicting signals
181
+ if (hours < 4)
182
+ return null;
183
+ const intensity = baseIntensity * 1.0;
184
+ return {
185
+ chemistryDelta: {
186
+ CORT: 5 * intensity,
187
+ OT: 5 * intensity,
188
+ NE: 3 * intensity,
189
+ },
190
+ description: "conflicting signals — wanting closeness and fearing it",
191
+ intensity: Math.min(1, intensity),
192
+ };
193
+ }
194
+ }
195
+ }
196
+ // ── 3. ReunionEffect ────────────────────────────────────────
197
+ /**
198
+ * Compute chemistry effects when reuniting after absence.
199
+ */
200
+ export function computeReunionEffect(attachment, minutesSinceLastInteraction) {
201
+ // No effect for short absence or weak attachment
202
+ if (minutesSinceLastInteraction < 60 || attachment.strength < 20) {
203
+ return null;
204
+ }
205
+ const hours = minutesSinceLastInteraction / 60;
206
+ // Scale with time (logarithmic) and attachment strength
207
+ const scale = (attachment.strength / 100) * Math.min(1, Math.log2(hours + 1) / 5);
208
+ switch (attachment.style) {
209
+ case "secure": {
210
+ // Warm reunion
211
+ return {
212
+ OT: 8 * scale,
213
+ DA: 5 * scale,
214
+ END: 3 * scale,
215
+ };
216
+ }
217
+ case "anxious": {
218
+ // Intense but short-lived relief (CORT still elevated)
219
+ return {
220
+ OT: 15 * scale,
221
+ DA: 10 * scale,
222
+ CORT: 5 * scale,
223
+ };
224
+ }
225
+ case "avoidant": {
226
+ // Cautious re-engagement
227
+ return {
228
+ OT: 3 * scale,
229
+ NE: 5 * scale,
230
+ };
231
+ }
232
+ case "disorganized": {
233
+ // Mixed signals
234
+ return {
235
+ OT: 5 * scale,
236
+ CORT: 5 * scale,
237
+ NE: 5 * scale,
238
+ };
239
+ }
240
+ }
241
+ }
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export { PsycheEngine } from "./core.js";
2
2
  export type { PsycheEngineConfig, ProcessInputResult, ProcessOutputResult, ProcessOutcomeResult } from "./core.js";
3
3
  export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
4
4
  export type { StorageAdapter } from "./storage.js";
5
- export type { PsycheState, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern, DriveType, InnateDrives, LearningState, LearnedVectorAdjustment, PredictionRecord, OutcomeScore, OutcomeSignals, } from "./types.js";
6
- export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
5
+ export type { PsycheState, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern, DriveType, InnateDrives, LearningState, LearnedVectorAdjustment, PredictionRecord, OutcomeScore, OutcomeSignals, AttachmentStyle, AttachmentData, } from "./types.js";
6
+ export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DEFAULT_ATTACHMENT, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
7
7
  export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
8
8
  export type { SelfReflection } from "./self-recognition.js";
9
9
  export { PsycheInteraction } from "./interaction.js";
@@ -15,6 +15,10 @@ export type { CustomProfileConfig, ResolvedProfile } from "./custom-profile.js";
15
15
  export { evaluateOutcome, getLearnedVector, updateLearnedVector, computeContextHash, predictChemistry, computePredictionError, recordPrediction, getAveragePredictionError, } from "./learning.js";
16
16
  export { classifyStimulusWithContext, extractContextFeatures, stimulusWarmth } from "./context-classifier.js";
17
17
  export type { ContextFeatures, ContextualClassification } from "./context-classifier.js";
18
+ export { predictNextStimulus, generateAnticipation, computeSurpriseEffect, computeRegret, } from "./temporal.js";
19
+ export type { StimulusPrediction, AnticipationState, RegretEntry } from "./temporal.js";
20
+ export { updateAttachment, computeSeparationEffect, computeReunionEffect, } from "./attachment.js";
21
+ export type { SeparationEffect } from "./attachment.js";
18
22
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
19
23
  export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
20
24
  export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  export { PsycheEngine } from "./core.js";
13
13
  // Storage
14
14
  export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
15
- export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
15
+ export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DEFAULT_ATTACHMENT, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
16
16
  // Self-recognition
17
17
  export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
18
18
  // Multi-agent interaction
@@ -25,6 +25,10 @@ export { createCustomProfile, validateProfileConfig, PRESET_PROFILES } from "./c
25
25
  export { evaluateOutcome, getLearnedVector, updateLearnedVector, computeContextHash, predictChemistry, computePredictionError, recordPrediction, getAveragePredictionError, } from "./learning.js";
26
26
  // Context-aware classification (P3)
27
27
  export { classifyStimulusWithContext, extractContextFeatures, stimulusWarmth } from "./context-classifier.js";
28
+ // Temporal consciousness (P4)
29
+ export { predictNextStimulus, generateAnticipation, computeSurpriseEffect, computeRegret, } from "./temporal.js";
30
+ // Attachment dynamics (P4)
31
+ export { updateAttachment, computeSeparationEffect, computeReunionEffect, } from "./attachment.js";
28
32
  // Utilities — for custom adapter / advanced use
29
33
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
30
34
  export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
@@ -0,0 +1,38 @@
1
+ import type { ChemicalState, ChemicalSnapshot, StimulusType, PsycheState, RelationshipState } from "./types.js";
2
+ export interface StimulusPrediction {
3
+ stimulus: StimulusType;
4
+ probability: number;
5
+ }
6
+ export interface AnticipationState {
7
+ predictions: StimulusPrediction[];
8
+ anticipatoryChemistry: Partial<ChemicalState>;
9
+ timestamp: string;
10
+ }
11
+ export interface RegretEntry {
12
+ turnIndex: number;
13
+ counterfactualDelta: Partial<ChemicalState>;
14
+ regretIntensity: number;
15
+ description: string;
16
+ timestamp: string;
17
+ }
18
+ /**
19
+ * Predict likely next stimulus based on interaction history.
20
+ * Uses simple Markov property: given recent stimulus sequence, what comes next?
21
+ */
22
+ export declare function predictNextStimulus(emotionalHistory: ChemicalSnapshot[], relationshipPhase: RelationshipState["phase"]): StimulusPrediction[];
23
+ /**
24
+ * Generate anticipatory chemistry changes based on predictions.
25
+ * High-probability positive prediction -> DA/OT micro-rise.
26
+ * High-probability negative prediction -> CORT micro-rise.
27
+ */
28
+ export declare function generateAnticipation(predictions: StimulusPrediction[], currentChemistry: ChemicalState): AnticipationState;
29
+ /**
30
+ * Compute disappointment/surprise when actual stimulus differs from prediction.
31
+ * Returns additional chemistry delta beyond the normal stimulus response.
32
+ */
33
+ export declare function computeSurpriseEffect(anticipated: AnticipationState, actualStimulus: StimulusType | null): Partial<ChemicalState>;
34
+ /**
35
+ * Evaluate if the last interaction would have gone better with different chemistry.
36
+ * Runs a counterfactual: "what if my chemistry had been at baseline?"
37
+ */
38
+ export declare function computeRegret(preInteractionState: PsycheState, postInteractionState: PsycheState, outcomeScore: number, appliedStimulus: StimulusType | null): RegretEntry | null;
@@ -0,0 +1,276 @@
1
+ // ============================================================
2
+ // Temporal Consciousness — Anticipation, Surprise, and Regret
3
+ //
4
+ // Implements predictive emotional processing:
5
+ // 1. PredictiveModel — Markov-based stimulus prediction
6
+ // 2. AnticipationEngine — pre-stimulus chemistry shifts
7
+ // 3. RegretComputer — counterfactual outcome analysis
8
+ //
9
+ // These enable the agent to "look forward" and "look back",
10
+ // creating a temporal dimension to emotional experience.
11
+ // ============================================================
12
+ import { CHEMICAL_KEYS } from "./types.js";
13
+ import { STIMULUS_VECTORS } from "./chemistry.js";
14
+ // ── All StimulusType values ──────────────────────────────────
15
+ const ALL_STIMULI = [
16
+ "praise", "criticism", "humor", "intellectual", "intimacy",
17
+ "conflict", "neglect", "surprise", "casual",
18
+ "sarcasm", "authority", "validation", "boredom", "vulnerability",
19
+ ];
20
+ // ── Phase Priors ─────────────────────────────────────────────
21
+ // Default probability weights per relationship phase.
22
+ const PHASE_PRIORS = {
23
+ stranger: {
24
+ casual: 3, intellectual: 2, humor: 1.5, boredom: 1.5,
25
+ criticism: 0.5, intimacy: 0.3, vulnerability: 0.3,
26
+ praise: 1, validation: 0.8, surprise: 1, conflict: 0.5,
27
+ neglect: 1, sarcasm: 0.8, authority: 0.8,
28
+ },
29
+ acquaintance: {
30
+ casual: 2.5, intellectual: 2, humor: 2, praise: 1.5,
31
+ validation: 1.2, surprise: 1, criticism: 0.8, conflict: 0.5,
32
+ intimacy: 0.5, vulnerability: 0.5, neglect: 0.8,
33
+ sarcasm: 0.8, authority: 0.8, boredom: 1,
34
+ },
35
+ familiar: {
36
+ casual: 2, humor: 2.5, praise: 2, validation: 2,
37
+ intellectual: 2, intimacy: 1.5, vulnerability: 1,
38
+ surprise: 1.2, criticism: 1, conflict: 0.8, neglect: 0.6,
39
+ sarcasm: 1, authority: 0.6, boredom: 0.8,
40
+ },
41
+ close: {
42
+ intimacy: 3, humor: 2.5, validation: 2.5, praise: 2,
43
+ vulnerability: 2, casual: 2, intellectual: 1.5,
44
+ surprise: 1.5, criticism: 1, conflict: 0.8, neglect: 0.5,
45
+ sarcasm: 0.8, authority: 0.5, boredom: 0.5,
46
+ },
47
+ deep: {
48
+ intimacy: 3.5, vulnerability: 3, validation: 2.5, humor: 2.5,
49
+ praise: 2, casual: 1.5, intellectual: 2, surprise: 1.5,
50
+ criticism: 1, conflict: 0.8, neglect: 0.3, sarcasm: 0.5,
51
+ authority: 0.3, boredom: 0.3,
52
+ },
53
+ };
54
+ // ── 1. PredictiveModel ──────────────────────────────────────
55
+ /**
56
+ * Predict likely next stimulus based on interaction history.
57
+ * Uses simple Markov property: given recent stimulus sequence, what comes next?
58
+ */
59
+ export function predictNextStimulus(emotionalHistory, relationshipPhase) {
60
+ const phasePrior = PHASE_PRIORS[relationshipPhase] ?? PHASE_PRIORS.acquaintance;
61
+ // Insufficient history: return flat prior weighted by phase
62
+ if (emotionalHistory.length < 3) {
63
+ return buildPhasePrior(phasePrior);
64
+ }
65
+ // Extract the last 2 stimuli for bigram transition
66
+ const recent = emotionalHistory.slice(-2);
67
+ const lastTwo = recent.map((s) => s.stimulus).filter((s) => s !== null);
68
+ if (lastTwo.length < 2) {
69
+ return buildPhasePrior(phasePrior);
70
+ }
71
+ // Build transition counts from history (all consecutive pairs)
72
+ const transitionCounts = new Map();
73
+ for (let i = 1; i < emotionalHistory.length; i++) {
74
+ const prev = emotionalHistory[i - 1].stimulus;
75
+ const cur = emotionalHistory[i].stimulus;
76
+ if (prev === null || cur === null)
77
+ continue;
78
+ const key = prev;
79
+ if (!transitionCounts.has(key)) {
80
+ transitionCounts.set(key, new Map());
81
+ }
82
+ const counts = transitionCounts.get(key);
83
+ counts.set(cur, (counts.get(cur) ?? 0) + 1);
84
+ }
85
+ // Get transition probabilities from the last stimulus
86
+ const lastStimulus = lastTwo[lastTwo.length - 1];
87
+ const transitions = transitionCounts.get(lastStimulus);
88
+ // If no transitions observed from this stimulus, fall back to phase prior
89
+ if (!transitions || transitions.size === 0) {
90
+ return buildPhasePrior(phasePrior);
91
+ }
92
+ // Merge Markov transitions with phase prior (50/50 blend)
93
+ let totalTransitions = 0;
94
+ for (const count of transitions.values()) {
95
+ totalTransitions += count;
96
+ }
97
+ const predictions = [];
98
+ let totalWeight = 0;
99
+ for (const stim of ALL_STIMULI) {
100
+ const markovProb = totalTransitions > 0
101
+ ? (transitions.get(stim) ?? 0) / totalTransitions
102
+ : 0;
103
+ const priorWeight = phasePrior[stim] ?? 0.5;
104
+ // Blend: 50% Markov, 50% phase prior (normalized)
105
+ const combined = markovProb * 0.5 + (priorWeight / 20) * 0.5;
106
+ totalWeight += combined;
107
+ predictions.push({ stimulus: stim, probability: combined });
108
+ }
109
+ // Normalize
110
+ if (totalWeight > 0) {
111
+ for (const p of predictions) {
112
+ p.probability = p.probability / totalWeight;
113
+ }
114
+ }
115
+ // Sort by probability descending
116
+ predictions.sort((a, b) => b.probability - a.probability);
117
+ return predictions;
118
+ }
119
+ /**
120
+ * Build a flat phase-weighted prior distribution.
121
+ */
122
+ function buildPhasePrior(weights) {
123
+ let totalWeight = 0;
124
+ const predictions = [];
125
+ for (const stim of ALL_STIMULI) {
126
+ const w = weights[stim] ?? 0.5;
127
+ totalWeight += w;
128
+ predictions.push({ stimulus: stim, probability: w });
129
+ }
130
+ // Normalize
131
+ if (totalWeight > 0) {
132
+ for (const p of predictions) {
133
+ p.probability = p.probability / totalWeight;
134
+ }
135
+ }
136
+ predictions.sort((a, b) => b.probability - a.probability);
137
+ return predictions;
138
+ }
139
+ // ── 2. AnticipationEngine ───────────────────────────────────
140
+ /**
141
+ * Generate anticipatory chemistry changes based on predictions.
142
+ * High-probability positive prediction -> DA/OT micro-rise.
143
+ * High-probability negative prediction -> CORT micro-rise.
144
+ */
145
+ export function generateAnticipation(predictions, currentChemistry) {
146
+ const anticipation = {};
147
+ for (const key of CHEMICAL_KEYS) {
148
+ anticipation[key] = 0;
149
+ }
150
+ // For each prediction with probability > 0.2, compute micro shift
151
+ for (const pred of predictions) {
152
+ if (pred.probability <= 0.2)
153
+ continue;
154
+ const vector = STIMULUS_VECTORS[pred.stimulus];
155
+ if (!vector)
156
+ continue;
157
+ const scale = 0.15 * pred.probability;
158
+ for (const key of CHEMICAL_KEYS) {
159
+ anticipation[key] += vector[key] * scale;
160
+ }
161
+ }
162
+ // Clamp total anticipation shift to +/-5 per chemical
163
+ const clamped = {};
164
+ for (const key of CHEMICAL_KEYS) {
165
+ const val = Math.max(-5, Math.min(5, anticipation[key]));
166
+ if (Math.abs(val) > 0.01) {
167
+ clamped[key] = Math.round(val * 100) / 100;
168
+ }
169
+ }
170
+ return {
171
+ predictions,
172
+ anticipatoryChemistry: clamped,
173
+ timestamp: new Date().toISOString(),
174
+ };
175
+ }
176
+ /**
177
+ * Compute disappointment/surprise when actual stimulus differs from prediction.
178
+ * Returns additional chemistry delta beyond the normal stimulus response.
179
+ */
180
+ export function computeSurpriseEffect(anticipated, actualStimulus) {
181
+ if (!actualStimulus || anticipated.predictions.length === 0) {
182
+ return {};
183
+ }
184
+ const topPrediction = anticipated.predictions[0];
185
+ const topConfidence = topPrediction.probability;
186
+ // Find the predicted probability for the actual stimulus
187
+ const actualPrediction = anticipated.predictions.find((p) => p.stimulus === actualStimulus);
188
+ const actualProbability = actualPrediction?.probability ?? 0;
189
+ // If actual matches top prediction, no surprise
190
+ if (actualStimulus === topPrediction.stimulus) {
191
+ return {};
192
+ }
193
+ // Determine if the actual stimulus is positive or negative
194
+ const actualVector = STIMULUS_VECTORS[actualStimulus];
195
+ if (!actualVector)
196
+ return {};
197
+ const actualValence = actualVector.DA + actualVector.HT + actualVector.OT - actualVector.CORT;
198
+ const topVector = STIMULUS_VECTORS[topPrediction.stimulus];
199
+ const topValence = topVector
200
+ ? topVector.DA + topVector.HT + topVector.OT - topVector.CORT
201
+ : 0;
202
+ // Surprise magnitude scales with: (1) how confident the prediction was, (2) how unexpected the actual is
203
+ const surpriseMagnitude = topConfidence * (1 - actualProbability);
204
+ if (actualValence > 0 && topValence <= actualValence) {
205
+ // Pleasant surprise: actual is more positive than expected
206
+ return {
207
+ DA: Math.round(5 * surpriseMagnitude * 100) / 100,
208
+ END: Math.round(3 * surpriseMagnitude * 100) / 100,
209
+ };
210
+ }
211
+ else if (actualValence < topValence) {
212
+ // Disappointment: actual is worse than expected (the "crash" from anticipated warmth)
213
+ return {
214
+ DA: Math.round(-5 * surpriseMagnitude * 100) / 100,
215
+ CORT: Math.round(5 * surpriseMagnitude * 100) / 100,
216
+ };
217
+ }
218
+ return {};
219
+ }
220
+ // ── 3. RegretComputer ───────────────────────────────────────
221
+ /** Chemical descriptions for regret messages */
222
+ const CHEMICAL_DESCRIPTIONS = {
223
+ DA: { high: "high dopamine made response too eager", low: "low dopamine made response flat" },
224
+ HT: { high: "high serotonin made response complacent", low: "low serotonin made response unstable" },
225
+ CORT: { high: "high CORT made response too defensive", low: "low CORT made response careless" },
226
+ OT: { high: "high oxytocin made response too trusting", low: "low oxytocin made response too cold" },
227
+ NE: { high: "high norepinephrine made response too reactive", low: "low norepinephrine made response sluggish" },
228
+ END: { high: "high endorphins made response too flippant", low: "low endorphins made response too serious" },
229
+ };
230
+ /**
231
+ * Evaluate if the last interaction would have gone better with different chemistry.
232
+ * Runs a counterfactual: "what if my chemistry had been at baseline?"
233
+ */
234
+ export function computeRegret(preInteractionState, postInteractionState, outcomeScore, appliedStimulus) {
235
+ // Only generate regret for bad outcomes
236
+ if (outcomeScore >= -0.2) {
237
+ return null;
238
+ }
239
+ const baseline = preInteractionState.baseline;
240
+ const preChemistry = preInteractionState.current;
241
+ // Check if chemistry was significantly deviated from baseline
242
+ let maxDeviation = 0;
243
+ let mostDeviatedKey = "DA";
244
+ for (const key of CHEMICAL_KEYS) {
245
+ const deviation = Math.abs(preChemistry[key] - baseline[key]);
246
+ if (deviation > maxDeviation) {
247
+ maxDeviation = deviation;
248
+ mostDeviatedKey = key;
249
+ }
250
+ }
251
+ // No regret if chemistry was near baseline (deviation < 15)
252
+ if (maxDeviation < 15) {
253
+ return null;
254
+ }
255
+ // Compute regret intensity: |outcomeScore| * (maxDeviation / 100)
256
+ const regretIntensity = Math.min(1, Math.abs(outcomeScore) * (maxDeviation / 100));
257
+ // Build counterfactual delta: difference between baseline and actual pre-interaction chemistry
258
+ const counterfactualDelta = {};
259
+ for (const key of CHEMICAL_KEYS) {
260
+ const diff = baseline[key] - preChemistry[key];
261
+ if (Math.abs(diff) > 5) {
262
+ counterfactualDelta[key] = Math.round(diff * 100) / 100;
263
+ }
264
+ }
265
+ // Build description identifying the most deviated chemical
266
+ const deviationDirection = preChemistry[mostDeviatedKey] > baseline[mostDeviatedKey]
267
+ ? "high" : "low";
268
+ const description = CHEMICAL_DESCRIPTIONS[mostDeviatedKey][deviationDirection];
269
+ return {
270
+ turnIndex: postInteractionState.meta.totalInteractions,
271
+ counterfactualDelta,
272
+ regretIntensity,
273
+ description,
274
+ timestamp: new Date().toISOString(),
275
+ };
276
+ }
package/dist/types.d.ts CHANGED
@@ -49,12 +49,27 @@ export interface EmotionPattern {
49
49
  expressionHint: string;
50
50
  behaviorGuide: string;
51
51
  }
52
+ /** Attachment style for relationship dynamics */
53
+ export type AttachmentStyle = "secure" | "anxious" | "avoidant" | "disorganized";
54
+ /** Attachment state tracked per-relationship */
55
+ export interface AttachmentData {
56
+ style: AttachmentStyle;
57
+ strength: number;
58
+ securityScore: number;
59
+ anxietyScore: number;
60
+ avoidanceScore: number;
61
+ lastInteractionAt: string;
62
+ interactionCount: number;
63
+ }
64
+ /** Default attachment for new relationships */
65
+ export declare const DEFAULT_ATTACHMENT: AttachmentData;
52
66
  /** Relationship tracking */
53
67
  export interface RelationshipState {
54
68
  trust: number;
55
69
  intimacy: number;
56
70
  phase: "stranger" | "acquaintance" | "familiar" | "close" | "deep";
57
71
  memory?: string[];
72
+ attachment?: AttachmentData;
58
73
  }
59
74
  /** Chemical state snapshot for emotional memory */
60
75
  export interface ChemicalSnapshot {
@@ -128,6 +143,8 @@ export declare const MAX_LEARNED_VECTORS = 200;
128
143
  export declare const MAX_PREDICTION_HISTORY = 50;
129
144
  /** Max outcome history entries */
130
145
  export declare const MAX_OUTCOME_HISTORY = 50;
146
+ /** Max regret history entries */
147
+ export declare const MAX_REGRET_HISTORY = 20;
131
148
  /** Persisted psyche state for an agent (v4: emotional learning) */
132
149
  export interface PsycheState {
133
150
  version: 3 | 4;
package/dist/types.js CHANGED
@@ -56,6 +56,16 @@ export const CHEMICAL_DECAY_SPEED = {
56
56
  NE: "fast",
57
57
  END: "fast",
58
58
  };
59
+ /** Default attachment for new relationships */
60
+ export const DEFAULT_ATTACHMENT = {
61
+ style: "secure",
62
+ strength: 0,
63
+ securityScore: 50,
64
+ anxietyScore: 50,
65
+ avoidanceScore: 50,
66
+ lastInteractionAt: new Date().toISOString(),
67
+ interactionCount: 0,
68
+ };
59
69
  /** Max history entries to keep */
60
70
  export const MAX_EMOTIONAL_HISTORY = 10;
61
71
  /** Max compressed session memories per relationship */
@@ -73,6 +83,8 @@ export const MAX_LEARNED_VECTORS = 200;
73
83
  export const MAX_PREDICTION_HISTORY = 50;
74
84
  /** Max outcome history entries */
75
85
  export const MAX_OUTCOME_HISTORY = 50;
86
+ /** Max regret history entries */
87
+ export const MAX_REGRET_HISTORY = 20;
76
88
  /** Default relationship for new users */
77
89
  export const DEFAULT_RELATIONSHIP = {
78
90
  trust: 50,
package/dist/update.js CHANGED
@@ -11,7 +11,7 @@ import { execFile } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
12
  const execFileAsync = promisify(execFile);
13
13
  const PACKAGE_NAME = "psyche-ai";
14
- const CURRENT_VERSION = "3.0.0";
14
+ const CURRENT_VERSION = "3.1.0";
15
15
  const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
16
16
  const CACHE_DIR = join(homedir(), ".psyche-ai");
17
17
  const CACHE_FILE = join(CACHE_DIR, "update-check.json");
@@ -2,7 +2,7 @@
2
2
  "id": "psyche-ai",
3
3
  "name": "Artificial Psyche",
4
4
  "description": "Virtual endocrine system, empathy engine, and agency for OpenClaw agents",
5
- "version": "3.0.0",
5
+ "version": "3.1.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psyche-ai",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Artificial Psyche — universal emotional intelligence plugin for any AI agent",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",