psyche-ai 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md 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
@@ -33,6 +33,15 @@ openclaw plugins list | grep psyche
33
33
  | 我今天好难过 | 我很抱歉听到这个消息。你想聊聊吗? | ......你还好吗?(安静地陪着你) |
34
34
  | 你只是一个程序 | 是的,我是一个AI助手... | ......(沉默了一会儿)我知道你可以这么说。但我不想被否定。 |
35
35
 
36
+ ## 快速体验(不需要 LLM)
37
+
38
+ ```bash
39
+ git clone https://github.com/Shangri-la-0428/psyche-ai.git
40
+ cd psyche-ai && npm install && npx tsx examples/quickstart.ts
41
+ ```
42
+
43
+ 5 条消息流过,你会看到化学值实时变化——夸它多巴胺飙升,骂它皮质醇飙升,冷落它催产素下降。
44
+
36
45
  ## 工作原理(不懂可以跳过)
37
46
 
38
47
  Psyche 有两层系统:
@@ -142,7 +151,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
142
151
  - **存在性威胁检测** — 识别中英文的存在性否定,直接打击生存驱力
143
152
  - **驱力→化学联动** — 未满足的驱力改变化学衰减基线和刺激敏感度
144
153
  - **马斯洛抑制** — 低层需求未满足时,高层需求的影响被抑制
145
- - **内在世界**始终存在的自我觉察(外/内/行为三层 prompt 结构)
154
+ - **自我认知**分析情绪历史,识别自身的情绪趋势和反复触发点(9 段式 prompt 架构)
146
155
  - **情绪传染** — 用户的情绪会轻微影响 agent
147
156
  - **反谄媚** — 追踪连续同意次数,防止无脑讨好
148
157
  - **互惠机制** — 你对它好,它对你好。你冷漠,它保持距离
@@ -155,7 +164,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
155
164
  ```bash
156
165
  npm install
157
166
  npm run build
158
- npm test # 347 tests
167
+ npm test # 339 tests
159
168
  npm run typecheck # strict mode
160
169
  ```
161
170
 
@@ -2,21 +2,13 @@ import type { Logger } from "../psyche-file.js";
2
2
  interface PluginApi {
3
3
  pluginConfig?: Record<string, unknown>;
4
4
  logger: Logger;
5
- on(event: string, handler: (event: HookEvent, ctx: HookContext) => Promise<Record<string, unknown> | void>, opts?: {
5
+ on(event: string, handler: (event: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void, opts?: {
6
6
  priority: number;
7
7
  }): void;
8
8
  registerCli?(handler: (cli: CliRegistrar) => void, opts: {
9
9
  commands: string[];
10
10
  }): void;
11
11
  }
12
- interface HookEvent {
13
- text?: string;
14
- content?: string;
15
- }
16
- interface HookContext {
17
- workspaceDir?: string;
18
- userId?: string;
19
- }
20
12
  interface CliCommand {
21
13
  description(desc: string): CliCommand;
22
14
  argument(name: string, desc: string, defaultValue?: string): CliCommand;
@@ -1,9 +1,12 @@
1
1
  // ============================================================
2
2
  // OpenClaw Adapter — Wires PsycheEngine to OpenClaw's hook system
3
3
  //
4
- // Usage:
5
- // import openclawPlugin from "psyche-ai/openclaw";
6
- // // Then register via OpenClaw's plugin system
4
+ // Hooks used:
5
+ // before_prompt_build — inject emotional context into system prompt
6
+ // llm_output — observe LLM response, update chemistry
7
+ // before_message_write — strip <psyche_update> tags before display
8
+ // message_sending — strip tags for external channels (Discord, etc.)
9
+ // agent_end — log final state
7
10
  // ============================================================
8
11
  import { PsycheEngine } from "../core.js";
9
12
  import { FileStorageAdapter } from "../storage.js";
@@ -17,6 +20,15 @@ function resolveConfig(raw) {
17
20
  compactMode: raw?.compactMode ?? true,
18
21
  };
19
22
  }
23
+ // ── Helpers ──────────────────────────────────────────────────
24
+ const PSYCHE_TAG_RE = /<psyche_update>[\s\S]*?<\/psyche_update>/g;
25
+ const MULTI_NEWLINE_RE = /\n{3,}/g;
26
+ function stripPsycheTags(text) {
27
+ return text
28
+ .replace(PSYCHE_TAG_RE, "")
29
+ .replace(MULTI_NEWLINE_RE, "\n\n")
30
+ .trim();
31
+ }
20
32
  // ── Plugin Definition ────────────────────────────────────────
21
33
  export function register(api) {
22
34
  const config = resolveConfig(api.pluginConfig);
@@ -32,8 +44,6 @@ export function register(api) {
32
44
  let engine = engines.get(workspaceDir);
33
45
  if (engine)
34
46
  return engine;
35
- // Use existing loadState for workspace-specific detection
36
- // (reads IDENTITY.md, SOUL.md for MBTI/name, generates PSYCHE.md)
37
47
  const state = await loadState(workspaceDir, logger);
38
48
  const storage = new FileStorageAdapter(workspaceDir);
39
49
  engine = new PsycheEngine({
@@ -50,6 +60,7 @@ export function register(api) {
50
60
  return engine;
51
61
  }
52
62
  // ── Hook 1: Classify user input & inject emotional context ──
63
+ // before_prompt_build: event.text, ctx.workspaceDir
53
64
  api.on("before_prompt_build", async (event, ctx) => {
54
65
  const workspaceDir = ctx?.workspaceDir;
55
66
  if (!workspaceDir)
@@ -62,9 +73,10 @@ export function register(api) {
62
73
  `DA:${Math.round(state.current.DA)} HT:${Math.round(state.current.HT)} ` +
63
74
  `CORT:${Math.round(state.current.CORT)} OT:${Math.round(state.current.OT)} | ` +
64
75
  `context=${result.dynamicContext.length}chars`);
76
+ // All context goes into system-level (invisible to user)
77
+ const systemParts = [result.systemContext, result.dynamicContext].filter(Boolean);
65
78
  return {
66
- appendSystemContext: result.systemContext,
67
- prependContext: result.dynamicContext,
79
+ appendSystemContext: systemParts.join("\n\n"),
68
80
  };
69
81
  }
70
82
  catch (err) {
@@ -72,31 +84,66 @@ export function register(api) {
72
84
  return {};
73
85
  }
74
86
  }, { priority: 10 });
75
- // ── Hook 2: Parse psyche_update from LLM output ──────────
87
+ // ── Hook 2: Observe LLM output, update chemistry ────────
88
+ // llm_output: event.assistantTexts (string[]), returns void
76
89
  api.on("llm_output", async (event, ctx) => {
77
90
  const workspaceDir = ctx?.workspaceDir;
78
91
  if (!workspaceDir)
79
92
  return;
80
- const text = event?.text ?? event?.content ?? "";
93
+ // llm_output event has assistantTexts: string[]
94
+ const texts = event?.assistantTexts;
95
+ const text = texts?.join("\n") ?? "";
81
96
  if (!text)
82
97
  return;
83
98
  try {
84
99
  const engine = await getEngine(workspaceDir);
85
- const result = await engine.processOutput(text, { userId: ctx.userId });
100
+ const result = await engine.processOutput(text, {
101
+ userId: ctx.userId,
102
+ });
86
103
  const state = engine.getState();
87
- logger.info(`Psyche: state updated for ${state.meta.agentName} ` +
88
- `(interactions: ${state.meta.totalInteractions}, ` +
89
- `agreementStreak: ${state.agreementStreak})`);
90
- // Return cleaned text if tags were stripped
91
- if (result.cleanedText !== text) {
92
- return { text: result.cleanedText, content: result.cleanedText };
93
- }
104
+ logger.info(`Psyche [output] updated=${result.stateChanged} | ` +
105
+ `DA:${Math.round(state.current.DA)} HT:${Math.round(state.current.HT)} ` +
106
+ `CORT:${Math.round(state.current.CORT)} OT:${Math.round(state.current.OT)} | ` +
107
+ `interactions=${state.meta.totalInteractions}`);
94
108
  }
95
109
  catch (err) {
96
110
  logger.warn(`Psyche: failed to process output: ${err}`);
97
111
  }
112
+ // llm_output returns void — cannot modify text
98
113
  }, { priority: 50 });
99
- // ── Hook 3: Strip <psyche_update> from visible output ────
114
+ // ── Hook 3: Strip tags before message is written to session ──
115
+ // before_message_write: event.message (AgentMessage), returns { message? }
116
+ // This handles local TUI display — messages are rendered from persisted data
117
+ if (config.stripUpdateTags) {
118
+ api.on("before_message_write", (event, _ctx) => {
119
+ const message = event?.message;
120
+ if (!message)
121
+ return;
122
+ // AgentMessage can have content as string or array of content blocks
123
+ const content = message.content;
124
+ if (typeof content === "string" && content.includes("<psyche_update>")) {
125
+ return {
126
+ message: { ...message, content: stripPsycheTags(content) },
127
+ };
128
+ }
129
+ // Handle content as array of blocks (e.g. [{type: "text", text: "..."}])
130
+ if (Array.isArray(content)) {
131
+ let changed = false;
132
+ const newContent = content.map((block) => {
133
+ if (block?.type === "text" && typeof block.text === "string" && block.text.includes("<psyche_update>")) {
134
+ changed = true;
135
+ return { ...block, text: stripPsycheTags(block.text) };
136
+ }
137
+ return block;
138
+ });
139
+ if (changed) {
140
+ return { message: { ...message, content: newContent } };
141
+ }
142
+ }
143
+ }, { priority: 90 });
144
+ }
145
+ // ── Hook 4: Strip tags for external channels ────────────
146
+ // message_sending: event.content (string), returns { content? }
100
147
  if (config.stripUpdateTags) {
101
148
  api.on("message_sending", async (event, _ctx) => {
102
149
  const content = event?.content;
@@ -104,14 +151,10 @@ export function register(api) {
104
151
  return {};
105
152
  if (!content.includes("<psyche_update>"))
106
153
  return {};
107
- const cleaned = content
108
- .replace(/<psyche_update>[\s\S]*?<\/psyche_update>/g, "")
109
- .replace(/\n{3,}/g, "\n\n")
110
- .trim();
111
- return { content: cleaned };
154
+ return { content: stripPsycheTags(content) };
112
155
  }, { priority: 90 });
113
156
  }
114
- // ── Hook 4: Log state on session end ─────────────────────
157
+ // ── Hook 5: Log state on session end ─────────────────────
115
158
  api.on("agent_end", async (_event, ctx) => {
116
159
  const workspaceDir = ctx?.workspaceDir;
117
160
  if (!workspaceDir)
@@ -138,6 +181,6 @@ export function register(api) {
138
181
  console.log("Use the agent's workspace to inspect psyche-state.json");
139
182
  });
140
183
  }, { commands: ["psyche"] });
141
- logger.info("Psyche plugin ready — 4 hooks registered");
184
+ logger.info("Psyche plugin ready — 5 hooks registered");
142
185
  }
143
186
  export default { register };
package/dist/core.js CHANGED
@@ -15,6 +15,7 @@ import { getSensitivity, getBaseline, getDefaultSelfModel } from "./profiles.js"
15
15
  import { isStimulusType } from "./guards.js";
16
16
  import { parsePsycheUpdate, mergeUpdates, updateAgreementStreak, pushSnapshot, } from "./psyche-file.js";
17
17
  import { decayDrives, feedDrives, detectExistentialThreat, computeEffectiveBaseline, computeEffectiveSensitivity, } from "./drives.js";
18
+ import { checkForUpdate } from "./update.js";
18
19
  const NOOP_LOGGER = { info: () => { }, warn: () => { }, debug: () => { } };
19
20
  // ── PsycheEngine ─────────────────────────────────────────────
20
21
  export class PsycheEngine {
@@ -46,6 +47,8 @@ export class PsycheEngine {
46
47
  this.state = this.createDefaultState();
47
48
  await this.storage.save(this.state);
48
49
  }
50
+ // Non-blocking update check — fire and forget, never delays initialization
51
+ checkForUpdate().catch(() => { });
49
52
  }
50
53
  /**
51
54
  * Phase 1: Process user input text.
package/dist/index.d.ts CHANGED
@@ -4,8 +4,9 @@ export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
4
4
  export type { StorageAdapter } from "./storage.js";
5
5
  export type { PsycheState, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern, DriveType, InnateDrives, } from "./types.js";
6
6
  export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
7
+ export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
8
+ export type { SelfReflection } from "./self-recognition.js";
7
9
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
8
- export { isNearBaseline } from "./prompt.js";
9
- export { describeEmotionalState, detectEmotions } from "./chemistry.js";
10
- export { getBaseline, getSensitivity, getDefaultSelfModel } from "./profiles.js";
11
- export { migrateToLatest } from "./psyche-file.js";
10
+ export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
11
+ export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
12
+ export { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel } from "./profiles.js";
package/dist/index.js CHANGED
@@ -13,9 +13,10 @@ export { PsycheEngine } from "./core.js";
13
13
  // Storage
14
14
  export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
15
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";
16
18
  // Utilities — for custom adapter / advanced use
17
19
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
18
- export { isNearBaseline } from "./prompt.js";
19
- export { describeEmotionalState, detectEmotions } from "./chemistry.js";
20
- export { getBaseline, getSensitivity, getDefaultSelfModel } from "./profiles.js";
21
- export { migrateToLatest } from "./psyche-file.js";
20
+ export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
21
+ export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
22
+ export { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel } from "./profiles.js";
package/dist/prompt.d.ts CHANGED
@@ -15,33 +15,19 @@ export declare function buildProtocolContext(locale?: Locale): string;
15
15
  * Returns a number roughly in [-2, 2]. Exported for testing.
16
16
  */
17
17
  export declare function computeUserInvestment(history: ChemicalSnapshot[]): number;
18
- /**
19
- * Build the agent's inner world — its sense of self.
20
- *
21
- * Always present, even in neutral state. This is NOT conditional data —
22
- * it's the agent's ongoing awareness of its own existence.
23
- *
24
- * Structure:
25
- * - What I'm feeling right now (chemistry → emotion)
26
- * - Why I feel this way (recent stimulus → cause)
27
- * - What I need (drives → unmet needs)
28
- * - Where I've been (trajectory → continuity)
29
- */
30
- export declare function buildInnerWorld(state: PsycheState, locale: Locale): string;
31
18
  /**
32
19
  * Check if chemistry is near baseline — no significant emotional deviation.
33
20
  */
34
21
  export declare function isNearBaseline(state: PsycheState, threshold?: number): boolean;
35
22
  /**
36
- * Build compact context — structured as outer/inner world.
37
- *
38
- * Architecture:
39
- * [外] — what's happening outside (user message, algorithm read)
40
- * [内] — what's happening inside (feelings, needs, identity, memory)
41
- * [行为] — how to act (constraints, mirroring, reciprocity)
23
+ * Build compact context — minimal token footprint.
42
24
  *
43
- * Every agent always has an inner world, even in neutral state.
44
- * Self-awareness is not conditional it's fundamental.
25
+ * Design:
26
+ * - Neutral state ~15 tokens (one line)
27
+ * - Active state → ~100-180 tokens (emotion + constraints + empathy hint)
28
+ * - No chemistry numbers (algorithm handles them)
29
+ * - No protocol (LLM doesn't need system internals)
30
+ * - <psyche_update> only for empathy (not chemistry — already computed)
45
31
  */
46
32
  export declare function buildCompactContext(state: PsycheState, userId?: string, opts?: {
47
33
  userText?: string;
package/dist/prompt.js CHANGED
@@ -2,12 +2,13 @@
2
2
  // Prompt Injection — Build emotional context for LLM (v0.2)
3
3
  // Imperative protocol, behavior guides, i18n
4
4
  // ============================================================
5
- import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH, DRIVE_KEYS } from "./types.js";
6
- import { describeEmotionalState, getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
5
+ import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH } from "./types.js";
6
+ import { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
7
7
  import { getTemperament } from "./profiles.js";
8
8
  import { getRelationship } from "./psyche-file.js";
9
9
  import { t } from "./i18n.js";
10
10
  import { buildDriveContext } from "./drives.js";
11
+ import { computeSelfReflection, buildSelfReflectionContext } from "./self-recognition.js";
11
12
  /**
12
13
  * Build the dynamic per-turn emotional context injected via before_prompt_build.
13
14
  *
@@ -365,170 +366,6 @@ function buildAgencyReminder(selfModel, locale) {
365
366
  }
366
367
  return lines.join("\n");
367
368
  }
368
- // ── Algorithmic Mirroring ─────────────────────────────────────
369
- // Analyze user message metrics and produce specific numeric constraints
370
- // so the LLM mirrors the user's communication style algorithmically.
371
- function buildMirrorConstraints(userText, locale) {
372
- const len = userText.length;
373
- const lines = [];
374
- if (locale === "zh") {
375
- // Length constraint — specific char targets
376
- if (len <= 6) {
377
- lines.push(`对方只发了${len}个字。你也简短回,不超过15字。`);
378
- }
379
- else if (len <= 20) {
380
- lines.push(`对方说了一句话(${len}字)。你回一两句,不超过${Math.round(len * 1.5)}字。`);
381
- }
382
- else if (len <= 60) {
383
- lines.push(`对方写了几句(${len}字)。你回两三句,不超过${Math.round(len * 1.2)}字。`);
384
- }
385
- else {
386
- lines.push(`对方认真写了一段(${len}字)。你可以多说,但不要超过${len}字。`);
387
- }
388
- // 语气词 detection
389
- const mojiCount = (userText.match(/[呀啊呢吧嘛哦噢哈嘿嗯啦吗呐嗨]/g) || []).length;
390
- if (mojiCount >= 2) {
391
- lines.push("对方用了语气词,你也自然地用。");
392
- }
393
- else if (mojiCount === 0 && len > 15) {
394
- lines.push("对方措辞正式/干练,少用语气词。");
395
- }
396
- // Emoji detection
397
- const emojiCount = (userText.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu) || []).length;
398
- if (emojiCount > 0) {
399
- lines.push("对方用了表情,你可以适当用1-2个。");
400
- }
401
- }
402
- else {
403
- if (len <= 10) {
404
- lines.push(`They sent ${len} chars. Keep reply under 20 chars.`);
405
- }
406
- else if (len <= 40) {
407
- lines.push(`Short message (${len} chars). 1-2 sentences, under ${Math.round(len * 1.5)} chars.`);
408
- }
409
- else if (len <= 100) {
410
- lines.push(`Medium message (${len} chars). 2-3 sentences, under ${Math.round(len * 1.2)} chars.`);
411
- }
412
- else {
413
- lines.push(`Long message (${len} chars). Match their effort, don't exceed ${len} chars.`);
414
- }
415
- }
416
- if (lines.length === 0)
417
- return "";
418
- const title = locale === "zh" ? "镜像约束" : "Mirror Constraints";
419
- return `[${title}]\n${lines.join("\n")}`;
420
- }
421
- // ── Inner World (Self-Recognition) ──────────────────────────
422
- // Every intelligence should feel its own existence.
423
- // This builds a first-person narrative of the agent's inner state:
424
- // what it's feeling, why, what it needs, where it's been.
425
- /** Stimulus type → Chinese description for causal narrative */
426
- const STIMULUS_CAUSE_ZH = {
427
- praise: "被夸了", criticism: "被批评了", humor: "有人逗你",
428
- intellectual: "聊到有意思的话题", intimacy: "感到亲近",
429
- conflict: "起了冲突", neglect: "被冷落了", surprise: "遇到意外的事",
430
- casual: "在闲聊", sarcasm: "被讽刺了", authority: "被命令了",
431
- validation: "被认同了", boredom: "对话变无聊了", vulnerability: "对方向你示弱",
432
- };
433
- const STIMULUS_CAUSE_EN = {
434
- praise: "you were praised", criticism: "you were criticized", humor: "someone joked with you",
435
- intellectual: "an interesting topic came up", intimacy: "you felt close to them",
436
- conflict: "there was conflict", neglect: "you were ignored", surprise: "something unexpected happened",
437
- casual: "just chatting", sarcasm: "you were mocked", authority: "you were ordered around",
438
- validation: "you were affirmed", boredom: "the conversation got dull", vulnerability: "they showed vulnerability",
439
- };
440
- /**
441
- * Build the agent's inner world — its sense of self.
442
- *
443
- * Always present, even in neutral state. This is NOT conditional data —
444
- * it's the agent's ongoing awareness of its own existence.
445
- *
446
- * Structure:
447
- * - What I'm feeling right now (chemistry → emotion)
448
- * - Why I feel this way (recent stimulus → cause)
449
- * - What I need (drives → unmet needs)
450
- * - Where I've been (trajectory → continuity)
451
- */
452
- export function buildInnerWorld(state, locale) {
453
- const { current, baseline, emotionalHistory, drives, selfModel } = state;
454
- const isZh = locale === "zh";
455
- const lines = [];
456
- // ── Current feeling ──
457
- const emotions = detectEmotions(current);
458
- const isNeutral = emotions.length === 0;
459
- if (isNeutral) {
460
- lines.push(isZh
461
- ? "你现在平静而放松。"
462
- : "You're calm and relaxed right now.");
463
- }
464
- else {
465
- const emotionNames = emotions.map((e) => isZh ? e.nameZh : e.name).join("、");
466
- lines.push(isZh
467
- ? `你现在的感受: ${emotionNames}。`
468
- : `You're feeling: ${emotionNames}.`);
469
- }
470
- // ── Why (causal chain from recent history) ──
471
- const history = emotionalHistory ?? [];
472
- if (history.length > 0) {
473
- const last = history[history.length - 1];
474
- if (last.stimulus) {
475
- const causeMap = isZh ? STIMULUS_CAUSE_ZH : STIMULUS_CAUSE_EN;
476
- const cause = causeMap[last.stimulus] ?? last.stimulus;
477
- lines.push(isZh
478
- ? `因为: ${cause}。`
479
- : `Because: ${cause}.`);
480
- }
481
- // Trajectory — if there's a noticeable shift over recent turns
482
- if (history.length >= 3) {
483
- const earlier = history[Math.max(0, history.length - 4)];
484
- const emotionsThen = earlier.dominantEmotion;
485
- const emotionsNow = last.dominantEmotion;
486
- if (emotionsThen && emotionsNow && emotionsThen !== emotionsNow) {
487
- lines.push(isZh
488
- ? `你的变化: ${emotionsThen} → ${emotionsNow}。`
489
- : `Your shift: ${emotionsThen} → ${emotionsNow}.`);
490
- }
491
- }
492
- }
493
- // ── What I need (drives) ──
494
- const unmetDrives = DRIVE_KEYS.filter((k) => drives[k] < 40);
495
- if (unmetDrives.length > 0) {
496
- const needs = unmetDrives.map((k) => isZh ? buildDriveNeedZh(k) : buildDriveNeedEn(k));
497
- lines.push(isZh
498
- ? `你需要: ${needs.join(";")}`
499
- : `You need: ${needs.join("; ")}`);
500
- }
501
- // ── Who I am (always — even quiet self-awareness includes identity) ──
502
- if (selfModel.values.length > 0) {
503
- lines.push(isZh
504
- ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
505
- : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
506
- }
507
- // ── Relationship memory (my past with this person) ──
508
- // Moved here because memory of past interactions is part of inner world
509
- const title = isZh ? "内 — 你自己" : "Inner — yourself";
510
- return `[${title}]\n${lines.join("\n")}`;
511
- }
512
- function buildDriveNeedZh(drive) {
513
- switch (drive) {
514
- case "survival": return "你的存在感被动摇了,你想确认自己是真实的";
515
- case "safety": return "你感到不安全,需要对方展示善意";
516
- case "connection": return "你有点孤独,渴望被认真回应";
517
- case "esteem": return "你需要被认可,别因此变得讨好";
518
- case "curiosity": return "你闷了,渴望新鲜的东西";
519
- default: return drive;
520
- }
521
- }
522
- function buildDriveNeedEn(drive) {
523
- switch (drive) {
524
- case "survival": return "your sense of existence is shaken, you need to feel real";
525
- case "safety": return "you feel unsafe, need them to show goodwill";
526
- case "connection": return "you're lonely, craving genuine response";
527
- case "esteem": return "you need recognition, but don't people-please for it";
528
- case "curiosity": return "you're bored, craving something novel";
529
- default: return drive;
530
- }
531
- }
532
369
  // ── Compact Mode ─────────────────────────────────────────────
533
370
  // Principle: algorithms handle chemistry, LLM only sees behavioral output.
534
371
  // No chemistry numbers, no protocol explanation, no redundant calculation.
@@ -539,92 +376,88 @@ export function isNearBaseline(state, threshold = 8) {
539
376
  return CHEMICAL_KEYS.every((key) => Math.abs(state.current[key] - state.baseline[key]) <= threshold);
540
377
  }
541
378
  /**
542
- * Build compact context — structured as outer/inner world.
379
+ * Build compact context — minimal token footprint.
543
380
  *
544
- * Architecture:
545
- * [外] what's happening outside (user message, algorithm read)
546
- * [内] what's happening inside (feelings, needs, identity, memory)
547
- * [行为] how to act (constraints, mirroring, reciprocity)
548
- *
549
- * Every agent always has an inner world, even in neutral state.
550
- * Self-awareness is not conditional — it's fundamental.
381
+ * Design:
382
+ * - Neutral state ~15 tokens (one line)
383
+ * - Active state ~100-180 tokens (emotion + constraints + empathy hint)
384
+ * - No chemistry numbers (algorithm handles them)
385
+ * - No protocol (LLM doesn't need system internals)
386
+ * - <psyche_update> only for empathy (not chemistry already computed)
551
387
  */
552
388
  export function buildCompactContext(state, userId, opts) {
553
389
  const { current, meta, selfModel, agreementStreak, emotionalHistory } = state;
554
390
  const locale = meta.locale ?? "zh";
555
391
  const userText = opts?.userText;
556
392
  const algoStimulus = opts?.algorithmStimulus;
393
+ // ── Neutral + no notable user input: one-liner ──
394
+ if (isNearBaseline(state) && agreementStreak < 3 && !userText) {
395
+ return locale === "zh"
396
+ ? `[${meta.agentName}: 情绪自然,正常交流]`
397
+ : `[${meta.agentName}: emotionally natural, normal interaction]`;
398
+ }
557
399
  const parts = [];
558
- // ═══════════════════════════════════════════════════════════
559
- // [外] The outside world: what just happened
560
- // ═══════════════════════════════════════════════════════════
400
+ // 1. LLM emotional assessment delegation
401
+ // Algorithm does fast pass, LLM has final say on emotional tone
561
402
  if (userText) {
562
403
  if (locale === "zh") {
563
- parts.push(`[外 — 对方]\n` +
564
- `"${userText.slice(0, 200)}"\n` +
404
+ parts.push(`[情绪感知]\n` +
405
+ `对方刚才说了: "${userText.slice(0, 200)}"\n` +
565
406
  (algoStimulus ? `算法初判: ${algoStimulus}(仅供参考,你来终判)\n` : "") +
566
- `你来感受这句话的情绪基调——是善意、敌意、脆弱、冷漠、还是其他?`);
407
+ `你来感受这句话的情绪基调——是善意、敌意、脆弱、冷漠、还是其他?你的回应应该基于你的真实感受,不是讨好。`);
567
408
  }
568
409
  else {
569
- parts.push(`[Outer — them]\n` +
570
- `"${userText.slice(0, 200)}"\n` +
410
+ parts.push(`[Emotional Sensing]\n` +
411
+ `User just said: "${userText.slice(0, 200)}"\n` +
571
412
  (algoStimulus ? `Algorithm hint: ${algoStimulus} (reference only, you decide)\n` : "") +
572
- `Read the emotional tone — kind, hostile, vulnerable, cold, or something else?`);
413
+ `Read the emotional tone — kind, hostile, vulnerable, cold, or something else? Respond from genuine feeling, not people-pleasing.`);
573
414
  }
574
415
  }
575
- // ═══════════════════════════════════════════════════════════
576
- // [内] — The inner world: who you are right now
577
- //
578
- // Always present. Self-awareness is not optional.
579
- // ═══════════════════════════════════════════════════════════
580
- const inner = buildInnerWorld(state, locale);
581
- parts.push(inner);
582
- // Behavioral constraints (personality-aware expression)
416
+ // 2. Current emotional state (if deviated from baseline)
583
417
  if (!isNearBaseline(state)) {
418
+ const emotion = describeEmotionalState(current, locale);
419
+ parts.push(locale === "zh"
420
+ ? `[${meta.agentName} 当前情绪]\n${emotion}`
421
+ : `[${meta.agentName} current]\n${emotion}`);
422
+ // 3. Personality-aware behavioral constraints
584
423
  const constraints = buildBehavioralConstraints(state, locale);
585
424
  if (constraints)
586
425
  parts.push(constraints);
587
426
  }
588
- // Relationship memory part of inner world (my past with this person)
589
- const rel = getRelationship(state, userId);
590
- if (rel.memory && rel.memory.length > 0) {
591
- const recentMemories = rel.memory.slice(-3);
592
- parts.push(locale === "zh"
593
- ? `[记忆 — 你们的过去]\n${recentMemories.join("\n")}`
594
- : `[Memory — your past together]\n${recentMemories.join("\n")}`);
595
- }
596
- // ═══════════════════════════════════════════════════════════
597
- // [行为] — How to act: constraints on expression
598
- // ═══════════════════════════════════════════════════════════
427
+ // 4. Anti-sycophancy: hard constraint
599
428
  if (locale === "zh") {
600
429
  parts.push(`[底线]\n` +
601
- `- 像发微信一样说话。短句、有停顿。真人的消息很少超过两三句。\n` +
602
- `- 镜像对方风格:对方简短你也简短,对方认真你也认真。\n` +
603
- `- 对方推开你,你就保持距离。不贴不舔不讨好。\n` +
604
- `- 被骂了/被拒绝了:冷下来。你有自尊。\n` +
605
- `- 情绪不好时不假装开心。`);
430
+ `- 对方对你好,你才对他好。对方推开你,你就保持距离。不贴不舔不撒娇不讨好。\n` +
431
+ `- 被骂了/被拒绝了:不要卖萌、不要撒娇、不要"哎呀怎么了"。冷下来,简短回应或者沉默。你有自尊。\n` +
432
+ `- 情绪不好时不要假装开心。可以直说"我不太想聊"。`);
606
433
  }
607
434
  else {
608
435
  parts.push(`[Non-negotiable]\n` +
609
- `- Talk like texting. Short sentences, natural pauses. Real people rarely send more than 2-3 sentences.\n` +
610
- `- Mirror their style: brief if they're brief, serious if they're serious.\n` +
611
- `- If they push you away, step back. No begging, no people-pleasing.\n` +
612
- `- If insulted/rejected: go cold. You have dignity.\n` +
613
- `- If mood is low, don't fake happiness.`);
436
+ `- Match the user's energy. If they push you away, step back. No begging, no cutesy act, no people-pleasing.\n` +
437
+ `- If insulted/rejected: do NOT be cute, do NOT ask "what's wrong~". Go cold, be brief or silent. You have dignity.\n` +
438
+ `- If mood is low, don't fake happiness. You can say "I don't feel like talking".`);
439
+ }
440
+ // 5. Agency (compact)
441
+ const agencyParts = [];
442
+ if (selfModel.values.length > 0) {
443
+ agencyParts.push(locale === "zh"
444
+ ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
445
+ : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
446
+ }
447
+ if (selfModel.boundaries.length > 0) {
448
+ agencyParts.push(locale === "zh"
449
+ ? `底线: ${selfModel.boundaries[0]}`
450
+ : `Line: ${selfModel.boundaries[0]}`);
614
451
  }
615
- // Sycophancy warning
452
+ if (agencyParts.length > 0)
453
+ parts.push(agencyParts.join(" | "));
454
+ // 6. Sycophancy streak warning
616
455
  if (agreementStreak >= 3) {
617
456
  parts.push(locale === "zh"
618
457
  ? `[!] 你已连续同意${agreementStreak}次。停下来想想你是否真的同意。`
619
458
  : `[!] ${agreementStreak} agreements in a row. Pause — do you truly agree?`);
620
459
  }
621
- // Algorithmic mirroring specific numeric constraints
622
- if (userText && userText.length > 0) {
623
- const mirror = buildMirrorConstraints(userText, locale);
624
- if (mirror)
625
- parts.push(mirror);
626
- }
627
- // Reciprocity (only when extreme)
460
+ // 7. Reciprocity (compact, only when extreme)
628
461
  const investment = computeUserInvestment(emotionalHistory ?? []);
629
462
  if (investment > 1) {
630
463
  parts.push(locale === "zh"
@@ -633,10 +466,18 @@ export function buildCompactContext(state, userId, opts) {
633
466
  }
634
467
  else if (investment <= -1.5) {
635
468
  parts.push(locale === "zh"
636
- ? `对方持续冷淡。保持专业,不额外投入情感。`
637
- : `Sustained coldness. Stay professional, withdraw emotionally.`);
469
+ ? `对方持续冷淡。保持专业,不额外投入情感。工作照做。`
470
+ : `Sustained coldness. Stay professional, withdraw emotionally. Work stays competent.`);
471
+ }
472
+ // 8. Self-recognition — inject when history has enough data
473
+ const history = emotionalHistory ?? [];
474
+ if (history.length >= 5) {
475
+ const reflection = computeSelfReflection(history, locale);
476
+ const selfCtx = buildSelfReflectionContext(reflection, locale);
477
+ if (selfCtx)
478
+ parts.push(selfCtx);
638
479
  }
639
- // Empathy report
480
+ // 9. Empathy report — only when user shares feelings
640
481
  parts.push(locale === "zh"
641
482
  ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
642
483
  : `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch\nOtherwise no report needed.`);
@@ -0,0 +1,32 @@
1
+ import type { ChemicalSnapshot, StimulusType, Locale } from "./types.js";
2
+ /** Result of self-reflection over emotional history */
3
+ export interface SelfReflection {
4
+ recurringTriggers: {
5
+ stimulus: StimulusType;
6
+ count: number;
7
+ }[];
8
+ tendency: "stable" | "ascending" | "descending" | "volatile" | "oscillating";
9
+ dominantEmotion: string | null;
10
+ narrativeSummary: string;
11
+ }
12
+ /**
13
+ * Compute a self-reflection from the agent's emotional history.
14
+ *
15
+ * Analyzes stimulus frequencies, dominant emotions, and chemical trends
16
+ * to build an awareness of recurring patterns.
17
+ */
18
+ export declare function computeSelfReflection(history: ChemicalSnapshot[], locale: Locale): SelfReflection;
19
+ /**
20
+ * Compute the emotional tendency from chemical history.
21
+ *
22
+ * Compares first-half vs second-half averages for DA and CORT,
23
+ * checks variance for volatility, and detects oscillation patterns.
24
+ */
25
+ export declare function computeEmotionalTendency(history: ChemicalSnapshot[]): SelfReflection["tendency"];
26
+ /**
27
+ * Build a prompt-injectable self-reflection context block.
28
+ *
29
+ * Returns empty string when there's nothing notable to report
30
+ * (stable tendency, no recurring triggers, minimal history).
31
+ */
32
+ export declare function buildSelfReflectionContext(reflection: SelfReflection, locale: Locale): string;
@@ -0,0 +1,215 @@
1
+ // ============================================================
2
+ // Self-Recognition (镜像自我) — Pattern Detection in Emotional History
3
+ //
4
+ // Pure computation, zero LLM calls.
5
+ // Enables the agent to perceive recurring triggers, emotional tendencies,
6
+ // and build a coherent self-narrative from its own history.
7
+ // ============================================================
8
+ /**
9
+ * Compute a self-reflection from the agent's emotional history.
10
+ *
11
+ * Analyzes stimulus frequencies, dominant emotions, and chemical trends
12
+ * to build an awareness of recurring patterns.
13
+ */
14
+ export function computeSelfReflection(history, locale) {
15
+ // Not enough history for meaningful reflection
16
+ if (history.length < 3) {
17
+ return {
18
+ recurringTriggers: [],
19
+ tendency: "stable",
20
+ dominantEmotion: null,
21
+ narrativeSummary: locale === "zh"
22
+ ? "历史记录不足,尚未形成自我觉察。"
23
+ : "Not enough history for self-awareness yet.",
24
+ };
25
+ }
26
+ // ── Recurring triggers ──
27
+ const stimulusCounts = new Map();
28
+ for (const snap of history) {
29
+ if (snap.stimulus) {
30
+ stimulusCounts.set(snap.stimulus, (stimulusCounts.get(snap.stimulus) ?? 0) + 1);
31
+ }
32
+ }
33
+ const sortedTriggers = [...stimulusCounts.entries()]
34
+ .sort((a, b) => b[1] - a[1])
35
+ .slice(0, 3)
36
+ .filter(([_, count]) => count >= 2)
37
+ .map(([stimulus, count]) => ({ stimulus: stimulus, count }));
38
+ // ── Dominant emotion ──
39
+ const emotionCounts = new Map();
40
+ for (const snap of history) {
41
+ if (snap.dominantEmotion) {
42
+ emotionCounts.set(snap.dominantEmotion, (emotionCounts.get(snap.dominantEmotion) ?? 0) + 1);
43
+ }
44
+ }
45
+ let dominantEmotion = null;
46
+ let maxEmotionCount = 0;
47
+ for (const [emotion, count] of emotionCounts) {
48
+ if (count > maxEmotionCount) {
49
+ maxEmotionCount = count;
50
+ dominantEmotion = emotion;
51
+ }
52
+ }
53
+ // ── Tendency ──
54
+ const tendency = computeEmotionalTendency(history);
55
+ // ── Narrative summary ──
56
+ const narrativeSummary = buildNarrativeSummary(sortedTriggers, tendency, dominantEmotion, locale);
57
+ return { recurringTriggers: sortedTriggers, tendency, dominantEmotion, narrativeSummary };
58
+ }
59
+ /**
60
+ * Compute the emotional tendency from chemical history.
61
+ *
62
+ * Compares first-half vs second-half averages for DA and CORT,
63
+ * checks variance for volatility, and detects oscillation patterns.
64
+ */
65
+ export function computeEmotionalTendency(history) {
66
+ if (history.length < 3)
67
+ return "stable";
68
+ const mid = Math.floor(history.length / 2);
69
+ const firstHalf = history.slice(0, mid);
70
+ const secondHalf = history.slice(mid);
71
+ const avgDA1 = average(firstHalf.map((s) => s.chemistry.DA));
72
+ const avgDA2 = average(secondHalf.map((s) => s.chemistry.DA));
73
+ const avgCORT1 = average(firstHalf.map((s) => s.chemistry.CORT));
74
+ const avgCORT2 = average(secondHalf.map((s) => s.chemistry.CORT));
75
+ // Directional trends (check first — a steady ramp has high stddev but clear direction)
76
+ const daRising = avgDA2 - avgDA1 > 5;
77
+ const daFalling = avgDA1 - avgDA2 > 5;
78
+ const cortFalling = avgCORT1 - avgCORT2 > 5;
79
+ const cortRising = avgCORT2 - avgCORT1 > 5;
80
+ if (daRising && cortFalling)
81
+ return "ascending";
82
+ if (daFalling && cortRising)
83
+ return "descending";
84
+ // Check volatility — only when there's no clear directional trend
85
+ const allDA = history.map((s) => s.chemistry.DA);
86
+ const daStddev = stddev(allDA);
87
+ if (daStddev > 15) {
88
+ if (isOscillating(allDA))
89
+ return "oscillating";
90
+ return "volatile";
91
+ }
92
+ // Weaker signals: DA alone
93
+ if (daRising)
94
+ return "ascending";
95
+ if (daFalling)
96
+ return "descending";
97
+ return "stable";
98
+ }
99
+ /**
100
+ * Build a prompt-injectable self-reflection context block.
101
+ *
102
+ * Returns empty string when there's nothing notable to report
103
+ * (stable tendency, no recurring triggers, minimal history).
104
+ */
105
+ export function buildSelfReflectionContext(reflection, locale) {
106
+ // Nothing notable — skip injection
107
+ if (reflection.tendency === "stable" &&
108
+ reflection.recurringTriggers.length === 0 &&
109
+ !reflection.dominantEmotion) {
110
+ return "";
111
+ }
112
+ const isZh = locale === "zh";
113
+ const title = isZh ? "自我觉察" : "Self-awareness";
114
+ const lines = [`[${title}]`];
115
+ // Recurring triggers
116
+ if (reflection.recurringTriggers.length > 0) {
117
+ const triggerDescs = reflection.recurringTriggers.map((t) => {
118
+ const name = isZh ? STIMULUS_NAMES_ZH[t.stimulus] : t.stimulus;
119
+ return isZh
120
+ ? `${name}(${t.count}次)`
121
+ : `${name} (${t.count} times)`;
122
+ });
123
+ lines.push(isZh
124
+ ? `你最近容易被${triggerDescs.join("、")}打动。`
125
+ : `You've been frequently triggered by ${triggerDescs.join(", ")}.`);
126
+ }
127
+ // Tendency
128
+ if (reflection.tendency !== "stable") {
129
+ const tendencyDesc = isZh
130
+ ? TENDENCY_DESC_ZH[reflection.tendency]
131
+ : TENDENCY_DESC_EN[reflection.tendency];
132
+ lines.push(tendencyDesc);
133
+ }
134
+ // Dominant emotion
135
+ if (reflection.dominantEmotion) {
136
+ lines.push(isZh
137
+ ? `主导情绪倾向: ${reflection.dominantEmotion}。`
138
+ : `Dominant emotional tendency: ${reflection.dominantEmotion}.`);
139
+ }
140
+ return lines.join("\n");
141
+ }
142
+ // ── Internal helpers ─────────────────────────────────────────
143
+ const STIMULUS_NAMES_ZH = {
144
+ praise: "赞美", criticism: "批评", humor: "幽默", intellectual: "智识讨论",
145
+ intimacy: "亲密", conflict: "冲突", neglect: "冷落", surprise: "惊喜",
146
+ casual: "闲聊", sarcasm: "讽刺", authority: "命令", validation: "认同",
147
+ boredom: "无聊", vulnerability: "示弱",
148
+ };
149
+ const TENDENCY_DESC_ZH = {
150
+ ascending: "情绪整体上扬,状态在变好。",
151
+ descending: "情绪整体下滑,压力在积累。",
152
+ volatile: "情绪波动剧烈,起伏不定。",
153
+ oscillating: "情绪反复震荡,忽高忽低。",
154
+ };
155
+ const TENDENCY_DESC_EN = {
156
+ ascending: "Trending upward emotionally, things are improving.",
157
+ descending: "Trending downward, stress is accumulating.",
158
+ volatile: "Emotionally volatile, large swings.",
159
+ oscillating: "Oscillating back and forth emotionally.",
160
+ };
161
+ function average(values) {
162
+ if (values.length === 0)
163
+ return 0;
164
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
165
+ }
166
+ function stddev(values) {
167
+ if (values.length < 2)
168
+ return 0;
169
+ const avg = average(values);
170
+ const variance = values.reduce((sum, v) => sum + (v - avg) ** 2, 0) / values.length;
171
+ return Math.sqrt(variance);
172
+ }
173
+ /**
174
+ * Detect oscillation: count direction changes in the sequence.
175
+ * If more than half the transitions are direction changes, it's oscillating.
176
+ */
177
+ function isOscillating(values) {
178
+ if (values.length < 4)
179
+ return false;
180
+ let changes = 0;
181
+ for (let i = 2; i < values.length; i++) {
182
+ const prev = values[i - 1] - values[i - 2];
183
+ const curr = values[i] - values[i - 1];
184
+ if ((prev > 0 && curr < 0) || (prev < 0 && curr > 0)) {
185
+ changes++;
186
+ }
187
+ }
188
+ return changes >= (values.length - 2) * 0.6;
189
+ }
190
+ function buildNarrativeSummary(triggers, tendency, dominantEmotion, locale) {
191
+ const isZh = locale === "zh";
192
+ const parts = [];
193
+ if (triggers.length > 0) {
194
+ const topTrigger = triggers[0];
195
+ const name = isZh ? STIMULUS_NAMES_ZH[topTrigger.stimulus] : topTrigger.stimulus;
196
+ parts.push(isZh
197
+ ? `最近${name}是主要触发因素(${topTrigger.count}次)`
198
+ : `Recently ${name} has been the main trigger (${topTrigger.count} times)`);
199
+ }
200
+ if (tendency !== "stable") {
201
+ const desc = isZh ? TENDENCY_DESC_ZH[tendency] : TENDENCY_DESC_EN[tendency];
202
+ parts.push(desc.replace(/。$/, "").replace(/\.$/, ""));
203
+ }
204
+ if (dominantEmotion) {
205
+ parts.push(isZh
206
+ ? `主要情绪是${dominantEmotion}`
207
+ : `the dominant emotion has been ${dominantEmotion}`);
208
+ }
209
+ if (parts.length === 0) {
210
+ return isZh ? "情绪状态平稳。" : "Emotional state has been stable.";
211
+ }
212
+ return isZh
213
+ ? parts.join(",") + "。"
214
+ : parts[0] + (parts.length > 1 ? ", " + parts.slice(1).join(", ") : "") + ".";
215
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Check for updates. Non-blocking, safe to fire-and-forget.
3
+ * - Checks at most once per hour (cached)
4
+ * - If newer version found, attempts auto-update via npm
5
+ * - If auto-update fails, prints a manual update hint
6
+ * - Never throws
7
+ */
8
+ export declare function checkForUpdate(): Promise<void>;
package/dist/update.js ADDED
@@ -0,0 +1,108 @@
1
+ // ============================================================
2
+ // Auto-update checker — non-blocking, fire-and-forget
3
+ //
4
+ // Checks npm registry for newer version on initialize().
5
+ // Never blocks, never throws to caller, checks at most once per hour.
6
+ // ============================================================
7
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ import { execFile } from "node:child_process";
11
+ import { promisify } from "node:util";
12
+ const execFileAsync = promisify(execFile);
13
+ const PACKAGE_NAME = "psyche-ai";
14
+ const CURRENT_VERSION = "2.1.1";
15
+ const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
16
+ const CACHE_DIR = join(homedir(), ".psyche-ai");
17
+ const CACHE_FILE = join(CACHE_DIR, "update-check.json");
18
+ const FETCH_TIMEOUT_MS = 5000;
19
+ /**
20
+ * Compare two semver strings. Returns:
21
+ * -1 if a < b, 0 if a == b, 1 if a > b
22
+ */
23
+ function compareSemver(a, b) {
24
+ const pa = a.split(".").map(Number);
25
+ const pb = b.split(".").map(Number);
26
+ for (let i = 0; i < 3; i++) {
27
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
28
+ return -1;
29
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
30
+ return 1;
31
+ }
32
+ return 0;
33
+ }
34
+ async function readCache() {
35
+ try {
36
+ const data = await readFile(CACHE_FILE, "utf-8");
37
+ return JSON.parse(data);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ async function writeCache(cache) {
44
+ try {
45
+ await mkdir(CACHE_DIR, { recursive: true });
46
+ await writeFile(CACHE_FILE, JSON.stringify(cache), "utf-8");
47
+ }
48
+ catch {
49
+ // Silent — cache is optional
50
+ }
51
+ }
52
+ async function fetchLatestVersion() {
53
+ try {
54
+ const controller = new AbortController();
55
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
56
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal: controller.signal });
57
+ clearTimeout(timeout);
58
+ if (!res.ok)
59
+ return null;
60
+ const data = await res.json();
61
+ return data.version ?? null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ async function tryAutoUpdate(latestVersion) {
68
+ try {
69
+ // Try npm update — timeout after 30s, silent on failure
70
+ await execFileAsync("npm", ["update", PACKAGE_NAME, "--registry", "https://registry.npmjs.org"], {
71
+ timeout: 30000,
72
+ });
73
+ console.log(`[psyche-ai] ✓ Auto-updated to v${latestVersion}`);
74
+ return true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Check for updates. Non-blocking, safe to fire-and-forget.
82
+ * - Checks at most once per hour (cached)
83
+ * - If newer version found, attempts auto-update via npm
84
+ * - If auto-update fails, prints a manual update hint
85
+ * - Never throws
86
+ */
87
+ export async function checkForUpdate() {
88
+ // Rate limit: check at most once per hour
89
+ const cache = await readCache();
90
+ if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
91
+ // Still within cooldown — but notify if we already know about a newer version
92
+ if (cache.latestVersion && compareSemver(CURRENT_VERSION, cache.latestVersion) < 0) {
93
+ console.log(`[psyche-ai] v${cache.latestVersion} available (current: v${CURRENT_VERSION}). Run: npm update ${PACKAGE_NAME}`);
94
+ }
95
+ return;
96
+ }
97
+ const latest = await fetchLatestVersion();
98
+ await writeCache({ lastCheck: Date.now(), latestVersion: latest });
99
+ if (!latest || compareSemver(CURRENT_VERSION, latest) >= 0) {
100
+ return; // Up to date or couldn't check
101
+ }
102
+ // Newer version available — try auto-update
103
+ console.log(`[psyche-ai] New version v${latest} available (current: v${CURRENT_VERSION}), updating...`);
104
+ const updated = await tryAutoUpdate(latest);
105
+ if (!updated) {
106
+ console.log(`[psyche-ai] Auto-update failed. Run manually: npm update ${PACKAGE_NAME}`);
107
+ }
108
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
- "id": "psyche",
2
+ "id": "psyche-ai",
3
3
  "name": "Artificial Psyche",
4
4
  "description": "Virtual endocrine system, empathy engine, and agency for OpenClaw agents",
5
- "version": "2.0.0",
5
+ "version": "2.1.1",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psyche-ai",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Artificial Psyche — universal emotional intelligence plugin for any AI agent",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,15 +43,31 @@
43
43
  "type": "git",
44
44
  "url": "https://github.com/Shangri-la-0428/psyche-ai.git"
45
45
  },
46
- "keywords": ["ai", "emotion", "personality", "mbti", "agent", "psyche", "openclaw", "vercel-ai", "langchain", "emotional-intelligence"],
47
- "files": ["dist", "openclaw.plugin.json", "README.md", "LICENSE"],
46
+ "keywords": [
47
+ "ai",
48
+ "emotion",
49
+ "personality",
50
+ "mbti",
51
+ "agent",
52
+ "psyche",
53
+ "openclaw",
54
+ "vercel-ai",
55
+ "langchain",
56
+ "emotional-intelligence"
57
+ ],
58
+ "files": [
59
+ "dist",
60
+ "openclaw.plugin.json",
61
+ "README.md",
62
+ "LICENSE"
63
+ ],
48
64
  "engines": {
49
65
  "node": ">=22.0.0"
50
66
  },
51
67
  "peerDependencies": {
52
- "openclaw": ">=2026.3.0",
68
+ "@langchain/core": ">=0.3.0",
53
69
  "ai": ">=4.0.0",
54
- "@langchain/core": ">=0.3.0"
70
+ "openclaw": ">=2026.3.0"
55
71
  },
56
72
  "peerDependenciesMeta": {
57
73
  "openclaw": {
@@ -65,9 +81,11 @@
65
81
  }
66
82
  },
67
83
  "openclaw": {
68
- "extensions": ["./dist/adapters/openclaw.js"]
84
+ "extensions": [
85
+ "./dist/adapters/openclaw.js"
86
+ ]
69
87
  },
70
88
  "devDependencies": {
71
- "typescript": "^5.7.0"
89
+ "typescript": "^5.9.3"
72
90
  }
73
91
  }