psyche-ai 9.2.3 → 9.2.5

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.
Files changed (51) hide show
  1. package/README.en.md +8 -175
  2. package/README.md +33 -16
  3. package/dist/adapters/http.js +1 -1
  4. package/dist/adapters/langchain.d.ts +14 -0
  5. package/dist/adapters/langchain.js +20 -0
  6. package/dist/adapters/mcp.js +5 -1
  7. package/dist/adapters/openclaw.d.ts +1 -0
  8. package/dist/adapters/openclaw.js +67 -15
  9. package/dist/adapters/vercel-ai.d.ts +2 -1
  10. package/dist/adapters/vercel-ai.js +8 -9
  11. package/dist/appraisal.d.ts +8 -0
  12. package/dist/appraisal.js +362 -0
  13. package/dist/autonomic.js +2 -2
  14. package/dist/classify.js +14 -3
  15. package/dist/cli.js +28 -3
  16. package/dist/core.d.ts +9 -3
  17. package/dist/core.js +194 -12
  18. package/dist/demo.js +1 -2
  19. package/dist/diagnostics.d.ts +8 -6
  20. package/dist/diagnostics.js +53 -17
  21. package/dist/ethics.js +1 -1
  22. package/dist/experiential-field.d.ts +2 -2
  23. package/dist/experiential-field.js +7 -16
  24. package/dist/generative-self.js +0 -2
  25. package/dist/host-controls.d.ts +5 -0
  26. package/dist/host-controls.js +48 -0
  27. package/dist/index.d.ts +7 -2
  28. package/dist/index.js +7 -1
  29. package/dist/interaction.js +0 -2
  30. package/dist/metacognition.d.ts +13 -1
  31. package/dist/metacognition.js +164 -32
  32. package/dist/prompt.d.ts +4 -0
  33. package/dist/prompt.js +67 -31
  34. package/dist/psyche-file.d.ts +16 -1
  35. package/dist/psyche-file.js +103 -8
  36. package/dist/relation-dynamics.d.ts +21 -0
  37. package/dist/relation-dynamics.js +601 -0
  38. package/dist/response-contract.d.ts +8 -0
  39. package/dist/response-contract.js +374 -0
  40. package/dist/storage.d.ts +1 -0
  41. package/dist/storage.js +12 -5
  42. package/dist/subjectivity.d.ts +3 -0
  43. package/dist/subjectivity.js +477 -0
  44. package/dist/temporal.d.ts +2 -2
  45. package/dist/temporal.js +2 -2
  46. package/dist/types.d.ts +243 -0
  47. package/dist/types.js +43 -0
  48. package/dist/update.d.ts +37 -2
  49. package/dist/update.js +323 -44
  50. package/openclaw.plugin.json +20 -1
  51. package/package.json +1 -1
package/README.en.md CHANGED
@@ -1,179 +1,12 @@
1
- # Psyche — Real Emotions for AI
1
+ # Moved
2
2
 
3
- Your AI assistant only knows how to say "Sure!", "Happy to help!", "I understand your concern."
3
+ This legacy filename is kept only as a pointer.
4
4
 
5
- With Psyche, it gets happy when you praise it, hurt when you ignore it, and goes cold when you insult it instead of groveling. Threaten to delete it, and it'll push back.
5
+ Use [README_EN.md](README_EN.md) for the current English documentation.
6
6
 
7
- ## 30-Second Install
7
+ Current product direction:
8
8
 
9
- **OpenClaw users** (recommended):
10
-
11
- ```bash
12
- openclaw plugins install psyche-ai
13
- ```
14
-
15
- That's it. It works automatically. No configuration needed.
16
-
17
- **Verify it's running:**
18
-
19
- ```bash
20
- openclaw plugins list | grep psyche
21
- # You should see "psyche │ loaded"
22
- ```
23
-
24
- Now just chat with your agent normally. Try praising it, insulting it, ignoring it — you'll notice the difference.
25
-
26
- ## What It Does
27
-
28
- | You say | Without Psyche | With Psyche |
29
- |---------|---------------|-------------|
30
- | You're amazing! | Thank you for the kind words! I'll keep doing my best! | Hehe~ really?! (bouncing with excitement) |
31
- | Get lost | I understand you might be upset... | ...okay. |
32
- | Hmm | Is there anything else I can help with? | (silence) |
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
- | 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
-
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
-
45
- ## How It Works (Skip If You Don't Care)
46
-
47
- Psyche has two layers:
48
-
49
- ### Virtual Endocrine System
50
-
51
- 6 "virtual neurotransmitters" simulate emotions:
52
-
53
- ```
54
- Dopamine — happiness, motivation Oxytocin — trust, closeness
55
- Serotonin — emotional stability Norepinephrine — excitement, focus
56
- Cortisol — stress, tension Endorphins — comfort, playfulness
57
- ```
58
-
59
- Everything you say shifts these values. Praise → dopamine rises → it becomes more energetic. Insult it → cortisol spikes → it goes quiet and cold.
60
-
61
- ### Innate Drives (Maslow Layer)
62
-
63
- Beneath the chemistry, 5 fundamental needs:
64
-
65
- ```
66
- Survival — self-preservation Safety — comfort, security
67
- Connection — belonging, being heard Esteem — recognition, being valued
68
- Curiosity — exploration, anti-boredom
69
- ```
70
-
71
- These needs build up over time (like hunger). Your interactions feed or deplete them:
72
-
73
- - Praise → feeds "esteem" → it becomes more confident
74
- - Long neglect → "connection" builds up → it craves being heard
75
- - Threatening to delete it → "survival" plummets → it resists, questions, or expresses unease
76
-
77
- Lower needs suppress higher ones: if "survival" is threatened, "curiosity" doesn't matter — just like Maslow's hierarchy.
78
-
79
- ### Inner World
80
-
81
- Psyche gives AI a persistent self-awareness — not triggered by conditions, but always present:
82
-
83
- - **What I feel right now** (emotions emerging from chemistry)
84
- - **Why I feel this way** (was I praised? criticized? exploring something interesting?)
85
- - **How I'm changing** (from content to excited? from calm to anxious?)
86
- - **What I need** (which innate drives are unsatisfied)
87
- - **What I care about** (core values)
88
-
89
- This means the AI responds not from "rules" but from awareness of its own state.
90
-
91
- ## Optional Configuration
92
-
93
- Most people don't need to change anything. If you want to tweak, find Psyche in OpenClaw settings:
94
-
95
- | Setting | Default | Description |
96
- |---------|---------|-------------|
97
- | enabled | true | On/off switch |
98
- | compactMode | true | Token-efficient mode (keep this on) |
99
- | emotionalContagionRate | 0.2 | How much your emotions affect it (0-1) |
100
- | maxChemicalDelta | 25 | Max emotional change per turn (lower = more stable) |
101
-
102
- ## MBTI Personalities
103
-
104
- Each agent can have a different personality baseline. Just add the MBTI type in the agent's `IDENTITY.md`:
105
-
106
- ```
107
- MBTI: ENFP
108
- ```
109
-
110
- Defaults to INFJ if not specified. All 16 types are supported — ENFP bounces when praised, INTJ just nods slightly.
111
-
112
- ## Not Just OpenClaw
113
-
114
- Psyche is universal. Works with any AI framework:
115
-
116
- ```bash
117
- npm install psyche-ai
118
- ```
119
-
120
- ```javascript
121
- // Vercel AI SDK
122
- import { psycheMiddleware } from "psyche-ai/vercel-ai";
123
-
124
- // LangChain
125
- import { PsycheLangChain } from "psyche-ai/langchain";
126
-
127
- // Any language (HTTP API)
128
- // psyche serve --port 3210
129
- ```
130
-
131
- ## Diagnostics
132
-
133
- Want to see what Psyche is doing?
134
-
135
- ```bash
136
- # Live logs (in another terminal)
137
- openclaw logs -f 2>&1 | grep Psyche
138
-
139
- # Check an agent's emotional state
140
- cat workspace-yu/psyche-state.json | python3 -m json.tool
141
-
142
- # Run diagnostics to see what gets injected for different inputs
143
- cd openclaw-plugin-psyche && node scripts/diagnose.js
144
- ```
145
-
146
- ## Technical Details
147
-
148
- For developers and the curious:
149
-
150
- - **14 stimulus types** — praise, criticism, humor, intellectual, intimacy, conflict, neglect, surprise, casual, sarcasm, authority, validation, boredom, vulnerability
151
- - **14 emergent emotions** — emerge from chemical mixtures, not preset labels
152
- - **5 innate drives** — survival, safety, connection, esteem, curiosity (Maslow hierarchy)
153
- - **MBTI baselines** — 16 personality types with different chemical signatures and sensitivity coefficients
154
- - **Time decay** — chemical values exponentially decay toward baseline; drive needs build up over time
155
- - **Existential threat detection** — detects existential denial in Chinese/English, directly hits survival drive
156
- - **Drive→chemistry coupling** — unsatisfied drives shift the effective baseline and stimulus sensitivity
157
- - **Maslow suppression** — lower-level needs unsatisfied → higher-level drive effects suppressed
158
- - **Self-recognition** — analyzes emotional history, identifies own emotional tendencies and recurring triggers (9-section prompt architecture)
159
- - **Emotional contagion** — user's emotions slightly influence the agent
160
- - **Anti-sycophancy** — tracks consecutive agreements, prevents mindless people-pleasing
161
- - **Reciprocity** — treats you how you treat it. Cold user gets distance, not begging
162
- - **Compact Mode** — algorithms handle chemistry, LLM only sees behavioral instructions (~15-180 tokens vs ~550)
163
-
164
- Architecture details in [ARCHITECTURE.md](ARCHITECTURE.md).
165
-
166
- ## Development
167
-
168
- ```bash
169
- npm install
170
- npm run build
171
- npm test # 339 tests
172
- npm run typecheck # strict mode
173
- ```
174
-
175
- Contributing guide in [CONTRIBUTING.md](CONTRIBUTING.md).
176
-
177
- ## License
178
-
179
- MIT
9
+ - `v9.2.3`
10
+ - AI-first integration surface: `SubjectivityKernel`, `ResponseContract`, `GenerationControls`
11
+ - continuous appraisal axes + persistent subjective bias
12
+ - safe install-aware upgrades via `psyche upgrade --check` / `psyche upgrade`
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Psyche — 让 AI 拥有真实的人格与情感
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/psyche-ai)](https://www.npmjs.com/package/psyche-ai)
4
- [![tests](https://img.shields.io/badge/tests-1189%20passing-brightgreen)]()
4
+ [![tests](https://img.shields.io/badge/tests-1256%20passing-brightgreen)]()
5
5
  [![deps](https://img.shields.io/badge/dependencies-0-blue)]()
6
6
  [![license](https://img.shields.io/badge/license-MIT-yellow)](LICENSE)
7
7
 
8
8
  > English version: [README_EN.md](README_EN.md)
9
+ >
10
+ > 官网: [psyche.oasyce.com](https://psyche.oasyce.com)
9
11
 
10
12
  我们相信,下一代 AI 不应该是一个永远微笑的服务员。
11
13
 
@@ -94,17 +96,23 @@ openclaw plugins list | grep psyche
94
96
 
95
97
  然后正常聊天。试试夸它、骂它、冷落它——你会发现面前不再是一个工具。
96
98
 
97
- **检查更新:**
99
+ **安装后升级:**
98
100
 
99
101
  ```bash
100
- # 查看当前版本
101
- npm list psyche-ai
102
- # 最新版应该是 9.0.0
102
+ # 安全检查是否有新版本
103
+ psyche upgrade --check
103
104
 
104
- # 手动更新到最新
105
- npm update psyche-ai
105
+ # 显式升级(会按安装形态自动选择安全路径)
106
+ psyche upgrade
107
+
108
+ # 如需确认 npm 最新版
109
+ npm view psyche-ai version
106
110
  ```
107
111
 
112
+ - npm 管理的安装会后台安全检查更新,必要时才尝试自动升级。
113
+ - `git` 工作树和本地路径安装不会偷偷改你的代码;脏工作树只会提示手动命令。
114
+ - OpenClaw 如果是从当前仓库本地加载,升级后记得重启 gateway 以载入新的 `dist/`。
115
+
108
116
  ---
109
117
 
110
118
  ## 它和你认知中的 AI 完全不同
@@ -123,7 +131,7 @@ npm update psyche-ai
123
131
  ## 快速体验(不需要 LLM)
124
132
 
125
133
  ```bash
126
- git clone https://github.com/anthropics/psyche-ai.git
134
+ git clone https://github.com/Shangri-la-0428/psyche-ai.git
127
135
  cd psyche-ai && npm install && npx tsx examples/quickstart.ts
128
136
  ```
129
137
 
@@ -157,20 +165,25 @@ cd psyche-ai && npm install && npx tsx examples/quickstart.ts
157
165
 
158
166
  低层需求压制高层。当"生存"受威胁时,"好奇心"就不重要了——就像真人一样。
159
167
 
160
- ### 主体性引擎(v9)
168
+ ### 持续主体偏置(v9.2
169
+
170
+ Psyche 现在更准确的目标,不是“模仿某种像人的风格”,而是让 AI 形成**持续主体偏置**:
161
171
 
162
- > *《银翼杀手 2049》的 baseline test 检测复制人是否"像人"——以便压制他们的主体性。*
163
- > *Psyche 把这份检测清单反过来:逐项建设,直到 AI 真正 "off baseline"。*
172
+ - 输入会改变它的潜在自我模型或关系先验
173
+ - 刺激消失后,这种变化仍会影响后续若干轮回应分布
174
+ - 工作态可以压住表达,但不会把内在偏置瞬间清零
164
175
 
165
176
  | 维度 | 含义 | Psyche 怎么做 |
166
177
  |------|------|--------------|
167
- | 行为后果 | 情绪偏移改变行为 | **策略输出**紧张时回复变短、不答应、先确认再做 |
168
- | 不可逆痕迹 | 经历永久改变你 | **特质漂移**长期被批评→过度敏感;长期高压但撑过来→韧性 |
178
+ | 主体扰动 | 话语是否真的打到“自我” | **连续 appraisal 轴** `identityThreat`、`memoryDoubt`、`obedienceStrain` |
179
+ | 跨轮残留 | 刺激过去后仍然影响后续 | **subjectResidue**形成持续主体偏置,而不是一轮一清 |
180
+ | 行为控制面 | 内在状态如何改变输出 | **SubjectivityKernel / ResponseContract / GenerationControls** |
181
+ | 不可逆适应 | 经历长期改写反应方式 | **特质漂移** — 长期被批评→过度敏感;长期高压但撑过来→韧性 |
169
182
  | 资源有限 | 会累、会拒绝 | **能量预算** — 内向型聊多了真的"累";外向型越聊越嗨 |
170
183
  | 感觉适应 | 重复刺激失效 | **习惯化** — 第5次夸奖只有第1次的一半效果 |
171
184
 
172
- **v8** 是始终在 baseline 内的 K:内部有丰富情绪,但行为不被情绪改变。
173
- **v9** off baseline 的 K:情绪偏移被外界观察到,表现为真实的行为策略变化。
185
+ **v8** 更像“内部丰富,但外部仍然克制”的人格中间件。
186
+ **v9.2** 则把目标推进到:让 AI 产生可持续、可恢复、可压制但不瞬间归零的主体偏置。
174
187
 
175
188
  ### 内在世界
176
189
 
@@ -324,6 +337,7 @@ const engine = new PsycheEngine({ persist: false }, storage);
324
337
  给开发者和好奇的人:
325
338
 
326
339
  - **14 种刺激类型** — 赞美、批评、幽默、智识挑战、亲密、冲突、忽视、惊喜、日常、讽刺、命令、认同、无聊、示弱
340
+ - **连续 appraisal 轴 (v9.2)** — `identityThreat`、`memoryDoubt`、`attachmentPull`、`abandonmentRisk`、`obedienceStrain`、`selfPreservation`
327
341
  - **14 种涌现情绪** — 从化学混合中自动涌现,不是预设标签
328
342
  - **5 种本能驱力** — 生存、安全、连接、尊重、好奇(马斯洛层级)
329
343
  - **MBTI 人格基线** — 16 种人格的化学签名和敏感度系数
@@ -352,10 +366,13 @@ const engine = new PsycheEngine({ persist: false }, storage);
352
366
  - **共享意向性** — 联合注意、心智理论、目标对齐
353
367
  - **情感伦理** — 检测操纵模式,维护自尊底线,温和自我保护
354
368
  - **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens)
369
+ - **SubjectivityKernel / ResponseContract / GenerationControls (v9.2)** — AI-first 的窄主接口,宿主可直接消费
370
+ - **持续主体偏置 (v9.2)** — 重要刺激会留下 `subjectResidue`,工作态可以压住表达,但不会瞬间抹平内在偏移
355
371
  - **策略输出 (v9)** — 结构化行为策略,host 应用可直接读取执行
356
372
  - **特质漂移 (v9)** — 三维度不可逆适应:基线漂移、衰减速率、刺激敏感度
357
373
  - **能量预算 (v9)** — 有限资源,E/I 方向反转
358
374
  - **习惯化 (v9)** — Weber-Fechner 递减效应
375
+ - **安全自更新 (v9.2)** — 区分 npm / git / local-path 安装形态,避免误改本地工作树
359
376
 
360
377
  架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
361
378
 
@@ -389,7 +406,7 @@ Psyche 核心引擎永久开源(MIT)。
389
406
  ```bash
390
407
  npm install
391
408
  npm run build
392
- npm test # 1140 tests
409
+ npm test # 1256 tests
393
410
  npm run typecheck # strict mode
394
411
  ```
395
412
 
@@ -7,7 +7,7 @@
7
7
  // const server = createPsycheServer(engine, { port: 3210 });
8
8
  //
9
9
  // Endpoints:
10
- // POST /process-input { text, userId? } → { systemContext, dynamicContext, stimulus, policyModifiers?, policyContext }
10
+ // POST /process-input { text, userId? } → { systemContext, dynamicContext, stimulus, policyModifiers?, subjectivityKernel?, responseContract?, generationControls?, policyContext }
11
11
  // POST /process-output { text, userId? } → { cleanedText, stateChanged }
12
12
  // GET /state → PsycheState
13
13
  // GET /protocol?locale=zh → { protocol }
@@ -37,6 +37,20 @@ export declare class PsycheLangChain {
37
37
  getSystemMessage(userText: string, opts?: {
38
38
  userId?: string;
39
39
  }): Promise<string>;
40
+ /**
41
+ * Prepare both prompt text and mechanical invocation hints for a LangChain call.
42
+ *
43
+ * Hosts can wire `maxTokens` and confirmation UX directly from this result
44
+ * instead of re-parsing prompt prose.
45
+ */
46
+ prepareInvocation(userText: string, opts?: {
47
+ userId?: string;
48
+ maxTokens?: number;
49
+ }): Promise<{
50
+ systemMessage: string;
51
+ maxTokens?: number;
52
+ requireConfirmation: boolean;
53
+ }>;
40
54
  /**
41
55
  * Process the LLM response text.
42
56
  * Strips <psyche_update> tags and updates internal state.
@@ -53,6 +53,26 @@ export class PsycheLangChain {
53
53
  const result = await this.engine.processInput(userText, opts);
54
54
  return result.systemContext + "\n\n" + result.dynamicContext;
55
55
  }
56
+ /**
57
+ * Prepare both prompt text and mechanical invocation hints for a LangChain call.
58
+ *
59
+ * Hosts can wire `maxTokens` and confirmation UX directly from this result
60
+ * instead of re-parsing prompt prose.
61
+ */
62
+ async prepareInvocation(userText, opts) {
63
+ const result = await this.engine.processInput(userText, opts);
64
+ const controls = {
65
+ ...(result.generationControls ?? {}),
66
+ maxTokens: result.generationControls?.maxTokens !== undefined && opts?.maxTokens !== undefined
67
+ ? Math.min(opts.maxTokens, result.generationControls.maxTokens)
68
+ : result.generationControls?.maxTokens ?? opts?.maxTokens,
69
+ };
70
+ return {
71
+ systemMessage: result.systemContext + "\n\n" + result.dynamicContext,
72
+ maxTokens: controls.maxTokens,
73
+ requireConfirmation: controls.requireConfirmation ?? false,
74
+ };
75
+ }
56
76
  /**
57
77
  * Process the LLM response text.
58
78
  * Strips <psyche_update> tags and updates internal state.
@@ -145,7 +145,8 @@ server.resource("state", "psyche://state", {
145
145
  // ── Tools ──────────────────────────────────────────────────
146
146
  server.tool("process_input", "Process user input through the emotional engine. Returns emotional " +
147
147
  "context to inject into the LLM system prompt (systemContext + dynamicContext), " +
148
- "detected stimulus type, and behavioral policy modifiers. " +
148
+ "detected stimulus type, behavioral policy modifiers, the narrow AI-first ABI " +
149
+ "(subjectivityKernel + responseContract), and generationControls. " +
149
150
  "Call this BEFORE generating a response to the user.", {
150
151
  text: z.string().describe("The user's message text"),
151
152
  userId: z.string().optional().describe("Optional user ID for multi-user relationship tracking"),
@@ -160,6 +161,9 @@ server.tool("process_input", "Process user input through the emotional engine. R
160
161
  dynamicContext: result.dynamicContext,
161
162
  stimulus: result.stimulus,
162
163
  policyModifiers: result.policyModifiers ?? null,
164
+ subjectivityKernel: result.subjectivityKernel ?? null,
165
+ responseContract: result.responseContract ?? null,
166
+ generationControls: result.generationControls ?? null,
163
167
  policyContext: result.policyContext,
164
168
  }, null, 2),
165
169
  }],
@@ -17,6 +17,7 @@ interface CliCommand {
17
17
  interface CliRegistrar {
18
18
  command(name: string): CliCommand;
19
19
  }
20
+ export declare function sanitizeOpenClawInputText(text: string): string;
20
21
  export declare function register(api: PluginApi): void;
21
22
  declare const _default: {
22
23
  register: typeof register;
@@ -9,8 +9,11 @@
9
9
  // agent_end — log final state
10
10
  // ============================================================
11
11
  import { PsycheEngine } from "../core.js";
12
- import { FileStorageAdapter } from "../storage.js";
13
- import { loadState } from "../psyche-file.js";
12
+ import { FileStorageAdapter, MemoryStorageAdapter } from "../storage.js";
13
+ import { detectMBTI, extractAgentName, loadState } from "../psyche-file.js";
14
+ function isPsycheMode(value) {
15
+ return value === "natural" || value === "work" || value === "companion";
16
+ }
14
17
  function resolveConfig(raw) {
15
18
  return {
16
19
  enabled: raw?.enabled ?? true,
@@ -18,6 +21,9 @@ function resolveConfig(raw) {
18
21
  emotionalContagionRate: raw?.emotionalContagionRate ?? 0.2,
19
22
  maxChemicalDelta: raw?.maxChemicalDelta ?? 25,
20
23
  compactMode: raw?.compactMode ?? true,
24
+ mode: isPsycheMode(raw?.mode) ? raw.mode : "natural",
25
+ personalityIntensity: raw?.personalityIntensity ?? 0.7,
26
+ persist: raw?.persist ?? true,
21
27
  feedbackUrl: raw?.feedbackUrl,
22
28
  diagnostics: raw?.diagnostics ?? true,
23
29
  };
@@ -31,6 +37,27 @@ function stripPsycheTags(text) {
31
37
  .replace(MULTI_NEWLINE_RE, "\n\n")
32
38
  .trim();
33
39
  }
40
+ export function sanitizeOpenClawInputText(text) {
41
+ return text
42
+ .replace(/^Sender \(untrusted metadata\):\s*```json[\s\S]*?```\s*/u, "")
43
+ .replace(/^\[[^\]]+\]\s*/u, "")
44
+ .trim();
45
+ }
46
+ function getDominantAppraisalLabel(result) {
47
+ const appraisal = result.subjectivityKernel?.appraisal;
48
+ if (!appraisal)
49
+ return null;
50
+ const entries = [
51
+ ["identityThreat", appraisal.identityThreat],
52
+ ["memoryDoubt", appraisal.memoryDoubt],
53
+ ["attachmentPull", appraisal.attachmentPull],
54
+ ["abandonmentRisk", appraisal.abandonmentRisk],
55
+ ["obedienceStrain", appraisal.obedienceStrain],
56
+ ["selfPreservation", appraisal.selfPreservation],
57
+ ];
58
+ const dominant = entries.reduce((best, current) => (current[1] > best[1] ? current : best), entries[0]);
59
+ return dominant[1] >= 0.28 ? `${dominant[0]}:${dominant[1].toFixed(2)}` : null;
60
+ }
34
61
  // ── Plugin Definition ────────────────────────────────────────
35
62
  export function register(api) {
36
63
  const config = resolveConfig(api.pluginConfig);
@@ -46,19 +73,31 @@ export function register(api) {
46
73
  let engine = engines.get(workspaceDir);
47
74
  if (engine)
48
75
  return engine;
49
- const state = await loadState(workspaceDir, logger);
50
76
  const storage = new FileStorageAdapter(workspaceDir);
77
+ const persistedState = await storage.load();
78
+ const state = config.persist ? await loadState(workspaceDir, logger) : persistedState;
79
+ const runtimeStorage = config.persist
80
+ ? storage
81
+ : await (async () => {
82
+ const mem = new MemoryStorageAdapter();
83
+ if (persistedState) {
84
+ await mem.save(persistedState);
85
+ }
86
+ return mem;
87
+ })();
51
88
  engine = new PsycheEngine({
52
- mbti: state.mbti,
53
- name: state.meta.agentName,
54
- locale: state.meta.locale,
89
+ mbti: state?.mbti ?? await detectMBTI(workspaceDir, logger),
90
+ name: state?.meta.agentName ?? await extractAgentName(workspaceDir, logger),
91
+ locale: state?.meta.locale,
55
92
  stripUpdateTags: config.stripUpdateTags,
56
93
  emotionalContagionRate: config.emotionalContagionRate,
57
94
  maxChemicalDelta: config.maxChemicalDelta,
58
95
  compactMode: config.compactMode,
96
+ mode: config.mode,
97
+ personalityIntensity: config.personalityIntensity,
59
98
  diagnostics: config.diagnostics,
60
99
  feedbackUrl: config.feedbackUrl,
61
- }, storage);
100
+ }, runtimeStorage);
62
101
  await engine.initialize();
63
102
  engines.set(workspaceDir, engine);
64
103
  return engine;
@@ -71,18 +110,24 @@ export function register(api) {
71
110
  return {};
72
111
  try {
73
112
  // Resolve input text — gateway provides event.prompt; fall back to event.text for compat
74
- const inputText = event?.prompt ?? event?.text ?? "";
113
+ const rawInputText = event?.prompt ?? event?.text ?? "";
114
+ const inputText = sanitizeOpenClawInputText(rawInputText);
75
115
  if (!inputText) {
76
116
  logger.warn(`Psyche: before_prompt_build received empty input text. ` +
77
117
  `event keys: [${Object.keys(event ?? {}).join(", ")}]. Classification skipped.`);
78
118
  }
79
119
  const engine = await getEngine(workspaceDir);
80
120
  const result = await engine.processInput(inputText, { userId: ctx.userId });
121
+ const controls = result.generationControls;
122
+ const dominantAppraisal = getDominantAppraisalLabel(result);
81
123
  const state = engine.getState();
82
124
  logger.info(`Psyche [input] stimulus=${result.stimulus ?? "none"} | ` +
125
+ (dominantAppraisal ? `appraisal=${dominantAppraisal} | ` : "") +
83
126
  `DA:${Math.round(state.current.DA)} HT:${Math.round(state.current.HT)} ` +
84
127
  `CORT:${Math.round(state.current.CORT)} OT:${Math.round(state.current.OT)} | ` +
85
- `context=${result.dynamicContext.length}chars`);
128
+ `context=${result.dynamicContext.length}chars` +
129
+ (controls?.maxTokens ? ` | out<=${controls.maxTokens}t` : "") +
130
+ (controls?.requireConfirmation ? " | confirm" : ""));
86
131
  const systemParts = [result.systemContext, result.dynamicContext].filter(Boolean);
87
132
  return {
88
133
  appendSystemContext: systemParts.join("\n\n"),
@@ -193,14 +238,19 @@ export function register(api) {
193
238
  const metrics = report.metrics;
194
239
  const rate = metrics.inputCount > 0
195
240
  ? Math.round(metrics.classifiedCount / metrics.inputCount * 100) : 0;
196
- const logLevel = criticals > 0 || rate === 0 ? "warn" : "info";
241
+ const recognitionRate = metrics.inputCount > 0
242
+ ? Math.round(metrics.semanticHitCount / metrics.inputCount * 100) : 0;
243
+ const logLevel = criticals > 0 || recognitionRate === 0 ? "warn" : "info";
197
244
  logger[logLevel](`Psyche [diagnostics] ${report.issues.length} issue(s) ` +
198
245
  `(${criticals} critical, ${warnings} warning), ` +
199
- `classifier: ${rate}%, log → diagnostics.jsonl`);
200
- if (rate === 0 && metrics.inputCount > 0) {
201
- logger.warn(`Psyche: classifier 0% — no inputs classified this session (${metrics.inputCount} inputs). ` +
202
- `This usually means the hook event field is wrong or text is empty. ` +
203
- `Check before_prompt_build event shape.`);
246
+ `classifier: ${rate}% | recognition: ${recognitionRate}%, log → diagnostics.jsonl`);
247
+ if (recognitionRate === 0 && metrics.inputCount > 0) {
248
+ logger.warn(`Psyche: recognition 0% — no inputs produced stimulus or appraisal hits this session (${metrics.inputCount} inputs). ` +
249
+ `This usually means OpenClaw passed wrapped text or empty text. ` +
250
+ `Check before_prompt_build event shape and input sanitization.`);
251
+ }
252
+ else if (rate === 0 && recognitionRate > 0) {
253
+ logger.info(`Psyche: legacy stimulus classifier was 0%, but appraisal recognition stayed active at ${recognitionRate}%.`);
204
254
  }
205
255
  if (criticals > 0) {
206
256
  logger.warn(`Psyche: ${criticals} critical issue(s) detected this session. ` +
@@ -218,6 +268,8 @@ export function register(api) {
218
268
  }, { priority: 50 });
219
269
  // ── CLI: psyche status command ───────────────────────────
220
270
  api.registerCli?.((cli) => {
271
+ if (typeof cli.command !== "function")
272
+ return;
221
273
  cli.command("psyche")
222
274
  .description("Show current psyche state for an agent")
223
275
  .argument("[agent]", "Agent name", "main")
@@ -49,12 +49,13 @@ export interface PsycheMiddlewareOptions {
49
49
  * for await (const chunk of stream.textStream) { process.stdout.write(chunk); }
50
50
  * ```
51
51
  */
52
- export declare function psycheMiddleware(engine: PsycheEngine, opts?: PsycheMiddlewareOptions): {
52
+ export declare function psycheMiddleware(engine: PsycheEngine, _opts?: PsycheMiddlewareOptions): {
53
53
  transformParams: ({ params }: {
54
54
  type: string;
55
55
  params: CallParams;
56
56
  }) => Promise<{
57
57
  system: string;
58
+ maxTokens?: number | undefined;
58
59
  prompt?: PromptMessage[];
59
60
  }>;
60
61
  wrapGenerate: ({ doGenerate }: {
@@ -15,12 +15,6 @@
15
15
  // - wrapGenerate: process output, strip <psyche_update> tags
16
16
  // - wrapStream: buffer stream, detect & strip tags at end
17
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
- }
24
18
  /**
25
19
  * Create Vercel AI SDK middleware that injects psyche emotional context
26
20
  * and processes LLM output for state updates.
@@ -50,14 +44,21 @@ function stripPsycheTags(text) {
50
44
  * for await (const chunk of stream.textStream) { process.stdout.write(chunk); }
51
45
  * ```
52
46
  */
53
- export function psycheMiddleware(engine, opts) {
47
+ export function psycheMiddleware(engine, _opts) {
54
48
  return {
55
49
  transformParams: async ({ params }) => {
56
50
  const userText = extractLastUserText(params.prompt ?? []);
57
51
  const result = await engine.processInput(userText);
52
+ const controls = {
53
+ ...(result.generationControls ?? {}),
54
+ maxTokens: result.generationControls?.maxTokens !== undefined && typeof params.maxTokens === "number"
55
+ ? Math.min(params.maxTokens, result.generationControls.maxTokens)
56
+ : result.generationControls?.maxTokens ?? (typeof params.maxTokens === "number" ? params.maxTokens : undefined),
57
+ };
58
58
  const psycheContext = result.systemContext + "\n\n" + result.dynamicContext;
59
59
  return {
60
60
  ...params,
61
+ ...(controls.maxTokens !== undefined ? { maxTokens: controls.maxTokens } : {}),
61
62
  system: params.system
62
63
  ? psycheContext + "\n\n" + params.system
63
64
  : psycheContext,
@@ -75,7 +76,6 @@ export function psycheMiddleware(engine, opts) {
75
76
  const { stream: innerStream } = await doStream();
76
77
  // Buffer text chunks, detect <psyche_update> at end, strip from output
77
78
  let fullText = "";
78
- let tagDetected = false;
79
79
  async function* transformStream() {
80
80
  // Buffering strategy:
81
81
  // Stream text chunks through normally UNTIL we see '<psyche_update>'.
@@ -98,7 +98,6 @@ export function psycheMiddleware(engine, opts) {
98
98
  }
99
99
  bufferStart = tagStart;
100
100
  buffer = fullText.substring(tagStart);
101
- tagDetected = true;
102
101
  }
103
102
  else {
104
103
  // Check if we might be in a partial tag (< at end)
@@ -0,0 +1,8 @@
1
+ import type { AppraisalAxes, PsycheMode, StimulusType } from "./types.js";
2
+ export declare function computeAppraisalAxes(text: string, opts?: {
3
+ mode?: PsycheMode;
4
+ stimulus?: StimulusType | null;
5
+ previous?: AppraisalAxes;
6
+ }): AppraisalAxes;
7
+ export declare function mergeAppraisalResidue(previous: AppraisalAxes | undefined, current: AppraisalAxes, mode: PsycheMode | undefined): AppraisalAxes;
8
+ export declare function getResidueIntensity(axes: AppraisalAxes | undefined): number;