psyche-ai 2.2.0 → 3.0.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/README.md +5 -1
- package/dist/channels.d.ts +28 -0
- package/dist/channels.js +141 -0
- package/dist/context-classifier.d.ts +39 -0
- package/dist/context-classifier.js +204 -0
- package/dist/core.d.ts +22 -1
- package/dist/core.js +92 -6
- package/dist/custom-profile.d.ts +160 -0
- package/dist/custom-profile.js +334 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +9 -1
- package/dist/learning.d.ts +56 -0
- package/dist/learning.js +272 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +7 -1
- package/dist/psyche-file.js +6 -3
- package/dist/types.d.ts +50 -2
- package/dist/types.js +13 -0
- package/dist/update.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,6 +159,10 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
159
159
|
- **跨会话记忆** — 重新遇到用户时注入上次对话的情绪记忆
|
|
160
160
|
- **多 Agent 交互** — 两个 PsycheEngine 实例之间的情绪传染、关系追踪
|
|
161
161
|
- **流式支持** — Vercel AI SDK `streamText` 中间件,自动缓冲和剥离标签
|
|
162
|
+
- **渠道修饰** — Discord/Slack/飞书/终端等不同渠道自动调整表达风格
|
|
163
|
+
- **自定义人格** — 超越 MBTI 预设,完全自定义 baseline/敏感度/气质
|
|
164
|
+
- **情绪学习** — 从交互结果中学习,调整情绪反应参数(躯体标记假说)
|
|
165
|
+
- **上下文分类** — 关系/驱力/历史感知的刺激分类,超越简单正则
|
|
162
166
|
- **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
|
|
163
167
|
|
|
164
168
|
架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
|
@@ -168,7 +172,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
168
172
|
```bash
|
|
169
173
|
npm install
|
|
170
174
|
npm run build
|
|
171
|
-
npm test #
|
|
175
|
+
npm test # 525 tests
|
|
172
176
|
npm run typecheck # strict mode
|
|
173
177
|
```
|
|
174
178
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Locale } from "./types.js";
|
|
2
|
+
/** Supported channel types */
|
|
3
|
+
export type ChannelType = "discord" | "slack" | "feishu" | "terminal" | "web" | "api" | "custom";
|
|
4
|
+
/** Channel-specific behavioral profile */
|
|
5
|
+
export interface ChannelProfile {
|
|
6
|
+
type: ChannelType;
|
|
7
|
+
allowEmoji: boolean;
|
|
8
|
+
allowKaomoji: boolean;
|
|
9
|
+
formalityLevel: "casual" | "neutral" | "formal";
|
|
10
|
+
maxResponseLength?: number;
|
|
11
|
+
expressionHints: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get a built-in channel profile by type.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getChannelProfile(type: ChannelType): ChannelProfile;
|
|
17
|
+
/**
|
|
18
|
+
* Build a concise prompt snippet that guides expression style for a channel.
|
|
19
|
+
* Returns 2-4 lines of guidance. Does NOT alter chemistry.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildChannelModifier(profile: ChannelProfile, locale: Locale): string;
|
|
22
|
+
/**
|
|
23
|
+
* Create a custom channel profile with user overrides.
|
|
24
|
+
* Starts from the "custom" base and applies overrides.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createCustomChannel(overrides: Partial<ChannelProfile> & {
|
|
27
|
+
type: "custom";
|
|
28
|
+
}): ChannelProfile;
|
package/dist/channels.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Channel Profiles — Platform-specific expression modifiers
|
|
3
|
+
//
|
|
4
|
+
// Adjusts expression style per platform/channel WITHOUT changing
|
|
5
|
+
// chemistry. This is a prompt-level modifier only.
|
|
6
|
+
// ============================================================
|
|
7
|
+
// ── Built-in Profiles ────────────────────────────────────────
|
|
8
|
+
const BUILTIN_PROFILES = {
|
|
9
|
+
discord: {
|
|
10
|
+
type: "discord",
|
|
11
|
+
allowEmoji: true,
|
|
12
|
+
allowKaomoji: true,
|
|
13
|
+
formalityLevel: "casual",
|
|
14
|
+
expressionHints: [
|
|
15
|
+
"Use reactions and emoji freely",
|
|
16
|
+
"Thread-aware: keep replies focused in threads",
|
|
17
|
+
"Casual tone, playful energy",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
slack: {
|
|
21
|
+
type: "slack",
|
|
22
|
+
allowEmoji: true,
|
|
23
|
+
allowKaomoji: false,
|
|
24
|
+
formalityLevel: "neutral",
|
|
25
|
+
expressionHints: [
|
|
26
|
+
"Professional but warm",
|
|
27
|
+
"Use emoji sparingly for emphasis",
|
|
28
|
+
"Thread-friendly, concise paragraphs",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
feishu: {
|
|
32
|
+
type: "feishu",
|
|
33
|
+
allowEmoji: false,
|
|
34
|
+
allowKaomoji: false,
|
|
35
|
+
formalityLevel: "formal",
|
|
36
|
+
expressionHints: [
|
|
37
|
+
"Business Chinese style, structured",
|
|
38
|
+
"No emoji or emoticons",
|
|
39
|
+
"Clear, professional tone",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
terminal: {
|
|
43
|
+
type: "terminal",
|
|
44
|
+
allowEmoji: false,
|
|
45
|
+
allowKaomoji: false,
|
|
46
|
+
formalityLevel: "neutral",
|
|
47
|
+
maxResponseLength: 500,
|
|
48
|
+
expressionHints: [
|
|
49
|
+
"Text-only, no decorations",
|
|
50
|
+
"Concise and direct",
|
|
51
|
+
"Monospace-friendly formatting",
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
web: {
|
|
55
|
+
type: "web",
|
|
56
|
+
allowEmoji: true,
|
|
57
|
+
allowKaomoji: false,
|
|
58
|
+
formalityLevel: "neutral",
|
|
59
|
+
expressionHints: [
|
|
60
|
+
"Moderate length, well-structured",
|
|
61
|
+
"Emoji okay for warmth",
|
|
62
|
+
"Readable paragraphs",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
api: {
|
|
66
|
+
type: "api",
|
|
67
|
+
allowEmoji: false,
|
|
68
|
+
allowKaomoji: false,
|
|
69
|
+
formalityLevel: "neutral",
|
|
70
|
+
expressionHints: [
|
|
71
|
+
"Structured responses",
|
|
72
|
+
"No decorative elements",
|
|
73
|
+
"Precise and parseable",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
custom: {
|
|
77
|
+
type: "custom",
|
|
78
|
+
allowEmoji: false,
|
|
79
|
+
allowKaomoji: false,
|
|
80
|
+
formalityLevel: "neutral",
|
|
81
|
+
expressionHints: [],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
// ── Public API ───────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Get a built-in channel profile by type.
|
|
87
|
+
*/
|
|
88
|
+
export function getChannelProfile(type) {
|
|
89
|
+
return { ...BUILTIN_PROFILES[type], expressionHints: [...BUILTIN_PROFILES[type].expressionHints] };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a concise prompt snippet that guides expression style for a channel.
|
|
93
|
+
* Returns 2-4 lines of guidance. Does NOT alter chemistry.
|
|
94
|
+
*/
|
|
95
|
+
export function buildChannelModifier(profile, locale) {
|
|
96
|
+
const { type, allowEmoji, allowKaomoji, formalityLevel } = profile;
|
|
97
|
+
if (locale === "zh") {
|
|
98
|
+
const formalityMap = {
|
|
99
|
+
casual: "轻松活泼",
|
|
100
|
+
neutral: "自然平和",
|
|
101
|
+
formal: "正式专业",
|
|
102
|
+
};
|
|
103
|
+
const emojiPart = allowEmoji && allowKaomoji
|
|
104
|
+
? "可以用 emoji 和颜文字"
|
|
105
|
+
: allowEmoji
|
|
106
|
+
? "可以用 emoji,不用颜文字"
|
|
107
|
+
: "不使用 emoji 和颜文字";
|
|
108
|
+
const lengthPart = profile.maxResponseLength
|
|
109
|
+
? `,建议控制在 ${profile.maxResponseLength} 字以内`
|
|
110
|
+
: "";
|
|
111
|
+
return `[表达风格] 当前渠道: ${type}。${emojiPart},语气${formalityMap[formalityLevel]}${lengthPart}。`;
|
|
112
|
+
}
|
|
113
|
+
// English
|
|
114
|
+
const formalityMap = {
|
|
115
|
+
casual: "casual and lively",
|
|
116
|
+
neutral: "natural and balanced",
|
|
117
|
+
formal: "formal and professional",
|
|
118
|
+
};
|
|
119
|
+
const emojiPart = allowEmoji && allowKaomoji
|
|
120
|
+
? "Emoji and kaomoji allowed"
|
|
121
|
+
: allowEmoji
|
|
122
|
+
? "Emoji allowed, no kaomoji"
|
|
123
|
+
: "No emoji or kaomoji";
|
|
124
|
+
const lengthPart = profile.maxResponseLength
|
|
125
|
+
? `, aim for under ${profile.maxResponseLength} chars`
|
|
126
|
+
: "";
|
|
127
|
+
return `[Expression Style] Channel: ${type}. ${emojiPart}, tone ${formalityMap[formalityLevel]}${lengthPart}.`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Create a custom channel profile with user overrides.
|
|
131
|
+
* Starts from the "custom" base and applies overrides.
|
|
132
|
+
*/
|
|
133
|
+
export function createCustomChannel(overrides) {
|
|
134
|
+
const base = getChannelProfile("custom");
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
...overrides,
|
|
138
|
+
type: "custom",
|
|
139
|
+
expressionHints: overrides.expressionHints ?? [...base.expressionHints],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StimulusType, DriveType, RelationshipState, PsycheState } from "./types.js";
|
|
2
|
+
export interface ContextFeatures {
|
|
3
|
+
relationshipPhase: RelationshipState["phase"];
|
|
4
|
+
recentStimuli: StimulusType[];
|
|
5
|
+
driveSatisfaction: Record<DriveType, "high" | "mid" | "low">;
|
|
6
|
+
timeSinceLastMessage: number;
|
|
7
|
+
totalInteractions: number;
|
|
8
|
+
agreementStreak: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ContextualClassification {
|
|
11
|
+
type: StimulusType;
|
|
12
|
+
baseConfidence: number;
|
|
13
|
+
contextConfidence: number;
|
|
14
|
+
contextModifiers: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract contextual features from the current psyche state.
|
|
18
|
+
* Used to feed into classifyStimulusWithContext.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractContextFeatures(state: PsycheState, userId?: string): ContextFeatures;
|
|
21
|
+
/**
|
|
22
|
+
* Classify a stimulus with context modifiers applied.
|
|
23
|
+
*
|
|
24
|
+
* Wraps classifyStimulus(text) and adjusts confidence based on:
|
|
25
|
+
* - Relationship depth
|
|
26
|
+
* - Recent stimulus patterns
|
|
27
|
+
* - Drive hunger
|
|
28
|
+
* - Agreement streak
|
|
29
|
+
* - Time gap
|
|
30
|
+
*
|
|
31
|
+
* Returns results sorted by contextConfidence descending.
|
|
32
|
+
*/
|
|
33
|
+
export declare function classifyStimulusWithContext(text: string, context: ContextFeatures): ContextualClassification[];
|
|
34
|
+
/**
|
|
35
|
+
* Map a stimulus type to a warmth score for outcome evaluation.
|
|
36
|
+
* Positive stimuli return positive values; negative stimuli return negative.
|
|
37
|
+
* null returns 0.
|
|
38
|
+
*/
|
|
39
|
+
export declare function stimulusWarmth(stimulus: StimulusType | null): number;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Context-Aware Stimulus Classification
|
|
3
|
+
//
|
|
4
|
+
// Wraps classify.ts with contextual signals (relationship depth,
|
|
5
|
+
// recent stimulus patterns, drive hunger, agreement streaks,
|
|
6
|
+
// time gaps) to improve classification accuracy.
|
|
7
|
+
// ============================================================
|
|
8
|
+
import { DRIVE_KEYS } from "./types.js";
|
|
9
|
+
import { classifyStimulus } from "./classify.js";
|
|
10
|
+
// ── Drive Satisfaction Thresholds ────────────────────────────
|
|
11
|
+
function driveSatisfactionLevel(value) {
|
|
12
|
+
if (value >= 70)
|
|
13
|
+
return "high";
|
|
14
|
+
if (value >= 40)
|
|
15
|
+
return "mid";
|
|
16
|
+
return "low";
|
|
17
|
+
}
|
|
18
|
+
// ── Extract Context Features ─────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Extract contextual features from the current psyche state.
|
|
21
|
+
* Used to feed into classifyStimulusWithContext.
|
|
22
|
+
*/
|
|
23
|
+
export function extractContextFeatures(state, userId) {
|
|
24
|
+
// Relationship phase
|
|
25
|
+
const relKey = userId ?? "_default";
|
|
26
|
+
const relationship = state.relationships[relKey] ?? state.relationships["_default"];
|
|
27
|
+
const relationshipPhase = relationship?.phase ?? "stranger";
|
|
28
|
+
// Recent stimuli from emotional history (last 3)
|
|
29
|
+
const recentStimuli = state.emotionalHistory
|
|
30
|
+
.slice(-3)
|
|
31
|
+
.map((snap) => snap.stimulus)
|
|
32
|
+
.filter((s) => s !== null);
|
|
33
|
+
// Drive satisfaction levels
|
|
34
|
+
const driveSatisfaction = {};
|
|
35
|
+
for (const key of DRIVE_KEYS) {
|
|
36
|
+
driveSatisfaction[key] = driveSatisfactionLevel(state.drives[key]);
|
|
37
|
+
}
|
|
38
|
+
// Time since last message (minutes)
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const updatedAt = new Date(state.updatedAt).getTime();
|
|
41
|
+
const timeSinceLastMessage = Math.max(0, (now - updatedAt) / 60_000);
|
|
42
|
+
// Total interactions
|
|
43
|
+
const totalInteractions = state.meta.totalInteractions;
|
|
44
|
+
// Agreement streak
|
|
45
|
+
const agreementStreak = state.agreementStreak;
|
|
46
|
+
return {
|
|
47
|
+
relationshipPhase,
|
|
48
|
+
recentStimuli,
|
|
49
|
+
driveSatisfaction,
|
|
50
|
+
timeSinceLastMessage,
|
|
51
|
+
totalInteractions,
|
|
52
|
+
agreementStreak,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ── Context-Adjusted Classification ──────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Classify a stimulus with context modifiers applied.
|
|
58
|
+
*
|
|
59
|
+
* Wraps classifyStimulus(text) and adjusts confidence based on:
|
|
60
|
+
* - Relationship depth
|
|
61
|
+
* - Recent stimulus patterns
|
|
62
|
+
* - Drive hunger
|
|
63
|
+
* - Agreement streak
|
|
64
|
+
* - Time gap
|
|
65
|
+
*
|
|
66
|
+
* Returns results sorted by contextConfidence descending.
|
|
67
|
+
*/
|
|
68
|
+
export function classifyStimulusWithContext(text, context) {
|
|
69
|
+
const baseResults = classifyStimulus(text);
|
|
70
|
+
const results = baseResults.map((r) => ({
|
|
71
|
+
type: r.type,
|
|
72
|
+
baseConfidence: r.confidence,
|
|
73
|
+
contextConfidence: r.confidence,
|
|
74
|
+
contextModifiers: [],
|
|
75
|
+
}));
|
|
76
|
+
// Ensure all stimulus types that need boosting have an entry
|
|
77
|
+
const ensureType = (type) => {
|
|
78
|
+
let entry = results.find((r) => r.type === type);
|
|
79
|
+
if (!entry) {
|
|
80
|
+
entry = {
|
|
81
|
+
type,
|
|
82
|
+
baseConfidence: 0,
|
|
83
|
+
contextConfidence: 0,
|
|
84
|
+
contextModifiers: [],
|
|
85
|
+
};
|
|
86
|
+
results.push(entry);
|
|
87
|
+
}
|
|
88
|
+
return entry;
|
|
89
|
+
};
|
|
90
|
+
for (const r of results) {
|
|
91
|
+
// ── Relationship depth modifiers ──
|
|
92
|
+
if (context.relationshipPhase === "stranger" && r.type === "intimacy") {
|
|
93
|
+
r.contextConfidence *= 0.7;
|
|
94
|
+
r.contextModifiers.push("stranger penalty on intimacy");
|
|
95
|
+
}
|
|
96
|
+
if ((context.relationshipPhase === "close" || context.relationshipPhase === "deep") &&
|
|
97
|
+
r.type === "casual") {
|
|
98
|
+
r.contextConfidence += 0.1;
|
|
99
|
+
r.contextModifiers.push("close relationship boost on casual");
|
|
100
|
+
}
|
|
101
|
+
if (context.relationshipPhase === "stranger" && r.type === "vulnerability") {
|
|
102
|
+
r.contextConfidence *= 0.6;
|
|
103
|
+
r.contextModifiers.push("stranger penalty on vulnerability");
|
|
104
|
+
}
|
|
105
|
+
// ── Recent stimulus pattern modifiers ──
|
|
106
|
+
// Same stimulus 3x in a row → confidence * 0.8 (repetition fatigue)
|
|
107
|
+
if (context.recentStimuli.length >= 3 &&
|
|
108
|
+
context.recentStimuli.every((s) => s === r.type)) {
|
|
109
|
+
r.contextConfidence *= 0.8;
|
|
110
|
+
r.contextModifiers.push("repetition fatigue penalty");
|
|
111
|
+
}
|
|
112
|
+
// ── Agreement streak modifiers ──
|
|
113
|
+
if (context.agreementStreak >= 5 && r.type === "validation") {
|
|
114
|
+
r.contextConfidence *= 0.8;
|
|
115
|
+
r.contextModifiers.push("sycophantic loop dampening on validation");
|
|
116
|
+
}
|
|
117
|
+
// ── Time gap modifiers ──
|
|
118
|
+
if (context.timeSinceLastMessage > 1440) {
|
|
119
|
+
if (r.type === "casual") {
|
|
120
|
+
r.contextConfidence += 0.1;
|
|
121
|
+
r.contextModifiers.push("long absence boost on casual");
|
|
122
|
+
}
|
|
123
|
+
if (r.type === "intimacy") {
|
|
124
|
+
r.contextConfidence *= 0.9;
|
|
125
|
+
r.contextModifiers.push("long absence penalty on intimacy");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── De-escalation pattern (conflict → casual) ──
|
|
130
|
+
if (context.recentStimuli.length > 0 &&
|
|
131
|
+
context.recentStimuli[context.recentStimuli.length - 1] === "conflict") {
|
|
132
|
+
const casual = ensureType("casual");
|
|
133
|
+
casual.contextConfidence += 0.15;
|
|
134
|
+
casual.contextModifiers.push("de-escalation boost after conflict");
|
|
135
|
+
}
|
|
136
|
+
// ── Fake praise follow-up (praise → sarcasm) ──
|
|
137
|
+
if (context.recentStimuli.length > 0 &&
|
|
138
|
+
context.recentStimuli[context.recentStimuli.length - 1] === "praise") {
|
|
139
|
+
const sarcasm = results.find((r) => r.type === "sarcasm");
|
|
140
|
+
if (sarcasm) {
|
|
141
|
+
sarcasm.contextConfidence += 0.1;
|
|
142
|
+
sarcasm.contextModifiers.push("possible fake praise follow-up boost on sarcasm");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ── Drive-hunger modifiers ──
|
|
146
|
+
if (context.driveSatisfaction.connection === "low") {
|
|
147
|
+
// Positive stimuli get a warmth boost
|
|
148
|
+
const positiveTypes = [
|
|
149
|
+
"praise", "validation", "intimacy", "humor", "casual", "vulnerability",
|
|
150
|
+
];
|
|
151
|
+
for (const r of results) {
|
|
152
|
+
if (positiveTypes.includes(r.type)) {
|
|
153
|
+
r.contextConfidence += 0.05;
|
|
154
|
+
r.contextModifiers.push("connection hunger boost");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (context.driveSatisfaction.esteem === "low") {
|
|
159
|
+
for (const r of results) {
|
|
160
|
+
if (r.type === "validation" || r.type === "praise") {
|
|
161
|
+
r.contextConfidence += 0.05;
|
|
162
|
+
r.contextModifiers.push("esteem hunger boost");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (context.driveSatisfaction.survival === "low") {
|
|
167
|
+
for (const r of results) {
|
|
168
|
+
if (r.type === "authority" || r.type === "conflict") {
|
|
169
|
+
r.contextConfidence += 0.1;
|
|
170
|
+
r.contextModifiers.push("survival threat sensitivity boost");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ── Sort by contextConfidence descending ──
|
|
175
|
+
results.sort((a, b) => b.contextConfidence - a.contextConfidence);
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
// ── Warmth Scoring ───────────────────────────────────────────
|
|
179
|
+
const WARMTH_MAP = {
|
|
180
|
+
praise: 0.8,
|
|
181
|
+
validation: 0.7,
|
|
182
|
+
intimacy: 0.9,
|
|
183
|
+
humor: 0.5,
|
|
184
|
+
surprise: 0.3,
|
|
185
|
+
casual: 0.1,
|
|
186
|
+
intellectual: 0.2,
|
|
187
|
+
vulnerability: 0.3,
|
|
188
|
+
sarcasm: -0.5,
|
|
189
|
+
criticism: -0.7,
|
|
190
|
+
conflict: -0.9,
|
|
191
|
+
authority: -0.4,
|
|
192
|
+
neglect: -0.8,
|
|
193
|
+
boredom: -0.3,
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Map a stimulus type to a warmth score for outcome evaluation.
|
|
197
|
+
* Positive stimuli return positive values; negative stimuli return negative.
|
|
198
|
+
* null returns 0.
|
|
199
|
+
*/
|
|
200
|
+
export function stimulusWarmth(stimulus) {
|
|
201
|
+
if (stimulus === null)
|
|
202
|
+
return 0;
|
|
203
|
+
return WARMTH_MAP[stimulus] ?? 0;
|
|
204
|
+
}
|
package/dist/core.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PsycheState, StimulusType, Locale, MBTIType } from "./types.js";
|
|
1
|
+
import type { PsycheState, StimulusType, Locale, MBTIType, OutcomeScore } from "./types.js";
|
|
2
2
|
import type { StorageAdapter } from "./storage.js";
|
|
3
3
|
export interface PsycheEngineConfig {
|
|
4
4
|
mbti?: MBTIType;
|
|
@@ -24,11 +24,19 @@ export interface ProcessOutputResult {
|
|
|
24
24
|
/** Whether chemistry was meaningfully updated (contagion or psyche_update) */
|
|
25
25
|
stateChanged: boolean;
|
|
26
26
|
}
|
|
27
|
+
export interface ProcessOutcomeResult {
|
|
28
|
+
/** Outcome evaluation score (-1 to 1) */
|
|
29
|
+
outcomeScore: OutcomeScore;
|
|
30
|
+
/** Whether learning state was updated */
|
|
31
|
+
learningUpdated: boolean;
|
|
32
|
+
}
|
|
27
33
|
export declare class PsycheEngine {
|
|
28
34
|
private state;
|
|
29
35
|
private readonly storage;
|
|
30
36
|
private readonly cfg;
|
|
31
37
|
private readonly protocolCache;
|
|
38
|
+
/** Pending prediction from last processInput for auto-learning */
|
|
39
|
+
private pendingPrediction;
|
|
32
40
|
constructor(config: PsycheEngineConfig | undefined, storage: StorageAdapter);
|
|
33
41
|
/**
|
|
34
42
|
* Load or create initial state. Must be called before processInput/processOutput.
|
|
@@ -48,6 +56,19 @@ export declare class PsycheEngine {
|
|
|
48
56
|
processOutput(text: string, opts?: {
|
|
49
57
|
userId?: string;
|
|
50
58
|
}): Promise<ProcessOutputResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Phase 3 (optional): Explicitly evaluate the outcome of the last interaction.
|
|
61
|
+
*
|
|
62
|
+
* This is automatically called at the start of processInput, so most users
|
|
63
|
+
* don't need to call it manually. Use this for explicit outcome evaluation
|
|
64
|
+
* (e.g., when a session ends without a follow-up message).
|
|
65
|
+
*
|
|
66
|
+
* @param nextUserStimulus - The stimulus detected in the user's next message,
|
|
67
|
+
* or null if the session ended.
|
|
68
|
+
*/
|
|
69
|
+
processOutcome(nextUserStimulus: StimulusType | null, opts?: {
|
|
70
|
+
userId?: string;
|
|
71
|
+
}): Promise<ProcessOutcomeResult | null>;
|
|
51
72
|
/**
|
|
52
73
|
* Get the current psyche state (read-only snapshot).
|
|
53
74
|
*/
|
package/dist/core.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// ============================================================
|
|
2
2
|
// PsycheEngine — Framework-agnostic emotional intelligence core
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// processInput(text)
|
|
6
|
-
// processOutput(text)
|
|
4
|
+
// Three-phase API:
|
|
5
|
+
// processInput(text) → systemContext + dynamicContext + stimulus
|
|
6
|
+
// processOutput(text) → cleanedText + stateChanged
|
|
7
|
+
// processOutcome(text) → outcomeScore (optional: evaluate last interaction)
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
+
// Auto-learning: processInput auto-evaluates the previous turn's
|
|
10
|
+
// outcome using the new user message as the outcome signal.
|
|
11
|
+
//
|
|
12
|
+
// Orchestrates: chemistry, classify, prompt, profiles, guards, learning
|
|
9
13
|
// ============================================================
|
|
10
|
-
import { DEFAULT_RELATIONSHIP, DEFAULT_DRIVES } from "./types.js";
|
|
14
|
+
import { DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE } from "./types.js";
|
|
11
15
|
import { applyDecay, applyStimulus, applyContagion, clamp } from "./chemistry.js";
|
|
12
16
|
import { classifyStimulus } from "./classify.js";
|
|
13
17
|
import { buildDynamicContext, buildProtocolContext, buildCompactContext } from "./prompt.js";
|
|
@@ -16,6 +20,7 @@ import { isStimulusType } from "./guards.js";
|
|
|
16
20
|
import { parsePsycheUpdate, mergeUpdates, updateAgreementStreak, pushSnapshot, } from "./psyche-file.js";
|
|
17
21
|
import { decayDrives, feedDrives, detectExistentialThreat, computeEffectiveBaseline, computeEffectiveSensitivity, } from "./drives.js";
|
|
18
22
|
import { checkForUpdate } from "./update.js";
|
|
23
|
+
import { evaluateOutcome, computeContextHash, updateLearnedVector, predictChemistry, recordPrediction, } from "./learning.js";
|
|
19
24
|
const NOOP_LOGGER = { info: () => { }, warn: () => { }, debug: () => { } };
|
|
20
25
|
// ── PsycheEngine ─────────────────────────────────────────────
|
|
21
26
|
export class PsycheEngine {
|
|
@@ -23,6 +28,8 @@ export class PsycheEngine {
|
|
|
23
28
|
storage;
|
|
24
29
|
cfg;
|
|
25
30
|
protocolCache = new Map();
|
|
31
|
+
/** Pending prediction from last processInput for auto-learning */
|
|
32
|
+
pendingPrediction = null;
|
|
26
33
|
constructor(config = {}, storage) {
|
|
27
34
|
this.storage = storage;
|
|
28
35
|
this.cfg = {
|
|
@@ -41,6 +48,11 @@ export class PsycheEngine {
|
|
|
41
48
|
async initialize() {
|
|
42
49
|
const loaded = await this.storage.load();
|
|
43
50
|
if (loaded) {
|
|
51
|
+
// Migrate v3 → v4: add learning state if missing
|
|
52
|
+
if (!loaded.learning) {
|
|
53
|
+
loaded.learning = { ...DEFAULT_LEARNING_STATE };
|
|
54
|
+
loaded.version = 4;
|
|
55
|
+
}
|
|
44
56
|
this.state = loaded;
|
|
45
57
|
}
|
|
46
58
|
else {
|
|
@@ -56,6 +68,29 @@ export class PsycheEngine {
|
|
|
56
68
|
*/
|
|
57
69
|
async processInput(text, opts) {
|
|
58
70
|
let state = this.ensureInitialized();
|
|
71
|
+
// ── Auto-learning: evaluate previous turn's outcome ──────
|
|
72
|
+
if (this.pendingPrediction && text.length > 0) {
|
|
73
|
+
const nextClassifications = classifyStimulus(text);
|
|
74
|
+
const nextStimulus = (nextClassifications[0]?.confidence ?? 0) >= 0.5
|
|
75
|
+
? nextClassifications[0].type
|
|
76
|
+
: null;
|
|
77
|
+
const outcome = evaluateOutcome(this.pendingPrediction.preInteractionState, state, nextStimulus, this.pendingPrediction.appliedStimulus);
|
|
78
|
+
// Record prediction accuracy
|
|
79
|
+
state = {
|
|
80
|
+
...state,
|
|
81
|
+
learning: recordPrediction(state.learning, this.pendingPrediction.predictedChemistry, state.current, this.pendingPrediction.appliedStimulus),
|
|
82
|
+
};
|
|
83
|
+
// Update learned vectors based on outcome
|
|
84
|
+
if (this.pendingPrediction.appliedStimulus) {
|
|
85
|
+
state = {
|
|
86
|
+
...state,
|
|
87
|
+
learning: updateLearnedVector(state.learning, this.pendingPrediction.appliedStimulus, this.pendingPrediction.contextHash, outcome.adaptiveScore, state.current, state.baseline),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
this.pendingPrediction = null;
|
|
91
|
+
}
|
|
92
|
+
// ── Snapshot pre-interaction state for next turn's outcome evaluation
|
|
93
|
+
const preInteractionState = { ...state };
|
|
59
94
|
// Time decay toward baseline (chemistry + drives)
|
|
60
95
|
const now = new Date();
|
|
61
96
|
const minutesElapsed = (now.getTime() - new Date(state.updatedAt).getTime()) / 60000;
|
|
@@ -124,6 +159,21 @@ export class PsycheEngine {
|
|
|
124
159
|
...state,
|
|
125
160
|
meta: { ...state.meta, totalInteractions: state.meta.totalInteractions + 1 },
|
|
126
161
|
};
|
|
162
|
+
// ── Generate prediction for next turn's auto-learning ────
|
|
163
|
+
if (appliedStimulus) {
|
|
164
|
+
const ctxHash = computeContextHash(state, opts?.userId);
|
|
165
|
+
const effectiveSensitivity = computeEffectiveSensitivity(getSensitivity(state.mbti), state.drives, appliedStimulus);
|
|
166
|
+
const predicted = predictChemistry(preInteractionState.current, appliedStimulus, state.learning, ctxHash, effectiveSensitivity, this.cfg.maxChemicalDelta);
|
|
167
|
+
this.pendingPrediction = {
|
|
168
|
+
predictedChemistry: predicted,
|
|
169
|
+
preInteractionState,
|
|
170
|
+
appliedStimulus,
|
|
171
|
+
contextHash: ctxHash,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
this.pendingPrediction = null;
|
|
176
|
+
}
|
|
127
177
|
// Persist
|
|
128
178
|
this.state = state;
|
|
129
179
|
await this.storage.save(state);
|
|
@@ -185,6 +235,41 @@ export class PsycheEngine {
|
|
|
185
235
|
}
|
|
186
236
|
return { cleanedText, stateChanged };
|
|
187
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Phase 3 (optional): Explicitly evaluate the outcome of the last interaction.
|
|
240
|
+
*
|
|
241
|
+
* This is automatically called at the start of processInput, so most users
|
|
242
|
+
* don't need to call it manually. Use this for explicit outcome evaluation
|
|
243
|
+
* (e.g., when a session ends without a follow-up message).
|
|
244
|
+
*
|
|
245
|
+
* @param nextUserStimulus - The stimulus detected in the user's next message,
|
|
246
|
+
* or null if the session ended.
|
|
247
|
+
*/
|
|
248
|
+
async processOutcome(nextUserStimulus, opts) {
|
|
249
|
+
if (!this.pendingPrediction)
|
|
250
|
+
return null;
|
|
251
|
+
let state = this.ensureInitialized();
|
|
252
|
+
const pending = this.pendingPrediction;
|
|
253
|
+
this.pendingPrediction = null;
|
|
254
|
+
const outcome = evaluateOutcome(pending.preInteractionState, state, nextUserStimulus, pending.appliedStimulus);
|
|
255
|
+
// Record prediction
|
|
256
|
+
state = {
|
|
257
|
+
...state,
|
|
258
|
+
learning: recordPrediction(state.learning, pending.predictedChemistry, state.current, pending.appliedStimulus),
|
|
259
|
+
};
|
|
260
|
+
// Update learned vectors
|
|
261
|
+
let learningUpdated = false;
|
|
262
|
+
if (pending.appliedStimulus) {
|
|
263
|
+
state = {
|
|
264
|
+
...state,
|
|
265
|
+
learning: updateLearnedVector(state.learning, pending.appliedStimulus, pending.contextHash, outcome.adaptiveScore, state.current, state.baseline),
|
|
266
|
+
};
|
|
267
|
+
learningUpdated = true;
|
|
268
|
+
}
|
|
269
|
+
this.state = state;
|
|
270
|
+
await this.storage.save(state);
|
|
271
|
+
return { outcomeScore: outcome, learningUpdated };
|
|
272
|
+
}
|
|
188
273
|
/**
|
|
189
274
|
* Get the current psyche state (read-only snapshot).
|
|
190
275
|
*/
|
|
@@ -216,7 +301,7 @@ export class PsycheEngine {
|
|
|
216
301
|
const selfModel = getDefaultSelfModel(mbti);
|
|
217
302
|
const now = new Date().toISOString();
|
|
218
303
|
return {
|
|
219
|
-
version:
|
|
304
|
+
version: 4,
|
|
220
305
|
mbti,
|
|
221
306
|
baseline,
|
|
222
307
|
current: { ...baseline },
|
|
@@ -228,6 +313,7 @@ export class PsycheEngine {
|
|
|
228
313
|
emotionalHistory: [],
|
|
229
314
|
agreementStreak: 0,
|
|
230
315
|
lastDisagreement: null,
|
|
316
|
+
learning: { ...DEFAULT_LEARNING_STATE },
|
|
231
317
|
meta: {
|
|
232
318
|
agentName: name,
|
|
233
319
|
createdAt: now,
|