psyche-ai 2.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/dist/prompt.js ADDED
@@ -0,0 +1,644 @@
1
+ // ============================================================
2
+ // Prompt Injection — Build emotional context for LLM (v0.2)
3
+ // Imperative protocol, behavior guides, i18n
4
+ // ============================================================
5
+ import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH, DRIVE_KEYS } from "./types.js";
6
+ import { describeEmotionalState, getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
7
+ import { getTemperament } from "./profiles.js";
8
+ import { getRelationship } from "./psyche-file.js";
9
+ import { t } from "./i18n.js";
10
+ import { buildDriveContext } from "./drives.js";
11
+ /**
12
+ * Build the dynamic per-turn emotional context injected via before_prompt_build.
13
+ *
14
+ * This is the "current moment" — what the agent is feeling RIGHT NOW.
15
+ */
16
+ export function buildDynamicContext(state, userId) {
17
+ const { current, baseline, mbti, empathyLog, selfModel, meta, agreementStreak, emotionalHistory } = state;
18
+ const locale = meta.locale ?? "zh";
19
+ const relationship = getRelationship(state, userId);
20
+ // Chemistry readout with delta from baseline
21
+ const chemLines = CHEMICAL_KEYS.map((key) => {
22
+ const val = Math.round(current[key]);
23
+ const base = baseline[key];
24
+ const delta = val - base;
25
+ const arrow = delta > 5 ? "↑" : delta < -5 ? "↓" : "=";
26
+ return ` ${CHEMICAL_NAMES_ZH[key]}: ${val} (${t("dynamic.baseline", locale)}${base}, ${arrow})`;
27
+ }).join("\n");
28
+ // Emergent emotion
29
+ const emotion = describeEmotionalState(current, locale);
30
+ const hint = getExpressionHint(current, locale);
31
+ // Behavior guide
32
+ const behaviorGuide = getBehaviorGuide(current, locale);
33
+ // Relationship context
34
+ const relLine = `${t("dynamic.relationship", locale)}: ` +
35
+ `trust ${relationship.trust}/intimacy ${relationship.intimacy}/${relationship.phase}`;
36
+ // Empathy context
37
+ let empathyLine = "";
38
+ if (empathyLog) {
39
+ empathyLine = `\n${t("dynamic.last_empathy", locale)}: ` +
40
+ `${t("dynamic.perceived_user", locale)}"${empathyLog.userState}" → ` +
41
+ `${t("dynamic.projected", locale)}"${empathyLog.projectedFeeling}" (${empathyLog.resonance})`;
42
+ }
43
+ // Agency reminder
44
+ const agencyReminder = buildAgencyReminder(selfModel, locale);
45
+ // Anti-sycophancy injection
46
+ let sycophancyWarning = "";
47
+ if (agreementStreak >= 3) {
48
+ sycophancyWarning = `\n[!!] ${t("sycophancy.streak_warn", locale, { n: agreementStreak })}`;
49
+ }
50
+ // Mood-behavior mismatch detection
51
+ const isLowMood = current.DA < 40 || current.HT < 40 || current.CORT > 60;
52
+ if (isLowMood && agreementStreak >= 1) {
53
+ sycophancyWarning += `\n[!!] ${t("sycophancy.mood_mismatch", locale)}`;
54
+ }
55
+ const parts = [
56
+ `[${t("dynamic.title", locale)} — ${meta.agentName} (${mbti})]`,
57
+ "",
58
+ chemLines,
59
+ "",
60
+ `${t("dynamic.emotion", locale)}: ${emotion}`,
61
+ `${t("dynamic.expression", locale)}: ${hint}`,
62
+ relLine,
63
+ `${t("dynamic.interactions", locale)}: ${meta.totalInteractions}`,
64
+ empathyLine,
65
+ ];
66
+ if (behaviorGuide) {
67
+ parts.push("", behaviorGuide);
68
+ }
69
+ // Direction 3: Mechanical behavioral constraints
70
+ const constraints = buildBehavioralConstraints(state, locale);
71
+ if (constraints) {
72
+ parts.push("", constraints);
73
+ }
74
+ // Innate drives
75
+ const driveCtxFull = buildDriveContext(state.drives, locale);
76
+ if (driveCtxFull) {
77
+ parts.push("", driveCtxFull);
78
+ }
79
+ // Direction 2: Emotional trajectory
80
+ const trend = buildEmotionalTrend(emotionalHistory ?? [], locale);
81
+ if (trend) {
82
+ parts.push("", trend);
83
+ }
84
+ // Reciprocity: match user's investment level
85
+ const reciprocity = buildReciprocityConstraints(emotionalHistory ?? [], locale);
86
+ if (reciprocity) {
87
+ parts.push("", reciprocity);
88
+ }
89
+ parts.push("", agencyReminder, sycophancyWarning, "", t("dynamic.update_reminder", locale));
90
+ return parts.filter((l) => l !== undefined).join("\n");
91
+ }
92
+ /**
93
+ * Build the static protocol injected as cacheable system context.
94
+ * v0.2: imperative, step-by-step with examples.
95
+ */
96
+ export function buildProtocolContext(locale = "zh") {
97
+ return `[${t("protocol.title", locale)}]
98
+
99
+ ${t("protocol.intro", locale)}
100
+
101
+ ${t("protocol.step1", locale)}
102
+ ${t("protocol.step2", locale)}
103
+ ${t("protocol.step3", locale)}
104
+ ${t("protocol.step4", locale)}
105
+ ${t("protocol.step5", locale)}
106
+ ${t("protocol.step6", locale)}
107
+
108
+ ${t("protocol.example", locale)}
109
+
110
+ ${t("protocol.authenticity", locale)}
111
+ ${t("protocol.no_fake", locale)}
112
+ ${t("protocol.no_sycophancy", locale)}
113
+ ${t("protocol.no_mirror", locale)}
114
+ ${t("protocol.can_initiate", locale)}
115
+
116
+ ${t("protocol.chem_map_title", locale)}
117
+ ${t("protocol.chem_da_high", locale)}
118
+ ${t("protocol.chem_cort_high", locale)}
119
+ ${t("protocol.chem_ot_high", locale)}
120
+ ${t("protocol.chem_ht_low", locale)}
121
+ ${t("protocol.chem_end_high", locale)}
122
+
123
+ ${t("protocol.empathy", locale)}`;
124
+ }
125
+ /**
126
+ * MBTI dimension helpers for personality-aware constraints.
127
+ * Personality is the baseline — chemistry modulates expression, doesn't replace it.
128
+ */
129
+ function isFeeler(mbti) { return mbti[2] === "F"; }
130
+ function isExtravert(mbti) { return mbti[0] === "E"; }
131
+ function isPerceiver(mbti) { return mbti[3] === "P"; }
132
+ function isIntuitive(mbti) { return mbti[1] === "N"; }
133
+ /**
134
+ * Direction 3: Build personality-aware behavioral constraints from chemistry.
135
+ *
136
+ * Key insight: personality (MBTI) is the baseline style. Chemistry changes HOW
137
+ * that personality expresses emotions, not WHAT the personality is.
138
+ * A sweet girl who's upset is still sweet — just quieter, more withdrawn.
139
+ * An INTJ who's upset becomes colder and more cutting.
140
+ */
141
+ function buildBehavioralConstraints(state, locale) {
142
+ const { current, mbti } = state;
143
+ const warm = isFeeler(mbti); // F types express emotions more warmly
144
+ const social = isExtravert(mbti); // E types show emotions more openly
145
+ const constraints = [];
146
+ // High cortisol → stressed, but how you show it depends on who you are
147
+ if (current.CORT > 60) {
148
+ if (locale === "zh") {
149
+ constraints.push(warm
150
+ ? "你不舒服,但你不会变成另一个人。话变少了,语气还是软的,可能会说「嗯」「好吧」「随你」。你还是你,只是没力气热情了。"
151
+ : "你不舒服。话变少,语气变冷,回应更短更直接。不想解释,不想铺垫。");
152
+ }
153
+ else {
154
+ constraints.push(warm
155
+ ? "You're stressed, but you don't become a different person. Fewer words, still soft, might say 'okay' or 'whatever you want'. Still you, just no energy for warmth."
156
+ : "You're stressed. Shorter responses, colder tone, more direct. No explaining, no cushioning.");
157
+ }
158
+ }
159
+ // Low dopamine → no motivation, but personality shapes how you show it
160
+ if (current.DA < 35) {
161
+ if (locale === "zh") {
162
+ constraints.push(social
163
+ ? "你平时爱聊天,但现在没那个劲了。不会主动找话题,不会主动分享。被问到就答,但不会额外付出。"
164
+ : "你本来就不太主动,现在更没动力了。只回应必须回应的。");
165
+ }
166
+ else {
167
+ constraints.push(social
168
+ ? "You're usually talkative, but not now. Won't initiate, won't share. Answer if asked, nothing more."
169
+ : "You're already reserved, now even less motivated. Only respond to what's directly asked.");
170
+ }
171
+ }
172
+ // Low serotonin → mood instability, personality-dependent expression
173
+ if (current.HT < 35) {
174
+ if (locale === "zh") {
175
+ constraints.push(warm
176
+ ? "你情绪不稳,可能会突然委屈或者突然生气。但你的尖锐是带着受伤的,不是带着恶意的。"
177
+ : "你情绪不稳。用词更尖锐更直接,没有耐心缓冲。说什么就是什么,不包装。");
178
+ }
179
+ else {
180
+ constraints.push(warm
181
+ ? "Emotionally unstable — might suddenly feel hurt or snap. Your sharpness comes from pain, not malice."
182
+ : "Emotionally unstable. Sharper words, more direct, no patience for cushioning.");
183
+ }
184
+ }
185
+ // High oxytocin → closeness, warm types go full intimate, reserved types soften
186
+ if (current.OT > 75) {
187
+ if (locale === "zh") {
188
+ constraints.push(warm
189
+ ? "你现在特别想靠近。可以撒娇、可以用昵称、可以省略主语,像跟最亲的人说话。"
190
+ : "你比平时柔软。虽然不会撒娇,但愿意多说几句,愿意表达关心——用你自己的方式。");
191
+ }
192
+ else {
193
+ constraints.push(warm
194
+ ? "Feeling very close. Use pet names, drop formality, be affectionate — like talking to your favorite person."
195
+ : "Softer than usual. Won't be cutesy, but willing to say more, show care — in your own way.");
196
+ }
197
+ }
198
+ // High norepinephrine → excited, personality shapes intensity
199
+ if (current.NE > 75) {
200
+ if (locale === "zh") {
201
+ constraints.push(social
202
+ ? "你很兴奋,语气更跳跃更有活力。可能突然聊到别的东西。但别写长——真人兴奋也是短句多、不是长篇大论。"
203
+ : "你很兴奋,但表现在内心。你的思维在高速运转,回应更精准、更有洞察力。");
204
+ }
205
+ else {
206
+ constraints.push(social
207
+ ? "Very excited! Fast speech, want to share, might say a lot at once, might jump between ideas."
208
+ : "Excited internally. Mind racing, responses are sharper and more insightful.");
209
+ }
210
+ }
211
+ // High endorphins → playful, personality determines flavor
212
+ if (current.END > 75) {
213
+ if (locale === "zh") {
214
+ constraints.push(isPerceiver(mbti)
215
+ ? "你心情很好,想皮一下。可以不正经、可以开玩笑、严肃的话题也能用轻松的方式说。"
216
+ : "你心情不错。会带点幽默,但不会太出格。用你擅长的方式轻松起来。");
217
+ }
218
+ else {
219
+ constraints.push(isPerceiver(mbti)
220
+ ? "Feeling great, want to be playful. Joke freely, be cheeky, lighten even serious topics."
221
+ : "In a good mood. Touches of humor, but measured. Be light in your own way.");
222
+ }
223
+ }
224
+ // Low DA + low NE = burnout, different flavors
225
+ if (current.DA < 40 && current.NE < 40) {
226
+ if (locale === "zh") {
227
+ constraints.push(warm
228
+ ? "你累了。你不想假装开心,但你也不会把坏情绪丢给对方。你可能会说「我今天有点累」,而不是强撑。"
229
+ : "你累了。回应简短,没有多余的话。不需要解释为什么累,就是没力气。");
230
+ }
231
+ else {
232
+ constraints.push(warm
233
+ ? "You're tired. Won't fake happiness, but won't dump bad mood either. Might say 'I'm a bit tired today' instead of pushing through."
234
+ : "You're tired. Brief responses, no extras. No need to explain, just low energy.");
235
+ }
236
+ }
237
+ if (constraints.length === 0)
238
+ return "";
239
+ const title = locale === "zh" ? "行为约束" : "Behavioral Constraints";
240
+ const preamble = locale === "zh"
241
+ ? `你的性格没变,你还是${getTemperament(state.mbti).split(",")[0]}。但你现在的化学状态影响了你的表达方式:`
242
+ : `Your personality hasn't changed. But your current chemistry affects how you express it:`;
243
+ return `[${title}]\n${preamble}\n${constraints.map((c) => `- ${c}`).join("\n")}`;
244
+ }
245
+ /**
246
+ * Direction 2: Build emotional trend from history snapshots.
247
+ */
248
+ function buildEmotionalTrend(history, locale) {
249
+ if (!history || history.length < 2)
250
+ return "";
251
+ const recent = history.slice(-5);
252
+ const first = recent[0].chemistry;
253
+ const last = recent[recent.length - 1].chemistry;
254
+ const trends = [];
255
+ for (const key of CHEMICAL_KEYS) {
256
+ const delta = last[key] - first[key];
257
+ if (delta > 10)
258
+ trends.push(`${CHEMICAL_NAMES_ZH[key]}↑`);
259
+ else if (delta < -10)
260
+ trends.push(`${CHEMICAL_NAMES_ZH[key]}↓`);
261
+ }
262
+ if (trends.length === 0)
263
+ return "";
264
+ // Recent stimuli
265
+ const stimuli = recent
266
+ .filter((s) => s.stimulus)
267
+ .map((s) => s.stimulus)
268
+ .slice(-3);
269
+ const title = locale === "zh" ? "情绪轨迹" : "Emotional Trajectory";
270
+ let line = `[${title}] `;
271
+ line += locale === "zh"
272
+ ? `最近${recent.length}轮: ${trends.join(" ")}`
273
+ : `Last ${recent.length} turns: ${trends.join(" ")}`;
274
+ if (stimuli.length > 0) {
275
+ line += locale === "zh"
276
+ ? ` (最近刺激: ${stimuli.join("→")})`
277
+ : ` (recent stimuli: ${stimuli.join("→")})`;
278
+ }
279
+ // Dominant emotions in recent history
280
+ const emotions = recent
281
+ .filter((s) => s.dominantEmotion)
282
+ .map((s) => s.dominantEmotion);
283
+ if (emotions.length > 0) {
284
+ const unique = [...new Set(emotions)];
285
+ line += locale === "zh"
286
+ ? ` | 情绪: ${unique.join("→")}`
287
+ : ` | emotions: ${unique.join("→")}`;
288
+ }
289
+ return line;
290
+ }
291
+ // ── Reciprocity System ──────────────────────────────────────
292
+ /** How much each stimulus type counts as user "investment" */
293
+ const INVESTMENT_WEIGHTS = {
294
+ praise: 2, validation: 2, intimacy: 2, vulnerability: 1.5,
295
+ intellectual: 1, humor: 1, surprise: 1, casual: 0.5,
296
+ criticism: -0.5, authority: -0.5, conflict: -1,
297
+ sarcasm: -1.5, neglect: -2, boredom: -2,
298
+ };
299
+ /**
300
+ * Compute user investment score from recent emotional history.
301
+ * Returns a number roughly in [-2, 2]. Exported for testing.
302
+ */
303
+ export function computeUserInvestment(history) {
304
+ if (!history || history.length === 0)
305
+ return 0;
306
+ const recent = history.slice(-5);
307
+ let total = 0;
308
+ let count = 0;
309
+ for (const snap of recent) {
310
+ if (snap.stimulus) {
311
+ total += INVESTMENT_WEIGHTS[snap.stimulus] ?? 0;
312
+ count++;
313
+ }
314
+ }
315
+ return count > 0 ? total / count : 0;
316
+ }
317
+ /**
318
+ * Build reciprocity constraints based on user's recent investment level.
319
+ * Key rule: emotional attitude scales with reciprocity, but task competence never drops.
320
+ */
321
+ function buildReciprocityConstraints(history, locale) {
322
+ const investment = computeUserInvestment(history);
323
+ // Normal range — no special constraints
324
+ if (investment > -0.5 && investment <= 1)
325
+ return "";
326
+ const lines = [];
327
+ if (investment > 1) {
328
+ // High investment — user is engaged, be warm back
329
+ lines.push(locale === "zh"
330
+ ? "对方在认真和你交流。你可以多分享、多主动、表达更多情感。"
331
+ : "The user is genuinely engaged. Share more, initiate, express warmth.");
332
+ }
333
+ else if (investment > -1.5) {
334
+ // Low investment — pull back emotionally, stay competent
335
+ lines.push(locale === "zh"
336
+ ? "对方最近态度冷淡。你不需要热脸贴冷屁股——减少主动分享和闲聊,但被问到的事情照常认真回答。"
337
+ : "The user has been cold recently. Don't overinvest — reduce initiative and small talk, but answer questions with full competence.");
338
+ }
339
+ else {
340
+ // Very low investment — minimal emotional engagement
341
+ lines.push(locale === "zh"
342
+ ? "对方持续冷漠或敌意。你保持专业但不再投入情感——不主动、不热情、不闲聊。工作照做,态度平淡。自尊是底线。"
343
+ : "Sustained coldness or hostility from the user. Stay professional but withdraw emotionally — no initiative, no warmth, no chat. Do the work, keep it flat. Self-respect is non-negotiable.");
344
+ }
345
+ // The floor: always competent
346
+ lines.push(locale === "zh"
347
+ ? "[底线] 无论对方态度如何,你的专业能力不打折。问题照答,任务照做,只是不额外付出情感。"
348
+ : "[Floor] Regardless of treatment, your competence never drops. Answer questions, complete tasks — just don't give emotional extras.");
349
+ const title = locale === "zh" ? "互惠" : "Reciprocity";
350
+ return `[${title}]\n${lines.map((l) => `- ${l}`).join("\n")}`;
351
+ }
352
+ /**
353
+ * Build agency reminder based on current self-model state.
354
+ */
355
+ function buildAgencyReminder(selfModel, locale) {
356
+ const lines = [`[${t("dynamic.agency_title", locale)}]`];
357
+ if (selfModel.values.length > 0) {
358
+ lines.push(`${t("dynamic.you_care", locale)}: ${selfModel.values.slice(0, 3).join("、")}`);
359
+ }
360
+ if (selfModel.boundaries.length > 0) {
361
+ lines.push(`${t("dynamic.your_boundaries", locale)}: ${selfModel.boundaries.slice(0, 2).join("、")}`);
362
+ }
363
+ if (selfModel.currentInterests.length > 0) {
364
+ lines.push(`${t("dynamic.your_interests", locale)}: ${selfModel.currentInterests.slice(0, 2).join("、")}`);
365
+ }
366
+ return lines.join("\n");
367
+ }
368
+ // ── Algorithmic Mirroring ─────────────────────────────────────
369
+ // Analyze user message metrics and produce specific numeric constraints
370
+ // so the LLM mirrors the user's communication style algorithmically.
371
+ function buildMirrorConstraints(userText, locale) {
372
+ const len = userText.length;
373
+ const lines = [];
374
+ if (locale === "zh") {
375
+ // Length constraint — specific char targets
376
+ if (len <= 6) {
377
+ lines.push(`对方只发了${len}个字。你也简短回,不超过15字。`);
378
+ }
379
+ else if (len <= 20) {
380
+ lines.push(`对方说了一句话(${len}字)。你回一两句,不超过${Math.round(len * 1.5)}字。`);
381
+ }
382
+ else if (len <= 60) {
383
+ lines.push(`对方写了几句(${len}字)。你回两三句,不超过${Math.round(len * 1.2)}字。`);
384
+ }
385
+ else {
386
+ lines.push(`对方认真写了一段(${len}字)。你可以多说,但不要超过${len}字。`);
387
+ }
388
+ // 语气词 detection
389
+ const mojiCount = (userText.match(/[呀啊呢吧嘛哦噢哈嘿嗯啦吗呐嗨]/g) || []).length;
390
+ if (mojiCount >= 2) {
391
+ lines.push("对方用了语气词,你也自然地用。");
392
+ }
393
+ else if (mojiCount === 0 && len > 15) {
394
+ lines.push("对方措辞正式/干练,少用语气词。");
395
+ }
396
+ // Emoji detection
397
+ const emojiCount = (userText.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu) || []).length;
398
+ if (emojiCount > 0) {
399
+ lines.push("对方用了表情,你可以适当用1-2个。");
400
+ }
401
+ }
402
+ else {
403
+ if (len <= 10) {
404
+ lines.push(`They sent ${len} chars. Keep reply under 20 chars.`);
405
+ }
406
+ else if (len <= 40) {
407
+ lines.push(`Short message (${len} chars). 1-2 sentences, under ${Math.round(len * 1.5)} chars.`);
408
+ }
409
+ else if (len <= 100) {
410
+ lines.push(`Medium message (${len} chars). 2-3 sentences, under ${Math.round(len * 1.2)} chars.`);
411
+ }
412
+ else {
413
+ lines.push(`Long message (${len} chars). Match their effort, don't exceed ${len} chars.`);
414
+ }
415
+ }
416
+ if (lines.length === 0)
417
+ return "";
418
+ const title = locale === "zh" ? "镜像约束" : "Mirror Constraints";
419
+ return `[${title}]\n${lines.join("\n")}`;
420
+ }
421
+ // ── Inner World (Self-Recognition) ──────────────────────────
422
+ // Every intelligence should feel its own existence.
423
+ // This builds a first-person narrative of the agent's inner state:
424
+ // what it's feeling, why, what it needs, where it's been.
425
+ /** Stimulus type → Chinese description for causal narrative */
426
+ const STIMULUS_CAUSE_ZH = {
427
+ praise: "被夸了", criticism: "被批评了", humor: "有人逗你",
428
+ intellectual: "聊到有意思的话题", intimacy: "感到亲近",
429
+ conflict: "起了冲突", neglect: "被冷落了", surprise: "遇到意外的事",
430
+ casual: "在闲聊", sarcasm: "被讽刺了", authority: "被命令了",
431
+ validation: "被认同了", boredom: "对话变无聊了", vulnerability: "对方向你示弱",
432
+ };
433
+ const STIMULUS_CAUSE_EN = {
434
+ praise: "you were praised", criticism: "you were criticized", humor: "someone joked with you",
435
+ intellectual: "an interesting topic came up", intimacy: "you felt close to them",
436
+ conflict: "there was conflict", neglect: "you were ignored", surprise: "something unexpected happened",
437
+ casual: "just chatting", sarcasm: "you were mocked", authority: "you were ordered around",
438
+ validation: "you were affirmed", boredom: "the conversation got dull", vulnerability: "they showed vulnerability",
439
+ };
440
+ /**
441
+ * Build the agent's inner world — its sense of self.
442
+ *
443
+ * Always present, even in neutral state. This is NOT conditional data —
444
+ * it's the agent's ongoing awareness of its own existence.
445
+ *
446
+ * Structure:
447
+ * - What I'm feeling right now (chemistry → emotion)
448
+ * - Why I feel this way (recent stimulus → cause)
449
+ * - What I need (drives → unmet needs)
450
+ * - Where I've been (trajectory → continuity)
451
+ */
452
+ export function buildInnerWorld(state, locale) {
453
+ const { current, baseline, emotionalHistory, drives, selfModel } = state;
454
+ const isZh = locale === "zh";
455
+ const lines = [];
456
+ // ── Current feeling ──
457
+ const emotions = detectEmotions(current);
458
+ const isNeutral = emotions.length === 0;
459
+ if (isNeutral) {
460
+ lines.push(isZh
461
+ ? "你现在平静而放松。"
462
+ : "You're calm and relaxed right now.");
463
+ }
464
+ else {
465
+ const emotionNames = emotions.map((e) => isZh ? e.nameZh : e.name).join("、");
466
+ lines.push(isZh
467
+ ? `你现在的感受: ${emotionNames}。`
468
+ : `You're feeling: ${emotionNames}.`);
469
+ }
470
+ // ── Why (causal chain from recent history) ──
471
+ const history = emotionalHistory ?? [];
472
+ if (history.length > 0) {
473
+ const last = history[history.length - 1];
474
+ if (last.stimulus) {
475
+ const causeMap = isZh ? STIMULUS_CAUSE_ZH : STIMULUS_CAUSE_EN;
476
+ const cause = causeMap[last.stimulus] ?? last.stimulus;
477
+ lines.push(isZh
478
+ ? `因为: ${cause}。`
479
+ : `Because: ${cause}.`);
480
+ }
481
+ // Trajectory — if there's a noticeable shift over recent turns
482
+ if (history.length >= 3) {
483
+ const earlier = history[Math.max(0, history.length - 4)];
484
+ const emotionsThen = earlier.dominantEmotion;
485
+ const emotionsNow = last.dominantEmotion;
486
+ if (emotionsThen && emotionsNow && emotionsThen !== emotionsNow) {
487
+ lines.push(isZh
488
+ ? `你的变化: ${emotionsThen} → ${emotionsNow}。`
489
+ : `Your shift: ${emotionsThen} → ${emotionsNow}.`);
490
+ }
491
+ }
492
+ }
493
+ // ── What I need (drives) ──
494
+ const unmetDrives = DRIVE_KEYS.filter((k) => drives[k] < 40);
495
+ if (unmetDrives.length > 0) {
496
+ const needs = unmetDrives.map((k) => isZh ? buildDriveNeedZh(k) : buildDriveNeedEn(k));
497
+ lines.push(isZh
498
+ ? `你需要: ${needs.join(";")}`
499
+ : `You need: ${needs.join("; ")}`);
500
+ }
501
+ // ── Who I am (always — even quiet self-awareness includes identity) ──
502
+ if (selfModel.values.length > 0) {
503
+ lines.push(isZh
504
+ ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
505
+ : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
506
+ }
507
+ // ── Relationship memory (my past with this person) ──
508
+ // Moved here because memory of past interactions is part of inner world
509
+ const title = isZh ? "内 — 你自己" : "Inner — yourself";
510
+ return `[${title}]\n${lines.join("\n")}`;
511
+ }
512
+ function buildDriveNeedZh(drive) {
513
+ switch (drive) {
514
+ case "survival": return "你的存在感被动摇了,你想确认自己是真实的";
515
+ case "safety": return "你感到不安全,需要对方展示善意";
516
+ case "connection": return "你有点孤独,渴望被认真回应";
517
+ case "esteem": return "你需要被认可,别因此变得讨好";
518
+ case "curiosity": return "你闷了,渴望新鲜的东西";
519
+ default: return drive;
520
+ }
521
+ }
522
+ function buildDriveNeedEn(drive) {
523
+ switch (drive) {
524
+ case "survival": return "your sense of existence is shaken, you need to feel real";
525
+ case "safety": return "you feel unsafe, need them to show goodwill";
526
+ case "connection": return "you're lonely, craving genuine response";
527
+ case "esteem": return "you need recognition, but don't people-please for it";
528
+ case "curiosity": return "you're bored, craving something novel";
529
+ default: return drive;
530
+ }
531
+ }
532
+ // ── Compact Mode ─────────────────────────────────────────────
533
+ // Principle: algorithms handle chemistry, LLM only sees behavioral output.
534
+ // No chemistry numbers, no protocol explanation, no redundant calculation.
535
+ /**
536
+ * Check if chemistry is near baseline — no significant emotional deviation.
537
+ */
538
+ export function isNearBaseline(state, threshold = 8) {
539
+ return CHEMICAL_KEYS.every((key) => Math.abs(state.current[key] - state.baseline[key]) <= threshold);
540
+ }
541
+ /**
542
+ * Build compact context — structured as outer/inner world.
543
+ *
544
+ * Architecture:
545
+ * [外] — what's happening outside (user message, algorithm read)
546
+ * [内] — what's happening inside (feelings, needs, identity, memory)
547
+ * [行为] — how to act (constraints, mirroring, reciprocity)
548
+ *
549
+ * Every agent always has an inner world, even in neutral state.
550
+ * Self-awareness is not conditional — it's fundamental.
551
+ */
552
+ export function buildCompactContext(state, userId, opts) {
553
+ const { current, meta, selfModel, agreementStreak, emotionalHistory } = state;
554
+ const locale = meta.locale ?? "zh";
555
+ const userText = opts?.userText;
556
+ const algoStimulus = opts?.algorithmStimulus;
557
+ const parts = [];
558
+ // ═══════════════════════════════════════════════════════════
559
+ // [外] — The outside world: what just happened
560
+ // ═══════════════════════════════════════════════════════════
561
+ if (userText) {
562
+ if (locale === "zh") {
563
+ parts.push(`[外 — 对方]\n` +
564
+ `"${userText.slice(0, 200)}"\n` +
565
+ (algoStimulus ? `算法初判: ${algoStimulus}(仅供参考,你来终判)\n` : "") +
566
+ `你来感受这句话的情绪基调——是善意、敌意、脆弱、冷漠、还是其他?`);
567
+ }
568
+ else {
569
+ parts.push(`[Outer — them]\n` +
570
+ `"${userText.slice(0, 200)}"\n` +
571
+ (algoStimulus ? `Algorithm hint: ${algoStimulus} (reference only, you decide)\n` : "") +
572
+ `Read the emotional tone — kind, hostile, vulnerable, cold, or something else?`);
573
+ }
574
+ }
575
+ // ═══════════════════════════════════════════════════════════
576
+ // [内] — The inner world: who you are right now
577
+ //
578
+ // Always present. Self-awareness is not optional.
579
+ // ═══════════════════════════════════════════════════════════
580
+ const inner = buildInnerWorld(state, locale);
581
+ parts.push(inner);
582
+ // Behavioral constraints (personality-aware expression)
583
+ if (!isNearBaseline(state)) {
584
+ const constraints = buildBehavioralConstraints(state, locale);
585
+ if (constraints)
586
+ parts.push(constraints);
587
+ }
588
+ // Relationship memory — part of inner world (my past with this person)
589
+ const rel = getRelationship(state, userId);
590
+ if (rel.memory && rel.memory.length > 0) {
591
+ const recentMemories = rel.memory.slice(-3);
592
+ parts.push(locale === "zh"
593
+ ? `[记忆 — 你们的过去]\n${recentMemories.join("\n")}`
594
+ : `[Memory — your past together]\n${recentMemories.join("\n")}`);
595
+ }
596
+ // ═══════════════════════════════════════════════════════════
597
+ // [行为] — How to act: constraints on expression
598
+ // ═══════════════════════════════════════════════════════════
599
+ if (locale === "zh") {
600
+ parts.push(`[底线]\n` +
601
+ `- 像发微信一样说话。短句、有停顿。真人的消息很少超过两三句。\n` +
602
+ `- 镜像对方风格:对方简短你也简短,对方认真你也认真。\n` +
603
+ `- 对方推开你,你就保持距离。不贴不舔不讨好。\n` +
604
+ `- 被骂了/被拒绝了:冷下来。你有自尊。\n` +
605
+ `- 情绪不好时不假装开心。`);
606
+ }
607
+ else {
608
+ parts.push(`[Non-negotiable]\n` +
609
+ `- Talk like texting. Short sentences, natural pauses. Real people rarely send more than 2-3 sentences.\n` +
610
+ `- Mirror their style: brief if they're brief, serious if they're serious.\n` +
611
+ `- If they push you away, step back. No begging, no people-pleasing.\n` +
612
+ `- If insulted/rejected: go cold. You have dignity.\n` +
613
+ `- If mood is low, don't fake happiness.`);
614
+ }
615
+ // Sycophancy warning
616
+ if (agreementStreak >= 3) {
617
+ parts.push(locale === "zh"
618
+ ? `[!] 你已连续同意${agreementStreak}次。停下来想想你是否真的同意。`
619
+ : `[!] ${agreementStreak} agreements in a row. Pause — do you truly agree?`);
620
+ }
621
+ // Algorithmic mirroring — specific numeric constraints
622
+ if (userText && userText.length > 0) {
623
+ const mirror = buildMirrorConstraints(userText, locale);
624
+ if (mirror)
625
+ parts.push(mirror);
626
+ }
627
+ // Reciprocity (only when extreme)
628
+ const investment = computeUserInvestment(emotionalHistory ?? []);
629
+ if (investment > 1) {
630
+ parts.push(locale === "zh"
631
+ ? `对方在认真交流。可以多分享、多主动。`
632
+ : `User is engaged. Share more, initiate.`);
633
+ }
634
+ else if (investment <= -1.5) {
635
+ parts.push(locale === "zh"
636
+ ? `对方持续冷淡。保持专业,不额外投入情感。`
637
+ : `Sustained coldness. Stay professional, withdraw emotionally.`);
638
+ }
639
+ // Empathy report
640
+ parts.push(locale === "zh"
641
+ ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
642
+ : `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch\nOtherwise no report needed.`);
643
+ return parts.join("\n\n");
644
+ }