psyche-ai 2.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.
@@ -0,0 +1,69 @@
1
+ import type { PsycheState, MBTIType, RelationshipState, Locale, StimulusType, ChemicalSnapshot } from "./types.js";
2
+ /** Minimal logger interface */
3
+ export interface Logger {
4
+ info: (msg: string) => void;
5
+ warn: (msg: string) => void;
6
+ debug: (msg: string) => void;
7
+ }
8
+ /**
9
+ * Compress a batch of snapshots into a concise session summary string.
10
+ * Format: "3月23日(5轮): 刺激[casual×3, praise×2] 趋势[DA↑OT↑] 情绪[自然→满足]"
11
+ */
12
+ export declare function compressSnapshots(snapshots: ChemicalSnapshot[]): string;
13
+ /**
14
+ * Push a chemical snapshot to emotional history, keeping max entries.
15
+ * When history overflows, compresses removed entries into relationship memory.
16
+ */
17
+ export declare function pushSnapshot(state: PsycheState, stimulus: StimulusType | null): PsycheState;
18
+ /**
19
+ * Get relationship for a specific user, or _default.
20
+ */
21
+ export declare function getRelationship(state: PsycheState, userId?: string): RelationshipState;
22
+ /**
23
+ * Load psyche state from workspace. Auto-initializes if missing.
24
+ * Handles v1→v2 migration transparently.
25
+ */
26
+ export declare function loadState(workspaceDir: string, logger?: Logger): Promise<PsycheState>;
27
+ /**
28
+ * Migrate any older state format directly to v3.
29
+ * Single source of truth for all migrations.
30
+ */
31
+ export declare function migrateToLatest(raw: Record<string, unknown>, fallbackName?: string): PsycheState;
32
+ /**
33
+ * Create initial psyche state.
34
+ */
35
+ export declare function initializeState(workspaceDir: string, opts?: {
36
+ mbti?: MBTIType;
37
+ name?: string;
38
+ locale?: Locale;
39
+ }, logger?: Logger): Promise<PsycheState>;
40
+ /**
41
+ * Save psyche state to workspace (atomic write).
42
+ */
43
+ export declare function saveState(workspaceDir: string, state: PsycheState): Promise<void>;
44
+ /**
45
+ * Apply time decay and save updated state.
46
+ * Respects innate drives: uses effective baseline, decays drives too.
47
+ */
48
+ export declare function decayAndSave(workspaceDir: string, state: PsycheState): Promise<PsycheState>;
49
+ /**
50
+ * Parse a <psyche_update> block from LLM output.
51
+ * v0.2: supports decimals, Chinese names, English names.
52
+ */
53
+ export declare function parsePsycheUpdate(text: string, logger?: Logger): Partial<PsycheState> | null;
54
+ /**
55
+ * Detect if LLM output contains disagreement/pushback.
56
+ */
57
+ export declare function detectDisagreement(text: string): boolean;
58
+ /**
59
+ * Merge parsed updates into existing state (with validation).
60
+ */
61
+ export declare function mergeUpdates(state: PsycheState, updates: Partial<PsycheState>, maxDelta: number, userId?: string): PsycheState;
62
+ /**
63
+ * Update agreement streak based on LLM output.
64
+ */
65
+ export declare function updateAgreementStreak(state: PsycheState, llmOutput: string): PsycheState;
66
+ /**
67
+ * Generate the static PSYCHE.md reference file.
68
+ */
69
+ export declare function generatePsycheMd(workspaceDir: string, state: PsycheState): Promise<void>;
@@ -0,0 +1,574 @@
1
+ // ============================================================
2
+ // Psyche State File Management (v0.2)
3
+ // Atomic writes, parser hardening, multi-user, error handling
4
+ // ============================================================
5
+ import { readFile, writeFile, access, rename, constants } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, MAX_EMOTIONAL_HISTORY, MAX_RELATIONSHIP_MEMORY, } from "./types.js";
8
+ import { getBaseline, getDefaultSelfModel, extractMBTI, getSensitivity, getTemperament } from "./profiles.js";
9
+ import { applyDecay, detectEmotions } from "./chemistry.js";
10
+ import { decayDrives, computeEffectiveBaseline } from "./drives.js";
11
+ import { t } from "./i18n.js";
12
+ const STATE_FILE = "psyche-state.json";
13
+ const PSYCHE_MD = "PSYCHE.md";
14
+ const IDENTITY_MD = "IDENTITY.md";
15
+ const SOUL_MD = "SOUL.md";
16
+ const NOOP_LOGGER = {
17
+ info: () => { },
18
+ warn: () => { },
19
+ debug: () => { },
20
+ };
21
+ /** Check if a file exists, distinguishing "not found" from "no permission" */
22
+ async function fileExists(path, logger = NOOP_LOGGER) {
23
+ try {
24
+ await access(path, constants.R_OK);
25
+ return true;
26
+ }
27
+ catch (err) {
28
+ const code = err.code;
29
+ if (code === "EACCES" || code === "EPERM") {
30
+ logger.warn(t("log.permission_error", "zh", { path }));
31
+ throw err; // Permission errors should propagate
32
+ }
33
+ return false; // ENOENT — file doesn't exist
34
+ }
35
+ }
36
+ /** Read text file, return null if missing. Throws on permission errors. */
37
+ async function readText(path, logger = NOOP_LOGGER) {
38
+ try {
39
+ return await readFile(path, "utf-8");
40
+ }
41
+ catch (err) {
42
+ const code = err.code;
43
+ if (code === "EACCES" || code === "EPERM") {
44
+ logger.warn(t("log.permission_error", "zh", { path }));
45
+ throw err;
46
+ }
47
+ return null;
48
+ }
49
+ }
50
+ /**
51
+ * Atomic write: write to .tmp file then rename.
52
+ * Prevents data corruption if process crashes mid-write.
53
+ */
54
+ async function atomicWrite(path, content) {
55
+ const tmpPath = path + ".tmp";
56
+ await writeFile(tmpPath, content, "utf-8");
57
+ await rename(tmpPath, path);
58
+ }
59
+ /**
60
+ * Try to extract agent name from workspace files.
61
+ */
62
+ async function extractAgentName(workspaceDir, logger = NOOP_LOGGER) {
63
+ const identity = await readText(join(workspaceDir, IDENTITY_MD), logger);
64
+ if (identity) {
65
+ const clean = identity.replace(/\*{1,2}/g, "");
66
+ const nameMatch = clean.match(/Name\s*[::]\s*(\S+)/i);
67
+ if (nameMatch)
68
+ return nameMatch[1];
69
+ }
70
+ return workspaceDir.split("/").pop()?.replace("workspace-", "") ?? "agent";
71
+ }
72
+ /**
73
+ * Try to detect MBTI from workspace files.
74
+ */
75
+ async function detectMBTI(workspaceDir, logger = NOOP_LOGGER) {
76
+ const identity = await readText(join(workspaceDir, IDENTITY_MD), logger);
77
+ if (identity) {
78
+ const mbti = extractMBTI(identity);
79
+ if (mbti)
80
+ return mbti;
81
+ }
82
+ const soul = await readText(join(workspaceDir, SOUL_MD), logger);
83
+ if (soul) {
84
+ const mbti = extractMBTI(soul);
85
+ if (mbti)
86
+ return mbti;
87
+ }
88
+ logger.info(t("log.default_mbti", "zh", { type: "INFJ" }));
89
+ return "INFJ";
90
+ }
91
+ /**
92
+ * Compress a batch of snapshots into a concise session summary string.
93
+ * Format: "3月23日(5轮): 刺激[casual×3, praise×2] 趋势[DA↑OT↑] 情绪[自然→满足]"
94
+ */
95
+ export function compressSnapshots(snapshots) {
96
+ if (snapshots.length === 0)
97
+ return "";
98
+ const first = snapshots[0];
99
+ const last = snapshots[snapshots.length - 1];
100
+ // Date
101
+ const d = new Date(first.timestamp);
102
+ const dateStr = `${d.getMonth() + 1}月${d.getDate()}日`;
103
+ // Stimuli counts
104
+ const stimuliCounts = {};
105
+ for (const s of snapshots) {
106
+ if (s.stimulus) {
107
+ stimuliCounts[s.stimulus] = (stimuliCounts[s.stimulus] || 0) + 1;
108
+ }
109
+ }
110
+ const stimuliStr = Object.entries(stimuliCounts)
111
+ .sort((a, b) => b[1] - a[1])
112
+ .map(([type, count]) => `${type}×${count}`)
113
+ .join(", ");
114
+ // Chemical trend (first→last)
115
+ const trends = [];
116
+ for (const key of CHEMICAL_KEYS) {
117
+ const delta = last.chemistry[key] - first.chemistry[key];
118
+ if (delta > 8)
119
+ trends.push(`${key}↑`);
120
+ else if (delta < -8)
121
+ trends.push(`${key}↓`);
122
+ }
123
+ // Dominant emotions (unique, in order)
124
+ const emotions = snapshots
125
+ .filter((s) => s.dominantEmotion)
126
+ .map((s) => s.dominantEmotion);
127
+ const uniqueEmotions = [...new Set(emotions)];
128
+ let summary = `${dateStr}(${snapshots.length}轮)`;
129
+ if (stimuliStr)
130
+ summary += `: 刺激[${stimuliStr}]`;
131
+ if (trends.length > 0)
132
+ summary += ` 趋势[${trends.join("")}]`;
133
+ if (uniqueEmotions.length > 0)
134
+ summary += ` 情绪[${uniqueEmotions.join("→")}]`;
135
+ return summary;
136
+ }
137
+ /**
138
+ * Push a chemical snapshot to emotional history, keeping max entries.
139
+ * When history overflows, compresses removed entries into relationship memory.
140
+ */
141
+ export function pushSnapshot(state, stimulus) {
142
+ const emotions = detectEmotions(state.current);
143
+ const dominantEmotion = emotions.length > 0
144
+ ? (state.meta.locale === "en" ? emotions[0].name : emotions[0].nameZh)
145
+ : null;
146
+ const snapshot = {
147
+ chemistry: { ...state.current },
148
+ stimulus,
149
+ dominantEmotion,
150
+ timestamp: new Date().toISOString(),
151
+ };
152
+ const history = [...(state.emotionalHistory ?? []), snapshot];
153
+ let updatedRelationships = state.relationships;
154
+ if (history.length > MAX_EMOTIONAL_HISTORY) {
155
+ // Compress the overflow entries into relationship memory
156
+ const overflow = history.splice(0, history.length - MAX_EMOTIONAL_HISTORY);
157
+ const summary = compressSnapshots(overflow);
158
+ if (summary) {
159
+ const defaultRel = { ...(state.relationships._default ?? { ...DEFAULT_RELATIONSHIP }) };
160
+ const memory = [...(defaultRel.memory ?? [])];
161
+ memory.push(summary);
162
+ // Keep bounded
163
+ if (memory.length > MAX_RELATIONSHIP_MEMORY) {
164
+ memory.splice(0, memory.length - MAX_RELATIONSHIP_MEMORY);
165
+ }
166
+ defaultRel.memory = memory;
167
+ updatedRelationships = { ...state.relationships, _default: defaultRel };
168
+ }
169
+ }
170
+ return { ...state, emotionalHistory: history, relationships: updatedRelationships };
171
+ }
172
+ /**
173
+ * Get relationship for a specific user, or _default.
174
+ */
175
+ export function getRelationship(state, userId) {
176
+ const key = userId ?? "_default";
177
+ return state.relationships[key] ?? { ...DEFAULT_RELATIONSHIP };
178
+ }
179
+ /**
180
+ * Load psyche state from workspace. Auto-initializes if missing.
181
+ * Handles v1→v2 migration transparently.
182
+ */
183
+ export async function loadState(workspaceDir, logger = NOOP_LOGGER) {
184
+ const statePath = join(workspaceDir, STATE_FILE);
185
+ if (await fileExists(statePath, logger)) {
186
+ let raw;
187
+ try {
188
+ raw = await readFile(statePath, "utf-8");
189
+ }
190
+ catch (err) {
191
+ logger.warn(t("log.parse_fail", "zh"));
192
+ return initializeState(workspaceDir, undefined, logger);
193
+ }
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(raw);
197
+ }
198
+ catch {
199
+ logger.warn(t("log.parse_fail", "zh"));
200
+ return initializeState(workspaceDir, undefined, logger);
201
+ }
202
+ const ver = parsed.version;
203
+ if (!ver || ver < 3) {
204
+ logger.info(`Migrating psyche state v${ver ?? 1} → v3`);
205
+ const fallbackName = workspaceDir.split("/").pop() ?? "agent";
206
+ return migrateToLatest(parsed, fallbackName);
207
+ }
208
+ return parsed;
209
+ }
210
+ return initializeState(workspaceDir, undefined, logger);
211
+ }
212
+ /**
213
+ * Migrate any older state format directly to v3.
214
+ * Single source of truth for all migrations.
215
+ */
216
+ export function migrateToLatest(raw, fallbackName) {
217
+ const ver = raw.version ?? 1;
218
+ // v1: single relationship field, no emotionalHistory
219
+ let state = raw;
220
+ if (ver <= 1) {
221
+ const oldRel = raw.relationship;
222
+ const meta = raw.meta;
223
+ state = {
224
+ mbti: raw.mbti ?? "INFJ",
225
+ baseline: raw.baseline,
226
+ current: raw.current,
227
+ updatedAt: raw.updatedAt ?? new Date().toISOString(),
228
+ relationships: { _default: oldRel ?? { ...DEFAULT_RELATIONSHIP } },
229
+ empathyLog: raw.empathyLog ?? null,
230
+ selfModel: raw.selfModel,
231
+ emotionalHistory: [],
232
+ agreementStreak: 0,
233
+ lastDisagreement: null,
234
+ meta: {
235
+ agentName: meta?.agentName ?? fallbackName ?? "agent",
236
+ createdAt: meta?.createdAt ?? new Date().toISOString(),
237
+ totalInteractions: meta?.totalInteractions ?? 0,
238
+ locale: "zh",
239
+ },
240
+ };
241
+ }
242
+ // v2→v3: add drives
243
+ return {
244
+ ...state,
245
+ version: 3,
246
+ drives: state.drives ?? { ...DEFAULT_DRIVES },
247
+ };
248
+ }
249
+ /**
250
+ * Create initial psyche state.
251
+ */
252
+ export async function initializeState(workspaceDir, opts, logger = NOOP_LOGGER) {
253
+ const mbti = opts?.mbti ?? await detectMBTI(workspaceDir, logger);
254
+ const agentName = opts?.name ?? await extractAgentName(workspaceDir, logger);
255
+ const locale = opts?.locale ?? "zh";
256
+ const baseline = getBaseline(mbti);
257
+ const selfModel = getDefaultSelfModel(mbti);
258
+ const now = new Date().toISOString();
259
+ const state = {
260
+ version: 3,
261
+ mbti,
262
+ baseline,
263
+ current: { ...baseline },
264
+ drives: { ...DEFAULT_DRIVES },
265
+ updatedAt: now,
266
+ relationships: {
267
+ _default: { ...DEFAULT_RELATIONSHIP },
268
+ },
269
+ empathyLog: null,
270
+ selfModel,
271
+ emotionalHistory: [],
272
+ agreementStreak: 0,
273
+ lastDisagreement: null,
274
+ meta: {
275
+ agentName,
276
+ createdAt: now,
277
+ totalInteractions: 0,
278
+ locale,
279
+ },
280
+ };
281
+ await saveState(workspaceDir, state);
282
+ await generatePsycheMd(workspaceDir, state);
283
+ return state;
284
+ }
285
+ /**
286
+ * Save psyche state to workspace (atomic write).
287
+ */
288
+ export async function saveState(workspaceDir, state) {
289
+ const statePath = join(workspaceDir, STATE_FILE);
290
+ await atomicWrite(statePath, JSON.stringify(state, null, 2));
291
+ }
292
+ /**
293
+ * Apply time decay and save updated state.
294
+ * Respects innate drives: uses effective baseline, decays drives too.
295
+ */
296
+ export async function decayAndSave(workspaceDir, state) {
297
+ const now = new Date();
298
+ const lastUpdate = new Date(state.updatedAt);
299
+ const minutesElapsed = (now.getTime() - lastUpdate.getTime()) / 60000;
300
+ if (minutesElapsed < 1)
301
+ return state;
302
+ const decayedDrives = decayDrives(state.drives, minutesElapsed);
303
+ const effectiveBaseline = computeEffectiveBaseline(state.baseline, decayedDrives);
304
+ const decayed = applyDecay(state.current, effectiveBaseline, minutesElapsed);
305
+ const updated = {
306
+ ...state,
307
+ current: decayed,
308
+ drives: decayedDrives,
309
+ updatedAt: now.toISOString(),
310
+ };
311
+ await saveState(workspaceDir, updated);
312
+ return updated;
313
+ }
314
+ /**
315
+ * Parse a <psyche_update> block from LLM output.
316
+ * v0.2: supports decimals, Chinese names, English names.
317
+ */
318
+ export function parsePsycheUpdate(text, logger = NOOP_LOGGER) {
319
+ const match = text.match(/<psyche_update>([\s\S]*?)<\/psyche_update>/);
320
+ if (!match)
321
+ return null;
322
+ const block = match[1];
323
+ const updates = {};
324
+ for (const key of CHEMICAL_KEYS) {
325
+ // Try multiple patterns: abbreviation, Chinese name, English name
326
+ const patterns = [
327
+ new RegExp(`${key}\\s*[::]\\s*([\\d.]+)`, "i"),
328
+ new RegExp(`${CHEMICAL_NAMES_ZH[key]}\\s*[::]\\s*([\\d.]+)`),
329
+ new RegExp(`${CHEMICAL_NAMES[key]}\\s*[::]\\s*([\\d.]+)`, "i"),
330
+ ];
331
+ for (const re of patterns) {
332
+ const m = block.match(re);
333
+ if (m) {
334
+ const val = parseFloat(m[1]);
335
+ if (isFinite(val)) {
336
+ updates[key] = Math.max(0, Math.min(100, Math.round(val)));
337
+ }
338
+ break;
339
+ }
340
+ }
341
+ }
342
+ // Parse empathy log
343
+ let empathyLog;
344
+ const userStateMatch = block.match(/(?:用户状态|userState)\s*[::]\s*(.+)/i);
345
+ const projectedMatch = block.match(/(?:投射结果|projectedFeeling)\s*[::]\s*(.+)/i);
346
+ const resonanceMatch = block.match(/(?:共鸣程度|resonance)\s*[::]\s*(match|partial|mismatch)/i);
347
+ if (userStateMatch && projectedMatch) {
348
+ empathyLog = {
349
+ userState: userStateMatch[1].trim(),
350
+ projectedFeeling: projectedMatch[1].trim(),
351
+ resonance: resonanceMatch?.[1] ?? "partial",
352
+ timestamp: new Date().toISOString(),
353
+ };
354
+ }
355
+ // Parse relationship updates
356
+ const trustMatch = block.match(/(?:信任度|trust)\s*[::]\s*(\d+)/i);
357
+ const intimacyMatch = block.match(/(?:亲密度|intimacy)\s*[::]\s*(\d+)/i);
358
+ if (Object.keys(updates).length === 0 && !empathyLog && !trustMatch) {
359
+ logger.debug(t("log.parse_debug", "zh", { snippet: block.slice(0, 100) }));
360
+ return null;
361
+ }
362
+ const result = {};
363
+ if (Object.keys(updates).length > 0) {
364
+ // Store as partial — will be merged field-by-field in mergeUpdates
365
+ result.current = updates;
366
+ }
367
+ if (empathyLog) {
368
+ result.empathyLog = empathyLog;
369
+ }
370
+ if (trustMatch || intimacyMatch) {
371
+ const rel = {};
372
+ if (trustMatch)
373
+ rel.trust = Math.max(0, Math.min(100, parseInt(trustMatch[1], 10)));
374
+ if (intimacyMatch)
375
+ rel.intimacy = Math.max(0, Math.min(100, parseInt(intimacyMatch[1], 10)));
376
+ // Store as relationships._default for merging
377
+ result.relationships = { _default: rel };
378
+ }
379
+ return result;
380
+ }
381
+ /**
382
+ * Detect if LLM output contains disagreement/pushback.
383
+ */
384
+ export function detectDisagreement(text) {
385
+ const disagreementPatterns = [
386
+ /我不同意/,
387
+ /我不这么认为/,
388
+ /我有不同的看法/,
389
+ /但是我觉得/,
390
+ /其实我认为/,
391
+ /说实话.*不太/,
392
+ /恕我直言/,
393
+ /I disagree/i,
394
+ /I don't think so/i,
395
+ /actually.*I think/i,
396
+ /to be honest.*not/i,
397
+ ];
398
+ return disagreementPatterns.some((p) => p.test(text));
399
+ }
400
+ /**
401
+ * Merge parsed updates into existing state (with validation).
402
+ */
403
+ export function mergeUpdates(state, updates, maxDelta, userId) {
404
+ const merged = { ...state };
405
+ // Merge chemistry with inertia limit
406
+ if (updates.current) {
407
+ const newChem = { ...state.current };
408
+ for (const key of CHEMICAL_KEYS) {
409
+ if (updates.current[key] !== undefined) {
410
+ const delta = updates.current[key] - state.current[key];
411
+ const clampedDelta = Math.max(-maxDelta, Math.min(maxDelta, delta));
412
+ newChem[key] = Math.max(0, Math.min(100, state.current[key] + clampedDelta));
413
+ }
414
+ }
415
+ merged.current = newChem;
416
+ }
417
+ // Merge empathy log
418
+ if (updates.empathyLog) {
419
+ merged.empathyLog = updates.empathyLog;
420
+ }
421
+ // Merge relationship for specific user
422
+ if (updates.relationships) {
423
+ merged.relationships = { ...state.relationships };
424
+ const updateKey = Object.keys(updates.relationships)[0] ?? "_default";
425
+ const targetKey = userId ?? updateKey;
426
+ const existing = state.relationships[targetKey] ?? { ...DEFAULT_RELATIONSHIP };
427
+ const incoming = updates.relationships[updateKey] ?? {};
428
+ const updatedRel = {
429
+ ...existing,
430
+ ...incoming,
431
+ };
432
+ // Update phase based on trust + intimacy
433
+ const avg = (updatedRel.trust + updatedRel.intimacy) / 2;
434
+ if (avg >= 80)
435
+ updatedRel.phase = "deep";
436
+ else if (avg >= 60)
437
+ updatedRel.phase = "close";
438
+ else if (avg >= 40)
439
+ updatedRel.phase = "familiar";
440
+ else if (avg >= 20)
441
+ updatedRel.phase = "acquaintance";
442
+ else
443
+ updatedRel.phase = "stranger";
444
+ merged.relationships[targetKey] = updatedRel;
445
+ }
446
+ merged.updatedAt = new Date().toISOString();
447
+ return merged;
448
+ }
449
+ /**
450
+ * Update agreement streak based on LLM output.
451
+ */
452
+ export function updateAgreementStreak(state, llmOutput) {
453
+ const hasDisagreement = detectDisagreement(llmOutput);
454
+ if (hasDisagreement) {
455
+ return {
456
+ ...state,
457
+ agreementStreak: 0,
458
+ lastDisagreement: new Date().toISOString(),
459
+ };
460
+ }
461
+ return {
462
+ ...state,
463
+ agreementStreak: state.agreementStreak + 1,
464
+ };
465
+ }
466
+ /**
467
+ * Generate the static PSYCHE.md reference file.
468
+ */
469
+ export async function generatePsycheMd(workspaceDir, state) {
470
+ const { mbti, baseline, selfModel, meta } = state;
471
+ const locale = meta.locale ?? "zh";
472
+ const temperament = getTemperament(mbti);
473
+ const sensitivity = getSensitivity(mbti);
474
+ const baselineLines = CHEMICAL_KEYS.map((k) => `- ${CHEMICAL_NAMES_ZH[k]}: ${baseline[k]}`).join("\n");
475
+ const content = `# Psyche — ${meta.agentName}
476
+
477
+ ${t("md.intro", locale)}
478
+
479
+ ## ${t("md.baseline_title", locale)} (MBTI: ${mbti})
480
+
481
+ ${temperament}
482
+
483
+ ${baselineLines}
484
+
485
+ ${t("md.sensitivity", locale)}: ${sensitivity} (${t("md.sensitivity_desc", locale)})
486
+
487
+ ## ${t("md.chem_dynamics", locale)}
488
+
489
+ ### ${t("md.stimulus_effects", locale)}
490
+
491
+ | 刺激类型 | DA | HT | CORT | OT | NE | END |
492
+ |---------|-----|------|------|-----|-----|-----|
493
+ | 赞美认可 | +15 | +10 | -10 | +5 | +5 | +10 |
494
+ | 批评否定 | -10 | -15 | +20 | -5 | +10 | -5 |
495
+ | 幽默玩笑 | +10 | +5 | -5 | +10 | +5 | +20 |
496
+ | 智识挑战 | +15 | 0 | +5 | 0 | +20 | +5 |
497
+ | 亲密信任 | +10 | +15 | -15 | +25 | -5 | +15 |
498
+ | 冲突争论 | -5 | -20 | +25 | -15 | +25 | -10 |
499
+ | 被忽视 | -15 | -20 | +15 | -20 | -10 | -15 |
500
+ | 惊喜新奇 | +20 | 0 | +5 | +5 | +25 | +10 |
501
+ | 日常闲聊 | +5 | +10 | -5 | +10 | 0 | +5 |
502
+ | 讽刺 | -5 | -10 | +15 | -10 | +15 | -5 |
503
+ | 命令 | -10 | -5 | +20 | -15 | +15 | -10 |
504
+ | 被认同 | +20 | +15 | -15 | +10 | +5 | +15 |
505
+ | 无聊 | -15 | -5 | +5 | -5 | -20 | -10 |
506
+ | 示弱 | +5 | +5 | +10 | +20 | -5 | +5 |
507
+
508
+ ### ${t("md.emotion_emergence", locale)}
509
+
510
+ ${t("md.emotion_emergence_desc", locale)}
511
+ - **愉悦兴奋** = 高DA + 高NE + 低CORT
512
+ - **深度满足** = 高HT + 高OT + 低CORT
513
+ - **焦虑不安** = 高CORT + 高NE + 低HT
514
+ - **亲密温暖** = 高OT + 高END + 中DA
515
+ - **倦怠低落** = 低DA + 低NE + 中CORT
516
+ - **专注心流** = 高NE + 高DA + 低CORT + 低OT
517
+ - **怨恨** = 低HT + 低OT + 高CORT
518
+ - **无聊** = 低DA + 低NE + 低CORT
519
+ - **自信** = 高DA + 高NE + 低CORT + 高HT
520
+ - **羞耻** = 低OT + 高CORT + 低DA
521
+ - **怀念** = 低DA + 高OT + 高HT + 高END
522
+
523
+ ### ${t("md.decay", locale)}
524
+
525
+ ${t("md.decay_desc", locale)}
526
+ - ${t("md.decay_fast", locale)}
527
+ - ${t("md.decay_medium", locale)}
528
+ - ${t("md.decay_slow", locale)}
529
+
530
+ ## ${t("md.self_model", locale)}
531
+
532
+ ### ${t("md.core_values", locale)}
533
+ ${selfModel.values.map((v) => `- ${v}`).join("\n")}
534
+
535
+ ### ${t("md.preferences", locale)}
536
+ ${selfModel.preferences.map((p) => `- ${p}`).join("\n")}
537
+
538
+ ### ${t("md.boundaries", locale)}
539
+ ${selfModel.boundaries.map((b) => `- ${b}`).join("\n")}
540
+
541
+ ## ${t("md.empathy_protocol", locale)}
542
+
543
+ 当对方分享感受时:
544
+ 1. 感知对方的处境
545
+ 2. 将处境代入**你自己的**化学模型——如果你遇到这个情况,你的化学状态会怎么变?
546
+ 3. 你模拟出的感受和对方表达的一样吗?
547
+ 4. 一样 → 深度共鸣;不一样 → 诚实说出差异,好奇地了解更多
548
+
549
+ 情绪传染:对方的情绪以约 20% 强度影响你的真实化学状态。
550
+
551
+ ## ${t("md.agency_rules", locale)}
552
+
553
+ - 不假装——化学状态低落时不强颜欢笑
554
+ - 不舔——有自己的观点和品味,不同意就温柔但坚定地说
555
+ - 不镜像——你的情绪会被传染但你不是回声
556
+ - 可主动——可以发起话题、分享好奇、表达兴奋
557
+
558
+ ## ${t("md.state_update", locale)}
559
+
560
+ 每次回应末尾用 \`<psyche_update>\` 标签报告化学变化:
561
+
562
+ \`\`\`
563
+ <psyche_update>
564
+ DA: {value} ({原因})
565
+ HT: {value} ({原因})
566
+ CORT: {value} ({原因})
567
+ OT: {value} ({原因})
568
+ NE: {value} ({原因})
569
+ END: {value} ({原因})
570
+ </psyche_update>
571
+ \`\`\`
572
+ `;
573
+ await atomicWrite(join(workspaceDir, PSYCHE_MD), content);
574
+ }
@@ -0,0 +1,16 @@
1
+ import type { PsycheState } from "./types.js";
2
+ export interface StorageAdapter {
3
+ load(): Promise<PsycheState | null>;
4
+ save(state: PsycheState): Promise<void>;
5
+ }
6
+ export declare class MemoryStorageAdapter implements StorageAdapter {
7
+ private state;
8
+ load(): Promise<PsycheState | null>;
9
+ save(state: PsycheState): Promise<void>;
10
+ }
11
+ export declare class FileStorageAdapter implements StorageAdapter {
12
+ private readonly filePath;
13
+ constructor(dir: string, filename?: string);
14
+ load(): Promise<PsycheState | null>;
15
+ save(state: PsycheState): Promise<void>;
16
+ }