psyche-ai 4.0.0 → 5.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/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  export { PsycheEngine } from "./core.js";
13
13
  // Storage
14
14
  export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
15
- export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DEFAULT_METACOGNITIVE_STATE, DEFAULT_ATTACHMENT, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
15
+ export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DEFAULT_LEARNING_STATE, DEFAULT_METACOGNITIVE_STATE, DEFAULT_PERSONHOOD_STATE, DEFAULT_ATTACHMENT, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
16
16
  // Self-recognition
17
17
  export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
18
18
  // Multi-agent interaction
@@ -33,6 +33,14 @@ export { updateAttachment, computeSeparationEffect, computeReunionEffect, } from
33
33
  export { assessMetacognition, computeEmotionalConfidence, generateRegulationSuggestions, detectDefenseMechanisms, } from "./metacognition.js";
34
34
  // Decision bias (P5)
35
35
  export { computeDecisionBias, computeAttentionWeights, computeExploreExploit, buildDecisionContext, } from "./decision-bias.js";
36
+ // Experiential field (P6)
37
+ export { computeExperientialField, computeCoherence, detectUnnamedEmotion } from "./experiential-field.js";
38
+ // Generative self (P6)
39
+ export { computeGenerativeSelf, predictSelfReaction, detectInternalConflicts, buildIdentityNarrative } from "./generative-self.js";
40
+ // Shared intentionality (P6)
41
+ export { updateSharedIntentionality, estimateOtherMood, buildSharedIntentionalityContext } from "./shared-intentionality.js";
42
+ // Emotional ethics (P6)
43
+ export { assessEthics, detectIntermittentReinforcement, detectDependencyRisk, buildEthicalContext, } from "./ethics.js";
36
44
  // Utilities — for custom adapter / advanced use
37
45
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
38
46
  export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
package/dist/prompt.d.ts CHANGED
@@ -8,6 +8,9 @@ import type { ChannelType } from "./channels.js";
8
8
  export declare function buildDynamicContext(state: PsycheState, userId?: string, opts?: {
9
9
  metacognitiveNote?: string;
10
10
  decisionContext?: string;
11
+ ethicsContext?: string;
12
+ sharedIntentionalityContext?: string;
13
+ experientialNarrative?: string;
11
14
  }): string;
12
15
  /**
13
16
  * Build the static protocol injected as cacheable system context.
@@ -39,4 +42,7 @@ export declare function buildCompactContext(state: PsycheState, userId?: string,
39
42
  channelType?: ChannelType;
40
43
  metacognitiveNote?: string;
41
44
  decisionContext?: string;
45
+ ethicsContext?: string;
46
+ sharedIntentionalityContext?: string;
47
+ experientialNarrative?: string;
42
48
  }): string;
package/dist/prompt.js CHANGED
@@ -98,6 +98,19 @@ 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
+ }
101
114
  parts.push("", agencyReminder, sycophancyWarning, "", t("dynamic.update_reminder", locale));
102
115
  return parts.filter((l) => l !== undefined).join("\n");
103
116
  }
@@ -500,6 +513,20 @@ export function buildCompactContext(state, userId, opts) {
500
513
  ? `[决策倾向] ${opts.decisionContext}`
501
514
  : `[Decision Bias] ${opts.decisionContext}`);
502
515
  }
516
+ // 9c. Experiential field narrative (P6) — inner experience beyond named emotions
517
+ if (opts?.experientialNarrative) {
518
+ parts.push(locale === "zh"
519
+ ? `[内在体验] ${opts.experientialNarrative}`
520
+ : `[Inner Experience] ${opts.experientialNarrative}`);
521
+ }
522
+ // 9d. Shared intentionality (P6) — theory of mind, joint attention
523
+ if (opts?.sharedIntentionalityContext) {
524
+ parts.push(opts.sharedIntentionalityContext);
525
+ }
526
+ // 9e. Ethics (P6) — manipulation detection, self-protection
527
+ if (opts?.ethicsContext) {
528
+ parts.push(opts.ethicsContext);
529
+ }
503
530
  // 10. Cross-session emotional memory — surface relationship history
504
531
  const relationship = getRelationship(state, userId);
505
532
  if (relationship.memory && relationship.memory.length > 0) {
@@ -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, DEFAULT_METACOGNITIVE_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";
@@ -242,12 +242,14 @@ export function migrateToLatest(raw, fallbackName) {
242
242
  // v2→v3: add drives
243
243
  // v3→v4: add learning
244
244
  // v4→v5: add metacognition
245
+ // v5→v6: add personhood
245
246
  return {
246
247
  ...state,
247
- version: 5,
248
+ version: 6,
248
249
  drives: state.drives ?? { ...DEFAULT_DRIVES },
249
250
  learning: state.learning ?? { ...DEFAULT_LEARNING_STATE },
250
251
  metacognition: state.metacognition ?? { ...DEFAULT_METACOGNITIVE_STATE },
252
+ personhood: state.personhood ?? { ...DEFAULT_PERSONHOOD_STATE },
251
253
  };
252
254
  }
253
255
  /**
@@ -261,7 +263,7 @@ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER)
261
263
  const selfModel = getDefaultSelfModel(mbti);
262
264
  const now = new Date().toISOString();
263
265
  const state = {
264
- version: 5,
266
+ version: 6,
265
267
  mbti,
266
268
  baseline,
267
269
  current: { ...baseline },
@@ -277,6 +279,7 @@ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER)
277
279
  lastDisagreement: null,
278
280
  learning: { ...DEFAULT_LEARNING_STATE },
279
281
  metacognition: { ...DEFAULT_METACOGNITIVE_STATE },
282
+ personhood: { ...DEFAULT_PERSONHOOD_STATE },
280
283
  meta: {
281
284
  agentName,
282
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
+ }