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/README.md +8 -1
- package/dist/cli.js +0 -0
- package/dist/core.js +45 -3
- package/dist/decision-bias.d.ts +58 -0
- package/dist/decision-bias.js +211 -0
- package/dist/ethics.d.ts +64 -0
- package/dist/ethics.js +577 -0
- package/dist/experiential-field.d.ts +46 -0
- package/dist/experiential-field.js +646 -0
- package/dist/generative-self.d.ts +88 -0
- package/dist/generative-self.js +647 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +13 -1
- package/dist/metacognition.d.ts +60 -0
- package/dist/metacognition.js +611 -0
- package/dist/prompt.d.ts +6 -1
- package/dist/prompt.js +26 -4
- package/dist/psyche-file.js +9 -3
- package/dist/shared-intentionality.d.ts +72 -0
- package/dist/shared-intentionality.js +486 -0
- package/dist/types.d.ts +68 -2
- package/dist/types.js +24 -0
- package/dist/update.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.`);
|
package/dist/psyche-file.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
+
}
|