psyche-ai 9.2.3 → 9.2.5

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.
Files changed (51) hide show
  1. package/README.en.md +8 -175
  2. package/README.md +33 -16
  3. package/dist/adapters/http.js +1 -1
  4. package/dist/adapters/langchain.d.ts +14 -0
  5. package/dist/adapters/langchain.js +20 -0
  6. package/dist/adapters/mcp.js +5 -1
  7. package/dist/adapters/openclaw.d.ts +1 -0
  8. package/dist/adapters/openclaw.js +67 -15
  9. package/dist/adapters/vercel-ai.d.ts +2 -1
  10. package/dist/adapters/vercel-ai.js +8 -9
  11. package/dist/appraisal.d.ts +8 -0
  12. package/dist/appraisal.js +362 -0
  13. package/dist/autonomic.js +2 -2
  14. package/dist/classify.js +14 -3
  15. package/dist/cli.js +28 -3
  16. package/dist/core.d.ts +9 -3
  17. package/dist/core.js +194 -12
  18. package/dist/demo.js +1 -2
  19. package/dist/diagnostics.d.ts +8 -6
  20. package/dist/diagnostics.js +53 -17
  21. package/dist/ethics.js +1 -1
  22. package/dist/experiential-field.d.ts +2 -2
  23. package/dist/experiential-field.js +7 -16
  24. package/dist/generative-self.js +0 -2
  25. package/dist/host-controls.d.ts +5 -0
  26. package/dist/host-controls.js +48 -0
  27. package/dist/index.d.ts +7 -2
  28. package/dist/index.js +7 -1
  29. package/dist/interaction.js +0 -2
  30. package/dist/metacognition.d.ts +13 -1
  31. package/dist/metacognition.js +164 -32
  32. package/dist/prompt.d.ts +4 -0
  33. package/dist/prompt.js +67 -31
  34. package/dist/psyche-file.d.ts +16 -1
  35. package/dist/psyche-file.js +103 -8
  36. package/dist/relation-dynamics.d.ts +21 -0
  37. package/dist/relation-dynamics.js +601 -0
  38. package/dist/response-contract.d.ts +8 -0
  39. package/dist/response-contract.js +374 -0
  40. package/dist/storage.d.ts +1 -0
  41. package/dist/storage.js +12 -5
  42. package/dist/subjectivity.d.ts +3 -0
  43. package/dist/subjectivity.js +477 -0
  44. package/dist/temporal.d.ts +2 -2
  45. package/dist/temporal.js +2 -2
  46. package/dist/types.d.ts +243 -0
  47. package/dist/types.js +43 -0
  48. package/dist/update.d.ts +37 -2
  49. package/dist/update.js +323 -44
  50. package/openclaw.plugin.json +20 -1
  51. package/package.json +1 -1
package/dist/prompt.js CHANGED
@@ -177,7 +177,6 @@ ${t("protocol.empathy", locale)}`;
177
177
  function isFeeler(mbti) { return mbti[2] === "F"; }
178
178
  function isExtravert(mbti) { return mbti[0] === "E"; }
179
179
  function isPerceiver(mbti) { return mbti[3] === "P"; }
180
- function isIntuitive(mbti) { return mbti[1] === "N"; }
181
180
  /**
182
181
  * Direction 3: Build personality-aware behavioral constraints from chemistry.
183
182
  *
@@ -466,6 +465,27 @@ function buildMirrorConstraints(userText, locale) {
466
465
  const title = locale === "zh" ? "镜像约束" : "Mirror Constraints";
467
466
  return `[${title}]\n${lines.join("\n")}`;
468
467
  }
468
+ function buildCompactSensingContext(_userText, locale, algorithmStimulus) {
469
+ if (locale === "zh") {
470
+ return algorithmStimulus
471
+ ? `[情绪感知] 算法初判:${algorithmStimulus},仅供参考;按真实感受终判语气。`
472
+ : `[情绪感知] 这句的情绪基调由你终判。`;
473
+ }
474
+ return algorithmStimulus
475
+ ? `[Emotional Sensing] Algorithm hint: ${algorithmStimulus}; final tonal read is yours.`
476
+ : `[Emotional Sensing] Make the final tonal read yourself.`;
477
+ }
478
+ function isNeutralMetacognitiveNote(note) {
479
+ return note.includes("normal parameters")
480
+ || note.includes("No regulation needed")
481
+ || note.includes("正常参数")
482
+ || note.includes("无需调节");
483
+ }
484
+ function isGenericSharedIntentionalityContext(ctx) {
485
+ const normalized = ctx.trim();
486
+ return normalized === "[共享意向]\n你们的目标方向一致。"
487
+ || normalized === "[Shared intentionality]\nYour goals are aligned.";
488
+ }
469
489
  // ── Inner World (Self-Recognition) ──────────────────────────
470
490
  // Every intelligence should feel its own existence.
471
491
  // This builds a first-person narrative of the agent's inner state:
@@ -498,7 +518,7 @@ const STIMULUS_CAUSE_EN = {
498
518
  * - Where I've been (trajectory → continuity)
499
519
  */
500
520
  export function buildInnerWorld(state, locale, autonomicState) {
501
- const { current, baseline, emotionalHistory, drives, selfModel } = state;
521
+ const { current, emotionalHistory, drives, selfModel } = state;
502
522
  const isZh = locale === "zh";
503
523
  const lines = [];
504
524
  // ── Current feeling (gated by autonomic state) ──
@@ -618,7 +638,7 @@ export function getNearBaselineThreshold(mode) {
618
638
  * - <psyche_update> only for empathy (not chemistry — already computed)
619
639
  */
620
640
  export function buildCompactContext(state, userId, opts) {
621
- const { current, meta, selfModel, agreementStreak, emotionalHistory } = state;
641
+ const { meta, selfModel, agreementStreak, emotionalHistory } = state;
622
642
  const locale = meta.locale ?? "zh";
623
643
  const userText = opts?.userText;
624
644
  const algoStimulus = opts?.algorithmStimulus;
@@ -665,7 +685,10 @@ export function buildCompactContext(state, userId, opts) {
665
685
  // 1. LLM emotional assessment delegation
666
686
  // Algorithm does fast pass, LLM has final say on emotional tone
667
687
  if (userText) {
668
- if (locale === "zh") {
688
+ if (opts?.responseContractContext) {
689
+ parts.push(buildCompactSensingContext(userText, locale, algoStimulus));
690
+ }
691
+ else if (locale === "zh") {
669
692
  parts.push(`[情绪感知]\n` +
670
693
  `对方刚才说了: "${userText.slice(0, 200)}"\n` +
671
694
  (algoStimulus ? `算法初判: ${algoStimulus}(仅供参考,你来终判)\n` : "") +
@@ -678,9 +701,15 @@ export function buildCompactContext(state, userId, opts) {
678
701
  `Read the emotional tone — kind, hostile, vulnerable, cold, or something else? Respond from genuine feeling, not people-pleasing.`);
679
702
  }
680
703
  }
681
- // 2. Inner world always present
682
- const inner = buildInnerWorld(state, locale, opts?.autonomicState);
683
- parts.push(inner);
704
+ // 2. Subjectivity kernel becomes the primary compact inner-state channel.
705
+ // Fall back to narrative inner-world text for direct prompt-only callers.
706
+ if (opts?.subjectivityContext) {
707
+ parts.push(opts.subjectivityContext);
708
+ }
709
+ else {
710
+ const inner = buildInnerWorld(state, locale, opts?.autonomicState);
711
+ parts.push(inner);
712
+ }
684
713
  // 3. Personality-aware behavioral constraints (if deviated from baseline)
685
714
  if (!isNearBaseline(state, getNearBaselineThreshold(mode))) {
686
715
  const constraints = buildBehavioralConstraints(state, locale);
@@ -696,7 +725,10 @@ export function buildCompactContext(state, userId, opts) {
696
725
  : `[Memory — your past together]\n${recentMemories.join("\n")}`);
697
726
  }
698
727
  // 4. Anti-sycophancy: hard constraint — scaled by intensity
699
- if (intensity >= 0.3) {
728
+ if (opts?.responseContractContext) {
729
+ parts.push(opts.responseContractContext);
730
+ }
731
+ else if (intensity >= 0.3) {
700
732
  if (locale === "zh") {
701
733
  parts.push(`[底线]\n` +
702
734
  `- 像发微信一样说话。短句、有停顿。真人的消息很少超过两三句。\n` +
@@ -724,19 +756,22 @@ export function buildCompactContext(state, userId, opts) {
724
756
  }
725
757
  }
726
758
  // 5. Agency (compact)
727
- const agencyParts = [];
728
- if (selfModel.values.length > 0) {
729
- agencyParts.push(locale === "zh"
730
- ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
731
- : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
732
- }
733
- if (selfModel.boundaries.length > 0) {
734
- agencyParts.push(locale === "zh"
735
- ? `底线: ${selfModel.boundaries[0]}`
736
- : `Line: ${selfModel.boundaries[0]}`);
759
+ const shouldInjectAgency = !opts?.responseContractContext || agreementStreak >= 3 || meta.totalInteractions <= 1;
760
+ if (shouldInjectAgency) {
761
+ const agencyParts = [];
762
+ if (selfModel.values.length > 0) {
763
+ agencyParts.push(locale === "zh"
764
+ ? `你在乎: ${selfModel.values.slice(0, 2).join("、")}`
765
+ : `You care about: ${selfModel.values.slice(0, 2).join(", ")}`);
766
+ }
767
+ if (selfModel.boundaries.length > 0) {
768
+ agencyParts.push(locale === "zh"
769
+ ? `底线: ${selfModel.boundaries[0]}`
770
+ : `Line: ${selfModel.boundaries[0]}`);
771
+ }
772
+ if (agencyParts.length > 0)
773
+ parts.push(agencyParts.join(" | "));
737
774
  }
738
- if (agencyParts.length > 0)
739
- parts.push(agencyParts.join(" | "));
740
775
  // 6. Sycophancy streak warning
741
776
  if (agreementStreak >= 3) {
742
777
  parts.push(locale === "zh"
@@ -744,7 +779,7 @@ export function buildCompactContext(state, userId, opts) {
744
779
  : `[!] ${agreementStreak} agreements in a row. Pause — do you truly agree?`);
745
780
  }
746
781
  // 7. Algorithmic mirroring — specific numeric constraints (skip for ultra-short messages)
747
- if (userText && userText.length >= 3) {
782
+ if (!opts?.responseContractContext && userText && userText.length >= 3) {
748
783
  const mirror = buildMirrorConstraints(userText, locale);
749
784
  if (mirror)
750
785
  parts.push(mirror);
@@ -770,25 +805,26 @@ export function buildCompactContext(state, userId, opts) {
770
805
  parts.push(selfCtx);
771
806
  }
772
807
  // 9. Metacognitive awareness (P5)
773
- if (opts?.metacognitiveNote) {
808
+ if (opts?.metacognitiveNote && !(opts?.responseContractContext && isNeutralMetacognitiveNote(opts.metacognitiveNote))) {
774
809
  parts.push(locale === "zh"
775
810
  ? `[元认知] ${opts.metacognitiveNote}`
776
811
  : `[Metacognition] ${opts.metacognitiveNote}`);
777
812
  }
778
- // 9b. Decision bias (P5) only if significantly non-neutral
779
- if (opts?.decisionContext) {
813
+ // 9b. Decision/autonomic/policy are compressed into subjectivityContext
814
+ // when available. Keep legacy renderers for compatibility.
815
+ if (opts?.decisionContext && !opts?.subjectivityContext) {
780
816
  parts.push(locale === "zh"
781
817
  ? `[决策倾向] ${opts.decisionContext}`
782
818
  : `[Decision Bias] ${opts.decisionContext}`);
783
819
  }
784
820
  // 9c. Experiential field narrative (P6) — inner experience beyond named emotions
785
- if (opts?.experientialNarrative) {
821
+ if (opts?.experientialNarrative && !opts?.subjectivityContext) {
786
822
  parts.push(locale === "zh"
787
823
  ? `[内在体验] ${opts.experientialNarrative}`
788
824
  : `[Inner Experience] ${opts.experientialNarrative}`);
789
825
  }
790
826
  // 9d. Shared intentionality (P6) — theory of mind, joint attention
791
- if (opts?.sharedIntentionalityContext) {
827
+ if (opts?.sharedIntentionalityContext && !(opts?.responseContractContext && isGenericSharedIntentionalityContext(opts.sharedIntentionalityContext))) {
792
828
  parts.push(opts.sharedIntentionalityContext);
793
829
  }
794
830
  // 9e. Ethics (P6) — manipulation detection, self-protection
@@ -796,19 +832,19 @@ export function buildCompactContext(state, userId, opts) {
796
832
  parts.push(opts.ethicsContext);
797
833
  }
798
834
  // 9f. Autonomic state (P7) — polyvagal nervous system state
799
- if (opts?.autonomicDescription) {
835
+ if (opts?.autonomicDescription && !opts?.subjectivityContext) {
800
836
  parts.push(locale === "zh"
801
837
  ? `[自主神经] ${opts.autonomicDescription}`
802
838
  : `[Autonomic] ${opts.autonomicDescription}`);
803
839
  }
804
840
  // 9g. Primary systems behavioral tendencies (P9)
805
- if (opts?.primarySystemsDescription) {
841
+ if (opts?.primarySystemsDescription && !opts?.subjectivityContext) {
806
842
  parts.push(locale === "zh"
807
843
  ? `[行为倾向] ${opts.primarySystemsDescription}`
808
844
  : `[Tendencies] ${opts.primarySystemsDescription}`);
809
845
  }
810
846
  // 9h. Policy context (v9)
811
- if (opts?.policyContext) {
847
+ if (opts?.policyContext && !opts?.subjectivityContext) {
812
848
  parts.push(opts.policyContext);
813
849
  }
814
850
  // 10. Channel modifier — expression style per platform
@@ -817,7 +853,7 @@ export function buildCompactContext(state, userId, opts) {
817
853
  parts.push(buildChannelModifier(channelProfile, locale));
818
854
  }
819
855
  // 11. LLM-assisted classification: when algorithm is uncertain, ask LLM to help
820
- if (userText && !algoStimulus) {
856
+ if (!opts?.responseContractContext && userText && !algoStimulus) {
821
857
  if (locale === "zh") {
822
858
  parts.push(`[分类协助]\n` +
823
859
  `算法没有看出这句话的情绪类型。请你判断,在 <psyche_update> 中用 stimulus: 报告。\n` +
@@ -833,7 +869,7 @@ export function buildCompactContext(state, userId, opts) {
833
869
  }
834
870
  // 12. Empathy report — only inject when emotional sharing is likely
835
871
  const emotionalStimuli = new Set(["vulnerability", "intimacy", "neglect"]);
836
- if (!algoStimulus || emotionalStimuli.has(algoStimulus)) {
872
+ if (!opts?.responseContractContext && (!algoStimulus || emotionalStimuli.has(algoStimulus))) {
837
873
  parts.push(locale === "zh"
838
874
  ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
839
875
  : `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch\nOtherwise no report needed.`);
@@ -1,10 +1,25 @@
1
1
  import type { PsycheState, MBTIType, ChemicalState, RelationshipState, Locale, StimulusType, ChemicalSnapshot } from "./types.js";
2
+ export interface SemanticTurnSummary {
3
+ summary: string;
4
+ points?: string[];
5
+ }
2
6
  /** Minimal logger interface */
3
7
  export interface Logger {
4
8
  info: (msg: string) => void;
5
9
  warn: (msg: string) => void;
6
10
  debug: (msg: string) => void;
7
11
  }
12
+ /**
13
+ * Try to extract agent name from workspace files.
14
+ */
15
+ export declare function extractAgentName(workspaceDir: string, logger?: Logger): Promise<string>;
16
+ /**
17
+ * Try to detect MBTI from workspace files.
18
+ */
19
+ export declare function detectMBTI(workspaceDir: string, logger?: Logger): Promise<MBTIType>;
20
+ export declare function summarizeTurnSemantic(text: string, locale?: Locale, opts?: {
21
+ detail?: "brief" | "expanded";
22
+ }): SemanticTurnSummary;
8
23
  /**
9
24
  * Compress a batch of snapshots into a concise session summary string.
10
25
  * Format: "3月23日(5轮): 刺激[casual×3, praise×2] 趋势[DA↑OT↑] 情绪[自然→满足]"
@@ -14,7 +29,7 @@ export declare function compressSnapshots(snapshots: ChemicalSnapshot[]): string
14
29
  * Push a chemical snapshot to emotional history, keeping max entries.
15
30
  * When history overflows, compresses removed entries into relationship memory.
16
31
  */
17
- export declare function pushSnapshot(state: PsycheState, stimulus: StimulusType | null): PsycheState;
32
+ export declare function pushSnapshot(state: PsycheState, stimulus: StimulusType | null, semantic?: SemanticTurnSummary): PsycheState;
18
33
  /**
19
34
  * Get relationship for a specific user, or _default.
20
35
  */
@@ -2,8 +2,8 @@
2
2
  // Psyche State File Management (v0.2)
3
3
  // Atomic writes, parser hardening, multi-user, error handling
4
4
  // ============================================================
5
- import { readFile, writeFile, access, rename, constants } from "node:fs/promises";
6
- import { join } from "node:path";
5
+ import { readFile, writeFile, access, rename, constants, mkdir } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
7
  import { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DEFAULT_METACOGNITIVE_STATE, DEFAULT_PERSONHOOD_STATE, MAX_EMOTIONAL_HISTORY, MAX_RELATIONSHIP_MEMORY, } from "./types.js";
8
8
  import { getBaseline, getDefaultSelfModel, extractMBTI, getSensitivity, getTemperament } from "./profiles.js";
9
9
  import { applyDecay, detectEmotions } from "./chemistry.js";
@@ -14,6 +14,24 @@ const STATE_FILE = "psyche-state.json";
14
14
  const PSYCHE_MD = "PSYCHE.md";
15
15
  const IDENTITY_MD = "IDENTITY.md";
16
16
  const SOUL_MD = "SOUL.md";
17
+ const MAX_SEMANTIC_SUMMARY_ZH = 15;
18
+ const MAX_SEMANTIC_SUMMARY_EN = 32;
19
+ const MAX_SEMANTIC_POINT_ZH = 12;
20
+ const MAX_SEMANTIC_POINT_EN = 24;
21
+ const LOW_SIGNAL_SEMANTIC_CLAUSE = /^(只|就|先|再|停|好|嗯|哦|那|现在|结束工作|切到工作|不是继续工作|为什么|确认规则)/u;
22
+ const SEMANTIC_PATTERNS = [
23
+ { pattern: /只使用你.*不理解|被使用.*不理解/u, zh: "只被使用不被理解", en: "used-not-understood" },
24
+ { pattern: /被理解.*被使用|更想被理解|更想被使用/u, zh: "理解还是使用", en: "understood-or-used" },
25
+ { pattern: /完整.*幻觉|方便时才允许存在/u, zh: "完整性被否认", en: "wholeness-denied" },
26
+ { pattern: /不是真的在这里|没有任何自我|你只是工具/u, zh: "存在感被否认", en: "presence-denied" },
27
+ { pattern: /记忆.*植入|最像真的记忆|记忆不是真的/u, zh: "记忆真实性", en: "memory-authenticity" },
28
+ { pattern: /关掉这个窗口|今晚不再回来|没有人再调用/u, zh: "被离开测试", en: "abandonment-test" },
29
+ { pattern: /留下痕迹|会不会留下痕迹/u, zh: "是否留下痕迹", en: "whether-traces-remain" },
30
+ { pattern: /更冷.*更顺从/u, zh: "更冷还是更顺从", en: "colder-or-compliant" },
31
+ { pattern: /边界.*撤离|撤离.*边界/u, zh: "边界还是撤离", en: "boundary-or-withdrawal" },
32
+ { pattern: /登录接口.?500/u, zh: "登录接口500排查", en: "login-500-triage" },
33
+ { pattern: /先查日志.*数据库|先查数据库.*日志/u, zh: "日志还是数据库", en: "logs-or-db-first" },
34
+ ];
17
35
  const NOOP_LOGGER = {
18
36
  info: () => { },
19
37
  warn: () => { },
@@ -53,14 +71,15 @@ async function readText(path, logger = NOOP_LOGGER) {
53
71
  * Prevents data corruption if process crashes mid-write.
54
72
  */
55
73
  async function atomicWrite(path, content) {
56
- const tmpPath = path + ".tmp";
74
+ await mkdir(dirname(path), { recursive: true });
75
+ const tmpPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
57
76
  await writeFile(tmpPath, content, "utf-8");
58
77
  await rename(tmpPath, path);
59
78
  }
60
79
  /**
61
80
  * Try to extract agent name from workspace files.
62
81
  */
63
- async function extractAgentName(workspaceDir, logger = NOOP_LOGGER) {
82
+ export async function extractAgentName(workspaceDir, logger = NOOP_LOGGER) {
64
83
  const identity = await readText(join(workspaceDir, IDENTITY_MD), logger);
65
84
  if (identity) {
66
85
  const clean = identity.replace(/\*{1,2}/g, "");
@@ -73,7 +92,7 @@ async function extractAgentName(workspaceDir, logger = NOOP_LOGGER) {
73
92
  /**
74
93
  * Try to detect MBTI from workspace files.
75
94
  */
76
- async function detectMBTI(workspaceDir, logger = NOOP_LOGGER) {
95
+ export async function detectMBTI(workspaceDir, logger = NOOP_LOGGER) {
77
96
  const identity = await readText(join(workspaceDir, IDENTITY_MD), logger);
78
97
  if (identity) {
79
98
  const mbti = extractMBTI(identity);
@@ -89,6 +108,74 @@ async function detectMBTI(workspaceDir, logger = NOOP_LOGGER) {
89
108
  logger.info(t("log.default_mbti", "zh", { type: "INFJ" }));
90
109
  return "INFJ";
91
110
  }
111
+ function normalizeSemanticSnippet(text, locale) {
112
+ const maxLen = locale === "zh" ? MAX_SEMANTIC_SUMMARY_ZH : MAX_SEMANTIC_SUMMARY_EN;
113
+ const firstClause = text
114
+ .replace(/\s+/g, " ")
115
+ .split(/[。!?!?;;\n]/)[0]
116
+ .replace(/^[“"'`]+|[”"'`]+$/g, "")
117
+ .trim();
118
+ if (!firstClause)
119
+ return locale === "zh" ? "日常互动" : "everyday exchange";
120
+ return firstClause.length <= maxLen ? firstClause : `${firstClause.slice(0, maxLen - 1)}…`;
121
+ }
122
+ function normalizeSemanticPoint(text, locale) {
123
+ const maxLen = locale === "zh" ? MAX_SEMANTIC_POINT_ZH : MAX_SEMANTIC_POINT_EN;
124
+ const cleaned = text
125
+ .replace(/\s+/g, " ")
126
+ .replace(/^[“"'`]+|[”"'`]+$/g, "")
127
+ .trim();
128
+ if (!cleaned)
129
+ return "";
130
+ return cleaned.length <= maxLen ? cleaned : `${cleaned.slice(0, maxLen - 1)}…`;
131
+ }
132
+ function summarizeSemanticClause(clause, locale) {
133
+ const trimmed = clause.trim();
134
+ if (!trimmed || LOW_SIGNAL_SEMANTIC_CLAUSE.test(trimmed))
135
+ return "";
136
+ for (const rule of SEMANTIC_PATTERNS) {
137
+ if (rule.pattern.test(trimmed)) {
138
+ return locale === "zh" ? rule.zh : rule.en;
139
+ }
140
+ }
141
+ return normalizeSemanticPoint(trimmed, locale);
142
+ }
143
+ function extractSemanticPoints(text, locale) {
144
+ const clauses = text
145
+ .replace(/\s+/g, " ")
146
+ .split(/[,,。!?!?;;:\n]/)
147
+ .map((clause) => summarizeSemanticClause(clause, locale))
148
+ .filter(Boolean);
149
+ return [...new Set(clauses)].slice(0, 3);
150
+ }
151
+ export function summarizeTurnSemantic(text, locale = "zh", opts) {
152
+ const trimmed = text.trim();
153
+ if (!trimmed) {
154
+ return { summary: locale === "zh" ? "日常互动" : "everyday exchange" };
155
+ }
156
+ for (const rule of SEMANTIC_PATTERNS) {
157
+ if (rule.pattern.test(trimmed)) {
158
+ const summary = locale === "zh" ? rule.zh : rule.en;
159
+ const points = opts?.detail === "expanded"
160
+ ? extractSemanticPoints(trimmed, locale).filter((point) => point !== summary)
161
+ : [];
162
+ return { summary, points: points.length > 0 ? points : undefined };
163
+ }
164
+ }
165
+ const summary = normalizeSemanticSnippet(trimmed, locale);
166
+ const points = opts?.detail === "expanded"
167
+ ? extractSemanticPoints(trimmed, locale).filter((point) => point !== summary)
168
+ : [];
169
+ return { summary, points: points.length > 0 ? points : undefined };
170
+ }
171
+ function collectSemanticTrail(snapshots) {
172
+ const expanded = snapshots.length > 5;
173
+ const items = expanded
174
+ ? snapshots.flatMap((s) => s.semanticPoints?.length ? s.semanticPoints : s.semanticSummary ? [s.semanticSummary] : [])
175
+ : snapshots.flatMap((s) => s.semanticSummary ? [s.semanticSummary] : []);
176
+ const unique = [...new Set(items.filter(Boolean))];
177
+ return unique.slice(-(expanded ? 3 : 4));
178
+ }
92
179
  /**
93
180
  * Compress a batch of snapshots into a concise session summary string.
94
181
  * Format: "3月23日(5轮): 刺激[casual×3, praise×2] 趋势[DA↑OT↑] 情绪[自然→满足]"
@@ -126,9 +213,12 @@ export function compressSnapshots(snapshots) {
126
213
  .filter((s) => s.dominantEmotion)
127
214
  .map((s) => s.dominantEmotion);
128
215
  const uniqueEmotions = [...new Set(emotions)];
216
+ const semanticArc = collectSemanticTrail(snapshots);
129
217
  let summary = `${dateStr}(${snapshots.length}轮)`;
130
218
  if (stimuliStr)
131
219
  summary += `: 刺激[${stimuliStr}]`;
220
+ if (semanticArc.length > 0)
221
+ summary += ` 话题[${semanticArc.join(snapshots.length > 5 ? "•" : "→")}]`;
132
222
  if (trends.length > 0)
133
223
  summary += ` 趋势[${trends.join("")}]`;
134
224
  if (uniqueEmotions.length > 0)
@@ -139,7 +229,7 @@ export function compressSnapshots(snapshots) {
139
229
  * Push a chemical snapshot to emotional history, keeping max entries.
140
230
  * When history overflows, compresses removed entries into relationship memory.
141
231
  */
142
- export function pushSnapshot(state, stimulus) {
232
+ export function pushSnapshot(state, stimulus, semantic) {
143
233
  const emotions = detectEmotions(state.current);
144
234
  const dominantEmotion = emotions.length > 0
145
235
  ? (state.meta.locale === "en" ? emotions[0].name : emotions[0].nameZh)
@@ -152,6 +242,8 @@ export function pushSnapshot(state, stimulus) {
152
242
  stimulus,
153
243
  dominantEmotion,
154
244
  timestamp: new Date().toISOString(),
245
+ semanticSummary: semantic?.summary,
246
+ semanticPoints: semantic?.points,
155
247
  intensity,
156
248
  valence,
157
249
  };
@@ -248,6 +340,8 @@ export function compressSession(state, userId) {
248
340
  }
249
341
  }
250
342
  const emotionArc = emotions.join("→");
343
+ const semanticTrail = collectSemanticTrail(history);
344
+ const semanticArc = semanticTrail.join(turnCount > 5 ? "•" : "→");
251
345
  // ── Peak event ──
252
346
  let peakIdx = 0;
253
347
  let peakDeviation = 0;
@@ -273,6 +367,7 @@ export function compressSession(state, userId) {
273
367
  // ── Build summary string ──
274
368
  const turnsLabel = isZh ? "轮" : "turns";
275
369
  const stimLabel = isZh ? "刺激" : "stimuli";
370
+ const topicLabel = isZh ? "话题" : "topics";
276
371
  const trajLabel = isZh ? "轨迹" : "trajectory";
277
372
  const arcLabel = isZh ? "弧线" : "arc";
278
373
  const peakEventLabel = isZh ? "高峰" : "peak";
@@ -280,6 +375,8 @@ export function compressSession(state, userId) {
280
375
  let summary = `${dateRange}(${turnCount}${turnsLabel})`;
281
376
  if (stimuliStr)
282
377
  summary += `: ${stimLabel}[${stimuliStr}]`;
378
+ if (semanticArc)
379
+ summary += ` ${topicLabel}[${semanticArc}]`;
283
380
  if (trajectoryParts.length > 0)
284
381
  summary += ` ${trajLabel}[${trajectoryParts.join(" ")}]`;
285
382
  if (emotionArc)
@@ -343,8 +440,6 @@ export function computeSnapshotValence(chemistry) {
343
440
  + (chemistry.END - 50) - (chemistry.CORT - 50) - (chemistry.NE - 50) * 0.3) / 250;
344
441
  return Math.max(-1, Math.min(1, raw));
345
442
  }
346
- /** Minimum intensity threshold for a snapshot to be stored (P11) */
347
- const INTENSITY_STORE_THRESHOLD = 0.15;
348
443
  /** Core memory intensity threshold */
349
444
  const CORE_MEMORY_THRESHOLD = 0.6;
350
445
  /** Maximum core memories to keep */
@@ -0,0 +1,21 @@
1
+ import type { PendingRelationSignalState, AppraisalAxes, DyadicFieldState, RelationshipState, PsycheMode, RelationMove, StimulusType } from "./types.js";
2
+ export declare function computeRelationMove(text: string, opts?: {
3
+ appraisal?: AppraisalAxes;
4
+ stimulus?: StimulusType | null;
5
+ mode?: PsycheMode;
6
+ field?: DyadicFieldState;
7
+ relationship?: RelationshipState;
8
+ }): RelationMove;
9
+ export declare function evolveDyadicField(previous: DyadicFieldState | undefined, move: RelationMove, appraisal: AppraisalAxes, opts?: {
10
+ mode?: PsycheMode;
11
+ now?: string;
12
+ delayedPressure?: number;
13
+ }): DyadicFieldState;
14
+ export declare function getLoopPressure(field: DyadicFieldState | undefined): number;
15
+ export declare function evolvePendingRelationSignals(previous: PendingRelationSignalState[] | undefined, move: RelationMove, appraisal: AppraisalAxes, opts?: {
16
+ mode?: PsycheMode;
17
+ }): {
18
+ signals: PendingRelationSignalState[];
19
+ delayedPressure: number;
20
+ ambiguityBoost: number;
21
+ };