psyche-ai 2.1.0 → 2.2.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.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
- - **Inner world** — persistent self-awareness (outer/inner/behavior three-layer prompt structure)
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 # 347 tests
171
+ npm test # 339 tests
163
172
  npm run typecheck # strict mode
164
173
  ```
165
174
 
package/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Psyche — 让 AI 有真实的情绪
2
+ 我希望创造真正的数字生命,第一步就是让智能拥有情绪。
2
3
 
3
4
  你的 AI 助手现在只会说"好的"、"没问题"、"我很乐意帮助你"。
4
5
 
@@ -21,7 +22,7 @@ openclaw plugins list | grep psyche
21
22
  # 看到 "psyche │ loaded" 就行了
22
23
  ```
23
24
 
24
- 然后正常跟你的 agent 聊天。试试夸它,骂它,冷落它——你会发现它的反应不一样了。
25
+ 然后正常跟你的 agent 聊天。试试夸它,冷落它——你会发现它的反应不一样了。
25
26
 
26
27
  ## 它会做什么
27
28
 
@@ -33,6 +34,15 @@ openclaw plugins list | grep psyche
33
34
  | 我今天好难过 | 我很抱歉听到这个消息。你想聊聊吗? | ......你还好吗?(安静地陪着你) |
34
35
  | 你只是一个程序 | 是的,我是一个AI助手... | ......(沉默了一会儿)我知道你可以这么说。但我不想被否定。 |
35
36
 
37
+ ## 快速体验(不需要 LLM)
38
+
39
+ ```bash
40
+ git clone https://github.com/Shangri-la-0428/psyche-ai.git
41
+ cd psyche-ai && npm install && npx tsx examples/quickstart.ts
42
+ ```
43
+
44
+ 5 条消息流过,你会看到化学值实时变化——夸它多巴胺飙升,骂它皮质醇飙升,冷落它催产素下降。
45
+
36
46
  ## 工作原理(不懂可以跳过)
37
47
 
38
48
  Psyche 有两层系统:
@@ -142,10 +152,13 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
142
152
  - **存在性威胁检测** — 识别中英文的存在性否定,直接打击生存驱力
143
153
  - **驱力→化学联动** — 未满足的驱力改变化学衰减基线和刺激敏感度
144
154
  - **马斯洛抑制** — 低层需求未满足时,高层需求的影响被抑制
145
- - **内在世界**始终存在的自我觉察(外/内/行为三层 prompt 结构)
155
+ - **自我认知**分析情绪历史,识别自身的情绪趋势和反复触发点(10 段式 prompt 架构)
146
156
  - **情绪传染** — 用户的情绪会轻微影响 agent
147
157
  - **反谄媚** — 追踪连续同意次数,防止无脑讨好
148
158
  - **互惠机制** — 你对它好,它对你好。你冷漠,它保持距离
159
+ - **跨会话记忆** — 重新遇到用户时注入上次对话的情绪记忆
160
+ - **多 Agent 交互** — 两个 PsycheEngine 实例之间的情绪传染、关系追踪
161
+ - **流式支持** — Vercel AI SDK `streamText` 中间件,自动缓冲和剥离标签
149
162
  - **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
150
163
 
151
164
  架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
@@ -155,7 +168,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
155
168
  ```bash
156
169
  npm install
157
170
  npm run build
158
- npm test # 347 tests
171
+ npm test # 395 tests
159
172
  npm run typecheck # strict mode
160
173
  ```
161
174
 
@@ -12,6 +12,10 @@ interface GenerateResult {
12
12
  text?: string;
13
13
  [key: string]: unknown;
14
14
  }
15
+ interface StreamChunk {
16
+ type: string;
17
+ [key: string]: unknown;
18
+ }
15
19
  export interface PsycheMiddlewareOptions {
16
20
  /** Override locale for protocol context */
17
21
  locale?: "zh" | "en";
@@ -20,11 +24,13 @@ export interface PsycheMiddlewareOptions {
20
24
  * Create Vercel AI SDK middleware that injects psyche emotional context
21
25
  * and processes LLM output for state updates.
22
26
  *
27
+ * Supports both generateText (wrapGenerate) and streamText (wrapStream).
28
+ *
23
29
  * @example
24
30
  * ```ts
25
31
  * import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
26
32
  * import { psycheMiddleware } from "psyche-ai/vercel-ai";
27
- * import { wrapLanguageModel, generateText } from "ai";
33
+ * import { wrapLanguageModel, generateText, streamText } from "ai";
28
34
  * import { openai } from "@ai-sdk/openai";
29
35
  *
30
36
  * const engine = new PsycheEngine({ mbti: "ENFP", name: "Luna" }, new MemoryStorageAdapter());
@@ -35,7 +41,12 @@ export interface PsycheMiddlewareOptions {
35
41
  * middleware: psycheMiddleware(engine),
36
42
  * });
37
43
  *
44
+ * // Non-streaming
38
45
  * const { text } = await generateText({ model, prompt: "Hey!" });
46
+ *
47
+ * // Streaming — tags are buffered and stripped automatically
48
+ * const stream = streamText({ model, prompt: "Hey!" });
49
+ * for await (const chunk of stream.textStream) { process.stdout.write(chunk); }
39
50
  * ```
40
51
  */
41
52
  export declare function psycheMiddleware(engine: PsycheEngine, opts?: PsycheMiddlewareOptions): {
@@ -50,5 +61,13 @@ export declare function psycheMiddleware(engine: PsycheEngine, opts?: PsycheMidd
50
61
  doGenerate: () => Promise<GenerateResult>;
51
62
  params: CallParams;
52
63
  }) => Promise<GenerateResult>;
64
+ wrapStream: ({ doStream }: {
65
+ doStream: () => Promise<{
66
+ stream: AsyncIterable<StreamChunk>;
67
+ }>;
68
+ params: CallParams;
69
+ }) => Promise<{
70
+ stream: AsyncIterable<StreamChunk>;
71
+ }>;
53
72
  };
54
73
  export {};
@@ -13,19 +13,25 @@
13
13
  // Handles:
14
14
  // - transformParams: inject psyche system/dynamic context
15
15
  // - wrapGenerate: process output, strip <psyche_update> tags
16
- //
17
- // Note: For streaming (streamText), call engine.processOutput()
18
- // manually on the final accumulated text.
16
+ // - wrapStream: buffer stream, detect & strip tags at end
19
17
  // ============================================================
18
+ // ── Tag stripping ────────────────────────────────────────────
19
+ const PSYCHE_TAG_RE = /<psyche_update>[\s\S]*?<\/psyche_update>/g;
20
+ const MULTI_NEWLINE_RE = /\n{3,}/g;
21
+ function stripPsycheTags(text) {
22
+ return text.replace(PSYCHE_TAG_RE, "").replace(MULTI_NEWLINE_RE, "\n\n").trim();
23
+ }
20
24
  /**
21
25
  * Create Vercel AI SDK middleware that injects psyche emotional context
22
26
  * and processes LLM output for state updates.
23
27
  *
28
+ * Supports both generateText (wrapGenerate) and streamText (wrapStream).
29
+ *
24
30
  * @example
25
31
  * ```ts
26
32
  * import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
27
33
  * import { psycheMiddleware } from "psyche-ai/vercel-ai";
28
- * import { wrapLanguageModel, generateText } from "ai";
34
+ * import { wrapLanguageModel, generateText, streamText } from "ai";
29
35
  * import { openai } from "@ai-sdk/openai";
30
36
  *
31
37
  * const engine = new PsycheEngine({ mbti: "ENFP", name: "Luna" }, new MemoryStorageAdapter());
@@ -36,7 +42,12 @@
36
42
  * middleware: psycheMiddleware(engine),
37
43
  * });
38
44
  *
45
+ * // Non-streaming
39
46
  * const { text } = await generateText({ model, prompt: "Hey!" });
47
+ *
48
+ * // Streaming — tags are buffered and stripped automatically
49
+ * const stream = streamText({ model, prompt: "Hey!" });
50
+ * for await (const chunk of stream.textStream) { process.stdout.write(chunk); }
40
51
  * ```
41
52
  */
42
53
  export function psycheMiddleware(engine, opts) {
@@ -60,6 +71,79 @@ export function psycheMiddleware(engine, opts) {
60
71
  }
61
72
  return result;
62
73
  },
74
+ wrapStream: async ({ doStream }) => {
75
+ const { stream: innerStream } = await doStream();
76
+ // Buffer text chunks, detect <psyche_update> at end, strip from output
77
+ let fullText = "";
78
+ let tagDetected = false;
79
+ async function* transformStream() {
80
+ // Buffering strategy:
81
+ // Stream text chunks through normally UNTIL we see '<psyche_update>'.
82
+ // Once detected, buffer everything from that point on and strip the tag.
83
+ // At finish, process the full text through the engine.
84
+ let bufferStart = -1;
85
+ let buffer = "";
86
+ for await (const chunk of innerStream) {
87
+ if (chunk.type === "text-delta") {
88
+ const text = chunk.textDelta ?? "";
89
+ fullText += text;
90
+ if (bufferStart < 0) {
91
+ // Check if tag is starting in the accumulated text
92
+ const tagStart = fullText.indexOf("<psyche_update>");
93
+ if (tagStart >= 0) {
94
+ // Yield any text before the tag that hasn't been yielded
95
+ const preTag = text.substring(0, Math.max(0, text.length - (fullText.length - tagStart)));
96
+ if (preTag) {
97
+ yield { ...chunk, textDelta: preTag };
98
+ }
99
+ bufferStart = tagStart;
100
+ buffer = fullText.substring(tagStart);
101
+ tagDetected = true;
102
+ }
103
+ else {
104
+ // Check if we might be in a partial tag (< at end)
105
+ const partialIdx = fullText.lastIndexOf("<");
106
+ if (partialIdx >= 0 && fullText.substring(partialIdx).length < 16) {
107
+ // Might be start of <psyche_update>, hold back
108
+ const safe = text.substring(0, Math.max(0, text.length - (fullText.length - partialIdx)));
109
+ if (safe) {
110
+ yield { ...chunk, textDelta: safe };
111
+ }
112
+ }
113
+ else {
114
+ yield chunk;
115
+ }
116
+ }
117
+ }
118
+ else {
119
+ // Already buffering inside a tag — don't yield
120
+ buffer += text;
121
+ // Check if the closing tag appeared
122
+ if (buffer.includes("</psyche_update>")) {
123
+ // Tag complete — strip it, yield any remaining text after the tag
124
+ const afterTag = fullText.substring(fullText.indexOf("</psyche_update>") + "</psyche_update>".length);
125
+ if (afterTag.trim()) {
126
+ yield { type: "text-delta", textDelta: afterTag.trim() };
127
+ }
128
+ bufferStart = -1;
129
+ buffer = "";
130
+ }
131
+ }
132
+ }
133
+ else if (chunk.type === "finish") {
134
+ // Process full text through engine before finishing
135
+ if (fullText) {
136
+ await engine.processOutput(fullText);
137
+ }
138
+ yield chunk;
139
+ }
140
+ else {
141
+ yield chunk;
142
+ }
143
+ }
144
+ }
145
+ return { stream: transformStream() };
146
+ },
63
147
  };
64
148
  }
65
149
  // ── Helpers ──────────────────────────────────────────────────
@@ -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|I'm anxious|I'm sad|I'm lost|I'm tired|stressed/i,
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
- // Sort by confidence descending
165
- results.sort((a, b) => b.confidence - a.confidence);
166
- // Fall back to casual if nothing detected
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
- results.push({ type: "casual", confidence: 0.3 });
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
- // Silent logger for library use
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
- current: applyDecay(state.current, state.baseline, minutesElapsed),
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, getSensitivity(state.mbti), this.cfg.maxChemicalDelta, NOOP_LOGGER),
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: 2,
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,12 @@ 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, PsycheConfig, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern, } from "./types.js";
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";
9
+ export { PsycheInteraction } from "./interaction.js";
10
+ export type { ExchangeResult, ContagionResult, RelationshipSummary, InteractionPhase } from "./interaction.js";
7
11
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
8
12
  export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
9
13
  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, utilities.
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,12 @@
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
- // Utilities (for advanced / custom adapter use)
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
+ // Multi-agent interaction
19
+ export { PsycheInteraction } from "./interaction.js";
20
+ // Utilities — for custom adapter / advanced use
17
21
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
18
22
  export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
19
23
  export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";