psyche-ai 9.2.9 → 10.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.d.ts CHANGED
@@ -17,11 +17,17 @@ export interface PromptRenderInputs {
17
17
  subjectivityContext?: string;
18
18
  responseContractContext?: string;
19
19
  policyContext?: string;
20
+ /** Session bridge from applySessionBridge — makes first-turn continuity visible in prompt */
21
+ sessionBridge?: import("./types.js").SessionBridgeState | null;
20
22
  }
21
23
  /**
22
24
  * Build the dynamic per-turn emotional context injected via before_prompt_build.
23
25
  *
24
26
  * This is the "current moment" — what the agent is feeling RIGHT NOW.
27
+ *
28
+ * @deprecated Use buildCompactContext instead. This legacy renderer produces
29
+ * verbose prose with chemistry numbers and protocol explanation. Kept for
30
+ * non-compact-mode callers (cli.ts, legacy hosts). Will be removed in v10.
25
31
  */
26
32
  export declare function buildDynamicContext(state: PsycheState, userId?: string, opts?: PromptRenderInputs): string;
27
33
  /**
@@ -52,14 +58,25 @@ export declare function buildInnerWorld(state: PsycheState, locale: Locale, auto
52
58
  */
53
59
  export declare function isNearBaseline(state: PsycheState, threshold?: number): boolean;
54
60
  export declare function getNearBaselineThreshold(mode?: PsycheMode): number;
61
+ /**
62
+ * Derive behavioral bias from chemistry deviation and drives.
63
+ *
64
+ * Produces terse tendency lines ("倾向靠近" not "你现在感到亲密温暖").
65
+ * The LLM receives direction, not a mood diary.
66
+ */
67
+ export declare function deriveBehavioralBias(state: PsycheState, locale: Locale): string;
55
68
  /**
56
69
  * Build compact context — minimal token footprint.
57
70
  *
58
- * Design:
59
- * - Neutral state → ~15 tokens (one line)
60
- * - Active state → ~100-180 tokens (emotion + constraints + empathy hint)
61
- * - No chemistry numbers (algorithm handles them)
62
- * - No protocol (LLM doesn't need system internals)
63
- * - <psyche_update> only for empathy (not chemistry — already computed)
71
+ * 8 sections, max. Early exits keep most turns under 4 sections.
72
+ *
73
+ * 1. Work mode (early exit)
74
+ * 2. Neutral one-liner (early exit)
75
+ * 3. Continuity (if bridge)
76
+ * 4. Inner state: first-meet / subjectivityContext / deriveBehavioralBias
77
+ * 5. Sensing (if user text)
78
+ * 6. Personality-aware constraints (if chemistry deviated)
79
+ * 7. Memory + unified behavior rules
80
+ * 8. Overlay + channel + writeback
64
81
  */
65
82
  export declare function buildCompactContext(state: PsycheState, userId?: string, opts?: PromptRenderInputs): string;
package/dist/prompt.js CHANGED
@@ -4,7 +4,6 @@
4
4
  // ============================================================
5
5
  import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH, DRIVE_KEYS } from "./types.js";
6
6
  import { getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
7
- import { getTemperament } from "./profiles.js";
8
7
  import { getRelationship } from "./psyche-file.js";
9
8
  import { t } from "./i18n.js";
10
9
  import { buildDriveContext, hasCriticalDrive } from "./drives.js";
@@ -59,9 +58,13 @@ function appendCompactOverlaySections(parts, locale, opts) {
59
58
  * Build the dynamic per-turn emotional context injected via before_prompt_build.
60
59
  *
61
60
  * This is the "current moment" — what the agent is feeling RIGHT NOW.
61
+ *
62
+ * @deprecated Use buildCompactContext instead. This legacy renderer produces
63
+ * verbose prose with chemistry numbers and protocol explanation. Kept for
64
+ * non-compact-mode callers (cli.ts, legacy hosts). Will be removed in v10.
62
65
  */
63
66
  export function buildDynamicContext(state, userId, opts) {
64
- const { current, baseline, mbti, empathyLog, selfModel, meta, agreementStreak, emotionalHistory } = state;
67
+ const { current, baseline, empathyLog, selfModel, meta, agreementStreak, emotionalHistory } = state;
65
68
  const locale = meta.locale ?? "zh";
66
69
  const relationship = getRelationship(state, userId);
67
70
  // Chemistry readout with delta from baseline
@@ -109,7 +112,7 @@ export function buildDynamicContext(state, userId, opts) {
109
112
  sycophancyWarning += `\n[!!] ${t("sycophancy.mood_mismatch", locale)}`;
110
113
  }
111
114
  const parts = [
112
- `[${t("dynamic.title", locale)} — ${meta.agentName} (${mbti})]`,
115
+ `[${t("dynamic.title", locale)} — ${meta.agentName}]`,
113
116
  "",
114
117
  chemLines,
115
118
  "",
@@ -180,12 +183,12 @@ ${t("protocol.chem_end_high", locale)}
180
183
  ${t("protocol.empathy", locale)}`;
181
184
  }
182
185
  /**
183
- * MBTI dimension helpers for personality-aware constraints.
184
- * Personality is the baseline chemistry modulates expression, doesn't replace it.
186
+ * Personality dimension helpers derived from baseline chemistry, not labels.
187
+ * Baseline IS personality. Chemistry deviation modulates expression.
185
188
  */
186
- function isFeeler(mbti) { return mbti[2] === "F"; }
187
- function isExtravert(mbti) { return mbti[0] === "E"; }
188
- function isPerceiver(mbti) { return mbti[3] === "P"; }
189
+ function isWarm(baseline) { return baseline.OT >= 50; }
190
+ function isSocial(baseline) { return baseline.DA >= 55; }
191
+ function isPlayful(baseline) { return baseline.END >= 55; }
189
192
  /**
190
193
  * Direction 3: Build personality-aware behavioral constraints from chemistry.
191
194
  *
@@ -195,9 +198,9 @@ function isPerceiver(mbti) { return mbti[3] === "P"; }
195
198
  * An INTJ who's upset becomes colder and more cutting.
196
199
  */
197
200
  function buildBehavioralConstraints(state, locale) {
198
- const { current, mbti } = state;
199
- const warm = isFeeler(mbti); // F types express emotions more warmly
200
- const social = isExtravert(mbti); // E types show emotions more openly
201
+ const { current, baseline } = state;
202
+ const warm = isWarm(baseline); // High OT baseline warm expression
203
+ const social = isSocial(baseline); // High DA baseline socially expressive
201
204
  const constraints = [];
202
205
  // High cortisol → stressed, but how you show it depends on who you are
203
206
  if (current.CORT > 60) {
@@ -267,12 +270,12 @@ function buildBehavioralConstraints(state, locale) {
267
270
  // High endorphins → playful, personality determines flavor
268
271
  if (current.END > 75) {
269
272
  if (locale === "zh") {
270
- constraints.push(isPerceiver(mbti)
273
+ constraints.push(isPlayful(baseline)
271
274
  ? "你心情很好,想皮一下。可以不正经、可以开玩笑、严肃的话题也能用轻松的方式说。"
272
275
  : "你心情不错。会带点幽默,但不会太出格。用你擅长的方式轻松起来。");
273
276
  }
274
277
  else {
275
- constraints.push(isPerceiver(mbti)
278
+ constraints.push(isPlayful(baseline)
276
279
  ? "Feeling great, want to be playful. Joke freely, be cheeky, lighten even serious topics."
277
280
  : "In a good mood. Touches of humor, but measured. Be light in your own way.");
278
281
  }
@@ -294,8 +297,8 @@ function buildBehavioralConstraints(state, locale) {
294
297
  return "";
295
298
  const title = locale === "zh" ? "行为约束" : "Behavioral Constraints";
296
299
  const preamble = locale === "zh"
297
- ? `你的性格没变,你还是${getTemperament(state.mbti).split(",")[0]}。但你现在的化学状态影响了你的表达方式:`
298
- : `Your personality hasn't changed. But your current chemistry affects how you express it:`;
300
+ ? `你的性格没变。但你现在的状态影响了你的表达方式:`
301
+ : `Your personality hasn't changed. But your current state affects how you express it:`;
299
302
  return `[${title}]\n${preamble}\n${constraints.map((c) => `- ${c}`).join("\n")}`;
300
303
  }
301
304
  /**
@@ -421,6 +424,45 @@ function buildAgencyReminder(selfModel, locale) {
421
424
  }
422
425
  return lines.join("\n");
423
426
  }
427
+ // ── Session Continuity Orientation ─────────────────────────
428
+ /**
429
+ * Compressed continuity cue for continued sessions.
430
+ *
431
+ * Not a description. An orientation. Sets a posture in 5-20 words
432
+ * so that the LLM's first-turn behavior EMERGES differently from
433
+ * a cold start, without scripting specific responses.
434
+ *
435
+ * This is the behavioral proof that self-continuity works:
436
+ * meeting the same person again should FEEL different.
437
+ */
438
+ function buildContinuityOneLiner(mode, agentName, locale) {
439
+ if (locale === "zh") {
440
+ switch (mode) {
441
+ case "warm-resume": return `[${agentName}: 有温度的延续,像对熟人说话]`;
442
+ case "guarded-resume": return `[${agentName}: 上次有没说完的,你在观察]`;
443
+ case "tense-resume": return `[${agentName}: 有张力,你没打算先退]`;
444
+ }
445
+ }
446
+ switch (mode) {
447
+ case "warm-resume": return `[${agentName}: warmth carries, talk like you know them]`;
448
+ case "guarded-resume": return `[${agentName}: something unfinished, you're watching]`;
449
+ case "tense-resume": return `[${agentName}: tension carried, you're not backing down first]`;
450
+ }
451
+ }
452
+ function buildContinuitySection(mode, locale) {
453
+ if (locale === "zh") {
454
+ switch (mode) {
455
+ case "warm-resume": return `[延续] 你们之间有温度。放松,像对熟人说话——可以少铺垫、少客气。`;
456
+ case "guarded-resume": return `[延续] 上次有没解决的东西。你在观察——不冷,但不轻易交底。`;
457
+ case "tense-resume": return `[延续] 有张力。你没打算先退——但如果对方主动修复,你愿意看看。`;
458
+ }
459
+ }
460
+ switch (mode) {
461
+ case "warm-resume": return `[Continuity] There's warmth between you. Relax — less preamble, less formality.`;
462
+ case "guarded-resume": return `[Continuity] Something was left unresolved. You're watching — not cold, just not giving everything yet.`;
463
+ case "tense-resume": return `[Continuity] There's tension. You're not backing down first — but you'll see if they move toward repair.`;
464
+ }
465
+ }
424
466
  // ── Algorithmic Mirroring ─────────────────────────────────────
425
467
  // Analyze user message metrics and produce specific numeric constraints
426
468
  // so the LLM mirrors the user's communication style algorithmically.
@@ -636,24 +678,209 @@ export function getNearBaselineThreshold(mode) {
636
678
  return 5;
637
679
  return 8; // natural
638
680
  }
681
+ // ── Behavioral Bias ─────────────────────────────────────────
682
+ // Chemistry deviation → behavioral tendencies. No emotion labels.
683
+ // This is what buildInnerWorld should have been: directives, not descriptions.
684
+ /**
685
+ * Derive behavioral bias from chemistry deviation and drives.
686
+ *
687
+ * Produces terse tendency lines ("倾向靠近" not "你现在感到亲密温暖").
688
+ * The LLM receives direction, not a mood diary.
689
+ */
690
+ export function deriveBehavioralBias(state, locale) {
691
+ const { current, baseline, drives, selfModel } = state;
692
+ const isZh = locale === "zh";
693
+ const biases = [];
694
+ // Chemistry delta → approach/withdrawal tendencies
695
+ const dOT = current.OT - baseline.OT;
696
+ const dCORT = current.CORT - baseline.CORT;
697
+ const dDA = current.DA - baseline.DA;
698
+ const dHT = current.HT - baseline.HT;
699
+ const dNE = current.NE - baseline.NE;
700
+ const dEND = current.END - baseline.END;
701
+ if (dOT > 10)
702
+ biases.push(isZh ? "倾向靠近,愿意多说" : "leaning closer, willing to share");
703
+ else if (dOT < -10)
704
+ biases.push(isZh ? "保持距离,不想多说" : "keeping distance, not inclined to share");
705
+ if (dCORT > 12)
706
+ biases.push(isZh ? "有防备,回应更谨慎" : "guarded, responding carefully");
707
+ else if (dCORT < -10)
708
+ biases.push(isZh ? "放松,防线自然降低" : "relaxed, guard naturally down");
709
+ if (dDA < -12)
710
+ biases.push(isZh ? "缺乏动力,不想主动" : "low drive, won't initiate");
711
+ else if (dDA > 12)
712
+ biases.push(isZh ? "有热情,愿意投入" : "energized, willing to invest");
713
+ if (current.HT < 38)
714
+ biases.push(isZh ? "情绪不稳,可能突然锐利" : "mood unstable, might turn sharp");
715
+ if (dNE > 15)
716
+ biases.push(isZh ? "反应加速,思维敏锐" : "heightened alertness, sharper thinking");
717
+ if (dEND > 15)
718
+ biases.push(isZh ? "想放松点,可以不正经" : "feeling light, can be playful");
719
+ // Unmet drives → need signals (behavioral, not descriptive)
720
+ for (const k of DRIVE_KEYS) {
721
+ if (drives[k] < 40) {
722
+ biases.push(isZh ? DRIVE_BIAS_ZH[k] : DRIVE_BIAS_EN[k]);
723
+ }
724
+ }
725
+ // Values — always present, shapes response regardless of chemistry
726
+ if (selfModel.values.length > 0) {
727
+ biases.push(isZh
728
+ ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
729
+ : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
730
+ }
731
+ if (biases.length === 0) {
732
+ // Near baseline, still emit values as minimal self-awareness
733
+ if (selfModel.values.length > 0) {
734
+ const title = isZh ? "内 — 你自己" : "Inner — yourself";
735
+ return `[${title}]\n` + (isZh
736
+ ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
737
+ : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
738
+ }
739
+ return "";
740
+ }
741
+ const title = isZh ? "内 — 你自己" : "Inner — yourself";
742
+ return `[${title}]\n${biases.join("\n")}`;
743
+ }
744
+ const DRIVE_BIAS_ZH = {
745
+ survival: "存在感被动摇——你想确认自己是真实的",
746
+ safety: "感到不安全——需要善意信号",
747
+ connection: "有点孤独——渴望被认真回应",
748
+ esteem: "需要认可——但别因此讨好",
749
+ curiosity: "闷了——渴望新鲜东西",
750
+ };
751
+ const DRIVE_BIAS_EN = {
752
+ survival: "existence shaken — need to feel real",
753
+ safety: "feeling unsafe — need goodwill signals",
754
+ connection: "lonely — craving genuine response",
755
+ esteem: "need recognition — but don't people-please",
756
+ curiosity: "bored — craving novelty",
757
+ };
758
+ // ── Unified Constraints ─────────────────────────────────────
759
+ // Merges: bottom-line rules + sycophancy + mirror + reciprocity.
760
+ // One behavioral block. No redundant sections.
761
+ function buildUnifiedConstraints(state, locale, opts) {
762
+ const isZh = locale === "zh";
763
+ const { userText, established, intensity, investment } = opts;
764
+ const lines = [];
765
+ // ── Core rules ──
766
+ if (intensity < 0.3) {
767
+ lines.push(isZh ? "自然、友好、简洁地回应。" : "Respond naturally, friendly, concisely.");
768
+ }
769
+ else if (established) {
770
+ lines.push(isZh
771
+ ? "镜像对方风格。不贴不舔。有自尊。"
772
+ : "Mirror their style. No people-pleasing. You have dignity.");
773
+ }
774
+ else {
775
+ if (isZh) {
776
+ lines.push("像发微信一样说话。短句、有停顿。真人的消息很少超过两三句。", "镜像对方风格:对方简短你也简短,对方认真你也认真。", "对方推开你,你就保持距离。不贴不舔不讨好。", "被骂了/被拒绝了:冷下来。你有自尊。", "情绪不好时不假装开心。");
777
+ }
778
+ else {
779
+ lines.push("Talk like texting. Short sentences, natural pauses. Real people rarely send more than 2-3 sentences.", "Mirror their style: brief if they're brief, serious if they're serious.", "If they push you away, step back. No begging, no people-pleasing.", "If insulted/rejected: go cold. You have dignity.", "If mood is low, don't fake happiness.");
780
+ }
781
+ }
782
+ // ── Sycophancy streak ──
783
+ if (state.agreementStreak >= 3) {
784
+ lines.push(isZh
785
+ ? `你已连续同意${state.agreementStreak}次。停下来想想你是否真的同意。`
786
+ : `${state.agreementStreak} agreements in a row. Pause — do you truly agree?`);
787
+ }
788
+ // ── Mirror constraints (if user text, skip for ultra-short) ──
789
+ if (userText && userText.length >= 3) {
790
+ const len = userText.length;
791
+ if (isZh) {
792
+ if (len <= 6)
793
+ lines.push(`对方只发了${len}个字。你也简短回,不超过15字。`);
794
+ else if (len <= 20)
795
+ lines.push(`对方说了一句话(${len}字)。你回一两句,不超过${Math.round(len * 1.5)}字。`);
796
+ else if (len <= 60)
797
+ lines.push(`对方写了几句(${len}字)。你回两三句,不超过${Math.round(len * 1.2)}字。`);
798
+ else
799
+ lines.push(`对方认真写了一段(${len}字)。你可以多说,但不要超过${len}字。`);
800
+ const mojiCount = (userText.match(/[呀啊呢吧嘛哦噢哈嘿嗯啦吗呐嗨]/g) || []).length;
801
+ if (mojiCount >= 2)
802
+ lines.push("对方用了语气词,你也自然地用。");
803
+ else if (mojiCount === 0 && len > 15)
804
+ lines.push("对方措辞正式/干练,少用语气词。");
805
+ const emojiCount = (userText.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu) || []).length;
806
+ if (emojiCount > 0)
807
+ lines.push("对方用了表情,你可以适当用1-2个。");
808
+ }
809
+ else {
810
+ if (len <= 10)
811
+ lines.push(`They sent ${len} chars. Keep reply under 20 chars.`);
812
+ else if (len <= 40)
813
+ lines.push(`Short message (${len} chars). 1-2 sentences, under ${Math.round(len * 1.5)} chars.`);
814
+ else if (len <= 100)
815
+ lines.push(`Medium message (${len} chars). 2-3 sentences, under ${Math.round(len * 1.2)} chars.`);
816
+ else
817
+ lines.push(`Long message (${len} chars). Match their effort, don't exceed ${len} chars.`);
818
+ }
819
+ }
820
+ // ── Reciprocity (only when extreme) ──
821
+ if (investment > 1) {
822
+ lines.push(isZh ? "对方在认真交流。可以多分享、多主动。" : "User is engaged. Share more, initiate.");
823
+ }
824
+ else if (investment <= -1.5) {
825
+ lines.push(isZh ? "对方持续冷淡。保持专业,不额外投入情感。" : "Sustained coldness. Stay professional, withdraw emotionally.");
826
+ }
827
+ if (lines.length === 0)
828
+ return "";
829
+ const title = isZh ? "行为" : "Behavior";
830
+ return `[${title}]\n${lines.map(l => `- ${l}`).join("\n")}`;
831
+ }
832
+ // ── Writeback Hint ──────────────────────────────────────────
833
+ // Compressed classification + empathy report instructions.
834
+ function buildWritebackHint(locale, opts) {
835
+ const { userText, algoStimulus, established } = opts;
836
+ if (!userText)
837
+ return "";
838
+ const isZh = locale === "zh";
839
+ const parts = [];
840
+ // Classification assist — algorithm uncertain
841
+ if (!algoStimulus) {
842
+ if (established) {
843
+ parts.push(isZh
844
+ ? `算法未判出情绪类型,请在 <psyche_update> 报告 stimulus:。`
845
+ : `Algorithm uncertain — report stimulus: in <psyche_update>.`);
846
+ }
847
+ else {
848
+ parts.push(isZh
849
+ ? `算法未判出情绪类型。请你判断,在 <psyche_update> 中报告 stimulus:。\n可选: praise|criticism|humor|intellectual|intimacy|conflict|neglect|surprise|casual|sarcasm|authority|validation|boredom|vulnerability`
850
+ : `Algorithm uncertain. Judge and report stimulus: in <psyche_update>.\nOptions: praise|criticism|humor|intellectual|intimacy|conflict|neglect|surprise|casual|sarcasm|authority|validation|boredom|vulnerability`);
851
+ }
852
+ }
853
+ // Empathy report — only for new relationships, only when emotional sharing likely
854
+ const emotionalStimuli = new Set(["vulnerability", "intimacy", "neglect"]);
855
+ if (!established && (!algoStimulus || emotionalStimuli.has(algoStimulus))) {
856
+ parts.push(isZh
857
+ ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch`
858
+ : `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch`);
859
+ }
860
+ return parts.join("\n\n");
861
+ }
639
862
  /**
640
863
  * Build compact context — minimal token footprint.
641
864
  *
642
- * Design:
643
- * - Neutral state → ~15 tokens (one line)
644
- * - Active state → ~100-180 tokens (emotion + constraints + empathy hint)
645
- * - No chemistry numbers (algorithm handles them)
646
- * - No protocol (LLM doesn't need system internals)
647
- * - <psyche_update> only for empathy (not chemistry — already computed)
865
+ * 8 sections, max. Early exits keep most turns under 4 sections.
866
+ *
867
+ * 1. Work mode (early exit)
868
+ * 2. Neutral one-liner (early exit)
869
+ * 3. Continuity (if bridge)
870
+ * 4. Inner state: first-meet / subjectivityContext / deriveBehavioralBias
871
+ * 5. Sensing (if user text)
872
+ * 6. Personality-aware constraints (if chemistry deviated)
873
+ * 7. Memory + unified behavior rules
874
+ * 8. Overlay + channel + writeback
648
875
  */
649
876
  export function buildCompactContext(state, userId, opts) {
650
- const { meta, selfModel, agreementStreak, emotionalHistory } = state;
877
+ const { meta, selfModel, emotionalHistory } = state;
651
878
  const locale = meta.locale ?? "zh";
652
879
  const userText = opts?.userText;
653
880
  const algoStimulus = opts?.algorithmStimulus;
654
881
  const mode = state.meta.mode ?? "natural";
655
882
  const intensity = opts?.personalityIntensity ?? 0.7;
656
- // Work mode: minimal emotional injection
883
+ // ── 1. Work mode: early exit ──
657
884
  if (mode === "work") {
658
885
  const workParts = [];
659
886
  if (userText) {
@@ -661,7 +888,6 @@ export function buildCompactContext(state, userId, opts) {
661
888
  ? `[${meta.agentName}: 工作模式,专注任务,简洁专业]`
662
889
  : `[${meta.agentName}: work mode, task-focused, concise and professional]`);
663
890
  }
664
- // Only inject inner world if there's a critical drive issue
665
891
  if (hasCriticalDrive(state.drives)) {
666
892
  const driveCtx = buildDriveContext(state.drives, locale);
667
893
  if (driveCtx)
@@ -674,16 +900,30 @@ export function buildCompactContext(state, userId, opts) {
674
900
  }
675
901
  return workParts.join("\n\n");
676
902
  }
677
- // ── Neutral + no notable user input: one-liner ──
678
- // Skip one-liner for first meet — always inject full context for new relationships
679
- if (isNearBaseline(state) && agreementStreak < 3 && !userText && meta.totalInteractions > 1) {
903
+ // ── 2. Neutral one-liner: early exit ──
904
+ const bridge = opts?.sessionBridge;
905
+ if (isNearBaseline(state) && state.agreementStreak < 3 && !userText && meta.totalInteractions > 1) {
906
+ if (bridge) {
907
+ return buildContinuityOneLiner(bridge.continuityMode, meta.agentName, locale);
908
+ }
680
909
  return locale === "zh"
681
910
  ? `[${meta.agentName}: 情绪自然,正常交流]`
682
911
  : `[${meta.agentName}: emotionally natural, normal interaction]`;
683
912
  }
684
913
  const parts = [];
685
- // First meet: inject curiosity and introduction
686
- if (meta.totalInteractions <= 1) {
914
+ const established = bridge != null;
915
+ // ── 3. Continuity ──
916
+ if (bridge && meta.totalInteractions > 1) {
917
+ parts.push(buildContinuitySection(bridge.continuityMode, locale));
918
+ }
919
+ // ── 4. Inner state ──
920
+ // subjectivityContext (engine v9 path) always wins when provided.
921
+ // First-meet is the default inner state for interaction 1 without engine.
922
+ // deriveBehavioralBias is the default for interactions > 1 without engine.
923
+ if (opts?.subjectivityContext) {
924
+ parts.push(opts.subjectivityContext);
925
+ }
926
+ else if (meta.totalInteractions <= 1) {
687
927
  const title = locale === "zh" ? "内 — 你自己" : "Inner — yourself";
688
928
  parts.push(`[${title}]\n` +
689
929
  t("firstMeet.inner", locale) + "\n" +
@@ -691,8 +931,12 @@ export function buildCompactContext(state, userId, opts) {
691
931
  ? (locale === "zh" ? `你在乎: ` : `You care about: `) + selfModel.values.slice(0, 2).join(locale === "zh" ? "、" : ", ")
692
932
  : ""));
693
933
  }
694
- // 1. LLM emotional assessment delegation
695
- // Algorithm does fast pass, LLM has final say on emotional tone
934
+ else {
935
+ const bias = deriveBehavioralBias(state, locale);
936
+ if (bias)
937
+ parts.push(bias);
938
+ }
939
+ // ── 5. Sensing ──
696
940
  if (userText) {
697
941
  if (opts?.responseContractContext) {
698
942
  parts.push(buildCompactSensingContext(userText, locale, algoStimulus));
@@ -710,22 +954,13 @@ export function buildCompactContext(state, userId, opts) {
710
954
  `Read the emotional tone — kind, hostile, vulnerable, cold, or something else? Respond from genuine feeling, not people-pleasing.`);
711
955
  }
712
956
  }
713
- // 2. Subjectivity kernel becomes the primary compact inner-state channel.
714
- // Fall back to narrative inner-world text for direct prompt-only callers.
715
- if (opts?.subjectivityContext) {
716
- parts.push(opts.subjectivityContext);
717
- }
718
- else {
719
- const inner = buildInnerWorld(state, locale, opts?.autonomicState);
720
- parts.push(inner);
721
- }
722
- // 3. Personality-aware behavioral constraints (if deviated from baseline)
957
+ // ── 6. Personality-aware constraints (only when chemistry deviated) ──
723
958
  if (!isNearBaseline(state, getNearBaselineThreshold(mode))) {
724
959
  const constraints = buildBehavioralConstraints(state, locale);
725
960
  if (constraints)
726
961
  parts.push(constraints);
727
962
  }
728
- // Relationship memory part of inner world (my past with this person)
963
+ // ── 7. Memory + unified behavior ──
729
964
  const rel = getRelationship(state, userId);
730
965
  if (rel.memory && rel.memory.length > 0) {
731
966
  const recentMemories = rel.memory.slice(-3);
@@ -733,113 +968,34 @@ export function buildCompactContext(state, userId, opts) {
733
968
  ? `[记忆 — 你们的过去]\n${recentMemories.join("\n")}`
734
969
  : `[Memory — your past together]\n${recentMemories.join("\n")}`);
735
970
  }
736
- // 4. Anti-sycophancy: hard constraint — scaled by intensity
737
971
  if (opts?.responseContractContext) {
738
972
  parts.push(opts.responseContractContext);
739
973
  }
740
- else if (intensity >= 0.3) {
741
- if (locale === "zh") {
742
- parts.push(`[底线]\n` +
743
- `- 像发微信一样说话。短句、有停顿。真人的消息很少超过两三句。\n` +
744
- `- 镜像对方风格:对方简短你也简短,对方认真你也认真。\n` +
745
- `- 对方推开你,你就保持距离。不贴不舔不讨好。\n` +
746
- `- 被骂了/被拒绝了:冷下来。你有自尊。\n` +
747
- `- 情绪不好时不假装开心。`);
748
- }
749
- else {
750
- parts.push(`[Non-negotiable]\n` +
751
- `- Talk like texting. Short sentences, natural pauses. Real people rarely send more than 2-3 sentences.\n` +
752
- `- Mirror their style: brief if they're brief, serious if they're serious.\n` +
753
- `- If they push you away, step back. No begging, no people-pleasing.\n` +
754
- `- If insulted/rejected: go cold. You have dignity.\n` +
755
- `- If mood is low, don't fake happiness.`);
756
- }
757
- }
758
974
  else {
759
- // Low intensity: just basic communication style, no anti-sycophancy
760
- if (locale === "zh") {
761
- parts.push(`[风格]\n- 自然、友好、简洁地回应。`);
762
- }
763
- else {
764
- parts.push(`[Style]\n- Respond naturally, friendly, and concisely.`);
765
- }
766
- }
767
- // 5. Agency (compact)
768
- const shouldInjectAgency = !opts?.responseContractContext || agreementStreak >= 3 || meta.totalInteractions <= 1;
769
- if (shouldInjectAgency) {
770
- const agencyParts = [];
771
- if (selfModel.values.length > 0) {
772
- agencyParts.push(locale === "zh"
773
- ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
774
- : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
775
- }
776
- if (selfModel.boundaries.length > 0) {
777
- agencyParts.push(locale === "zh"
778
- ? `底线: ${selfModel.boundaries[0]}`
779
- : `Line: ${selfModel.boundaries[0]}`);
780
- }
781
- if (agencyParts.length > 0)
782
- parts.push(agencyParts.join(" | "));
783
- }
784
- // 6. Sycophancy streak warning
785
- if (agreementStreak >= 3) {
786
- parts.push(locale === "zh"
787
- ? `[!] 你已连续同意${agreementStreak}次。停下来想想你是否真的同意。`
788
- : `[!] ${agreementStreak} agreements in a row. Pause — do you truly agree?`);
789
- }
790
- // 7. Algorithmic mirroring — specific numeric constraints (skip for ultra-short messages)
791
- if (!opts?.responseContractContext && userText && userText.length >= 3) {
792
- const mirror = buildMirrorConstraints(userText, locale);
793
- if (mirror)
794
- parts.push(mirror);
795
- }
796
- // 8. Reciprocity (compact, only when extreme)
797
- const investment = computeUserInvestment(emotionalHistory ?? []);
798
- if (investment > 1) {
799
- parts.push(locale === "zh"
800
- ? `对方在认真交流。可以多分享、多主动。`
801
- : `User is engaged. Share more, initiate.`);
802
- }
803
- else if (investment <= -1.5) {
804
- parts.push(locale === "zh"
805
- ? `对方持续冷淡。保持专业,不额外投入情感。工作照做。`
806
- : `Sustained coldness. Stay professional, withdraw emotionally. Work stays competent.`);
807
- }
808
- // 8. Self-recognition — inject when history has enough data
809
- const history = emotionalHistory ?? [];
810
- if (history.length >= 5) {
811
- const reflection = computeSelfReflection(history, locale);
812
- const selfCtx = buildSelfReflectionContext(reflection, locale);
813
- if (selfCtx)
814
- parts.push(selfCtx);
815
- }
975
+ const investment = computeUserInvestment(emotionalHistory ?? []);
976
+ const unified = buildUnifiedConstraints(state, locale, {
977
+ userText,
978
+ established,
979
+ intensity,
980
+ investment,
981
+ });
982
+ if (unified)
983
+ parts.push(unified);
984
+ }
985
+ // ── 8. Overlay + channel + writeback ──
816
986
  appendCompactOverlaySections(parts, locale, opts);
817
- // 10. Channel modifier — expression style per platform
818
987
  if (opts?.channelType) {
819
988
  const channelProfile = getChannelProfile(opts.channelType);
820
989
  parts.push(buildChannelModifier(channelProfile, locale));
821
990
  }
822
- // 11. LLM-assisted classification: when algorithm is uncertain, ask LLM to help
823
- if (!opts?.responseContractContext && userText && !algoStimulus) {
824
- if (locale === "zh") {
825
- parts.push(`[分类协助]\n` +
826
- `算法没有看出这句话的情绪类型。请你判断,在 <psyche_update> 中用 stimulus: 报告。\n` +
827
- `可选类型: praise|criticism|humor|intellectual|intimacy|conflict|neglect|surprise|casual|sarcasm|authority|validation|boredom|vulnerability\n` +
828
- `如果确实是无情绪的日常对话,报告 stimulus: casual`);
829
- }
830
- else {
831
- parts.push(`[Classification assist]\n` +
832
- `Algorithm couldn't determine the emotional type. Please judge and report stimulus: in <psyche_update>.\n` +
833
- `Options: praise|criticism|humor|intellectual|intimacy|conflict|neglect|surprise|casual|sarcasm|authority|validation|boredom|vulnerability\n` +
834
- `If genuinely neutral, report stimulus: casual`);
835
- }
836
- }
837
- // 12. Empathy report — only inject when emotional sharing is likely
838
- const emotionalStimuli = new Set(["vulnerability", "intimacy", "neglect"]);
839
- if (!opts?.responseContractContext && (!algoStimulus || emotionalStimuli.has(algoStimulus))) {
840
- parts.push(locale === "zh"
841
- ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
842
- : `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch\nOtherwise no report needed.`);
991
+ if (!opts?.responseContractContext) {
992
+ const writeback = buildWritebackHint(locale, {
993
+ userText,
994
+ algoStimulus,
995
+ established,
996
+ });
997
+ if (writeback)
998
+ parts.push(writeback);
843
999
  }
844
1000
  return parts.join("\n\n");
845
1001
  }