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/README.md +24 -3
- package/dist/cli.js +8 -3
- package/dist/core.d.ts +4 -2
- package/dist/core.js +28 -31
- package/dist/diagnostics.d.ts +52 -1
- package/dist/diagnostics.js +248 -17
- package/dist/drives.d.ts +3 -3
- package/dist/drives.js +6 -6
- package/dist/generative-self.d.ts +1 -1
- package/dist/generative-self.js +9 -10
- package/dist/index.d.ts +9 -57
- package/dist/index.js +175 -60
- package/dist/observability.d.ts +14 -0
- package/dist/observability.js +392 -0
- package/dist/prompt.d.ts +23 -6
- package/dist/prompt.js +296 -140
- package/dist/psyche-file.js +18 -7
- package/dist/relation-dynamics.js +41 -19
- package/dist/types.d.ts +152 -6
- package/dist/types.js +5 -0
- package/llms.txt +19 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/server.json +3 -3
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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,
|
|
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}
|
|
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
|
-
*
|
|
184
|
-
*
|
|
186
|
+
* Personality dimension helpers — derived from baseline chemistry, not labels.
|
|
187
|
+
* Baseline IS personality. Chemistry deviation modulates expression.
|
|
185
188
|
*/
|
|
186
|
-
function
|
|
187
|
-
function
|
|
188
|
-
function
|
|
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,
|
|
199
|
-
const warm =
|
|
200
|
-
const social =
|
|
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(
|
|
273
|
+
constraints.push(isPlayful(baseline)
|
|
271
274
|
? "你心情很好,想皮一下。可以不正经、可以开玩笑、严肃的话题也能用轻松的方式说。"
|
|
272
275
|
: "你心情不错。会带点幽默,但不会太出格。用你擅长的方式轻松起来。");
|
|
273
276
|
}
|
|
274
277
|
else {
|
|
275
|
-
constraints.push(
|
|
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
|
-
?
|
|
298
|
-
: `Your personality hasn't changed. But your current
|
|
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
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
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,
|
|
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:
|
|
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
|
|
678
|
-
|
|
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
|
-
|
|
686
|
-
|
|
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
|
-
|
|
695
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
}
|