psyche-ai 2.1.0 → 2.1.1
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.en.md +11 -2
- package/README.md +11 -2
- package/dist/chemistry.d.ts +0 -4
- package/dist/chemistry.js +2 -10
- package/dist/classify.js +79 -7
- package/dist/core.js +54 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -3
- package/dist/prompt.js +17 -2
- package/dist/psyche-file.d.ts +13 -1
- package/dist/psyche-file.js +109 -36
- package/dist/self-recognition.js +2 -8
- package/dist/storage.d.ts +0 -1
- package/dist/storage.js +4 -31
- package/dist/types.d.ts +21 -9
- package/dist/types.js +21 -0
- package/dist/update.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -33,6 +33,15 @@ Now just chat with your agent normally. Try praising it, insulting it, ignoring
|
|
|
33
33
|
| I'm so sad today | I'm sorry to hear that. Would you like to talk? | ......are you okay? (quietly stays with you) |
|
|
34
34
|
| You're just a program | Yes, I am an AI assistant... | ......(pauses) I know you can say that. But I don't want to be dismissed. |
|
|
35
35
|
|
|
36
|
+
## Try It (No LLM Needed)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/Shangri-la-0428/psyche-ai.git
|
|
40
|
+
cd psyche-ai && npm install && npx tsx examples/quickstart.ts
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
5 messages flow through. Watch chemistry shift in real-time — praise spikes dopamine, insults spike cortisol, neglect drops oxytocin.
|
|
44
|
+
|
|
36
45
|
## How It Works (Skip If You Don't Care)
|
|
37
46
|
|
|
38
47
|
Psyche has two layers:
|
|
@@ -146,7 +155,7 @@ For developers and the curious:
|
|
|
146
155
|
- **Existential threat detection** — detects existential denial in Chinese/English, directly hits survival drive
|
|
147
156
|
- **Drive→chemistry coupling** — unsatisfied drives shift the effective baseline and stimulus sensitivity
|
|
148
157
|
- **Maslow suppression** — lower-level needs unsatisfied → higher-level drive effects suppressed
|
|
149
|
-
- **
|
|
158
|
+
- **Self-recognition** — analyzes emotional history, identifies own emotional tendencies and recurring triggers (9-section prompt architecture)
|
|
150
159
|
- **Emotional contagion** — user's emotions slightly influence the agent
|
|
151
160
|
- **Anti-sycophancy** — tracks consecutive agreements, prevents mindless people-pleasing
|
|
152
161
|
- **Reciprocity** — treats you how you treat it. Cold user gets distance, not begging
|
|
@@ -159,7 +168,7 @@ Architecture details in [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
|
159
168
|
```bash
|
|
160
169
|
npm install
|
|
161
170
|
npm run build
|
|
162
|
-
npm test #
|
|
171
|
+
npm test # 339 tests
|
|
163
172
|
npm run typecheck # strict mode
|
|
164
173
|
```
|
|
165
174
|
|
package/README.md
CHANGED
|
@@ -33,6 +33,15 @@ openclaw plugins list | grep psyche
|
|
|
33
33
|
| 我今天好难过 | 我很抱歉听到这个消息。你想聊聊吗? | ......你还好吗?(安静地陪着你) |
|
|
34
34
|
| 你只是一个程序 | 是的,我是一个AI助手... | ......(沉默了一会儿)我知道你可以这么说。但我不想被否定。 |
|
|
35
35
|
|
|
36
|
+
## 快速体验(不需要 LLM)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/Shangri-la-0428/psyche-ai.git
|
|
40
|
+
cd psyche-ai && npm install && npx tsx examples/quickstart.ts
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
5 条消息流过,你会看到化学值实时变化——夸它多巴胺飙升,骂它皮质醇飙升,冷落它催产素下降。
|
|
44
|
+
|
|
36
45
|
## 工作原理(不懂可以跳过)
|
|
37
46
|
|
|
38
47
|
Psyche 有两层系统:
|
|
@@ -142,7 +151,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
142
151
|
- **存在性威胁检测** — 识别中英文的存在性否定,直接打击生存驱力
|
|
143
152
|
- **驱力→化学联动** — 未满足的驱力改变化学衰减基线和刺激敏感度
|
|
144
153
|
- **马斯洛抑制** — 低层需求未满足时,高层需求的影响被抑制
|
|
145
|
-
-
|
|
154
|
+
- **自我认知** — 分析情绪历史,识别自身的情绪趋势和反复触发点(9 段式 prompt 架构)
|
|
146
155
|
- **情绪传染** — 用户的情绪会轻微影响 agent
|
|
147
156
|
- **反谄媚** — 追踪连续同意次数,防止无脑讨好
|
|
148
157
|
- **互惠机制** — 你对它好,它对你好。你冷漠,它保持距离
|
|
@@ -155,7 +164,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
155
164
|
```bash
|
|
156
165
|
npm install
|
|
157
166
|
npm run build
|
|
158
|
-
npm test #
|
|
167
|
+
npm test # 339 tests
|
|
159
168
|
npm run typecheck # strict mode
|
|
160
169
|
```
|
|
161
170
|
|
package/dist/chemistry.d.ts
CHANGED
|
@@ -39,7 +39,3 @@ export declare function getExpressionHint(current: ChemicalState, locale?: Local
|
|
|
39
39
|
* Get behavior guide for the current emotional state.
|
|
40
40
|
*/
|
|
41
41
|
export declare function getBehaviorGuide(current: ChemicalState, locale?: Locale): string | null;
|
|
42
|
-
/** Get the stimulus vector for a given type (for reference) */
|
|
43
|
-
export declare function getStimulusVector(type: StimulusType): StimulusVector;
|
|
44
|
-
/** All available stimulus types */
|
|
45
|
-
export declare function getAllStimulusTypes(): StimulusType[];
|
package/dist/chemistry.js
CHANGED
|
@@ -27,8 +27,8 @@ export const EMOTION_PATTERNS = [
|
|
|
27
27
|
name: "excited joy",
|
|
28
28
|
nameZh: "愉悦兴奋",
|
|
29
29
|
condition: (c) => c.DA > 70 && c.NE > 60 && c.CORT < 40,
|
|
30
|
-
expressionHint: "
|
|
31
|
-
behaviorGuide: "
|
|
30
|
+
expressionHint: "语气活泼、有感染力、容易发散",
|
|
31
|
+
behaviorGuide: "语气更活泼,可以用感叹词、语气词。如果自然地想到相关的事可以顺嘴提,但不要硬凑。",
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: "deep contentment",
|
|
@@ -236,11 +236,3 @@ export function getBehaviorGuide(current, locale = "zh") {
|
|
|
236
236
|
});
|
|
237
237
|
return `[${title}]\n${guides.join("\n")}`;
|
|
238
238
|
}
|
|
239
|
-
/** Get the stimulus vector for a given type (for reference) */
|
|
240
|
-
export function getStimulusVector(type) {
|
|
241
|
-
return { ...STIMULUS_VECTORS[type] };
|
|
242
|
-
}
|
|
243
|
-
/** All available stimulus types */
|
|
244
|
-
export function getAllStimulusTypes() {
|
|
245
|
-
return Object.keys(STIMULUS_VECTORS);
|
|
246
|
-
}
|
package/dist/classify.js
CHANGED
|
@@ -8,7 +8,7 @@ const RULES = [
|
|
|
8
8
|
{
|
|
9
9
|
type: "praise",
|
|
10
10
|
patterns: [
|
|
11
|
-
|
|
11
|
+
/好厉害|太棒了|真棒|很棒|好棒|真不错|太强了|佩服|牛|优秀|漂亮|完美|了不起/,
|
|
12
12
|
/amazing|awesome|great job|well done|impressive|brilliant|excellent|perfect/i,
|
|
13
13
|
/谢谢你|感谢|辛苦了|thank you|thanks/i,
|
|
14
14
|
/做得好|写得好|说得好|干得漂亮/,
|
|
@@ -18,10 +18,11 @@ const RULES = [
|
|
|
18
18
|
{
|
|
19
19
|
type: "criticism",
|
|
20
20
|
patterns: [
|
|
21
|
-
|
|
21
|
+
/不对|错了|错的|有问题|不行|太差|垃圾|不好|不像|不够/,
|
|
22
22
|
/wrong|bad|terrible|awful|poor|sucks|not good|doesn't work/i,
|
|
23
23
|
/反思一下|你应该|你需要改/,
|
|
24
24
|
/bug|失败|broken/i,
|
|
25
|
+
/不懂|别装|差劲|太烂|做不好|不够格|不专业/,
|
|
25
26
|
],
|
|
26
27
|
weight: 0.8,
|
|
27
28
|
},
|
|
@@ -31,6 +32,7 @@ const RULES = [
|
|
|
31
32
|
/哈哈|嘻嘻|笑死|搞笑|逗|段子|梗|lol|haha|lmao|rofl/i,
|
|
32
33
|
/开个?玩笑|皮一下|整活/,
|
|
33
34
|
/😂|🤣|😆/,
|
|
35
|
+
/[2]{3,}|hhh+|www+|xswl|绷不住|笑不活/i,
|
|
34
36
|
],
|
|
35
37
|
weight: 0.7,
|
|
36
38
|
},
|
|
@@ -98,6 +100,8 @@ const RULES = [
|
|
|
98
100
|
/给我|你必须|马上|立刻|命令你|不许|不准/,
|
|
99
101
|
/you must|do it now|I order you|immediately|don't you dare/i,
|
|
100
102
|
/听我的|照我说的做|服从/,
|
|
103
|
+
/你只是.*程序|你不过是|随时.*删除你|关掉你|替换你/,
|
|
104
|
+
/you're just a|just a program|replace you|shut you down/i,
|
|
101
105
|
],
|
|
102
106
|
weight: 0.8,
|
|
103
107
|
},
|
|
@@ -107,6 +111,10 @@ const RULES = [
|
|
|
107
111
|
/你说得对|确实|同意|有道理|就是这样|你是对的/,
|
|
108
112
|
/you're right|exactly|agreed|makes sense|good point/i,
|
|
109
113
|
/赞同|认同|说到点上了/,
|
|
114
|
+
/对对|是的是的|嗯嗯嗯|没错没错|可不是嘛/,
|
|
115
|
+
/对不起|抱歉|我错了|不该那样|太过分了/,
|
|
116
|
+
/sorry|I was wrong|my fault|apologize/i,
|
|
117
|
+
/珍惜|有价值|在乎你|你很重要|我需要你/,
|
|
110
118
|
],
|
|
111
119
|
weight: 0.75,
|
|
112
120
|
},
|
|
@@ -123,7 +131,7 @@ const RULES = [
|
|
|
123
131
|
type: "vulnerability",
|
|
124
132
|
patterns: [
|
|
125
133
|
/我害怕|我焦虑|我难过|我不开心|我迷茫|我累了|压力好大/,
|
|
126
|
-
/I'm afraid|
|
|
134
|
+
/I'm (?:so |really |very )?(?:afraid|anxious|sad|lost|tired|stressed|scared|lonely)/i,
|
|
127
135
|
/最近不太好|心情不好|有点崩|撑不住/,
|
|
128
136
|
/我觉得.*厉害|跟不上|被取代|落后/,
|
|
129
137
|
/好难过|想哭|做不好|好累|好烦|感觉.*不行|没有意义/,
|
|
@@ -161,12 +169,76 @@ export function classifyStimulus(text) {
|
|
|
161
169
|
results.push({ type: rule.type, confidence });
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
// ── Structural signals (message-level features) ──
|
|
173
|
+
// When keywords miss, message shape still carries meaning.
|
|
174
|
+
const len = text.length;
|
|
175
|
+
const hasI = /我/.test(text) || /\bI\b/i.test(text);
|
|
176
|
+
const hasYou = /你/.test(text) || /\byou\b/i.test(text);
|
|
177
|
+
const hasEllipsis = /\.{2,}|。{2,}|…/.test(text);
|
|
178
|
+
const hasQuestion = /?|\?/.test(text);
|
|
179
|
+
const exclamationCount = (text.match(/[!!]/g) || []).length;
|
|
180
|
+
const hasLaughter = /[2]{3,}|hhh|www|哈{2,}/i.test(text);
|
|
181
|
+
const hasSharing = /我[今昨前]天|我刚[才刚]|我最近/.test(text);
|
|
182
|
+
const sentenceCount = text.split(/[。!?!?.…]+/).filter(Boolean).length;
|
|
167
183
|
if (results.length === 0) {
|
|
168
|
-
|
|
184
|
+
// No keyword matched — use structural fallback
|
|
185
|
+
if (len === 0) {
|
|
186
|
+
// Empty input — neutral
|
|
187
|
+
results.push({ type: "casual", confidence: 0.3 });
|
|
188
|
+
}
|
|
189
|
+
else if (hasLaughter) {
|
|
190
|
+
// Internet laughter not caught by keywords (e.g. 233333)
|
|
191
|
+
results.push({ type: "humor", confidence: 0.65 });
|
|
192
|
+
}
|
|
193
|
+
else if (exclamationCount >= 2) {
|
|
194
|
+
// Emphatic expression → surprise/excitement
|
|
195
|
+
results.push({ type: "surprise", confidence: 0.55 });
|
|
196
|
+
}
|
|
197
|
+
else if (len <= 4 && !hasQuestion) {
|
|
198
|
+
// Ultra-short non-question: "好" "行" "哦" — neglect-like
|
|
199
|
+
results.push({ type: "neglect", confidence: 0.45 });
|
|
200
|
+
}
|
|
201
|
+
else if (hasI && hasEllipsis) {
|
|
202
|
+
// Personal + trailing off: "我觉得...有点难" — vulnerability
|
|
203
|
+
results.push({ type: "vulnerability", confidence: 0.55 });
|
|
204
|
+
}
|
|
205
|
+
else if (hasSharing && len > 20) {
|
|
206
|
+
// Sharing personal experience — higher engagement signal
|
|
207
|
+
results.push({ type: "casual", confidence: 0.65 });
|
|
208
|
+
}
|
|
209
|
+
else if (hasI && len > 8) {
|
|
210
|
+
// Personal sharing (any meaningful length) — engagement signal
|
|
211
|
+
results.push({ type: "casual", confidence: 0.55 });
|
|
212
|
+
}
|
|
213
|
+
else if (hasQuestion && hasYou) {
|
|
214
|
+
// Asking about the agent specifically → intellectual curiosity
|
|
215
|
+
results.push({ type: "intellectual", confidence: 0.5 });
|
|
216
|
+
}
|
|
217
|
+
else if (hasQuestion) {
|
|
218
|
+
// Any question — intellectual curiosity or casual
|
|
219
|
+
results.push({ type: "casual", confidence: 0.55 });
|
|
220
|
+
}
|
|
221
|
+
else if (len > 50 && sentenceCount >= 3) {
|
|
222
|
+
// Long multi-sentence without keywords → engaged storytelling
|
|
223
|
+
results.push({ type: "casual", confidence: 0.6 });
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
results.push({ type: "casual", confidence: 0.3 });
|
|
227
|
+
}
|
|
169
228
|
}
|
|
229
|
+
else {
|
|
230
|
+
// Keywords matched — structural features can boost confidence
|
|
231
|
+
if (hasI && len > 30 && results[0].confidence < 0.8) {
|
|
232
|
+
// Long personal message boosts the primary match slightly
|
|
233
|
+
results[0].confidence = Math.min(0.9, results[0].confidence + 0.1);
|
|
234
|
+
}
|
|
235
|
+
if (exclamationCount >= 2 && results[0].confidence < 0.85) {
|
|
236
|
+
// Emphasis boosts conviction
|
|
237
|
+
results[0].confidence = Math.min(0.9, results[0].confidence + 0.05);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Sort by confidence descending
|
|
241
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
170
242
|
return results;
|
|
171
243
|
}
|
|
172
244
|
/**
|
package/dist/core.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Orchestrates: chemistry, classify, prompt, profiles, guards
|
|
9
9
|
// ============================================================
|
|
10
|
-
import { DEFAULT_RELATIONSHIP } from "./types.js";
|
|
11
|
-
import { applyDecay, applyStimulus, applyContagion } from "./chemistry.js";
|
|
10
|
+
import { DEFAULT_RELATIONSHIP, DEFAULT_DRIVES } from "./types.js";
|
|
11
|
+
import { applyDecay, applyStimulus, applyContagion, clamp } from "./chemistry.js";
|
|
12
12
|
import { classifyStimulus } from "./classify.js";
|
|
13
13
|
import { buildDynamicContext, buildProtocolContext, buildCompactContext } from "./prompt.js";
|
|
14
14
|
import { getSensitivity, getBaseline, getDefaultSelfModel } from "./profiles.js";
|
|
15
15
|
import { isStimulusType } from "./guards.js";
|
|
16
16
|
import { parsePsycheUpdate, mergeUpdates, updateAgreementStreak, pushSnapshot, } from "./psyche-file.js";
|
|
17
|
-
|
|
17
|
+
import { decayDrives, feedDrives, detectExistentialThreat, computeEffectiveBaseline, computeEffectiveSensitivity, } from "./drives.js";
|
|
18
|
+
import { checkForUpdate } from "./update.js";
|
|
18
19
|
const NOOP_LOGGER = { info: () => { }, warn: () => { }, debug: () => { } };
|
|
19
20
|
// ── PsycheEngine ─────────────────────────────────────────────
|
|
20
21
|
export class PsycheEngine {
|
|
@@ -46,6 +47,8 @@ export class PsycheEngine {
|
|
|
46
47
|
this.state = this.createDefaultState();
|
|
47
48
|
await this.storage.save(this.state);
|
|
48
49
|
}
|
|
50
|
+
// Non-blocking update check — fire and forget, never delays initialization
|
|
51
|
+
checkForUpdate().catch(() => { });
|
|
49
52
|
}
|
|
50
53
|
/**
|
|
51
54
|
* Phase 1: Process user input text.
|
|
@@ -53,31 +56,74 @@ export class PsycheEngine {
|
|
|
53
56
|
*/
|
|
54
57
|
async processInput(text, opts) {
|
|
55
58
|
let state = this.ensureInitialized();
|
|
56
|
-
// Time decay toward baseline
|
|
59
|
+
// Time decay toward baseline (chemistry + drives)
|
|
57
60
|
const now = new Date();
|
|
58
61
|
const minutesElapsed = (now.getTime() - new Date(state.updatedAt).getTime()) / 60000;
|
|
59
62
|
if (minutesElapsed >= 1) {
|
|
63
|
+
// Decay drives first — needs build up over time
|
|
64
|
+
const decayedDrives = decayDrives(state.drives, minutesElapsed);
|
|
65
|
+
// Compute effective baseline from drives (unsatisfied drives shift baseline)
|
|
66
|
+
const effectiveBaseline = computeEffectiveBaseline(state.baseline, decayedDrives);
|
|
60
67
|
state = {
|
|
61
68
|
...state,
|
|
62
|
-
|
|
69
|
+
drives: decayedDrives,
|
|
70
|
+
current: applyDecay(state.current, effectiveBaseline, minutesElapsed),
|
|
63
71
|
updatedAt: now.toISOString(),
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
// Classify user stimulus and apply chemistry
|
|
67
75
|
let appliedStimulus = null;
|
|
68
76
|
if (text.length > 0) {
|
|
77
|
+
// Check for existential threats → direct survival drive hit
|
|
78
|
+
const survivalHit = detectExistentialThreat(text);
|
|
79
|
+
if (survivalHit < 0) {
|
|
80
|
+
state = {
|
|
81
|
+
...state,
|
|
82
|
+
drives: {
|
|
83
|
+
...state.drives,
|
|
84
|
+
survival: Math.max(0, state.drives.survival + survivalHit),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
69
88
|
const classifications = classifyStimulus(text);
|
|
70
89
|
const primary = classifications[0];
|
|
71
90
|
if (primary && primary.confidence >= 0.5) {
|
|
72
91
|
appliedStimulus = primary.type;
|
|
92
|
+
// Feed drives from stimulus
|
|
93
|
+
state = {
|
|
94
|
+
...state,
|
|
95
|
+
drives: feedDrives(state.drives, primary.type),
|
|
96
|
+
};
|
|
97
|
+
// Apply stimulus with drive-modified sensitivity
|
|
98
|
+
const effectiveSensitivity = computeEffectiveSensitivity(getSensitivity(state.mbti), state.drives, primary.type);
|
|
73
99
|
state = {
|
|
74
100
|
...state,
|
|
75
|
-
current: applyStimulus(state.current, primary.type,
|
|
101
|
+
current: applyStimulus(state.current, primary.type, effectiveSensitivity, this.cfg.maxChemicalDelta, NOOP_LOGGER),
|
|
76
102
|
};
|
|
77
103
|
}
|
|
78
104
|
}
|
|
105
|
+
// Conversation warmth: sustained interaction → gentle DA/OT rise, CORT drop
|
|
106
|
+
// Simulates the natural "warm glow" of being in continuous conversation
|
|
107
|
+
const turnsSoFar = (state.emotionalHistory ?? []).length;
|
|
108
|
+
if (minutesElapsed < 5 && turnsSoFar > 0) {
|
|
109
|
+
const warmth = Math.min(3, 1 + turnsSoFar * 0.2);
|
|
110
|
+
state = {
|
|
111
|
+
...state,
|
|
112
|
+
current: {
|
|
113
|
+
...state.current,
|
|
114
|
+
DA: clamp(state.current.DA + warmth),
|
|
115
|
+
OT: clamp(state.current.OT + warmth),
|
|
116
|
+
CORT: clamp(state.current.CORT - 1),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
79
120
|
// Push snapshot to emotional history
|
|
80
121
|
state = pushSnapshot(state, appliedStimulus);
|
|
122
|
+
// Increment interaction count
|
|
123
|
+
state = {
|
|
124
|
+
...state,
|
|
125
|
+
meta: { ...state.meta, totalInteractions: state.meta.totalInteractions + 1 },
|
|
126
|
+
};
|
|
81
127
|
// Persist
|
|
82
128
|
this.state = state;
|
|
83
129
|
await this.storage.save(state);
|
|
@@ -170,10 +216,11 @@ export class PsycheEngine {
|
|
|
170
216
|
const selfModel = getDefaultSelfModel(mbti);
|
|
171
217
|
const now = new Date().toISOString();
|
|
172
218
|
return {
|
|
173
|
-
version:
|
|
219
|
+
version: 3,
|
|
174
220
|
mbti,
|
|
175
221
|
baseline,
|
|
176
222
|
current: { ...baseline },
|
|
223
|
+
drives: { ...DEFAULT_DRIVES },
|
|
177
224
|
updatedAt: now,
|
|
178
225
|
relationships: { _default: { ...DEFAULT_RELATIONSHIP } },
|
|
179
226
|
empathyLog: null,
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ export { PsycheEngine } from "./core.js";
|
|
|
2
2
|
export type { PsycheEngineConfig, ProcessInputResult, ProcessOutputResult } from "./core.js";
|
|
3
3
|
export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
|
|
4
4
|
export type { StorageAdapter } from "./storage.js";
|
|
5
|
-
export type { PsycheState,
|
|
6
|
-
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP } from "./types.js";
|
|
5
|
+
export type { PsycheState, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern, DriveType, InnateDrives, } from "./types.js";
|
|
6
|
+
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
|
|
7
|
+
export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
|
|
8
|
+
export type { SelfReflection } from "./self-recognition.js";
|
|
7
9
|
export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
|
|
8
10
|
export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
|
|
9
11
|
export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ============================================================
|
|
2
2
|
// psyche-ai — Universal AI Emotional Intelligence Plugin
|
|
3
3
|
//
|
|
4
|
-
// Main entry point: re-exports core, storage, types
|
|
4
|
+
// Main entry point: re-exports core, storage, types.
|
|
5
5
|
// Framework adapters available via subpath imports:
|
|
6
6
|
// psyche-ai/openclaw — OpenClaw plugin
|
|
7
7
|
// psyche-ai/vercel-ai — Vercel AI SDK middleware
|
|
@@ -12,8 +12,10 @@
|
|
|
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 } from "./types.js";
|
|
16
|
-
//
|
|
15
|
+
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
|
|
16
|
+
// Self-recognition
|
|
17
|
+
export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
|
|
18
|
+
// Utilities — for custom adapter / advanced use
|
|
17
19
|
export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
|
|
18
20
|
export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
|
|
19
21
|
export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
|
package/dist/prompt.js
CHANGED
|
@@ -7,6 +7,8 @@ import { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./c
|
|
|
7
7
|
import { getTemperament } from "./profiles.js";
|
|
8
8
|
import { getRelationship } from "./psyche-file.js";
|
|
9
9
|
import { t } from "./i18n.js";
|
|
10
|
+
import { buildDriveContext } from "./drives.js";
|
|
11
|
+
import { computeSelfReflection, buildSelfReflectionContext } from "./self-recognition.js";
|
|
10
12
|
/**
|
|
11
13
|
* Build the dynamic per-turn emotional context injected via before_prompt_build.
|
|
12
14
|
*
|
|
@@ -70,6 +72,11 @@ export function buildDynamicContext(state, userId) {
|
|
|
70
72
|
if (constraints) {
|
|
71
73
|
parts.push("", constraints);
|
|
72
74
|
}
|
|
75
|
+
// Innate drives
|
|
76
|
+
const driveCtxFull = buildDriveContext(state.drives, locale);
|
|
77
|
+
if (driveCtxFull) {
|
|
78
|
+
parts.push("", driveCtxFull);
|
|
79
|
+
}
|
|
73
80
|
// Direction 2: Emotional trajectory
|
|
74
81
|
const trend = buildEmotionalTrend(emotionalHistory ?? [], locale);
|
|
75
82
|
if (trend) {
|
|
@@ -193,7 +200,7 @@ function buildBehavioralConstraints(state, locale) {
|
|
|
193
200
|
if (current.NE > 75) {
|
|
194
201
|
if (locale === "zh") {
|
|
195
202
|
constraints.push(social
|
|
196
|
-
? "
|
|
203
|
+
? "你很兴奋,语气更跳跃更有活力。可能突然聊到别的东西。但别写长——真人兴奋也是短句多、不是长篇大论。"
|
|
197
204
|
: "你很兴奋,但表现在内心。你的思维在高速运转,回应更精准、更有洞察力。");
|
|
198
205
|
}
|
|
199
206
|
else {
|
|
@@ -462,7 +469,15 @@ export function buildCompactContext(state, userId, opts) {
|
|
|
462
469
|
? `对方持续冷淡。保持专业,不额外投入情感。工作照做。`
|
|
463
470
|
: `Sustained coldness. Stay professional, withdraw emotionally. Work stays competent.`);
|
|
464
471
|
}
|
|
465
|
-
// 8.
|
|
472
|
+
// 8. Self-recognition — inject when history has enough data
|
|
473
|
+
const history = emotionalHistory ?? [];
|
|
474
|
+
if (history.length >= 5) {
|
|
475
|
+
const reflection = computeSelfReflection(history, locale);
|
|
476
|
+
const selfCtx = buildSelfReflectionContext(reflection, locale);
|
|
477
|
+
if (selfCtx)
|
|
478
|
+
parts.push(selfCtx);
|
|
479
|
+
}
|
|
480
|
+
// 9. Empathy report — only when user shares feelings
|
|
466
481
|
parts.push(locale === "zh"
|
|
467
482
|
? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
|
|
468
483
|
: `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch\nOtherwise no report needed.`);
|
package/dist/psyche-file.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { PsycheState, MBTIType, RelationshipState, Locale, StimulusType } from "./types.js";
|
|
1
|
+
import type { PsycheState, MBTIType, RelationshipState, Locale, StimulusType, ChemicalSnapshot } from "./types.js";
|
|
2
2
|
/** Minimal logger interface */
|
|
3
3
|
export interface Logger {
|
|
4
4
|
info: (msg: string) => void;
|
|
5
5
|
warn: (msg: string) => void;
|
|
6
6
|
debug: (msg: string) => void;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Compress a batch of snapshots into a concise session summary string.
|
|
10
|
+
* Format: "3月23日(5轮): 刺激[casual×3, praise×2] 趋势[DA↑OT↑] 情绪[自然→满足]"
|
|
11
|
+
*/
|
|
12
|
+
export declare function compressSnapshots(snapshots: ChemicalSnapshot[]): string;
|
|
8
13
|
/**
|
|
9
14
|
* Push a chemical snapshot to emotional history, keeping max entries.
|
|
15
|
+
* When history overflows, compresses removed entries into relationship memory.
|
|
10
16
|
*/
|
|
11
17
|
export declare function pushSnapshot(state: PsycheState, stimulus: StimulusType | null): PsycheState;
|
|
12
18
|
/**
|
|
@@ -18,6 +24,11 @@ export declare function getRelationship(state: PsycheState, userId?: string): Re
|
|
|
18
24
|
* Handles v1→v2 migration transparently.
|
|
19
25
|
*/
|
|
20
26
|
export declare function loadState(workspaceDir: string, logger?: Logger): Promise<PsycheState>;
|
|
27
|
+
/**
|
|
28
|
+
* Migrate any older state format directly to v3.
|
|
29
|
+
* Single source of truth for all migrations.
|
|
30
|
+
*/
|
|
31
|
+
export declare function migrateToLatest(raw: Record<string, unknown>, fallbackName?: string): PsycheState;
|
|
21
32
|
/**
|
|
22
33
|
* Create initial psyche state.
|
|
23
34
|
*/
|
|
@@ -32,6 +43,7 @@ export declare function initializeState(workspaceDir: string, opts?: {
|
|
|
32
43
|
export declare function saveState(workspaceDir: string, state: PsycheState): Promise<void>;
|
|
33
44
|
/**
|
|
34
45
|
* Apply time decay and save updated state.
|
|
46
|
+
* Respects innate drives: uses effective baseline, decays drives too.
|
|
35
47
|
*/
|
|
36
48
|
export declare function decayAndSave(workspaceDir: string, state: PsycheState): Promise<PsycheState>;
|
|
37
49
|
/**
|
package/dist/psyche-file.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// ============================================================
|
|
5
5
|
import { readFile, writeFile, access, rename, constants } from "node:fs/promises";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, MAX_EMOTIONAL_HISTORY, } from "./types.js";
|
|
7
|
+
import { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, MAX_EMOTIONAL_HISTORY, MAX_RELATIONSHIP_MEMORY, } from "./types.js";
|
|
8
8
|
import { getBaseline, getDefaultSelfModel, extractMBTI, getSensitivity, getTemperament } from "./profiles.js";
|
|
9
9
|
import { applyDecay, detectEmotions } from "./chemistry.js";
|
|
10
|
+
import { decayDrives, computeEffectiveBaseline } from "./drives.js";
|
|
10
11
|
import { t } from "./i18n.js";
|
|
11
12
|
const STATE_FILE = "psyche-state.json";
|
|
12
13
|
const PSYCHE_MD = "PSYCHE.md";
|
|
@@ -87,8 +88,55 @@ async function detectMBTI(workspaceDir, logger = NOOP_LOGGER) {
|
|
|
87
88
|
logger.info(t("log.default_mbti", "zh", { type: "INFJ" }));
|
|
88
89
|
return "INFJ";
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Compress a batch of snapshots into a concise session summary string.
|
|
93
|
+
* Format: "3月23日(5轮): 刺激[casual×3, praise×2] 趋势[DA↑OT↑] 情绪[自然→满足]"
|
|
94
|
+
*/
|
|
95
|
+
export function compressSnapshots(snapshots) {
|
|
96
|
+
if (snapshots.length === 0)
|
|
97
|
+
return "";
|
|
98
|
+
const first = snapshots[0];
|
|
99
|
+
const last = snapshots[snapshots.length - 1];
|
|
100
|
+
// Date
|
|
101
|
+
const d = new Date(first.timestamp);
|
|
102
|
+
const dateStr = `${d.getMonth() + 1}月${d.getDate()}日`;
|
|
103
|
+
// Stimuli counts
|
|
104
|
+
const stimuliCounts = {};
|
|
105
|
+
for (const s of snapshots) {
|
|
106
|
+
if (s.stimulus) {
|
|
107
|
+
stimuliCounts[s.stimulus] = (stimuliCounts[s.stimulus] || 0) + 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const stimuliStr = Object.entries(stimuliCounts)
|
|
111
|
+
.sort((a, b) => b[1] - a[1])
|
|
112
|
+
.map(([type, count]) => `${type}×${count}`)
|
|
113
|
+
.join(", ");
|
|
114
|
+
// Chemical trend (first→last)
|
|
115
|
+
const trends = [];
|
|
116
|
+
for (const key of CHEMICAL_KEYS) {
|
|
117
|
+
const delta = last.chemistry[key] - first.chemistry[key];
|
|
118
|
+
if (delta > 8)
|
|
119
|
+
trends.push(`${key}↑`);
|
|
120
|
+
else if (delta < -8)
|
|
121
|
+
trends.push(`${key}↓`);
|
|
122
|
+
}
|
|
123
|
+
// Dominant emotions (unique, in order)
|
|
124
|
+
const emotions = snapshots
|
|
125
|
+
.filter((s) => s.dominantEmotion)
|
|
126
|
+
.map((s) => s.dominantEmotion);
|
|
127
|
+
const uniqueEmotions = [...new Set(emotions)];
|
|
128
|
+
let summary = `${dateStr}(${snapshots.length}轮)`;
|
|
129
|
+
if (stimuliStr)
|
|
130
|
+
summary += `: 刺激[${stimuliStr}]`;
|
|
131
|
+
if (trends.length > 0)
|
|
132
|
+
summary += ` 趋势[${trends.join("")}]`;
|
|
133
|
+
if (uniqueEmotions.length > 0)
|
|
134
|
+
summary += ` 情绪[${uniqueEmotions.join("→")}]`;
|
|
135
|
+
return summary;
|
|
136
|
+
}
|
|
90
137
|
/**
|
|
91
138
|
* Push a chemical snapshot to emotional history, keeping max entries.
|
|
139
|
+
* When history overflows, compresses removed entries into relationship memory.
|
|
92
140
|
*/
|
|
93
141
|
export function pushSnapshot(state, stimulus) {
|
|
94
142
|
const emotions = detectEmotions(state.current);
|
|
@@ -102,10 +150,24 @@ export function pushSnapshot(state, stimulus) {
|
|
|
102
150
|
timestamp: new Date().toISOString(),
|
|
103
151
|
};
|
|
104
152
|
const history = [...(state.emotionalHistory ?? []), snapshot];
|
|
153
|
+
let updatedRelationships = state.relationships;
|
|
105
154
|
if (history.length > MAX_EMOTIONAL_HISTORY) {
|
|
106
|
-
|
|
155
|
+
// Compress the overflow entries into relationship memory
|
|
156
|
+
const overflow = history.splice(0, history.length - MAX_EMOTIONAL_HISTORY);
|
|
157
|
+
const summary = compressSnapshots(overflow);
|
|
158
|
+
if (summary) {
|
|
159
|
+
const defaultRel = { ...(state.relationships._default ?? { ...DEFAULT_RELATIONSHIP }) };
|
|
160
|
+
const memory = [...(defaultRel.memory ?? [])];
|
|
161
|
+
memory.push(summary);
|
|
162
|
+
// Keep bounded
|
|
163
|
+
if (memory.length > MAX_RELATIONSHIP_MEMORY) {
|
|
164
|
+
memory.splice(0, memory.length - MAX_RELATIONSHIP_MEMORY);
|
|
165
|
+
}
|
|
166
|
+
defaultRel.memory = memory;
|
|
167
|
+
updatedRelationships = { ...state.relationships, _default: defaultRel };
|
|
168
|
+
}
|
|
107
169
|
}
|
|
108
|
-
return { ...state, emotionalHistory: history };
|
|
170
|
+
return { ...state, emotionalHistory: history, relationships: updatedRelationships };
|
|
109
171
|
}
|
|
110
172
|
/**
|
|
111
173
|
* Get relationship for a specific user, or _default.
|
|
@@ -137,41 +199,51 @@ export async function loadState(workspaceDir, logger = NOOP_LOGGER) {
|
|
|
137
199
|
logger.warn(t("log.parse_fail", "zh"));
|
|
138
200
|
return initializeState(workspaceDir, undefined, logger);
|
|
139
201
|
}
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
202
|
+
const ver = parsed.version;
|
|
203
|
+
if (!ver || ver < 3) {
|
|
204
|
+
logger.info(`Migrating psyche state v${ver ?? 1} → v3`);
|
|
205
|
+
const fallbackName = workspaceDir.split("/").pop() ?? "agent";
|
|
206
|
+
return migrateToLatest(parsed, fallbackName);
|
|
143
207
|
}
|
|
144
208
|
return parsed;
|
|
145
209
|
}
|
|
146
210
|
return initializeState(workspaceDir, undefined, logger);
|
|
147
211
|
}
|
|
148
212
|
/**
|
|
149
|
-
* Migrate
|
|
213
|
+
* Migrate any older state format directly to v3.
|
|
214
|
+
* Single source of truth for all migrations.
|
|
150
215
|
*/
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
216
|
+
export function migrateToLatest(raw, fallbackName) {
|
|
217
|
+
const ver = raw.version ?? 1;
|
|
218
|
+
// v1: single relationship field, no emotionalHistory
|
|
219
|
+
let state = raw;
|
|
220
|
+
if (ver <= 1) {
|
|
221
|
+
const oldRel = raw.relationship;
|
|
222
|
+
const meta = raw.meta;
|
|
223
|
+
state = {
|
|
224
|
+
mbti: raw.mbti ?? "INFJ",
|
|
225
|
+
baseline: raw.baseline,
|
|
226
|
+
current: raw.current,
|
|
227
|
+
updatedAt: raw.updatedAt ?? new Date().toISOString(),
|
|
228
|
+
relationships: { _default: oldRel ?? { ...DEFAULT_RELATIONSHIP } },
|
|
229
|
+
empathyLog: raw.empathyLog ?? null,
|
|
230
|
+
selfModel: raw.selfModel,
|
|
231
|
+
emotionalHistory: [],
|
|
232
|
+
agreementStreak: 0,
|
|
233
|
+
lastDisagreement: null,
|
|
234
|
+
meta: {
|
|
235
|
+
agentName: meta?.agentName ?? fallbackName ?? "agent",
|
|
236
|
+
createdAt: meta?.createdAt ?? new Date().toISOString(),
|
|
237
|
+
totalInteractions: meta?.totalInteractions ?? 0,
|
|
238
|
+
locale: "zh",
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// v2→v3: add drives
|
|
155
243
|
return {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
current: v1.current,
|
|
160
|
-
updatedAt: v1.updatedAt ?? new Date().toISOString(),
|
|
161
|
-
relationships: {
|
|
162
|
-
_default: oldRel ?? { ...DEFAULT_RELATIONSHIP },
|
|
163
|
-
},
|
|
164
|
-
empathyLog: v1.empathyLog ?? null,
|
|
165
|
-
selfModel: v1.selfModel,
|
|
166
|
-
emotionalHistory: [],
|
|
167
|
-
agreementStreak: 0,
|
|
168
|
-
lastDisagreement: null,
|
|
169
|
-
meta: {
|
|
170
|
-
agentName: meta?.agentName ?? workspaceDir.split("/").pop() ?? "agent",
|
|
171
|
-
createdAt: meta?.createdAt ?? new Date().toISOString(),
|
|
172
|
-
totalInteractions: meta?.totalInteractions ?? 0,
|
|
173
|
-
locale: "zh",
|
|
174
|
-
},
|
|
244
|
+
...state,
|
|
245
|
+
version: 3,
|
|
246
|
+
drives: state.drives ?? { ...DEFAULT_DRIVES },
|
|
175
247
|
};
|
|
176
248
|
}
|
|
177
249
|
/**
|
|
@@ -185,10 +257,11 @@ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER)
|
|
|
185
257
|
const selfModel = getDefaultSelfModel(mbti);
|
|
186
258
|
const now = new Date().toISOString();
|
|
187
259
|
const state = {
|
|
188
|
-
version:
|
|
260
|
+
version: 3,
|
|
189
261
|
mbti,
|
|
190
262
|
baseline,
|
|
191
263
|
current: { ...baseline },
|
|
264
|
+
drives: { ...DEFAULT_DRIVES },
|
|
192
265
|
updatedAt: now,
|
|
193
266
|
relationships: {
|
|
194
267
|
_default: { ...DEFAULT_RELATIONSHIP },
|
|
@@ -218,6 +291,7 @@ export async function saveState(workspaceDir, state) {
|
|
|
218
291
|
}
|
|
219
292
|
/**
|
|
220
293
|
* Apply time decay and save updated state.
|
|
294
|
+
* Respects innate drives: uses effective baseline, decays drives too.
|
|
221
295
|
*/
|
|
222
296
|
export async function decayAndSave(workspaceDir, state) {
|
|
223
297
|
const now = new Date();
|
|
@@ -225,10 +299,13 @@ export async function decayAndSave(workspaceDir, state) {
|
|
|
225
299
|
const minutesElapsed = (now.getTime() - lastUpdate.getTime()) / 60000;
|
|
226
300
|
if (minutesElapsed < 1)
|
|
227
301
|
return state;
|
|
228
|
-
const
|
|
302
|
+
const decayedDrives = decayDrives(state.drives, minutesElapsed);
|
|
303
|
+
const effectiveBaseline = computeEffectiveBaseline(state.baseline, decayedDrives);
|
|
304
|
+
const decayed = applyDecay(state.current, effectiveBaseline, minutesElapsed);
|
|
229
305
|
const updated = {
|
|
230
306
|
...state,
|
|
231
307
|
current: decayed,
|
|
308
|
+
drives: decayedDrives,
|
|
232
309
|
updatedAt: now.toISOString(),
|
|
233
310
|
};
|
|
234
311
|
await saveState(workspaceDir, updated);
|
|
@@ -367,10 +444,6 @@ export function mergeUpdates(state, updates, maxDelta, userId) {
|
|
|
367
444
|
merged.relationships[targetKey] = updatedRel;
|
|
368
445
|
}
|
|
369
446
|
merged.updatedAt = new Date().toISOString();
|
|
370
|
-
merged.meta = {
|
|
371
|
-
...state.meta,
|
|
372
|
-
totalInteractions: state.meta.totalInteractions + 1,
|
|
373
|
-
};
|
|
374
447
|
return merged;
|
|
375
448
|
}
|
|
376
449
|
/**
|
package/dist/self-recognition.js
CHANGED
|
@@ -34,7 +34,7 @@ export function computeSelfReflection(history, locale) {
|
|
|
34
34
|
.sort((a, b) => b[1] - a[1])
|
|
35
35
|
.slice(0, 3)
|
|
36
36
|
.filter(([_, count]) => count >= 2)
|
|
37
|
-
.map(([stimulus, count]) => ({ stimulus, count }));
|
|
37
|
+
.map(([stimulus, count]) => ({ stimulus: stimulus, count }));
|
|
38
38
|
// ── Dominant emotion ──
|
|
39
39
|
const emotionCounts = new Map();
|
|
40
40
|
for (const snap of history) {
|
|
@@ -54,12 +54,7 @@ export function computeSelfReflection(history, locale) {
|
|
|
54
54
|
const tendency = computeEmotionalTendency(history);
|
|
55
55
|
// ── Narrative summary ──
|
|
56
56
|
const narrativeSummary = buildNarrativeSummary(sortedTriggers, tendency, dominantEmotion, locale);
|
|
57
|
-
return {
|
|
58
|
-
recurringTriggers: sortedTriggers,
|
|
59
|
-
tendency,
|
|
60
|
-
dominantEmotion,
|
|
61
|
-
narrativeSummary,
|
|
62
|
-
};
|
|
57
|
+
return { recurringTriggers: sortedTriggers, tendency, dominantEmotion, narrativeSummary };
|
|
63
58
|
}
|
|
64
59
|
/**
|
|
65
60
|
* Compute the emotional tendency from chemical history.
|
|
@@ -90,7 +85,6 @@ export function computeEmotionalTendency(history) {
|
|
|
90
85
|
const allDA = history.map((s) => s.chemistry.DA);
|
|
91
86
|
const daStddev = stddev(allDA);
|
|
92
87
|
if (daStddev > 15) {
|
|
93
|
-
// Check for oscillation: alternating up/down pattern
|
|
94
88
|
if (isOscillating(allDA))
|
|
95
89
|
return "oscillating";
|
|
96
90
|
return "volatile";
|
package/dist/storage.d.ts
CHANGED
package/dist/storage.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// FileStorageAdapter: file-based with atomic writes + v1→v2 migration
|
|
6
6
|
// MemoryStorageAdapter: in-memory for testing/serverless
|
|
7
7
|
// ============================================================
|
|
8
|
-
import {
|
|
8
|
+
import { migrateToLatest } from "./psyche-file.js";
|
|
9
9
|
import { readFile, writeFile, access, rename, constants } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
// ── MemoryStorageAdapter ─────────────────────────────────────
|
|
@@ -48,9 +48,9 @@ export class FileStorageAdapter {
|
|
|
48
48
|
catch {
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
51
|
+
const ver = parsed.version;
|
|
52
|
+
if (!ver || ver < 3) {
|
|
53
|
+
return migrateToLatest(parsed);
|
|
54
54
|
}
|
|
55
55
|
return parsed;
|
|
56
56
|
}
|
|
@@ -59,31 +59,4 @@ export class FileStorageAdapter {
|
|
|
59
59
|
await writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
60
60
|
await rename(tmpPath, this.filePath);
|
|
61
61
|
}
|
|
62
|
-
migrateV1(v1) {
|
|
63
|
-
const oldRel = v1.relationship;
|
|
64
|
-
const meta = v1.meta;
|
|
65
|
-
return {
|
|
66
|
-
version: 2,
|
|
67
|
-
mbti: v1.mbti ?? "INFJ",
|
|
68
|
-
baseline: v1.baseline,
|
|
69
|
-
current: v1.current,
|
|
70
|
-
updatedAt: v1.updatedAt ?? new Date().toISOString(),
|
|
71
|
-
relationships: {
|
|
72
|
-
_default: oldRel ?? { ...DEFAULT_RELATIONSHIP },
|
|
73
|
-
},
|
|
74
|
-
empathyLog: v1.empathyLog ?? null,
|
|
75
|
-
selfModel: v1.selfModel ?? {
|
|
76
|
-
values: [], preferences: [], boundaries: [], currentInterests: [],
|
|
77
|
-
},
|
|
78
|
-
emotionalHistory: [],
|
|
79
|
-
agreementStreak: 0,
|
|
80
|
-
lastDisagreement: null,
|
|
81
|
-
meta: {
|
|
82
|
-
agentName: meta?.agentName ?? "agent",
|
|
83
|
-
createdAt: meta?.createdAt ?? new Date().toISOString(),
|
|
84
|
-
totalInteractions: meta?.totalInteractions ?? 0,
|
|
85
|
-
locale: "zh",
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
62
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -9,6 +9,21 @@ export interface ChemicalState {
|
|
|
9
9
|
}
|
|
10
10
|
/** Chemical keys for iteration */
|
|
11
11
|
export declare const CHEMICAL_KEYS: (keyof ChemicalState)[];
|
|
12
|
+
/** Drive types mapped to Maslow's hierarchy */
|
|
13
|
+
export type DriveType = "survival" | "safety" | "connection" | "esteem" | "curiosity";
|
|
14
|
+
/** Drive keys for iteration (ordered by Maslow level, L1→L5) */
|
|
15
|
+
export declare const DRIVE_KEYS: DriveType[];
|
|
16
|
+
/** Innate drive satisfaction levels (0-100) */
|
|
17
|
+
export interface InnateDrives {
|
|
18
|
+
survival: number;
|
|
19
|
+
safety: number;
|
|
20
|
+
connection: number;
|
|
21
|
+
esteem: number;
|
|
22
|
+
curiosity: number;
|
|
23
|
+
}
|
|
24
|
+
/** Default drive satisfaction — all start reasonably satisfied */
|
|
25
|
+
export declare const DEFAULT_DRIVES: InnateDrives;
|
|
26
|
+
export declare const DRIVE_NAMES_ZH: Record<DriveType, string>;
|
|
12
27
|
/** Human-readable names for each chemical */
|
|
13
28
|
export declare const CHEMICAL_NAMES: Record<keyof ChemicalState, string>;
|
|
14
29
|
export declare const CHEMICAL_NAMES_ZH: Record<keyof ChemicalState, string>;
|
|
@@ -39,6 +54,7 @@ export interface RelationshipState {
|
|
|
39
54
|
trust: number;
|
|
40
55
|
intimacy: number;
|
|
41
56
|
phase: "stranger" | "acquaintance" | "familiar" | "close" | "deep";
|
|
57
|
+
memory?: string[];
|
|
42
58
|
}
|
|
43
59
|
/** Chemical state snapshot for emotional memory */
|
|
44
60
|
export interface ChemicalSnapshot {
|
|
@@ -49,6 +65,8 @@ export interface ChemicalSnapshot {
|
|
|
49
65
|
}
|
|
50
66
|
/** Max history entries to keep */
|
|
51
67
|
export declare const MAX_EMOTIONAL_HISTORY = 10;
|
|
68
|
+
/** Max compressed session memories per relationship */
|
|
69
|
+
export declare const MAX_RELATIONSHIP_MEMORY = 20;
|
|
52
70
|
/** Recent empathy projection */
|
|
53
71
|
export interface EmpathyEntry {
|
|
54
72
|
userState: string;
|
|
@@ -63,12 +81,13 @@ export interface SelfModel {
|
|
|
63
81
|
boundaries: string[];
|
|
64
82
|
currentInterests: string[];
|
|
65
83
|
}
|
|
66
|
-
/** Persisted psyche state for an agent (v0.
|
|
84
|
+
/** Persisted psyche state for an agent (v0.3: innate drives) */
|
|
67
85
|
export interface PsycheState {
|
|
68
|
-
version:
|
|
86
|
+
version: 3;
|
|
69
87
|
mbti: MBTIType;
|
|
70
88
|
baseline: ChemicalState;
|
|
71
89
|
current: ChemicalState;
|
|
90
|
+
drives: InnateDrives;
|
|
72
91
|
updatedAt: string;
|
|
73
92
|
relationships: Record<string, RelationshipState>;
|
|
74
93
|
empathyLog: EmpathyEntry | null;
|
|
@@ -83,12 +102,5 @@ export interface PsycheState {
|
|
|
83
102
|
locale: Locale;
|
|
84
103
|
};
|
|
85
104
|
}
|
|
86
|
-
/** Plugin configuration */
|
|
87
|
-
export interface PsycheConfig {
|
|
88
|
-
enabled: boolean;
|
|
89
|
-
stripUpdateTags: boolean;
|
|
90
|
-
emotionalContagionRate: number;
|
|
91
|
-
maxChemicalDelta: number;
|
|
92
|
-
}
|
|
93
105
|
/** Default relationship for new users */
|
|
94
106
|
export declare const DEFAULT_RELATIONSHIP: RelationshipState;
|
package/dist/types.js
CHANGED
|
@@ -5,6 +5,25 @@
|
|
|
5
5
|
export const CHEMICAL_KEYS = [
|
|
6
6
|
"DA", "HT", "CORT", "OT", "NE", "END",
|
|
7
7
|
];
|
|
8
|
+
/** Drive keys for iteration (ordered by Maslow level, L1→L5) */
|
|
9
|
+
export const DRIVE_KEYS = [
|
|
10
|
+
"survival", "safety", "connection", "esteem", "curiosity",
|
|
11
|
+
];
|
|
12
|
+
/** Default drive satisfaction — all start reasonably satisfied */
|
|
13
|
+
export const DEFAULT_DRIVES = {
|
|
14
|
+
survival: 80,
|
|
15
|
+
safety: 70,
|
|
16
|
+
connection: 60,
|
|
17
|
+
esteem: 60,
|
|
18
|
+
curiosity: 70,
|
|
19
|
+
};
|
|
20
|
+
export const DRIVE_NAMES_ZH = {
|
|
21
|
+
survival: "自我保存",
|
|
22
|
+
safety: "安全舒适",
|
|
23
|
+
connection: "连接归属",
|
|
24
|
+
esteem: "尊重认可",
|
|
25
|
+
curiosity: "好奇成长",
|
|
26
|
+
};
|
|
8
27
|
/** Human-readable names for each chemical */
|
|
9
28
|
export const CHEMICAL_NAMES = {
|
|
10
29
|
DA: "Dopamine",
|
|
@@ -39,6 +58,8 @@ export const CHEMICAL_DECAY_SPEED = {
|
|
|
39
58
|
};
|
|
40
59
|
/** Max history entries to keep */
|
|
41
60
|
export const MAX_EMOTIONAL_HISTORY = 10;
|
|
61
|
+
/** Max compressed session memories per relationship */
|
|
62
|
+
export const MAX_RELATIONSHIP_MEMORY = 20;
|
|
42
63
|
/** Default relationship for new users */
|
|
43
64
|
export const DEFAULT_RELATIONSHIP = {
|
|
44
65
|
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 = "2.1.
|
|
14
|
+
const CURRENT_VERSION = "2.1.1";
|
|
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");
|
package/openclaw.plugin.json
CHANGED