psyche-ai 5.0.0 → 7.1.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.
@@ -9,6 +9,7 @@ import { getBaseline, getDefaultSelfModel, extractMBTI, getSensitivity, getTempe
9
9
  import { applyDecay, detectEmotions } from "./chemistry.js";
10
10
  import { decayDrives, computeEffectiveBaseline } from "./drives.js";
11
11
  import { t } from "./i18n.js";
12
+ import { computeSelfReflection } from "./self-recognition.js";
12
13
  const STATE_FILE = "psyche-state.json";
13
14
  const PSYCHE_MD = "PSYCHE.md";
14
15
  const IDENTITY_MD = "IDENTITY.md";
@@ -176,6 +177,127 @@ export function getRelationship(state, userId) {
176
177
  const key = userId ?? "_default";
177
178
  return state.relationships[key] ?? { ...DEFAULT_RELATIONSHIP };
178
179
  }
180
+ // ── Tendency display labels ────────────────────────────────────
181
+ const TENDENCY_LABEL_ZH = {
182
+ ascending: "上扬",
183
+ descending: "下沉",
184
+ volatile: "波动",
185
+ oscillating: "起伏",
186
+ stable: "平稳",
187
+ };
188
+ const TENDENCY_LABEL_EN = {
189
+ ascending: "ascending",
190
+ descending: "descending",
191
+ volatile: "volatile",
192
+ oscillating: "oscillating",
193
+ stable: "stable",
194
+ };
195
+ /**
196
+ * Compress the full emotionalHistory into a rich session summary and store it
197
+ * in the user's relationship.memory[]. Called ONCE at session end.
198
+ *
199
+ * Pure computation, no LLM calls.
200
+ */
201
+ export function compressSession(state, userId) {
202
+ const history = state.emotionalHistory ?? [];
203
+ // Need at least 2 entries for a meaningful summary
204
+ if (history.length < 2)
205
+ return state;
206
+ const locale = state.meta.locale ?? "zh";
207
+ const isZh = locale === "zh";
208
+ const first = history[0];
209
+ const last = history[history.length - 1];
210
+ // ── Date range ──
211
+ const d1 = new Date(first.timestamp);
212
+ const d2 = new Date(last.timestamp);
213
+ const pad = (n) => String(n).padStart(2, "0");
214
+ const dateRange = isZh
215
+ ? `${d1.getMonth() + 1}月${d1.getDate()}日 ${pad(d1.getHours())}:${pad(d1.getMinutes())}-${pad(d2.getHours())}:${pad(d2.getMinutes())}`
216
+ : `${d1.getMonth() + 1}/${d1.getDate()} ${pad(d1.getHours())}:${pad(d1.getMinutes())}-${pad(d2.getHours())}:${pad(d2.getMinutes())}`;
217
+ // ── Turn count ──
218
+ const turnCount = history.length;
219
+ // ── Stimulus distribution ──
220
+ const stimuliCounts = {};
221
+ for (const snap of history) {
222
+ if (snap.stimulus) {
223
+ stimuliCounts[snap.stimulus] = (stimuliCounts[snap.stimulus] || 0) + 1;
224
+ }
225
+ }
226
+ const stimuliStr = Object.entries(stimuliCounts)
227
+ .sort((a, b) => b[1] - a[1])
228
+ .map(([type, count]) => `${type}×${count}`)
229
+ .join(",");
230
+ // ── Chemical trajectory ──
231
+ const trajectoryParts = [];
232
+ for (const key of CHEMICAL_KEYS) {
233
+ const delta = last.chemistry[key] - first.chemistry[key];
234
+ if (Math.abs(delta) > 10) {
235
+ trajectoryParts.push(`${key}${Math.round(first.chemistry[key])}→${Math.round(last.chemistry[key])}`);
236
+ }
237
+ }
238
+ // ── Emotion arc ──
239
+ const emotions = [];
240
+ for (const snap of history) {
241
+ if (snap.dominantEmotion && (emotions.length === 0 || emotions[emotions.length - 1] !== snap.dominantEmotion)) {
242
+ emotions.push(snap.dominantEmotion);
243
+ }
244
+ }
245
+ const emotionArc = emotions.join("→");
246
+ // ── Peak event ──
247
+ let peakIdx = 0;
248
+ let peakDeviation = 0;
249
+ for (let i = 0; i < history.length; i++) {
250
+ let deviation = 0;
251
+ for (const key of CHEMICAL_KEYS) {
252
+ deviation += Math.abs(history[i].chemistry[key] - state.baseline[key]);
253
+ }
254
+ if (deviation > peakDeviation) {
255
+ peakDeviation = deviation;
256
+ peakIdx = i;
257
+ }
258
+ }
259
+ const peakSnap = history[peakIdx];
260
+ const peakLabel = isZh
261
+ ? `第${peakIdx + 1}轮:${peakSnap.stimulus ?? "?"}→${peakSnap.dominantEmotion ?? "?"}`
262
+ : `turn${peakIdx + 1}:${peakSnap.stimulus ?? "?"}→${peakSnap.dominantEmotion ?? "?"}`;
263
+ // ── Tendency ──
264
+ const reflection = computeSelfReflection(history, locale);
265
+ const tendencyLabel = isZh
266
+ ? (TENDENCY_LABEL_ZH[reflection.tendency] ?? reflection.tendency)
267
+ : (TENDENCY_LABEL_EN[reflection.tendency] ?? reflection.tendency);
268
+ // ── Build summary string ──
269
+ const turnsLabel = isZh ? "轮" : "turns";
270
+ const stimLabel = isZh ? "刺激" : "stimuli";
271
+ const trajLabel = isZh ? "轨迹" : "trajectory";
272
+ const arcLabel = isZh ? "弧线" : "arc";
273
+ const peakEventLabel = isZh ? "高峰" : "peak";
274
+ const tendLabel = isZh ? "倾向" : "tendency";
275
+ let summary = `${dateRange}(${turnCount}${turnsLabel})`;
276
+ if (stimuliStr)
277
+ summary += `: ${stimLabel}[${stimuliStr}]`;
278
+ if (trajectoryParts.length > 0)
279
+ summary += ` ${trajLabel}[${trajectoryParts.join(" ")}]`;
280
+ if (emotionArc)
281
+ summary += ` ${arcLabel}[${emotionArc}]`;
282
+ summary += ` ${peakEventLabel}[${peakLabel}]`;
283
+ summary += ` ${tendLabel}[${tendencyLabel}]`;
284
+ // ── Store in relationship memory ──
285
+ const relKey = userId ?? "_default";
286
+ const existing = state.relationships[relKey] ?? { ...DEFAULT_RELATIONSHIP };
287
+ const memory = [...(existing.memory ?? [])];
288
+ memory.push(summary);
289
+ if (memory.length > MAX_RELATIONSHIP_MEMORY) {
290
+ memory.splice(0, memory.length - MAX_RELATIONSHIP_MEMORY);
291
+ }
292
+ const updatedRel = { ...existing, memory };
293
+ const updatedRelationships = { ...state.relationships, [relKey]: updatedRel };
294
+ // ── Clear history and return ──
295
+ return {
296
+ ...state,
297
+ emotionalHistory: [],
298
+ relationships: updatedRelationships,
299
+ };
300
+ }
179
301
  /**
180
302
  * Load psyche state from workspace. Auto-initializes if missing.
181
303
  * Handles v1→v2 migration transparently.
@@ -285,6 +407,7 @@ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER)
285
407
  createdAt: now,
286
408
  totalInteractions: 0,
287
409
  locale,
410
+ mode: "natural",
288
411
  },
289
412
  };
290
413
  await saveState(workspaceDir, state);
@@ -323,6 +446,7 @@ export async function decayAndSave(workspaceDir, state) {
323
446
  /**
324
447
  * Parse a <psyche_update> block from LLM output.
325
448
  * v0.2: supports decimals, Chinese names, English names.
449
+ * v2.1: supports LLM-assisted stimulus classification.
326
450
  */
327
451
  export function parsePsycheUpdate(text, logger = NOOP_LOGGER) {
328
452
  const match = text.match(/<psyche_update>([\s\S]*?)<\/psyche_update>/);
@@ -361,20 +485,34 @@ export function parsePsycheUpdate(text, logger = NOOP_LOGGER) {
361
485
  timestamp: new Date().toISOString(),
362
486
  };
363
487
  }
488
+ // Parse LLM-assisted stimulus classification
489
+ let llmStimulus;
490
+ const stimulusMatch = block.match(/(?:stimulus|刺激类型)\s*[::]\s*(\w+)/i);
491
+ if (stimulusMatch) {
492
+ const candidate = stimulusMatch[1].trim().toLowerCase();
493
+ const VALID_STIMULI = new Set([
494
+ "praise", "criticism", "humor", "intellectual", "intimacy", "conflict",
495
+ "neglect", "surprise", "casual", "sarcasm", "authority", "validation",
496
+ "boredom", "vulnerability",
497
+ ]);
498
+ if (VALID_STIMULI.has(candidate)) {
499
+ llmStimulus = candidate;
500
+ }
501
+ }
364
502
  // Parse relationship updates
365
503
  const trustMatch = block.match(/(?:信任度|trust)\s*[::]\s*(\d+)/i);
366
504
  const intimacyMatch = block.match(/(?:亲密度|intimacy)\s*[::]\s*(\d+)/i);
367
- if (Object.keys(updates).length === 0 && !empathyLog && !trustMatch) {
505
+ if (Object.keys(updates).length === 0 && !empathyLog && !trustMatch && !llmStimulus) {
368
506
  logger.debug(t("log.parse_debug", "zh", { snippet: block.slice(0, 100) }));
369
507
  return null;
370
508
  }
371
- const result = {};
509
+ const stateUpdates = {};
372
510
  if (Object.keys(updates).length > 0) {
373
511
  // Store as partial — will be merged field-by-field in mergeUpdates
374
- result.current = updates;
512
+ stateUpdates.current = updates;
375
513
  }
376
514
  if (empathyLog) {
377
- result.empathyLog = empathyLog;
515
+ stateUpdates.empathyLog = empathyLog;
378
516
  }
379
517
  if (trustMatch || intimacyMatch) {
380
518
  const rel = {};
@@ -383,7 +521,11 @@ export function parsePsycheUpdate(text, logger = NOOP_LOGGER) {
383
521
  if (intimacyMatch)
384
522
  rel.intimacy = Math.max(0, Math.min(100, parseInt(intimacyMatch[1], 10)));
385
523
  // Store as relationships._default for merging
386
- result.relationships = { _default: rel };
524
+ stateUpdates.relationships = { _default: rel };
525
+ }
526
+ const result = { state: stateUpdates };
527
+ if (llmStimulus) {
528
+ result.llmStimulus = llmStimulus;
387
529
  }
388
530
  return result;
389
531
  }
package/dist/types.d.ts CHANGED
@@ -33,6 +33,16 @@ export type DecaySpeed = "fast" | "medium" | "slow";
33
33
  export declare const DECAY_FACTORS: Record<DecaySpeed, number>;
34
34
  /** Which chemicals decay at which speed */
35
35
  export declare const CHEMICAL_DECAY_SPEED: Record<keyof ChemicalState, DecaySpeed>;
36
+ /** Psyche operating mode */
37
+ export type PsycheMode = "natural" | "work" | "companion";
38
+ /** Big Five personality traits (0-100 each) */
39
+ export interface PersonalityTraits {
40
+ openness: number;
41
+ conscientiousness: number;
42
+ extraversion: number;
43
+ agreeableness: number;
44
+ neuroticism: number;
45
+ }
36
46
  /** MBTI type string */
37
47
  export type MBTIType = "INTJ" | "INTP" | "ENTJ" | "ENTP" | "INFJ" | "INFP" | "ENFJ" | "ENFP" | "ISTJ" | "ISFJ" | "ESTJ" | "ESFJ" | "ISTP" | "ISFP" | "ESTP" | "ESFP";
38
48
  /** Stimulus types that affect chemistry (v0.2: +5 new types) */
@@ -211,7 +221,7 @@ export interface PersonhoodState {
211
221
  export declare const DEFAULT_PERSONHOOD_STATE: PersonhoodState;
212
222
  /** Persisted psyche state for an agent (v6: digital personhood) */
213
223
  export interface PsycheState {
214
- version: 3 | 4 | 5 | 6;
224
+ version: 3 | 4 | 5 | 6 | 7;
215
225
  mbti: MBTIType;
216
226
  baseline: ChemicalState;
217
227
  current: ChemicalState;
@@ -226,11 +236,16 @@ export interface PsycheState {
226
236
  learning: LearningState;
227
237
  metacognition: MetacognitiveState;
228
238
  personhood: PersonhoodState;
239
+ /** v7: autonomic nervous system state (Polyvagal Theory) */
240
+ autonomicState?: "ventral-vagal" | "sympathetic" | "dorsal-vagal";
241
+ /** v7: session start time for homeostatic pressure calculation */
242
+ sessionStartedAt?: string;
229
243
  meta: {
230
244
  agentName: string;
231
245
  createdAt: string;
232
246
  totalInteractions: number;
233
247
  locale: Locale;
248
+ mode?: PsycheMode;
234
249
  };
235
250
  }
236
251
  /** Default relationship for new users */
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 = "5.0.0";
14
+ const CURRENT_VERSION = "5.1.0";
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");
@@ -2,7 +2,7 @@
2
2
  "id": "psyche-ai",
3
3
  "name": "Artificial Psyche",
4
4
  "description": "Virtual endocrine system, empathy engine, and agency for OpenClaw agents",
5
- "version": "5.0.0",
5
+ "version": "5.1.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -31,6 +31,24 @@
31
31
  "type": "boolean",
32
32
  "default": true,
33
33
  "description": "Compact mode: algorithms handle chemistry, LLM only sees behavioral instructions (~15-180 tokens vs ~550)"
34
+ },
35
+ "mode": {
36
+ "type": "string",
37
+ "enum": ["natural", "work", "companion"],
38
+ "default": "natural",
39
+ "description": "Operating mode: natural (balanced), work (minimal emotions), companion (full emotions)"
40
+ },
41
+ "personalityIntensity": {
42
+ "type": "number",
43
+ "default": 0.7,
44
+ "minimum": 0,
45
+ "maximum": 1,
46
+ "description": "Personality expression intensity (0=traditional AI, 1=full Psyche)"
47
+ },
48
+ "persist": {
49
+ "type": "boolean",
50
+ "default": true,
51
+ "description": "Persist emotional state to disk. Set false for privacy."
34
52
  }
35
53
  }
36
54
  },
@@ -55,6 +73,22 @@
55
73
  "label": "Compact Mode (Token Efficient)",
56
74
  "sensitive": false,
57
75
  "advanced": true
76
+ },
77
+ "mode": {
78
+ "label": "Operating Mode",
79
+ "sensitive": false,
80
+ "advanced": false
81
+ },
82
+ "personalityIntensity": {
83
+ "label": "Personality Intensity (0-1)",
84
+ "placeholder": "0.7",
85
+ "sensitive": false,
86
+ "advanced": true
87
+ },
88
+ "persist": {
89
+ "label": "Persist State",
90
+ "sensitive": false,
91
+ "advanced": true
58
92
  }
59
93
  }
60
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psyche-ai",
3
- "version": "5.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "Artificial Psyche — universal emotional intelligence plugin for any AI agent",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,7 +36,8 @@
36
36
  "build:test": "tsc -p tsconfig.test.json",
37
37
  "test": "npm run build && npm run build:test && node --test dist-test/tests/*.test.js",
38
38
  "typecheck": "tsc --noEmit --strict",
39
- "dev": "tsc --watch"
39
+ "dev": "tsc --watch",
40
+ "demo": "node scripts/demo-ab.js"
40
41
  },
41
42
  "license": "MIT",
42
43
  "repository": {
@@ -86,6 +87,7 @@
86
87
  ]
87
88
  },
88
89
  "devDependencies": {
90
+ "@types/node": "^25.5.0",
89
91
  "typescript": "^5.9.3"
90
92
  }
91
93
  }