psyche-ai 5.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.
- package/README.md +67 -0
- package/dist/adapters/openclaw.js +7 -0
- package/dist/autonomic.d.ts +59 -0
- package/dist/autonomic.js +239 -0
- package/dist/chemistry.d.ts +2 -2
- package/dist/chemistry.js +19 -4
- package/dist/circadian.d.ts +55 -0
- package/dist/circadian.js +163 -0
- package/dist/classify.d.ts +29 -2
- package/dist/classify.js +339 -53
- package/dist/cli.js +132 -13
- package/dist/core.d.ts +33 -2
- package/dist/core.js +263 -63
- package/dist/decision-bias.d.ts +13 -1
- package/dist/decision-bias.js +189 -0
- package/dist/drives.d.ts +11 -3
- package/dist/drives.js +181 -2
- package/dist/experiential-field.d.ts +25 -2
- package/dist/experiential-field.js +118 -125
- package/dist/i18n.js +14 -0
- package/dist/index.d.ts +18 -9
- package/dist/index.js +19 -9
- package/dist/primary-systems.d.ts +55 -0
- package/dist/primary-systems.js +218 -0
- package/dist/profiles.d.ts +12 -1
- package/dist/profiles.js +42 -0
- package/dist/prompt.d.ts +22 -1
- package/dist/prompt.js +311 -33
- package/dist/psyche-file.d.ts +36 -2
- package/dist/psyche-file.js +258 -6
- package/dist/types.d.ts +96 -3
- package/dist/types.js +21 -2
- package/openclaw.plugin.json +34 -0
- package/package.json +4 -2
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,59 @@
|
|
|
1
|
+
import type { ChemicalState, InnateDrives, Locale, EnergyBudgets } 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
|
+
processingDepth: number;
|
|
9
|
+
skippedStages: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface AutonomicTransition {
|
|
12
|
+
from: AutonomicState;
|
|
13
|
+
to: AutonomicState;
|
|
14
|
+
transitionMinutes: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compute the raw autonomic state from chemistry and drives.
|
|
18
|
+
* No transition inertia — returns the "target" state.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeAutonomicState(chemistry: ChemicalState, drives: InnateDrives): AutonomicState;
|
|
21
|
+
/**
|
|
22
|
+
* Gate emotions based on autonomic state.
|
|
23
|
+
* - Ventral vagal: all emotions pass through
|
|
24
|
+
* - Sympathetic: blocks positive social emotions
|
|
25
|
+
* - Dorsal vagal: only allows numbness/introspection/burnout (whitelist)
|
|
26
|
+
*/
|
|
27
|
+
export declare function gateEmotions(autonomicState: AutonomicState, emotions: string[]): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Get the transition time in minutes between two autonomic states.
|
|
30
|
+
* Asymmetric: activation is faster than recovery.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getTransitionTime(from: AutonomicState, to: AutonomicState): number;
|
|
33
|
+
/**
|
|
34
|
+
* Describe an autonomic state in the given locale.
|
|
35
|
+
*/
|
|
36
|
+
export declare function describeAutonomicState(state: AutonomicState, locale: Locale): string;
|
|
37
|
+
/**
|
|
38
|
+
* P10: Compute processing depth from autonomic state and chemistry.
|
|
39
|
+
*
|
|
40
|
+
* Processing depth represents the cognitive resource available for reflection:
|
|
41
|
+
* - 0 = pure System 1 (intuition only, no metacognition)
|
|
42
|
+
* - 1 = full System 2 (complete reflective capacity)
|
|
43
|
+
*
|
|
44
|
+
* This is a natural extension of autonomic state: you can't deeply reflect
|
|
45
|
+
* when your nervous system is in fight/flight/freeze mode.
|
|
46
|
+
*/
|
|
47
|
+
export declare function computeProcessingDepth(autonomicState: AutonomicState, chemistry: ChemicalState, baseline: ChemicalState, energyBudgets?: EnergyBudgets): {
|
|
48
|
+
depth: number;
|
|
49
|
+
skippedStages: string[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Compute the full autonomic result with transition inertia.
|
|
53
|
+
*
|
|
54
|
+
* If previousState differs from the target state, transition progress
|
|
55
|
+
* is based on elapsed time vs required transition time.
|
|
56
|
+
*
|
|
57
|
+
* Includes P10 processing depth (dual-process cognitive gating).
|
|
58
|
+
*/
|
|
59
|
+
export declare function computeAutonomicResult(chemistry: ChemicalState, drives: InnateDrives, previousState: AutonomicState | null, minutesSinceLastUpdate: number, locale?: Locale, baseline?: ChemicalState, energyBudgets?: EnergyBudgets): AutonomicResult;
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
* P10: Compute processing depth from autonomic state and chemistry.
|
|
126
|
+
*
|
|
127
|
+
* Processing depth represents the cognitive resource available for reflection:
|
|
128
|
+
* - 0 = pure System 1 (intuition only, no metacognition)
|
|
129
|
+
* - 1 = full System 2 (complete reflective capacity)
|
|
130
|
+
*
|
|
131
|
+
* This is a natural extension of autonomic state: you can't deeply reflect
|
|
132
|
+
* when your nervous system is in fight/flight/freeze mode.
|
|
133
|
+
*/
|
|
134
|
+
export function computeProcessingDepth(autonomicState, chemistry, baseline, energyBudgets) {
|
|
135
|
+
const { DA, HT, CORT, OT, NE, END } = chemistry;
|
|
136
|
+
// Chemical deviation from baseline (0-1)
|
|
137
|
+
let totalDeviation = 0;
|
|
138
|
+
const keys = ["DA", "HT", "CORT", "OT", "NE", "END"];
|
|
139
|
+
for (const k of keys) {
|
|
140
|
+
totalDeviation += Math.abs(chemistry[k] - baseline[k]);
|
|
141
|
+
}
|
|
142
|
+
const chemDeviation = Math.min(1, totalDeviation / 600);
|
|
143
|
+
// Base depth from autonomic state
|
|
144
|
+
let baseDepth;
|
|
145
|
+
if (autonomicState === "dorsal-vagal") {
|
|
146
|
+
baseDepth = 0;
|
|
147
|
+
}
|
|
148
|
+
else if (autonomicState === "sympathetic") {
|
|
149
|
+
// Higher CORT in sympathetic = less cognitive resource
|
|
150
|
+
baseDepth = CORT >= 60 ? 0.15 : 0.35;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// ventral-vagal: safe, most cognitive resource available
|
|
154
|
+
baseDepth = 0.85;
|
|
155
|
+
}
|
|
156
|
+
// Chemical deviation reduces depth (strong emotions = less reflection)
|
|
157
|
+
let depth = Math.max(0, Math.min(1, baseDepth * (1 - chemDeviation * 0.5)));
|
|
158
|
+
// v9: Low attention energy further reduces processing depth
|
|
159
|
+
if (energyBudgets && energyBudgets.attention < 30) {
|
|
160
|
+
const attentionPenalty = (30 - energyBudgets.attention) / 30 * 0.3;
|
|
161
|
+
depth = Math.max(0, depth - attentionPenalty);
|
|
162
|
+
}
|
|
163
|
+
// Map depth to skipped pipeline stages
|
|
164
|
+
const skippedStages = [];
|
|
165
|
+
if (depth < 0.8)
|
|
166
|
+
skippedStages.push("generative-self");
|
|
167
|
+
if (depth < 0.5) {
|
|
168
|
+
skippedStages.push("ethics");
|
|
169
|
+
skippedStages.push("shared-intentionality");
|
|
170
|
+
}
|
|
171
|
+
if (depth < 0.2) {
|
|
172
|
+
skippedStages.push("metacognition");
|
|
173
|
+
skippedStages.push("experiential-field");
|
|
174
|
+
}
|
|
175
|
+
return { depth, skippedStages };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Compute the full autonomic result with transition inertia.
|
|
179
|
+
*
|
|
180
|
+
* If previousState differs from the target state, transition progress
|
|
181
|
+
* is based on elapsed time vs required transition time.
|
|
182
|
+
*
|
|
183
|
+
* Includes P10 processing depth (dual-process cognitive gating).
|
|
184
|
+
*/
|
|
185
|
+
export function computeAutonomicResult(chemistry, drives, previousState, minutesSinceLastUpdate, locale = "zh", baseline, energyBudgets) {
|
|
186
|
+
const targetState = computeAutonomicState(chemistry, drives);
|
|
187
|
+
const effectiveBaseline = baseline ?? { DA: 50, HT: 50, CORT: 50, OT: 50, NE: 50, END: 50 };
|
|
188
|
+
// Helper to build full result with processing depth
|
|
189
|
+
const buildResult = (state, transitionProgress) => {
|
|
190
|
+
const { depth, skippedStages } = computeProcessingDepth(state, chemistry, effectiveBaseline, energyBudgets);
|
|
191
|
+
return {
|
|
192
|
+
state,
|
|
193
|
+
transitionProgress,
|
|
194
|
+
gatedEmotionCategories: getGatedCategories(state),
|
|
195
|
+
description: describeAutonomicState(state, locale),
|
|
196
|
+
processingDepth: depth,
|
|
197
|
+
skippedStages,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
// First call or same state — immediate
|
|
201
|
+
if (previousState === null || previousState === targetState) {
|
|
202
|
+
return buildResult(targetState, 1);
|
|
203
|
+
}
|
|
204
|
+
// Transitioning between states
|
|
205
|
+
const transitionTime = getTransitionTime(previousState, targetState);
|
|
206
|
+
const progress = transitionTime === 0
|
|
207
|
+
? 1
|
|
208
|
+
: Math.min(1, minutesSinceLastUpdate / transitionTime);
|
|
209
|
+
// If transition is complete, use the new state
|
|
210
|
+
if (progress >= 1) {
|
|
211
|
+
return buildResult(targetState, 1);
|
|
212
|
+
}
|
|
213
|
+
// Transition in progress
|
|
214
|
+
return buildResult(targetState, progress);
|
|
215
|
+
}
|
|
216
|
+
// ── Internal Helpers ─────────────────────────────────────────
|
|
217
|
+
/** Get the list of emotion categories that are blocked/gated for a state */
|
|
218
|
+
function getGatedCategories(state) {
|
|
219
|
+
if (state === "ventral-vagal") {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
if (state === "sympathetic") {
|
|
223
|
+
return [...SYMPATHETIC_BLOCKED];
|
|
224
|
+
}
|
|
225
|
+
// dorsal-vagal gates everything except the whitelist
|
|
226
|
+
return [
|
|
227
|
+
"excited joy",
|
|
228
|
+
"warm intimacy",
|
|
229
|
+
"playful mischief",
|
|
230
|
+
"deep contentment",
|
|
231
|
+
"focused alertness",
|
|
232
|
+
"righteous anger",
|
|
233
|
+
"anxious tension",
|
|
234
|
+
"tender affection",
|
|
235
|
+
"serene peace",
|
|
236
|
+
"grateful warmth",
|
|
237
|
+
"compassionate care",
|
|
238
|
+
];
|
|
239
|
+
}
|
package/dist/chemistry.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function clamp(v: number): number;
|
|
|
8
8
|
*
|
|
9
9
|
* decayed = baseline + (current - baseline) * factor^(minutes/60)
|
|
10
10
|
*/
|
|
11
|
-
export declare function applyDecay(current: ChemicalState, baseline: ChemicalState, minutesElapsed: number): ChemicalState;
|
|
11
|
+
export declare function applyDecay(current: ChemicalState, baseline: ChemicalState, minutesElapsed: number, decayRateModifiers?: Partial<Record<keyof ChemicalState, number>>): ChemicalState;
|
|
12
12
|
/**
|
|
13
13
|
* Apply a stimulus to the current state.
|
|
14
14
|
* Respects emotional inertia (maxDelta) and personality sensitivity.
|
|
@@ -16,7 +16,7 @@ export declare function applyDecay(current: ChemicalState, baseline: ChemicalSta
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function applyStimulus(current: ChemicalState, stimulus: StimulusType, sensitivity: number, maxDelta: number, logger?: {
|
|
18
18
|
warn: (msg: string) => void;
|
|
19
|
-
}): ChemicalState;
|
|
19
|
+
}, recentSameCount?: number): ChemicalState;
|
|
20
20
|
/**
|
|
21
21
|
* Apply emotional contagion: the detected user emotion partially
|
|
22
22
|
* influences the agent's chemistry.
|
package/dist/chemistry.js
CHANGED
|
@@ -133,13 +133,22 @@ export function clamp(v) {
|
|
|
133
133
|
*
|
|
134
134
|
* decayed = baseline + (current - baseline) * factor^(minutes/60)
|
|
135
135
|
*/
|
|
136
|
-
export function applyDecay(current, baseline, minutesElapsed) {
|
|
136
|
+
export function applyDecay(current, baseline, minutesElapsed, decayRateModifiers) {
|
|
137
137
|
if (minutesElapsed <= 0)
|
|
138
138
|
return { ...current };
|
|
139
139
|
const result = { ...current };
|
|
140
140
|
for (const key of CHEMICAL_KEYS) {
|
|
141
141
|
const speed = CHEMICAL_DECAY_SPEED[key];
|
|
142
|
-
const
|
|
142
|
+
const baseFactor = Math.pow(DECAY_FACTORS[speed], minutesElapsed / 60);
|
|
143
|
+
// v9: decayRateModifiers alter decay speed per chemical
|
|
144
|
+
// > 1 = slower recovery (trauma: factor closer to 1)
|
|
145
|
+
// < 1 = faster recovery (resilience: factor closer to 0)
|
|
146
|
+
let factor = baseFactor;
|
|
147
|
+
if (decayRateModifiers?.[key] !== undefined) {
|
|
148
|
+
const mod = decayRateModifiers[key];
|
|
149
|
+
// Raise the factor to the modifier power: mod>1 → slower decay, mod<1 → faster
|
|
150
|
+
factor = Math.pow(baseFactor, 1 / mod);
|
|
151
|
+
}
|
|
143
152
|
result[key] = clamp(baseline[key] + (current[key] - baseline[key]) * factor);
|
|
144
153
|
}
|
|
145
154
|
return result;
|
|
@@ -149,15 +158,21 @@ export function applyDecay(current, baseline, minutesElapsed) {
|
|
|
149
158
|
* Respects emotional inertia (maxDelta) and personality sensitivity.
|
|
150
159
|
* Logs a warning for unknown stimulus types.
|
|
151
160
|
*/
|
|
152
|
-
export function applyStimulus(current, stimulus, sensitivity, maxDelta, logger) {
|
|
161
|
+
export function applyStimulus(current, stimulus, sensitivity, maxDelta, logger, recentSameCount) {
|
|
153
162
|
const vector = STIMULUS_VECTORS[stimulus];
|
|
154
163
|
if (!vector) {
|
|
155
164
|
logger?.warn(t("log.unknown_stimulus", "zh", { type: stimulus }));
|
|
156
165
|
return { ...current };
|
|
157
166
|
}
|
|
167
|
+
// v9: Habituation — Weber-Fechner diminishing returns
|
|
168
|
+
// First 2 exposures: 100%. 3rd: 77%, 5th: 53%, 10th: 29%
|
|
169
|
+
let effectiveSensitivity = sensitivity;
|
|
170
|
+
if (recentSameCount !== undefined && recentSameCount > 2) {
|
|
171
|
+
effectiveSensitivity *= 1 / (1 + 0.3 * (recentSameCount - 2));
|
|
172
|
+
}
|
|
158
173
|
const result = { ...current };
|
|
159
174
|
for (const key of CHEMICAL_KEYS) {
|
|
160
|
-
const raw = vector[key] *
|
|
175
|
+
const raw = vector[key] * effectiveSensitivity;
|
|
161
176
|
const clamped = Math.max(-maxDelta, Math.min(maxDelta, raw));
|
|
162
177
|
result[key] = clamp(current[key] + clamped);
|
|
163
178
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ChemicalState, EnergyBudgets, StimulusType } 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
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Deplete energy budgets from a single interaction turn.
|
|
40
|
+
*
|
|
41
|
+
* - Attention: -3/turn base, extra for intellectual/conflict
|
|
42
|
+
* - Social energy: extraverts +2/turn (charging), introverts -3/turn (draining)
|
|
43
|
+
* - Decision capacity: varies by stimulus complexity
|
|
44
|
+
*
|
|
45
|
+
* Extraverts can exceed 100 (up to 120 — "supercharged").
|
|
46
|
+
*/
|
|
47
|
+
export declare function computeEnergyDepletion(budgets: EnergyBudgets, stimulus: StimulusType | null, isExtravert: boolean): EnergyBudgets;
|
|
48
|
+
/**
|
|
49
|
+
* Recover energy budgets during absence (between sessions or long pauses).
|
|
50
|
+
*
|
|
51
|
+
* - Attention: +20/hour
|
|
52
|
+
* - Social energy: extraverts -3/hour (drain when alone), introverts +15/hour (recharge)
|
|
53
|
+
* - Decision capacity: +25/hour
|
|
54
|
+
*/
|
|
55
|
+
export declare function computeEnergyRecovery(budgets: EnergyBudgets, minutesElapsed: number, isExtravert: boolean): EnergyBudgets;
|
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
}
|
|
98
|
+
// ── Energy Budgets (v9) ─────────────────────────────────────
|
|
99
|
+
// Finite cognitive/social resources that deplete during interaction.
|
|
100
|
+
// Extraverts GAIN social energy from interaction; introverts LOSE it.
|
|
101
|
+
/** Stimulus-specific attention costs (higher = more draining) */
|
|
102
|
+
const ATTENTION_COSTS = {
|
|
103
|
+
intellectual: 5,
|
|
104
|
+
conflict: 5,
|
|
105
|
+
authority: 4,
|
|
106
|
+
vulnerability: 3,
|
|
107
|
+
sarcasm: 3,
|
|
108
|
+
criticism: 3,
|
|
109
|
+
surprise: 2,
|
|
110
|
+
};
|
|
111
|
+
/** Stimulus-specific decision costs */
|
|
112
|
+
const DECISION_COSTS = {
|
|
113
|
+
conflict: 4,
|
|
114
|
+
authority: 4,
|
|
115
|
+
vulnerability: 3,
|
|
116
|
+
criticism: 2,
|
|
117
|
+
sarcasm: 2,
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Deplete energy budgets from a single interaction turn.
|
|
121
|
+
*
|
|
122
|
+
* - Attention: -3/turn base, extra for intellectual/conflict
|
|
123
|
+
* - Social energy: extraverts +2/turn (charging), introverts -3/turn (draining)
|
|
124
|
+
* - Decision capacity: varies by stimulus complexity
|
|
125
|
+
*
|
|
126
|
+
* Extraverts can exceed 100 (up to 120 — "supercharged").
|
|
127
|
+
*/
|
|
128
|
+
export function computeEnergyDepletion(budgets, stimulus, isExtravert) {
|
|
129
|
+
const extravertMax = 120;
|
|
130
|
+
const introvertMax = 100;
|
|
131
|
+
const max = isExtravert ? extravertMax : introvertMax;
|
|
132
|
+
// Attention: base -3, extra from stimulus
|
|
133
|
+
const attentionCost = 3 + (stimulus ? (ATTENTION_COSTS[stimulus] ?? 0) : 0);
|
|
134
|
+
const attention = clamp(budgets.attention - attentionCost, 0, 100);
|
|
135
|
+
// Social energy: E charges, I drains
|
|
136
|
+
const socialDelta = isExtravert ? 2 : -3;
|
|
137
|
+
const socialEnergy = clamp(budgets.socialEnergy + socialDelta, 0, max);
|
|
138
|
+
// Decision capacity: base -1, extra from stimulus
|
|
139
|
+
const decisionCost = 1 + (stimulus ? (DECISION_COSTS[stimulus] ?? 0) : 0);
|
|
140
|
+
const decisionCapacity = clamp(budgets.decisionCapacity - decisionCost, 0, 100);
|
|
141
|
+
return { attention, socialEnergy, decisionCapacity };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Recover energy budgets during absence (between sessions or long pauses).
|
|
145
|
+
*
|
|
146
|
+
* - Attention: +20/hour
|
|
147
|
+
* - Social energy: extraverts -3/hour (drain when alone), introverts +15/hour (recharge)
|
|
148
|
+
* - Decision capacity: +25/hour
|
|
149
|
+
*/
|
|
150
|
+
export function computeEnergyRecovery(budgets, minutesElapsed, isExtravert) {
|
|
151
|
+
if (minutesElapsed <= 0)
|
|
152
|
+
return { ...budgets };
|
|
153
|
+
const hours = minutesElapsed / 60;
|
|
154
|
+
const extravertMax = 120;
|
|
155
|
+
const introvertMax = 100;
|
|
156
|
+
const max = isExtravert ? extravertMax : introvertMax;
|
|
157
|
+
const attention = clamp(budgets.attention + hours * 20, 0, 100);
|
|
158
|
+
// E drains alone, I recharges alone
|
|
159
|
+
const socialDelta = isExtravert ? -3 * hours : 15 * hours;
|
|
160
|
+
const socialEnergy = clamp(budgets.socialEnergy + socialDelta, 0, max);
|
|
161
|
+
const decisionCapacity = clamp(budgets.decisionCapacity + hours * 25, 0, 100);
|
|
162
|
+
return { attention, socialEnergy, decisionCapacity };
|
|
163
|
+
}
|