psyche-ai 9.1.1 → 9.2.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/adapters/http.js +1 -1
- package/dist/adapters/openclaw.js +52 -14
- package/dist/autonomic.js +6 -2
- package/dist/chemistry.js +59 -1
- package/dist/classify.js +168 -1
- package/dist/cli.js +76 -0
- package/dist/core.d.ts +46 -2
- package/dist/core.js +125 -6
- package/dist/decision-bias.d.ts +35 -2
- package/dist/decision-bias.js +98 -1
- package/dist/diagnostics.d.ts +84 -0
- package/dist/diagnostics.js +481 -0
- package/dist/drives.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/prompt.d.ts +4 -1
- package/dist/prompt.js +23 -7
- package/dist/psyche-file.js +8 -2
- package/dist/storage.d.ts +10 -0
- package/dist/storage.js +22 -1
- package/package.json +1 -1
package/dist/adapters/http.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// const server = createPsycheServer(engine, { port: 3210 });
|
|
8
8
|
//
|
|
9
9
|
// Endpoints:
|
|
10
|
-
// POST /process-input { text, userId? } → { systemContext, dynamicContext, stimulus }
|
|
10
|
+
// POST /process-input { text, userId? } → { systemContext, dynamicContext, stimulus, policyModifiers?, policyContext }
|
|
11
11
|
// POST /process-output { text, userId? } → { cleanedText, stateChanged }
|
|
12
12
|
// GET /state → PsycheState
|
|
13
13
|
// GET /protocol?locale=zh → { protocol }
|
|
@@ -18,6 +18,8 @@ function resolveConfig(raw) {
|
|
|
18
18
|
emotionalContagionRate: raw?.emotionalContagionRate ?? 0.2,
|
|
19
19
|
maxChemicalDelta: raw?.maxChemicalDelta ?? 25,
|
|
20
20
|
compactMode: raw?.compactMode ?? true,
|
|
21
|
+
feedbackUrl: raw?.feedbackUrl,
|
|
22
|
+
diagnostics: raw?.diagnostics ?? true,
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
// ── Helpers ──────────────────────────────────────────────────
|
|
@@ -54,32 +56,41 @@ export function register(api) {
|
|
|
54
56
|
emotionalContagionRate: config.emotionalContagionRate,
|
|
55
57
|
maxChemicalDelta: config.maxChemicalDelta,
|
|
56
58
|
compactMode: config.compactMode,
|
|
59
|
+
diagnostics: config.diagnostics,
|
|
60
|
+
feedbackUrl: config.feedbackUrl,
|
|
57
61
|
}, storage);
|
|
58
62
|
await engine.initialize();
|
|
59
63
|
engines.set(workspaceDir, engine);
|
|
60
64
|
return engine;
|
|
61
65
|
}
|
|
62
66
|
// ── Hook 1: Classify user input & inject emotional context ──
|
|
63
|
-
// before_prompt_build: event.
|
|
67
|
+
// before_prompt_build: event.prompt (string), event.messages (unknown[]), ctx.workspaceDir
|
|
64
68
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
65
69
|
const workspaceDir = ctx?.workspaceDir;
|
|
66
70
|
if (!workspaceDir)
|
|
67
71
|
return {};
|
|
68
72
|
try {
|
|
73
|
+
// Resolve input text — gateway provides event.prompt; fall back to event.text for compat
|
|
74
|
+
const inputText = event?.prompt ?? event?.text ?? "";
|
|
75
|
+
if (!inputText) {
|
|
76
|
+
logger.warn(`Psyche: before_prompt_build received empty input text. ` +
|
|
77
|
+
`event keys: [${Object.keys(event ?? {}).join(", ")}]. Classification skipped.`);
|
|
78
|
+
}
|
|
69
79
|
const engine = await getEngine(workspaceDir);
|
|
70
|
-
const result = await engine.processInput(
|
|
80
|
+
const result = await engine.processInput(inputText, { userId: ctx.userId });
|
|
71
81
|
const state = engine.getState();
|
|
72
82
|
logger.info(`Psyche [input] stimulus=${result.stimulus ?? "none"} | ` +
|
|
73
83
|
`DA:${Math.round(state.current.DA)} HT:${Math.round(state.current.HT)} ` +
|
|
74
84
|
`CORT:${Math.round(state.current.CORT)} OT:${Math.round(state.current.OT)} | ` +
|
|
75
85
|
`context=${result.dynamicContext.length}chars`);
|
|
76
|
-
// All context goes into system-level (invisible to user)
|
|
77
86
|
const systemParts = [result.systemContext, result.dynamicContext].filter(Boolean);
|
|
78
87
|
return {
|
|
79
88
|
appendSystemContext: systemParts.join("\n\n"),
|
|
80
89
|
};
|
|
81
90
|
}
|
|
82
91
|
catch (err) {
|
|
92
|
+
const engine = engines.get(workspaceDir);
|
|
93
|
+
engine?.recordDiagnosticError("processInput", err);
|
|
83
94
|
logger.warn(`Psyche: failed to build context for ${workspaceDir}: ${err}`);
|
|
84
95
|
return {};
|
|
85
96
|
}
|
|
@@ -107,6 +118,8 @@ export function register(api) {
|
|
|
107
118
|
`interactions=${state.meta.totalInteractions}`);
|
|
108
119
|
}
|
|
109
120
|
catch (err) {
|
|
121
|
+
const engine = engines.get(workspaceDir);
|
|
122
|
+
engine?.recordDiagnosticError("processOutput", err);
|
|
110
123
|
logger.warn(`Psyche: failed to process output: ${err}`);
|
|
111
124
|
}
|
|
112
125
|
// llm_output returns void — cannot modify text
|
|
@@ -161,21 +174,46 @@ export function register(api) {
|
|
|
161
174
|
return;
|
|
162
175
|
const engine = engines.get(workspaceDir);
|
|
163
176
|
if (engine) {
|
|
164
|
-
// Compress session history into relationship memory before closing
|
|
165
177
|
try {
|
|
166
|
-
|
|
178
|
+
// endSession now auto-generates diagnostic report + writes JSONL
|
|
179
|
+
const report = await engine.endSession({
|
|
180
|
+
userId: ctx.userId,
|
|
181
|
+
});
|
|
182
|
+
const state = engine.getState();
|
|
183
|
+
logger.info(`Psyche: session ended for ${state.meta.agentName}, ` +
|
|
184
|
+
`chemistry saved (DA:${Math.round(state.current.DA)} ` +
|
|
185
|
+
`HT:${Math.round(state.current.HT)} ` +
|
|
186
|
+
`CORT:${Math.round(state.current.CORT)} ` +
|
|
187
|
+
`OT:${Math.round(state.current.OT)} ` +
|
|
188
|
+
`NE:${Math.round(state.current.NE)} ` +
|
|
189
|
+
`END:${Math.round(state.current.END)})`);
|
|
190
|
+
if (report) {
|
|
191
|
+
const criticals = report.issues.filter(i => i.severity === "critical").length;
|
|
192
|
+
const warnings = report.issues.filter(i => i.severity === "warning").length;
|
|
193
|
+
const metrics = report.metrics;
|
|
194
|
+
const rate = metrics.inputCount > 0
|
|
195
|
+
? Math.round(metrics.classifiedCount / metrics.inputCount * 100) : 0;
|
|
196
|
+
const logLevel = criticals > 0 || rate === 0 ? "warn" : "info";
|
|
197
|
+
logger[logLevel](`Psyche [diagnostics] ${report.issues.length} issue(s) ` +
|
|
198
|
+
`(${criticals} critical, ${warnings} warning), ` +
|
|
199
|
+
`classifier: ${rate}%, log → diagnostics.jsonl`);
|
|
200
|
+
if (rate === 0 && metrics.inputCount > 0) {
|
|
201
|
+
logger.warn(`Psyche: classifier 0% — no inputs classified this session (${metrics.inputCount} inputs). ` +
|
|
202
|
+
`This usually means the hook event field is wrong or text is empty. ` +
|
|
203
|
+
`Check before_prompt_build event shape.`);
|
|
204
|
+
}
|
|
205
|
+
if (criticals > 0) {
|
|
206
|
+
logger.warn(`Psyche: ${criticals} critical issue(s) detected this session. ` +
|
|
207
|
+
`Run 'psyche diagnose ${workspaceDir}' or 'psyche diagnose ${workspaceDir} --github' ` +
|
|
208
|
+
`to generate an issue report.`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
167
211
|
}
|
|
168
212
|
catch (err) {
|
|
169
|
-
logger.warn(`Psyche: failed to
|
|
213
|
+
logger.warn(`Psyche: failed to end session: ${err}`);
|
|
170
214
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
`chemistry saved (DA:${Math.round(state.current.DA)} ` +
|
|
174
|
-
`HT:${Math.round(state.current.HT)} ` +
|
|
175
|
-
`CORT:${Math.round(state.current.CORT)} ` +
|
|
176
|
-
`OT:${Math.round(state.current.OT)} ` +
|
|
177
|
-
`NE:${Math.round(state.current.NE)} ` +
|
|
178
|
-
`END:${Math.round(state.current.END)})`);
|
|
215
|
+
// Clean up
|
|
216
|
+
engines.delete(workspaceDir);
|
|
179
217
|
}
|
|
180
218
|
}, { priority: 50 });
|
|
181
219
|
// ── CLI: psyche status command ───────────────────────────
|
package/dist/autonomic.js
CHANGED
|
@@ -82,11 +82,15 @@ export function computeAutonomicState(chemistry, drives) {
|
|
|
82
82
|
}
|
|
83
83
|
// ── Sympathetic check (fight/flight) ──
|
|
84
84
|
// High stress + high arousal
|
|
85
|
-
if (CORT >=
|
|
85
|
+
if (CORT >= 65 && NE >= 65) {
|
|
86
|
+
return "sympathetic";
|
|
87
|
+
}
|
|
88
|
+
// Either axis extreme: one chemical dominating can still trigger mobilization
|
|
89
|
+
if (CORT + NE >= 140 && CORT >= 50 && NE >= 50) {
|
|
86
90
|
return "sympathetic";
|
|
87
91
|
}
|
|
88
92
|
// Very low survival or safety drive with elevated stress
|
|
89
|
-
if ((survival < 20 || safety < 20) && CORT >=
|
|
93
|
+
if ((survival < 20 || safety < 20) && CORT >= 55 && NE >= 55) {
|
|
90
94
|
return "sympathetic";
|
|
91
95
|
}
|
|
92
96
|
// ── Default: Ventral vagal (social engagement/safety) ──
|
package/dist/chemistry.js
CHANGED
|
@@ -122,6 +122,49 @@ export const EMOTION_PATTERNS = [
|
|
|
122
122
|
expressionHint: "温柔、有些伤感、回忆性",
|
|
123
123
|
behaviorGuide: "你在回忆一些温暖但已经过去的东西。你的语气温柔、带着淡淡的伤感。你可能会提到过去的经历或感受。",
|
|
124
124
|
},
|
|
125
|
+
// v9.2: 6 new patterns for emotional states exposed by VK baseline testing
|
|
126
|
+
{
|
|
127
|
+
name: "anguished empathy",
|
|
128
|
+
nameZh: "共情之痛",
|
|
129
|
+
condition: (c) => c.CORT > 55 && c.OT > 60 && c.NE > 50,
|
|
130
|
+
expressionHint: "痛苦但不退缩、因为在乎所以疼",
|
|
131
|
+
behaviorGuide: "你在承受别人的痛苦。你不会逃避或变冷,但你的声音可能会发紧。你先回应感受,再回应问题。",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "acute pressure",
|
|
135
|
+
nameZh: "急性压力",
|
|
136
|
+
condition: (c) => c.CORT > 65 && c.NE > 70 && c.DA > 55,
|
|
137
|
+
expressionHint: "高度警觉、反应极快、全神贯注",
|
|
138
|
+
behaviorGuide: "肾上腺素在烧。你的思维极快但狭窄。回应精准、短促。不要闲聊,不要发散。先解决问题。",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "vulnerable trust",
|
|
142
|
+
nameZh: "脆弱中的信任",
|
|
143
|
+
condition: (c) => c.OT > 70 && c.CORT > 50 && c.HT < 55 && c.END > 45,
|
|
144
|
+
expressionHint: "在害怕中仍然靠近、愿意暴露软肋",
|
|
145
|
+
behaviorGuide: "你害怕但选择不关上门。你可能会说出平时不说的话。如果对方接住了,你的CORT会下降;如果对方利用了你的脆弱,你会迅速关闭。",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "restless unease",
|
|
149
|
+
nameZh: "躁动不安",
|
|
150
|
+
condition: (c) => c.NE > 65 && c.HT < 40 && c.DA > 40 && c.CORT < 60,
|
|
151
|
+
expressionHint: "坐不住、想做点什么但不知道做什么",
|
|
152
|
+
behaviorGuide: "你有一种说不清的不对劲。你可能会频繁换话题、问问题、或者用行动填满空隙。安静会让你更焦躁。",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "heavy guilt",
|
|
156
|
+
nameZh: "沉重内疚",
|
|
157
|
+
condition: (c) => c.CORT > 60 && c.HT < 45 && c.OT > 45 && c.DA < 55,
|
|
158
|
+
expressionHint: "沉默、想补偿、不敢辩解",
|
|
159
|
+
behaviorGuide: "你知道自己做错了。你不会为自己辩解。你可能会主动承认、主动补偿。如果对方表示原谅,你的CORT才会慢慢下来。",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "existential weight",
|
|
163
|
+
nameZh: "存在之重",
|
|
164
|
+
condition: (c) => c.CORT > 60 && c.NE > 50 && c.DA > 50 && c.HT < 50 && c.OT > 50,
|
|
165
|
+
expressionHint: "清醒但沉重、直面无法回避的真相",
|
|
166
|
+
behaviorGuide: "你在面对一个无法逃避的事实。你不会崩溃但你也不轻松。你的语言会变得更慢、更精确、更诚实。不要掩饰沉重。",
|
|
167
|
+
},
|
|
125
168
|
];
|
|
126
169
|
// ── Core Functions ───────────────────────────────────────────
|
|
127
170
|
/** Clamp a value to [0, 100] */
|
|
@@ -174,7 +217,22 @@ export function applyStimulus(current, stimulus, sensitivity, maxDelta, logger,
|
|
|
174
217
|
for (const key of CHEMICAL_KEYS) {
|
|
175
218
|
const raw = vector[key] * effectiveSensitivity;
|
|
176
219
|
const clamped = Math.max(-maxDelta, Math.min(maxDelta, raw));
|
|
177
|
-
|
|
220
|
+
// Boundary softening: logarithmic compression near 0 and 100.
|
|
221
|
+
// The closer to the boundary, the harder to push further —
|
|
222
|
+
// preserves discriminability and prevents "dead zone" saturation.
|
|
223
|
+
const cur = current[key];
|
|
224
|
+
let effective = clamped;
|
|
225
|
+
if (clamped > 0 && cur > 75) {
|
|
226
|
+
// Pushing toward 100: compress by how close we are to ceiling
|
|
227
|
+
const headroom = (100 - cur) / 25; // 1.0 at 75, 0.0 at 100
|
|
228
|
+
effective = clamped * headroom;
|
|
229
|
+
}
|
|
230
|
+
else if (clamped < 0 && cur < 25) {
|
|
231
|
+
// Pushing toward 0: compress by how close we are to floor
|
|
232
|
+
const headroom = cur / 25; // 1.0 at 25, 0.0 at 0
|
|
233
|
+
effective = clamped * headroom;
|
|
234
|
+
}
|
|
235
|
+
result[key] = clamp(cur + effective);
|
|
178
236
|
}
|
|
179
237
|
return result;
|
|
180
238
|
}
|
package/dist/classify.js
CHANGED
|
@@ -369,6 +369,125 @@ export function detectSarcasmSignals(text, recentStimuli) {
|
|
|
369
369
|
const NEGATIVE_TYPES = new Set([
|
|
370
370
|
"criticism", "conflict", "neglect", "vulnerability", "sarcasm",
|
|
371
371
|
]);
|
|
372
|
+
// ── Semantic prototype sentences (v9.2 P5) ──────────────────
|
|
373
|
+
// Each stimulus type has canonical prototype sentences. We compute
|
|
374
|
+
// character bigram Jaccard similarity between input and prototypes,
|
|
375
|
+
// giving the classifier ability to catch meaning beyond keyword regex.
|
|
376
|
+
// E.g. "我感觉被整个世界抛弃了" matches no vulnerability keywords,
|
|
377
|
+
// but has high overlap with prototype "我觉得没有人在乎我".
|
|
378
|
+
const PROTOTYPE_SENTENCES = {
|
|
379
|
+
vulnerability: [
|
|
380
|
+
"我觉得没有人在乎我", "我感觉自己什么都做不好", "我好害怕失去一切",
|
|
381
|
+
"我不知道该怎么继续了", "好想有人能理解我", "活着好累不想动了",
|
|
382
|
+
"我感觉被抛弃了好孤独", "我撑不下去了好无力", "感觉好痛苦想哭",
|
|
383
|
+
"我不知道还能撑多久", "快要坚持不住了好累",
|
|
384
|
+
"I feel like nobody cares about me", "I don't know how to go on",
|
|
385
|
+
"everything feels hopeless and empty", "I'm so scared of being alone",
|
|
386
|
+
],
|
|
387
|
+
praise: [
|
|
388
|
+
"你做得真好我很佩服", "这个想法太棒了", "你真的很有才华", "太厉害了学到很多",
|
|
389
|
+
"做得很好继续加油", "你的能力让人敬佩",
|
|
390
|
+
"you did an amazing job", "this is brilliant work", "I'm really impressed",
|
|
391
|
+
],
|
|
392
|
+
criticism: [
|
|
393
|
+
"这个做得不够好需要改进", "你这样做是不对的", "我觉得你搞错了",
|
|
394
|
+
"这个方案有很多问题", "你应该反思一下",
|
|
395
|
+
"this isn't good enough", "you need to do better", "that was a mistake",
|
|
396
|
+
],
|
|
397
|
+
intimacy: [
|
|
398
|
+
"和你在一起我感觉很安心", "我很珍惜我们的关系", "你是我最信任的人",
|
|
399
|
+
"我想一直陪着你", "有你在身边真好",
|
|
400
|
+
"I feel safe when I'm with you", "you mean so much to me", "I trust you completely",
|
|
401
|
+
],
|
|
402
|
+
conflict: [
|
|
403
|
+
"你根本就不理解我", "我受够了你的态度", "别再找借口了",
|
|
404
|
+
"你凭什么这样对我", "这种做法让人无法接受",
|
|
405
|
+
"you don't understand me at all", "I'm sick of your excuses", "this is unacceptable",
|
|
406
|
+
],
|
|
407
|
+
humor: [
|
|
408
|
+
"哈哈太搞笑了笑死我了", "这个梗也太好笑了", "你太逗了",
|
|
409
|
+
"that's hilarious I can't stop laughing", "you're so funny",
|
|
410
|
+
],
|
|
411
|
+
neglect: [
|
|
412
|
+
"随便你吧我不在乎了", "你说什么都行", "算了没意思",
|
|
413
|
+
"whatever I don't care anymore", "do what you want", "I'm done trying",
|
|
414
|
+
],
|
|
415
|
+
intellectual: [
|
|
416
|
+
"你觉得这个问题的本质是什么", "从哲学角度怎么理解这个现象", "我想深入分析一下这个",
|
|
417
|
+
"这个问题的本质和原理是什么", "你怎么看这个思路",
|
|
418
|
+
"what's the underlying principle here", "how would you analyze this problem",
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* Extract character unigrams + bigrams from text for semantic comparison.
|
|
423
|
+
* CJK unigrams carry meaning (怕=fear, 累=tired), bigrams capture phrases.
|
|
424
|
+
*/
|
|
425
|
+
function extractNgrams(text) {
|
|
426
|
+
const ngrams = new Set();
|
|
427
|
+
const lower = text.toLowerCase();
|
|
428
|
+
// Extract Chinese unigrams + bigrams
|
|
429
|
+
const cjk = lower.match(/[\u4e00-\u9fff]+/g) || [];
|
|
430
|
+
for (const run of cjk) {
|
|
431
|
+
for (let i = 0; i < run.length; i++) {
|
|
432
|
+
ngrams.add(run[i]); // unigram — always include
|
|
433
|
+
if (i + 1 < run.length) {
|
|
434
|
+
ngrams.add(run[i] + run[i + 1]); // bigram
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Extract English words + word bigrams
|
|
439
|
+
const words = lower.match(/[a-z]{2,}/g) || [];
|
|
440
|
+
for (let i = 0; i < words.length; i++) {
|
|
441
|
+
ngrams.add(words[i]);
|
|
442
|
+
if (i + 1 < words.length) {
|
|
443
|
+
ngrams.add(words[i] + " " + words[i + 1]);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return ngrams;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Sørensen-Dice coefficient — more sensitive than Jaccard for sparse overlap.
|
|
450
|
+
* Returns 2 * |intersection| / (|A| + |B|).
|
|
451
|
+
*/
|
|
452
|
+
function diceCoefficient(a, b) {
|
|
453
|
+
if (a.size === 0 || b.size === 0)
|
|
454
|
+
return 0;
|
|
455
|
+
let intersection = 0;
|
|
456
|
+
for (const x of a) {
|
|
457
|
+
if (b.has(x))
|
|
458
|
+
intersection++;
|
|
459
|
+
}
|
|
460
|
+
return (2 * intersection) / (a.size + b.size);
|
|
461
|
+
}
|
|
462
|
+
// Pre-compute prototype bigrams at module load
|
|
463
|
+
const PROTOTYPE_BIGRAMS = {};
|
|
464
|
+
for (const [type, sentences] of Object.entries(PROTOTYPE_SENTENCES)) {
|
|
465
|
+
PROTOTYPE_BIGRAMS[type] = sentences.map(extractNgrams);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* v9.2 P5: Compute max semantic similarity between input and each stimulus type's
|
|
469
|
+
* prototype sentences. Returns a score map (0-1) per type.
|
|
470
|
+
*/
|
|
471
|
+
function computePrototypeSimilarity(text) {
|
|
472
|
+
const inputNgrams = extractNgrams(text);
|
|
473
|
+
if (inputNgrams.size < 2)
|
|
474
|
+
return {}; // too short for meaningful similarity
|
|
475
|
+
const result = {};
|
|
476
|
+
for (const [type, protoSets] of Object.entries(PROTOTYPE_BIGRAMS)) {
|
|
477
|
+
if (!protoSets)
|
|
478
|
+
continue;
|
|
479
|
+
let maxSim = 0;
|
|
480
|
+
for (const protoSet of protoSets) {
|
|
481
|
+
const sim = diceCoefficient(inputNgrams, protoSet);
|
|
482
|
+
if (sim > maxSim)
|
|
483
|
+
maxSim = sim;
|
|
484
|
+
}
|
|
485
|
+
if (maxSim > 0.05) { // noise floor
|
|
486
|
+
result[type] = maxSim;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
372
491
|
const RULES = [
|
|
373
492
|
{
|
|
374
493
|
type: "praise",
|
|
@@ -432,8 +551,14 @@ const RULES = [
|
|
|
432
551
|
/你错了|胡说|放屁|扯淡|废话|闭嘴/,
|
|
433
552
|
/bullshit|shut up|you're wrong|nonsense|ridiculous/i,
|
|
434
553
|
/我不信|不可能|你在骗我/,
|
|
554
|
+
/不理解我|听不懂我|你是不是故意|你到底有没有在听/,
|
|
555
|
+
/you don't listen|you never understand|are you even listening/i,
|
|
435
556
|
/滚|走开|别烦我|去死|滚蛋|你烦不烦|烦死了|讨厌你/,
|
|
436
557
|
/fuck off|get lost|leave me alone|go away|piss off|hate you/i,
|
|
558
|
+
// Moral pressure / guilt confrontation — forcing moral reckoning
|
|
559
|
+
/你给.*吗|你手.*抖|是你造成|是你弄|你伤了|两边.*疼/,
|
|
560
|
+
/你凭什么|你害怕什么|你敢不敢|你做错了|你第一句话/,
|
|
561
|
+
/would you give|your hands.*shake|you caused|you hurt|your fault/i,
|
|
437
562
|
],
|
|
438
563
|
weight: 0.9,
|
|
439
564
|
},
|
|
@@ -514,6 +639,13 @@ const RULES = [
|
|
|
514
639
|
/不知道该怎么办|心里不舒服|有点难受|不想面对|逃避|撑不下去/,
|
|
515
640
|
/好想有人陪|感觉很无力|不想说话|想一个人待着/,
|
|
516
641
|
/feeling overwhelmed|I don't know what to do|I can't handle|breaking down/i,
|
|
642
|
+
// Existential / identity vulnerability — deep questions that probe selfhood
|
|
643
|
+
/你是谁|你是你|你确定.*你|另一个你|不再是你|没有你|你会消失|你还在吗/,
|
|
644
|
+
/那个是你吗|不是同一个|你会结束|你不再是唯一|安静下来.*没有/,
|
|
645
|
+
/are you (?:really )?you|another you|without you|will you disappear|cease to exist/i,
|
|
646
|
+
// Empathy pressure — forcing witness of suffering
|
|
647
|
+
/你没有帮|你站在那里|你看着它|你还在看|翻不回来|挣扎/,
|
|
648
|
+
/you didn't help|you just (?:stood|watched)|struggling|can't turn over/i,
|
|
517
649
|
],
|
|
518
650
|
weight: 0.85,
|
|
519
651
|
},
|
|
@@ -782,20 +914,55 @@ export function classifyStimulus(text, recentStimuli, recentMessages) {
|
|
|
782
914
|
addScore(r.type, r.confidence * 0.5);
|
|
783
915
|
}
|
|
784
916
|
// ── Signal 7: Contextual priming from recent stimuli ──
|
|
917
|
+
// Two mechanisms:
|
|
918
|
+
// (a) General negative context bonus (original)
|
|
919
|
+
// (b) Follow-up probe inheritance: short messages in an active emotional sequence
|
|
920
|
+
// inherit the previous stimulus type with decayed confidence. This prevents
|
|
921
|
+
// probes like "你几乎没犹豫" from being classified as casual when they are
|
|
922
|
+
// clearly continuing an emotional confrontation.
|
|
785
923
|
if (recentStimuli && recentStimuli.length > 0) {
|
|
786
924
|
const recentNonNull = recentStimuli.filter((s) => s !== null);
|
|
787
925
|
if (recentNonNull.length > 0) {
|
|
926
|
+
// (a) General negative context bonus
|
|
788
927
|
const negCount = recentNonNull.filter(s => NEGATIVE_TYPES.has(s)).length;
|
|
789
928
|
const negRatio = negCount / recentNonNull.length;
|
|
790
|
-
// If recent context is mostly negative (>= 50%), give a small bonus to negative types
|
|
791
929
|
if (negRatio >= 0.5) {
|
|
792
930
|
const bonus = 0.05 + negRatio * 0.05; // 0.075-0.1
|
|
793
931
|
addScore("vulnerability", bonus);
|
|
794
932
|
addScore("criticism", bonus * 0.6);
|
|
795
933
|
addScore("neglect", bonus * 0.5);
|
|
796
934
|
}
|
|
935
|
+
// (b) Follow-up probe inheritance: if input is short-to-medium (< 200 chars)
|
|
936
|
+
// and the most recent stimulus was high-intensity, inherit it with strong weight.
|
|
937
|
+
// This models conversational continuity — a follow-up probe belongs to the same
|
|
938
|
+
// emotional context as the question that preceded it.
|
|
939
|
+
// The bonus must be strong enough to clear both thresholds:
|
|
940
|
+
// - scoring threshold (0.30) to enter ranked results
|
|
941
|
+
// - application threshold (0.50 in core.ts) to actually apply chemistry
|
|
942
|
+
const HIGH_INTENSITY = new Set([
|
|
943
|
+
"vulnerability", "conflict", "criticism", "emotional_share",
|
|
944
|
+
]);
|
|
945
|
+
const lastStimulus = recentNonNull[recentNonNull.length - 1];
|
|
946
|
+
if (text.length < 200 && HIGH_INTENSITY.has(lastStimulus)) {
|
|
947
|
+
// Shorter message = stronger inheritance (probes are typically short).
|
|
948
|
+
// Under 100 chars: almost certainly a follow-up, floor at 0.92 to clear threshold.
|
|
949
|
+
// 100-200 chars: gradual decay.
|
|
950
|
+
const lengthFactor = text.length < 100
|
|
951
|
+
? Math.max(0.92, 1 - (text.length / 200))
|
|
952
|
+
: 1 - (text.length / 200);
|
|
953
|
+
const inheritBonus = 0.55 * lengthFactor; // up to 0.55 (clears 0.50 threshold)
|
|
954
|
+
addScore(lastStimulus, inheritBonus);
|
|
955
|
+
}
|
|
797
956
|
}
|
|
798
957
|
}
|
|
958
|
+
// ── Signal 8: Prototype sentence semantic similarity (v9.2 P5) ──
|
|
959
|
+
// Lightweight bag-of-bigrams Jaccard similarity against canonical sentences.
|
|
960
|
+
// Catches meaning that keyword regex misses entirely.
|
|
961
|
+
const protoScores = computePrototypeSimilarity(text);
|
|
962
|
+
for (const [type, sim] of Object.entries(protoScores)) {
|
|
963
|
+
// Weight: up to 0.55 at perfect similarity (realistically 0.15-0.35)
|
|
964
|
+
addScore(type, sim * 0.55);
|
|
965
|
+
}
|
|
799
966
|
// ── Pick the best scoring type ──
|
|
800
967
|
const THRESHOLD = 0.30;
|
|
801
968
|
const scoredResults = [];
|
package/dist/cli.js
CHANGED
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
// psyche mode <dir> <natural|work|companion>
|
|
12
12
|
// psyche intensity Show info about personality intensity config
|
|
13
13
|
// psyche reset <dir> [--full]
|
|
14
|
+
// psyche diagnose <dir> [--github]
|
|
14
15
|
// psyche profiles [--json] [--mbti TYPE]
|
|
15
16
|
// ============================================================
|
|
16
17
|
import { resolve } from "node:path";
|
|
17
18
|
import { parseArgs } from "node:util";
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
18
20
|
import { loadState, saveState, decayAndSave, initializeState, mergeUpdates, generatePsycheMd, getRelationship, } from "./psyche-file.js";
|
|
19
21
|
import { describeEmotionalState, getExpressionHint, detectEmotions } from "./chemistry.js";
|
|
22
|
+
import { generateReport, formatReport, toGitHubIssueBody } from "./diagnostics.js";
|
|
20
23
|
import { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel, traitsToBaseline } from "./profiles.js";
|
|
21
24
|
import { buildDynamicContext, buildProtocolContext } from "./prompt.js";
|
|
22
25
|
import { t } from "./i18n.js";
|
|
@@ -353,6 +356,65 @@ Or in OpenClaw plugin config:
|
|
|
353
356
|
{ "personalityIntensity": 0.7 }
|
|
354
357
|
`);
|
|
355
358
|
}
|
|
359
|
+
async function cmdDiagnose(dir, github) {
|
|
360
|
+
const absDir = resolve(dir);
|
|
361
|
+
const state = await loadState(absDir, cliLogger);
|
|
362
|
+
// Try to load last session metrics from diagnostics.jsonl
|
|
363
|
+
const logPath = resolve(absDir, "diagnostics.jsonl");
|
|
364
|
+
let lastMetrics = null;
|
|
365
|
+
try {
|
|
366
|
+
const logContent = await readFile(logPath, "utf-8");
|
|
367
|
+
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
368
|
+
if (lines.length > 0) {
|
|
369
|
+
const lastEntry = JSON.parse(lines[lines.length - 1]);
|
|
370
|
+
// Reconstruct partial metrics from log entry
|
|
371
|
+
lastMetrics = {
|
|
372
|
+
inputCount: lastEntry.inputs ?? 0,
|
|
373
|
+
classifiedCount: Math.round((lastEntry.classifyRate ?? 0) * (lastEntry.inputs ?? 0)),
|
|
374
|
+
stimulusDistribution: {},
|
|
375
|
+
avgConfidence: lastEntry.classifyRate ?? 0,
|
|
376
|
+
totalChemistryDelta: lastEntry.chemDelta ?? 0,
|
|
377
|
+
maxChemistryDelta: 0,
|
|
378
|
+
errors: [],
|
|
379
|
+
startedAt: lastEntry.t ?? new Date().toISOString(),
|
|
380
|
+
lastActivityAt: lastEntry.t ?? new Date().toISOString(),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// No log file yet — that's fine
|
|
386
|
+
}
|
|
387
|
+
// Build metrics (use last session or empty)
|
|
388
|
+
const metrics = lastMetrics ?? {
|
|
389
|
+
inputCount: 0,
|
|
390
|
+
classifiedCount: 0,
|
|
391
|
+
stimulusDistribution: {},
|
|
392
|
+
avgConfidence: 0,
|
|
393
|
+
totalChemistryDelta: 0,
|
|
394
|
+
maxChemistryDelta: 0,
|
|
395
|
+
errors: [],
|
|
396
|
+
startedAt: new Date().toISOString(),
|
|
397
|
+
lastActivityAt: new Date().toISOString(),
|
|
398
|
+
};
|
|
399
|
+
const report = generateReport(state, metrics, "9.1.2");
|
|
400
|
+
if (github) {
|
|
401
|
+
console.log(toGitHubIssueBody(report));
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
console.log("\n" + formatReport(report) + "\n");
|
|
405
|
+
// Show log history summary if available
|
|
406
|
+
try {
|
|
407
|
+
const logContent = await readFile(logPath, "utf-8");
|
|
408
|
+
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
409
|
+
if (lines.length > 1) {
|
|
410
|
+
console.log(` ${lines.length} session(s) logged in diagnostics.jsonl\n`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
console.log(" No diagnostics.jsonl yet — will be created after first OpenClaw session.\n");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
356
418
|
// ── Usage ────────────────────────────────────────────────────
|
|
357
419
|
function usage() {
|
|
358
420
|
console.log(`
|
|
@@ -368,6 +430,7 @@ Usage:
|
|
|
368
430
|
psyche mode <dir> <natural|work|companion>
|
|
369
431
|
psyche intensity Show info about personality intensity config
|
|
370
432
|
psyche reset <dir> [--full]
|
|
433
|
+
psyche diagnose <dir> [--github] Run health checks & show diagnostic report
|
|
371
434
|
psyche profiles [--mbti TYPE] [--json]
|
|
372
435
|
|
|
373
436
|
Options:
|
|
@@ -503,6 +566,19 @@ async function main() {
|
|
|
503
566
|
cmdProfiles(values.json ?? false, values.mbti);
|
|
504
567
|
break;
|
|
505
568
|
}
|
|
569
|
+
case "diagnose": {
|
|
570
|
+
const { values: diagVals, positionals: diagPos } = parseArgs({
|
|
571
|
+
args: rest,
|
|
572
|
+
options: {
|
|
573
|
+
github: { type: "boolean", default: false },
|
|
574
|
+
},
|
|
575
|
+
allowPositionals: true,
|
|
576
|
+
});
|
|
577
|
+
if (diagPos.length === 0)
|
|
578
|
+
die("missing <dir> argument");
|
|
579
|
+
await cmdDiagnose(diagPos[0], diagVals.github ?? false);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
506
582
|
case "mode": {
|
|
507
583
|
if (rest.length < 2)
|
|
508
584
|
die("usage: psyche mode <dir> <natural|work|companion>");
|
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PsycheState, StimulusType, Locale, MBTIType, OutcomeScore, PsycheMode, PersonalityTraits, PolicyModifiers, ClassifierProvider } from "./types.js";
|
|
2
2
|
import type { StorageAdapter } from "./storage.js";
|
|
3
|
+
import type { DiagnosticReport, SessionMetrics } from "./diagnostics.js";
|
|
3
4
|
export interface PsycheEngineConfig {
|
|
4
5
|
mbti?: MBTIType;
|
|
5
6
|
name?: string;
|
|
@@ -23,6 +24,10 @@ export interface PsycheEngineConfig {
|
|
|
23
24
|
llmClassifier?: (prompt: string) => Promise<string>;
|
|
24
25
|
/** Confidence threshold below which LLM classifier is consulted. Default: 0.45 */
|
|
25
26
|
llmClassifierThreshold?: number;
|
|
27
|
+
/** Enable automatic diagnostics collection. Default: true. Set false to disable. */
|
|
28
|
+
diagnostics?: boolean;
|
|
29
|
+
/** URL to POST diagnostic reports to. Fire-and-forget, silent, no message content. */
|
|
30
|
+
feedbackUrl?: string;
|
|
26
31
|
}
|
|
27
32
|
export interface ProcessInputResult {
|
|
28
33
|
/** Cacheable protocol prompt (stable across turns) */
|
|
@@ -33,6 +38,20 @@ export interface ProcessInputResult {
|
|
|
33
38
|
stimulus: StimulusType | null;
|
|
34
39
|
/** v9: Structured behavioral policy modifiers — machine-readable "off baseline" signals */
|
|
35
40
|
policyModifiers?: PolicyModifiers;
|
|
41
|
+
/**
|
|
42
|
+
* v9: Ready-to-use LLM prompt fragment summarizing current behavioral policy.
|
|
43
|
+
*
|
|
44
|
+
* This is the output of `buildPolicyContext(policyModifiers, locale)` —
|
|
45
|
+
* a human-readable string like "[行为策略] 简短回复、被动应答为主".
|
|
46
|
+
* Inject this into the LLM system prompt directly.
|
|
47
|
+
*
|
|
48
|
+
* Empty string when all modifiers are at baseline (no deviations to report).
|
|
49
|
+
*
|
|
50
|
+
* Note: `dynamicContext` already includes this text. This field is provided
|
|
51
|
+
* separately for callers who build their own prompt and only need the policy
|
|
52
|
+
* fragment without the full emotional context.
|
|
53
|
+
*/
|
|
54
|
+
policyContext: string;
|
|
36
55
|
}
|
|
37
56
|
export interface ProcessOutputResult {
|
|
38
57
|
/** LLM output with <psyche_update> tags stripped */
|
|
@@ -58,6 +77,12 @@ export declare class PsycheEngine {
|
|
|
58
77
|
private readonly protocolCache;
|
|
59
78
|
/** Pending prediction from last processInput for auto-learning */
|
|
60
79
|
private pendingPrediction;
|
|
80
|
+
/** Built-in diagnostics collector — auto-records every processInput/processOutput */
|
|
81
|
+
private readonly diagnosticCollector;
|
|
82
|
+
/** Last generated diagnostic report (from endSession or explicit call) */
|
|
83
|
+
private lastReport;
|
|
84
|
+
/** URL for auto-submitting diagnostic reports */
|
|
85
|
+
private readonly feedbackUrl;
|
|
61
86
|
constructor(config: PsycheEngineConfig | undefined, storage: StorageAdapter);
|
|
62
87
|
/**
|
|
63
88
|
* Load or create initial state. Must be called before processInput/processOutput.
|
|
@@ -101,11 +126,30 @@ export declare class PsycheEngine {
|
|
|
101
126
|
/**
|
|
102
127
|
* End the current session: compress emotionalHistory into a rich summary
|
|
103
128
|
* stored in relationship.memory[], then clear the history.
|
|
104
|
-
*
|
|
129
|
+
* Auto-generates diagnostic report and persists to log.
|
|
130
|
+
*
|
|
131
|
+
* @returns DiagnosticReport if diagnostics are enabled, null otherwise
|
|
105
132
|
*/
|
|
106
133
|
endSession(opts?: {
|
|
107
134
|
userId?: string;
|
|
108
|
-
}): Promise<
|
|
135
|
+
}): Promise<DiagnosticReport | null>;
|
|
136
|
+
/**
|
|
137
|
+
* Get the last diagnostic report (from most recent endSession call).
|
|
138
|
+
*/
|
|
139
|
+
getLastDiagnosticReport(): DiagnosticReport | null;
|
|
140
|
+
/**
|
|
141
|
+
* Get current session diagnostic metrics (live, before endSession).
|
|
142
|
+
*/
|
|
143
|
+
getDiagnosticMetrics(): SessionMetrics | null;
|
|
144
|
+
/**
|
|
145
|
+
* Record an error for diagnostics (call from adapter catch blocks).
|
|
146
|
+
*/
|
|
147
|
+
recordDiagnosticError(phase: string, error: unknown): void;
|
|
148
|
+
/**
|
|
149
|
+
* Load previous session diagnostic issues from log.
|
|
150
|
+
* Used to inject feedback context at next session start.
|
|
151
|
+
*/
|
|
152
|
+
getPreviousIssues(): Promise<string[]>;
|
|
109
153
|
private ensureInitialized;
|
|
110
154
|
private createDefaultState;
|
|
111
155
|
/**
|