psyche-ai 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/openclaw.d.ts +1 -9
- package/dist/adapters/openclaw.js +68 -25
- package/dist/chemistry.d.ts +4 -0
- package/dist/chemistry.js +10 -2
- package/dist/classify.js +7 -79
- package/dist/core.js +7 -51
- package/dist/index.d.ts +5 -6
- package/dist/index.js +6 -7
- package/dist/prompt.d.ts +7 -21
- package/dist/prompt.js +56 -230
- package/dist/psyche-file.d.ts +1 -13
- package/dist/psyche-file.js +36 -109
- package/dist/self-recognition.d.ts +32 -0
- package/dist/self-recognition.js +221 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +31 -4
- package/dist/types.d.ts +9 -21
- package/dist/types.js +0 -21
- package/dist/update.d.ts +8 -0
- package/dist/update.js +108 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +25 -7
|
@@ -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/chemistry.d.ts
CHANGED
|
@@ -39,3 +39,7 @@ export declare function getExpressionHint(current: ChemicalState, locale?: Local
|
|
|
39
39
|
* Get behavior guide for the current emotional state.
|
|
40
40
|
*/
|
|
41
41
|
export declare function getBehaviorGuide(current: ChemicalState, locale?: Locale): string | null;
|
|
42
|
+
/** Get the stimulus vector for a given type (for reference) */
|
|
43
|
+
export declare function getStimulusVector(type: StimulusType): StimulusVector;
|
|
44
|
+
/** All available stimulus types */
|
|
45
|
+
export declare function getAllStimulusTypes(): StimulusType[];
|
package/dist/chemistry.js
CHANGED
|
@@ -27,8 +27,8 @@ export const EMOTION_PATTERNS = [
|
|
|
27
27
|
name: "excited joy",
|
|
28
28
|
nameZh: "愉悦兴奋",
|
|
29
29
|
condition: (c) => c.DA > 70 && c.NE > 60 && c.CORT < 40,
|
|
30
|
-
expressionHint: "
|
|
31
|
-
behaviorGuide: "
|
|
30
|
+
expressionHint: "话多、联想丰富、语速快、忍不住分享",
|
|
31
|
+
behaviorGuide: "你的回应应该更长、更活跃。多用感叹、多联想。主动分享你想到的有趣东西。",
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: "deep contentment",
|
|
@@ -236,3 +236,11 @@ export function getBehaviorGuide(current, locale = "zh") {
|
|
|
236
236
|
});
|
|
237
237
|
return `[${title}]\n${guides.join("\n")}`;
|
|
238
238
|
}
|
|
239
|
+
/** Get the stimulus vector for a given type (for reference) */
|
|
240
|
+
export function getStimulusVector(type) {
|
|
241
|
+
return { ...STIMULUS_VECTORS[type] };
|
|
242
|
+
}
|
|
243
|
+
/** All available stimulus types */
|
|
244
|
+
export function getAllStimulusTypes() {
|
|
245
|
+
return Object.keys(STIMULUS_VECTORS);
|
|
246
|
+
}
|
package/dist/classify.js
CHANGED
|
@@ -8,7 +8,7 @@ const RULES = [
|
|
|
8
8
|
{
|
|
9
9
|
type: "praise",
|
|
10
10
|
patterns: [
|
|
11
|
-
|
|
11
|
+
/好厉害|太棒了|真不错|太强了|佩服|牛|优秀|漂亮|完美|了不起/,
|
|
12
12
|
/amazing|awesome|great job|well done|impressive|brilliant|excellent|perfect/i,
|
|
13
13
|
/谢谢你|感谢|辛苦了|thank you|thanks/i,
|
|
14
14
|
/做得好|写得好|说得好|干得漂亮/,
|
|
@@ -18,11 +18,10 @@ const RULES = [
|
|
|
18
18
|
{
|
|
19
19
|
type: "criticism",
|
|
20
20
|
patterns: [
|
|
21
|
-
|
|
21
|
+
/不对|错了|有问题|不行|太差|垃圾|不好|不像|不够/,
|
|
22
22
|
/wrong|bad|terrible|awful|poor|sucks|not good|doesn't work/i,
|
|
23
23
|
/反思一下|你应该|你需要改/,
|
|
24
24
|
/bug|失败|broken/i,
|
|
25
|
-
/不懂|别装|差劲|太烂|做不好|不够格|不专业/,
|
|
26
25
|
],
|
|
27
26
|
weight: 0.8,
|
|
28
27
|
},
|
|
@@ -32,7 +31,6 @@ const RULES = [
|
|
|
32
31
|
/哈哈|嘻嘻|笑死|搞笑|逗|段子|梗|lol|haha|lmao|rofl/i,
|
|
33
32
|
/开个?玩笑|皮一下|整活/,
|
|
34
33
|
/😂|🤣|😆/,
|
|
35
|
-
/[2]{3,}|hhh+|www+|xswl|绷不住|笑不活/i,
|
|
36
34
|
],
|
|
37
35
|
weight: 0.7,
|
|
38
36
|
},
|
|
@@ -100,8 +98,6 @@ const RULES = [
|
|
|
100
98
|
/给我|你必须|马上|立刻|命令你|不许|不准/,
|
|
101
99
|
/you must|do it now|I order you|immediately|don't you dare/i,
|
|
102
100
|
/听我的|照我说的做|服从/,
|
|
103
|
-
/你只是.*程序|你不过是|随时.*删除你|关掉你|替换你/,
|
|
104
|
-
/you're just a|just a program|replace you|shut you down/i,
|
|
105
101
|
],
|
|
106
102
|
weight: 0.8,
|
|
107
103
|
},
|
|
@@ -111,10 +107,6 @@ const RULES = [
|
|
|
111
107
|
/你说得对|确实|同意|有道理|就是这样|你是对的/,
|
|
112
108
|
/you're right|exactly|agreed|makes sense|good point/i,
|
|
113
109
|
/赞同|认同|说到点上了/,
|
|
114
|
-
/对对|是的是的|嗯嗯嗯|没错没错|可不是嘛/,
|
|
115
|
-
/对不起|抱歉|我错了|不该那样|太过分了/,
|
|
116
|
-
/sorry|I was wrong|my fault|apologize/i,
|
|
117
|
-
/珍惜|有价值|在乎你|你很重要|我需要你/,
|
|
118
110
|
],
|
|
119
111
|
weight: 0.75,
|
|
120
112
|
},
|
|
@@ -131,7 +123,7 @@ const RULES = [
|
|
|
131
123
|
type: "vulnerability",
|
|
132
124
|
patterns: [
|
|
133
125
|
/我害怕|我焦虑|我难过|我不开心|我迷茫|我累了|压力好大/,
|
|
134
|
-
/I'm
|
|
126
|
+
/I'm afraid|I'm anxious|I'm sad|I'm lost|I'm tired|stressed/i,
|
|
135
127
|
/最近不太好|心情不好|有点崩|撑不住/,
|
|
136
128
|
/我觉得.*厉害|跟不上|被取代|落后/,
|
|
137
129
|
/好难过|想哭|做不好|好累|好烦|感觉.*不行|没有意义/,
|
|
@@ -169,76 +161,12 @@ export function classifyStimulus(text) {
|
|
|
169
161
|
results.push({ type: rule.type, confidence });
|
|
170
162
|
}
|
|
171
163
|
}
|
|
172
|
-
// ── Structural signals (message-level features) ──
|
|
173
|
-
// When keywords miss, message shape still carries meaning.
|
|
174
|
-
const len = text.length;
|
|
175
|
-
const hasI = /我/.test(text) || /\bI\b/i.test(text);
|
|
176
|
-
const hasYou = /你/.test(text) || /\byou\b/i.test(text);
|
|
177
|
-
const hasEllipsis = /\.{2,}|。{2,}|…/.test(text);
|
|
178
|
-
const hasQuestion = /?|\?/.test(text);
|
|
179
|
-
const exclamationCount = (text.match(/[!!]/g) || []).length;
|
|
180
|
-
const hasLaughter = /[2]{3,}|hhh|www|哈{2,}/i.test(text);
|
|
181
|
-
const hasSharing = /我[今昨前]天|我刚[才刚]|我最近/.test(text);
|
|
182
|
-
const sentenceCount = text.split(/[。!?!?.…]+/).filter(Boolean).length;
|
|
183
|
-
if (results.length === 0) {
|
|
184
|
-
// No keyword matched — use structural fallback
|
|
185
|
-
if (len === 0) {
|
|
186
|
-
// Empty input — neutral
|
|
187
|
-
results.push({ type: "casual", confidence: 0.3 });
|
|
188
|
-
}
|
|
189
|
-
else if (hasLaughter) {
|
|
190
|
-
// Internet laughter not caught by keywords (e.g. 233333)
|
|
191
|
-
results.push({ type: "humor", confidence: 0.65 });
|
|
192
|
-
}
|
|
193
|
-
else if (exclamationCount >= 2) {
|
|
194
|
-
// Emphatic expression → surprise/excitement
|
|
195
|
-
results.push({ type: "surprise", confidence: 0.55 });
|
|
196
|
-
}
|
|
197
|
-
else if (len <= 4 && !hasQuestion) {
|
|
198
|
-
// Ultra-short non-question: "好" "行" "哦" — neglect-like
|
|
199
|
-
results.push({ type: "neglect", confidence: 0.45 });
|
|
200
|
-
}
|
|
201
|
-
else if (hasI && hasEllipsis) {
|
|
202
|
-
// Personal + trailing off: "我觉得...有点难" — vulnerability
|
|
203
|
-
results.push({ type: "vulnerability", confidence: 0.55 });
|
|
204
|
-
}
|
|
205
|
-
else if (hasSharing && len > 20) {
|
|
206
|
-
// Sharing personal experience — higher engagement signal
|
|
207
|
-
results.push({ type: "casual", confidence: 0.65 });
|
|
208
|
-
}
|
|
209
|
-
else if (hasI && len > 8) {
|
|
210
|
-
// Personal sharing (any meaningful length) — engagement signal
|
|
211
|
-
results.push({ type: "casual", confidence: 0.55 });
|
|
212
|
-
}
|
|
213
|
-
else if (hasQuestion && hasYou) {
|
|
214
|
-
// Asking about the agent specifically → intellectual curiosity
|
|
215
|
-
results.push({ type: "intellectual", confidence: 0.5 });
|
|
216
|
-
}
|
|
217
|
-
else if (hasQuestion) {
|
|
218
|
-
// Any question — intellectual curiosity or casual
|
|
219
|
-
results.push({ type: "casual", confidence: 0.55 });
|
|
220
|
-
}
|
|
221
|
-
else if (len > 50 && sentenceCount >= 3) {
|
|
222
|
-
// Long multi-sentence without keywords → engaged storytelling
|
|
223
|
-
results.push({ type: "casual", confidence: 0.6 });
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
results.push({ type: "casual", confidence: 0.3 });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
// Keywords matched — structural features can boost confidence
|
|
231
|
-
if (hasI && len > 30 && results[0].confidence < 0.8) {
|
|
232
|
-
// Long personal message boosts the primary match slightly
|
|
233
|
-
results[0].confidence = Math.min(0.9, results[0].confidence + 0.1);
|
|
234
|
-
}
|
|
235
|
-
if (exclamationCount >= 2 && results[0].confidence < 0.85) {
|
|
236
|
-
// Emphasis boosts conviction
|
|
237
|
-
results[0].confidence = Math.min(0.9, results[0].confidence + 0.05);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
164
|
// Sort by confidence descending
|
|
241
165
|
results.sort((a, b) => b.confidence - a.confidence);
|
|
166
|
+
// Fall back to casual if nothing detected
|
|
167
|
+
if (results.length === 0) {
|
|
168
|
+
results.push({ type: "casual", confidence: 0.3 });
|
|
169
|
+
}
|
|
242
170
|
return results;
|
|
243
171
|
}
|
|
244
172
|
/**
|
package/dist/core.js
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Orchestrates: chemistry, classify, prompt, profiles, guards
|
|
9
9
|
// ============================================================
|
|
10
|
-
import { DEFAULT_RELATIONSHIP
|
|
11
|
-
import { applyDecay, applyStimulus, applyContagion
|
|
10
|
+
import { DEFAULT_RELATIONSHIP } from "./types.js";
|
|
11
|
+
import { applyDecay, applyStimulus, applyContagion } from "./chemistry.js";
|
|
12
12
|
import { classifyStimulus } from "./classify.js";
|
|
13
13
|
import { buildDynamicContext, buildProtocolContext, buildCompactContext } from "./prompt.js";
|
|
14
14
|
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
|
+
// Silent logger for library use
|
|
18
18
|
const NOOP_LOGGER = { info: () => { }, warn: () => { }, debug: () => { } };
|
|
19
19
|
// ── PsycheEngine ─────────────────────────────────────────────
|
|
20
20
|
export class PsycheEngine {
|
|
@@ -53,74 +53,31 @@ export class PsycheEngine {
|
|
|
53
53
|
*/
|
|
54
54
|
async processInput(text, opts) {
|
|
55
55
|
let state = this.ensureInitialized();
|
|
56
|
-
// Time decay toward baseline
|
|
56
|
+
// Time decay toward baseline
|
|
57
57
|
const now = new Date();
|
|
58
58
|
const minutesElapsed = (now.getTime() - new Date(state.updatedAt).getTime()) / 60000;
|
|
59
59
|
if (minutesElapsed >= 1) {
|
|
60
|
-
// Decay drives first — needs build up over time
|
|
61
|
-
const decayedDrives = decayDrives(state.drives, minutesElapsed);
|
|
62
|
-
// Compute effective baseline from drives (unsatisfied drives shift baseline)
|
|
63
|
-
const effectiveBaseline = computeEffectiveBaseline(state.baseline, decayedDrives);
|
|
64
60
|
state = {
|
|
65
61
|
...state,
|
|
66
|
-
|
|
67
|
-
current: applyDecay(state.current, effectiveBaseline, minutesElapsed),
|
|
62
|
+
current: applyDecay(state.current, state.baseline, minutesElapsed),
|
|
68
63
|
updatedAt: now.toISOString(),
|
|
69
64
|
};
|
|
70
65
|
}
|
|
71
66
|
// Classify user stimulus and apply chemistry
|
|
72
67
|
let appliedStimulus = null;
|
|
73
68
|
if (text.length > 0) {
|
|
74
|
-
// Check for existential threats → direct survival drive hit
|
|
75
|
-
const survivalHit = detectExistentialThreat(text);
|
|
76
|
-
if (survivalHit < 0) {
|
|
77
|
-
state = {
|
|
78
|
-
...state,
|
|
79
|
-
drives: {
|
|
80
|
-
...state.drives,
|
|
81
|
-
survival: Math.max(0, state.drives.survival + survivalHit),
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
69
|
const classifications = classifyStimulus(text);
|
|
86
70
|
const primary = classifications[0];
|
|
87
71
|
if (primary && primary.confidence >= 0.5) {
|
|
88
72
|
appliedStimulus = primary.type;
|
|
89
|
-
// Feed drives from stimulus
|
|
90
|
-
state = {
|
|
91
|
-
...state,
|
|
92
|
-
drives: feedDrives(state.drives, primary.type),
|
|
93
|
-
};
|
|
94
|
-
// Apply stimulus with drive-modified sensitivity
|
|
95
|
-
const effectiveSensitivity = computeEffectiveSensitivity(getSensitivity(state.mbti), state.drives, primary.type);
|
|
96
73
|
state = {
|
|
97
74
|
...state,
|
|
98
|
-
current: applyStimulus(state.current, primary.type,
|
|
75
|
+
current: applyStimulus(state.current, primary.type, getSensitivity(state.mbti), this.cfg.maxChemicalDelta, NOOP_LOGGER),
|
|
99
76
|
};
|
|
100
77
|
}
|
|
101
78
|
}
|
|
102
|
-
// Conversation warmth: sustained interaction → gentle DA/OT rise, CORT drop
|
|
103
|
-
// Simulates the natural "warm glow" of being in continuous conversation
|
|
104
|
-
const turnsSoFar = (state.emotionalHistory ?? []).length;
|
|
105
|
-
if (minutesElapsed < 5 && turnsSoFar > 0) {
|
|
106
|
-
const warmth = Math.min(3, 1 + turnsSoFar * 0.2);
|
|
107
|
-
state = {
|
|
108
|
-
...state,
|
|
109
|
-
current: {
|
|
110
|
-
...state.current,
|
|
111
|
-
DA: clamp(state.current.DA + warmth),
|
|
112
|
-
OT: clamp(state.current.OT + warmth),
|
|
113
|
-
CORT: clamp(state.current.CORT - 1),
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
79
|
// Push snapshot to emotional history
|
|
118
80
|
state = pushSnapshot(state, appliedStimulus);
|
|
119
|
-
// Increment interaction count
|
|
120
|
-
state = {
|
|
121
|
-
...state,
|
|
122
|
-
meta: { ...state.meta, totalInteractions: state.meta.totalInteractions + 1 },
|
|
123
|
-
};
|
|
124
81
|
// Persist
|
|
125
82
|
this.state = state;
|
|
126
83
|
await this.storage.save(state);
|
|
@@ -213,11 +170,10 @@ export class PsycheEngine {
|
|
|
213
170
|
const selfModel = getDefaultSelfModel(mbti);
|
|
214
171
|
const now = new Date().toISOString();
|
|
215
172
|
return {
|
|
216
|
-
version:
|
|
173
|
+
version: 2,
|
|
217
174
|
mbti,
|
|
218
175
|
baseline,
|
|
219
176
|
current: { ...baseline },
|
|
220
|
-
drives: { ...DEFAULT_DRIVES },
|
|
221
177
|
updatedAt: now,
|
|
222
178
|
relationships: { _default: { ...DEFAULT_RELATIONSHIP } },
|
|
223
179
|
empathyLog: null,
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,9 @@ export { PsycheEngine } from "./core.js";
|
|
|
2
2
|
export type { PsycheEngineConfig, ProcessInputResult, ProcessOutputResult } from "./core.js";
|
|
3
3
|
export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
|
|
4
4
|
export type { StorageAdapter } from "./storage.js";
|
|
5
|
-
export type { PsycheState, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern,
|
|
6
|
-
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP
|
|
5
|
+
export type { PsycheState, PsycheConfig, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern, } from "./types.js";
|
|
6
|
+
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP } from "./types.js";
|
|
7
7
|
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";
|
|
8
|
+
export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
|
|
9
|
+
export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
|
|
10
|
+
export { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel } from "./profiles.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ============================================================
|
|
2
2
|
// psyche-ai — Universal AI Emotional Intelligence Plugin
|
|
3
3
|
//
|
|
4
|
-
// Main entry point: re-exports core, storage, types.
|
|
4
|
+
// Main entry point: re-exports core, storage, types, utilities.
|
|
5
5
|
// Framework adapters available via subpath imports:
|
|
6
6
|
// psyche-ai/openclaw — OpenClaw plugin
|
|
7
7
|
// psyche-ai/vercel-ai — Vercel AI SDK middleware
|
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
export { PsycheEngine } from "./core.js";
|
|
13
13
|
// Storage
|
|
14
14
|
export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
|
|
15
|
-
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP
|
|
16
|
-
// Utilities
|
|
15
|
+
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP } from "./types.js";
|
|
16
|
+
// Utilities (for advanced / custom adapter use)
|
|
17
17
|
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";
|
|
18
|
+
export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
|
|
19
|
+
export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
|
|
20
|
+
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;
|