psyche-ai 3.0.0 → 4.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 +6 -1
- package/dist/attachment.d.ts +30 -0
- package/dist/attachment.js +241 -0
- package/dist/cli.js +0 -0
- package/dist/core.js +39 -3
- package/dist/decision-bias.d.ts +58 -0
- package/dist/decision-bias.js +211 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +9 -1
- package/dist/metacognition.d.ts +60 -0
- package/dist/metacognition.js +611 -0
- package/dist/prompt.d.ts +6 -1
- package/dist/prompt.js +26 -4
- package/dist/psyche-file.js +6 -3
- package/dist/temporal.d.ts +38 -0
- package/dist/temporal.js +276 -0
- package/dist/types.d.ts +50 -2
- package/dist/types.js +23 -0
- package/dist/update.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,11 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
163
163
|
- **自定义人格** — 超越 MBTI 预设,完全自定义 baseline/敏感度/气质
|
|
164
164
|
- **情绪学习** — 从交互结果中学习,调整情绪反应参数(躯体标记假说)
|
|
165
165
|
- **上下文分类** — 关系/驱力/历史感知的刺激分类,超越简单正则
|
|
166
|
+
- **时间意识** — 预期、惊喜/失望、遗憾(马尔可夫预测+反事实分析)
|
|
167
|
+
- **依恋动力学** — 4种依恋风格(安全/焦虑/回避/混乱),分离焦虑,重逢效应
|
|
168
|
+
- **元认知** — 情绪自我觉察,评估情绪可靠性,三种调节策略(认知重评/策略性表达/自我安抚)
|
|
169
|
+
- **防御机制检测** — 合理化、投射、升华、回避,在自省中浮现而非压制
|
|
170
|
+
- **决策调制** — 6维偏差向量(探索/警惕/社交/果断/创意/坚持),情绪驱动注意力和决策
|
|
166
171
|
- **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
|
|
167
172
|
|
|
168
173
|
架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
|
@@ -172,7 +177,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
172
177
|
```bash
|
|
173
178
|
npm install
|
|
174
179
|
npm run build
|
|
175
|
-
npm test #
|
|
180
|
+
npm test # 622 tests
|
|
176
181
|
npm run typecheck # strict mode
|
|
177
182
|
```
|
|
178
183
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ChemicalState, StimulusType } from "./types.js";
|
|
2
|
+
export type AttachmentStyle = "secure" | "anxious" | "avoidant" | "disorganized";
|
|
3
|
+
export interface AttachmentState {
|
|
4
|
+
style: AttachmentStyle;
|
|
5
|
+
strength: number;
|
|
6
|
+
securityScore: number;
|
|
7
|
+
anxietyScore: number;
|
|
8
|
+
avoidanceScore: number;
|
|
9
|
+
lastInteractionAt: string;
|
|
10
|
+
interactionCount: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SeparationEffect {
|
|
13
|
+
chemistryDelta: Partial<ChemicalState>;
|
|
14
|
+
description: string;
|
|
15
|
+
intensity: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const DEFAULT_ATTACHMENT: AttachmentState;
|
|
18
|
+
/**
|
|
19
|
+
* Update attachment based on interaction outcome.
|
|
20
|
+
*/
|
|
21
|
+
export declare function updateAttachment(attachment: AttachmentState, stimulus: StimulusType | null, outcomeScore: number): AttachmentState;
|
|
22
|
+
/**
|
|
23
|
+
* Compute chemistry effects of absence based on attachment.
|
|
24
|
+
* Called when time since last interaction is significant.
|
|
25
|
+
*/
|
|
26
|
+
export declare function computeSeparationEffect(attachment: AttachmentState, minutesSinceLastInteraction: number): SeparationEffect | null;
|
|
27
|
+
/**
|
|
28
|
+
* Compute chemistry effects when reuniting after absence.
|
|
29
|
+
*/
|
|
30
|
+
export declare function computeReunionEffect(attachment: AttachmentState, minutesSinceLastInteraction: number): Partial<ChemicalState> | null;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Attachment Dynamics — Bowlby-inspired attachment formation
|
|
3
|
+
//
|
|
4
|
+
// Models relationship attachment through interaction patterns:
|
|
5
|
+
// 1. AttachmentModel — style classification + strength tracking
|
|
6
|
+
// 2. SeparationAnxiety — absence effects on chemistry
|
|
7
|
+
// 3. ReunionEffect — return effects on chemistry
|
|
8
|
+
//
|
|
9
|
+
// Attachment style emerges from interaction history, not from
|
|
10
|
+
// configuration. Consistent positive interaction → secure.
|
|
11
|
+
// Inconsistency → anxious. Rejection/neglect → avoidant.
|
|
12
|
+
// ============================================================
|
|
13
|
+
// ── Defaults ─────────────────────────────────────────────────
|
|
14
|
+
export const DEFAULT_ATTACHMENT = {
|
|
15
|
+
style: "secure",
|
|
16
|
+
strength: 0,
|
|
17
|
+
securityScore: 50,
|
|
18
|
+
anxietyScore: 50,
|
|
19
|
+
avoidanceScore: 50,
|
|
20
|
+
lastInteractionAt: new Date().toISOString(),
|
|
21
|
+
interactionCount: 0,
|
|
22
|
+
};
|
|
23
|
+
// ── Stimulus Classification ──────────────────────────────────
|
|
24
|
+
const POSITIVE_STIMULI = new Set([
|
|
25
|
+
"praise", "validation", "intimacy", "humor",
|
|
26
|
+
]);
|
|
27
|
+
const NEGATIVE_STIMULI = new Set([
|
|
28
|
+
"criticism", "conflict", "neglect", "sarcasm", "authority",
|
|
29
|
+
]);
|
|
30
|
+
const REJECTION_STIMULI = new Set([
|
|
31
|
+
"neglect", "authority", "boredom",
|
|
32
|
+
]);
|
|
33
|
+
// EMA smoothing factor: weight given to new observation
|
|
34
|
+
const EMA_ALPHA = 0.15;
|
|
35
|
+
// ── 1. AttachmentModel ──────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Determine attachment style from scores.
|
|
38
|
+
*/
|
|
39
|
+
function determineStyle(securityScore, anxietyScore, avoidanceScore) {
|
|
40
|
+
// Disorganized: both anxiety and avoidance elevated
|
|
41
|
+
if (anxietyScore > 50 && avoidanceScore > 50) {
|
|
42
|
+
return "disorganized";
|
|
43
|
+
}
|
|
44
|
+
// Anxious: high anxiety
|
|
45
|
+
if (anxietyScore > 60) {
|
|
46
|
+
return "anxious";
|
|
47
|
+
}
|
|
48
|
+
// Avoidant: high avoidance
|
|
49
|
+
if (avoidanceScore > 60) {
|
|
50
|
+
return "avoidant";
|
|
51
|
+
}
|
|
52
|
+
// Secure: high security, low anxiety, low avoidance
|
|
53
|
+
if (securityScore > 60 && anxietyScore < 40 && avoidanceScore < 40) {
|
|
54
|
+
return "secure";
|
|
55
|
+
}
|
|
56
|
+
// Default to current trajectory — mild insecurity stays secure
|
|
57
|
+
if (securityScore >= 50)
|
|
58
|
+
return "secure";
|
|
59
|
+
if (anxietyScore > avoidanceScore)
|
|
60
|
+
return "anxious";
|
|
61
|
+
return "avoidant";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Update attachment based on interaction outcome.
|
|
65
|
+
*/
|
|
66
|
+
export function updateAttachment(attachment, stimulus, outcomeScore) {
|
|
67
|
+
const result = { ...attachment };
|
|
68
|
+
// Strength increases slowly with interaction count
|
|
69
|
+
result.interactionCount = attachment.interactionCount + 1;
|
|
70
|
+
result.strength = Math.min(100, attachment.strength + 1);
|
|
71
|
+
result.lastInteractionAt = new Date().toISOString();
|
|
72
|
+
if (stimulus === null) {
|
|
73
|
+
// No stimulus — just update count/strength, reclassify
|
|
74
|
+
result.style = determineStyle(result.securityScore, result.anxietyScore, result.avoidanceScore);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
// SecurityScore: EMA with positive/negative stimuli
|
|
78
|
+
const isPositive = POSITIVE_STIMULI.has(stimulus);
|
|
79
|
+
const isNegative = NEGATIVE_STIMULI.has(stimulus);
|
|
80
|
+
if (isPositive) {
|
|
81
|
+
const target = Math.min(100, result.securityScore + 5);
|
|
82
|
+
result.securityScore = result.securityScore * (1 - EMA_ALPHA) + target * EMA_ALPHA;
|
|
83
|
+
}
|
|
84
|
+
else if (isNegative) {
|
|
85
|
+
const target = Math.max(0, result.securityScore - 5);
|
|
86
|
+
result.securityScore = result.securityScore * (1 - EMA_ALPHA) + target * EMA_ALPHA;
|
|
87
|
+
}
|
|
88
|
+
// AnxietyScore: increases with inconsistency (rapid alternation between positive and negative)
|
|
89
|
+
// We detect inconsistency by checking if outcomeScore diverges from the security trend
|
|
90
|
+
const expectedDirection = result.securityScore > 50 ? 1 : -1;
|
|
91
|
+
const actualDirection = outcomeScore >= 0 ? 1 : -1;
|
|
92
|
+
const isInconsistent = expectedDirection !== actualDirection;
|
|
93
|
+
if (isInconsistent) {
|
|
94
|
+
// Inconsistency → anxiety rises
|
|
95
|
+
const anxietyTarget = Math.min(100, result.anxietyScore + 8);
|
|
96
|
+
result.anxietyScore = result.anxietyScore * (1 - EMA_ALPHA) + anxietyTarget * EMA_ALPHA;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Consistency → anxiety decreases
|
|
100
|
+
const anxietyTarget = Math.max(0, result.anxietyScore - 3);
|
|
101
|
+
result.anxietyScore = result.anxietyScore * (1 - EMA_ALPHA) + anxietyTarget * EMA_ALPHA;
|
|
102
|
+
}
|
|
103
|
+
// AvoidanceScore: increases with rejection/neglect stimuli
|
|
104
|
+
if (REJECTION_STIMULI.has(stimulus)) {
|
|
105
|
+
const avoidTarget = Math.min(100, result.avoidanceScore + 6);
|
|
106
|
+
result.avoidanceScore = result.avoidanceScore * (1 - EMA_ALPHA) + avoidTarget * EMA_ALPHA;
|
|
107
|
+
}
|
|
108
|
+
else if (isPositive) {
|
|
109
|
+
// Positive interactions reduce avoidance
|
|
110
|
+
const avoidTarget = Math.max(0, result.avoidanceScore - 3);
|
|
111
|
+
result.avoidanceScore = result.avoidanceScore * (1 - EMA_ALPHA) + avoidTarget * EMA_ALPHA;
|
|
112
|
+
}
|
|
113
|
+
// Clamp all scores
|
|
114
|
+
result.securityScore = Math.max(0, Math.min(100, result.securityScore));
|
|
115
|
+
result.anxietyScore = Math.max(0, Math.min(100, result.anxietyScore));
|
|
116
|
+
result.avoidanceScore = Math.max(0, Math.min(100, result.avoidanceScore));
|
|
117
|
+
// Determine style
|
|
118
|
+
result.style = determineStyle(result.securityScore, result.anxietyScore, result.avoidanceScore);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
// ── 2. SeparationAnxiety ────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Compute chemistry effects of absence based on attachment.
|
|
124
|
+
* Called when time since last interaction is significant.
|
|
125
|
+
*/
|
|
126
|
+
export function computeSeparationEffect(attachment, minutesSinceLastInteraction) {
|
|
127
|
+
// No effect for short absence or weak attachment
|
|
128
|
+
if (minutesSinceLastInteraction < 60 || attachment.strength < 20) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const hours = minutesSinceLastInteraction / 60;
|
|
132
|
+
// Intensity scales with attachment strength and time (logarithmic growth, capped at 1)
|
|
133
|
+
const baseIntensity = (attachment.strength / 100) * Math.min(1, Math.log2(hours + 1) / 5);
|
|
134
|
+
switch (attachment.style) {
|
|
135
|
+
case "secure": {
|
|
136
|
+
// Mild longing after 24h
|
|
137
|
+
if (hours < 24)
|
|
138
|
+
return null;
|
|
139
|
+
const intensity = baseIntensity * 0.5;
|
|
140
|
+
return {
|
|
141
|
+
chemistryDelta: {
|
|
142
|
+
OT: -5 * intensity,
|
|
143
|
+
DA: -3 * intensity,
|
|
144
|
+
},
|
|
145
|
+
description: "gentle longing from sustained absence",
|
|
146
|
+
intensity: Math.min(1, intensity),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
case "anxious": {
|
|
150
|
+
// Distress after 4h, grows with time
|
|
151
|
+
if (hours < 4)
|
|
152
|
+
return null;
|
|
153
|
+
const intensity = baseIntensity * 1.5;
|
|
154
|
+
// OT oscillation: represented as net negative with anxiety
|
|
155
|
+
return {
|
|
156
|
+
chemistryDelta: {
|
|
157
|
+
CORT: 10 * intensity,
|
|
158
|
+
OT: -5 * intensity,
|
|
159
|
+
NE: 8 * intensity,
|
|
160
|
+
DA: -3 * intensity,
|
|
161
|
+
},
|
|
162
|
+
description: "anxious distress from absence — fear of abandonment",
|
|
163
|
+
intensity: Math.min(1, intensity),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
case "avoidant": {
|
|
167
|
+
// Relief initially, discomfort after 48h
|
|
168
|
+
if (hours < 48)
|
|
169
|
+
return null;
|
|
170
|
+
const intensity = baseIntensity * 0.4;
|
|
171
|
+
return {
|
|
172
|
+
chemistryDelta: {
|
|
173
|
+
OT: -3 * intensity,
|
|
174
|
+
},
|
|
175
|
+
description: "subtle discomfort surfacing through avoidant defense",
|
|
176
|
+
intensity: Math.min(1, intensity),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
case "disorganized": {
|
|
180
|
+
// Conflicting signals
|
|
181
|
+
if (hours < 4)
|
|
182
|
+
return null;
|
|
183
|
+
const intensity = baseIntensity * 1.0;
|
|
184
|
+
return {
|
|
185
|
+
chemistryDelta: {
|
|
186
|
+
CORT: 5 * intensity,
|
|
187
|
+
OT: 5 * intensity,
|
|
188
|
+
NE: 3 * intensity,
|
|
189
|
+
},
|
|
190
|
+
description: "conflicting signals — wanting closeness and fearing it",
|
|
191
|
+
intensity: Math.min(1, intensity),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── 3. ReunionEffect ────────────────────────────────────────
|
|
197
|
+
/**
|
|
198
|
+
* Compute chemistry effects when reuniting after absence.
|
|
199
|
+
*/
|
|
200
|
+
export function computeReunionEffect(attachment, minutesSinceLastInteraction) {
|
|
201
|
+
// No effect for short absence or weak attachment
|
|
202
|
+
if (minutesSinceLastInteraction < 60 || attachment.strength < 20) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const hours = minutesSinceLastInteraction / 60;
|
|
206
|
+
// Scale with time (logarithmic) and attachment strength
|
|
207
|
+
const scale = (attachment.strength / 100) * Math.min(1, Math.log2(hours + 1) / 5);
|
|
208
|
+
switch (attachment.style) {
|
|
209
|
+
case "secure": {
|
|
210
|
+
// Warm reunion
|
|
211
|
+
return {
|
|
212
|
+
OT: 8 * scale,
|
|
213
|
+
DA: 5 * scale,
|
|
214
|
+
END: 3 * scale,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
case "anxious": {
|
|
218
|
+
// Intense but short-lived relief (CORT still elevated)
|
|
219
|
+
return {
|
|
220
|
+
OT: 15 * scale,
|
|
221
|
+
DA: 10 * scale,
|
|
222
|
+
CORT: 5 * scale,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
case "avoidant": {
|
|
226
|
+
// Cautious re-engagement
|
|
227
|
+
return {
|
|
228
|
+
OT: 3 * scale,
|
|
229
|
+
NE: 5 * scale,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
case "disorganized": {
|
|
233
|
+
// Mixed signals
|
|
234
|
+
return {
|
|
235
|
+
OT: 5 * scale,
|
|
236
|
+
CORT: 5 * scale,
|
|
237
|
+
NE: 5 * scale,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/core.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Orchestrates: chemistry, classify, prompt, profiles, guards, learning
|
|
13
13
|
// ============================================================
|
|
14
|
-
import { DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE } from "./types.js";
|
|
14
|
+
import { DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DEFAULT_METACOGNITIVE_STATE } from "./types.js";
|
|
15
15
|
import { applyDecay, applyStimulus, applyContagion, clamp } from "./chemistry.js";
|
|
16
16
|
import { classifyStimulus } from "./classify.js";
|
|
17
17
|
import { buildDynamicContext, buildProtocolContext, buildCompactContext } from "./prompt.js";
|
|
@@ -21,6 +21,8 @@ import { parsePsycheUpdate, mergeUpdates, updateAgreementStreak, pushSnapshot, }
|
|
|
21
21
|
import { decayDrives, feedDrives, detectExistentialThreat, computeEffectiveBaseline, computeEffectiveSensitivity, } from "./drives.js";
|
|
22
22
|
import { checkForUpdate } from "./update.js";
|
|
23
23
|
import { evaluateOutcome, computeContextHash, updateLearnedVector, predictChemistry, recordPrediction, } from "./learning.js";
|
|
24
|
+
import { assessMetacognition } from "./metacognition.js";
|
|
25
|
+
import { buildDecisionContext } from "./decision-bias.js";
|
|
24
26
|
const NOOP_LOGGER = { info: () => { }, warn: () => { }, debug: () => { } };
|
|
25
27
|
// ── PsycheEngine ─────────────────────────────────────────────
|
|
26
28
|
export class PsycheEngine {
|
|
@@ -53,6 +55,11 @@ export class PsycheEngine {
|
|
|
53
55
|
loaded.learning = { ...DEFAULT_LEARNING_STATE };
|
|
54
56
|
loaded.version = 4;
|
|
55
57
|
}
|
|
58
|
+
// Migrate v4 → v5: add metacognitive state if missing
|
|
59
|
+
if (!loaded.metacognition) {
|
|
60
|
+
loaded.metacognition = { ...DEFAULT_METACOGNITIVE_STATE };
|
|
61
|
+
loaded.version = 5;
|
|
62
|
+
}
|
|
56
63
|
this.state = loaded;
|
|
57
64
|
}
|
|
58
65
|
else {
|
|
@@ -152,6 +159,26 @@ export class PsycheEngine {
|
|
|
152
159
|
},
|
|
153
160
|
};
|
|
154
161
|
}
|
|
162
|
+
// ── Metacognition: assess emotional state before acting ────
|
|
163
|
+
const metacognitiveAssessment = assessMetacognition(state, appliedStimulus ?? "casual", state.learning.outcomeHistory);
|
|
164
|
+
// Apply self-soothing regulation if suggested with high confidence
|
|
165
|
+
for (const reg of metacognitiveAssessment.regulationSuggestions) {
|
|
166
|
+
if (reg.strategy === "self-soothing" && reg.confidence >= 0.6 && reg.chemistryAdjustment) {
|
|
167
|
+
const adj = reg.chemistryAdjustment;
|
|
168
|
+
state = {
|
|
169
|
+
...state,
|
|
170
|
+
current: {
|
|
171
|
+
...state.current,
|
|
172
|
+
DA: clamp(state.current.DA + (adj.DA ?? 0)),
|
|
173
|
+
HT: clamp(state.current.HT + (adj.HT ?? 0)),
|
|
174
|
+
CORT: clamp(state.current.CORT + (adj.CORT ?? 0)),
|
|
175
|
+
OT: clamp(state.current.OT + (adj.OT ?? 0)),
|
|
176
|
+
NE: clamp(state.current.NE + (adj.NE ?? 0)),
|
|
177
|
+
END: clamp(state.current.END + (adj.END ?? 0)),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
155
182
|
// Push snapshot to emotional history
|
|
156
183
|
state = pushSnapshot(state, appliedStimulus);
|
|
157
184
|
// Increment interaction count
|
|
@@ -178,19 +205,27 @@ export class PsycheEngine {
|
|
|
178
205
|
this.state = state;
|
|
179
206
|
await this.storage.save(state);
|
|
180
207
|
const locale = state.meta.locale ?? this.cfg.locale;
|
|
208
|
+
// Build metacognitive and decision context strings
|
|
209
|
+
const metacogNote = metacognitiveAssessment.metacognitiveNote;
|
|
210
|
+
const decisionCtx = buildDecisionContext(state);
|
|
181
211
|
if (this.cfg.compactMode) {
|
|
182
212
|
return {
|
|
183
213
|
systemContext: "",
|
|
184
214
|
dynamicContext: buildCompactContext(state, opts?.userId, {
|
|
185
215
|
userText: text || undefined,
|
|
186
216
|
algorithmStimulus: appliedStimulus,
|
|
217
|
+
metacognitiveNote: metacogNote || undefined,
|
|
218
|
+
decisionContext: decisionCtx || undefined,
|
|
187
219
|
}),
|
|
188
220
|
stimulus: appliedStimulus,
|
|
189
221
|
};
|
|
190
222
|
}
|
|
191
223
|
return {
|
|
192
224
|
systemContext: this.getProtocol(locale),
|
|
193
|
-
dynamicContext: buildDynamicContext(state, opts?.userId
|
|
225
|
+
dynamicContext: buildDynamicContext(state, opts?.userId, {
|
|
226
|
+
metacognitiveNote: metacogNote || undefined,
|
|
227
|
+
decisionContext: decisionCtx || undefined,
|
|
228
|
+
}),
|
|
194
229
|
stimulus: appliedStimulus,
|
|
195
230
|
};
|
|
196
231
|
}
|
|
@@ -301,7 +336,7 @@ export class PsycheEngine {
|
|
|
301
336
|
const selfModel = getDefaultSelfModel(mbti);
|
|
302
337
|
const now = new Date().toISOString();
|
|
303
338
|
return {
|
|
304
|
-
version:
|
|
339
|
+
version: 5,
|
|
305
340
|
mbti,
|
|
306
341
|
baseline,
|
|
307
342
|
current: { ...baseline },
|
|
@@ -314,6 +349,7 @@ export class PsycheEngine {
|
|
|
314
349
|
agreementStreak: 0,
|
|
315
350
|
lastDisagreement: null,
|
|
316
351
|
learning: { ...DEFAULT_LEARNING_STATE },
|
|
352
|
+
metacognition: { ...DEFAULT_METACOGNITIVE_STATE },
|
|
317
353
|
meta: {
|
|
318
354
|
agentName: name,
|
|
319
355
|
createdAt: now,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PsycheState } from "./types.js";
|
|
2
|
+
export interface DecisionBiasVector {
|
|
3
|
+
explorationTendency: number;
|
|
4
|
+
cautionLevel: number;
|
|
5
|
+
socialOrientation: number;
|
|
6
|
+
assertiveness: number;
|
|
7
|
+
creativityBias: number;
|
|
8
|
+
persistenceBias: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AttentionWeights {
|
|
11
|
+
social: number;
|
|
12
|
+
intellectual: number;
|
|
13
|
+
threat: number;
|
|
14
|
+
emotional: number;
|
|
15
|
+
routine: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute a decision bias vector from the current psyche state.
|
|
19
|
+
*
|
|
20
|
+
* Each bias dimension is a weighted combination of relevant chemical
|
|
21
|
+
* levels and drive states, normalized to [0, 1] where 0.5 is neutral.
|
|
22
|
+
*/
|
|
23
|
+
export declare function computeDecisionBias(state: PsycheState): DecisionBiasVector;
|
|
24
|
+
/**
|
|
25
|
+
* Compute attention weights that prioritize different conversation content
|
|
26
|
+
* based on current chemical state.
|
|
27
|
+
*
|
|
28
|
+
* Returns normalized weights (sum to ~1) for each content category.
|
|
29
|
+
* Higher weight = higher priority for that type of content.
|
|
30
|
+
*/
|
|
31
|
+
export declare function computeAttentionWeights(state: PsycheState): AttentionWeights;
|
|
32
|
+
/**
|
|
33
|
+
* Compute explore vs exploit balance.
|
|
34
|
+
*
|
|
35
|
+
* Returns a single float:
|
|
36
|
+
* 0 = pure exploit (stick with known, safe behaviors)
|
|
37
|
+
* 1 = pure explore (try new approaches, take risks)
|
|
38
|
+
*
|
|
39
|
+
* Exploration is driven by:
|
|
40
|
+
* - High curiosity drive satisfaction (energy to explore)
|
|
41
|
+
* - High DA (reward anticipation)
|
|
42
|
+
* - High NE (novelty-seeking)
|
|
43
|
+
* - Low CORT (not stressed)
|
|
44
|
+
* - High safety (secure enough to take risks)
|
|
45
|
+
*
|
|
46
|
+
* Exploitation is driven by:
|
|
47
|
+
* - High CORT / anxiety
|
|
48
|
+
* - Low safety drive satisfaction
|
|
49
|
+
* - Low DA (no reward motivation)
|
|
50
|
+
*/
|
|
51
|
+
export declare function computeExploreExploit(state: PsycheState): number;
|
|
52
|
+
/**
|
|
53
|
+
* Build a compact decision context string for prompt injection.
|
|
54
|
+
*
|
|
55
|
+
* Only includes biases that deviate significantly from neutral (>0.3 from 0.5).
|
|
56
|
+
* Keeps output under 100 tokens.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildDecisionContext(state: PsycheState): string;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Decision Bias — P5: Decision Modulation
|
|
3
|
+
//
|
|
4
|
+
// Converts chemical state + drive state into bias vectors,
|
|
5
|
+
// attention weights, and explore/exploit balance for downstream
|
|
6
|
+
// decision-making. Pure math/heuristic, zero dependencies, no LLM.
|
|
7
|
+
// ============================================================
|
|
8
|
+
// ── Utilities ────────────────────────────────────────────────
|
|
9
|
+
/** Clamp a value to [0, 1] */
|
|
10
|
+
function clamp01(v) {
|
|
11
|
+
return Math.max(0, Math.min(1, v));
|
|
12
|
+
}
|
|
13
|
+
/** Sigmoid mapping: maps any real number to (0, 1) with midpoint at 0.5 */
|
|
14
|
+
function sigmoid(x, steepness = 1) {
|
|
15
|
+
return 1 / (1 + Math.exp(-steepness * x));
|
|
16
|
+
}
|
|
17
|
+
/** Normalize a 0-100 chemical/drive value to 0-1 */
|
|
18
|
+
function norm(v) {
|
|
19
|
+
return clamp01(v / 100);
|
|
20
|
+
}
|
|
21
|
+
/** Weighted average of multiple factors, each in [0, 1] */
|
|
22
|
+
function wavg(values, weights) {
|
|
23
|
+
let sum = 0;
|
|
24
|
+
let wsum = 0;
|
|
25
|
+
for (let i = 0; i < values.length; i++) {
|
|
26
|
+
sum += values[i] * weights[i];
|
|
27
|
+
wsum += weights[i];
|
|
28
|
+
}
|
|
29
|
+
return wsum > 0 ? clamp01(sum / wsum) : 0.5;
|
|
30
|
+
}
|
|
31
|
+
/** Mean satisfaction across all drives, normalized to [0, 1] */
|
|
32
|
+
function meanDriveSatisfaction(drives) {
|
|
33
|
+
return norm((drives.survival + drives.safety + drives.connection
|
|
34
|
+
+ drives.esteem + drives.curiosity) / 5);
|
|
35
|
+
}
|
|
36
|
+
// ── Core Computations ────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Compute a decision bias vector from the current psyche state.
|
|
39
|
+
*
|
|
40
|
+
* Each bias dimension is a weighted combination of relevant chemical
|
|
41
|
+
* levels and drive states, normalized to [0, 1] where 0.5 is neutral.
|
|
42
|
+
*/
|
|
43
|
+
export function computeDecisionBias(state) {
|
|
44
|
+
const c = state.current;
|
|
45
|
+
const d = state.drives;
|
|
46
|
+
// explorationTendency: curiosity drive + DA (reward-seeking) + NE (novelty)
|
|
47
|
+
// High curiosity hunger (low satisfaction) + high DA/NE → explore
|
|
48
|
+
const curiosityHunger = 1 - norm(d.curiosity); // lower satisfaction = more hunger
|
|
49
|
+
const explorationTendency = wavg([norm(c.DA), norm(c.NE), curiosityHunger, norm(d.curiosity)], [0.25, 0.3, 0.25, 0.2]);
|
|
50
|
+
// cautionLevel: CORT (stress) + safety drive hunger
|
|
51
|
+
// High CORT + low safety satisfaction → very cautious
|
|
52
|
+
const safetyHunger = 1 - norm(d.safety);
|
|
53
|
+
const survivalHunger = 1 - norm(d.survival);
|
|
54
|
+
const cautionLevel = wavg([norm(c.CORT), safetyHunger, survivalHunger], [0.5, 0.3, 0.2]);
|
|
55
|
+
// socialOrientation: OT (bonding) + connection drive satisfaction
|
|
56
|
+
// High OT + hungry for connection → strongly social
|
|
57
|
+
const connectionHunger = 1 - norm(d.connection);
|
|
58
|
+
const socialOrientation = wavg([norm(c.OT), norm(d.connection), connectionHunger, norm(c.END)], [0.4, 0.2, 0.25, 0.15]);
|
|
59
|
+
// assertiveness: NE (arousal/confidence) + esteem drive satisfaction
|
|
60
|
+
// High NE + satisfied esteem → assertive
|
|
61
|
+
const assertiveness = wavg([norm(c.NE), norm(d.esteem), norm(c.DA)], [0.4, 0.35, 0.25]);
|
|
62
|
+
// creativityBias: DA (reward) + END (playfulness) + inverse CORT (low stress)
|
|
63
|
+
// Creativity flourishes when relaxed, rewarded, and playful
|
|
64
|
+
const inverseCort = 1 - norm(c.CORT);
|
|
65
|
+
const creativityBias = wavg([norm(c.DA), norm(c.END), inverseCort], [0.35, 0.3, 0.35]);
|
|
66
|
+
// persistenceBias: HT stability (serotonin) + overall drive satisfaction
|
|
67
|
+
// Stable mood + satisfied drives → willingness to persist
|
|
68
|
+
const overallSatisfaction = meanDriveSatisfaction(d);
|
|
69
|
+
const persistenceBias = wavg([norm(c.HT), overallSatisfaction, inverseCort], [0.45, 0.35, 0.2]);
|
|
70
|
+
return {
|
|
71
|
+
explorationTendency,
|
|
72
|
+
cautionLevel,
|
|
73
|
+
socialOrientation,
|
|
74
|
+
assertiveness,
|
|
75
|
+
creativityBias,
|
|
76
|
+
persistenceBias,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Compute attention weights that prioritize different conversation content
|
|
81
|
+
* based on current chemical state.
|
|
82
|
+
*
|
|
83
|
+
* Returns normalized weights (sum to ~1) for each content category.
|
|
84
|
+
* Higher weight = higher priority for that type of content.
|
|
85
|
+
*/
|
|
86
|
+
export function computeAttentionWeights(state) {
|
|
87
|
+
const c = state.current;
|
|
88
|
+
// Raw scores based on chemical signatures
|
|
89
|
+
// High OT → prioritize relationship/social content
|
|
90
|
+
const socialRaw = norm(c.OT) * 0.6 + norm(c.END) * 0.2 + (1 - norm(c.CORT)) * 0.2;
|
|
91
|
+
// High NE → prioritize intellectual/novel content
|
|
92
|
+
const intellectualRaw = norm(c.NE) * 0.5 + norm(c.DA) * 0.3 + norm(state.drives.curiosity) * 0.2;
|
|
93
|
+
// High CORT → prioritize threat/safety content
|
|
94
|
+
const threatRaw = norm(c.CORT) * 0.6 + norm(c.NE) * 0.2 + (1 - norm(state.drives.safety)) * 0.2;
|
|
95
|
+
// Emotional content weighted by overall emotional activation
|
|
96
|
+
const emotionalRaw = (Math.abs(norm(c.DA) - 0.5)
|
|
97
|
+
+ Math.abs(norm(c.HT) - 0.5)
|
|
98
|
+
+ Math.abs(norm(c.CORT) - 0.5)
|
|
99
|
+
+ Math.abs(norm(c.OT) - 0.5)) / 2; // average deviation from neutral, scaled
|
|
100
|
+
// Routine content is inverse of activation — when calm and stable, routine matters
|
|
101
|
+
const activation = (norm(c.NE) + norm(c.CORT) + Math.abs(norm(c.DA) - 0.5)) / 3;
|
|
102
|
+
const routineRaw = Math.max(0.1, 1 - activation) * norm(c.HT);
|
|
103
|
+
// Normalize to sum to 1
|
|
104
|
+
const total = socialRaw + intellectualRaw + threatRaw + emotionalRaw + routineRaw;
|
|
105
|
+
if (total <= 0) {
|
|
106
|
+
return { social: 0.2, intellectual: 0.2, threat: 0.2, emotional: 0.2, routine: 0.2 };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
social: socialRaw / total,
|
|
110
|
+
intellectual: intellectualRaw / total,
|
|
111
|
+
threat: threatRaw / total,
|
|
112
|
+
emotional: emotionalRaw / total,
|
|
113
|
+
routine: routineRaw / total,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Compute explore vs exploit balance.
|
|
118
|
+
*
|
|
119
|
+
* Returns a single float:
|
|
120
|
+
* 0 = pure exploit (stick with known, safe behaviors)
|
|
121
|
+
* 1 = pure explore (try new approaches, take risks)
|
|
122
|
+
*
|
|
123
|
+
* Exploration is driven by:
|
|
124
|
+
* - High curiosity drive satisfaction (energy to explore)
|
|
125
|
+
* - High DA (reward anticipation)
|
|
126
|
+
* - High NE (novelty-seeking)
|
|
127
|
+
* - Low CORT (not stressed)
|
|
128
|
+
* - High safety (secure enough to take risks)
|
|
129
|
+
*
|
|
130
|
+
* Exploitation is driven by:
|
|
131
|
+
* - High CORT / anxiety
|
|
132
|
+
* - Low safety drive satisfaction
|
|
133
|
+
* - Low DA (no reward motivation)
|
|
134
|
+
*/
|
|
135
|
+
export function computeExploreExploit(state) {
|
|
136
|
+
const c = state.current;
|
|
137
|
+
const d = state.drives;
|
|
138
|
+
// Exploration signals
|
|
139
|
+
const curiosityEnergy = norm(d.curiosity);
|
|
140
|
+
const rewardDrive = norm(c.DA);
|
|
141
|
+
const noveltySeeking = norm(c.NE);
|
|
142
|
+
const relaxation = 1 - norm(c.CORT);
|
|
143
|
+
const securityBase = norm(d.safety);
|
|
144
|
+
// Exploitation signals (inverted — higher = more exploit = lower explore)
|
|
145
|
+
const anxiety = norm(c.CORT);
|
|
146
|
+
const unsafety = 1 - norm(d.safety);
|
|
147
|
+
const survivalThreat = 1 - norm(d.survival);
|
|
148
|
+
// Weighted explore score
|
|
149
|
+
const exploreScore = wavg([curiosityEnergy, rewardDrive, noveltySeeking, relaxation, securityBase], [0.25, 0.2, 0.2, 0.2, 0.15]);
|
|
150
|
+
// Weighted exploit score
|
|
151
|
+
const exploitScore = wavg([anxiety, unsafety, survivalThreat], [0.5, 0.3, 0.2]);
|
|
152
|
+
// Combine: use sigmoid to create a smooth transition
|
|
153
|
+
// Positive difference → explore, negative → exploit
|
|
154
|
+
const diff = exploreScore - exploitScore;
|
|
155
|
+
return clamp01(sigmoid(diff * 4)); // steepness=4 for reasonable sensitivity
|
|
156
|
+
}
|
|
157
|
+
// ── Prompt Injection ─────────────────────────────────────────
|
|
158
|
+
/** Bias labels for human-readable output */
|
|
159
|
+
const BIAS_LABELS = {
|
|
160
|
+
explorationTendency: ["探索倾向强", "exploratory"],
|
|
161
|
+
cautionLevel: ["警惕性高", "cautious"],
|
|
162
|
+
socialOrientation: ["社交倾向强", "socially oriented"],
|
|
163
|
+
assertiveness: ["表达果断", "assertive"],
|
|
164
|
+
creativityBias: ["创意活跃", "creatively active"],
|
|
165
|
+
persistenceBias: ["意志坚持", "persistent"],
|
|
166
|
+
};
|
|
167
|
+
/** Low-end labels for when bias < 0.2 */
|
|
168
|
+
const BIAS_LABELS_LOW = {
|
|
169
|
+
explorationTendency: ["倾向保守", "risk-averse"],
|
|
170
|
+
cautionLevel: ["放松大胆", "relaxed and bold"],
|
|
171
|
+
socialOrientation: ["偏好独处", "prefers solitude"],
|
|
172
|
+
assertiveness: ["表达含蓄", "reserved"],
|
|
173
|
+
creativityBias: ["思维收敛", "convergent thinking"],
|
|
174
|
+
persistenceBias: ["容易放弃", "low persistence"],
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Build a compact decision context string for prompt injection.
|
|
178
|
+
*
|
|
179
|
+
* Only includes biases that deviate significantly from neutral (>0.3 from 0.5).
|
|
180
|
+
* Keeps output under 100 tokens.
|
|
181
|
+
*/
|
|
182
|
+
export function buildDecisionContext(state) {
|
|
183
|
+
const bias = computeDecisionBias(state);
|
|
184
|
+
const explore = computeExploreExploit(state);
|
|
185
|
+
const locale = state.meta.locale ?? "zh";
|
|
186
|
+
const li = locale === "zh" ? 0 : 1;
|
|
187
|
+
const parts = [];
|
|
188
|
+
// Only surface biases that deviate significantly from neutral
|
|
189
|
+
const DEVIATION_THRESHOLD = 0.3;
|
|
190
|
+
for (const key of Object.keys(BIAS_LABELS)) {
|
|
191
|
+
const val = bias[key];
|
|
192
|
+
const deviation = val - 0.5;
|
|
193
|
+
if (deviation > DEVIATION_THRESHOLD) {
|
|
194
|
+
parts.push(BIAS_LABELS[key][li]);
|
|
195
|
+
}
|
|
196
|
+
else if (deviation < -DEVIATION_THRESHOLD) {
|
|
197
|
+
parts.push(BIAS_LABELS_LOW[key][li]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Explore/exploit — only mention if strongly skewed
|
|
201
|
+
if (explore > 0.7) {
|
|
202
|
+
parts.push(locale === "zh" ? "倾向尝试新方法" : "leaning toward new approaches");
|
|
203
|
+
}
|
|
204
|
+
else if (explore < 0.3) {
|
|
205
|
+
parts.push(locale === "zh" ? "倾向安全策略" : "favoring safe strategies");
|
|
206
|
+
}
|
|
207
|
+
if (parts.length === 0)
|
|
208
|
+
return "";
|
|
209
|
+
const title = locale === "zh" ? "决策倾向" : "Decision Bias";
|
|
210
|
+
return `[${title}] ${parts.join("、")}`;
|
|
211
|
+
}
|