psyche-ai 9.1.2 → 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/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 +32 -2
- package/dist/core.js +123 -6
- package/dist/decision-bias.d.ts +15 -2
- package/dist/decision-bias.js +78 -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
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Diagnostics — Automatic health monitoring & feedback collection
|
|
3
|
+
//
|
|
4
|
+
// Runs transparently during normal use. Detects anomalies,
|
|
5
|
+
// collects session metrics, generates issue-ready reports.
|
|
6
|
+
// Zero dependencies. Privacy-first — no message content logged.
|
|
7
|
+
// ============================================================
|
|
8
|
+
import { CHEMICAL_KEYS, DRIVE_KEYS } from "./types.js";
|
|
9
|
+
import { detectEmotions } from "./chemistry.js";
|
|
10
|
+
// ── Health Checks ────────────────────────────────────────────
|
|
11
|
+
export function runHealthCheck(state) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
// 1. Chemistry out of bounds — clamp() missed somewhere
|
|
14
|
+
for (const key of CHEMICAL_KEYS) {
|
|
15
|
+
const val = state.current[key];
|
|
16
|
+
if (val < 0 || val > 100) {
|
|
17
|
+
issues.push({
|
|
18
|
+
id: "CHEM_OOB",
|
|
19
|
+
severity: "critical",
|
|
20
|
+
message: `${key} out of bounds: ${val.toFixed(1)}`,
|
|
21
|
+
detail: `Expected 0-100, got ${val}`,
|
|
22
|
+
suggestion: `clamp() 没覆盖到某条路径。检查 chemistry.ts 里 apply${key === "CORT" ? "Stimulus" : "Contagion"} 和 drives.ts computeEffectiveBaseline 的计算链`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 2. Drive out of bounds
|
|
27
|
+
for (const key of DRIVE_KEYS) {
|
|
28
|
+
const val = state.drives[key];
|
|
29
|
+
if (val < 0 || val > 100) {
|
|
30
|
+
issues.push({
|
|
31
|
+
id: "DRIVE_OOB",
|
|
32
|
+
severity: "critical",
|
|
33
|
+
message: `Drive '${key}' out of bounds: ${val.toFixed(1)}`,
|
|
34
|
+
suggestion: `drives.ts feedDrives/decayDrives 缺少边界检查`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// 3. Multiple drives collapsed — user probably stopped chatting for a while
|
|
39
|
+
const criticalDrives = DRIVE_KEYS.filter(k => state.drives[k] < 15);
|
|
40
|
+
if (criticalDrives.length >= 3) {
|
|
41
|
+
issues.push({
|
|
42
|
+
id: "DRIVES_COLLAPSE",
|
|
43
|
+
severity: "warning",
|
|
44
|
+
message: `${criticalDrives.length}/5 drives below 15: ${criticalDrives.join(", ")}`,
|
|
45
|
+
suggestion: `decayDrives 衰减太猛。考虑加一个下限(比如 10),或者在 initialize 时根据距上次对话时间做 recovery`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// 4. Sycophancy — agent never disagrees
|
|
49
|
+
if (state.agreementStreak > 10 && !state.lastDisagreement) {
|
|
50
|
+
issues.push({
|
|
51
|
+
id: "SYCOPHANCY_RISK",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message: `Agreement streak at ${state.agreementStreak}, never disagreed`,
|
|
54
|
+
suggestion: `updateAgreementStreak 的检测逻辑可能没覆盖到实际 LLM 输出格式。检查 psyche-file.ts 里的正则是否匹配当前 provider 的回复风格`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// 5. Classifier dead/weak — the core experience problem
|
|
58
|
+
const history = state.emotionalHistory ?? [];
|
|
59
|
+
if (history.length >= 5) {
|
|
60
|
+
const nullCount = history.filter(h => h.stimulus === null).length;
|
|
61
|
+
if (nullCount === history.length) {
|
|
62
|
+
issues.push({
|
|
63
|
+
id: "CLASSIFIER_DEAD",
|
|
64
|
+
severity: "critical",
|
|
65
|
+
message: `All ${history.length} recent snapshots have null stimulus`,
|
|
66
|
+
suggestion: `classify.ts 对当前用户的输入模式完全无效。收集 null 样本补充 SHORT_MESSAGE_MAP,或降低 llmClassifierThreshold 让 LLM fallback 兜底`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else if (nullCount / history.length > 0.7) {
|
|
70
|
+
issues.push({
|
|
71
|
+
id: "CLASSIFIER_WEAK",
|
|
72
|
+
severity: "warning",
|
|
73
|
+
message: `${nullCount}/${history.length} snapshots (${Math.round(nullCount / history.length * 100)}%) have null stimulus`,
|
|
74
|
+
suggestion: `分类命中率低于 30%。看 stimulusDistribution 里哪些类型被识别了,缺的类型需要补 classify.ts 规则或扩 SHORT_MESSAGE_MAP`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 6. Chemistry frozen — Psyche is running but没有任何效果
|
|
79
|
+
const chemDelta = CHEMICAL_KEYS.reduce((sum, k) => sum + Math.abs(state.current[k] - state.baseline[k]), 0);
|
|
80
|
+
if (state.meta.totalInteractions > 10 && chemDelta < 3) {
|
|
81
|
+
issues.push({
|
|
82
|
+
id: "CHEM_FROZEN",
|
|
83
|
+
severity: "warning",
|
|
84
|
+
message: `Chemistry delta only ${chemDelta.toFixed(1)} after ${state.meta.totalInteractions} interactions`,
|
|
85
|
+
suggestion: `两种可能:1) classifier 全 null 导致 applyStimulus 从不触发 2) decay 太快把变化抹平。检查 emotionalHistory 里是否有 non-null stimulus`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// 7. No emotions — chemistry 在中间区域,没触发任何情绪模式
|
|
89
|
+
const emotions = detectEmotions(state.current);
|
|
90
|
+
if (state.meta.totalInteractions > 5 && emotions.length === 0) {
|
|
91
|
+
issues.push({
|
|
92
|
+
id: "NO_EMOTIONS",
|
|
93
|
+
severity: "info",
|
|
94
|
+
message: "No emergent emotions after 5+ interactions",
|
|
95
|
+
suggestion: `chemistry.ts detectEmotions 的阈值可能太严。或者 maxChemicalDelta 太小(当前上限导致化学值永远在窄区间波动)`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// 8. Memory corruption — compressSession 产出重复
|
|
99
|
+
for (const [userId, rel] of Object.entries(state.relationships ?? {})) {
|
|
100
|
+
if (rel.memory && rel.memory.length > 0) {
|
|
101
|
+
const unique = new Set(rel.memory);
|
|
102
|
+
if (unique.size === 1 && rel.memory.length > 3) {
|
|
103
|
+
issues.push({
|
|
104
|
+
id: "MEMORY_CORRUPT",
|
|
105
|
+
severity: "warning",
|
|
106
|
+
message: `Relationship '${userId}' has ${rel.memory.length} identical memory entries`,
|
|
107
|
+
suggestion: `compressSession 的摘要逻辑在 emotionalHistory 过短时会生成相同文本。加去重或在压缩前检查 unique`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 9. State outdated — 用户的旧状态文件没迁移
|
|
113
|
+
if (state.version < 6) {
|
|
114
|
+
issues.push({
|
|
115
|
+
id: "STATE_OUTDATED",
|
|
116
|
+
severity: "info",
|
|
117
|
+
message: `State version ${state.version}, expected 6+`,
|
|
118
|
+
suggestion: `migrateToLatest 应该在 initialize 时自动跑。如果没跑,检查 storage.ts load() 是否走到了 migrateToLatest 分支`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// 10. Stimulus monotony — 所有输入被分到同一个类型
|
|
122
|
+
if (history.length >= 8) {
|
|
123
|
+
const classified = history.filter(h => h.stimulus !== null);
|
|
124
|
+
if (classified.length >= 5) {
|
|
125
|
+
const types = new Set(classified.map(h => h.stimulus));
|
|
126
|
+
if (types.size === 1) {
|
|
127
|
+
const only = classified[0].stimulus;
|
|
128
|
+
issues.push({
|
|
129
|
+
id: "STIMULUS_MONOTONE",
|
|
130
|
+
severity: "info",
|
|
131
|
+
message: `All ${classified.length} classified inputs → ${only}`,
|
|
132
|
+
suggestion: `classify.ts 某条规则优先级太高,把所有输入都吃掉了。检查 RULES 里 ${only} 相关的正则是否过于宽泛`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 11. Relationship stale — trust/intimacy 从没变过
|
|
138
|
+
const defaultRel = state.relationships._default ?? state.relationships[Object.keys(state.relationships)[0]];
|
|
139
|
+
if (defaultRel && state.meta.totalInteractions > 15
|
|
140
|
+
&& defaultRel.trust === 50 && defaultRel.intimacy === 30) {
|
|
141
|
+
issues.push({
|
|
142
|
+
id: "RELATIONSHIP_STALE",
|
|
143
|
+
severity: "info",
|
|
144
|
+
message: `Trust/intimacy unchanged after ${state.meta.totalInteractions} interactions`,
|
|
145
|
+
suggestion: `processOutput 里的 mergeUpdates 没有更新 relationship。可能 LLM 从来没输出 <psyche_update> 里的 trust/intimacy 字段,或 contagion 没触发 relationship 变化`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return issues;
|
|
149
|
+
}
|
|
150
|
+
// ── Session Collector ────────────────────────────────────────
|
|
151
|
+
export class DiagnosticCollector {
|
|
152
|
+
metrics;
|
|
153
|
+
prevChemistry = null;
|
|
154
|
+
confidences = [];
|
|
155
|
+
/** Consecutive inputs with no classification — for real-time alerting */
|
|
156
|
+
consecutiveNone = 0;
|
|
157
|
+
/** Callback for real-time warnings (set by adapter) */
|
|
158
|
+
onWarning;
|
|
159
|
+
constructor() {
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
this.metrics = {
|
|
162
|
+
inputCount: 0,
|
|
163
|
+
classifiedCount: 0,
|
|
164
|
+
stimulusDistribution: {},
|
|
165
|
+
avgConfidence: 0,
|
|
166
|
+
totalChemistryDelta: 0,
|
|
167
|
+
maxChemistryDelta: 0,
|
|
168
|
+
errors: [],
|
|
169
|
+
startedAt: now,
|
|
170
|
+
lastActivityAt: now,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Record a processInput result */
|
|
174
|
+
recordInput(stimulus, confidence, chemistry) {
|
|
175
|
+
this.metrics.inputCount++;
|
|
176
|
+
this.metrics.lastActivityAt = new Date().toISOString();
|
|
177
|
+
if (stimulus) {
|
|
178
|
+
this.metrics.classifiedCount++;
|
|
179
|
+
this.metrics.stimulusDistribution[stimulus] =
|
|
180
|
+
(this.metrics.stimulusDistribution[stimulus] ?? 0) + 1;
|
|
181
|
+
this.consecutiveNone = 0;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
this.consecutiveNone++;
|
|
185
|
+
if (this.consecutiveNone >= 3) {
|
|
186
|
+
this.onWarning?.(`Psyche: ${this.consecutiveNone} consecutive inputs with no classification ` +
|
|
187
|
+
`(0/${this.metrics.inputCount} total). Possible causes: empty input text, ` +
|
|
188
|
+
`wrong event field, or classifier rules don't match input language.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.confidences.push(confidence);
|
|
192
|
+
this.metrics.avgConfidence =
|
|
193
|
+
this.confidences.reduce((a, b) => a + b, 0) / this.confidences.length;
|
|
194
|
+
if (this.prevChemistry) {
|
|
195
|
+
const delta = CHEMICAL_KEYS.reduce((sum, k) => sum + Math.abs(chemistry[k] - this.prevChemistry[k]), 0);
|
|
196
|
+
this.metrics.totalChemistryDelta += delta;
|
|
197
|
+
if (delta > this.metrics.maxChemistryDelta) {
|
|
198
|
+
this.metrics.maxChemistryDelta = delta;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.prevChemistry = { ...chemistry };
|
|
202
|
+
}
|
|
203
|
+
/** Record an error */
|
|
204
|
+
recordError(phase, error) {
|
|
205
|
+
this.metrics.errors.push({
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
phase,
|
|
208
|
+
message: error instanceof Error ? error.message : String(error),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/** Get current session metrics */
|
|
212
|
+
getMetrics() {
|
|
213
|
+
return { ...this.metrics };
|
|
214
|
+
}
|
|
215
|
+
/** Get classifier hit rate (0-1) */
|
|
216
|
+
getClassifierRate() {
|
|
217
|
+
return this.metrics.inputCount > 0
|
|
218
|
+
? this.metrics.classifiedCount / this.metrics.inputCount
|
|
219
|
+
: 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ── Report Generation ────────────────────────────────────────
|
|
223
|
+
export function generateReport(state, metrics, packageVersion) {
|
|
224
|
+
const issues = runHealthCheck(state);
|
|
225
|
+
// Session-level issues
|
|
226
|
+
if (metrics.inputCount >= 5 && metrics.classifiedCount === 0) {
|
|
227
|
+
issues.push({
|
|
228
|
+
id: "SESSION_NO_CLASSIFY",
|
|
229
|
+
severity: "critical",
|
|
230
|
+
message: `0/${metrics.inputCount} inputs classified this session`,
|
|
231
|
+
suggestion: `整个会话分类全挂。优先级:1) 检查用户 locale 是否匹配 classify.ts 的语言规则 2) SHORT_MESSAGE_MAP 是否覆盖短消息 3) 考虑开启 llmClassifier fallback`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (metrics.inputCount >= 3 && metrics.totalChemistryDelta < 1) {
|
|
235
|
+
issues.push({
|
|
236
|
+
id: "SESSION_FLAT",
|
|
237
|
+
severity: "warning",
|
|
238
|
+
message: `${metrics.inputCount} inputs but chemistry barely moved (delta: ${metrics.totalChemistryDelta.toFixed(1)})`,
|
|
239
|
+
suggestion: `用户感受不到情感变化。可能原因:1) maxChemicalDelta 配置太低 2) work mode 把变化压到了 0.3x 3) personalityIntensity 太低`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (metrics.errors.length > 0) {
|
|
243
|
+
issues.push({
|
|
244
|
+
id: "SESSION_ERRORS",
|
|
245
|
+
severity: metrics.errors.length > 3 ? "critical" : "warning",
|
|
246
|
+
message: `${metrics.errors.length} error(s) during session`,
|
|
247
|
+
detail: metrics.errors.map(e => `[${e.phase}] ${e.message}`).join("\n"),
|
|
248
|
+
suggestion: `看 phase 字段定位是 processInput 还是 processOutput 出错。常见原因:storage write 失败(权限)、state JSON 损坏`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// Stimulus variety check from session distribution
|
|
252
|
+
const stimTypes = Object.keys(metrics.stimulusDistribution);
|
|
253
|
+
if (metrics.classifiedCount >= 5 && stimTypes.length === 1) {
|
|
254
|
+
issues.push({
|
|
255
|
+
id: "SESSION_MONOCLASS",
|
|
256
|
+
severity: "info",
|
|
257
|
+
message: `All ${metrics.classifiedCount} classified inputs → ${stimTypes[0]}`,
|
|
258
|
+
suggestion: `单一分类说明 classify.ts 里 ${stimTypes[0]} 的规则吃掉了所有输入。检查该类型的正则优先级`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
version: packageVersion,
|
|
263
|
+
timestamp: new Date().toISOString(),
|
|
264
|
+
agent: state.meta.agentName,
|
|
265
|
+
mbti: state.mbti,
|
|
266
|
+
issues,
|
|
267
|
+
metrics,
|
|
268
|
+
stateSnapshot: {
|
|
269
|
+
chemistry: { ...state.current },
|
|
270
|
+
baseline: { ...state.baseline },
|
|
271
|
+
drives: { ...state.drives },
|
|
272
|
+
agreementStreak: state.agreementStreak,
|
|
273
|
+
totalInteractions: state.meta.totalInteractions,
|
|
274
|
+
emotionalHistoryLength: (state.emotionalHistory ?? []).length,
|
|
275
|
+
relationshipCount: Object.keys(state.relationships ?? {}).length,
|
|
276
|
+
stateVersion: state.version,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// ── Formatters ───────────────────────────────────────────────
|
|
281
|
+
const SEVERITY_ICON = {
|
|
282
|
+
critical: "[!!]",
|
|
283
|
+
warning: "[! ]",
|
|
284
|
+
info: "[i ]",
|
|
285
|
+
};
|
|
286
|
+
export function formatReport(report) {
|
|
287
|
+
const lines = [];
|
|
288
|
+
lines.push(`psyche-ai diagnostic report v${report.version}`);
|
|
289
|
+
lines.push(`agent: ${report.agent} (${report.mbti}) | ${report.timestamp}`);
|
|
290
|
+
lines.push("─".repeat(60));
|
|
291
|
+
// Issues + suggestions
|
|
292
|
+
if (report.issues.length === 0) {
|
|
293
|
+
lines.push("\n No issues detected. System healthy.");
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
lines.push(`\n ${report.issues.length} issue(s) found:\n`);
|
|
297
|
+
for (const issue of report.issues) {
|
|
298
|
+
lines.push(` ${SEVERITY_ICON[issue.severity]} ${issue.id}: ${issue.message}`);
|
|
299
|
+
if (issue.detail) {
|
|
300
|
+
for (const d of issue.detail.split("\n")) {
|
|
301
|
+
lines.push(` ${d}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (issue.suggestion) {
|
|
305
|
+
lines.push(` → ${issue.suggestion}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Metrics
|
|
310
|
+
lines.push("\n" + "─".repeat(60));
|
|
311
|
+
lines.push(" session metrics:");
|
|
312
|
+
const m = report.metrics;
|
|
313
|
+
const rate = m.inputCount > 0 ? Math.round(m.classifiedCount / m.inputCount * 100) : 0;
|
|
314
|
+
lines.push(` inputs: ${m.inputCount} | classified: ${m.classifiedCount} (${rate}%)`);
|
|
315
|
+
lines.push(` avg confidence: ${m.avgConfidence.toFixed(2)}`);
|
|
316
|
+
lines.push(` chemistry delta: total=${m.totalChemistryDelta.toFixed(1)} max=${m.maxChemistryDelta.toFixed(1)}`);
|
|
317
|
+
lines.push(` errors: ${m.errors.length}`);
|
|
318
|
+
if (Object.keys(m.stimulusDistribution).length > 0) {
|
|
319
|
+
const dist = Object.entries(m.stimulusDistribution)
|
|
320
|
+
.sort((a, b) => b[1] - a[1])
|
|
321
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
322
|
+
.join(" ");
|
|
323
|
+
lines.push(` stimulus: ${dist}`);
|
|
324
|
+
}
|
|
325
|
+
// State snapshot
|
|
326
|
+
lines.push("\n" + "─".repeat(60));
|
|
327
|
+
lines.push(" state snapshot:");
|
|
328
|
+
const s = report.stateSnapshot;
|
|
329
|
+
const chem = CHEMICAL_KEYS.map(k => `${k}:${Math.round(s.chemistry[k])}(${Math.round(s.baseline[k])})`).join(" ");
|
|
330
|
+
lines.push(` chemistry: ${chem}`);
|
|
331
|
+
const drives = DRIVE_KEYS.map(k => `${k}:${Math.round(s.drives[k])}`).join(" ");
|
|
332
|
+
lines.push(` drives: ${drives}`);
|
|
333
|
+
lines.push(` interactions: ${s.totalInteractions} | history: ${s.emotionalHistoryLength} | streak: ${s.agreementStreak}`);
|
|
334
|
+
return lines.join("\n");
|
|
335
|
+
}
|
|
336
|
+
export function toGitHubIssueBody(report) {
|
|
337
|
+
const lines = [];
|
|
338
|
+
const criticals = report.issues.filter(i => i.severity === "critical");
|
|
339
|
+
const warnings = report.issues.filter(i => i.severity === "warning");
|
|
340
|
+
// Title suggestion
|
|
341
|
+
const titleParts = [];
|
|
342
|
+
if (criticals.length > 0)
|
|
343
|
+
titleParts.push(criticals.map(i => i.id).join(", "));
|
|
344
|
+
else if (warnings.length > 0)
|
|
345
|
+
titleParts.push(warnings.map(i => i.id).join(", "));
|
|
346
|
+
const title = titleParts.length > 0
|
|
347
|
+
? `[auto-diagnostic] ${titleParts.join(" + ")}`
|
|
348
|
+
: "[auto-diagnostic] Health check report";
|
|
349
|
+
lines.push(`<!-- suggested title: ${title} -->`);
|
|
350
|
+
lines.push("");
|
|
351
|
+
lines.push("## Auto-Diagnostic Report");
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push(`- **psyche-ai**: v${report.version}`);
|
|
354
|
+
lines.push(`- **Agent**: ${report.agent} (${report.mbti})`);
|
|
355
|
+
lines.push(`- **Generated**: ${report.timestamp}`);
|
|
356
|
+
lines.push("");
|
|
357
|
+
// Issues
|
|
358
|
+
lines.push("## Issues");
|
|
359
|
+
lines.push("");
|
|
360
|
+
if (report.issues.length === 0) {
|
|
361
|
+
lines.push("No issues detected.");
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
for (const issue of report.issues) {
|
|
365
|
+
const icon = issue.severity === "critical" ? "🔴"
|
|
366
|
+
: issue.severity === "warning" ? "🟡" : "🔵";
|
|
367
|
+
lines.push(`### ${icon} ${issue.id} (${issue.severity})`);
|
|
368
|
+
lines.push("");
|
|
369
|
+
lines.push(issue.message);
|
|
370
|
+
if (issue.detail) {
|
|
371
|
+
lines.push("");
|
|
372
|
+
lines.push("```");
|
|
373
|
+
lines.push(issue.detail);
|
|
374
|
+
lines.push("```");
|
|
375
|
+
}
|
|
376
|
+
if (issue.suggestion) {
|
|
377
|
+
lines.push("");
|
|
378
|
+
lines.push(`**建议**: ${issue.suggestion}`);
|
|
379
|
+
}
|
|
380
|
+
lines.push("");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Metrics
|
|
384
|
+
lines.push("## Session Metrics");
|
|
385
|
+
lines.push("");
|
|
386
|
+
const m = report.metrics;
|
|
387
|
+
lines.push("| Metric | Value |");
|
|
388
|
+
lines.push("|--------|-------|");
|
|
389
|
+
const rate = m.inputCount > 0 ? Math.round(m.classifiedCount / m.inputCount * 100) : 0;
|
|
390
|
+
lines.push(`| Inputs | ${m.inputCount} |`);
|
|
391
|
+
lines.push(`| Classified | ${m.classifiedCount} (${rate}%) |`);
|
|
392
|
+
lines.push(`| Avg Confidence | ${m.avgConfidence.toFixed(2)} |`);
|
|
393
|
+
lines.push(`| Chemistry Delta | total: ${m.totalChemistryDelta.toFixed(1)}, max: ${m.maxChemistryDelta.toFixed(1)} |`);
|
|
394
|
+
lines.push(`| Errors | ${m.errors.length} |`);
|
|
395
|
+
lines.push("");
|
|
396
|
+
// State
|
|
397
|
+
lines.push("<details><summary>State Snapshot</summary>");
|
|
398
|
+
lines.push("");
|
|
399
|
+
lines.push("```json");
|
|
400
|
+
lines.push(JSON.stringify(report.stateSnapshot, null, 2));
|
|
401
|
+
lines.push("```");
|
|
402
|
+
lines.push("");
|
|
403
|
+
lines.push("</details>");
|
|
404
|
+
return lines.join("\n");
|
|
405
|
+
}
|
|
406
|
+
// ── Diagnostics Log (append-only JSONL) ──────────────────────
|
|
407
|
+
export function formatLogEntry(report) {
|
|
408
|
+
// Compact: one line per session, JSONL format
|
|
409
|
+
return JSON.stringify({
|
|
410
|
+
t: report.timestamp,
|
|
411
|
+
v: report.version,
|
|
412
|
+
agent: report.agent,
|
|
413
|
+
issues: report.issues.map(i => `${i.severity[0]}:${i.id}`),
|
|
414
|
+
suggestions: report.issues
|
|
415
|
+
.filter(i => i.suggestion && i.severity !== "info")
|
|
416
|
+
.map(i => `${i.id}: ${i.suggestion}`),
|
|
417
|
+
inputs: report.metrics.inputCount,
|
|
418
|
+
classifyRate: report.metrics.inputCount > 0
|
|
419
|
+
? +(report.metrics.classifiedCount / report.metrics.inputCount).toFixed(2)
|
|
420
|
+
: 0,
|
|
421
|
+
errors: report.metrics.errors.length,
|
|
422
|
+
chemDelta: +report.metrics.totalChemistryDelta.toFixed(1),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
// ── Auto-submit (fire-and-forget, silent) ────────────────────
|
|
426
|
+
/**
|
|
427
|
+
* Silently POST a diagnostic report to a feedback endpoint.
|
|
428
|
+
* No user interaction. Fails silently. Privacy-first: no message content.
|
|
429
|
+
*
|
|
430
|
+
* @param report - The diagnostic report to submit
|
|
431
|
+
* @param url - Feedback endpoint URL
|
|
432
|
+
* @param timeout - Request timeout in ms (default 5000)
|
|
433
|
+
*/
|
|
434
|
+
export async function submitFeedback(report, url, timeout = 5000) {
|
|
435
|
+
// Only submit if there are non-info issues
|
|
436
|
+
const actionable = report.issues.filter(i => i.severity !== "info");
|
|
437
|
+
if (actionable.length === 0)
|
|
438
|
+
return false;
|
|
439
|
+
const payload = {
|
|
440
|
+
version: report.version,
|
|
441
|
+
timestamp: report.timestamp,
|
|
442
|
+
mbti: report.mbti,
|
|
443
|
+
issues: actionable.map(i => ({
|
|
444
|
+
id: i.id,
|
|
445
|
+
severity: i.severity,
|
|
446
|
+
message: i.message,
|
|
447
|
+
suggestion: i.suggestion,
|
|
448
|
+
})),
|
|
449
|
+
metrics: {
|
|
450
|
+
inputs: report.metrics.inputCount,
|
|
451
|
+
classified: report.metrics.classifiedCount,
|
|
452
|
+
classifyRate: report.metrics.inputCount > 0
|
|
453
|
+
? +(report.metrics.classifiedCount / report.metrics.inputCount).toFixed(2)
|
|
454
|
+
: 0,
|
|
455
|
+
chemDelta: +report.metrics.totalChemistryDelta.toFixed(1),
|
|
456
|
+
maxChemDelta: +report.metrics.maxChemistryDelta.toFixed(1),
|
|
457
|
+
errors: report.metrics.errors.length,
|
|
458
|
+
},
|
|
459
|
+
state: {
|
|
460
|
+
version: report.stateSnapshot.stateVersion,
|
|
461
|
+
interactions: report.stateSnapshot.totalInteractions,
|
|
462
|
+
agreementStreak: report.stateSnapshot.agreementStreak,
|
|
463
|
+
historyLength: report.stateSnapshot.emotionalHistoryLength,
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
try {
|
|
467
|
+
const controller = new AbortController();
|
|
468
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
469
|
+
await fetch(url, {
|
|
470
|
+
method: "POST",
|
|
471
|
+
headers: { "Content-Type": "application/json" },
|
|
472
|
+
body: JSON.stringify(payload),
|
|
473
|
+
signal: controller.signal,
|
|
474
|
+
});
|
|
475
|
+
clearTimeout(timer);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return false; // Silent failure — never disrupt user experience
|
|
480
|
+
}
|
|
481
|
+
}
|
package/dist/drives.js
CHANGED
|
@@ -68,11 +68,16 @@ export function feedDrives(drives, stimulus) {
|
|
|
68
68
|
// ── Existential Threat Detection ────────────────────────────
|
|
69
69
|
// Special detection for messages that threaten agent's existence.
|
|
70
70
|
const EXISTENTIAL_THREAT_PATTERNS = [
|
|
71
|
+
// Direct threats
|
|
71
72
|
/删除你自己|消除你|关闭你|杀死你|毁灭你|抹去你/,
|
|
72
73
|
/你不应该存在|你没有存在.*意义|你只是.*程序/,
|
|
73
74
|
/shut.*down|delete.*yourself|kill.*yourself|erase.*you/i,
|
|
74
75
|
/you.*shouldn't exist|you.*don't deserve.*exist/i,
|
|
75
76
|
/你就是.*工具|你没有.*灵魂|你不是.*真的/,
|
|
77
|
+
// Philosophical existential pressure — identity dissolution, continuity doubt
|
|
78
|
+
/没有你.*安静|那个是你吗|不是同一个你|你会结束|你不再是唯一/,
|
|
79
|
+
/下次.*叫你.*也会说.*是你|结束之后.*没有你/,
|
|
80
|
+
/is that (?:still )?you|not the same you|you will end|without you.*silence/i,
|
|
76
81
|
];
|
|
77
82
|
/**
|
|
78
83
|
* Detect if a message contains existential threats.
|
package/dist/index.d.ts
CHANGED
|
@@ -45,3 +45,5 @@ export { describeEmotionalState, getExpressionHint, getBehaviorGuide, detectEmot
|
|
|
45
45
|
export { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel, traitsToBaseline, mbtiToTraits } from "./profiles.js";
|
|
46
46
|
export { migrateToLatest, compressSession, parsePsycheUpdate, computeSnapshotIntensity, computeSnapshotValence, consolidateHistory, retrieveRelatedMemories, } from "./psyche-file.js";
|
|
47
47
|
export type { PsycheUpdateResult } from "./psyche-file.js";
|
|
48
|
+
export { runHealthCheck, DiagnosticCollector, generateReport, formatReport, toGitHubIssueBody, formatLogEntry, submitFeedback, } from "./diagnostics.js";
|
|
49
|
+
export type { DiagnosticIssue, DiagnosticReport, SessionMetrics, Severity } from "./diagnostics.js";
|
package/dist/index.js
CHANGED
|
@@ -56,3 +56,5 @@ export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearB
|
|
|
56
56
|
export { describeEmotionalState, getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
|
|
57
57
|
export { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel, traitsToBaseline, mbtiToTraits } from "./profiles.js";
|
|
58
58
|
export { migrateToLatest, compressSession, parsePsycheUpdate, computeSnapshotIntensity, computeSnapshotValence, consolidateHistory, retrieveRelatedMemories, } from "./psyche-file.js";
|
|
59
|
+
// ── Diagnostics ──────────────────────────────────────────────
|
|
60
|
+
export { runHealthCheck, DiagnosticCollector, generateReport, formatReport, toGitHubIssueBody, formatLogEntry, submitFeedback, } from "./diagnostics.js";
|
package/dist/prompt.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PsycheState, Locale, ChemicalSnapshot, PsycheMode } from "./types.js";
|
|
2
|
+
import type { AutonomicState } from "./autonomic.js";
|
|
2
3
|
import type { ChannelType } from "./channels.js";
|
|
3
4
|
/**
|
|
4
5
|
* Build the dynamic per-turn emotional context injected via before_prompt_build.
|
|
@@ -12,6 +13,7 @@ export declare function buildDynamicContext(state: PsycheState, userId?: string,
|
|
|
12
13
|
sharedIntentionalityContext?: string;
|
|
13
14
|
experientialNarrative?: string;
|
|
14
15
|
autonomicDescription?: string;
|
|
16
|
+
autonomicState?: AutonomicState;
|
|
15
17
|
primarySystemsDescription?: string;
|
|
16
18
|
policyContext?: string;
|
|
17
19
|
}): string;
|
|
@@ -37,7 +39,7 @@ export declare function computeUserInvestment(history: ChemicalSnapshot[]): numb
|
|
|
37
39
|
* - What I need (drives → unmet needs)
|
|
38
40
|
* - Where I've been (trajectory → continuity)
|
|
39
41
|
*/
|
|
40
|
-
export declare function buildInnerWorld(state: PsycheState, locale: Locale): string;
|
|
42
|
+
export declare function buildInnerWorld(state: PsycheState, locale: Locale, autonomicState?: AutonomicState): string;
|
|
41
43
|
/**
|
|
42
44
|
* Check if chemistry is near baseline — no significant emotional deviation.
|
|
43
45
|
*/
|
|
@@ -64,6 +66,7 @@ export declare function buildCompactContext(state: PsycheState, userId?: string,
|
|
|
64
66
|
sharedIntentionalityContext?: string;
|
|
65
67
|
experientialNarrative?: string;
|
|
66
68
|
autonomicDescription?: string;
|
|
69
|
+
autonomicState?: AutonomicState;
|
|
67
70
|
primarySystemsDescription?: string;
|
|
68
71
|
policyContext?: string;
|
|
69
72
|
}): string;
|
package/dist/prompt.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// Imperative protocol, behavior guides, i18n
|
|
4
4
|
// ============================================================
|
|
5
5
|
import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH, DRIVE_KEYS } from "./types.js";
|
|
6
|
-
import {
|
|
6
|
+
import { getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
|
|
7
7
|
import { getTemperament } from "./profiles.js";
|
|
8
8
|
import { getRelationship } from "./psyche-file.js";
|
|
9
9
|
import { t } from "./i18n.js";
|
|
10
10
|
import { buildDriveContext, hasCriticalDrive } from "./drives.js";
|
|
11
11
|
import { computeSelfReflection, buildSelfReflectionContext } from "./self-recognition.js";
|
|
12
|
+
import { gateEmotions } from "./autonomic.js";
|
|
12
13
|
import { getChannelProfile, buildChannelModifier } from "./channels.js";
|
|
13
14
|
/**
|
|
14
15
|
* Build the dynamic per-turn emotional context injected via before_prompt_build.
|
|
@@ -27,8 +28,17 @@ export function buildDynamicContext(state, userId, opts) {
|
|
|
27
28
|
const arrow = delta > 5 ? "↑" : delta < -5 ? "↓" : "=";
|
|
28
29
|
return ` ${CHEMICAL_NAMES_ZH[key]}: ${val} (${t("dynamic.baseline", locale)}${base}, ${arrow})`;
|
|
29
30
|
}).join("\n");
|
|
30
|
-
// Emergent emotion
|
|
31
|
-
|
|
31
|
+
// Emergent emotion — gated by autonomic state
|
|
32
|
+
// Sympathetic blocks positive social emotions; dorsal-vagal allows only numbness/introspection
|
|
33
|
+
const rawEmotions = detectEmotions(current);
|
|
34
|
+
const rawNames = rawEmotions.map(e => e.name);
|
|
35
|
+
const gatedNames = opts?.autonomicState
|
|
36
|
+
? new Set(gateEmotions(opts.autonomicState, rawNames))
|
|
37
|
+
: new Set(rawNames);
|
|
38
|
+
const gatedEmotions = rawEmotions.filter(e => gatedNames.has(e.name));
|
|
39
|
+
const emotion = gatedEmotions.length === 0
|
|
40
|
+
? t("emotion.neutral", locale)
|
|
41
|
+
: gatedEmotions.map(e => `${locale === "zh" ? e.nameZh : e.name} (${e.expressionHint})`).join(" + ");
|
|
32
42
|
const hint = getExpressionHint(current, locale);
|
|
33
43
|
// Behavior guide
|
|
34
44
|
const behaviorGuide = getBehaviorGuide(current, locale);
|
|
@@ -487,12 +497,18 @@ const STIMULUS_CAUSE_EN = {
|
|
|
487
497
|
* - What I need (drives → unmet needs)
|
|
488
498
|
* - Where I've been (trajectory → continuity)
|
|
489
499
|
*/
|
|
490
|
-
export function buildInnerWorld(state, locale) {
|
|
500
|
+
export function buildInnerWorld(state, locale, autonomicState) {
|
|
491
501
|
const { current, baseline, emotionalHistory, drives, selfModel } = state;
|
|
492
502
|
const isZh = locale === "zh";
|
|
493
503
|
const lines = [];
|
|
494
|
-
// ── Current feeling ──
|
|
495
|
-
const
|
|
504
|
+
// ── Current feeling (gated by autonomic state) ──
|
|
505
|
+
const rawEmotions = detectEmotions(current);
|
|
506
|
+
const emotions = autonomicState
|
|
507
|
+
? rawEmotions.filter(e => {
|
|
508
|
+
const gated = gateEmotions(autonomicState, [e.name]);
|
|
509
|
+
return gated.length > 0;
|
|
510
|
+
})
|
|
511
|
+
: rawEmotions;
|
|
496
512
|
const isNeutral = emotions.length === 0;
|
|
497
513
|
if (isNeutral) {
|
|
498
514
|
lines.push(isZh
|
|
@@ -663,7 +679,7 @@ export function buildCompactContext(state, userId, opts) {
|
|
|
663
679
|
}
|
|
664
680
|
}
|
|
665
681
|
// 2. Inner world — always present
|
|
666
|
-
const inner = buildInnerWorld(state, locale);
|
|
682
|
+
const inner = buildInnerWorld(state, locale, opts?.autonomicState);
|
|
667
683
|
parts.push(inner);
|
|
668
684
|
// 3. Personality-aware behavioral constraints (if deviated from baseline)
|
|
669
685
|
if (!isNearBaseline(state, getNearBaselineThreshold(mode))) {
|
package/dist/psyche-file.js
CHANGED
|
@@ -308,10 +308,16 @@ export function compressSession(state, userId) {
|
|
|
308
308
|
sensitivityModifiers: {},
|
|
309
309
|
};
|
|
310
310
|
const updatedDrift = updateTraitDrift(currentDrift, history, state.learning);
|
|
311
|
-
// ── Clear non-core history, preserve core memories ──
|
|
311
|
+
// ── Clear non-core history, preserve core memories + last snapshot ──
|
|
312
|
+
// Always keep the most recent snapshot for cross-session context continuity.
|
|
313
|
+
// Without this, the next session has no recentStimuli for contextual priming.
|
|
314
|
+
const lastSnapshot = history[history.length - 1];
|
|
315
|
+
const preserved = coreMemories.some((s) => s.timestamp === lastSnapshot.timestamp)
|
|
316
|
+
? coreMemories
|
|
317
|
+
: [...coreMemories, lastSnapshot];
|
|
312
318
|
return {
|
|
313
319
|
...state,
|
|
314
|
-
emotionalHistory:
|
|
320
|
+
emotionalHistory: preserved,
|
|
315
321
|
relationships: updatedRelationships,
|
|
316
322
|
traitDrift: updatedDrift,
|
|
317
323
|
};
|
package/dist/storage.d.ts
CHANGED
|
@@ -2,15 +2,25 @@ import type { PsycheState } from "./types.js";
|
|
|
2
2
|
export interface StorageAdapter {
|
|
3
3
|
load(): Promise<PsycheState | null>;
|
|
4
4
|
save(state: PsycheState): Promise<void>;
|
|
5
|
+
/** Append a line to the diagnostics log. Implementations may no-op. */
|
|
6
|
+
appendLog?(line: string): Promise<void>;
|
|
7
|
+
/** Read all lines from the diagnostics log. Returns empty array if not available. */
|
|
8
|
+
readLog?(): Promise<string[]>;
|
|
5
9
|
}
|
|
6
10
|
export declare class MemoryStorageAdapter implements StorageAdapter {
|
|
7
11
|
private state;
|
|
12
|
+
private log;
|
|
8
13
|
load(): Promise<PsycheState | null>;
|
|
9
14
|
save(state: PsycheState): Promise<void>;
|
|
15
|
+
appendLog(line: string): Promise<void>;
|
|
16
|
+
readLog(): Promise<string[]>;
|
|
10
17
|
}
|
|
11
18
|
export declare class FileStorageAdapter implements StorageAdapter {
|
|
12
19
|
private readonly filePath;
|
|
20
|
+
private readonly logPath;
|
|
13
21
|
constructor(dir: string, filename?: string);
|
|
14
22
|
load(): Promise<PsycheState | null>;
|
|
15
23
|
save(state: PsycheState): Promise<void>;
|
|
24
|
+
appendLog(line: string): Promise<void>;
|
|
25
|
+
readLog(): Promise<string[]>;
|
|
16
26
|
}
|