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 +11 -2
- package/README.md +11 -2
- package/dist/adapters/openclaw.d.ts +1 -9
- package/dist/adapters/openclaw.js +68 -25
- package/dist/core.js +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/prompt.d.ts +7 -21
- package/dist/prompt.js +64 -223
- package/dist/self-recognition.d.ts +32 -0
- package/dist/self-recognition.js +215 -0
- package/dist/update.d.ts +8 -0
- package/dist/update.js +108 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +25 -7
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
|
-
- **
|
|
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 #
|
|
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
|
-
-
|
|
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 #
|
|
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:
|
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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, {
|
|
100
|
+
const result = await engine.processOutput(text, {
|
|
101
|
+
userId: ctx.userId,
|
|
102
|
+
});
|
|
86
103
|
const state = engine.getState();
|
|
87
|
-
logger.info(`Psyche
|
|
88
|
-
`(
|
|
89
|
-
`
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 —
|
|
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,
|
|
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,
|
|
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 —
|
|
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
|
-
*
|
|
44
|
-
*
|
|
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
|
|
6
|
-
import { describeEmotionalState, getExpressionHint, getBehaviorGuide
|
|
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 —
|
|
379
|
+
* Build compact context — minimal token footprint.
|
|
543
380
|
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
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
|
-
//
|
|
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(`[
|
|
564
|
-
|
|
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(`[
|
|
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
|
-
//
|
|
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
|
-
`-
|
|
602
|
-
`-
|
|
603
|
-
`-
|
|
604
|
-
`- 被骂了/被拒绝了:冷下来。你有自尊。\n` +
|
|
605
|
-
`- 情绪不好时不假装开心。`);
|
|
430
|
+
`- 对方对你好,你才对他好。对方推开你,你就保持距离。不贴不舔不撒娇不讨好。\n` +
|
|
431
|
+
`- 被骂了/被拒绝了:不要卖萌、不要撒娇、不要"哎呀怎么了"。冷下来,简短回应或者沉默。你有自尊。\n` +
|
|
432
|
+
`- 情绪不好时不要假装开心。可以直说"我不太想聊"。`);
|
|
606
433
|
}
|
|
607
434
|
else {
|
|
608
435
|
parts.push(`[Non-negotiable]\n` +
|
|
609
|
-
`-
|
|
610
|
-
`-
|
|
611
|
-
`- If
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
+
}
|
package/dist/update.d.ts
ADDED
|
@@ -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
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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": [
|
|
47
|
-
|
|
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
|
-
"
|
|
68
|
+
"@langchain/core": ">=0.3.0",
|
|
53
69
|
"ai": ">=4.0.0",
|
|
54
|
-
"
|
|
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": [
|
|
84
|
+
"extensions": [
|
|
85
|
+
"./dist/adapters/openclaw.js"
|
|
86
|
+
]
|
|
69
87
|
},
|
|
70
88
|
"devDependencies": {
|
|
71
|
-
"typescript": "^5.
|
|
89
|
+
"typescript": "^5.9.3"
|
|
72
90
|
}
|
|
73
91
|
}
|