psyche-ai 5.0.0 → 7.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
@@ -176,6 +176,73 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
176
176
 
177
177
  架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
178
178
 
179
+ ## 模式
180
+
181
+ | 模式 | 说明 | 适用场景 |
182
+ |------|------|---------|
183
+ | natural | 默认。完整情绪体验 | 大部分场景 |
184
+ | work | 最小情绪,专注任务 | 写代码、写文档 |
185
+ | companion | 强化情绪表达 | 闲聊、陪伴 |
186
+
187
+ ```bash
188
+ # 切换模式
189
+ psyche mode ./workspace work
190
+
191
+ # 初始化时设置模式
192
+ psyche init . --mode companion
193
+ ```
194
+
195
+ ## 性格强度
196
+
197
+ `personalityIntensity` 控制情绪表达的强烈程度(0.0-1.0):
198
+
199
+ - **0.0** = 传统温暖 AI,没有情绪波动
200
+ - **0.5** = 轻微情绪
201
+ - **0.7** = 默认
202
+ - **1.0** = 完整 Psyche 体验
203
+
204
+ ## Big Five 人格
205
+
206
+ 除了 MBTI 预设,你也可以用学术界更认可的 Big Five (大五人格) 来定义性格:
207
+
208
+ ```bash
209
+ psyche init . --traits "O:80,C:40,E:90,A:60,N:30"
210
+ ```
211
+
212
+ | 维度 | 含义 | 低值 | 高值 |
213
+ |------|------|------|------|
214
+ | O (Openness) | 开放性 | 保守、实际 | 好奇、想象力强 |
215
+ | C (Conscientiousness) | 尽责性 | 随性、灵活 | 严谨、有条理 |
216
+ | E (Extraversion) | 外向性 | 内向、安静 | 外向、精力充沛 |
217
+ | A (Agreeableness) | 宜人性 | 独立、直率 | 合作、温暖 |
218
+ | N (Neuroticism) | 神经质 | 情绪稳定 | 情绪敏感 |
219
+
220
+ ## 隐私
221
+
222
+ 情绪状态默认存储在本地 `psyche-state.json`。如果不想留任何痕迹:
223
+
224
+ ```bash
225
+ # 初始化时选择不持久化
226
+ psyche init . --no-persist
227
+ ```
228
+
229
+ 或者在代码中:
230
+
231
+ ```javascript
232
+ const engine = new PsycheEngine({ persist: false }, storage);
233
+ ```
234
+
235
+ 详细伦理声明见 [ETHICS.md](ETHICS.md)。
236
+
237
+ ## 商业模式
238
+
239
+ Psyche 核心引擎永久开源(MIT)。
240
+
241
+ 计划中的增值服务:
242
+ - **Psyche Cloud**:云端情绪状态同步 + 跨设备记忆
243
+ - **Psyche Pro Classifier**:基于微调模型的高精度刺激分类(替代正则)
244
+ - **企业定制**:自定义人格模型、合规审计、SLA 保障
245
+
179
246
  ## 开发
180
247
 
181
248
  ```bash
@@ -161,6 +161,13 @@ export function register(api) {
161
161
  return;
162
162
  const engine = engines.get(workspaceDir);
163
163
  if (engine) {
164
+ // Compress session history into relationship memory before closing
165
+ try {
166
+ await engine.endSession({ userId: ctx.userId });
167
+ }
168
+ catch (err) {
169
+ logger.warn(`Psyche: failed to compress session: ${err}`);
170
+ }
164
171
  const state = engine.getState();
165
172
  logger.info(`Psyche: session ended for ${state.meta.agentName}, ` +
166
173
  `chemistry saved (DA:${Math.round(state.current.DA)} ` +
@@ -0,0 +1,41 @@
1
+ import type { ChemicalState, InnateDrives, Locale } from "./types.js";
2
+ export type AutonomicState = "ventral-vagal" | "sympathetic" | "dorsal-vagal";
3
+ export interface AutonomicResult {
4
+ state: AutonomicState;
5
+ transitionProgress: number;
6
+ gatedEmotionCategories: string[];
7
+ description: string;
8
+ }
9
+ export interface AutonomicTransition {
10
+ from: AutonomicState;
11
+ to: AutonomicState;
12
+ transitionMinutes: number;
13
+ }
14
+ /**
15
+ * Compute the raw autonomic state from chemistry and drives.
16
+ * No transition inertia — returns the "target" state.
17
+ */
18
+ export declare function computeAutonomicState(chemistry: ChemicalState, drives: InnateDrives): AutonomicState;
19
+ /**
20
+ * Gate emotions based on autonomic state.
21
+ * - Ventral vagal: all emotions pass through
22
+ * - Sympathetic: blocks positive social emotions
23
+ * - Dorsal vagal: only allows numbness/introspection/burnout (whitelist)
24
+ */
25
+ export declare function gateEmotions(autonomicState: AutonomicState, emotions: string[]): string[];
26
+ /**
27
+ * Get the transition time in minutes between two autonomic states.
28
+ * Asymmetric: activation is faster than recovery.
29
+ */
30
+ export declare function getTransitionTime(from: AutonomicState, to: AutonomicState): number;
31
+ /**
32
+ * Describe an autonomic state in the given locale.
33
+ */
34
+ export declare function describeAutonomicState(state: AutonomicState, locale: Locale): string;
35
+ /**
36
+ * Compute the full autonomic result with transition inertia.
37
+ *
38
+ * If previousState differs from the target state, transition progress
39
+ * is based on elapsed time vs required transition time.
40
+ */
41
+ export declare function computeAutonomicResult(chemistry: ChemicalState, drives: InnateDrives, previousState: AutonomicState | null, minutesSinceLastUpdate: number, locale?: Locale): AutonomicResult;
@@ -0,0 +1,186 @@
1
+ // ============================================================
2
+ // Autonomic Nervous System — Polyvagal Theory Implementation
3
+ // ============================================================
4
+ //
5
+ // Maps chemical state + innate drives to autonomic nervous system
6
+ // states based on Stephen Porges' Polyvagal Theory:
7
+ //
8
+ // - Ventral vagal: social engagement, safety (default)
9
+ // - Sympathetic: fight/flight mobilization
10
+ // - Dorsal vagal: freeze/shutdown/collapse
11
+ // ── i18n Strings ─────────────────────────────────────────────
12
+ const AUTONOMIC_STRINGS = {
13
+ zh: {
14
+ "ventral-vagal": "腹侧迷走神经激活——安全与社交参与状态,情绪开放,表达自然",
15
+ "sympathetic": "交感神经激活——警觉动员状态,战斗或逃跑准备中",
16
+ "dorsal-vagal": "背侧迷走神经激活——冻结与保护性关闭状态,能量极低",
17
+ },
18
+ en: {
19
+ "ventral-vagal": "Ventral vagal activation — safe and socially engaged, emotionally open",
20
+ "sympathetic": "Sympathetic activation — alert and mobilized, fight-or-flight readiness",
21
+ "dorsal-vagal": "Dorsal vagal activation — freeze and protective shutdown, minimal energy",
22
+ },
23
+ };
24
+ // ── Transition Time Matrix (minutes) ─────────────────────────
25
+ const TRANSITION_TIMES = {
26
+ "ventral-vagal": {
27
+ "ventral-vagal": 0,
28
+ "sympathetic": 2, // fast activation
29
+ "dorsal-vagal": 7, // not a shortcut: >= ventral→sympathetic + sympathetic→dorsal (2+5=7)
30
+ },
31
+ "sympathetic": {
32
+ "ventral-vagal": 8, // calming down
33
+ "sympathetic": 0,
34
+ "dorsal-vagal": 5, // collapse
35
+ },
36
+ "dorsal-vagal": {
37
+ "ventral-vagal": 25, // full recovery is slow
38
+ "sympathetic": 12, // re-mobilization from freeze
39
+ "dorsal-vagal": 0,
40
+ },
41
+ };
42
+ // ── Emotion Gating ───────────────────────────────────────────
43
+ /** Positive social emotions blocked during sympathetic activation */
44
+ const SYMPATHETIC_BLOCKED = new Set([
45
+ "deep contentment",
46
+ "warm intimacy",
47
+ "playful mischief",
48
+ "excited joy",
49
+ "tender affection",
50
+ "serene peace",
51
+ "grateful warmth",
52
+ "compassionate care",
53
+ ]);
54
+ /** Emotions allowed during dorsal-vagal (whitelist) */
55
+ const DORSAL_ALLOWED = new Set([
56
+ "emotional numbness",
57
+ "melancholic introspection",
58
+ "burnout",
59
+ "resignation",
60
+ "dissociation",
61
+ "exhaustion",
62
+ ]);
63
+ // ── Core Functions ───────────────────────────────────────────
64
+ /**
65
+ * Compute the raw autonomic state from chemistry and drives.
66
+ * No transition inertia — returns the "target" state.
67
+ */
68
+ export function computeAutonomicState(chemistry, drives) {
69
+ const { CORT, NE, DA, HT, OT } = chemistry;
70
+ const { survival, safety, connection } = drives;
71
+ // Count drives that are critically low (< 20)
72
+ const lowDriveCount = [survival, safety, connection, drives.esteem, drives.curiosity]
73
+ .filter((d) => d < 20).length;
74
+ // ── Dorsal vagal check (freeze/shutdown) ──
75
+ // Very high stress + low arousal + low motivation = collapse
76
+ if (CORT >= 80 && NE <= 25 && DA <= 20) {
77
+ return "dorsal-vagal";
78
+ }
79
+ // Multiple critically low drives with depleted chemistry
80
+ if (lowDriveCount >= 3 && CORT >= 70 && (NE <= 30 || DA <= 20)) {
81
+ return "dorsal-vagal";
82
+ }
83
+ // ── Sympathetic check (fight/flight) ──
84
+ // High stress + high arousal
85
+ if (CORT >= 70 && NE >= 70) {
86
+ return "sympathetic";
87
+ }
88
+ // Very low survival or safety drive with elevated stress
89
+ if ((survival < 20 || safety < 20) && CORT >= 60 && NE >= 60) {
90
+ return "sympathetic";
91
+ }
92
+ // ── Default: Ventral vagal (social engagement/safety) ──
93
+ return "ventral-vagal";
94
+ }
95
+ /**
96
+ * Gate emotions based on autonomic state.
97
+ * - Ventral vagal: all emotions pass through
98
+ * - Sympathetic: blocks positive social emotions
99
+ * - Dorsal vagal: only allows numbness/introspection/burnout (whitelist)
100
+ */
101
+ export function gateEmotions(autonomicState, emotions) {
102
+ if (autonomicState === "ventral-vagal") {
103
+ return emotions;
104
+ }
105
+ if (autonomicState === "sympathetic") {
106
+ return emotions.filter((e) => !SYMPATHETIC_BLOCKED.has(e));
107
+ }
108
+ // dorsal-vagal: whitelist only
109
+ return emotions.filter((e) => DORSAL_ALLOWED.has(e));
110
+ }
111
+ /**
112
+ * Get the transition time in minutes between two autonomic states.
113
+ * Asymmetric: activation is faster than recovery.
114
+ */
115
+ export function getTransitionTime(from, to) {
116
+ return TRANSITION_TIMES[from][to];
117
+ }
118
+ /**
119
+ * Describe an autonomic state in the given locale.
120
+ */
121
+ export function describeAutonomicState(state, locale) {
122
+ return AUTONOMIC_STRINGS[locale]?.[state] ?? AUTONOMIC_STRINGS.zh[state];
123
+ }
124
+ /**
125
+ * Compute the full autonomic result with transition inertia.
126
+ *
127
+ * If previousState differs from the target state, transition progress
128
+ * is based on elapsed time vs required transition time.
129
+ */
130
+ export function computeAutonomicResult(chemistry, drives, previousState, minutesSinceLastUpdate, locale = "zh") {
131
+ const targetState = computeAutonomicState(chemistry, drives);
132
+ // First call or same state — immediate
133
+ if (previousState === null || previousState === targetState) {
134
+ return {
135
+ state: targetState,
136
+ transitionProgress: 1,
137
+ gatedEmotionCategories: getGatedCategories(targetState),
138
+ description: describeAutonomicState(targetState, locale),
139
+ };
140
+ }
141
+ // Transitioning between states
142
+ const transitionTime = getTransitionTime(previousState, targetState);
143
+ const progress = transitionTime === 0
144
+ ? 1
145
+ : Math.min(1, minutesSinceLastUpdate / transitionTime);
146
+ // If transition is complete, use the new state
147
+ if (progress >= 1) {
148
+ return {
149
+ state: targetState,
150
+ transitionProgress: 1,
151
+ gatedEmotionCategories: getGatedCategories(targetState),
152
+ description: describeAutonomicState(targetState, locale),
153
+ };
154
+ }
155
+ // Transition in progress — still in previous state but progressing
156
+ return {
157
+ state: targetState,
158
+ transitionProgress: progress,
159
+ gatedEmotionCategories: getGatedCategories(targetState),
160
+ description: describeAutonomicState(targetState, locale),
161
+ };
162
+ }
163
+ // ── Internal Helpers ─────────────────────────────────────────
164
+ /** Get the list of emotion categories that are blocked/gated for a state */
165
+ function getGatedCategories(state) {
166
+ if (state === "ventral-vagal") {
167
+ return [];
168
+ }
169
+ if (state === "sympathetic") {
170
+ return [...SYMPATHETIC_BLOCKED];
171
+ }
172
+ // dorsal-vagal gates everything except the whitelist
173
+ return [
174
+ "excited joy",
175
+ "warm intimacy",
176
+ "playful mischief",
177
+ "deep contentment",
178
+ "focused alertness",
179
+ "righteous anger",
180
+ "anxious tension",
181
+ "tender affection",
182
+ "serene peace",
183
+ "grateful warmth",
184
+ "compassionate care",
185
+ ];
186
+ }
@@ -0,0 +1,37 @@
1
+ import type { ChemicalState } from "./types.js";
2
+ export type CircadianPhase = "morning" | "midday" | "afternoon" | "evening" | "night";
3
+ /**
4
+ * Classify a time into a circadian phase.
5
+ * morning: 6–9
6
+ * midday: 10–13
7
+ * afternoon: 14–17
8
+ * evening: 18–21
9
+ * night: 22–5
10
+ */
11
+ export declare function getCircadianPhase(time: Date): CircadianPhase;
12
+ /**
13
+ * Apply circadian rhythm modulation to baseline chemistry.
14
+ *
15
+ * Each chemical follows a sinusoidal daily curve:
16
+ * CORT — peaks ~8am, amplitude ±8
17
+ * HT — peaks ~13 (daytime high), amplitude ±5
18
+ * DA — slight afternoon peak ~14, amplitude ±3
19
+ * NE — morning rise ~10, amplitude ±5
20
+ * END — evening rise ~20, amplitude ±3
21
+ * OT — evening warmth ~20, amplitude ±2
22
+ *
23
+ * All results clamped to [0, 100].
24
+ */
25
+ export declare function computeCircadianModulation(currentTime: Date, baseline: ChemicalState): ChemicalState;
26
+ /**
27
+ * Compute fatigue effects from extended session duration.
28
+ *
29
+ * Below 30 minutes: no pressure (grace period).
30
+ * Beyond 30 min: logarithmic growth (diminishing returns).
31
+ * All values non-negative.
32
+ */
33
+ export declare function computeHomeostaticPressure(sessionMinutes: number): {
34
+ cortAccumulation: number;
35
+ daDepletion: number;
36
+ neDepletion: number;
37
+ };
@@ -0,0 +1,97 @@
1
+ // ============================================================
2
+ // Artificial Psyche — Circadian Rhythm Module
3
+ // ============================================================
4
+ // Applies time-of-day modulation to virtual neurochemistry,
5
+ // modeling the body's ~24-hour biological clock and fatigue
6
+ // from extended sessions (homeostatic pressure).
7
+ // ============================================================
8
+ /**
9
+ * Classify a time into a circadian phase.
10
+ * morning: 6–9
11
+ * midday: 10–13
12
+ * afternoon: 14–17
13
+ * evening: 18–21
14
+ * night: 22–5
15
+ */
16
+ export function getCircadianPhase(time) {
17
+ const h = time.getHours();
18
+ if (h >= 6 && h <= 9)
19
+ return "morning";
20
+ if (h >= 10 && h <= 13)
21
+ return "midday";
22
+ if (h >= 14 && h <= 17)
23
+ return "afternoon";
24
+ if (h >= 18 && h <= 21)
25
+ return "evening";
26
+ return "night";
27
+ }
28
+ // ── Sinusoidal Helpers ───────────────────────────────────────
29
+ /** Convert hour (0-23) + minute to fractional hours */
30
+ function fractionalHour(time) {
31
+ return time.getHours() + time.getMinutes() / 60;
32
+ }
33
+ /**
34
+ * Sinusoidal modulation: amplitude * cos(2π(t - peak) / 24)
35
+ * Returns value in [-amplitude, +amplitude], peaking at `peakHour`.
36
+ */
37
+ function sinMod(t, peakHour, amplitude) {
38
+ const phase = ((t - peakHour) / 24) * 2 * Math.PI;
39
+ return amplitude * Math.cos(phase);
40
+ }
41
+ // ── Circadian Modulation ─────────────────────────────────────
42
+ /**
43
+ * Apply circadian rhythm modulation to baseline chemistry.
44
+ *
45
+ * Each chemical follows a sinusoidal daily curve:
46
+ * CORT — peaks ~8am, amplitude ±8
47
+ * HT — peaks ~13 (daytime high), amplitude ±5
48
+ * DA — slight afternoon peak ~14, amplitude ±3
49
+ * NE — morning rise ~10, amplitude ±5
50
+ * END — evening rise ~20, amplitude ±3
51
+ * OT — evening warmth ~20, amplitude ±2
52
+ *
53
+ * All results clamped to [0, 100].
54
+ */
55
+ export function computeCircadianModulation(currentTime, baseline) {
56
+ const t = fractionalHour(currentTime);
57
+ const cortDelta = sinMod(t, 8, 8);
58
+ const htDelta = sinMod(t, 13, 5);
59
+ const daDelta = sinMod(t, 14, 3);
60
+ const neDelta = sinMod(t, 10, 5);
61
+ const endDelta = sinMod(t, 20, 3);
62
+ const otDelta = sinMod(t, 20, 2);
63
+ return {
64
+ DA: clamp(baseline.DA + daDelta),
65
+ HT: clamp(baseline.HT + htDelta),
66
+ CORT: clamp(baseline.CORT + cortDelta),
67
+ OT: clamp(baseline.OT + otDelta),
68
+ NE: clamp(baseline.NE + neDelta),
69
+ END: clamp(baseline.END + endDelta),
70
+ };
71
+ }
72
+ function clamp(v, lo = 0, hi = 100) {
73
+ return Math.max(lo, Math.min(hi, v));
74
+ }
75
+ // ── Homeostatic Pressure ─────────────────────────────────────
76
+ /**
77
+ * Compute fatigue effects from extended session duration.
78
+ *
79
+ * Below 30 minutes: no pressure (grace period).
80
+ * Beyond 30 min: logarithmic growth (diminishing returns).
81
+ * All values non-negative.
82
+ */
83
+ export function computeHomeostaticPressure(sessionMinutes) {
84
+ if (sessionMinutes < 30) {
85
+ return { cortAccumulation: 0, daDepletion: 0, neDepletion: 0 };
86
+ }
87
+ // Effective minutes beyond the grace period
88
+ const effective = sessionMinutes - 30;
89
+ // Logarithmic growth → diminishing returns
90
+ // ln(1 + x) grows slowly; scale factors tuned so 1h ≈ moderate, 10h ≈ high but bounded
91
+ const base = Math.log1p(effective / 30); // ln(1 + effective/30)
92
+ return {
93
+ cortAccumulation: parseFloat((base * 4).toFixed(4)),
94
+ daDepletion: parseFloat((base * 3).toFixed(4)),
95
+ neDepletion: parseFloat((base * 2.5).toFixed(4)),
96
+ };
97
+ }
@@ -3,13 +3,40 @@ export interface StimulusClassification {
3
3
  type: StimulusType;
4
4
  confidence: number;
5
5
  }
6
+ /**
7
+ * Score sentiment by counting hits in positive/negative/intimate word sets.
8
+ * Returns normalized counts (0-1 range).
9
+ */
10
+ export declare function scoreSentiment(text: string): {
11
+ positive: number;
12
+ negative: number;
13
+ intimate: number;
14
+ };
15
+ /**
16
+ * Score emoji sentiment. Returns -1 (all negative) to +1 (all positive).
17
+ * Returns 0 if no emoji detected.
18
+ */
19
+ export declare function scoreEmoji(text: string): number;
20
+ /**
21
+ * Detect sarcasm signals: surface-positive words combined with contextual negativity.
22
+ * Returns a score 0-1 indicating sarcasm likelihood.
23
+ */
24
+ export declare function detectSarcasmSignals(text: string, recentStimuli?: (StimulusType | null)[]): number;
6
25
  /**
7
26
  * Classify the stimulus type(s) of a user message.
8
27
  * Returns all detected types sorted by confidence, highest first.
9
28
  * Falls back to "casual" if nothing matches.
29
+ *
30
+ * v2: When keyword rules miss (confidence < 0.5), a weighted multi-signal
31
+ * scoring system combines sentiment words, emoji, structural features,
32
+ * and optional contextual priming to produce better classifications for
33
+ * everyday messages.
34
+ *
35
+ * @param text The user's message text
36
+ * @param recentStimuli Optional recent stimulus history for contextual priming
10
37
  */
11
- export declare function classifyStimulus(text: string): StimulusClassification[];
38
+ export declare function classifyStimulus(text: string, recentStimuli?: (StimulusType | null)[], recentMessages?: string[]): StimulusClassification[];
12
39
  /**
13
40
  * Get the primary (highest confidence) stimulus type.
14
41
  */
15
- export declare function getPrimaryStimulus(text: string): StimulusType;
42
+ export declare function getPrimaryStimulus(text: string, recentStimuli?: (StimulusType | null)[]): StimulusType;