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/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,6 +443,7 @@ 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
  }),
@@ -439,6 +461,7 @@ export class PsycheEngine {
439
461
  sharedIntentionalityContext: sharedCtx || undefined,
440
462
  experientialNarrative: experientialNarrative,
441
463
  autonomicDescription: autonomicDesc,
464
+ autonomicState: autonomicResult.state,
442
465
  primarySystemsDescription: primarySystemsDescription || undefined,
443
466
  policyContext: policyCtx || undefined,
444
467
  }),
@@ -465,6 +488,41 @@ export class PsycheEngine {
465
488
  stateChanged = true;
466
489
  }
467
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
+ }
468
526
  // Anti-sycophancy: track agreement streak
469
527
  state = updateAgreementStreak(state, text);
470
528
  // Parse and merge <psyche_update> from LLM output
@@ -557,17 +615,76 @@ export class PsycheEngine {
557
615
  /**
558
616
  * End the current session: compress emotionalHistory into a rich summary
559
617
  * stored in relationship.memory[], then clear the history.
560
- * 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
561
621
  */
562
622
  async endSession(opts) {
563
623
  let state = this.ensureInitialized();
564
- if ((state.emotionalHistory ?? []).length < 2)
565
- return;
566
- 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
+ }
567
647
  // Reset session tracking for homeostatic pressure
568
648
  state = { ...state, sessionStartedAt: undefined };
569
649
  this.state = state;
570
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
+ }
571
688
  }
572
689
  // ── Private ──────────────────────────────────────────────
573
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
  *
@@ -87,4 +100,4 @@ export declare function computePolicyModifiers(state: PsycheState): PolicyModifi
87
100
  * (e.g., clamping max_tokens, requiring UI confirmation dialogs) —
88
101
  * not for replacing the LLM's decision-making.
89
102
  */
90
- 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,
@@ -386,7 +454,7 @@ export function computePolicyModifiers(state) {
386
454
  * (e.g., clamping max_tokens, requiring UI confirmation dialogs) —
387
455
  * not for replacing the LLM's decision-making.
388
456
  */
389
- export function buildPolicyContext(modifiers, locale) {
457
+ export function buildPolicyContext(modifiers, locale, drives) {
390
458
  const parts = [];
391
459
  const zh = locale === "zh";
392
460
  if (modifiers.responseLengthFactor < 0.6) {
@@ -413,6 +481,15 @@ export function buildPolicyContext(modifiers, locale) {
413
481
  else if (modifiers.emotionalDisclosure > 0.8) {
414
482
  parts.push(zh ? "坦诚分享感受" : "share feelings openly");
415
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
+ }
416
493
  if (parts.length === 0)
417
494
  return "";
418
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>;