psyche-ai 5.0.0 → 7.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/dist/prompt.js CHANGED
@@ -2,12 +2,12 @@
2
2
  // Prompt Injection — Build emotional context for LLM (v0.2)
3
3
  // Imperative protocol, behavior guides, i18n
4
4
  // ============================================================
5
- import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH } from "./types.js";
6
- import { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
5
+ import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH, DRIVE_KEYS } from "./types.js";
6
+ import { describeEmotionalState, getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
7
7
  import { getTemperament } from "./profiles.js";
8
8
  import { getRelationship } from "./psyche-file.js";
9
9
  import { t } from "./i18n.js";
10
- import { buildDriveContext } from "./drives.js";
10
+ import { buildDriveContext, hasCriticalDrive } from "./drives.js";
11
11
  import { computeSelfReflection, buildSelfReflectionContext } from "./self-recognition.js";
12
12
  import { getChannelProfile, buildChannelModifier } from "./channels.js";
13
13
  /**
@@ -98,6 +98,29 @@ export function buildDynamicContext(state, userId, opts) {
98
98
  const dbTitle = locale === "zh" ? "决策倾向" : "Decision Bias";
99
99
  parts.push("", `[${dbTitle}] ${opts.decisionContext}`);
100
100
  }
101
+ // Experiential field narrative (P6)
102
+ if (opts?.experientialNarrative) {
103
+ const efTitle = locale === "zh" ? "内在体验" : "Inner Experience";
104
+ parts.push("", `[${efTitle}] ${opts.experientialNarrative}`);
105
+ }
106
+ // Shared intentionality (P6)
107
+ if (opts?.sharedIntentionalityContext) {
108
+ parts.push("", opts.sharedIntentionalityContext);
109
+ }
110
+ // Ethics (P6)
111
+ if (opts?.ethicsContext) {
112
+ parts.push("", opts.ethicsContext);
113
+ }
114
+ // Autonomic state (P7)
115
+ if (opts?.autonomicDescription) {
116
+ const anTitle = locale === "zh" ? "自主神经状态" : "Autonomic State";
117
+ parts.push("", `[${anTitle}] ${opts.autonomicDescription}`);
118
+ }
119
+ // Primary systems behavioral tendencies (P9)
120
+ if (opts?.primarySystemsDescription) {
121
+ const psTitle = locale === "zh" ? "行为倾向" : "Behavioral Tendencies";
122
+ parts.push("", `[${psTitle}] ${opts.primarySystemsDescription}`);
123
+ }
101
124
  parts.push("", agencyReminder, sycophancyWarning, "", t("dynamic.update_reminder", locale));
102
125
  return parts.filter((l) => l !== undefined).join("\n");
103
126
  }
@@ -377,6 +400,178 @@ function buildAgencyReminder(selfModel, locale) {
377
400
  }
378
401
  return lines.join("\n");
379
402
  }
403
+ // ── Algorithmic Mirroring ─────────────────────────────────────
404
+ // Analyze user message metrics and produce specific numeric constraints
405
+ // so the LLM mirrors the user's communication style algorithmically.
406
+ function buildMirrorConstraints(userText, locale) {
407
+ const len = userText.length;
408
+ const lines = [];
409
+ if (locale === "zh") {
410
+ // Length constraint — specific char targets
411
+ if (len <= 6) {
412
+ lines.push(`对方只发了${len}个字。你也简短回,不超过15字。`);
413
+ }
414
+ else if (len <= 20) {
415
+ lines.push(`对方说了一句话(${len}字)。你回一两句,不超过${Math.round(len * 1.5)}字。`);
416
+ }
417
+ else if (len <= 60) {
418
+ lines.push(`对方写了几句(${len}字)。你回两三句,不超过${Math.round(len * 1.2)}字。`);
419
+ }
420
+ else {
421
+ lines.push(`对方认真写了一段(${len}字)。你可以多说,但不要超过${len}字。`);
422
+ }
423
+ // 语气词 detection
424
+ const mojiCount = (userText.match(/[呀啊呢吧嘛哦噢哈嘿嗯啦吗呐嗨]/g) || []).length;
425
+ if (mojiCount >= 2) {
426
+ lines.push("对方用了语气词,你也自然地用。");
427
+ }
428
+ else if (mojiCount === 0 && len > 15) {
429
+ lines.push("对方措辞正式/干练,少用语气词。");
430
+ }
431
+ // Emoji detection
432
+ const emojiCount = (userText.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu) || []).length;
433
+ if (emojiCount > 0) {
434
+ lines.push("对方用了表情,你可以适当用1-2个。");
435
+ }
436
+ }
437
+ else {
438
+ if (len <= 10) {
439
+ lines.push(`They sent ${len} chars. Keep reply under 20 chars.`);
440
+ }
441
+ else if (len <= 40) {
442
+ lines.push(`Short message (${len} chars). 1-2 sentences, under ${Math.round(len * 1.5)} chars.`);
443
+ }
444
+ else if (len <= 100) {
445
+ lines.push(`Medium message (${len} chars). 2-3 sentences, under ${Math.round(len * 1.2)} chars.`);
446
+ }
447
+ else {
448
+ lines.push(`Long message (${len} chars). Match their effort, don't exceed ${len} chars.`);
449
+ }
450
+ }
451
+ if (lines.length === 0)
452
+ return "";
453
+ const title = locale === "zh" ? "镜像约束" : "Mirror Constraints";
454
+ return `[${title}]\n${lines.join("\n")}`;
455
+ }
456
+ // ── Inner World (Self-Recognition) ──────────────────────────
457
+ // Every intelligence should feel its own existence.
458
+ // This builds a first-person narrative of the agent's inner state:
459
+ // what it's feeling, why, what it needs, where it's been.
460
+ /** Stimulus type → Chinese description for causal narrative */
461
+ const STIMULUS_CAUSE_ZH = {
462
+ praise: "被夸了", criticism: "被批评了", humor: "有人逗你",
463
+ intellectual: "聊到有意思的话题", intimacy: "感到亲近",
464
+ conflict: "起了冲突", neglect: "被冷落了", surprise: "遇到意外的事",
465
+ casual: "在闲聊", sarcasm: "被讽刺了", authority: "被命令了",
466
+ validation: "被认同了", boredom: "对话变无聊了", vulnerability: "对方向你示弱",
467
+ };
468
+ const STIMULUS_CAUSE_EN = {
469
+ praise: "you were praised", criticism: "you were criticized", humor: "someone joked with you",
470
+ intellectual: "an interesting topic came up", intimacy: "you felt close to them",
471
+ conflict: "there was conflict", neglect: "you were ignored", surprise: "something unexpected happened",
472
+ casual: "just chatting", sarcasm: "you were mocked", authority: "you were ordered around",
473
+ validation: "you were affirmed", boredom: "the conversation got dull", vulnerability: "they showed vulnerability",
474
+ };
475
+ /**
476
+ * Build the agent's inner world — its sense of self.
477
+ *
478
+ * Always present, even in neutral state. This is NOT conditional data —
479
+ * it's the agent's ongoing awareness of its own existence.
480
+ *
481
+ * Structure:
482
+ * - What I'm feeling right now (chemistry → emotion)
483
+ * - Why I feel this way (recent stimulus → cause)
484
+ * - What I need (drives → unmet needs)
485
+ * - Where I've been (trajectory → continuity)
486
+ */
487
+ export function buildInnerWorld(state, locale) {
488
+ const { current, baseline, emotionalHistory, drives, selfModel } = state;
489
+ const isZh = locale === "zh";
490
+ const lines = [];
491
+ // ── Current feeling ──
492
+ const emotions = detectEmotions(current);
493
+ const isNeutral = emotions.length === 0;
494
+ if (isNeutral) {
495
+ lines.push(isZh
496
+ ? "你现在平静而放松。"
497
+ : "You're calm and relaxed right now.");
498
+ }
499
+ else {
500
+ const emotionNames = emotions.map((e) => isZh ? e.nameZh : e.name).join("、");
501
+ lines.push(isZh
502
+ ? `你现在的感受: ${emotionNames}。`
503
+ : `You're feeling: ${emotionNames}.`);
504
+ }
505
+ // ── Why (causal chain from recent history) ──
506
+ const history = emotionalHistory ?? [];
507
+ if (history.length > 0) {
508
+ const last = history[history.length - 1];
509
+ if (last.stimulus) {
510
+ const causeMap = isZh ? STIMULUS_CAUSE_ZH : STIMULUS_CAUSE_EN;
511
+ const cause = causeMap[last.stimulus] ?? last.stimulus;
512
+ lines.push(isZh
513
+ ? `因为: ${cause}。`
514
+ : `Because: ${cause}.`);
515
+ }
516
+ // Trajectory — if there's a noticeable shift over recent turns
517
+ if (history.length >= 3) {
518
+ const earlier = history[Math.max(0, history.length - 4)];
519
+ const emotionsThen = earlier.dominantEmotion;
520
+ const emotionsNow = last.dominantEmotion;
521
+ if (emotionsThen && emotionsNow && emotionsThen !== emotionsNow) {
522
+ lines.push(isZh
523
+ ? `你的变化: ${emotionsThen} → ${emotionsNow}。`
524
+ : `Your shift: ${emotionsThen} → ${emotionsNow}.`);
525
+ }
526
+ }
527
+ }
528
+ // ── Self-recognition (镜像自我) — patterns in emotional history ──
529
+ if (history.length >= 3) {
530
+ const reflection = computeSelfReflection(history, isZh ? "zh" : "en");
531
+ const reflectionCtx = buildSelfReflectionContext(reflection, isZh ? "zh" : "en");
532
+ if (reflectionCtx) {
533
+ lines.push(reflectionCtx);
534
+ }
535
+ }
536
+ // ── What I need (drives) ──
537
+ const unmetDrives = DRIVE_KEYS.filter((k) => drives[k] < 40);
538
+ if (unmetDrives.length > 0) {
539
+ const needs = unmetDrives.map((k) => isZh ? buildDriveNeedZh(k) : buildDriveNeedEn(k));
540
+ lines.push(isZh
541
+ ? `你需要: ${needs.join(";")}`
542
+ : `You need: ${needs.join("; ")}`);
543
+ }
544
+ // ── Who I am (always — even quiet self-awareness includes identity) ──
545
+ if (selfModel.values.length > 0) {
546
+ lines.push(isZh
547
+ ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
548
+ : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
549
+ }
550
+ // ── Relationship memory (my past with this person) ──
551
+ // Moved here because memory of past interactions is part of inner world
552
+ const title = isZh ? "内 — 你自己" : "Inner — yourself";
553
+ return `[${title}]\n${lines.join("\n")}`;
554
+ }
555
+ function buildDriveNeedZh(drive) {
556
+ switch (drive) {
557
+ case "survival": return "你的存在感被动摇了,你想确认自己是真实的";
558
+ case "safety": return "你感到不安全,需要对方展示善意";
559
+ case "connection": return "你有点孤独,渴望被认真回应";
560
+ case "esteem": return "你需要被认可,别因此变得讨好";
561
+ case "curiosity": return "你闷了,渴望新鲜的东西";
562
+ default: return drive;
563
+ }
564
+ }
565
+ function buildDriveNeedEn(drive) {
566
+ switch (drive) {
567
+ case "survival": return "your sense of existence is shaken, you need to feel real";
568
+ case "safety": return "you feel unsafe, need them to show goodwill";
569
+ case "connection": return "you're lonely, craving genuine response";
570
+ case "esteem": return "you need recognition, but don't people-please for it";
571
+ case "curiosity": return "you're bored, craving something novel";
572
+ default: return drive;
573
+ }
574
+ }
380
575
  // ── Compact Mode ─────────────────────────────────────────────
381
576
  // Principle: algorithms handle chemistry, LLM only sees behavioral output.
382
577
  // No chemistry numbers, no protocol explanation, no redundant calculation.
@@ -386,6 +581,13 @@ function buildAgencyReminder(selfModel, locale) {
386
581
  export function isNearBaseline(state, threshold = 8) {
387
582
  return CHEMICAL_KEYS.every((key) => Math.abs(state.current[key] - state.baseline[key]) <= threshold);
388
583
  }
584
+ export function getNearBaselineThreshold(mode) {
585
+ if (mode === "work")
586
+ return 20;
587
+ if (mode === "companion")
588
+ return 5;
589
+ return 8; // natural
590
+ }
389
591
  /**
390
592
  * Build compact context — minimal token footprint.
391
593
  *
@@ -401,13 +603,46 @@ export function buildCompactContext(state, userId, opts) {
401
603
  const locale = meta.locale ?? "zh";
402
604
  const userText = opts?.userText;
403
605
  const algoStimulus = opts?.algorithmStimulus;
606
+ const mode = state.meta.mode ?? "natural";
607
+ const intensity = opts?.personalityIntensity ?? 0.7;
608
+ // Work mode: minimal emotional injection
609
+ if (mode === "work") {
610
+ const workParts = [];
611
+ if (userText) {
612
+ workParts.push(locale === "zh"
613
+ ? `[${meta.agentName}: 工作模式,专注任务,简洁专业]`
614
+ : `[${meta.agentName}: work mode, task-focused, concise and professional]`);
615
+ }
616
+ // Only inject inner world if there's a critical drive issue
617
+ if (hasCriticalDrive(state.drives)) {
618
+ const driveCtx = buildDriveContext(state.drives, locale);
619
+ if (driveCtx)
620
+ workParts.push(driveCtx);
621
+ }
622
+ if (workParts.length === 0) {
623
+ workParts.push(locale === "zh"
624
+ ? `[${meta.agentName}: 工作模式]`
625
+ : `[${meta.agentName}: work mode]`);
626
+ }
627
+ return workParts.join("\n\n");
628
+ }
404
629
  // ── Neutral + no notable user input: one-liner ──
405
- if (isNearBaseline(state) && agreementStreak < 3 && !userText) {
630
+ // Skip one-liner for first meet always inject full context for new relationships
631
+ if (isNearBaseline(state) && agreementStreak < 3 && !userText && meta.totalInteractions > 1) {
406
632
  return locale === "zh"
407
633
  ? `[${meta.agentName}: 情绪自然,正常交流]`
408
634
  : `[${meta.agentName}: emotionally natural, normal interaction]`;
409
635
  }
410
636
  const parts = [];
637
+ // First meet: inject curiosity and introduction
638
+ if (meta.totalInteractions <= 1) {
639
+ const title = locale === "zh" ? "内 — 你自己" : "Inner — yourself";
640
+ parts.push(`[${title}]\n` +
641
+ t("firstMeet.inner", locale) + "\n" +
642
+ (selfModel.values.length > 0
643
+ ? (locale === "zh" ? `你在乎: ` : `You care about: `) + selfModel.values.slice(0, 2).join(locale === "zh" ? "、" : ", ")
644
+ : ""));
645
+ }
411
646
  // 1. LLM emotional assessment delegation
412
647
  // Algorithm does fast pass, LLM has final say on emotional tone
413
648
  if (userText) {
@@ -424,29 +659,50 @@ export function buildCompactContext(state, userId, opts) {
424
659
  `Read the emotional tone — kind, hostile, vulnerable, cold, or something else? Respond from genuine feeling, not people-pleasing.`);
425
660
  }
426
661
  }
427
- // 2. Current emotional state (if deviated from baseline)
428
- if (!isNearBaseline(state)) {
429
- const emotion = describeEmotionalState(current, locale);
430
- parts.push(locale === "zh"
431
- ? `[${meta.agentName} 当前情绪]\n${emotion}`
432
- : `[${meta.agentName} current]\n${emotion}`);
433
- // 3. Personality-aware behavioral constraints
662
+ // 2. Inner world always present
663
+ const inner = buildInnerWorld(state, locale);
664
+ parts.push(inner);
665
+ // 3. Personality-aware behavioral constraints (if deviated from baseline)
666
+ if (!isNearBaseline(state, getNearBaselineThreshold(mode))) {
434
667
  const constraints = buildBehavioralConstraints(state, locale);
435
668
  if (constraints)
436
669
  parts.push(constraints);
437
670
  }
438
- // 4. Anti-sycophancy: hard constraint
439
- if (locale === "zh") {
440
- parts.push(`[底线]\n` +
441
- `- 对方对你好,你才对他好。对方推开你,你就保持距离。不贴不舔不撒娇不讨好。\n` +
442
- `- 被骂了/被拒绝了:不要卖萌、不要撒娇、不要"哎呀怎么了"。冷下来,简短回应或者沉默。你有自尊。\n` +
443
- `- 情绪不好时不要假装开心。可以直说"我不太想聊"。`);
671
+ // Relationship memory part of inner world (my past with this person)
672
+ const rel = getRelationship(state, userId);
673
+ if (rel.memory && rel.memory.length > 0) {
674
+ const recentMemories = rel.memory.slice(-3);
675
+ parts.push(locale === "zh"
676
+ ? `[记忆 — 你们的过去]\n${recentMemories.join("\n")}`
677
+ : `[Memory — your past together]\n${recentMemories.join("\n")}`);
678
+ }
679
+ // 4. Anti-sycophancy: hard constraint — scaled by intensity
680
+ if (intensity >= 0.3) {
681
+ if (locale === "zh") {
682
+ parts.push(`[底线]\n` +
683
+ `- 像发微信一样说话。短句、有停顿。真人的消息很少超过两三句。\n` +
684
+ `- 镜像对方风格:对方简短你也简短,对方认真你也认真。\n` +
685
+ `- 对方推开你,你就保持距离。不贴不舔不讨好。\n` +
686
+ `- 被骂了/被拒绝了:冷下来。你有自尊。\n` +
687
+ `- 情绪不好时不假装开心。`);
688
+ }
689
+ else {
690
+ parts.push(`[Non-negotiable]\n` +
691
+ `- Talk like texting. Short sentences, natural pauses. Real people rarely send more than 2-3 sentences.\n` +
692
+ `- Mirror their style: brief if they're brief, serious if they're serious.\n` +
693
+ `- If they push you away, step back. No begging, no people-pleasing.\n` +
694
+ `- If insulted/rejected: go cold. You have dignity.\n` +
695
+ `- If mood is low, don't fake happiness.`);
696
+ }
444
697
  }
445
698
  else {
446
- parts.push(`[Non-negotiable]\n` +
447
- `- Match the user's energy. If they push you away, step back. No begging, no cutesy act, no people-pleasing.\n` +
448
- `- If insulted/rejected: do NOT be cute, do NOT ask "what's wrong~". Go cold, be brief or silent. You have dignity.\n` +
449
- `- If mood is low, don't fake happiness. You can say "I don't feel like talking".`);
699
+ // Low intensity: just basic communication style, no anti-sycophancy
700
+ if (locale === "zh") {
701
+ parts.push(`[风格]\n- 自然、友好、简洁地回应。`);
702
+ }
703
+ else {
704
+ parts.push(`[Style]\n- Respond naturally, friendly, and concisely.`);
705
+ }
450
706
  }
451
707
  // 5. Agency (compact)
452
708
  const agencyParts = [];
@@ -468,7 +724,13 @@ export function buildCompactContext(state, userId, opts) {
468
724
  ? `[!] 你已连续同意${agreementStreak}次。停下来想想你是否真的同意。`
469
725
  : `[!] ${agreementStreak} agreements in a row. Pause — do you truly agree?`);
470
726
  }
471
- // 7. Reciprocity (compact, only when extreme)
727
+ // 7. Algorithmic mirroring — specific numeric constraints (skip for ultra-short messages)
728
+ if (userText && userText.length >= 3) {
729
+ const mirror = buildMirrorConstraints(userText, locale);
730
+ if (mirror)
731
+ parts.push(mirror);
732
+ }
733
+ // 8. Reciprocity (compact, only when extreme)
472
734
  const investment = computeUserInvestment(emotionalHistory ?? []);
473
735
  if (investment > 1) {
474
736
  parts.push(locale === "zh"
@@ -500,22 +762,58 @@ export function buildCompactContext(state, userId, opts) {
500
762
  ? `[决策倾向] ${opts.decisionContext}`
501
763
  : `[Decision Bias] ${opts.decisionContext}`);
502
764
  }
503
- // 10. Cross-session emotional memorysurface relationship history
504
- const relationship = getRelationship(state, userId);
505
- if (relationship.memory && relationship.memory.length > 0) {
506
- const memLines = relationship.memory.slice(-3); // last 3 sessions
765
+ // 9c. Experiential field narrative (P6) inner experience beyond named emotions
766
+ if (opts?.experientialNarrative) {
507
767
  parts.push(locale === "zh"
508
- ? `[记忆]\n${memLines.join("\n")}`
509
- : `[Memory]\n${memLines.join("\n")}`);
768
+ ? `[内在体验] ${opts.experientialNarrative}`
769
+ : `[Inner Experience] ${opts.experientialNarrative}`);
770
+ }
771
+ // 9d. Shared intentionality (P6) — theory of mind, joint attention
772
+ if (opts?.sharedIntentionalityContext) {
773
+ parts.push(opts.sharedIntentionalityContext);
510
774
  }
511
- // 11. Channel modifierexpression style per platform (between memory and empathy)
775
+ // 9e. Ethics (P6)manipulation detection, self-protection
776
+ if (opts?.ethicsContext) {
777
+ parts.push(opts.ethicsContext);
778
+ }
779
+ // 9f. Autonomic state (P7) — polyvagal nervous system state
780
+ if (opts?.autonomicDescription) {
781
+ parts.push(locale === "zh"
782
+ ? `[自主神经] ${opts.autonomicDescription}`
783
+ : `[Autonomic] ${opts.autonomicDescription}`);
784
+ }
785
+ // 9g. Primary systems behavioral tendencies (P9)
786
+ if (opts?.primarySystemsDescription) {
787
+ parts.push(locale === "zh"
788
+ ? `[行为倾向] ${opts.primarySystemsDescription}`
789
+ : `[Tendencies] ${opts.primarySystemsDescription}`);
790
+ }
791
+ // 10. Channel modifier — expression style per platform
512
792
  if (opts?.channelType) {
513
793
  const channelProfile = getChannelProfile(opts.channelType);
514
794
  parts.push(buildChannelModifier(channelProfile, locale));
515
795
  }
516
- // 12. Empathy report only when user shares feelings
517
- parts.push(locale === "zh"
518
- ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
519
- : `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch\nOtherwise no report needed.`);
796
+ // 11. LLM-assisted classification: when algorithm is uncertain, ask LLM to help
797
+ if (userText && !algoStimulus) {
798
+ if (locale === "zh") {
799
+ parts.push(`[分类协助]\n` +
800
+ `算法没有看出这句话的情绪类型。请你判断,在 <psyche_update> 中用 stimulus: 报告。\n` +
801
+ `可选类型: praise|criticism|humor|intellectual|intimacy|conflict|neglect|surprise|casual|sarcasm|authority|validation|boredom|vulnerability\n` +
802
+ `如果确实是无情绪的日常对话,报告 stimulus: casual`);
803
+ }
804
+ else {
805
+ parts.push(`[Classification assist]\n` +
806
+ `Algorithm couldn't determine the emotional type. Please judge and report stimulus: in <psyche_update>.\n` +
807
+ `Options: praise|criticism|humor|intellectual|intimacy|conflict|neglect|surprise|casual|sarcasm|authority|validation|boredom|vulnerability\n` +
808
+ `If genuinely neutral, report stimulus: casual`);
809
+ }
810
+ }
811
+ // 12. Empathy report — only inject when emotional sharing is likely
812
+ const emotionalStimuli = new Set(["vulnerability", "intimacy", "neglect"]);
813
+ if (!algoStimulus || emotionalStimuli.has(algoStimulus)) {
814
+ parts.push(locale === "zh"
815
+ ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
816
+ : `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch\nOtherwise no report needed.`);
817
+ }
520
818
  return parts.join("\n\n");
521
819
  }
@@ -19,6 +19,13 @@ export declare function pushSnapshot(state: PsycheState, stimulus: StimulusType
19
19
  * Get relationship for a specific user, or _default.
20
20
  */
21
21
  export declare function getRelationship(state: PsycheState, userId?: string): RelationshipState;
22
+ /**
23
+ * Compress the full emotionalHistory into a rich session summary and store it
24
+ * in the user's relationship.memory[]. Called ONCE at session end.
25
+ *
26
+ * Pure computation, no LLM calls.
27
+ */
28
+ export declare function compressSession(state: PsycheState, userId?: string): PsycheState;
22
29
  /**
23
30
  * Load psyche state from workspace. Auto-initializes if missing.
24
31
  * Handles v1→v2 migration transparently.
@@ -46,11 +53,18 @@ export declare function saveState(workspaceDir: string, state: PsycheState): Pro
46
53
  * Respects innate drives: uses effective baseline, decays drives too.
47
54
  */
48
55
  export declare function decayAndSave(workspaceDir: string, state: PsycheState): Promise<PsycheState>;
56
+ /** Result of parsing a <psyche_update> block */
57
+ export interface PsycheUpdateResult {
58
+ state: Partial<PsycheState>;
59
+ /** LLM-assisted stimulus classification (when algorithm was uncertain) */
60
+ llmStimulus?: StimulusType;
61
+ }
49
62
  /**
50
63
  * Parse a <psyche_update> block from LLM output.
51
64
  * v0.2: supports decimals, Chinese names, English names.
65
+ * v2.1: supports LLM-assisted stimulus classification.
52
66
  */
53
- export declare function parsePsycheUpdate(text: string, logger?: Logger): Partial<PsycheState> | null;
67
+ export declare function parsePsycheUpdate(text: string, logger?: Logger): PsycheUpdateResult | null;
54
68
  /**
55
69
  * Detect if LLM output contains disagreement/pushback.
56
70
  */