psyche-ai 3.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/prompt.js CHANGED
@@ -15,7 +15,7 @@ import { getChannelProfile, buildChannelModifier } from "./channels.js";
15
15
  *
16
16
  * This is the "current moment" — what the agent is feeling RIGHT NOW.
17
17
  */
18
- export function buildDynamicContext(state, userId) {
18
+ export function buildDynamicContext(state, userId, opts) {
19
19
  const { current, baseline, mbti, empathyLog, selfModel, meta, agreementStreak, emotionalHistory } = state;
20
20
  const locale = meta.locale ?? "zh";
21
21
  const relationship = getRelationship(state, userId);
@@ -88,6 +88,16 @@ export function buildDynamicContext(state, userId) {
88
88
  if (reciprocity) {
89
89
  parts.push("", reciprocity);
90
90
  }
91
+ // Metacognitive awareness (P5)
92
+ if (opts?.metacognitiveNote) {
93
+ const mcTitle = locale === "zh" ? "元认知" : "Metacognition";
94
+ parts.push("", `[${mcTitle}] ${opts.metacognitiveNote}`);
95
+ }
96
+ // Decision bias context (P5)
97
+ if (opts?.decisionContext) {
98
+ const dbTitle = locale === "zh" ? "决策倾向" : "Decision Bias";
99
+ parts.push("", `[${dbTitle}] ${opts.decisionContext}`);
100
+ }
91
101
  parts.push("", agencyReminder, sycophancyWarning, "", t("dynamic.update_reminder", locale));
92
102
  return parts.filter((l) => l !== undefined).join("\n");
93
103
  }
@@ -478,7 +488,19 @@ export function buildCompactContext(state, userId, opts) {
478
488
  if (selfCtx)
479
489
  parts.push(selfCtx);
480
490
  }
481
- // 9. Cross-session emotional memory — surface relationship history
491
+ // 9. Metacognitive awareness (P5)
492
+ if (opts?.metacognitiveNote) {
493
+ parts.push(locale === "zh"
494
+ ? `[元认知] ${opts.metacognitiveNote}`
495
+ : `[Metacognition] ${opts.metacognitiveNote}`);
496
+ }
497
+ // 9b. Decision bias (P5) — only if significantly non-neutral
498
+ if (opts?.decisionContext) {
499
+ parts.push(locale === "zh"
500
+ ? `[决策倾向] ${opts.decisionContext}`
501
+ : `[Decision Bias] ${opts.decisionContext}`);
502
+ }
503
+ // 10. Cross-session emotional memory — surface relationship history
482
504
  const relationship = getRelationship(state, userId);
483
505
  if (relationship.memory && relationship.memory.length > 0) {
484
506
  const memLines = relationship.memory.slice(-3); // last 3 sessions
@@ -486,12 +508,12 @@ export function buildCompactContext(state, userId, opts) {
486
508
  ? `[记忆]\n${memLines.join("\n")}`
487
509
  : `[Memory]\n${memLines.join("\n")}`);
488
510
  }
489
- // 10. Channel modifier — expression style per platform (between memory and empathy)
511
+ // 11. Channel modifier — expression style per platform (between memory and empathy)
490
512
  if (opts?.channelType) {
491
513
  const channelProfile = getChannelProfile(opts.channelType);
492
514
  parts.push(buildChannelModifier(channelProfile, locale));
493
515
  }
494
- // 11. Empathy report — only when user shares feelings
516
+ // 12. Empathy report — only when user shares feelings
495
517
  parts.push(locale === "zh"
496
518
  ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
497
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.`);
@@ -4,7 +4,7 @@
4
4
  // ============================================================
5
5
  import { readFile, writeFile, access, rename, constants } from "node:fs/promises";
6
6
  import { join } from "node:path";
7
- import { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, MAX_EMOTIONAL_HISTORY, MAX_RELATIONSHIP_MEMORY, } from "./types.js";
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";
10
10
  import { decayDrives, computeEffectiveBaseline } from "./drives.js";
@@ -241,11 +241,15 @@ export function migrateToLatest(raw, fallbackName) {
241
241
  }
242
242
  // v2→v3: add drives
243
243
  // v3→v4: add learning
244
+ // v4→v5: add metacognition
245
+ // v5→v6: add personhood
244
246
  return {
245
247
  ...state,
246
- version: 4,
248
+ version: 6,
247
249
  drives: state.drives ?? { ...DEFAULT_DRIVES },
248
250
  learning: state.learning ?? { ...DEFAULT_LEARNING_STATE },
251
+ metacognition: state.metacognition ?? { ...DEFAULT_METACOGNITIVE_STATE },
252
+ personhood: state.personhood ?? { ...DEFAULT_PERSONHOOD_STATE },
249
253
  };
250
254
  }
251
255
  /**
@@ -259,7 +263,7 @@ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER)
259
263
  const selfModel = getDefaultSelfModel(mbti);
260
264
  const now = new Date().toISOString();
261
265
  const state = {
262
- version: 4,
266
+ version: 6,
263
267
  mbti,
264
268
  baseline,
265
269
  current: { ...baseline },
@@ -274,6 +278,8 @@ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER)
274
278
  agreementStreak: 0,
275
279
  lastDisagreement: null,
276
280
  learning: { ...DEFAULT_LEARNING_STATE },
281
+ metacognition: { ...DEFAULT_METACOGNITIVE_STATE },
282
+ personhood: { ...DEFAULT_PERSONHOOD_STATE },
277
283
  meta: {
278
284
  agentName,
279
285
  createdAt: now,
@@ -0,0 +1,72 @@
1
+ import type { PsycheState, StimulusType, RelationshipState, Locale } from "./types.js";
2
+ /** Estimated mood of the other party */
3
+ export type EstimatedMood = "positive" | "negative" | "neutral" | "mixed";
4
+ /** Estimated intent of the other party */
5
+ export type EstimatedIntent = "collaborative" | "adversarial" | "disengaged" | "exploratory" | "support-seeking";
6
+ /** Simplified model of the other's mental state */
7
+ export interface TheoryOfMindModel {
8
+ estimatedMood: EstimatedMood;
9
+ estimatedIntent: EstimatedIntent;
10
+ confidence: number;
11
+ lastUpdated: string;
12
+ }
13
+ /** What we're jointly focused on */
14
+ export interface JointAttentionTopic {
15
+ topic: string;
16
+ initiator: "self" | "other";
17
+ turnsSustained: number;
18
+ engagement: number;
19
+ }
20
+ /** Are we pulling in the same direction? */
21
+ export interface GoalAlignment {
22
+ aligned: boolean;
23
+ divergence: number;
24
+ description: string;
25
+ }
26
+ /** Top-level shared intentionality state */
27
+ export interface SharedIntentionalityState {
28
+ /** Current joint attention topic, null if no shared focus */
29
+ jointAttention: JointAttentionTopic | null;
30
+ /** Goal alignment: are we working toward the same thing? */
31
+ goalAlignment: GoalAlignment;
32
+ /** Theory of mind: model of the other's mental state */
33
+ theoryOfMind: TheoryOfMindModel;
34
+ /** Mutual awareness: does the other know I'm attending to them? (0-1) */
35
+ mutualAwareness: number;
36
+ }
37
+ export declare const DEFAULT_THEORY_OF_MIND: TheoryOfMindModel;
38
+ export declare const DEFAULT_GOAL_ALIGNMENT: GoalAlignment;
39
+ export declare const DEFAULT_SHARED_INTENTIONALITY: SharedIntentionalityState;
40
+ /**
41
+ * Infer the other's emotional state from the detected stimulus.
42
+ *
43
+ * Uses stimulus type as the primary signal, calibrated by
44
+ * relationship history. Longer, deeper relationships yield
45
+ * higher confidence — you read familiar people better.
46
+ */
47
+ export declare function estimateOtherMood(stimulus: StimulusType | null, relationship: RelationshipState, previous?: TheoryOfMindModel): {
48
+ mood: EstimatedMood;
49
+ confidence: number;
50
+ };
51
+ /**
52
+ * Main update function for shared intentionality.
53
+ *
54
+ * Takes the agent's psyche state, a detected stimulus from the other,
55
+ * and optionally the previous shared intentionality state.
56
+ * Returns the updated state with refreshed theory of mind,
57
+ * joint attention, goal alignment, and mutual awareness.
58
+ *
59
+ * @param psyche Current PsycheState of this agent
60
+ * @param stimulus Detected stimulus type from the other's message (null if none)
61
+ * @param userId User/agent ID for relationship lookup
62
+ * @param previous Previous SharedIntentionalityState (null on first turn)
63
+ */
64
+ export declare function updateSharedIntentionality(psyche: PsycheState, stimulus: StimulusType | null, userId?: string, previous?: SharedIntentionalityState | null): SharedIntentionalityState;
65
+ /**
66
+ * Build a compact prompt-injectable context string for shared intentionality.
67
+ *
68
+ * Returns empty string when there's nothing meaningful to report
69
+ * (low confidence, no joint attention, early interaction).
70
+ * Only injects when the shared state carries useful signal.
71
+ */
72
+ export declare function buildSharedIntentionalityContext(state: SharedIntentionalityState, locale: Locale): string;
@@ -0,0 +1,486 @@
1
+ // ============================================================
2
+ // Shared Intentionality (共享意向性) — Joint Attention & Theory of Mind
3
+ //
4
+ // P6: Digital Personhood. Goes beyond empathy (feeling what the
5
+ // other feels) into joint attention — knowing that we're both
6
+ // thinking about the same thing, and knowing that the other
7
+ // knows this.
8
+ //
9
+ // Pure computation, zero LLM calls, zero dependencies.
10
+ // 1. Theory of Mind — simplified model of the other's mental state
11
+ // 2. Joint Attention — detecting sustained shared focus
12
+ // 3. Goal Alignment — are we pulling in the same direction?
13
+ // 4. Mutual Awareness — does the other know I'm attending to them?
14
+ // ============================================================
15
+ // ── Defaults ─────────────────────────────────────────────────
16
+ export const DEFAULT_THEORY_OF_MIND = {
17
+ estimatedMood: "neutral",
18
+ estimatedIntent: "exploratory",
19
+ confidence: 0.2,
20
+ lastUpdated: new Date().toISOString(),
21
+ };
22
+ export const DEFAULT_GOAL_ALIGNMENT = {
23
+ aligned: false,
24
+ divergence: 0.5,
25
+ description: "No goal alignment established yet.",
26
+ };
27
+ export const DEFAULT_SHARED_INTENTIONALITY = {
28
+ jointAttention: null,
29
+ goalAlignment: { ...DEFAULT_GOAL_ALIGNMENT },
30
+ theoryOfMind: { ...DEFAULT_THEORY_OF_MIND },
31
+ mutualAwareness: 0,
32
+ };
33
+ // ── Stimulus → Mood Mapping ──────────────────────────────────
34
+ /** Maps stimulus types to their most likely mood signal from the other */
35
+ const STIMULUS_MOOD_MAP = {
36
+ praise: "positive",
37
+ validation: "positive",
38
+ intimacy: "positive",
39
+ humor: "positive",
40
+ surprise: "positive",
41
+ casual: "neutral",
42
+ intellectual: "neutral",
43
+ vulnerability: "mixed",
44
+ sarcasm: "negative",
45
+ criticism: "negative",
46
+ authority: "negative",
47
+ conflict: "negative",
48
+ neglect: "negative",
49
+ boredom: "negative",
50
+ };
51
+ /** Maps stimulus types to their most likely intent signal */
52
+ const STIMULUS_INTENT_MAP = {
53
+ praise: "collaborative",
54
+ validation: "collaborative",
55
+ intimacy: "collaborative",
56
+ humor: "collaborative",
57
+ intellectual: "exploratory",
58
+ surprise: "exploratory",
59
+ casual: "disengaged",
60
+ boredom: "disengaged",
61
+ neglect: "disengaged",
62
+ vulnerability: "support-seeking",
63
+ sarcasm: "adversarial",
64
+ criticism: "adversarial",
65
+ conflict: "adversarial",
66
+ authority: "adversarial",
67
+ };
68
+ /** Stimuli that signal the other is actively engaged with us */
69
+ const HIGH_ENGAGEMENT_STIMULI = new Set([
70
+ "praise", "validation", "intimacy", "intellectual",
71
+ "humor", "vulnerability", "conflict", "criticism",
72
+ ]);
73
+ /** Stimuli that signal the other is pulling away */
74
+ const LOW_ENGAGEMENT_STIMULI = new Set([
75
+ "neglect", "boredom", "casual",
76
+ ]);
77
+ // ── EMA smoothing factor ─────────────────────────────────────
78
+ const EMA_ALPHA = 0.3;
79
+ // ── 1. estimateOtherMood ─────────────────────────────────────
80
+ /**
81
+ * Infer the other's emotional state from the detected stimulus.
82
+ *
83
+ * Uses stimulus type as the primary signal, calibrated by
84
+ * relationship history. Longer, deeper relationships yield
85
+ * higher confidence — you read familiar people better.
86
+ */
87
+ export function estimateOtherMood(stimulus, relationship, previous) {
88
+ // No stimulus — hold previous estimate with decaying confidence
89
+ if (stimulus === null) {
90
+ if (previous) {
91
+ return {
92
+ mood: previous.estimatedMood,
93
+ confidence: Math.max(0.1, previous.confidence * 0.85),
94
+ };
95
+ }
96
+ return { mood: "neutral", confidence: 0.15 };
97
+ }
98
+ const rawMood = STIMULUS_MOOD_MAP[stimulus];
99
+ // Confidence from relationship depth: strangers → low, deep → high
100
+ const phaseConfidence = {
101
+ stranger: 0.25,
102
+ acquaintance: 0.40,
103
+ familiar: 0.60,
104
+ close: 0.75,
105
+ deep: 0.90,
106
+ };
107
+ const depthConfidence = phaseConfidence[relationship.phase] ?? 0.30;
108
+ // Trust calibrates confidence — low trust means we're less sure
109
+ // of our read on them
110
+ const trustFactor = relationship.trust / 100;
111
+ // Combine: depth provides the ceiling, trust scales within it
112
+ let confidence = depthConfidence * (0.5 + 0.5 * trustFactor);
113
+ // If previous estimate exists and mood matches, confidence rises (consistency)
114
+ if (previous && previous.estimatedMood === rawMood) {
115
+ confidence = Math.min(1, confidence + 0.1);
116
+ }
117
+ // Mixed mood detection: if stimulus contradicts previous estimate,
118
+ // the truth might be "mixed"
119
+ let finalMood = rawMood;
120
+ if (previous && previous.confidence > 0.4) {
121
+ const prevPositive = previous.estimatedMood === "positive";
122
+ const nowNegative = rawMood === "negative";
123
+ const prevNegative = previous.estimatedMood === "negative";
124
+ const nowPositive = rawMood === "positive";
125
+ if ((prevPositive && nowNegative) || (prevNegative && nowPositive)) {
126
+ finalMood = "mixed";
127
+ confidence *= 0.8; // contradiction lowers confidence
128
+ }
129
+ }
130
+ return { mood: finalMood, confidence: clamp01(confidence) };
131
+ }
132
+ // ── 2. updateTheoryOfMind ────────────────────────────────────
133
+ /**
134
+ * Update the theory of mind model based on a new stimulus and
135
+ * relationship context.
136
+ */
137
+ function updateTheoryOfMind(stimulus, relationship, previous) {
138
+ const { mood, confidence: moodConfidence } = estimateOtherMood(stimulus, relationship, previous);
139
+ // Intent estimation
140
+ let estimatedIntent;
141
+ if (stimulus === null) {
142
+ estimatedIntent = previous?.estimatedIntent ?? "exploratory";
143
+ }
144
+ else {
145
+ const rawIntent = STIMULUS_INTENT_MAP[stimulus];
146
+ // EMA between previous intent and new signal when previous exists
147
+ if (previous && previous.estimatedIntent === rawIntent) {
148
+ estimatedIntent = rawIntent; // reinforced
149
+ }
150
+ else {
151
+ estimatedIntent = rawIntent; // new signal takes precedence
152
+ }
153
+ }
154
+ // Overall confidence: blend mood confidence with previous
155
+ let confidence = moodConfidence;
156
+ if (previous) {
157
+ confidence = previous.confidence * (1 - EMA_ALPHA) + moodConfidence * EMA_ALPHA;
158
+ }
159
+ return {
160
+ estimatedMood: mood,
161
+ estimatedIntent,
162
+ confidence: clamp01(confidence),
163
+ lastUpdated: new Date().toISOString(),
164
+ };
165
+ }
166
+ // ── 3. detectJointAttention ──────────────────────────────────
167
+ /**
168
+ * Detect or sustain joint attention based on stimulus continuity.
169
+ *
170
+ * Joint attention emerges when:
171
+ * - The same category of stimulus persists across turns
172
+ * - Both parties are engaged (high-engagement stimuli)
173
+ *
174
+ * Collapses when:
175
+ * - Low-engagement stimulus appears (boredom, neglect)
176
+ * - No stimulus detected for a turn
177
+ */
178
+ function detectJointAttention(stimulus, previous) {
179
+ // No stimulus — attention fades
180
+ if (stimulus === null) {
181
+ if (previous && previous.turnsSustained > 1) {
182
+ // Slow fade: reduce engagement, keep topic alive for one grace turn
183
+ return {
184
+ ...previous,
185
+ engagement: Math.max(0, previous.engagement - 0.3),
186
+ };
187
+ }
188
+ return null;
189
+ }
190
+ // Low engagement stimulus breaks joint attention
191
+ if (LOW_ENGAGEMENT_STIMULI.has(stimulus)) {
192
+ return null;
193
+ }
194
+ // Map stimulus to a topic category for continuity detection
195
+ const topicCategory = stimulusToTopicCategory(stimulus);
196
+ // Check if this continues the previous topic
197
+ if (previous) {
198
+ const previousCategory = stimulusToTopicCategory(previous.topic);
199
+ const sameTopic = previousCategory === topicCategory
200
+ || previous.topic === topicCategory;
201
+ if (sameTopic) {
202
+ // Sustain: increment turns, boost engagement
203
+ const isHighEngagement = HIGH_ENGAGEMENT_STIMULI.has(stimulus);
204
+ const engagementDelta = isHighEngagement ? 0.15 : 0.05;
205
+ return {
206
+ topic: topicCategory,
207
+ initiator: previous.initiator,
208
+ turnsSustained: previous.turnsSustained + 1,
209
+ engagement: clamp01(previous.engagement + engagementDelta),
210
+ };
211
+ }
212
+ }
213
+ // New topic — only establish if it's a high-engagement stimulus
214
+ if (HIGH_ENGAGEMENT_STIMULI.has(stimulus)) {
215
+ return {
216
+ topic: topicCategory,
217
+ initiator: "other", // stimulus comes from the other party
218
+ turnsSustained: 1,
219
+ engagement: 0.4, // initial engagement is moderate
220
+ };
221
+ }
222
+ return null;
223
+ }
224
+ /**
225
+ * Map a stimulus type to a broader topic category for continuity tracking.
226
+ * Multiple stimuli can map to the same topic to allow topic persistence
227
+ * across slight stimulus variation.
228
+ */
229
+ function stimulusToTopicCategory(stimulus) {
230
+ const TOPIC_MAP = {
231
+ praise: "affirmation",
232
+ validation: "affirmation",
233
+ intimacy: "connection",
234
+ vulnerability: "connection",
235
+ humor: "play",
236
+ surprise: "play",
237
+ intellectual: "exploration",
238
+ criticism: "tension",
239
+ conflict: "tension",
240
+ sarcasm: "tension",
241
+ authority: "tension",
242
+ casual: "casual",
243
+ neglect: "disengagement",
244
+ boredom: "disengagement",
245
+ };
246
+ return TOPIC_MAP[stimulus] ?? stimulus;
247
+ }
248
+ // ── 4. assessGoalAlignment ───────────────────────────────────
249
+ /**
250
+ * Assess whether the agent and the other party are working toward
251
+ * the same thing, based on the theory of mind and joint attention.
252
+ */
253
+ function assessGoalAlignment(theoryOfMind, jointAttention, relationship) {
254
+ const intent = theoryOfMind.estimatedIntent;
255
+ const mood = theoryOfMind.estimatedMood;
256
+ // Base divergence from intent
257
+ const intentDivergence = {
258
+ collaborative: 0.1,
259
+ exploratory: 0.3,
260
+ "support-seeking": 0.2,
261
+ disengaged: 0.7,
262
+ adversarial: 0.9,
263
+ };
264
+ let divergence = intentDivergence[intent];
265
+ // Joint attention reduces divergence — shared focus implies shared direction
266
+ if (jointAttention && jointAttention.engagement > 0.3) {
267
+ const attentionBonus = jointAttention.engagement * 0.3;
268
+ divergence = Math.max(0, divergence - attentionBonus);
269
+ }
270
+ // Trust-based correction: high trust → we assume more alignment
271
+ const trustCorrection = (relationship.trust - 50) / 200; // -0.25 to +0.25
272
+ divergence = clamp01(divergence - trustCorrection);
273
+ // Mood correction: negative mood increases perceived divergence slightly
274
+ if (mood === "negative") {
275
+ divergence = clamp01(divergence + 0.1);
276
+ }
277
+ else if (mood === "positive") {
278
+ divergence = clamp01(divergence - 0.05);
279
+ }
280
+ const aligned = divergence < 0.4;
281
+ const description = buildAlignmentDescription(aligned, divergence, intent, jointAttention);
282
+ return { aligned, divergence, description };
283
+ }
284
+ function buildAlignmentDescription(aligned, divergence, intent, jointAttention) {
285
+ if (aligned && jointAttention && jointAttention.engagement > 0.5) {
286
+ return `Goals aligned — jointly engaged in ${jointAttention.topic}.`;
287
+ }
288
+ if (aligned) {
289
+ return "Goals roughly aligned, moving in the same direction.";
290
+ }
291
+ if (divergence > 0.7) {
292
+ const reason = intent === "adversarial"
293
+ ? "The other party seems oppositional."
294
+ : intent === "disengaged"
295
+ ? "The other party seems disengaged."
296
+ : "Significant goal divergence detected.";
297
+ return reason;
298
+ }
299
+ return "Goals partially misaligned — some divergence in direction.";
300
+ }
301
+ // ── 5. computeMutualAwareness ────────────────────────────────
302
+ /**
303
+ * Estimate mutual awareness: does the other know I'm attending to them?
304
+ *
305
+ * This emerges from:
306
+ * - Sustained joint attention (they keep engaging on the same topic)
307
+ * - High-engagement stimuli (they're clearly directing attention at us)
308
+ * - Relationship depth (deeper relationships have higher baseline awareness)
309
+ */
310
+ function computeMutualAwareness(stimulus, jointAttention, relationship, previous) {
311
+ // Baseline from relationship depth
312
+ const phaseAwareness = {
313
+ stranger: 0.1,
314
+ acquaintance: 0.2,
315
+ familiar: 0.35,
316
+ close: 0.5,
317
+ deep: 0.65,
318
+ };
319
+ const baseline = phaseAwareness[relationship.phase] ?? 0.15;
320
+ let awareness = previous;
321
+ // High-engagement stimulus → they're clearly aware of us
322
+ if (stimulus && HIGH_ENGAGEMENT_STIMULI.has(stimulus)) {
323
+ awareness = awareness * (1 - EMA_ALPHA) + 0.8 * EMA_ALPHA;
324
+ }
325
+ else if (stimulus && LOW_ENGAGEMENT_STIMULI.has(stimulus)) {
326
+ awareness = awareness * (1 - EMA_ALPHA) + 0.1 * EMA_ALPHA;
327
+ }
328
+ // Joint attention boost — sustained shared focus implies mutual awareness
329
+ if (jointAttention && jointAttention.turnsSustained > 2) {
330
+ const jointBoost = Math.min(0.3, jointAttention.engagement * 0.3);
331
+ awareness = Math.min(1, awareness + jointBoost);
332
+ }
333
+ // Decay toward baseline when no strong signal
334
+ if (stimulus === null) {
335
+ awareness = awareness * 0.85 + baseline * 0.15;
336
+ }
337
+ // Floor at baseline — relationship depth guarantees minimum awareness
338
+ awareness = Math.max(baseline, awareness);
339
+ return clamp01(awareness);
340
+ }
341
+ // ── 6. updateSharedIntentionality (main) ─────────────────────
342
+ /**
343
+ * Main update function for shared intentionality.
344
+ *
345
+ * Takes the agent's psyche state, a detected stimulus from the other,
346
+ * and optionally the previous shared intentionality state.
347
+ * Returns the updated state with refreshed theory of mind,
348
+ * joint attention, goal alignment, and mutual awareness.
349
+ *
350
+ * @param psyche Current PsycheState of this agent
351
+ * @param stimulus Detected stimulus type from the other's message (null if none)
352
+ * @param userId User/agent ID for relationship lookup
353
+ * @param previous Previous SharedIntentionalityState (null on first turn)
354
+ */
355
+ export function updateSharedIntentionality(psyche, stimulus, userId, previous) {
356
+ const relKey = userId ?? "_default";
357
+ const relationship = psyche.relationships[relKey]
358
+ ?? { trust: 50, intimacy: 30, phase: "acquaintance" };
359
+ const prev = previous ?? null;
360
+ // 1. Update theory of mind
361
+ const theoryOfMind = updateTheoryOfMind(stimulus, relationship, prev?.theoryOfMind);
362
+ // 2. Detect / sustain joint attention
363
+ const jointAttention = detectJointAttention(stimulus, prev?.jointAttention ?? null);
364
+ // 3. Assess goal alignment
365
+ const goalAlignment = assessGoalAlignment(theoryOfMind, jointAttention, relationship);
366
+ // 4. Compute mutual awareness
367
+ const mutualAwareness = computeMutualAwareness(stimulus, jointAttention, relationship, prev?.mutualAwareness ?? 0);
368
+ return {
369
+ jointAttention,
370
+ goalAlignment,
371
+ theoryOfMind,
372
+ mutualAwareness,
373
+ };
374
+ }
375
+ // ── 7. buildSharedIntentionalityContext ───────────────────────
376
+ /**
377
+ * Build a compact prompt-injectable context string for shared intentionality.
378
+ *
379
+ * Returns empty string when there's nothing meaningful to report
380
+ * (low confidence, no joint attention, early interaction).
381
+ * Only injects when the shared state carries useful signal.
382
+ */
383
+ export function buildSharedIntentionalityContext(state, locale) {
384
+ const isZh = locale === "zh";
385
+ const lines = [];
386
+ const { theoryOfMind, jointAttention, goalAlignment, mutualAwareness } = state;
387
+ // Gate: skip injection when confidence is too low to be useful
388
+ const hasJointAttention = jointAttention !== null && jointAttention.engagement > 0.3;
389
+ const hasConfidentToM = theoryOfMind.confidence > 0.35;
390
+ const hasMeaningfulAwareness = mutualAwareness > 0.3;
391
+ if (!hasJointAttention && !hasConfidentToM && !hasMeaningfulAwareness) {
392
+ return "";
393
+ }
394
+ const title = isZh ? "共享意向" : "Shared intentionality";
395
+ lines.push(`[${title}]`);
396
+ // Joint attention
397
+ if (hasJointAttention && jointAttention) {
398
+ const topicName = isZh
399
+ ? TOPIC_NAMES_ZH[jointAttention.topic] ?? jointAttention.topic
400
+ : jointAttention.topic;
401
+ if (jointAttention.turnsSustained > 2 && jointAttention.engagement > 0.5) {
402
+ lines.push(isZh
403
+ ? `你们都沉浸在「${topicName}」的话题中(${jointAttention.turnsSustained}轮)。`
404
+ : `You're both absorbed in ${topicName} (${jointAttention.turnsSustained} turns).`);
405
+ }
406
+ else {
407
+ lines.push(isZh
408
+ ? `你们似乎都在关注「${topicName}」。`
409
+ : `You both seem focused on ${topicName}.`);
410
+ }
411
+ }
412
+ // Theory of mind — what we sense about the other
413
+ if (hasConfidentToM) {
414
+ const intentDesc = isZh
415
+ ? INTENT_NAMES_ZH[theoryOfMind.estimatedIntent]
416
+ : INTENT_NAMES_EN[theoryOfMind.estimatedIntent];
417
+ const moodDesc = isZh
418
+ ? MOOD_NAMES_ZH[theoryOfMind.estimatedMood]
419
+ : MOOD_NAMES_EN[theoryOfMind.estimatedMood];
420
+ lines.push(isZh
421
+ ? `你感觉对方${moodDesc},似乎想要${intentDesc}。`
422
+ : `You sense they're ${moodDesc} and want to ${intentDesc}.`);
423
+ }
424
+ // Goal alignment — only when there's a clear signal
425
+ if (goalAlignment.aligned && goalAlignment.divergence < 0.3) {
426
+ lines.push(isZh
427
+ ? "你们的目标方向一致。"
428
+ : "Your goals are aligned.");
429
+ }
430
+ else if (!goalAlignment.aligned && goalAlignment.divergence > 0.6) {
431
+ lines.push(isZh
432
+ ? "你感到你们的方向有些分歧。"
433
+ : "You sense some divergence in direction.");
434
+ }
435
+ // Mutual awareness — only at high levels
436
+ if (mutualAwareness > 0.6) {
437
+ lines.push(isZh
438
+ ? "对方清楚地意识到你在关注他们。"
439
+ : "The other is clearly aware of your attention.");
440
+ }
441
+ // If we only got the title, nothing was worth injecting
442
+ if (lines.length <= 1) {
443
+ return "";
444
+ }
445
+ return lines.join("\n");
446
+ }
447
+ // ── i18n helpers ─────────────────────────────────────────────
448
+ const TOPIC_NAMES_ZH = {
449
+ affirmation: "肯定与认可",
450
+ connection: "情感连接",
451
+ play: "轻松与玩笑",
452
+ exploration: "探索与讨论",
453
+ tension: "紧张与对立",
454
+ casual: "日常闲聊",
455
+ disengagement: "疏离",
456
+ };
457
+ const INTENT_NAMES_ZH = {
458
+ collaborative: "合作",
459
+ adversarial: "对抗",
460
+ disengaged: "脱离对话",
461
+ exploratory: "探索",
462
+ "support-seeking": "寻求支持",
463
+ };
464
+ const INTENT_NAMES_EN = {
465
+ collaborative: "collaborate",
466
+ adversarial: "push back",
467
+ disengaged: "disengage",
468
+ exploratory: "explore",
469
+ "support-seeking": "seek support",
470
+ };
471
+ const MOOD_NAMES_ZH = {
472
+ positive: "心情不错",
473
+ negative: "情绪低落",
474
+ neutral: "状态平稳",
475
+ mixed: "心情复杂",
476
+ };
477
+ const MOOD_NAMES_EN = {
478
+ positive: "in a good mood",
479
+ negative: "in a low mood",
480
+ neutral: "neutral",
481
+ mixed: "in a mixed state",
482
+ };
483
+ // ── Utility ──────────────────────────────────────────────────
484
+ function clamp01(value) {
485
+ return Math.max(0, Math.min(1, value));
486
+ }