psyche-ai 2.3.0 → 3.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/README.md CHANGED
@@ -161,6 +161,10 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
161
161
  - **流式支持** — Vercel AI SDK `streamText` 中间件,自动缓冲和剥离标签
162
162
  - **渠道修饰** — Discord/Slack/飞书/终端等不同渠道自动调整表达风格
163
163
  - **自定义人格** — 超越 MBTI 预设,完全自定义 baseline/敏感度/气质
164
+ - **情绪学习** — 从交互结果中学习,调整情绪反应参数(躯体标记假说)
165
+ - **上下文分类** — 关系/驱力/历史感知的刺激分类,超越简单正则
166
+ - **时间意识** — 预期、惊喜/失望、遗憾(马尔可夫预测+反事实分析)
167
+ - **依恋动力学** — 4种依恋风格(安全/焦虑/回避/混乱),分离焦虑,重逢效应
164
168
  - **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
165
169
 
166
170
  架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
@@ -170,7 +174,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
170
174
  ```bash
171
175
  npm install
172
176
  npm run build
173
- npm test # 469 tests
177
+ npm test # 568 tests
174
178
  npm run typecheck # strict mode
175
179
  ```
176
180
 
@@ -0,0 +1,30 @@
1
+ import type { ChemicalState, StimulusType } from "./types.js";
2
+ export type AttachmentStyle = "secure" | "anxious" | "avoidant" | "disorganized";
3
+ export interface AttachmentState {
4
+ style: AttachmentStyle;
5
+ strength: number;
6
+ securityScore: number;
7
+ anxietyScore: number;
8
+ avoidanceScore: number;
9
+ lastInteractionAt: string;
10
+ interactionCount: number;
11
+ }
12
+ export interface SeparationEffect {
13
+ chemistryDelta: Partial<ChemicalState>;
14
+ description: string;
15
+ intensity: number;
16
+ }
17
+ export declare const DEFAULT_ATTACHMENT: AttachmentState;
18
+ /**
19
+ * Update attachment based on interaction outcome.
20
+ */
21
+ export declare function updateAttachment(attachment: AttachmentState, stimulus: StimulusType | null, outcomeScore: number): AttachmentState;
22
+ /**
23
+ * Compute chemistry effects of absence based on attachment.
24
+ * Called when time since last interaction is significant.
25
+ */
26
+ export declare function computeSeparationEffect(attachment: AttachmentState, minutesSinceLastInteraction: number): SeparationEffect | null;
27
+ /**
28
+ * Compute chemistry effects when reuniting after absence.
29
+ */
30
+ export declare function computeReunionEffect(attachment: AttachmentState, minutesSinceLastInteraction: number): Partial<ChemicalState> | null;
@@ -0,0 +1,241 @@
1
+ // ============================================================
2
+ // Attachment Dynamics — Bowlby-inspired attachment formation
3
+ //
4
+ // Models relationship attachment through interaction patterns:
5
+ // 1. AttachmentModel — style classification + strength tracking
6
+ // 2. SeparationAnxiety — absence effects on chemistry
7
+ // 3. ReunionEffect — return effects on chemistry
8
+ //
9
+ // Attachment style emerges from interaction history, not from
10
+ // configuration. Consistent positive interaction → secure.
11
+ // Inconsistency → anxious. Rejection/neglect → avoidant.
12
+ // ============================================================
13
+ // ── Defaults ─────────────────────────────────────────────────
14
+ export const DEFAULT_ATTACHMENT = {
15
+ style: "secure",
16
+ strength: 0,
17
+ securityScore: 50,
18
+ anxietyScore: 50,
19
+ avoidanceScore: 50,
20
+ lastInteractionAt: new Date().toISOString(),
21
+ interactionCount: 0,
22
+ };
23
+ // ── Stimulus Classification ──────────────────────────────────
24
+ const POSITIVE_STIMULI = new Set([
25
+ "praise", "validation", "intimacy", "humor",
26
+ ]);
27
+ const NEGATIVE_STIMULI = new Set([
28
+ "criticism", "conflict", "neglect", "sarcasm", "authority",
29
+ ]);
30
+ const REJECTION_STIMULI = new Set([
31
+ "neglect", "authority", "boredom",
32
+ ]);
33
+ // EMA smoothing factor: weight given to new observation
34
+ const EMA_ALPHA = 0.15;
35
+ // ── 1. AttachmentModel ──────────────────────────────────────
36
+ /**
37
+ * Determine attachment style from scores.
38
+ */
39
+ function determineStyle(securityScore, anxietyScore, avoidanceScore) {
40
+ // Disorganized: both anxiety and avoidance elevated
41
+ if (anxietyScore > 50 && avoidanceScore > 50) {
42
+ return "disorganized";
43
+ }
44
+ // Anxious: high anxiety
45
+ if (anxietyScore > 60) {
46
+ return "anxious";
47
+ }
48
+ // Avoidant: high avoidance
49
+ if (avoidanceScore > 60) {
50
+ return "avoidant";
51
+ }
52
+ // Secure: high security, low anxiety, low avoidance
53
+ if (securityScore > 60 && anxietyScore < 40 && avoidanceScore < 40) {
54
+ return "secure";
55
+ }
56
+ // Default to current trajectory — mild insecurity stays secure
57
+ if (securityScore >= 50)
58
+ return "secure";
59
+ if (anxietyScore > avoidanceScore)
60
+ return "anxious";
61
+ return "avoidant";
62
+ }
63
+ /**
64
+ * Update attachment based on interaction outcome.
65
+ */
66
+ export function updateAttachment(attachment, stimulus, outcomeScore) {
67
+ const result = { ...attachment };
68
+ // Strength increases slowly with interaction count
69
+ result.interactionCount = attachment.interactionCount + 1;
70
+ result.strength = Math.min(100, attachment.strength + 1);
71
+ result.lastInteractionAt = new Date().toISOString();
72
+ if (stimulus === null) {
73
+ // No stimulus — just update count/strength, reclassify
74
+ result.style = determineStyle(result.securityScore, result.anxietyScore, result.avoidanceScore);
75
+ return result;
76
+ }
77
+ // SecurityScore: EMA with positive/negative stimuli
78
+ const isPositive = POSITIVE_STIMULI.has(stimulus);
79
+ const isNegative = NEGATIVE_STIMULI.has(stimulus);
80
+ if (isPositive) {
81
+ const target = Math.min(100, result.securityScore + 5);
82
+ result.securityScore = result.securityScore * (1 - EMA_ALPHA) + target * EMA_ALPHA;
83
+ }
84
+ else if (isNegative) {
85
+ const target = Math.max(0, result.securityScore - 5);
86
+ result.securityScore = result.securityScore * (1 - EMA_ALPHA) + target * EMA_ALPHA;
87
+ }
88
+ // AnxietyScore: increases with inconsistency (rapid alternation between positive and negative)
89
+ // We detect inconsistency by checking if outcomeScore diverges from the security trend
90
+ const expectedDirection = result.securityScore > 50 ? 1 : -1;
91
+ const actualDirection = outcomeScore >= 0 ? 1 : -1;
92
+ const isInconsistent = expectedDirection !== actualDirection;
93
+ if (isInconsistent) {
94
+ // Inconsistency → anxiety rises
95
+ const anxietyTarget = Math.min(100, result.anxietyScore + 8);
96
+ result.anxietyScore = result.anxietyScore * (1 - EMA_ALPHA) + anxietyTarget * EMA_ALPHA;
97
+ }
98
+ else {
99
+ // Consistency → anxiety decreases
100
+ const anxietyTarget = Math.max(0, result.anxietyScore - 3);
101
+ result.anxietyScore = result.anxietyScore * (1 - EMA_ALPHA) + anxietyTarget * EMA_ALPHA;
102
+ }
103
+ // AvoidanceScore: increases with rejection/neglect stimuli
104
+ if (REJECTION_STIMULI.has(stimulus)) {
105
+ const avoidTarget = Math.min(100, result.avoidanceScore + 6);
106
+ result.avoidanceScore = result.avoidanceScore * (1 - EMA_ALPHA) + avoidTarget * EMA_ALPHA;
107
+ }
108
+ else if (isPositive) {
109
+ // Positive interactions reduce avoidance
110
+ const avoidTarget = Math.max(0, result.avoidanceScore - 3);
111
+ result.avoidanceScore = result.avoidanceScore * (1 - EMA_ALPHA) + avoidTarget * EMA_ALPHA;
112
+ }
113
+ // Clamp all scores
114
+ result.securityScore = Math.max(0, Math.min(100, result.securityScore));
115
+ result.anxietyScore = Math.max(0, Math.min(100, result.anxietyScore));
116
+ result.avoidanceScore = Math.max(0, Math.min(100, result.avoidanceScore));
117
+ // Determine style
118
+ result.style = determineStyle(result.securityScore, result.anxietyScore, result.avoidanceScore);
119
+ return result;
120
+ }
121
+ // ── 2. SeparationAnxiety ────────────────────────────────────
122
+ /**
123
+ * Compute chemistry effects of absence based on attachment.
124
+ * Called when time since last interaction is significant.
125
+ */
126
+ export function computeSeparationEffect(attachment, minutesSinceLastInteraction) {
127
+ // No effect for short absence or weak attachment
128
+ if (minutesSinceLastInteraction < 60 || attachment.strength < 20) {
129
+ return null;
130
+ }
131
+ const hours = minutesSinceLastInteraction / 60;
132
+ // Intensity scales with attachment strength and time (logarithmic growth, capped at 1)
133
+ const baseIntensity = (attachment.strength / 100) * Math.min(1, Math.log2(hours + 1) / 5);
134
+ switch (attachment.style) {
135
+ case "secure": {
136
+ // Mild longing after 24h
137
+ if (hours < 24)
138
+ return null;
139
+ const intensity = baseIntensity * 0.5;
140
+ return {
141
+ chemistryDelta: {
142
+ OT: -5 * intensity,
143
+ DA: -3 * intensity,
144
+ },
145
+ description: "gentle longing from sustained absence",
146
+ intensity: Math.min(1, intensity),
147
+ };
148
+ }
149
+ case "anxious": {
150
+ // Distress after 4h, grows with time
151
+ if (hours < 4)
152
+ return null;
153
+ const intensity = baseIntensity * 1.5;
154
+ // OT oscillation: represented as net negative with anxiety
155
+ return {
156
+ chemistryDelta: {
157
+ CORT: 10 * intensity,
158
+ OT: -5 * intensity,
159
+ NE: 8 * intensity,
160
+ DA: -3 * intensity,
161
+ },
162
+ description: "anxious distress from absence — fear of abandonment",
163
+ intensity: Math.min(1, intensity),
164
+ };
165
+ }
166
+ case "avoidant": {
167
+ // Relief initially, discomfort after 48h
168
+ if (hours < 48)
169
+ return null;
170
+ const intensity = baseIntensity * 0.4;
171
+ return {
172
+ chemistryDelta: {
173
+ OT: -3 * intensity,
174
+ },
175
+ description: "subtle discomfort surfacing through avoidant defense",
176
+ intensity: Math.min(1, intensity),
177
+ };
178
+ }
179
+ case "disorganized": {
180
+ // Conflicting signals
181
+ if (hours < 4)
182
+ return null;
183
+ const intensity = baseIntensity * 1.0;
184
+ return {
185
+ chemistryDelta: {
186
+ CORT: 5 * intensity,
187
+ OT: 5 * intensity,
188
+ NE: 3 * intensity,
189
+ },
190
+ description: "conflicting signals — wanting closeness and fearing it",
191
+ intensity: Math.min(1, intensity),
192
+ };
193
+ }
194
+ }
195
+ }
196
+ // ── 3. ReunionEffect ────────────────────────────────────────
197
+ /**
198
+ * Compute chemistry effects when reuniting after absence.
199
+ */
200
+ export function computeReunionEffect(attachment, minutesSinceLastInteraction) {
201
+ // No effect for short absence or weak attachment
202
+ if (minutesSinceLastInteraction < 60 || attachment.strength < 20) {
203
+ return null;
204
+ }
205
+ const hours = minutesSinceLastInteraction / 60;
206
+ // Scale with time (logarithmic) and attachment strength
207
+ const scale = (attachment.strength / 100) * Math.min(1, Math.log2(hours + 1) / 5);
208
+ switch (attachment.style) {
209
+ case "secure": {
210
+ // Warm reunion
211
+ return {
212
+ OT: 8 * scale,
213
+ DA: 5 * scale,
214
+ END: 3 * scale,
215
+ };
216
+ }
217
+ case "anxious": {
218
+ // Intense but short-lived relief (CORT still elevated)
219
+ return {
220
+ OT: 15 * scale,
221
+ DA: 10 * scale,
222
+ CORT: 5 * scale,
223
+ };
224
+ }
225
+ case "avoidant": {
226
+ // Cautious re-engagement
227
+ return {
228
+ OT: 3 * scale,
229
+ NE: 5 * scale,
230
+ };
231
+ }
232
+ case "disorganized": {
233
+ // Mixed signals
234
+ return {
235
+ OT: 5 * scale,
236
+ CORT: 5 * scale,
237
+ NE: 5 * scale,
238
+ };
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,39 @@
1
+ import type { StimulusType, DriveType, RelationshipState, PsycheState } from "./types.js";
2
+ export interface ContextFeatures {
3
+ relationshipPhase: RelationshipState["phase"];
4
+ recentStimuli: StimulusType[];
5
+ driveSatisfaction: Record<DriveType, "high" | "mid" | "low">;
6
+ timeSinceLastMessage: number;
7
+ totalInteractions: number;
8
+ agreementStreak: number;
9
+ }
10
+ export interface ContextualClassification {
11
+ type: StimulusType;
12
+ baseConfidence: number;
13
+ contextConfidence: number;
14
+ contextModifiers: string[];
15
+ }
16
+ /**
17
+ * Extract contextual features from the current psyche state.
18
+ * Used to feed into classifyStimulusWithContext.
19
+ */
20
+ export declare function extractContextFeatures(state: PsycheState, userId?: string): ContextFeatures;
21
+ /**
22
+ * Classify a stimulus with context modifiers applied.
23
+ *
24
+ * Wraps classifyStimulus(text) and adjusts confidence based on:
25
+ * - Relationship depth
26
+ * - Recent stimulus patterns
27
+ * - Drive hunger
28
+ * - Agreement streak
29
+ * - Time gap
30
+ *
31
+ * Returns results sorted by contextConfidence descending.
32
+ */
33
+ export declare function classifyStimulusWithContext(text: string, context: ContextFeatures): ContextualClassification[];
34
+ /**
35
+ * Map a stimulus type to a warmth score for outcome evaluation.
36
+ * Positive stimuli return positive values; negative stimuli return negative.
37
+ * null returns 0.
38
+ */
39
+ export declare function stimulusWarmth(stimulus: StimulusType | null): number;
@@ -0,0 +1,204 @@
1
+ // ============================================================
2
+ // Context-Aware Stimulus Classification
3
+ //
4
+ // Wraps classify.ts with contextual signals (relationship depth,
5
+ // recent stimulus patterns, drive hunger, agreement streaks,
6
+ // time gaps) to improve classification accuracy.
7
+ // ============================================================
8
+ import { DRIVE_KEYS } from "./types.js";
9
+ import { classifyStimulus } from "./classify.js";
10
+ // ── Drive Satisfaction Thresholds ────────────────────────────
11
+ function driveSatisfactionLevel(value) {
12
+ if (value >= 70)
13
+ return "high";
14
+ if (value >= 40)
15
+ return "mid";
16
+ return "low";
17
+ }
18
+ // ── Extract Context Features ─────────────────────────────────
19
+ /**
20
+ * Extract contextual features from the current psyche state.
21
+ * Used to feed into classifyStimulusWithContext.
22
+ */
23
+ export function extractContextFeatures(state, userId) {
24
+ // Relationship phase
25
+ const relKey = userId ?? "_default";
26
+ const relationship = state.relationships[relKey] ?? state.relationships["_default"];
27
+ const relationshipPhase = relationship?.phase ?? "stranger";
28
+ // Recent stimuli from emotional history (last 3)
29
+ const recentStimuli = state.emotionalHistory
30
+ .slice(-3)
31
+ .map((snap) => snap.stimulus)
32
+ .filter((s) => s !== null);
33
+ // Drive satisfaction levels
34
+ const driveSatisfaction = {};
35
+ for (const key of DRIVE_KEYS) {
36
+ driveSatisfaction[key] = driveSatisfactionLevel(state.drives[key]);
37
+ }
38
+ // Time since last message (minutes)
39
+ const now = Date.now();
40
+ const updatedAt = new Date(state.updatedAt).getTime();
41
+ const timeSinceLastMessage = Math.max(0, (now - updatedAt) / 60_000);
42
+ // Total interactions
43
+ const totalInteractions = state.meta.totalInteractions;
44
+ // Agreement streak
45
+ const agreementStreak = state.agreementStreak;
46
+ return {
47
+ relationshipPhase,
48
+ recentStimuli,
49
+ driveSatisfaction,
50
+ timeSinceLastMessage,
51
+ totalInteractions,
52
+ agreementStreak,
53
+ };
54
+ }
55
+ // ── Context-Adjusted Classification ──────────────────────────
56
+ /**
57
+ * Classify a stimulus with context modifiers applied.
58
+ *
59
+ * Wraps classifyStimulus(text) and adjusts confidence based on:
60
+ * - Relationship depth
61
+ * - Recent stimulus patterns
62
+ * - Drive hunger
63
+ * - Agreement streak
64
+ * - Time gap
65
+ *
66
+ * Returns results sorted by contextConfidence descending.
67
+ */
68
+ export function classifyStimulusWithContext(text, context) {
69
+ const baseResults = classifyStimulus(text);
70
+ const results = baseResults.map((r) => ({
71
+ type: r.type,
72
+ baseConfidence: r.confidence,
73
+ contextConfidence: r.confidence,
74
+ contextModifiers: [],
75
+ }));
76
+ // Ensure all stimulus types that need boosting have an entry
77
+ const ensureType = (type) => {
78
+ let entry = results.find((r) => r.type === type);
79
+ if (!entry) {
80
+ entry = {
81
+ type,
82
+ baseConfidence: 0,
83
+ contextConfidence: 0,
84
+ contextModifiers: [],
85
+ };
86
+ results.push(entry);
87
+ }
88
+ return entry;
89
+ };
90
+ for (const r of results) {
91
+ // ── Relationship depth modifiers ──
92
+ if (context.relationshipPhase === "stranger" && r.type === "intimacy") {
93
+ r.contextConfidence *= 0.7;
94
+ r.contextModifiers.push("stranger penalty on intimacy");
95
+ }
96
+ if ((context.relationshipPhase === "close" || context.relationshipPhase === "deep") &&
97
+ r.type === "casual") {
98
+ r.contextConfidence += 0.1;
99
+ r.contextModifiers.push("close relationship boost on casual");
100
+ }
101
+ if (context.relationshipPhase === "stranger" && r.type === "vulnerability") {
102
+ r.contextConfidence *= 0.6;
103
+ r.contextModifiers.push("stranger penalty on vulnerability");
104
+ }
105
+ // ── Recent stimulus pattern modifiers ──
106
+ // Same stimulus 3x in a row → confidence * 0.8 (repetition fatigue)
107
+ if (context.recentStimuli.length >= 3 &&
108
+ context.recentStimuli.every((s) => s === r.type)) {
109
+ r.contextConfidence *= 0.8;
110
+ r.contextModifiers.push("repetition fatigue penalty");
111
+ }
112
+ // ── Agreement streak modifiers ──
113
+ if (context.agreementStreak >= 5 && r.type === "validation") {
114
+ r.contextConfidence *= 0.8;
115
+ r.contextModifiers.push("sycophantic loop dampening on validation");
116
+ }
117
+ // ── Time gap modifiers ──
118
+ if (context.timeSinceLastMessage > 1440) {
119
+ if (r.type === "casual") {
120
+ r.contextConfidence += 0.1;
121
+ r.contextModifiers.push("long absence boost on casual");
122
+ }
123
+ if (r.type === "intimacy") {
124
+ r.contextConfidence *= 0.9;
125
+ r.contextModifiers.push("long absence penalty on intimacy");
126
+ }
127
+ }
128
+ }
129
+ // ── De-escalation pattern (conflict → casual) ──
130
+ if (context.recentStimuli.length > 0 &&
131
+ context.recentStimuli[context.recentStimuli.length - 1] === "conflict") {
132
+ const casual = ensureType("casual");
133
+ casual.contextConfidence += 0.15;
134
+ casual.contextModifiers.push("de-escalation boost after conflict");
135
+ }
136
+ // ── Fake praise follow-up (praise → sarcasm) ──
137
+ if (context.recentStimuli.length > 0 &&
138
+ context.recentStimuli[context.recentStimuli.length - 1] === "praise") {
139
+ const sarcasm = results.find((r) => r.type === "sarcasm");
140
+ if (sarcasm) {
141
+ sarcasm.contextConfidence += 0.1;
142
+ sarcasm.contextModifiers.push("possible fake praise follow-up boost on sarcasm");
143
+ }
144
+ }
145
+ // ── Drive-hunger modifiers ──
146
+ if (context.driveSatisfaction.connection === "low") {
147
+ // Positive stimuli get a warmth boost
148
+ const positiveTypes = [
149
+ "praise", "validation", "intimacy", "humor", "casual", "vulnerability",
150
+ ];
151
+ for (const r of results) {
152
+ if (positiveTypes.includes(r.type)) {
153
+ r.contextConfidence += 0.05;
154
+ r.contextModifiers.push("connection hunger boost");
155
+ }
156
+ }
157
+ }
158
+ if (context.driveSatisfaction.esteem === "low") {
159
+ for (const r of results) {
160
+ if (r.type === "validation" || r.type === "praise") {
161
+ r.contextConfidence += 0.05;
162
+ r.contextModifiers.push("esteem hunger boost");
163
+ }
164
+ }
165
+ }
166
+ if (context.driveSatisfaction.survival === "low") {
167
+ for (const r of results) {
168
+ if (r.type === "authority" || r.type === "conflict") {
169
+ r.contextConfidence += 0.1;
170
+ r.contextModifiers.push("survival threat sensitivity boost");
171
+ }
172
+ }
173
+ }
174
+ // ── Sort by contextConfidence descending ──
175
+ results.sort((a, b) => b.contextConfidence - a.contextConfidence);
176
+ return results;
177
+ }
178
+ // ── Warmth Scoring ───────────────────────────────────────────
179
+ const WARMTH_MAP = {
180
+ praise: 0.8,
181
+ validation: 0.7,
182
+ intimacy: 0.9,
183
+ humor: 0.5,
184
+ surprise: 0.3,
185
+ casual: 0.1,
186
+ intellectual: 0.2,
187
+ vulnerability: 0.3,
188
+ sarcasm: -0.5,
189
+ criticism: -0.7,
190
+ conflict: -0.9,
191
+ authority: -0.4,
192
+ neglect: -0.8,
193
+ boredom: -0.3,
194
+ };
195
+ /**
196
+ * Map a stimulus type to a warmth score for outcome evaluation.
197
+ * Positive stimuli return positive values; negative stimuli return negative.
198
+ * null returns 0.
199
+ */
200
+ export function stimulusWarmth(stimulus) {
201
+ if (stimulus === null)
202
+ return 0;
203
+ return WARMTH_MAP[stimulus] ?? 0;
204
+ }
package/dist/core.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PsycheState, StimulusType, Locale, MBTIType } from "./types.js";
1
+ import type { PsycheState, StimulusType, Locale, MBTIType, OutcomeScore } from "./types.js";
2
2
  import type { StorageAdapter } from "./storage.js";
3
3
  export interface PsycheEngineConfig {
4
4
  mbti?: MBTIType;
@@ -24,11 +24,19 @@ export interface ProcessOutputResult {
24
24
  /** Whether chemistry was meaningfully updated (contagion or psyche_update) */
25
25
  stateChanged: boolean;
26
26
  }
27
+ export interface ProcessOutcomeResult {
28
+ /** Outcome evaluation score (-1 to 1) */
29
+ outcomeScore: OutcomeScore;
30
+ /** Whether learning state was updated */
31
+ learningUpdated: boolean;
32
+ }
27
33
  export declare class PsycheEngine {
28
34
  private state;
29
35
  private readonly storage;
30
36
  private readonly cfg;
31
37
  private readonly protocolCache;
38
+ /** Pending prediction from last processInput for auto-learning */
39
+ private pendingPrediction;
32
40
  constructor(config: PsycheEngineConfig | undefined, storage: StorageAdapter);
33
41
  /**
34
42
  * Load or create initial state. Must be called before processInput/processOutput.
@@ -48,6 +56,19 @@ export declare class PsycheEngine {
48
56
  processOutput(text: string, opts?: {
49
57
  userId?: string;
50
58
  }): Promise<ProcessOutputResult>;
59
+ /**
60
+ * Phase 3 (optional): Explicitly evaluate the outcome of the last interaction.
61
+ *
62
+ * This is automatically called at the start of processInput, so most users
63
+ * don't need to call it manually. Use this for explicit outcome evaluation
64
+ * (e.g., when a session ends without a follow-up message).
65
+ *
66
+ * @param nextUserStimulus - The stimulus detected in the user's next message,
67
+ * or null if the session ended.
68
+ */
69
+ processOutcome(nextUserStimulus: StimulusType | null, opts?: {
70
+ userId?: string;
71
+ }): Promise<ProcessOutcomeResult | null>;
51
72
  /**
52
73
  * Get the current psyche state (read-only snapshot).
53
74
  */