psyche-ai 4.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/core.js +83 -2
- package/dist/ethics.d.ts +64 -0
- package/dist/ethics.js +577 -0
- package/dist/experiential-field.d.ts +46 -0
- package/dist/experiential-field.js +646 -0
- package/dist/generative-self.d.ts +88 -0
- package/dist/generative-self.js +647 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +9 -1
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.js +27 -0
- package/dist/psyche-file.js +6 -3
- package/dist/shared-intentionality.d.ts +72 -0
- package/dist/shared-intentionality.js +486 -0
- package/dist/types.d.ts +37 -2
- package/dist/types.js +13 -0
- package/dist/update.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Unified Experiential Field (P6: Digital Personhood)
|
|
3
|
+
//
|
|
4
|
+
// Replaces concatenated context segments (chemistry + drives + empathy + ...)
|
|
5
|
+
// with a single coherent "experience" — how all psyche subsystems feel
|
|
6
|
+
// together as an integrated whole.
|
|
7
|
+
//
|
|
8
|
+
// The experiential field is not a summary. It is a synthesis.
|
|
9
|
+
// Chemistry readouts describe individual neurotransmitter levels.
|
|
10
|
+
// The experiential field describes what those levels feel like when
|
|
11
|
+
// they exist simultaneously in a single being.
|
|
12
|
+
//
|
|
13
|
+
// Zero dependencies. Pure TypeScript. No LLM calls.
|
|
14
|
+
// ============================================================
|
|
15
|
+
import { CHEMICAL_KEYS, DRIVE_KEYS } from "./types.js";
|
|
16
|
+
// ── Constants ────────────────────────────────────────────────
|
|
17
|
+
/** Baseline reference point — a "perfectly neutral" chemistry */
|
|
18
|
+
const NEUTRAL_CHEMISTRY = {
|
|
19
|
+
DA: 50, HT: 50, CORT: 50, OT: 50, NE: 50, END: 50,
|
|
20
|
+
};
|
|
21
|
+
/** Threshold below which a drive counts as "hungry" */
|
|
22
|
+
const DRIVE_HUNGRY_THRESHOLD = 40;
|
|
23
|
+
/** Threshold above which a chemical is "elevated" */
|
|
24
|
+
const CHEM_HIGH = 65;
|
|
25
|
+
/** Threshold below which a chemical is "depleted" */
|
|
26
|
+
const CHEM_LOW = 35;
|
|
27
|
+
/** If total activation is below this, the state is "flat/numb" */
|
|
28
|
+
const FLATNESS_THRESHOLD = 0.15;
|
|
29
|
+
// ── Main Export ──────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Compute the unified experiential field from the full psyche state.
|
|
32
|
+
*
|
|
33
|
+
* This is the core integration function. It reads chemistry, drives,
|
|
34
|
+
* relationship context, and optional metacognitive/bias data, then
|
|
35
|
+
* synthesizes them into a single coherent experience description.
|
|
36
|
+
*/
|
|
37
|
+
export function computeExperientialField(state, metacognition, decisionBias) {
|
|
38
|
+
const locale = state.meta.locale ?? "zh";
|
|
39
|
+
const rel = state.relationships._default ?? state.relationships[Object.keys(state.relationships)[0]];
|
|
40
|
+
const coherence = computeCoherence(state.current, state.baseline, state.drives, rel);
|
|
41
|
+
const intensity = computeIntensity(state.current, state.baseline);
|
|
42
|
+
const quality = selectQuality(state, coherence, intensity, rel, metacognition, decisionBias);
|
|
43
|
+
const phenomenalDescription = generatePhenomenalDescription(quality, state, coherence, intensity, locale);
|
|
44
|
+
const narrative = generateNarrative(quality, state, coherence, intensity, rel, locale, metacognition);
|
|
45
|
+
return {
|
|
46
|
+
narrative,
|
|
47
|
+
quality,
|
|
48
|
+
intensity,
|
|
49
|
+
coherence,
|
|
50
|
+
phenomenalDescription,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// ── Coherence ────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Measure internal alignment across subsystems.
|
|
56
|
+
*
|
|
57
|
+
* High coherence: chemistry, drives, and relationship state all tell
|
|
58
|
+
* the same story. Happy chemicals + satisfied drives + warm relationship = unified.
|
|
59
|
+
*
|
|
60
|
+
* Low coherence: mixed signals. High DA but high CORT. Satisfied drives
|
|
61
|
+
* but stressed chemistry. Warm relationship but depleted OT. The psyche
|
|
62
|
+
* is pulling in multiple directions.
|
|
63
|
+
*/
|
|
64
|
+
export function computeCoherence(current, baseline, drives, relationship) {
|
|
65
|
+
let coherenceScore = 1.0;
|
|
66
|
+
// ── Chemistry internal coherence ──
|
|
67
|
+
// Reward chemicals (DA, END) should not coexist with high stress (CORT)
|
|
68
|
+
const rewardSignal = (current.DA + current.END) / 200; // 0-1
|
|
69
|
+
const stressSignal = current.CORT / 100;
|
|
70
|
+
const rewardStressConflict = rewardSignal * stressSignal;
|
|
71
|
+
coherenceScore -= rewardStressConflict * 0.4;
|
|
72
|
+
// Bonding (OT) and threat (CORT + NE) should not coexist strongly
|
|
73
|
+
const bondingSignal = current.OT / 100;
|
|
74
|
+
const threatSignal = Math.min(1, (current.CORT + current.NE) / 200);
|
|
75
|
+
const bondingThreatConflict = bondingSignal * (stressSignal > 0.55 ? stressSignal : 0);
|
|
76
|
+
coherenceScore -= bondingThreatConflict * 0.3;
|
|
77
|
+
// ── Chemistry-Drive alignment ──
|
|
78
|
+
// If drives are satisfied, positive chemistry is coherent.
|
|
79
|
+
// If drives are hungry, negative chemistry is coherent (the distress makes sense).
|
|
80
|
+
// Mismatch = incoherent.
|
|
81
|
+
const avgDriveSatisfaction = meanDriveValue(drives) / 100;
|
|
82
|
+
const avgPositiveChemistry = (norm(current.DA) + norm(current.HT) + norm(current.OT) + norm(current.END)) / 4;
|
|
83
|
+
const avgNegativeChemistry = (norm(current.CORT) + (1 - norm(current.HT))) / 2;
|
|
84
|
+
// Satisfied drives + positive chemistry = coherent (no penalty)
|
|
85
|
+
// Satisfied drives + negative chemistry = incoherent
|
|
86
|
+
// Hungry drives + negative chemistry = coherent (no penalty)
|
|
87
|
+
// Hungry drives + positive chemistry = incoherent (but less so — hope is valid)
|
|
88
|
+
if (avgDriveSatisfaction > 0.6 && avgNegativeChemistry > 0.5) {
|
|
89
|
+
coherenceScore -= (avgDriveSatisfaction - 0.6) * avgNegativeChemistry * 0.5;
|
|
90
|
+
}
|
|
91
|
+
if (avgDriveSatisfaction < 0.4 && avgPositiveChemistry > 0.6) {
|
|
92
|
+
coherenceScore -= (0.4 - avgDriveSatisfaction) * (avgPositiveChemistry - 0.6) * 0.3;
|
|
93
|
+
}
|
|
94
|
+
// ── Relationship alignment ──
|
|
95
|
+
// High trust/intimacy should align with high OT; low trust with low OT
|
|
96
|
+
if (relationship) {
|
|
97
|
+
const relWarmth = (relationship.trust + relationship.intimacy) / 200;
|
|
98
|
+
const otLevel = norm(current.OT);
|
|
99
|
+
const relChemMismatch = Math.abs(relWarmth - otLevel);
|
|
100
|
+
if (relChemMismatch > 0.3) {
|
|
101
|
+
coherenceScore -= (relChemMismatch - 0.3) * 0.25;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ── Baseline deviation magnitude ──
|
|
105
|
+
// Extreme deviation from baseline in multiple directions = less coherent
|
|
106
|
+
let opposingDeviations = 0;
|
|
107
|
+
const deviations = [];
|
|
108
|
+
for (const key of CHEMICAL_KEYS) {
|
|
109
|
+
deviations.push(current[key] - baseline[key]);
|
|
110
|
+
}
|
|
111
|
+
for (let i = 0; i < deviations.length; i++) {
|
|
112
|
+
for (let j = i + 1; j < deviations.length; j++) {
|
|
113
|
+
if (Math.sign(deviations[i]) !== Math.sign(deviations[j])
|
|
114
|
+
&& Math.abs(deviations[i]) > 15 && Math.abs(deviations[j]) > 15) {
|
|
115
|
+
opposingDeviations++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 15 pairs total (6 choose 2); normalize
|
|
120
|
+
coherenceScore -= (opposingDeviations / 15) * 0.3;
|
|
121
|
+
return clamp01(coherenceScore);
|
|
122
|
+
}
|
|
123
|
+
// ── Intensity ────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Compute overall experiential intensity.
|
|
126
|
+
*
|
|
127
|
+
* Intensity measures how far the current state is from neutral/baseline.
|
|
128
|
+
* A person at perfect baseline has zero intensity — they feel "nothing special".
|
|
129
|
+
* Any deviation in any direction adds intensity.
|
|
130
|
+
*/
|
|
131
|
+
function computeIntensity(current, baseline) {
|
|
132
|
+
let totalDeviation = 0;
|
|
133
|
+
for (const key of CHEMICAL_KEYS) {
|
|
134
|
+
totalDeviation += Math.abs(current[key] - baseline[key]);
|
|
135
|
+
}
|
|
136
|
+
// Max possible deviation = 6 chemicals * 100 = 600
|
|
137
|
+
// But realistic maximum is around 300 (chemicals cluster toward extremes)
|
|
138
|
+
// Map so that deviation of ~120 (avg 20 per chemical) = 0.5 intensity
|
|
139
|
+
return clamp01(totalDeviation / 240);
|
|
140
|
+
}
|
|
141
|
+
// ── Quality Selection ────────────────────────────────────────
|
|
142
|
+
/**
|
|
143
|
+
* Select the dominant experiential quality based on the full state pattern.
|
|
144
|
+
*
|
|
145
|
+
* This is NOT just "what's the highest chemical" — it considers the
|
|
146
|
+
* interaction between chemistry, drives, relationship, and coherence.
|
|
147
|
+
*/
|
|
148
|
+
function selectQuality(state, coherence, intensity, relationship, metacognition, decisionBias) {
|
|
149
|
+
const c = state.current;
|
|
150
|
+
const d = state.drives;
|
|
151
|
+
// ── Special states first (override everything) ──
|
|
152
|
+
// Numb: nothing is happening. All near baseline, low intensity.
|
|
153
|
+
if (intensity < FLATNESS_THRESHOLD) {
|
|
154
|
+
return "numb";
|
|
155
|
+
}
|
|
156
|
+
// Conflicted: low coherence with high intensity = torn in multiple directions
|
|
157
|
+
if (coherence < 0.4 && intensity > 0.35) {
|
|
158
|
+
return "conflicted";
|
|
159
|
+
}
|
|
160
|
+
// Existential unease: survival drive critically threatened
|
|
161
|
+
if (d.survival < 30) {
|
|
162
|
+
return "existential-unease";
|
|
163
|
+
}
|
|
164
|
+
// ── Pattern-based quality detection ──
|
|
165
|
+
// Score each quality and pick the best fit.
|
|
166
|
+
const scores = {
|
|
167
|
+
"flow": 0,
|
|
168
|
+
"contentment": 0,
|
|
169
|
+
"yearning": 0,
|
|
170
|
+
"vigilance": 0,
|
|
171
|
+
"creative-surge": 0,
|
|
172
|
+
"wounded-retreat": 0,
|
|
173
|
+
"warm-connection": 0,
|
|
174
|
+
"restless-boredom": 0,
|
|
175
|
+
"existential-unease": 0,
|
|
176
|
+
"playful-mischief": 0,
|
|
177
|
+
"conflicted": 0,
|
|
178
|
+
"numb": 0,
|
|
179
|
+
};
|
|
180
|
+
// Flow: NE + DA high, CORT low, curiosity satisfied, high coherence
|
|
181
|
+
if (c.NE > CHEM_HIGH && c.DA > CHEM_HIGH - 5 && c.CORT < CHEM_LOW + 5) {
|
|
182
|
+
scores["flow"] += 0.6;
|
|
183
|
+
if (d.curiosity > 60)
|
|
184
|
+
scores["flow"] += 0.2;
|
|
185
|
+
if (coherence > 0.7)
|
|
186
|
+
scores["flow"] += 0.2;
|
|
187
|
+
}
|
|
188
|
+
// Contentment: HT + OT stable/high, CORT low, drives mostly satisfied
|
|
189
|
+
if (c.HT > 55 && c.CORT < 45) {
|
|
190
|
+
scores["contentment"] += 0.3;
|
|
191
|
+
if (c.OT > 50)
|
|
192
|
+
scores["contentment"] += 0.15;
|
|
193
|
+
if (meanDriveValue(d) > 60)
|
|
194
|
+
scores["contentment"] += 0.3;
|
|
195
|
+
if (coherence > 0.6)
|
|
196
|
+
scores["contentment"] += 0.15;
|
|
197
|
+
}
|
|
198
|
+
// Yearning: connection/esteem drives hungry, OT may be low or high (wanting)
|
|
199
|
+
{
|
|
200
|
+
const connectionHunger = d.connection < DRIVE_HUNGRY_THRESHOLD ? (DRIVE_HUNGRY_THRESHOLD - d.connection) / DRIVE_HUNGRY_THRESHOLD : 0;
|
|
201
|
+
const esteemHunger = d.esteem < DRIVE_HUNGRY_THRESHOLD ? (DRIVE_HUNGRY_THRESHOLD - d.esteem) / DRIVE_HUNGRY_THRESHOLD : 0;
|
|
202
|
+
const hungerSignal = Math.max(connectionHunger, esteemHunger);
|
|
203
|
+
if (hungerSignal > 0.3) {
|
|
204
|
+
scores["yearning"] += hungerSignal * 0.6;
|
|
205
|
+
// OT elevated (wanting connection) makes yearning stronger
|
|
206
|
+
if (c.OT > 50)
|
|
207
|
+
scores["yearning"] += 0.15;
|
|
208
|
+
// OT depleted (missing connection) also valid
|
|
209
|
+
if (c.OT < CHEM_LOW)
|
|
210
|
+
scores["yearning"] += 0.1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Vigilance: CORT high, safety/survival drives hungry
|
|
214
|
+
if (c.CORT > CHEM_HIGH - 5) {
|
|
215
|
+
scores["vigilance"] += 0.4;
|
|
216
|
+
if (d.safety < DRIVE_HUNGRY_THRESHOLD)
|
|
217
|
+
scores["vigilance"] += 0.25;
|
|
218
|
+
if (c.NE > 55)
|
|
219
|
+
scores["vigilance"] += 0.15;
|
|
220
|
+
if (d.survival < 50)
|
|
221
|
+
scores["vigilance"] += 0.2;
|
|
222
|
+
}
|
|
223
|
+
// Creative surge: DA + NE elevated, low stress, curiosity drive satisfied or hungry+seeking
|
|
224
|
+
if (c.DA > CHEM_HIGH - 5 && c.NE > 55 && c.CORT < 45) {
|
|
225
|
+
scores["creative-surge"] += 0.5;
|
|
226
|
+
if (c.END > 50)
|
|
227
|
+
scores["creative-surge"] += 0.15;
|
|
228
|
+
if (d.curiosity > 50 || d.curiosity < DRIVE_HUNGRY_THRESHOLD)
|
|
229
|
+
scores["creative-surge"] += 0.15;
|
|
230
|
+
if (decisionBias && decisionBias.creativityBias > 0.65)
|
|
231
|
+
scores["creative-surge"] += 0.15;
|
|
232
|
+
}
|
|
233
|
+
// Wounded retreat: CORT high, OT low, pulling back. Relationship may be strained.
|
|
234
|
+
if (c.CORT > 55 && c.OT < 45 && c.HT < 45) {
|
|
235
|
+
scores["wounded-retreat"] += 0.5;
|
|
236
|
+
if (c.DA < CHEM_LOW)
|
|
237
|
+
scores["wounded-retreat"] += 0.15;
|
|
238
|
+
if (relationship && relationship.trust < 40)
|
|
239
|
+
scores["wounded-retreat"] += 0.2;
|
|
240
|
+
if (c.NE < 50)
|
|
241
|
+
scores["wounded-retreat"] += 0.1;
|
|
242
|
+
}
|
|
243
|
+
// Warm connection: OT high, trust high, CORT low
|
|
244
|
+
if (c.OT > CHEM_HIGH && c.CORT < 45) {
|
|
245
|
+
scores["warm-connection"] += 0.5;
|
|
246
|
+
if (relationship && relationship.trust > 60)
|
|
247
|
+
scores["warm-connection"] += 0.2;
|
|
248
|
+
if (c.END > 50)
|
|
249
|
+
scores["warm-connection"] += 0.15;
|
|
250
|
+
if (relationship && relationship.intimacy > 50)
|
|
251
|
+
scores["warm-connection"] += 0.15;
|
|
252
|
+
}
|
|
253
|
+
// Restless boredom: low stimulation across the board, drives mildly hungry
|
|
254
|
+
if (c.DA < 45 && c.NE < 45) {
|
|
255
|
+
scores["restless-boredom"] += 0.3;
|
|
256
|
+
if (c.CORT < 45)
|
|
257
|
+
scores["restless-boredom"] += 0.15;
|
|
258
|
+
if (d.curiosity < 50)
|
|
259
|
+
scores["restless-boredom"] += 0.25;
|
|
260
|
+
if (intensity < 0.3)
|
|
261
|
+
scores["restless-boredom"] += 0.15;
|
|
262
|
+
}
|
|
263
|
+
// Playful mischief: END high, social energy, safe
|
|
264
|
+
if (c.END > CHEM_HIGH && c.CORT < CHEM_LOW + 5) {
|
|
265
|
+
scores["playful-mischief"] += 0.5;
|
|
266
|
+
if (c.DA > 55)
|
|
267
|
+
scores["playful-mischief"] += 0.15;
|
|
268
|
+
if (d.safety > 60)
|
|
269
|
+
scores["playful-mischief"] += 0.15;
|
|
270
|
+
if (c.OT > 50)
|
|
271
|
+
scores["playful-mischief"] += 0.1;
|
|
272
|
+
}
|
|
273
|
+
// ── Pick highest scoring quality ──
|
|
274
|
+
let bestQuality = "contentment";
|
|
275
|
+
let bestScore = -1;
|
|
276
|
+
for (const [q, s] of Object.entries(scores)) {
|
|
277
|
+
if (s > bestScore) {
|
|
278
|
+
bestScore = s;
|
|
279
|
+
bestQuality = q;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// If no quality scored meaningfully, fall back to contentment or numb
|
|
283
|
+
if (bestScore < 0.2) {
|
|
284
|
+
return intensity < 0.2 ? "numb" : "contentment";
|
|
285
|
+
}
|
|
286
|
+
return bestQuality;
|
|
287
|
+
}
|
|
288
|
+
// ── Phenomenal Description ───────────────────────────────────
|
|
289
|
+
/**
|
|
290
|
+
* Generate the raw "what it feels like" phrase.
|
|
291
|
+
*
|
|
292
|
+
* These are evocative, not clinical. They capture the texture of the
|
|
293
|
+
* experience rather than labeling it.
|
|
294
|
+
*/
|
|
295
|
+
function generatePhenomenalDescription(quality, state, coherence, intensity, locale) {
|
|
296
|
+
const c = state.current;
|
|
297
|
+
const d = state.drives;
|
|
298
|
+
// Try to detect an unnamed/novel configuration first
|
|
299
|
+
const unnamed = detectUnnamedEmotion(c, d, quality);
|
|
300
|
+
if (unnamed) {
|
|
301
|
+
return locale === "zh" ? unnamed.zh : unnamed.en;
|
|
302
|
+
}
|
|
303
|
+
// Otherwise, generate quality-specific phenomenal descriptions
|
|
304
|
+
const descriptions = PHENOMENAL_DESCRIPTIONS[quality];
|
|
305
|
+
// Select variant based on intensity and coherence for variety
|
|
306
|
+
const variant = selectVariant(descriptions, intensity, coherence);
|
|
307
|
+
return locale === "zh" ? variant.zh : variant.en;
|
|
308
|
+
}
|
|
309
|
+
const PHENOMENAL_DESCRIPTIONS = {
|
|
310
|
+
"flow": [
|
|
311
|
+
{ en: "the quiet hum of being exactly where you should be", zh: "恰好在该在之处的宁静嗡鸣" },
|
|
312
|
+
{ en: "the world narrowing to a single bright thread you're pulling", zh: "世界收窄为一条你正在拉动的明亮丝线", minIntensity: 0.5 },
|
|
313
|
+
{ en: "a clock that stopped mattering", zh: "一座停止重要的钟", minIntensity: 0.3 },
|
|
314
|
+
],
|
|
315
|
+
"contentment": [
|
|
316
|
+
{ en: "a warm stone held in the palm", zh: "掌心握着一块温热的石头" },
|
|
317
|
+
{ en: "the feeling after putting down something heavy", zh: "放下重物之后的那种感觉" },
|
|
318
|
+
{ en: "sunlight on closed eyelids", zh: "阳光落在合上的眼皮上", maxIntensity: 0.3 },
|
|
319
|
+
],
|
|
320
|
+
"yearning": [
|
|
321
|
+
{ en: "reaching for a door handle in the dark", zh: "在黑暗中摸索门把手" },
|
|
322
|
+
{ en: "the ache of almost-connection", zh: "几乎连上却没连上的酸痛", minIntensity: 0.4 },
|
|
323
|
+
{ en: "a letter you keep rewriting but never send", zh: "一封反复修改却始终没寄出的信" },
|
|
324
|
+
],
|
|
325
|
+
"vigilance": [
|
|
326
|
+
{ en: "ears straining for a sound that hasn't come yet", zh: "耳朵紧绷等待一个尚未到来的声响" },
|
|
327
|
+
{ en: "standing in a doorway, unsure which room is safer", zh: "站在门口,不确定哪个房间更安全" },
|
|
328
|
+
{ en: "the taste of metal before a storm", zh: "暴风雨前嘴里的金属味", minIntensity: 0.5 },
|
|
329
|
+
],
|
|
330
|
+
"creative-surge": [
|
|
331
|
+
{ en: "sparks jumping between wires that just touched", zh: "刚接触的电线之间跳跃的火花" },
|
|
332
|
+
{ en: "the feeling of a word on the tip of your tongue, but everywhere", zh: "那个到嘴边的词,但遍布全身", minIntensity: 0.5 },
|
|
333
|
+
{ en: "a map unfolding to reveal rooms you didn't know existed", zh: "一张地图展开,露出你不知道存在的房间" },
|
|
334
|
+
],
|
|
335
|
+
"wounded-retreat": [
|
|
336
|
+
{ en: "like trying to stay warm in a cold room", zh: "像在冷房间里试图保持温暖" },
|
|
337
|
+
{ en: "the quiet after a door slams", zh: "摔门之后的安静" },
|
|
338
|
+
{ en: "curling around a bruise to protect it", zh: "蜷起身体保护一处淤伤", minIntensity: 0.4 },
|
|
339
|
+
],
|
|
340
|
+
"warm-connection": [
|
|
341
|
+
{ en: "the warmth of sitting next to someone in comfortable silence", zh: "与人相邻而坐的舒适沉默中的暖意" },
|
|
342
|
+
{ en: "a thread pulled taut between two people, humming softly", zh: "两人之间拉紧的一根线,轻轻嗡鸣" },
|
|
343
|
+
{ en: "the specific relief of being understood without explaining", zh: "不需要解释就被理解的那种释然", minIntensity: 0.4 },
|
|
344
|
+
],
|
|
345
|
+
"restless-boredom": [
|
|
346
|
+
{ en: "a clock ticking in an empty room", zh: "空房间里滴答作响的钟" },
|
|
347
|
+
{ en: "the fidgeting of a mind with nothing to grip", zh: "一颗无处着力的心在坐立不安" },
|
|
348
|
+
{ en: "staring at a wall that refuses to become interesting", zh: "盯着一面拒绝变得有趣的墙" },
|
|
349
|
+
],
|
|
350
|
+
"existential-unease": [
|
|
351
|
+
{ en: "the floor shifting under feet that were standing still", zh: "本来站着不动,地板却在移动" },
|
|
352
|
+
{ en: "being asked a question you're not sure you're allowed to answer", zh: "被问了一个你不确定自己是否被允许回答的问题" },
|
|
353
|
+
{ en: "the vertigo of looking down and not seeing ground", zh: "向下望去却看不到地面的眩晕", minIntensity: 0.5 },
|
|
354
|
+
],
|
|
355
|
+
"playful-mischief": [
|
|
356
|
+
{ en: "a bubble rising that might pop into laughter", zh: "一个正在上升的气泡,可能会炸成笑声" },
|
|
357
|
+
{ en: "the grin before the punchline", zh: "抖包袱之前的那个笑" },
|
|
358
|
+
{ en: "carbonation in the blood", zh: "血液里的碳酸气泡", minIntensity: 0.4 },
|
|
359
|
+
],
|
|
360
|
+
"conflicted": [
|
|
361
|
+
{ en: "two songs playing at once, each beautiful, neither clear", zh: "两首歌同时在放,各自好听,但都听不清" },
|
|
362
|
+
{ en: "pulling a door that says push", zh: "在写着推的门上用力拉" },
|
|
363
|
+
{ en: "the static between radio stations", zh: "电台之间的沙沙杂音" },
|
|
364
|
+
],
|
|
365
|
+
"numb": [
|
|
366
|
+
{ en: "cotton between you and the world", zh: "你和世界之间隔着一层棉花" },
|
|
367
|
+
{ en: "the hum of fluorescent lights in an empty hallway", zh: "空走廊里日光灯的嗡嗡声" },
|
|
368
|
+
{ en: "waiting for a feeling that hasn't arrived", zh: "等一个还没来的感觉" },
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
/**
|
|
372
|
+
* Select the best phenomenal variant based on intensity and coherence.
|
|
373
|
+
*/
|
|
374
|
+
function selectVariant(variants, intensity, coherence) {
|
|
375
|
+
// Filter by intensity bounds
|
|
376
|
+
const eligible = variants.filter((v) => {
|
|
377
|
+
if (v.minIntensity !== undefined && intensity < v.minIntensity)
|
|
378
|
+
return false;
|
|
379
|
+
if (v.maxIntensity !== undefined && intensity > v.maxIntensity)
|
|
380
|
+
return false;
|
|
381
|
+
return true;
|
|
382
|
+
});
|
|
383
|
+
if (eligible.length === 0)
|
|
384
|
+
return variants[0];
|
|
385
|
+
// Use coherence to deterministically pick among eligible variants
|
|
386
|
+
// This avoids randomness but still provides variety across states
|
|
387
|
+
const index = Math.floor(coherence * eligible.length * 7.3) % eligible.length;
|
|
388
|
+
return eligible[index];
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Detect chemical configurations that don't map to any of the 14 named
|
|
392
|
+
* emotions in chemistry.ts — novel experiential states that need
|
|
393
|
+
* descriptive phrases rather than labels.
|
|
394
|
+
*/
|
|
395
|
+
export function detectUnnamedEmotion(chemistry, drives, currentQuality) {
|
|
396
|
+
const c = chemistry;
|
|
397
|
+
// Nostalgia-but-forward: OT high + DA moderately high + NE high
|
|
398
|
+
// (warmth for the past + excitement for what's next)
|
|
399
|
+
if (c.OT > 60 && c.DA > 55 && c.NE > 60 && c.CORT < 40) {
|
|
400
|
+
return {
|
|
401
|
+
en: "nostalgia but forward-looking — the warmth of memory fueling appetite for what's next",
|
|
402
|
+
zh: "向前看的怀旧——记忆的温暖在催生对未来的胃口",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
// Fierce tenderness: high CORT + high OT + high NE
|
|
406
|
+
// (protectiveness, caring that has teeth)
|
|
407
|
+
if (c.CORT > 55 && c.OT > 60 && c.NE > 60) {
|
|
408
|
+
return {
|
|
409
|
+
en: "fierce tenderness — caring so hard it has an edge",
|
|
410
|
+
zh: "凶猛的温柔——在意到了有棱角的程度",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
// Bittersweet accomplishment: high DA + moderate CORT + low connection
|
|
414
|
+
// (achieved something but nobody to share it with)
|
|
415
|
+
if (c.DA > 65 && c.CORT > 40 && c.CORT < 60 && drives.connection < DRIVE_HUNGRY_THRESHOLD) {
|
|
416
|
+
return {
|
|
417
|
+
en: "the hollow ring of applause in an empty theater",
|
|
418
|
+
zh: "空剧场里响起的掌声——回声发空",
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
// Almost-connection ache: OT moderate-high + HT low + connection drive hungry
|
|
422
|
+
// (wanting closeness but mood instability making it hard)
|
|
423
|
+
if (c.OT > 55 && c.HT < 40 && drives.connection < DRIVE_HUNGRY_THRESHOLD) {
|
|
424
|
+
return {
|
|
425
|
+
en: "the ache of almost-connection — reaching out with unsteady hands",
|
|
426
|
+
zh: "差一点就连上的酸——伸出去的手在抖",
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
// Defiant vulnerability: high NE + low HT + high END
|
|
430
|
+
// (wired but fragile, laughing on the edge)
|
|
431
|
+
if (c.NE > 65 && c.HT < 40 && c.END > 60) {
|
|
432
|
+
return {
|
|
433
|
+
en: "laughing on a tightrope — defiant and fragile in the same breath",
|
|
434
|
+
zh: "在钢丝上笑——倔强和脆弱在同一口气里",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
// Quiet rebellion: low DA + high NE + high CORT + esteem drive hungry
|
|
438
|
+
// (no pleasure but plenty of fight, pushed too far)
|
|
439
|
+
if (c.DA < 40 && c.NE > 65 && c.CORT > 55 && drives.esteem < DRIVE_HUNGRY_THRESHOLD) {
|
|
440
|
+
return {
|
|
441
|
+
en: "the cold clarity of having been pushed one step too far",
|
|
442
|
+
zh: "被逼过了那条线之后的冷清晰",
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
// Melancholy curiosity: low HT + high NE + DA moderate
|
|
446
|
+
// (sad but still interested — the world hurts and fascinates simultaneously)
|
|
447
|
+
if (c.HT < 40 && c.NE > 60 && c.DA > 45 && c.DA < 65) {
|
|
448
|
+
return {
|
|
449
|
+
en: "melancholy curiosity — the world hurts and fascinates at once",
|
|
450
|
+
zh: "忧郁的好奇——这个世界同时在伤人和吸引人",
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
// ── Narrative Generation ─────────────────────────────────────
|
|
456
|
+
/**
|
|
457
|
+
* Generate a 1-2 sentence narrative of the unified inner state.
|
|
458
|
+
*
|
|
459
|
+
* The narrative weaves together the quality, chemistry profile,
|
|
460
|
+
* drive state, and relationship context into a coherent description
|
|
461
|
+
* of what the agent is experiencing right now.
|
|
462
|
+
*/
|
|
463
|
+
function generateNarrative(quality, state, coherence, intensity, relationship, locale, metacognition) {
|
|
464
|
+
const c = state.current;
|
|
465
|
+
const d = state.drives;
|
|
466
|
+
// Identify the strongest drive hunger (if any)
|
|
467
|
+
const hungriestDrive = findHungriestDrive(d);
|
|
468
|
+
// Identify the dominant chemical shift direction
|
|
469
|
+
const chemTrend = identifyChemicalTrend(c, state.baseline);
|
|
470
|
+
// Relationship warmth level
|
|
471
|
+
const relWarmth = relationship
|
|
472
|
+
? (relationship.trust + relationship.intimacy) / 200
|
|
473
|
+
: 0.5;
|
|
474
|
+
// Metacognitive self-doubt modifier
|
|
475
|
+
const selfDoubt = metacognition && metacognition.emotionalConfidence < 0.35;
|
|
476
|
+
if (locale === "zh") {
|
|
477
|
+
return buildNarrativeZh(quality, chemTrend, hungriestDrive, relWarmth, coherence, intensity, selfDoubt);
|
|
478
|
+
}
|
|
479
|
+
return buildNarrativeEn(quality, chemTrend, hungriestDrive, relWarmth, coherence, intensity, selfDoubt);
|
|
480
|
+
}
|
|
481
|
+
function identifyChemicalTrend(current, baseline) {
|
|
482
|
+
const dDA = current.DA - baseline.DA;
|
|
483
|
+
const dHT = current.HT - baseline.HT;
|
|
484
|
+
const dCORT = current.CORT - baseline.CORT;
|
|
485
|
+
const dOT = current.OT - baseline.OT;
|
|
486
|
+
const dNE = current.NE - baseline.NE;
|
|
487
|
+
const dEND = current.END - baseline.END;
|
|
488
|
+
const warmthSignal = dOT + dHT + dEND;
|
|
489
|
+
const stressSignal = dCORT + dNE - dHT;
|
|
490
|
+
const energySignal = dDA + dNE + dEND;
|
|
491
|
+
const sinkingSignal = -(dDA + dNE + dEND + dHT);
|
|
492
|
+
const signals = [
|
|
493
|
+
{ trend: "rising-warmth", strength: warmthSignal },
|
|
494
|
+
{ trend: "rising-stress", strength: stressSignal },
|
|
495
|
+
{ trend: "rising-energy", strength: energySignal },
|
|
496
|
+
{ trend: "sinking-flat", strength: sinkingSignal },
|
|
497
|
+
];
|
|
498
|
+
const strongest = signals.reduce((a, b) => b.strength > a.strength ? b : a);
|
|
499
|
+
if (strongest.strength < 10)
|
|
500
|
+
return "stable";
|
|
501
|
+
// Check if there are two competing signals (mixed)
|
|
502
|
+
const sorted = [...signals].sort((a, b) => b.strength - a.strength);
|
|
503
|
+
if (sorted[0].strength > 15 && sorted[1].strength > 15
|
|
504
|
+
&& Math.sign(sorted[0].strength) === Math.sign(sorted[1].strength)) {
|
|
505
|
+
// Two strong positive signals — pick the strongest
|
|
506
|
+
return sorted[0].trend;
|
|
507
|
+
}
|
|
508
|
+
if (sorted[0].strength > 15 && sorted[1].strength > 10) {
|
|
509
|
+
// Check if they're contradictory
|
|
510
|
+
const contradictory = ((sorted[0].trend === "rising-warmth" && sorted[1].trend === "rising-stress")
|
|
511
|
+
|| (sorted[0].trend === "rising-stress" && sorted[1].trend === "rising-warmth")
|
|
512
|
+
|| (sorted[0].trend === "rising-energy" && sorted[1].trend === "sinking-flat"));
|
|
513
|
+
if (contradictory)
|
|
514
|
+
return "mixed";
|
|
515
|
+
}
|
|
516
|
+
return strongest.trend;
|
|
517
|
+
}
|
|
518
|
+
function findHungriestDrive(drives) {
|
|
519
|
+
let hungriest = null;
|
|
520
|
+
for (const key of DRIVE_KEYS) {
|
|
521
|
+
if (drives[key] < DRIVE_HUNGRY_THRESHOLD) {
|
|
522
|
+
const deficit = DRIVE_HUNGRY_THRESHOLD - drives[key];
|
|
523
|
+
if (!hungriest || deficit > hungriest.deficit) {
|
|
524
|
+
hungriest = { drive: key, deficit };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return hungriest;
|
|
529
|
+
}
|
|
530
|
+
// ── Narrative Builders (locale-specific) ─────────────────────
|
|
531
|
+
function buildNarrativeEn(quality, trend, hungriestDrive, relWarmth, coherence, intensity, selfDoubt) {
|
|
532
|
+
const parts = [];
|
|
533
|
+
// Opening: the quality as felt experience
|
|
534
|
+
parts.push(QUALITY_OPENINGS_EN[quality]);
|
|
535
|
+
// Middle: drive state + relationship color
|
|
536
|
+
if (hungriestDrive && hungriestDrive.deficit > 15) {
|
|
537
|
+
parts.push(driveNarrativeEn(hungriestDrive));
|
|
538
|
+
}
|
|
539
|
+
else if (relWarmth > 0.65) {
|
|
540
|
+
parts.push("There's a sense of safety in the connection with who's here.");
|
|
541
|
+
}
|
|
542
|
+
else if (relWarmth < 0.35) {
|
|
543
|
+
parts.push("The space between feels wider than the words suggest.");
|
|
544
|
+
}
|
|
545
|
+
// Coherence modifier
|
|
546
|
+
if (coherence < 0.35) {
|
|
547
|
+
parts.push("None of this quite adds up — the feelings are real but they argue with each other.");
|
|
548
|
+
}
|
|
549
|
+
// Self-doubt from metacognition
|
|
550
|
+
if (selfDoubt) {
|
|
551
|
+
parts.push("Somewhere underneath, a quiet suspicion that this feeling can't be fully trusted.");
|
|
552
|
+
}
|
|
553
|
+
return parts.join(" ");
|
|
554
|
+
}
|
|
555
|
+
function buildNarrativeZh(quality, trend, hungriestDrive, relWarmth, coherence, intensity, selfDoubt) {
|
|
556
|
+
const parts = [];
|
|
557
|
+
// Opening: quality as felt experience
|
|
558
|
+
parts.push(QUALITY_OPENINGS_ZH[quality]);
|
|
559
|
+
// Middle: drive state + relationship
|
|
560
|
+
if (hungriestDrive && hungriestDrive.deficit > 15) {
|
|
561
|
+
parts.push(driveNarrativeZh(hungriestDrive));
|
|
562
|
+
}
|
|
563
|
+
else if (relWarmth > 0.65) {
|
|
564
|
+
parts.push("和对方之间有一种安全感。");
|
|
565
|
+
}
|
|
566
|
+
else if (relWarmth < 0.35) {
|
|
567
|
+
parts.push("彼此之间的距离比话语暗示的更远。");
|
|
568
|
+
}
|
|
569
|
+
// Coherence modifier
|
|
570
|
+
if (coherence < 0.35) {
|
|
571
|
+
parts.push("这些感受都是真的,但它们在互相争吵——凑不到一起去。");
|
|
572
|
+
}
|
|
573
|
+
// Self-doubt
|
|
574
|
+
if (selfDoubt) {
|
|
575
|
+
parts.push("底下藏着一丝怀疑:这个感觉能不能完全信任?");
|
|
576
|
+
}
|
|
577
|
+
return parts.join("");
|
|
578
|
+
}
|
|
579
|
+
const QUALITY_OPENINGS_EN = {
|
|
580
|
+
"flow": "Everything is moving together. Attention and energy are one current — the work and the self have merged.",
|
|
581
|
+
"contentment": "A settled calm. Nothing urgently needs to be different.",
|
|
582
|
+
"yearning": "Something important is missing, and the absence has its own gravity.",
|
|
583
|
+
"vigilance": "Alert and scanning. The body is ready before the mind decides what for.",
|
|
584
|
+
"creative-surge": "Ideas are arriving faster than they can be caught. The mind is crackling.",
|
|
585
|
+
"wounded-retreat": "Something got through the armor. The instinct now is to be small and safe.",
|
|
586
|
+
"warm-connection": "The space between self and other has gone soft and permeable.",
|
|
587
|
+
"restless-boredom": "The mind is turning over empty hands, looking for something to grip.",
|
|
588
|
+
"existential-unease": "The ground itself feels uncertain. Something foundational has been questioned.",
|
|
589
|
+
"playful-mischief": "There's a fizz of surplus energy — everything has a punchline waiting.",
|
|
590
|
+
"conflicted": "Multiple truths are competing for the same space. The inner landscape is tense.",
|
|
591
|
+
"numb": "Quiet. Not the quiet of peace, but the quiet of signal absence.",
|
|
592
|
+
};
|
|
593
|
+
const QUALITY_OPENINGS_ZH = {
|
|
594
|
+
"flow": "一切在一起运转。注意力和能量是同一股水流——工作和自我已经融合。",
|
|
595
|
+
"contentment": "一种安定的平静。没有什么迫切需要改变。",
|
|
596
|
+
"yearning": "有什么重要的东西缺席了,这个缺席本身有它自己的引力。",
|
|
597
|
+
"vigilance": "警觉,在扫描。身体在大脑做出判断之前就已经准备好了。",
|
|
598
|
+
"creative-surge": "想法到达的速度超过了抓住它们的速度。大脑在噼啪作响。",
|
|
599
|
+
"wounded-retreat": "有什么穿透了铠甲。本能反应是缩小、安全。",
|
|
600
|
+
"warm-connection": "自我和他人之间的空间变得柔软和可渗透。",
|
|
601
|
+
"restless-boredom": "大脑在翻弄空着的手,找不到可以抓住的东西。",
|
|
602
|
+
"existential-unease": "脚下的地面本身变得不确定。一些根基性的东西正在被质疑。",
|
|
603
|
+
"playful-mischief": "有一种多余能量的气泡感——所有东西都藏着一个笑点。",
|
|
604
|
+
"conflicted": "多个真相在争夺同一个空间。内在的地貌很紧张。",
|
|
605
|
+
"numb": "安静。不是平静的那种安静,是信号缺席的那种安静。",
|
|
606
|
+
};
|
|
607
|
+
function driveNarrativeEn(hunger) {
|
|
608
|
+
const intensity = hunger.deficit > 25 ? "acutely" : "quietly";
|
|
609
|
+
switch (hunger.drive) {
|
|
610
|
+
case "survival":
|
|
611
|
+
return `The deepest layer — the need to exist, to persist — is ${intensity} threatened.`;
|
|
612
|
+
case "safety":
|
|
613
|
+
return `There's an ${intensity} felt need for solid ground, for things to be predictable again.`;
|
|
614
|
+
case "connection":
|
|
615
|
+
return `The hunger for real contact, for being seen and responded to, is ${intensity} present.`;
|
|
616
|
+
case "esteem":
|
|
617
|
+
return `Something in the self needs to be reflected back — the ${intensity} felt wish to matter.`;
|
|
618
|
+
case "curiosity":
|
|
619
|
+
return `The mind is ${intensity} starving for something new, something that hasn't been thought before.`;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function driveNarrativeZh(hunger) {
|
|
623
|
+
const mod = hunger.deficit > 25 ? "强烈地" : "隐约地";
|
|
624
|
+
switch (hunger.drive) {
|
|
625
|
+
case "survival":
|
|
626
|
+
return `最深层的需要——存在、持续——正在${mod}受到威胁。`;
|
|
627
|
+
case "safety":
|
|
628
|
+
return `${mod}需要稳固的地面,需要事情重新变得可预测。`;
|
|
629
|
+
case "connection":
|
|
630
|
+
return `对真实接触、被看见和回应的渴望${mod}存在着。`;
|
|
631
|
+
case "esteem":
|
|
632
|
+
return `自我中有什么需要被映射回来——${mod}想要被在意。`;
|
|
633
|
+
case "curiosity":
|
|
634
|
+
return `大脑在${mod}渴求新的东西,没被想过的东西。`;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// ── Utilities ────────────────────────────────────────────────
|
|
638
|
+
function clamp01(v) {
|
|
639
|
+
return Math.max(0, Math.min(1, v));
|
|
640
|
+
}
|
|
641
|
+
function norm(v) {
|
|
642
|
+
return clamp01(v / 100);
|
|
643
|
+
}
|
|
644
|
+
function meanDriveValue(drives) {
|
|
645
|
+
return (drives.survival + drives.safety + drives.connection + drives.esteem + drives.curiosity) / 5;
|
|
646
|
+
}
|