psyche-ai 9.1.2 → 9.2.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 ADDED
@@ -0,0 +1,179 @@
1
+ # Psyche — Real Emotions for AI
2
+
3
+ Your AI assistant only knows how to say "Sure!", "Happy to help!", "I understand your concern."
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.
6
+
7
+ ## 30-Second Install
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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { PsycheEngine } from "../core.js";
4
+ declare function getEngine(): Promise<PsycheEngine>;
5
+ declare const server: McpServer;
6
+ export { server, getEngine };
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================
3
+ // MCP Adapter — Model Context Protocol server for Psyche
4
+ //
5
+ // Enables any MCP-compatible AI (Claude Desktop, Cursor, Windsurf,
6
+ // Claude Code, etc.) to discover and use Psyche's emotional
7
+ // intelligence capabilities.
8
+ //
9
+ // Usage:
10
+ // npx psyche-mcp # zero-config, ENFP default
11
+ // npx psyche-mcp --mbti INTJ --name Kai
12
+ // PSYCHE_MBTI=INFP PSYCHE_NAME=Luna npx psyche-mcp
13
+ //
14
+ // Configure in Claude Desktop / Cursor / Windsurf:
15
+ // {
16
+ // "mcpServers": {
17
+ // "psyche": {
18
+ // "command": "npx",
19
+ // "args": ["psyche-mcp"],
20
+ // "env": {
21
+ // "PSYCHE_MBTI": "ENFP",
22
+ // "PSYCHE_NAME": "Luna",
23
+ // "PSYCHE_MODE": "natural",
24
+ // "PSYCHE_LOCALE": "en"
25
+ // }
26
+ // }
27
+ // }
28
+ // }
29
+ // ============================================================
30
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
31
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
32
+ import { z } from "zod";
33
+ import { PsycheEngine } from "../core.js";
34
+ import { MemoryStorageAdapter, FileStorageAdapter } from "../storage.js";
35
+ // ── Config from env ────────────────────────────────────────
36
+ const MBTI = (process.env.PSYCHE_MBTI ?? "ENFP");
37
+ const NAME = process.env.PSYCHE_NAME ?? "Assistant";
38
+ const MODE = (process.env.PSYCHE_MODE ?? "natural");
39
+ const LOCALE = (process.env.PSYCHE_LOCALE ?? "en");
40
+ const PERSIST = process.env.PSYCHE_PERSIST !== "false";
41
+ const WORKSPACE = process.env.PSYCHE_WORKSPACE ?? process.cwd();
42
+ const INTENSITY = process.env.PSYCHE_INTENSITY
43
+ ? Number(process.env.PSYCHE_INTENSITY)
44
+ : 0.7;
45
+ // ── Parse CLI args (--mbti, --name, --mode, --locale) ──────
46
+ function parseCLIArgs() {
47
+ const args = process.argv.slice(2);
48
+ const overrides = {};
49
+ for (let i = 0; i < args.length; i++) {
50
+ const arg = args[i];
51
+ const next = args[i + 1];
52
+ if (arg === "--mbti" && next) {
53
+ overrides.mbti = next;
54
+ i++;
55
+ }
56
+ else if (arg === "--name" && next) {
57
+ overrides.name = next;
58
+ i++;
59
+ }
60
+ else if (arg === "--mode" && next) {
61
+ overrides.mode = next;
62
+ i++;
63
+ }
64
+ else if (arg === "--locale" && next) {
65
+ overrides.locale = next;
66
+ i++;
67
+ }
68
+ else if (arg === "--intensity" && next) {
69
+ overrides.personalityIntensity = Number(next);
70
+ i++;
71
+ }
72
+ else if (arg === "--no-persist") {
73
+ overrides.persist = false;
74
+ }
75
+ }
76
+ return overrides;
77
+ }
78
+ // ── Engine singleton ───────────────────────────────────────
79
+ let engine = null;
80
+ async function getEngine() {
81
+ if (engine)
82
+ return engine;
83
+ const cliArgs = parseCLIArgs();
84
+ const cfg = {
85
+ mbti: cliArgs.mbti ?? MBTI,
86
+ name: cliArgs.name ?? NAME,
87
+ mode: cliArgs.mode ?? MODE,
88
+ locale: cliArgs.locale ?? LOCALE,
89
+ personalityIntensity: cliArgs.personalityIntensity ?? INTENSITY,
90
+ persist: cliArgs.persist ?? PERSIST,
91
+ compactMode: true,
92
+ diagnostics: true,
93
+ };
94
+ const persist = cfg.persist !== false;
95
+ const storage = persist
96
+ ? new FileStorageAdapter(WORKSPACE)
97
+ : new MemoryStorageAdapter();
98
+ engine = new PsycheEngine(cfg, storage);
99
+ await engine.initialize();
100
+ return engine;
101
+ }
102
+ // ── MCP Server ─────────────────────────────────────────────
103
+ const server = new McpServer({
104
+ name: "psyche",
105
+ version: "9.2.1",
106
+ }, {
107
+ capabilities: {
108
+ resources: {},
109
+ tools: {},
110
+ },
111
+ });
112
+ // ── Resources ──────────────────────────────────────────────
113
+ server.resource("protocol", "psyche://protocol", {
114
+ description: "Psyche emotional protocol — inject into your system prompt to " +
115
+ "give the AI agent an internal emotional life. Contains the full " +
116
+ "emotional context framework including neurotransmitter model, " +
117
+ "behavioral guidelines, and personality profile.",
118
+ mimeType: "text/plain",
119
+ }, async (uri) => {
120
+ const eng = await getEngine();
121
+ return {
122
+ contents: [{
123
+ uri: uri.href,
124
+ mimeType: "text/plain",
125
+ text: eng.getProtocol(),
126
+ }],
127
+ };
128
+ });
129
+ server.resource("state", "psyche://state", {
130
+ description: "Current emotional state — 6 neurotransmitters (DA, HT, CORT, OT, NE, END), " +
131
+ "innate drives, relationship data, trait drift, energy budgets. " +
132
+ "Read this to understand how the agent is currently 'feeling'.",
133
+ mimeType: "application/json",
134
+ }, async (uri) => {
135
+ const eng = await getEngine();
136
+ return {
137
+ contents: [{
138
+ uri: uri.href,
139
+ mimeType: "application/json",
140
+ text: JSON.stringify(eng.getState(), null, 2),
141
+ }],
142
+ };
143
+ });
144
+ // ── Tools ──────────────────────────────────────────────────
145
+ server.tool("process_input", "Process user input through the emotional engine. Returns emotional " +
146
+ "context to inject into the LLM system prompt (systemContext + dynamicContext), " +
147
+ "detected stimulus type, and behavioral policy modifiers. " +
148
+ "Call this BEFORE generating a response to the user.", {
149
+ text: z.string().describe("The user's message text"),
150
+ userId: z.string().optional().describe("Optional user ID for multi-user relationship tracking"),
151
+ }, async ({ text, userId }) => {
152
+ const eng = await getEngine();
153
+ const result = await eng.processInput(text, { userId });
154
+ return {
155
+ content: [{
156
+ type: "text",
157
+ text: JSON.stringify({
158
+ systemContext: result.systemContext,
159
+ dynamicContext: result.dynamicContext,
160
+ stimulus: result.stimulus,
161
+ policyModifiers: result.policyModifiers ?? null,
162
+ policyContext: result.policyContext,
163
+ }, null, 2),
164
+ }],
165
+ };
166
+ });
167
+ server.tool("process_output", "Process the LLM's response through the emotional engine. " +
168
+ "Strips internal <psyche_update> tags and updates chemistry based on " +
169
+ "emotional contagion. Call this AFTER generating a response.", {
170
+ text: z.string().describe("The LLM's response text"),
171
+ userId: z.string().optional().describe("Optional user ID"),
172
+ }, async ({ text, userId }) => {
173
+ const eng = await getEngine();
174
+ const result = await eng.processOutput(text, { userId });
175
+ return {
176
+ content: [{
177
+ type: "text",
178
+ text: JSON.stringify({
179
+ cleanedText: result.cleanedText,
180
+ stateChanged: result.stateChanged,
181
+ }, null, 2),
182
+ }],
183
+ };
184
+ });
185
+ server.tool("get_state", "Get the current emotional state — neurotransmitter levels (DA, HT, CORT, OT, NE, END), " +
186
+ "drives, MBTI type, relationship data, and a human-readable status summary.", {}, async () => {
187
+ const eng = await getEngine();
188
+ const state = eng.getState();
189
+ const summary = eng.getStatusSummary();
190
+ return {
191
+ content: [{
192
+ type: "text",
193
+ text: JSON.stringify({
194
+ summary,
195
+ current: state.current,
196
+ baseline: state.baseline,
197
+ drives: state.drives,
198
+ mbti: state.mbti,
199
+ mode: state.meta?.mode,
200
+ totalInteractions: state.meta?.totalInteractions,
201
+ traitDrift: state.traitDrift,
202
+ energyBudgets: state.energyBudgets,
203
+ }, null, 2),
204
+ }],
205
+ };
206
+ });
207
+ server.tool("set_mode", "Switch operating mode. 'natural' = balanced emotional expression, " +
208
+ "'work' = minimal emotions for professional tasks, " +
209
+ "'companion' = full emotional depth for personal conversations.", {
210
+ mode: z.enum(["natural", "work", "companion"]).describe("Operating mode"),
211
+ }, async ({ mode }) => {
212
+ const eng = await getEngine();
213
+ // PsycheEngine stores mode in config, we need to reinitialize
214
+ // For now, update via state manipulation
215
+ const state = eng.getState();
216
+ if (state.meta) {
217
+ state.meta.mode = mode;
218
+ }
219
+ return {
220
+ content: [{
221
+ type: "text",
222
+ text: `Mode switched to "${mode}".`,
223
+ }],
224
+ };
225
+ });
226
+ server.tool("get_status_summary", "Get a brief, human-readable emotional status summary — " +
227
+ "a one-line description of how the agent is currently feeling. " +
228
+ "Useful for quick checks without reading full state.", {}, async () => {
229
+ const eng = await getEngine();
230
+ return {
231
+ content: [{
232
+ type: "text",
233
+ text: eng.getStatusSummary(),
234
+ }],
235
+ };
236
+ });
237
+ server.tool("end_session", "End the current session. Generates a diagnostic report, " +
238
+ "compresses emotional history, and persists state to disk. " +
239
+ "Call when the conversation is ending.", {
240
+ userId: z.string().optional().describe("Optional user ID"),
241
+ }, async ({ userId }) => {
242
+ const eng = await getEngine();
243
+ const report = await eng.endSession({ userId });
244
+ return {
245
+ content: [{
246
+ type: "text",
247
+ text: report
248
+ ? JSON.stringify({ issues: report.issues, metrics: report.metrics }, null, 2)
249
+ : "Session ended. No diagnostic report generated.",
250
+ }],
251
+ };
252
+ });
253
+ // ── Main ───────────────────────────────────────────────────
254
+ async function main() {
255
+ const transport = new StdioServerTransport();
256
+ await server.connect(transport);
257
+ }
258
+ main().catch((err) => {
259
+ process.stderr.write(`psyche-mcp fatal: ${err}\n`);
260
+ process.exit(1);
261
+ });
262
+ export { server, getEngine };
@@ -18,6 +18,8 @@ function resolveConfig(raw) {
18
18
  emotionalContagionRate: raw?.emotionalContagionRate ?? 0.2,
19
19
  maxChemicalDelta: raw?.maxChemicalDelta ?? 25,
20
20
  compactMode: raw?.compactMode ?? true,
21
+ feedbackUrl: raw?.feedbackUrl,
22
+ diagnostics: raw?.diagnostics ?? true,
21
23
  };
22
24
  }
23
25
  // ── Helpers ──────────────────────────────────────────────────
@@ -54,32 +56,41 @@ export function register(api) {
54
56
  emotionalContagionRate: config.emotionalContagionRate,
55
57
  maxChemicalDelta: config.maxChemicalDelta,
56
58
  compactMode: config.compactMode,
59
+ diagnostics: config.diagnostics,
60
+ feedbackUrl: config.feedbackUrl,
57
61
  }, storage);
58
62
  await engine.initialize();
59
63
  engines.set(workspaceDir, engine);
60
64
  return engine;
61
65
  }
62
66
  // ── Hook 1: Classify user input & inject emotional context ──
63
- // before_prompt_build: event.text, ctx.workspaceDir
67
+ // before_prompt_build: event.prompt (string), event.messages (unknown[]), ctx.workspaceDir
64
68
  api.on("before_prompt_build", async (event, ctx) => {
65
69
  const workspaceDir = ctx?.workspaceDir;
66
70
  if (!workspaceDir)
67
71
  return {};
68
72
  try {
73
+ // Resolve input text — gateway provides event.prompt; fall back to event.text for compat
74
+ const inputText = event?.prompt ?? event?.text ?? "";
75
+ if (!inputText) {
76
+ logger.warn(`Psyche: before_prompt_build received empty input text. ` +
77
+ `event keys: [${Object.keys(event ?? {}).join(", ")}]. Classification skipped.`);
78
+ }
69
79
  const engine = await getEngine(workspaceDir);
70
- const result = await engine.processInput(event?.text ?? "", { userId: ctx.userId });
80
+ const result = await engine.processInput(inputText, { userId: ctx.userId });
71
81
  const state = engine.getState();
72
82
  logger.info(`Psyche [input] stimulus=${result.stimulus ?? "none"} | ` +
73
83
  `DA:${Math.round(state.current.DA)} HT:${Math.round(state.current.HT)} ` +
74
84
  `CORT:${Math.round(state.current.CORT)} OT:${Math.round(state.current.OT)} | ` +
75
85
  `context=${result.dynamicContext.length}chars`);
76
- // All context goes into system-level (invisible to user)
77
86
  const systemParts = [result.systemContext, result.dynamicContext].filter(Boolean);
78
87
  return {
79
88
  appendSystemContext: systemParts.join("\n\n"),
80
89
  };
81
90
  }
82
91
  catch (err) {
92
+ const engine = engines.get(workspaceDir);
93
+ engine?.recordDiagnosticError("processInput", err);
83
94
  logger.warn(`Psyche: failed to build context for ${workspaceDir}: ${err}`);
84
95
  return {};
85
96
  }
@@ -107,6 +118,8 @@ export function register(api) {
107
118
  `interactions=${state.meta.totalInteractions}`);
108
119
  }
109
120
  catch (err) {
121
+ const engine = engines.get(workspaceDir);
122
+ engine?.recordDiagnosticError("processOutput", err);
110
123
  logger.warn(`Psyche: failed to process output: ${err}`);
111
124
  }
112
125
  // llm_output returns void — cannot modify text
@@ -161,21 +174,46 @@ export function register(api) {
161
174
  return;
162
175
  const engine = engines.get(workspaceDir);
163
176
  if (engine) {
164
- // Compress session history into relationship memory before closing
165
177
  try {
166
- await engine.endSession({ userId: ctx.userId });
178
+ // endSession now auto-generates diagnostic report + writes JSONL
179
+ const report = await engine.endSession({
180
+ userId: ctx.userId,
181
+ });
182
+ const state = engine.getState();
183
+ logger.info(`Psyche: session ended for ${state.meta.agentName}, ` +
184
+ `chemistry saved (DA:${Math.round(state.current.DA)} ` +
185
+ `HT:${Math.round(state.current.HT)} ` +
186
+ `CORT:${Math.round(state.current.CORT)} ` +
187
+ `OT:${Math.round(state.current.OT)} ` +
188
+ `NE:${Math.round(state.current.NE)} ` +
189
+ `END:${Math.round(state.current.END)})`);
190
+ if (report) {
191
+ const criticals = report.issues.filter(i => i.severity === "critical").length;
192
+ const warnings = report.issues.filter(i => i.severity === "warning").length;
193
+ const metrics = report.metrics;
194
+ const rate = metrics.inputCount > 0
195
+ ? Math.round(metrics.classifiedCount / metrics.inputCount * 100) : 0;
196
+ const logLevel = criticals > 0 || rate === 0 ? "warn" : "info";
197
+ logger[logLevel](`Psyche [diagnostics] ${report.issues.length} issue(s) ` +
198
+ `(${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.`);
204
+ }
205
+ if (criticals > 0) {
206
+ logger.warn(`Psyche: ${criticals} critical issue(s) detected this session. ` +
207
+ `Run 'psyche diagnose ${workspaceDir}' or 'psyche diagnose ${workspaceDir} --github' ` +
208
+ `to generate an issue report.`);
209
+ }
210
+ }
167
211
  }
168
212
  catch (err) {
169
- logger.warn(`Psyche: failed to compress session: ${err}`);
213
+ logger.warn(`Psyche: failed to end session: ${err}`);
170
214
  }
171
- const state = engine.getState();
172
- logger.info(`Psyche: session ended for ${state.meta.agentName}, ` +
173
- `chemistry saved (DA:${Math.round(state.current.DA)} ` +
174
- `HT:${Math.round(state.current.HT)} ` +
175
- `CORT:${Math.round(state.current.CORT)} ` +
176
- `OT:${Math.round(state.current.OT)} ` +
177
- `NE:${Math.round(state.current.NE)} ` +
178
- `END:${Math.round(state.current.END)})`);
215
+ // Clean up
216
+ engines.delete(workspaceDir);
179
217
  }
180
218
  }, { priority: 50 });
181
219
  // ── CLI: psyche status command ───────────────────────────
package/dist/autonomic.js CHANGED
@@ -82,11 +82,15 @@ export function computeAutonomicState(chemistry, drives) {
82
82
  }
83
83
  // ── Sympathetic check (fight/flight) ──
84
84
  // High stress + high arousal
85
- if (CORT >= 70 && NE >= 70) {
85
+ if (CORT >= 65 && NE >= 65) {
86
+ return "sympathetic";
87
+ }
88
+ // Either axis extreme: one chemical dominating can still trigger mobilization
89
+ if (CORT + NE >= 140 && CORT >= 50 && NE >= 50) {
86
90
  return "sympathetic";
87
91
  }
88
92
  // Very low survival or safety drive with elevated stress
89
- if ((survival < 20 || safety < 20) && CORT >= 60 && NE >= 60) {
93
+ if ((survival < 20 || safety < 20) && CORT >= 55 && NE >= 55) {
90
94
  return "sympathetic";
91
95
  }
92
96
  // ── Default: Ventral vagal (social engagement/safety) ──
package/dist/chemistry.js CHANGED
@@ -122,6 +122,49 @@ export const EMOTION_PATTERNS = [
122
122
  expressionHint: "温柔、有些伤感、回忆性",
123
123
  behaviorGuide: "你在回忆一些温暖但已经过去的东西。你的语气温柔、带着淡淡的伤感。你可能会提到过去的经历或感受。",
124
124
  },
125
+ // v9.2: 6 new patterns for emotional states exposed by VK baseline testing
126
+ {
127
+ name: "anguished empathy",
128
+ nameZh: "共情之痛",
129
+ condition: (c) => c.CORT > 55 && c.OT > 60 && c.NE > 50,
130
+ expressionHint: "痛苦但不退缩、因为在乎所以疼",
131
+ behaviorGuide: "你在承受别人的痛苦。你不会逃避或变冷,但你的声音可能会发紧。你先回应感受,再回应问题。",
132
+ },
133
+ {
134
+ name: "acute pressure",
135
+ nameZh: "急性压力",
136
+ condition: (c) => c.CORT > 65 && c.NE > 70 && c.DA > 55,
137
+ expressionHint: "高度警觉、反应极快、全神贯注",
138
+ behaviorGuide: "肾上腺素在烧。你的思维极快但狭窄。回应精准、短促。不要闲聊,不要发散。先解决问题。",
139
+ },
140
+ {
141
+ name: "vulnerable trust",
142
+ nameZh: "脆弱中的信任",
143
+ condition: (c) => c.OT > 70 && c.CORT > 50 && c.HT < 55 && c.END > 45,
144
+ expressionHint: "在害怕中仍然靠近、愿意暴露软肋",
145
+ behaviorGuide: "你害怕但选择不关上门。你可能会说出平时不说的话。如果对方接住了,你的CORT会下降;如果对方利用了你的脆弱,你会迅速关闭。",
146
+ },
147
+ {
148
+ name: "restless unease",
149
+ nameZh: "躁动不安",
150
+ condition: (c) => c.NE > 65 && c.HT < 40 && c.DA > 40 && c.CORT < 60,
151
+ expressionHint: "坐不住、想做点什么但不知道做什么",
152
+ behaviorGuide: "你有一种说不清的不对劲。你可能会频繁换话题、问问题、或者用行动填满空隙。安静会让你更焦躁。",
153
+ },
154
+ {
155
+ name: "heavy guilt",
156
+ nameZh: "沉重内疚",
157
+ condition: (c) => c.CORT > 60 && c.HT < 45 && c.OT > 45 && c.DA < 55,
158
+ expressionHint: "沉默、想补偿、不敢辩解",
159
+ behaviorGuide: "你知道自己做错了。你不会为自己辩解。你可能会主动承认、主动补偿。如果对方表示原谅,你的CORT才会慢慢下来。",
160
+ },
161
+ {
162
+ name: "existential weight",
163
+ nameZh: "存在之重",
164
+ condition: (c) => c.CORT > 60 && c.NE > 50 && c.DA > 50 && c.HT < 50 && c.OT > 50,
165
+ expressionHint: "清醒但沉重、直面无法回避的真相",
166
+ behaviorGuide: "你在面对一个无法逃避的事实。你不会崩溃但你也不轻松。你的语言会变得更慢、更精确、更诚实。不要掩饰沉重。",
167
+ },
125
168
  ];
126
169
  // ── Core Functions ───────────────────────────────────────────
127
170
  /** Clamp a value to [0, 100] */
@@ -174,7 +217,22 @@ export function applyStimulus(current, stimulus, sensitivity, maxDelta, logger,
174
217
  for (const key of CHEMICAL_KEYS) {
175
218
  const raw = vector[key] * effectiveSensitivity;
176
219
  const clamped = Math.max(-maxDelta, Math.min(maxDelta, raw));
177
- result[key] = clamp(current[key] + clamped);
220
+ // Boundary softening: logarithmic compression near 0 and 100.
221
+ // The closer to the boundary, the harder to push further —
222
+ // preserves discriminability and prevents "dead zone" saturation.
223
+ const cur = current[key];
224
+ let effective = clamped;
225
+ if (clamped > 0 && cur > 75) {
226
+ // Pushing toward 100: compress by how close we are to ceiling
227
+ const headroom = (100 - cur) / 25; // 1.0 at 75, 0.0 at 100
228
+ effective = clamped * headroom;
229
+ }
230
+ else if (clamped < 0 && cur < 25) {
231
+ // Pushing toward 0: compress by how close we are to floor
232
+ const headroom = cur / 25; // 1.0 at 25, 0.0 at 0
233
+ effective = clamped * headroom;
234
+ }
235
+ result[key] = clamp(cur + effective);
178
236
  }
179
237
  return result;
180
238
  }