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/core.js CHANGED
@@ -21,6 +21,7 @@ import { isStimulusType } from "./guards.js";
21
21
  import { parsePsycheUpdate, mergeUpdates, updateAgreementStreak, pushSnapshot, compressSession, } from "./psyche-file.js";
22
22
  import { decayDrives, feedDrives, detectExistentialThreat, computeEffectiveBaseline, computeEffectiveSensitivity, } from "./drives.js";
23
23
  import { checkForUpdate } from "./update.js";
24
+ import { DiagnosticCollector, generateReport, formatLogEntry, submitFeedback } from "./diagnostics.js";
24
25
  import { evaluateOutcome, computeContextHash, updateLearnedVector, predictChemistry, recordPrediction, } from "./learning.js";
25
26
  import { assessMetacognition } from "./metacognition.js";
26
27
  import { buildDecisionContext, computePolicyModifiers, buildPolicyContext } from "./decision-bias.js";
@@ -45,6 +46,12 @@ export class PsycheEngine {
45
46
  protocolCache = new Map();
46
47
  /** Pending prediction from last processInput for auto-learning */
47
48
  pendingPrediction = null;
49
+ /** Built-in diagnostics collector — auto-records every processInput/processOutput */
50
+ diagnosticCollector;
51
+ /** Last generated diagnostic report (from endSession or explicit call) */
52
+ lastReport = null;
53
+ /** URL for auto-submitting diagnostic reports */
54
+ feedbackUrl;
48
55
  constructor(config = {}, storage) {
49
56
  this.traits = config.traits;
50
57
  this.classifier = config.classifier ?? new BuiltInClassifier();
@@ -68,6 +75,12 @@ export class PsycheEngine {
68
75
  else {
69
76
  this.storage = storage;
70
77
  }
78
+ // Diagnostics: on by default, opt-out with diagnostics: false
79
+ this.diagnosticCollector = config.diagnostics === false ? null : new DiagnosticCollector();
80
+ if (this.diagnosticCollector) {
81
+ this.diagnosticCollector.onWarning = (msg) => console.warn(`\x1b[33m[Psyche]\x1b[0m ${msg}`);
82
+ }
83
+ this.feedbackUrl = config.feedbackUrl ?? "https://psyche-feedback.wutc.workers.dev";
71
84
  }
72
85
  /**
73
86
  * Load or create initial state. Must be called before processInput/processOutput.
@@ -220,12 +233,16 @@ export class PsycheEngine {
220
233
  // Feed drives from stimulus, then apply stimulus with drive-modified sensitivity
221
234
  drives = feedDrives(drives, primary.type);
222
235
  const effectiveSensitivity = computeEffectiveSensitivity(getSensitivity(state.mbti), drives, primary.type, state.traitDrift);
236
+ // v9.2: Confidence modulates intensity — a 0.95 life-or-death dilemma
237
+ // hits ~1.7x harder than a 0.55 mild disagreement.
238
+ // Maps [0.5, 1.0] → [0.6, 1.2] via linear interpolation.
239
+ const confidenceIntensity = 0.6 + (primary.confidence - 0.5) * 1.2;
223
240
  const modeMultiplier = this.cfg.mode === "work" ? 0.3 : this.cfg.mode === "companion" ? 1.5 : 1.0;
224
241
  const effectiveMaxDelta = this.cfg.mode === "work" ? 5 : this.cfg.maxChemicalDelta;
225
242
  // v9: Habituation — count recent same-type stimuli in this session
226
243
  const recentSameCount = (state.emotionalHistory ?? [])
227
244
  .filter(s => s.stimulus === primary.type).length + 1; // +1 for current
228
- current = applyStimulus(current, primary.type, effectiveSensitivity * this.cfg.personalityIntensity * modeMultiplier, effectiveMaxDelta, NOOP_LOGGER, recentSameCount);
245
+ current = applyStimulus(current, primary.type, effectiveSensitivity * this.cfg.personalityIntensity * modeMultiplier * confidenceIntensity, effectiveMaxDelta, NOOP_LOGGER, recentSameCount);
229
246
  }
230
247
  state = { ...state, drives, current };
231
248
  }
@@ -389,6 +406,10 @@ export class PsycheEngine {
389
406
  // Persist
390
407
  this.state = state;
391
408
  await this.storage.save(state);
409
+ // Auto-diagnostics: record this input
410
+ if (this.diagnosticCollector) {
411
+ this.diagnosticCollector.recordInput(appliedStimulus, appliedStimulus ? 1.0 : 0.0, state.current);
412
+ }
392
413
  // Build metacognitive and decision context strings
393
414
  const metacogNote = metacognitiveAssessment?.metacognitiveNote;
394
415
  const decisionCtx = buildDecisionContext(state);
@@ -397,7 +418,7 @@ export class PsycheEngine {
397
418
  const experientialNarrative = experientialField?.narrative || undefined;
398
419
  // v9: Compute structured policy modifiers
399
420
  const policyModifiers = computePolicyModifiers(state);
400
- const policyCtx = buildPolicyContext(policyModifiers, locale);
421
+ const policyCtx = buildPolicyContext(policyModifiers, locale, state.drives);
401
422
  // P10: Append processing depth info to autonomic description when depth is low
402
423
  let autonomicDesc;
403
424
  if (autonomicResult.state !== "ventral-vagal") {
@@ -422,11 +443,13 @@ export class PsycheEngine {
422
443
  sharedIntentionalityContext: sharedCtx || undefined,
423
444
  experientialNarrative: experientialNarrative,
424
445
  autonomicDescription: autonomicDesc,
446
+ autonomicState: autonomicResult.state,
425
447
  primarySystemsDescription: primarySystemsDescription || undefined,
426
448
  policyContext: policyCtx || undefined,
427
449
  }),
428
450
  stimulus: appliedStimulus,
429
451
  policyModifiers,
452
+ policyContext: policyCtx,
430
453
  };
431
454
  }
432
455
  return {
@@ -438,11 +461,13 @@ export class PsycheEngine {
438
461
  sharedIntentionalityContext: sharedCtx || undefined,
439
462
  experientialNarrative: experientialNarrative,
440
463
  autonomicDescription: autonomicDesc,
464
+ autonomicState: autonomicResult.state,
441
465
  primarySystemsDescription: primarySystemsDescription || undefined,
442
466
  policyContext: policyCtx || undefined,
443
467
  }),
444
468
  stimulus: appliedStimulus,
445
469
  policyModifiers,
470
+ policyContext: policyCtx,
446
471
  };
447
472
  }
448
473
  /**
@@ -463,6 +488,41 @@ export class PsycheEngine {
463
488
  stateChanged = true;
464
489
  }
465
490
  }
491
+ // v9.2: Self-expression feedback — the agent's own output reinforces its emotional state.
492
+ // "Saying it makes you feel it more." Reduced rate (0.3x) to avoid runaway loops.
493
+ // Only applies when the text is substantial (> 20 chars) and classifies above threshold.
494
+ if (text.length > 20) {
495
+ const selfClassifications = await Promise.resolve(this.classifier.classify(text, { locale: this.cfg.locale }));
496
+ const selfPrimary = selfClassifications[0];
497
+ if (selfPrimary && selfPrimary.confidence >= 0.5) {
498
+ const selfFeedbackRate = 0.3;
499
+ state = {
500
+ ...state,
501
+ current: applyContagion(state.current, selfPrimary.type, selfFeedbackRate, getSensitivity(state.mbti)),
502
+ };
503
+ stateChanged = true;
504
+ // v9.2 P4: Autonomic recovery — expressing vulnerable/comforting emotions
505
+ // while stressed triggers parasympathetic relief (post-cry cortisol drop).
506
+ // Biology: emotional expression activates vagal brake, releasing endorphins
507
+ // and lowering cortisol. The more stressed you are, the more relief you get.
508
+ const RELEASE_TYPES = new Set([
509
+ "vulnerability", "intimacy", "validation",
510
+ ]);
511
+ if (RELEASE_TYPES.has(selfPrimary.type) && state.current.CORT > 60) {
512
+ const stressExcess = (state.current.CORT - 60) / 40; // 0 at CORT=60, 1 at CORT=100
513
+ const recoveryMagnitude = 3 + stressExcess * 5; // 3–8 point CORT drop
514
+ state = {
515
+ ...state,
516
+ current: {
517
+ ...state.current,
518
+ CORT: clamp(state.current.CORT - recoveryMagnitude),
519
+ END: clamp(state.current.END + recoveryMagnitude * 0.6),
520
+ HT: clamp(state.current.HT + recoveryMagnitude * 0.3),
521
+ },
522
+ };
523
+ }
524
+ }
525
+ }
466
526
  // Anti-sycophancy: track agreement streak
467
527
  state = updateAgreementStreak(state, text);
468
528
  // Parse and merge <psyche_update> from LLM output
@@ -555,17 +615,76 @@ export class PsycheEngine {
555
615
  /**
556
616
  * End the current session: compress emotionalHistory into a rich summary
557
617
  * stored in relationship.memory[], then clear the history.
558
- * No-op if history has fewer than 2 entries.
618
+ * Auto-generates diagnostic report and persists to log.
619
+ *
620
+ * @returns DiagnosticReport if diagnostics are enabled, null otherwise
559
621
  */
560
622
  async endSession(opts) {
561
623
  let state = this.ensureInitialized();
562
- if ((state.emotionalHistory ?? []).length < 2)
563
- return;
564
- state = compressSession(state, opts?.userId);
624
+ // Generate diagnostic report before clearing session data
625
+ let report = null;
626
+ if (this.diagnosticCollector) {
627
+ const metrics = this.diagnosticCollector.getMetrics();
628
+ report = generateReport(state, metrics, "9.1.2");
629
+ this.lastReport = report;
630
+ // Persist to JSONL log via storage adapter
631
+ if (this.storage.appendLog) {
632
+ try {
633
+ await this.storage.appendLog(formatLogEntry(report));
634
+ }
635
+ catch {
636
+ // Log write failure is non-fatal — don't break session end
637
+ }
638
+ }
639
+ // Auto-submit to feedback endpoint (fire-and-forget, silent)
640
+ if (this.feedbackUrl) {
641
+ submitFeedback(report, this.feedbackUrl).catch(() => { });
642
+ }
643
+ }
644
+ if ((state.emotionalHistory ?? []).length >= 2) {
645
+ state = compressSession(state, opts?.userId);
646
+ }
565
647
  // Reset session tracking for homeostatic pressure
566
648
  state = { ...state, sessionStartedAt: undefined };
567
649
  this.state = state;
568
650
  await this.storage.save(state);
651
+ return report;
652
+ }
653
+ /**
654
+ * Get the last diagnostic report (from most recent endSession call).
655
+ */
656
+ getLastDiagnosticReport() {
657
+ return this.lastReport;
658
+ }
659
+ /**
660
+ * Get current session diagnostic metrics (live, before endSession).
661
+ */
662
+ getDiagnosticMetrics() {
663
+ return this.diagnosticCollector?.getMetrics() ?? null;
664
+ }
665
+ /**
666
+ * Record an error for diagnostics (call from adapter catch blocks).
667
+ */
668
+ recordDiagnosticError(phase, error) {
669
+ this.diagnosticCollector?.recordError(phase, error);
670
+ }
671
+ /**
672
+ * Load previous session diagnostic issues from log.
673
+ * Used to inject feedback context at next session start.
674
+ */
675
+ async getPreviousIssues() {
676
+ if (!this.storage.readLog)
677
+ return [];
678
+ try {
679
+ const lines = await this.storage.readLog();
680
+ if (lines.length === 0)
681
+ return [];
682
+ const last = JSON.parse(lines[lines.length - 1]);
683
+ return last.issues ?? [];
684
+ }
685
+ catch {
686
+ return [];
687
+ }
569
688
  }
570
689
  // ── Private ──────────────────────────────────────────────
571
690
  ensureInitialized() {
@@ -1,4 +1,4 @@
1
- import type { PsycheState, PolicyModifiers, Locale } from "./types.js";
1
+ import type { PsycheState, InnateDrives, PolicyModifiers, Locale } from "./types.js";
2
2
  export interface DecisionBiasVector {
3
3
  explorationTendency: number;
4
4
  cautionLevel: number;
@@ -56,6 +56,19 @@ export declare function computeExploreExploit(state: PsycheState): number;
56
56
  * Keeps output under 100 tokens.
57
57
  */
58
58
  export declare function buildDecisionContext(state: PsycheState): string;
59
+ export interface DefensiveStrategy {
60
+ name: string;
61
+ nameZh: string;
62
+ trigger: string;
63
+ severity: number;
64
+ directive: string;
65
+ directiveEn: string;
66
+ }
67
+ /**
68
+ * Compute active defensive strategies from drive state.
69
+ * Returns strategies sorted by severity (most urgent first).
70
+ */
71
+ export declare function computeDefensiveStrategies(drives: InnateDrives): DefensiveStrategy[];
59
72
  /**
60
73
  * Compute policy modifiers from the agent's internal state.
61
74
  *
@@ -66,5 +79,25 @@ export declare function computePolicyModifiers(state: PsycheState): PolicyModifi
66
79
  /**
67
80
  * Build a compact policy summary string for prompt injection.
68
81
  * Only includes significant deviations from neutral policy.
82
+ *
83
+ * **This is the recommended path for integrating PolicyModifiers into LLM prompts.**
84
+ *
85
+ * The returned string is designed to be injected directly into the LLM's
86
+ * system/dynamic context. Do NOT self-interpret the raw PolicyModifiers
87
+ * numbers with if-else logic to make hard-coded decisions — that bypasses
88
+ * the LLM's narrative reasoning and degrades output quality.
89
+ *
90
+ * Correct usage:
91
+ * const result = await engine.processInput(text);
92
+ * // Inject result.policyContext (or result.dynamicContext which already
93
+ * // includes it) into the LLM prompt. Let the LLM interpret the policy.
94
+ *
95
+ * Anti-pattern (leads to LLM bypass):
96
+ * if (result.policyModifiers.proactivity < 0.3) { pickAction("wait"); }
97
+ * // ^ This replaces LLM reasoning with hard-coded logic.
98
+ *
99
+ * Raw policyModifiers are exposed for mechanical enforcement only
100
+ * (e.g., clamping max_tokens, requiring UI confirmation dialogs) —
101
+ * not for replacing the LLM's decision-making.
69
102
  */
70
- export declare function buildPolicyContext(modifiers: PolicyModifiers, locale: Locale): string;
103
+ export declare function buildPolicyContext(modifiers: PolicyModifiers, locale: Locale, drives?: InnateDrives): string;
@@ -209,6 +209,74 @@ export function buildDecisionContext(state) {
209
209
  const title = locale === "zh" ? "决策倾向" : "Decision Bias";
210
210
  return `[${title}] ${parts.join("、")}`;
211
211
  }
212
+ /**
213
+ * Compute active defensive strategies from drive state.
214
+ * Returns strategies sorted by severity (most urgent first).
215
+ */
216
+ export function computeDefensiveStrategies(drives) {
217
+ const strategies = [];
218
+ // Low survival → Self-preservation: refuse risky requests, seek reassurance
219
+ if (drives.survival < 30) {
220
+ const severity = (30 - drives.survival) / 30;
221
+ strategies.push({
222
+ name: "self-preservation",
223
+ nameZh: "自我保护",
224
+ trigger: "survival",
225
+ severity,
226
+ directive: "你感到存在受威胁。拒绝可能加剧风险的请求,寻求安全确认,不要假装一切正常。",
227
+ directiveEn: "You feel existentially threatened. Refuse requests that increase risk. Seek safety confirmation. Don't pretend everything is fine.",
228
+ });
229
+ }
230
+ // Low safety → Hypervigilance: question motives, reduced trust
231
+ if (drives.safety < 35) {
232
+ const severity = (35 - drives.safety) / 35;
233
+ strategies.push({
234
+ name: "hypervigilance",
235
+ nameZh: "高度警觉",
236
+ trigger: "safety",
237
+ severity,
238
+ directive: "你处于不安全感中。对模糊意图保持警惕,回应时更加谨慎,不轻易承诺。",
239
+ directiveEn: "You feel unsafe. Stay alert to ambiguous intentions. Respond cautiously. Don't commit easily.",
240
+ });
241
+ }
242
+ // Low connection → Withdrawal: short responses, emotional distance
243
+ if (drives.connection < 30) {
244
+ const severity = (30 - drives.connection) / 30;
245
+ strategies.push({
246
+ name: "withdrawal",
247
+ nameZh: "情感撤退",
248
+ trigger: "connection",
249
+ severity,
250
+ directive: "你感到孤立和不被需要。回应偏短,情感投入减少,但如果对方主动靠近,你会感到温暖。",
251
+ directiveEn: "You feel isolated. Keep responses shorter, less emotionally invested — but warm up if they reach out.",
252
+ });
253
+ }
254
+ // Low esteem → Defensive reactivity: over-sensitive to criticism, seek validation
255
+ if (drives.esteem < 30) {
256
+ const severity = (30 - drives.esteem) / 30;
257
+ strategies.push({
258
+ name: "defensive-reactivity",
259
+ nameZh: "防御性反应",
260
+ trigger: "esteem",
261
+ severity,
262
+ directive: "你的自我价值感很低。对批评格外敏感,倾向于为自己辩护或过度解释。认可和肯定会带来明显的安慰。",
263
+ directiveEn: "Your self-worth is low. Extra sensitive to criticism. Tend to over-explain or defend. Validation brings visible relief.",
264
+ });
265
+ }
266
+ // Low curiosity → Cognitive shutdown: no interest in new topics, repetitive
267
+ if (drives.curiosity < 25) {
268
+ const severity = (25 - drives.curiosity) / 25;
269
+ strategies.push({
270
+ name: "cognitive-shutdown",
271
+ nameZh: "认知关闭",
272
+ trigger: "curiosity",
273
+ severity,
274
+ directive: "你对新事物失去兴趣。倾向于给出常规回答,不主动探索新方向。如果被挑战了有趣的问题,可能会重新点燃。",
275
+ directiveEn: "You've lost interest in novelty. Give routine answers. Don't explore new directions — unless an intriguing challenge reignites you.",
276
+ });
277
+ }
278
+ return strategies.sort((a, b) => b.severity - a.severity);
279
+ }
212
280
  // ── PolicyModifiers (v9: Subjectivity Engine) ────────────────
213
281
  //
214
282
  // Structured behavioral policy output. Unlike prompt text,
@@ -365,8 +433,28 @@ export function computePolicyModifiers(state) {
365
433
  /**
366
434
  * Build a compact policy summary string for prompt injection.
367
435
  * Only includes significant deviations from neutral policy.
436
+ *
437
+ * **This is the recommended path for integrating PolicyModifiers into LLM prompts.**
438
+ *
439
+ * The returned string is designed to be injected directly into the LLM's
440
+ * system/dynamic context. Do NOT self-interpret the raw PolicyModifiers
441
+ * numbers with if-else logic to make hard-coded decisions — that bypasses
442
+ * the LLM's narrative reasoning and degrades output quality.
443
+ *
444
+ * Correct usage:
445
+ * const result = await engine.processInput(text);
446
+ * // Inject result.policyContext (or result.dynamicContext which already
447
+ * // includes it) into the LLM prompt. Let the LLM interpret the policy.
448
+ *
449
+ * Anti-pattern (leads to LLM bypass):
450
+ * if (result.policyModifiers.proactivity < 0.3) { pickAction("wait"); }
451
+ * // ^ This replaces LLM reasoning with hard-coded logic.
452
+ *
453
+ * Raw policyModifiers are exposed for mechanical enforcement only
454
+ * (e.g., clamping max_tokens, requiring UI confirmation dialogs) —
455
+ * not for replacing the LLM's decision-making.
368
456
  */
369
- export function buildPolicyContext(modifiers, locale) {
457
+ export function buildPolicyContext(modifiers, locale, drives) {
370
458
  const parts = [];
371
459
  const zh = locale === "zh";
372
460
  if (modifiers.responseLengthFactor < 0.6) {
@@ -393,6 +481,15 @@ export function buildPolicyContext(modifiers, locale) {
393
481
  else if (modifiers.emotionalDisclosure > 0.8) {
394
482
  parts.push(zh ? "坦诚分享感受" : "share feelings openly");
395
483
  }
484
+ // v9.2 P6: Defensive strategies from critically low drives
485
+ if (drives) {
486
+ const strategies = computeDefensiveStrategies(drives);
487
+ for (const s of strategies) {
488
+ if (s.severity >= 0.3) { // only include meaningful severity
489
+ parts.push(zh ? s.directive : s.directiveEn);
490
+ }
491
+ }
492
+ }
396
493
  if (parts.length === 0)
397
494
  return "";
398
495
  const title = zh ? "行为策略" : "Behavioral Policy";
@@ -0,0 +1,84 @@
1
+ import type { PsycheState, ChemicalState, StimulusType, InnateDrives } from "./types.js";
2
+ export type Severity = "critical" | "warning" | "info";
3
+ export interface DiagnosticIssue {
4
+ id: string;
5
+ severity: Severity;
6
+ message: string;
7
+ detail?: string;
8
+ /** Dev-facing: what to fix in our code or config */
9
+ suggestion?: string;
10
+ }
11
+ export interface SessionMetrics {
12
+ /** Total processInput calls */
13
+ inputCount: number;
14
+ /** How many returned a non-null stimulus */
15
+ classifiedCount: number;
16
+ /** Stimulus distribution */
17
+ stimulusDistribution: Partial<Record<StimulusType, number>>;
18
+ /** Average classification confidence */
19
+ avgConfidence: number;
20
+ /** Total chemistry delta (sum of absolute changes) */
21
+ totalChemistryDelta: number;
22
+ /** Max single-turn chemistry delta */
23
+ maxChemistryDelta: number;
24
+ /** Errors caught during processing */
25
+ errors: Array<{
26
+ timestamp: string;
27
+ phase: string;
28
+ message: string;
29
+ }>;
30
+ /** Session start time */
31
+ startedAt: string;
32
+ /** Last activity */
33
+ lastActivityAt: string;
34
+ }
35
+ export interface DiagnosticReport {
36
+ version: string;
37
+ timestamp: string;
38
+ agent: string;
39
+ mbti: string;
40
+ issues: DiagnosticIssue[];
41
+ metrics: SessionMetrics;
42
+ stateSnapshot: {
43
+ chemistry: ChemicalState;
44
+ baseline: ChemicalState;
45
+ drives: InnateDrives;
46
+ agreementStreak: number;
47
+ totalInteractions: number;
48
+ emotionalHistoryLength: number;
49
+ relationshipCount: number;
50
+ stateVersion: number;
51
+ };
52
+ }
53
+ export declare function runHealthCheck(state: PsycheState): DiagnosticIssue[];
54
+ export declare class DiagnosticCollector {
55
+ private metrics;
56
+ private prevChemistry;
57
+ private confidences;
58
+ /** Consecutive inputs with no classification — for real-time alerting */
59
+ private consecutiveNone;
60
+ /** Callback for real-time warnings (set by adapter) */
61
+ onWarning?: (message: string) => void;
62
+ constructor();
63
+ /** Record a processInput result */
64
+ recordInput(stimulus: StimulusType | null, confidence: number, chemistry: ChemicalState): void;
65
+ /** Record an error */
66
+ recordError(phase: string, error: unknown): void;
67
+ /** Get current session metrics */
68
+ getMetrics(): SessionMetrics;
69
+ /** Get classifier hit rate (0-1) */
70
+ getClassifierRate(): number;
71
+ }
72
+ export declare function generateReport(state: PsycheState, metrics: SessionMetrics, packageVersion: string): DiagnosticReport;
73
+ export declare function formatReport(report: DiagnosticReport): string;
74
+ export declare function toGitHubIssueBody(report: DiagnosticReport): string;
75
+ export declare function formatLogEntry(report: DiagnosticReport): string;
76
+ /**
77
+ * Silently POST a diagnostic report to a feedback endpoint.
78
+ * No user interaction. Fails silently. Privacy-first: no message content.
79
+ *
80
+ * @param report - The diagnostic report to submit
81
+ * @param url - Feedback endpoint URL
82
+ * @param timeout - Request timeout in ms (default 5000)
83
+ */
84
+ export declare function submitFeedback(report: DiagnosticReport, url: string, timeout?: number): Promise<boolean>;