psyche-ai 2.3.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 +3 -1
- 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/index.d.ts +6 -3
- package/dist/index.js +5 -1
- package/dist/learning.d.ts +56 -0
- package/dist/learning.js +272 -0
- 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
|
@@ -161,6 +161,8 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
161
161
|
- **流式支持** — Vercel AI SDK `streamText` 中间件,自动缓冲和剥离标签
|
|
162
162
|
- **渠道修饰** — Discord/Slack/飞书/终端等不同渠道自动调整表达风格
|
|
163
163
|
- **自定义人格** — 超越 MBTI 预设,完全自定义 baseline/敏感度/气质
|
|
164
|
+
- **情绪学习** — 从交互结果中学习,调整情绪反应参数(躯体标记假说)
|
|
165
|
+
- **上下文分类** — 关系/驱力/历史感知的刺激分类,超越简单正则
|
|
164
166
|
- **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
|
|
165
167
|
|
|
166
168
|
架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
|
@@ -170,7 +172,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
170
172
|
```bash
|
|
171
173
|
npm install
|
|
172
174
|
npm run build
|
|
173
|
-
npm test #
|
|
175
|
+
npm test # 525 tests
|
|
174
176
|
npm run typecheck # strict mode
|
|
175
177
|
```
|
|
176
178
|
|
|
@@ -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,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { PsycheEngine } from "./core.js";
|
|
2
|
-
export type { PsycheEngineConfig, ProcessInputResult, ProcessOutputResult } from "./core.js";
|
|
2
|
+
export type { PsycheEngineConfig, ProcessInputResult, ProcessOutputResult, ProcessOutcomeResult } 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, DriveType, InnateDrives, } from "./types.js";
|
|
6
|
-
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
|
|
5
|
+
export type { PsycheState, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern, DriveType, InnateDrives, LearningState, LearnedVectorAdjustment, PredictionRecord, OutcomeScore, OutcomeSignals, } from "./types.js";
|
|
6
|
+
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
|
|
7
7
|
export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
|
|
8
8
|
export type { SelfReflection } from "./self-recognition.js";
|
|
9
9
|
export { PsycheInteraction } from "./interaction.js";
|
|
@@ -12,6 +12,9 @@ export { getChannelProfile, buildChannelModifier, createCustomChannel } from "./
|
|
|
12
12
|
export type { ChannelType, ChannelProfile } from "./channels.js";
|
|
13
13
|
export { createCustomProfile, validateProfileConfig, PRESET_PROFILES } from "./custom-profile.js";
|
|
14
14
|
export type { CustomProfileConfig, ResolvedProfile } from "./custom-profile.js";
|
|
15
|
+
export { evaluateOutcome, getLearnedVector, updateLearnedVector, computeContextHash, predictChemistry, computePredictionError, recordPrediction, getAveragePredictionError, } from "./learning.js";
|
|
16
|
+
export { classifyStimulusWithContext, extractContextFeatures, stimulusWarmth } from "./context-classifier.js";
|
|
17
|
+
export type { ContextFeatures, ContextualClassification } from "./context-classifier.js";
|
|
15
18
|
export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
|
|
16
19
|
export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
|
|
17
20
|
export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
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, DEFAULT_DRIVES, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
|
|
15
|
+
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
|
|
16
16
|
// Self-recognition
|
|
17
17
|
export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
|
|
18
18
|
// Multi-agent interaction
|
|
@@ -21,6 +21,10 @@ export { PsycheInteraction } from "./interaction.js";
|
|
|
21
21
|
export { getChannelProfile, buildChannelModifier, createCustomChannel } from "./channels.js";
|
|
22
22
|
// Custom profiles — beyond MBTI presets
|
|
23
23
|
export { createCustomProfile, validateProfileConfig, PRESET_PROFILES } from "./custom-profile.js";
|
|
24
|
+
// Emotional learning (P3)
|
|
25
|
+
export { evaluateOutcome, getLearnedVector, updateLearnedVector, computeContextHash, predictChemistry, computePredictionError, recordPrediction, getAveragePredictionError, } from "./learning.js";
|
|
26
|
+
// Context-aware classification (P3)
|
|
27
|
+
export { classifyStimulusWithContext, extractContextFeatures, stimulusWarmth } from "./context-classifier.js";
|
|
24
28
|
// Utilities — for custom adapter / advanced use
|
|
25
29
|
export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
|
|
26
30
|
export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PsycheState, StimulusType, ChemicalState, StimulusVector, LearningState, OutcomeScore } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Evaluate the adaptive outcome of an interaction turn.
|
|
4
|
+
*
|
|
5
|
+
* Computes a score from -1 to 1 using multiple signals:
|
|
6
|
+
* drive changes, relationship changes, user warmth, conversation continuation.
|
|
7
|
+
*/
|
|
8
|
+
export declare function evaluateOutcome(prevState: PsycheState, currentState: PsycheState, nextUserStimulus: StimulusType | null, appliedStimulus: StimulusType | null): OutcomeScore;
|
|
9
|
+
/**
|
|
10
|
+
* Get the effective stimulus vector for a given stimulus + context,
|
|
11
|
+
* combining the base vector with any learned adjustment.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getLearnedVector(learning: LearningState, stimulus: StimulusType, contextHash: string): StimulusVector;
|
|
14
|
+
/**
|
|
15
|
+
* Update a learned vector adjustment based on an outcome.
|
|
16
|
+
*
|
|
17
|
+
* Learning rule:
|
|
18
|
+
* - Positive outcome → reinforce (adjust toward actual delta)
|
|
19
|
+
* - Negative outcome → suppress (adjust away from actual delta)
|
|
20
|
+
* - Learning rate: 0.05 * |outcomeScore|
|
|
21
|
+
* - Each adjustment clamped to +/- 50% of base vector value
|
|
22
|
+
*/
|
|
23
|
+
export declare function updateLearnedVector(learning: LearningState, stimulus: StimulusType, contextHash: string, outcomeScore: number, actualChemistry: ChemicalState, baselineChemistry: ChemicalState): LearningState;
|
|
24
|
+
/**
|
|
25
|
+
* Compute a context hash from the current psyche state.
|
|
26
|
+
*
|
|
27
|
+
* Format: "{phase}:{last3stimuli}:{driveLevels}"
|
|
28
|
+
* Drive levels are encoded as h(igh)/m(id)/l(ow) for each of the 5 drives.
|
|
29
|
+
*
|
|
30
|
+
* Example: "familiar:praise,casual,intellectual:hml_hh"
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeContextHash(state: PsycheState, _userId?: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Predict the resulting chemistry after applying a stimulus,
|
|
35
|
+
* using learned vectors instead of raw base vectors.
|
|
36
|
+
*
|
|
37
|
+
* Same math as applyStimulus in chemistry.ts but with learned adjustments.
|
|
38
|
+
*/
|
|
39
|
+
export declare function predictChemistry(current: ChemicalState, stimulus: StimulusType, learning: LearningState, contextHash: string, sensitivity: number, maxDelta: number): ChemicalState;
|
|
40
|
+
/**
|
|
41
|
+
* Compute the prediction error between predicted and actual chemistry.
|
|
42
|
+
*
|
|
43
|
+
* Euclidean distance across all 6 chemicals, normalized to 0-1 range.
|
|
44
|
+
* Normalization factor: sqrt(6 * 100^2) = sqrt(60000) ~= 244.95
|
|
45
|
+
*/
|
|
46
|
+
export declare function computePredictionError(predicted: ChemicalState, actual: ChemicalState): number;
|
|
47
|
+
/**
|
|
48
|
+
* Record a prediction and its actual outcome.
|
|
49
|
+
* Pushes to predictionHistory and trims to MAX_PREDICTION_HISTORY.
|
|
50
|
+
*/
|
|
51
|
+
export declare function recordPrediction(learning: LearningState, predicted: ChemicalState, actual: ChemicalState, stimulus: StimulusType | null): LearningState;
|
|
52
|
+
/**
|
|
53
|
+
* Get the average prediction error over recent history.
|
|
54
|
+
* Returns 1.0 if no history exists (maximum uncertainty).
|
|
55
|
+
*/
|
|
56
|
+
export declare function getAveragePredictionError(learning: LearningState): number;
|
package/dist/learning.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Emotional Learning Engine — Damasio's Somatic Marker Hypothesis
|
|
3
|
+
//
|
|
4
|
+
// The system learns from interaction outcomes which emotional
|
|
5
|
+
// reactions are adaptive. Over time, stimulus→chemistry mappings
|
|
6
|
+
// shift based on what actually worked in context.
|
|
7
|
+
//
|
|
8
|
+
// Components:
|
|
9
|
+
// 1. OutcomeEvaluator — scores interaction outcomes
|
|
10
|
+
// 2. StimulusVectorStore — learned vector adjustments
|
|
11
|
+
// 3. PredictionEngine — predict & track prediction error
|
|
12
|
+
// ============================================================
|
|
13
|
+
import { CHEMICAL_KEYS, DRIVE_KEYS, MAX_LEARNED_VECTORS, MAX_PREDICTION_HISTORY, } from "./types.js";
|
|
14
|
+
import { STIMULUS_VECTORS, clamp } from "./chemistry.js";
|
|
15
|
+
// ── 1. OutcomeEvaluator ─────────────────────────────────────
|
|
16
|
+
/** Warmth mapping for nextUserStimulus */
|
|
17
|
+
const WARMTH_MAP = {
|
|
18
|
+
praise: 0.8,
|
|
19
|
+
validation: 0.7,
|
|
20
|
+
intimacy: 0.9,
|
|
21
|
+
humor: 0.5,
|
|
22
|
+
casual: 0,
|
|
23
|
+
intellectual: 0.2,
|
|
24
|
+
surprise: 0.3,
|
|
25
|
+
vulnerability: 0.4,
|
|
26
|
+
criticism: -0.6,
|
|
27
|
+
conflict: -0.8,
|
|
28
|
+
neglect: -0.9,
|
|
29
|
+
sarcasm: -0.5,
|
|
30
|
+
authority: -0.4,
|
|
31
|
+
boredom: -0.3,
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate the adaptive outcome of an interaction turn.
|
|
35
|
+
*
|
|
36
|
+
* Computes a score from -1 to 1 using multiple signals:
|
|
37
|
+
* drive changes, relationship changes, user warmth, conversation continuation.
|
|
38
|
+
*/
|
|
39
|
+
export function evaluateOutcome(prevState, currentState, nextUserStimulus, appliedStimulus) {
|
|
40
|
+
// Drive delta: sum of all drive changes, normalized
|
|
41
|
+
let driveSum = 0;
|
|
42
|
+
for (const key of DRIVE_KEYS) {
|
|
43
|
+
driveSum += currentState.drives[key] - prevState.drives[key];
|
|
44
|
+
}
|
|
45
|
+
const driveDelta = Math.max(-1, Math.min(1, driveSum / 50));
|
|
46
|
+
// Relationship delta: change in trust + intimacy of _default relationship
|
|
47
|
+
const prevRel = prevState.relationships._default ?? { trust: 50, intimacy: 30 };
|
|
48
|
+
const curRel = currentState.relationships._default ?? { trust: 50, intimacy: 30 };
|
|
49
|
+
const relChange = (curRel.trust - prevRel.trust) + (curRel.intimacy - prevRel.intimacy);
|
|
50
|
+
const relationshipDelta = Math.max(-1, Math.min(1, relChange / 20));
|
|
51
|
+
// User warmth: what the user said next
|
|
52
|
+
const userWarmthDelta = nextUserStimulus !== null ? (WARMTH_MAP[nextUserStimulus] ?? 0) : 0;
|
|
53
|
+
// Conversation continued
|
|
54
|
+
const conversationContinued = nextUserStimulus !== null;
|
|
55
|
+
// Weighted average
|
|
56
|
+
const continuedBonus = conversationContinued ? 0.15 : -0.15;
|
|
57
|
+
const raw = driveDelta * 0.25
|
|
58
|
+
+ relationshipDelta * 0.25
|
|
59
|
+
+ userWarmthDelta * 0.35
|
|
60
|
+
+ continuedBonus;
|
|
61
|
+
const adaptiveScore = Math.max(-1, Math.min(1, raw));
|
|
62
|
+
const signals = {
|
|
63
|
+
driveDelta,
|
|
64
|
+
relationshipDelta,
|
|
65
|
+
userWarmthDelta,
|
|
66
|
+
conversationContinued,
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
turnIndex: currentState.meta.totalInteractions,
|
|
70
|
+
stimulus: appliedStimulus,
|
|
71
|
+
adaptiveScore,
|
|
72
|
+
signals,
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// ── 2. StimulusVectorStore ──────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Get the effective stimulus vector for a given stimulus + context,
|
|
79
|
+
* combining the base vector with any learned adjustment.
|
|
80
|
+
*/
|
|
81
|
+
export function getLearnedVector(learning, stimulus, contextHash) {
|
|
82
|
+
const base = STIMULUS_VECTORS[stimulus];
|
|
83
|
+
if (!base) {
|
|
84
|
+
// Unknown stimulus — return zeros
|
|
85
|
+
return { DA: 0, HT: 0, CORT: 0, OT: 0, NE: 0, END: 0 };
|
|
86
|
+
}
|
|
87
|
+
// Look for a learned adjustment matching this stimulus + context
|
|
88
|
+
const entry = learning.learnedVectors.find((v) => v.stimulus === stimulus && v.contextHash === contextHash);
|
|
89
|
+
if (!entry)
|
|
90
|
+
return { ...base };
|
|
91
|
+
// Apply adjustment
|
|
92
|
+
const result = { ...base };
|
|
93
|
+
for (const key of CHEMICAL_KEYS) {
|
|
94
|
+
const adj = entry.adjustment[key] ?? 0;
|
|
95
|
+
result[key] = base[key] + adj;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Update a learned vector adjustment based on an outcome.
|
|
101
|
+
*
|
|
102
|
+
* Learning rule:
|
|
103
|
+
* - Positive outcome → reinforce (adjust toward actual delta)
|
|
104
|
+
* - Negative outcome → suppress (adjust away from actual delta)
|
|
105
|
+
* - Learning rate: 0.05 * |outcomeScore|
|
|
106
|
+
* - Each adjustment clamped to +/- 50% of base vector value
|
|
107
|
+
*/
|
|
108
|
+
export function updateLearnedVector(learning, stimulus, contextHash, outcomeScore, actualChemistry, baselineChemistry) {
|
|
109
|
+
const base = STIMULUS_VECTORS[stimulus];
|
|
110
|
+
if (!base)
|
|
111
|
+
return learning;
|
|
112
|
+
// Chemistry delta: what actually happened
|
|
113
|
+
const chemDelta = {};
|
|
114
|
+
for (const key of CHEMICAL_KEYS) {
|
|
115
|
+
chemDelta[key] = actualChemistry[key] - baselineChemistry[key];
|
|
116
|
+
}
|
|
117
|
+
// Learning rate: conservative, proportional to outcome strength
|
|
118
|
+
const learningRate = 0.05 * Math.abs(outcomeScore);
|
|
119
|
+
const direction = outcomeScore >= 0 ? 1 : -1;
|
|
120
|
+
// Find or create entry
|
|
121
|
+
const existingIdx = learning.learnedVectors.findIndex((v) => v.stimulus === stimulus && v.contextHash === contextHash);
|
|
122
|
+
let entry;
|
|
123
|
+
if (existingIdx >= 0) {
|
|
124
|
+
entry = { ...learning.learnedVectors[existingIdx] };
|
|
125
|
+
entry.adjustment = { ...entry.adjustment };
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
entry = {
|
|
129
|
+
stimulus,
|
|
130
|
+
contextHash,
|
|
131
|
+
adjustment: {},
|
|
132
|
+
confidence: 0,
|
|
133
|
+
sampleCount: 0,
|
|
134
|
+
lastUpdated: new Date().toISOString(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Update adjustment for each chemical
|
|
138
|
+
for (const key of CHEMICAL_KEYS) {
|
|
139
|
+
const currentAdj = entry.adjustment[key] ?? 0;
|
|
140
|
+
const delta = chemDelta[key] * direction * learningRate;
|
|
141
|
+
let newAdj = currentAdj + delta;
|
|
142
|
+
// Clamp to +/- 50% of base vector absolute value
|
|
143
|
+
const baseAbs = Math.abs(base[key]);
|
|
144
|
+
const maxAdj = Math.max(baseAbs * 0.5, 1); // at least 1 to allow learning on zero-base values
|
|
145
|
+
newAdj = Math.max(-maxAdj, Math.min(maxAdj, newAdj));
|
|
146
|
+
entry.adjustment[key] = newAdj;
|
|
147
|
+
}
|
|
148
|
+
// Update metadata
|
|
149
|
+
entry.sampleCount += 1;
|
|
150
|
+
entry.confidence = 0.9 * entry.confidence + 0.1 * Math.abs(outcomeScore);
|
|
151
|
+
entry.lastUpdated = new Date().toISOString();
|
|
152
|
+
// Build new vectors array
|
|
153
|
+
let newVectors;
|
|
154
|
+
if (existingIdx >= 0) {
|
|
155
|
+
newVectors = [...learning.learnedVectors];
|
|
156
|
+
newVectors[existingIdx] = entry;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
newVectors = [...learning.learnedVectors, entry];
|
|
160
|
+
}
|
|
161
|
+
// Trim to MAX_LEARNED_VECTORS: keep highest sampleCount entries
|
|
162
|
+
if (newVectors.length > MAX_LEARNED_VECTORS) {
|
|
163
|
+
newVectors.sort((a, b) => b.sampleCount - a.sampleCount);
|
|
164
|
+
newVectors = newVectors.slice(0, MAX_LEARNED_VECTORS);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
...learning,
|
|
168
|
+
learnedVectors: newVectors,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Compute a context hash from the current psyche state.
|
|
173
|
+
*
|
|
174
|
+
* Format: "{phase}:{last3stimuli}:{driveLevels}"
|
|
175
|
+
* Drive levels are encoded as h(igh)/m(id)/l(ow) for each of the 5 drives.
|
|
176
|
+
*
|
|
177
|
+
* Example: "familiar:praise,casual,intellectual:hml_hh"
|
|
178
|
+
*/
|
|
179
|
+
export function computeContextHash(state, _userId) {
|
|
180
|
+
// Relationship phase
|
|
181
|
+
const rel = state.relationships._default ?? { phase: "stranger" };
|
|
182
|
+
const phase = rel.phase;
|
|
183
|
+
// Last 3 stimuli from emotional history
|
|
184
|
+
const history = state.emotionalHistory ?? [];
|
|
185
|
+
const recentStimuli = history
|
|
186
|
+
.slice(-3)
|
|
187
|
+
.map((s) => s.stimulus ?? "none")
|
|
188
|
+
.join(",");
|
|
189
|
+
// Drive satisfaction levels: h(igh >=67), m(id 34-66), l(ow <34)
|
|
190
|
+
const driveLevels = [];
|
|
191
|
+
for (const key of DRIVE_KEYS) {
|
|
192
|
+
const val = state.drives[key];
|
|
193
|
+
if (val >= 67)
|
|
194
|
+
driveLevels.push("h");
|
|
195
|
+
else if (val >= 34)
|
|
196
|
+
driveLevels.push("m");
|
|
197
|
+
else
|
|
198
|
+
driveLevels.push("l");
|
|
199
|
+
}
|
|
200
|
+
// Format: separate safety from the rest for readability
|
|
201
|
+
// survival_safety_connection_esteem_curiosity
|
|
202
|
+
const driveStr = driveLevels.join("");
|
|
203
|
+
return `${phase}:${recentStimuli || "none"}:${driveStr}`;
|
|
204
|
+
}
|
|
205
|
+
// ── 3. PredictionEngine ─────────────────────────────────────
|
|
206
|
+
/**
|
|
207
|
+
* Predict the resulting chemistry after applying a stimulus,
|
|
208
|
+
* using learned vectors instead of raw base vectors.
|
|
209
|
+
*
|
|
210
|
+
* Same math as applyStimulus in chemistry.ts but with learned adjustments.
|
|
211
|
+
*/
|
|
212
|
+
export function predictChemistry(current, stimulus, learning, contextHash, sensitivity, maxDelta) {
|
|
213
|
+
const vector = getLearnedVector(learning, stimulus, contextHash);
|
|
214
|
+
const result = { ...current };
|
|
215
|
+
for (const key of CHEMICAL_KEYS) {
|
|
216
|
+
const raw = vector[key] * sensitivity;
|
|
217
|
+
const clamped = Math.max(-maxDelta, Math.min(maxDelta, raw));
|
|
218
|
+
result[key] = clamp(current[key] + clamped);
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Compute the prediction error between predicted and actual chemistry.
|
|
224
|
+
*
|
|
225
|
+
* Euclidean distance across all 6 chemicals, normalized to 0-1 range.
|
|
226
|
+
* Normalization factor: sqrt(6 * 100^2) = sqrt(60000) ~= 244.95
|
|
227
|
+
*/
|
|
228
|
+
export function computePredictionError(predicted, actual) {
|
|
229
|
+
let sumSq = 0;
|
|
230
|
+
for (const key of CHEMICAL_KEYS) {
|
|
231
|
+
const diff = predicted[key] - actual[key];
|
|
232
|
+
sumSq += diff * diff;
|
|
233
|
+
}
|
|
234
|
+
const maxDistance = Math.sqrt(6 * 100 * 100); // ~244.95
|
|
235
|
+
return Math.sqrt(sumSq) / maxDistance;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Record a prediction and its actual outcome.
|
|
239
|
+
* Pushes to predictionHistory and trims to MAX_PREDICTION_HISTORY.
|
|
240
|
+
*/
|
|
241
|
+
export function recordPrediction(learning, predicted, actual, stimulus) {
|
|
242
|
+
const error = computePredictionError(predicted, actual);
|
|
243
|
+
const record = {
|
|
244
|
+
predictedChemistry: { ...predicted },
|
|
245
|
+
actualChemistry: { ...actual },
|
|
246
|
+
stimulus,
|
|
247
|
+
predictionError: error,
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
};
|
|
250
|
+
let newHistory = [...learning.predictionHistory, record];
|
|
251
|
+
if (newHistory.length > MAX_PREDICTION_HISTORY) {
|
|
252
|
+
newHistory = newHistory.slice(newHistory.length - MAX_PREDICTION_HISTORY);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
...learning,
|
|
256
|
+
predictionHistory: newHistory,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// ── 4. Utility ──────────────────────────────────────────────
|
|
260
|
+
/**
|
|
261
|
+
* Get the average prediction error over recent history.
|
|
262
|
+
* Returns 1.0 if no history exists (maximum uncertainty).
|
|
263
|
+
*/
|
|
264
|
+
export function getAveragePredictionError(learning) {
|
|
265
|
+
if (learning.predictionHistory.length === 0)
|
|
266
|
+
return 1.0;
|
|
267
|
+
let sum = 0;
|
|
268
|
+
for (const record of learning.predictionHistory) {
|
|
269
|
+
sum += record.predictionError;
|
|
270
|
+
}
|
|
271
|
+
return sum / learning.predictionHistory.length;
|
|
272
|
+
}
|
package/dist/psyche-file.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// ============================================================
|
|
5
5
|
import { readFile, writeFile, access, rename, constants } from "node:fs/promises";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, MAX_EMOTIONAL_HISTORY, MAX_RELATIONSHIP_MEMORY, } from "./types.js";
|
|
7
|
+
import { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, MAX_EMOTIONAL_HISTORY, MAX_RELATIONSHIP_MEMORY, } from "./types.js";
|
|
8
8
|
import { getBaseline, getDefaultSelfModel, extractMBTI, getSensitivity, getTemperament } from "./profiles.js";
|
|
9
9
|
import { applyDecay, detectEmotions } from "./chemistry.js";
|
|
10
10
|
import { decayDrives, computeEffectiveBaseline } from "./drives.js";
|
|
@@ -240,10 +240,12 @@ export function migrateToLatest(raw, fallbackName) {
|
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
242
|
// v2→v3: add drives
|
|
243
|
+
// v3→v4: add learning
|
|
243
244
|
return {
|
|
244
245
|
...state,
|
|
245
|
-
version:
|
|
246
|
+
version: 4,
|
|
246
247
|
drives: state.drives ?? { ...DEFAULT_DRIVES },
|
|
248
|
+
learning: state.learning ?? { ...DEFAULT_LEARNING_STATE },
|
|
247
249
|
};
|
|
248
250
|
}
|
|
249
251
|
/**
|
|
@@ -257,7 +259,7 @@ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER)
|
|
|
257
259
|
const selfModel = getDefaultSelfModel(mbti);
|
|
258
260
|
const now = new Date().toISOString();
|
|
259
261
|
const state = {
|
|
260
|
-
version:
|
|
262
|
+
version: 4,
|
|
261
263
|
mbti,
|
|
262
264
|
baseline,
|
|
263
265
|
current: { ...baseline },
|
|
@@ -271,6 +273,7 @@ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER)
|
|
|
271
273
|
emotionalHistory: [],
|
|
272
274
|
agreementStreak: 0,
|
|
273
275
|
lastDisagreement: null,
|
|
276
|
+
learning: { ...DEFAULT_LEARNING_STATE },
|
|
274
277
|
meta: {
|
|
275
278
|
agentName,
|
|
276
279
|
createdAt: now,
|
package/dist/types.d.ts
CHANGED
|
@@ -81,9 +81,56 @@ export interface SelfModel {
|
|
|
81
81
|
boundaries: string[];
|
|
82
82
|
currentInterests: string[];
|
|
83
83
|
}
|
|
84
|
-
/**
|
|
84
|
+
/** Learned adjustment to a stimulus vector for a specific context */
|
|
85
|
+
export interface LearnedVectorAdjustment {
|
|
86
|
+
stimulus: StimulusType;
|
|
87
|
+
contextHash: string;
|
|
88
|
+
adjustment: Partial<StimulusVector>;
|
|
89
|
+
confidence: number;
|
|
90
|
+
sampleCount: number;
|
|
91
|
+
lastUpdated: string;
|
|
92
|
+
}
|
|
93
|
+
/** A single prediction record for prediction error tracking */
|
|
94
|
+
export interface PredictionRecord {
|
|
95
|
+
predictedChemistry: ChemicalState;
|
|
96
|
+
actualChemistry: ChemicalState;
|
|
97
|
+
stimulus: StimulusType | null;
|
|
98
|
+
predictionError: number;
|
|
99
|
+
timestamp: string;
|
|
100
|
+
}
|
|
101
|
+
/** Outcome evaluation signals for a turn */
|
|
102
|
+
export interface OutcomeSignals {
|
|
103
|
+
driveDelta: number;
|
|
104
|
+
relationshipDelta: number;
|
|
105
|
+
userWarmthDelta: number;
|
|
106
|
+
conversationContinued: boolean;
|
|
107
|
+
}
|
|
108
|
+
/** Outcome evaluation for a single interaction turn */
|
|
109
|
+
export interface OutcomeScore {
|
|
110
|
+
turnIndex: number;
|
|
111
|
+
stimulus: StimulusType | null;
|
|
112
|
+
adaptiveScore: number;
|
|
113
|
+
signals: OutcomeSignals;
|
|
114
|
+
timestamp: string;
|
|
115
|
+
}
|
|
116
|
+
/** Persisted learning state */
|
|
117
|
+
export interface LearningState {
|
|
118
|
+
learnedVectors: LearnedVectorAdjustment[];
|
|
119
|
+
predictionHistory: PredictionRecord[];
|
|
120
|
+
outcomeHistory: OutcomeScore[];
|
|
121
|
+
totalOutcomesProcessed: number;
|
|
122
|
+
}
|
|
123
|
+
/** Default empty learning state */
|
|
124
|
+
export declare const DEFAULT_LEARNING_STATE: LearningState;
|
|
125
|
+
/** Max learned vector entries */
|
|
126
|
+
export declare const MAX_LEARNED_VECTORS = 200;
|
|
127
|
+
/** Max prediction history entries */
|
|
128
|
+
export declare const MAX_PREDICTION_HISTORY = 50;
|
|
129
|
+
/** Max outcome history entries */
|
|
130
|
+
export declare const MAX_OUTCOME_HISTORY = 50;
|
|
131
|
+
/** Persisted psyche state for an agent (v4: emotional learning) */
|
|
85
132
|
export interface PsycheState {
|
|
86
|
-
version: 3;
|
|
133
|
+
version: 3 | 4;
|
|
87
134
|
mbti: MBTIType;
|
|
88
135
|
baseline: ChemicalState;
|
|
89
136
|
current: ChemicalState;
|
|
@@ -95,6 +142,7 @@ export interface PsycheState {
|
|
|
95
142
|
emotionalHistory: ChemicalSnapshot[];
|
|
96
143
|
agreementStreak: number;
|
|
97
144
|
lastDisagreement: string | null;
|
|
145
|
+
learning: LearningState;
|
|
98
146
|
meta: {
|
|
99
147
|
agentName: string;
|
|
100
148
|
createdAt: string;
|
package/dist/types.js
CHANGED
|
@@ -60,6 +60,19 @@ export const CHEMICAL_DECAY_SPEED = {
|
|
|
60
60
|
export const MAX_EMOTIONAL_HISTORY = 10;
|
|
61
61
|
/** Max compressed session memories per relationship */
|
|
62
62
|
export const MAX_RELATIONSHIP_MEMORY = 20;
|
|
63
|
+
/** Default empty learning state */
|
|
64
|
+
export const DEFAULT_LEARNING_STATE = {
|
|
65
|
+
learnedVectors: [],
|
|
66
|
+
predictionHistory: [],
|
|
67
|
+
outcomeHistory: [],
|
|
68
|
+
totalOutcomesProcessed: 0,
|
|
69
|
+
};
|
|
70
|
+
/** Max learned vector entries */
|
|
71
|
+
export const MAX_LEARNED_VECTORS = 200;
|
|
72
|
+
/** Max prediction history entries */
|
|
73
|
+
export const MAX_PREDICTION_HISTORY = 50;
|
|
74
|
+
/** Max outcome history entries */
|
|
75
|
+
export const MAX_OUTCOME_HISTORY = 50;
|
|
63
76
|
/** Default relationship for new users */
|
|
64
77
|
export const DEFAULT_RELATIONSHIP = {
|
|
65
78
|
trust: 50,
|
package/dist/update.js
CHANGED
|
@@ -11,7 +11,7 @@ import { execFile } from "node:child_process";
|
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
const PACKAGE_NAME = "psyche-ai";
|
|
14
|
-
const CURRENT_VERSION = "
|
|
14
|
+
const CURRENT_VERSION = "3.0.0";
|
|
15
15
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
16
16
|
const CACHE_DIR = join(homedir(), ".psyche-ai");
|
|
17
17
|
const CACHE_FILE = join(CACHE_DIR, "update-check.json");
|
package/openclaw.plugin.json
CHANGED