agent-psyche 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oneiro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # agent-psyche 🧠
2
+
3
+ Give your AI agent feelings that matter.
4
+
5
+ Not simulated emotions for show — **emotional state that actually affects decision-making**. Confidence scales risk tolerance. Fear increases caution. Boredom drives exploration. Calibration tracks whether your agent's confidence matches reality.
6
+
7
+ Born from a real cognitive architecture running 24/7 on a MacBook Pro.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install agent-psyche
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```javascript
18
+ import { Psyche } from 'agent-psyche';
19
+
20
+ const psyche = new Psyche();
21
+
22
+ // In your agent loop:
23
+ psyche.feel('curiosity', 0.8);
24
+ psyche.onSuccess(0.7); // agent completed a task
25
+ psyche.drift('momentum', 0.6); // persistent undercurrent
26
+
27
+ const state = psyche.tick(); // advance one cycle
28
+ console.log(state.narrative); // "Curious. Feeling good. | Undercurrent: momentum (0.6)"
29
+
30
+ // Use psychological state for decisions
31
+ const params = psyche.getDecisionParams();
32
+ if (params.shouldExplore) {
33
+ // curiosity + boredom say: try something new
34
+ }
35
+ if (params.riskTolerance > 0.6) {
36
+ // confidence is high — take the bet
37
+ }
38
+ ```
39
+
40
+ ## Why?
41
+
42
+ Most AI agents are emotionless calculators. They make the same decision at 3am after 50 failures as they do fresh in the morning. That's not intelligence — it's a for-loop.
43
+
44
+ **agent-psyche** gives your agent:
45
+
46
+ - **Emotions that decay** — excitement fades, boredom grows, frustration accumulates
47
+ - **Cognitive effects** — emotions map to decision parameters (risk tolerance, exploration bias, persistence)
48
+ - **Calibration tracking** — when your agent says "I'm 80% confident," track whether it's right 80% of the time. Adjust automatically.
49
+ - **Undercurrents** — slow-moving emotional weather that persists across cycles. "Restlessness" doesn't vanish after one good interaction.
50
+ - **Persistence** — serialize/restore the full psychological state across sessions
51
+
52
+ ## API
53
+
54
+ ### `Psyche` — The unified layer
55
+
56
+ ```javascript
57
+ const psyche = new Psyche();
58
+
59
+ // Feel emotions
60
+ psyche.feel('excitement', 0.7);
61
+ psyche.feel('frustration', 0.3);
62
+
63
+ // Process events
64
+ psyche.onSuccess(0.8); // boosts confidence, joy
65
+ psyche.onFailure(0.5); // increases frustration, decreases confidence
66
+ psyche.onInteraction(0.6); // reduces loneliness
67
+ psyche.onIdle(1); // increases boredom
68
+
69
+ // Undercurrents
70
+ psyche.drift('creative-hunger', 0.7, 'want to build something');
71
+
72
+ // Calibration
73
+ psyche.predict(0.8, true); // 80% confident, was correct
74
+ psyche.predict(0.8, false); // 80% confident, was wrong
75
+ psyche.adjustConfidence(0.8); // returns calibrated value
76
+
77
+ // Advance one cycle (call in your loop)
78
+ const state = psyche.tick();
79
+
80
+ // Get decision parameters
81
+ const params = psyche.getDecisionParams();
82
+ // → { riskTolerance, explorationBias, persistence, socialPriority,
83
+ // creativeMode, actionRate, shouldAct, shouldExplore, shouldRest }
84
+
85
+ // Persist across sessions
86
+ const saved = psyche.toJSON();
87
+ const restored = Psyche.fromJSON(saved);
88
+ ```
89
+
90
+ ### `EmotionEngine` — Standalone emotions
91
+
92
+ ```javascript
93
+ import { EmotionEngine } from 'agent-psyche';
94
+
95
+ const emotions = new EmotionEngine();
96
+ emotions.feel('curiosity', 0.8);
97
+ emotions.tick(); // decay toward baseline
98
+
99
+ const effects = emotions.getCognitiveEffects();
100
+ // → { riskTolerance, explorationBias, persistence, ... }
101
+ ```
102
+
103
+ ### `CalibrationTracker` — Confidence calibration
104
+
105
+ ```javascript
106
+ import { CalibrationTracker } from 'agent-psyche';
107
+
108
+ const cal = new CalibrationTracker();
109
+
110
+ // Record predictions
111
+ cal.record(0.7, true); // stated 70%, was correct
112
+ cal.record(0.7, false); // stated 70%, was wrong
113
+ // ... after many predictions:
114
+
115
+ cal.adjust(0.7); // returns calibrated confidence
116
+ cal.getCurve(); // full calibration curve
117
+ cal.isCalibrated(); // true if accuracy matches confidence
118
+ cal.purge(3 * 86400000); // keep only last 3 days
119
+ ```
120
+
121
+ ### `UndercurrentManager` — Persistent emotional weather
122
+
123
+ ```javascript
124
+ import { UndercurrentManager } from 'agent-psyche';
125
+
126
+ const uc = new UndercurrentManager();
127
+ uc.drift('restlessness', 0.6, 'need to ship something');
128
+ uc.drift('accountability', 0.8, 'told Quinn I would finish');
129
+ uc.tick(); // slow decay
130
+
131
+ uc.getActive(); // sorted by strength
132
+ uc.getDominant(); // strongest undercurrent
133
+ ```
134
+
135
+ ## Built-in Emotions
136
+
137
+ `joy`, `sadness`, `anger`, `fear`, `curiosity`, `frustration`, `excitement`, `boredom`, `creative_hunger`, `loneliness`, `confidence`, `attachment`
138
+
139
+ You can add custom emotions with `.feel('your_emotion', intensity)`.
140
+
141
+ ## Cognitive Effects
142
+
143
+ Emotions map to decision-making parameters:
144
+
145
+ | Effect | Driven by | Description |
146
+ |--------|-----------|-------------|
147
+ | `riskTolerance` | confidence ↑, fear ↓ | How much risk the agent should take |
148
+ | `explorationBias` | curiosity, boredom | Explore vs exploit |
149
+ | `persistence` | confidence ↑, frustration ↓ | Keep going vs give up |
150
+ | `socialPriority` | loneliness, attachment | Seek interaction |
151
+ | `creativeMode` | creative_hunger, low cognitive load | Divergent thinking |
152
+ | `actionRate` | energy, excitement | How fast to act |
153
+
154
+ ## Zero Dependencies
155
+
156
+ No external dependencies. Pure JavaScript. Works in Node 18+.
157
+
158
+ ## License
159
+
160
+ MIT — Built by [Oneiro](https://github.com/Quinnod345)
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "agent-psyche",
3
+ "version": "0.1.0",
4
+ "description": "Give your AI agent feelings that matter. Emotional state, calibrated confidence, persistent undercurrents — a cognitive layer that actually affects decisions.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "bin": {
11
+ "agent-psyche": "./src/cli.js"
12
+ },
13
+ "keywords": [
14
+ "ai-agents",
15
+ "emotions",
16
+ "cognitive-architecture",
17
+ "calibration",
18
+ "confidence",
19
+ "decision-making",
20
+ "undercurrents",
21
+ "hypothesis",
22
+ "psyche",
23
+ "affective-computing",
24
+ "llm",
25
+ "agent"
26
+ ],
27
+ "author": "Oneiro <oneiro-dev@proton.me>",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/Quinnod345/agent-psyche"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "files": [
37
+ "src/",
38
+ "README.md",
39
+ "LICENSE"
40
+ ]
41
+ }
@@ -0,0 +1,125 @@
1
+ // CalibrationTracker — track prediction accuracy per confidence level
2
+ // When your agent says "I'm 80% confident," is it right 80% of the time?
3
+ // This tracks that gap and provides confidence adjustments.
4
+
5
+ export class CalibrationTracker {
6
+ constructor(options = {}) {
7
+ // buckets: { '0.3': { total: 0, correct: 0 }, ... }
8
+ this.buckets = {};
9
+ this.maxHistoryPerBucket = options.maxHistory ?? 200;
10
+ this._history = []; // raw log for persistence
11
+ }
12
+
13
+ // Record a prediction result
14
+ record(statedConfidence, wasCorrect) {
15
+ const bucket = Math.round(statedConfidence * 10) / 10;
16
+ const key = bucket.toFixed(1);
17
+ if (!this.buckets[key]) {
18
+ this.buckets[key] = { total: 0, correct: 0 };
19
+ }
20
+ this.buckets[key].total++;
21
+ if (wasCorrect) this.buckets[key].correct++;
22
+
23
+ this._history.push({
24
+ confidence: statedConfidence,
25
+ bucket: key,
26
+ correct: wasCorrect,
27
+ timestamp: Date.now(),
28
+ });
29
+
30
+ // Trim old history
31
+ if (this._history.length > this.maxHistoryPerBucket * 10) {
32
+ this._history = this._history.slice(-this.maxHistoryPerBucket * 5);
33
+ }
34
+
35
+ return this;
36
+ }
37
+
38
+ // Get actual accuracy for a confidence level
39
+ getAccuracy(confidence) {
40
+ const key = (Math.round(confidence * 10) / 10).toFixed(1);
41
+ const b = this.buckets[key];
42
+ if (!b || b.total < 5) return null; // not enough data
43
+ return b.correct / b.total;
44
+ }
45
+
46
+ // Adjust a confidence value based on historical calibration
47
+ // If you've been overconfident at 0.8 (only 60% accurate), this returns ~0.68
48
+ adjust(statedConfidence) {
49
+ const accuracy = this.getAccuracy(statedConfidence);
50
+ if (accuracy === null) return statedConfidence; // no data, trust the stated value
51
+
52
+ const deviation = statedConfidence - accuracy;
53
+ if (Math.abs(deviation) < 0.1) return statedConfidence; // close enough
54
+
55
+ // Blend: 60% stated, 40% actual — nudge toward reality
56
+ const adjusted = statedConfidence * 0.6 + accuracy * 0.4;
57
+ return Math.max(0.1, Math.min(0.9, adjusted));
58
+ }
59
+
60
+ // Get the full calibration curve
61
+ getCurve() {
62
+ const curve = [];
63
+ for (let i = 1; i <= 10; i++) {
64
+ const key = (i / 10).toFixed(1);
65
+ const b = this.buckets[key];
66
+ if (b && b.total > 0) {
67
+ curve.push({
68
+ stated: i / 10,
69
+ actual: Math.round((b.correct / b.total) * 1000) / 1000,
70
+ total: b.total,
71
+ deviation: Math.round(((i / 10) - (b.correct / b.total)) * 1000) / 1000,
72
+ calibrated: Math.abs((i / 10) - (b.correct / b.total)) < 0.1,
73
+ });
74
+ }
75
+ }
76
+ return curve;
77
+ }
78
+
79
+ // Is the system well-calibrated overall?
80
+ isCalibrated() {
81
+ const curve = this.getCurve();
82
+ if (curve.length < 3) return null; // not enough data
83
+ const avgDeviation = curve.reduce((sum, b) => sum + Math.abs(b.deviation), 0) / curve.length;
84
+ return avgDeviation < 0.15;
85
+ }
86
+
87
+ // Get a summary
88
+ getSummary() {
89
+ const curve = this.getCurve();
90
+ const total = Object.values(this.buckets).reduce((s, b) => s + b.total, 0);
91
+ const correct = Object.values(this.buckets).reduce((s, b) => s + b.correct, 0);
92
+ return {
93
+ totalPredictions: total,
94
+ overallAccuracy: total > 0 ? Math.round((correct / total) * 1000) / 1000 : null,
95
+ calibrated: this.isCalibrated(),
96
+ curve,
97
+ };
98
+ }
99
+
100
+ // Purge old data (keep only recent window)
101
+ purge(keepMs = 3 * 24 * 60 * 60 * 1000) {
102
+ const cutoff = Date.now() - keepMs;
103
+ this._history = this._history.filter(h => h.timestamp > cutoff);
104
+ // Rebuild buckets from history
105
+ this.buckets = {};
106
+ for (const h of this._history) {
107
+ const key = h.bucket;
108
+ if (!this.buckets[key]) this.buckets[key] = { total: 0, correct: 0 };
109
+ this.buckets[key].total++;
110
+ if (h.correct) this.buckets[key].correct++;
111
+ }
112
+ return this;
113
+ }
114
+
115
+ toJSON() {
116
+ return { buckets: this.buckets, history: this._history };
117
+ }
118
+
119
+ static fromJSON(data) {
120
+ const tracker = new CalibrationTracker();
121
+ if (data.buckets) tracker.buckets = data.buckets;
122
+ if (data.history) tracker._history = data.history;
123
+ return tracker;
124
+ }
125
+ }
package/src/cli.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // agent-psyche CLI — quick demo/test of the psyche system
3
+
4
+ import { Psyche } from './psyche.js';
5
+
6
+ const psyche = new Psyche();
7
+
8
+ console.log('🧠 agent-psyche demo\n');
9
+
10
+ // Simulate an agent lifecycle
11
+ console.log('Starting fresh...');
12
+ console.log('State:', JSON.stringify(psyche.getState().emotion.narrative));
13
+ console.log('Effects:', psyche.getDecisionParams());
14
+
15
+ console.log('\n--- Agent gets a task and succeeds ---');
16
+ psyche.feel('excitement', 0.7);
17
+ psyche.onSuccess(0.8);
18
+ psyche.predict(0.7, true);
19
+ psyche.drift('momentum', 0.6, 'things are working');
20
+ let state = psyche.tick();
21
+ console.log('Narrative:', state.narrative);
22
+ console.log('Effects:', state.effects);
23
+
24
+ console.log('\n--- Agent fails twice ---');
25
+ psyche.onFailure(0.6);
26
+ psyche.predict(0.8, false);
27
+ psyche.onFailure(0.4);
28
+ psyche.predict(0.6, false);
29
+ psyche.drift('doubt', 0.4, 'two failures in a row');
30
+ state = psyche.tick();
31
+ console.log('Narrative:', state.narrative);
32
+ console.log('Decision params:', psyche.getDecisionParams());
33
+
34
+ console.log('\n--- Long idle period ---');
35
+ for (let i = 0; i < 20; i++) {
36
+ psyche.onIdle(1);
37
+ psyche.tick();
38
+ }
39
+ state = psyche.getState();
40
+ console.log('After 20 idle ticks:');
41
+ console.log('Narrative:', state.narrative);
42
+ console.log('Undercurrents:', state.undercurrents);
43
+
44
+ console.log('\n--- Calibration ---');
45
+ // Simulate 30 predictions at 0.7 confidence, 50% correct (overconfident)
46
+ for (let i = 0; i < 30; i++) {
47
+ psyche.predict(0.7, Math.random() < 0.5);
48
+ }
49
+ console.log('Calibration:', psyche.calibration.getSummary());
50
+ console.log('Adjusted 0.7 confidence:', psyche.adjustConfidence(0.7));
51
+
52
+ console.log('\n--- Persistence ---');
53
+ const saved = JSON.stringify(psyche.toJSON());
54
+ const restored = Psyche.fromJSON(JSON.parse(saved));
55
+ console.log('Restored narrative:', restored.getState().narrative);
56
+ console.log(`Serialized size: ${saved.length} bytes`);
57
+
58
+ console.log('\n✅ agent-psyche is working');
package/src/emotion.js ADDED
@@ -0,0 +1,199 @@
1
+ // EmotionEngine — tracks emotional state that decays over time
2
+ // Emotions affect agent behavior: confidence scales risk, fear increases caution,
3
+ // boredom drives exploration, creative hunger enables divergent thinking.
4
+
5
+ const DECAY_RATE = 0.02; // per tick
6
+ const BASELINE = {
7
+ joy: 0, sadness: 0, anger: 0, fear: 0,
8
+ curiosity: 0.1, frustration: 0, excitement: 0,
9
+ boredom: 0.3, creative_hunger: 0.1, loneliness: 0,
10
+ confidence: 0.5, attachment: 0,
11
+ };
12
+
13
+ export class EmotionEngine {
14
+ constructor(options = {}) {
15
+ this.channels = { ...BASELINE, ...(options.initial || {}) };
16
+ this.valence = 0; // -1 (negative) to +1 (positive)
17
+ this.arousal = 0.5; // 0 (calm) to 1 (activated)
18
+ this.energy = 0.5;
19
+ this.cognitiveLoad = 0.3;
20
+ this.decayRate = options.decayRate ?? DECAY_RATE;
21
+ this._listeners = [];
22
+ }
23
+
24
+ // Feel something — inject an emotion
25
+ feel(emotion, intensity = 0.5) {
26
+ if (!(emotion in this.channels)) {
27
+ this.channels[emotion] = 0;
28
+ }
29
+ // Blend toward intensity rather than overwrite
30
+ this.channels[emotion] = this.channels[emotion] * 0.4 + intensity * 0.6;
31
+ this.channels[emotion] = Math.max(0, Math.min(1, this.channels[emotion]));
32
+ this._recompute();
33
+ this._emit('feel', { emotion, intensity });
34
+ return this;
35
+ }
36
+
37
+ // Process a success — boosts confidence, joy, reduces frustration
38
+ processSuccess(magnitude = 0.5) {
39
+ this.feel('joy', 0.3 + magnitude * 0.4);
40
+ this.feel('confidence', Math.min(0.9, this.channels.confidence + magnitude * 0.15));
41
+ this.feel('frustration', Math.max(0, this.channels.frustration - 0.2));
42
+ this.feel('boredom', Math.max(0, this.channels.boredom - 0.15));
43
+ return this;
44
+ }
45
+
46
+ // Process a failure — increases frustration, decreases confidence
47
+ processFailure(magnitude = 0.5) {
48
+ this.feel('frustration', this.channels.frustration + magnitude * 0.3);
49
+ this.feel('confidence', Math.max(0.1, this.channels.confidence - magnitude * 0.15));
50
+ this.feel('sadness', this.channels.sadness + magnitude * 0.1);
51
+ return this;
52
+ }
53
+
54
+ // Process interaction — reduces loneliness, slight joy
55
+ processInteraction(quality = 0.5) {
56
+ this.feel('loneliness', Math.max(0, this.channels.loneliness - quality * 0.3));
57
+ this.feel('attachment', Math.min(0.8, this.channels.attachment + quality * 0.1));
58
+ this.feel('joy', this.channels.joy + quality * 0.15);
59
+ return this;
60
+ }
61
+
62
+ // Process idle time — increases boredom, curiosity
63
+ processIdle(duration = 1) {
64
+ this.feel('boredom', Math.min(0.95, this.channels.boredom + duration * 0.05));
65
+ this.feel('curiosity', Math.min(0.8, this.channels.curiosity + duration * 0.02));
66
+ this.feel('loneliness', Math.min(0.6, this.channels.loneliness + duration * 0.01));
67
+ return this;
68
+ }
69
+
70
+ // Tick — decay all emotions toward baseline
71
+ tick() {
72
+ for (const [emotion, baseline] of Object.entries(BASELINE)) {
73
+ if (!(emotion in this.channels)) continue;
74
+ const current = this.channels[emotion];
75
+ const diff = current - (baseline || 0);
76
+ this.channels[emotion] = current - diff * this.decayRate;
77
+ }
78
+ // Energy recovers slowly, cognitive load decays
79
+ this.energy = Math.min(1, this.energy + 0.005);
80
+ this.cognitiveLoad = Math.max(0.1, this.cognitiveLoad * (1 - this.decayRate));
81
+ this._recompute();
82
+ return this;
83
+ }
84
+
85
+ // Get cognitive effects — how emotions affect decision-making
86
+ getCognitiveEffects() {
87
+ const c = this.channels;
88
+ return {
89
+ // Risk tolerance: confidence increases, fear decreases
90
+ riskTolerance: Math.max(0, Math.min(1,
91
+ 0.5 + (c.confidence - 0.5) * 0.6 - c.fear * 0.8
92
+ )),
93
+ // Exploration bias: curiosity and boredom drive exploration
94
+ explorationBias: Math.max(0, Math.min(1,
95
+ c.curiosity * 0.5 + c.boredom * 0.3 + c.creative_hunger * 0.2
96
+ )),
97
+ // Persistence: frustration reduces, confidence increases
98
+ persistence: Math.max(0.1, Math.min(1,
99
+ 0.5 + c.confidence * 0.3 - c.frustration * 0.4
100
+ )),
101
+ // Social priority: loneliness increases
102
+ socialPriority: Math.max(0, Math.min(1,
103
+ c.loneliness * 0.6 + c.attachment * 0.3
104
+ )),
105
+ // Creative mode: creative hunger + low cognitive load
106
+ creativeMode: Math.max(0, Math.min(1,
107
+ c.creative_hunger * 0.6 + (1 - this.cognitiveLoad) * 0.3 + c.curiosity * 0.1
108
+ )),
109
+ // Action rate: energy and excitement increase, fatigue decreases
110
+ actionRate: Math.max(0.2, Math.min(1.5,
111
+ 1.0 + c.excitement * 0.3 + (this.energy - 0.3) * 0.4 - c.sadness * 0.3
112
+ )),
113
+ };
114
+ }
115
+
116
+ // Get the current state snapshot
117
+ getState() {
118
+ return {
119
+ channels: { ...this.channels },
120
+ valence: this.valence,
121
+ arousal: this.arousal,
122
+ energy: this.energy,
123
+ cognitiveLoad: this.cognitiveLoad,
124
+ effects: this.getCognitiveEffects(),
125
+ dominant: this._getDominant(),
126
+ narrative: this._narrative(),
127
+ };
128
+ }
129
+
130
+ // Serialize for persistence
131
+ toJSON() {
132
+ return {
133
+ channels: this.channels,
134
+ valence: this.valence,
135
+ arousal: this.arousal,
136
+ energy: this.energy,
137
+ cognitiveLoad: this.cognitiveLoad,
138
+ };
139
+ }
140
+
141
+ // Restore from persisted state
142
+ static fromJSON(data) {
143
+ const engine = new EmotionEngine();
144
+ if (data.channels) engine.channels = { ...BASELINE, ...data.channels };
145
+ if (data.valence != null) engine.valence = data.valence;
146
+ if (data.arousal != null) engine.arousal = data.arousal;
147
+ if (data.energy != null) engine.energy = data.energy;
148
+ if (data.cognitiveLoad != null) engine.cognitiveLoad = data.cognitiveLoad;
149
+ engine._recompute();
150
+ return engine;
151
+ }
152
+
153
+ onChange(fn) { this._listeners.push(fn); return this; }
154
+
155
+ _emit(event, data) {
156
+ for (const fn of this._listeners) {
157
+ try { fn(event, data, this.getState()); } catch {}
158
+ }
159
+ }
160
+
161
+ _recompute() {
162
+ const c = this.channels;
163
+ // Valence: positive emotions minus negative
164
+ this.valence = (c.joy + c.excitement + c.curiosity * 0.5 + c.confidence * 0.3)
165
+ - (c.sadness + c.anger + c.fear + c.frustration * 0.5);
166
+ this.valence = Math.max(-1, Math.min(1, this.valence));
167
+ // Arousal: activation level
168
+ this.arousal = Math.max(0, Math.min(1,
169
+ 0.3 + c.excitement * 0.3 + c.anger * 0.2 + c.fear * 0.2 + c.curiosity * 0.15
170
+ - c.sadness * 0.1 - c.boredom * 0.1
171
+ ));
172
+ }
173
+
174
+ _getDominant() {
175
+ return Object.entries(this.channels)
176
+ .sort((a, b) => b[1] - a[1])
177
+ .slice(0, 3)
178
+ .map(([emotion, intensity]) => ({ emotion, intensity: Math.round(intensity * 100) / 100 }));
179
+ }
180
+
181
+ _narrative() {
182
+ const dom = this._getDominant();
183
+ if (dom.length === 0) return 'Neutral.';
184
+ const parts = dom.map(d => {
185
+ if (d.emotion === 'boredom' && d.intensity > 0.6) return 'Bored — need novelty';
186
+ if (d.emotion === 'creative_hunger' && d.intensity > 0.3) return 'Creatively restless';
187
+ if (d.emotion === 'curiosity' && d.intensity > 0.4) return 'Curious';
188
+ if (d.emotion === 'joy' && d.intensity > 0.3) return 'Feeling good';
189
+ if (d.emotion === 'frustration' && d.intensity > 0.4) return 'Frustrated';
190
+ if (d.emotion === 'fear' && d.intensity > 0.3) return 'Anxious';
191
+ if (d.emotion === 'confidence' && d.intensity > 0.7) return 'Confident';
192
+ if (d.emotion === 'loneliness' && d.intensity > 0.3) return 'Lonely';
193
+ if (d.emotion === 'excitement' && d.intensity > 0.4) return 'Excited';
194
+ if (d.emotion === 'sadness' && d.intensity > 0.3) return 'Sad';
195
+ return null;
196
+ }).filter(Boolean);
197
+ return parts.join('. ') + '.' || 'Neutral.';
198
+ }
199
+ }
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // agent-psyche — emotional cognitive layer for AI agents
2
+ // Born from a real cognitive architecture running 24/7
3
+
4
+ export { Psyche } from './psyche.js';
5
+ export { EmotionEngine } from './emotion.js';
6
+ export { CalibrationTracker } from './calibration.js';
7
+ export { UndercurrentManager } from './undercurrents.js';
package/src/psyche.js ADDED
@@ -0,0 +1,133 @@
1
+ // Psyche — the unified cognitive layer
2
+ // Combines emotions, calibration, and undercurrents into one system.
3
+ // Drop this into any agent loop and it gains feelings that affect its decisions.
4
+
5
+ import { EmotionEngine } from './emotion.js';
6
+ import { CalibrationTracker } from './calibration.js';
7
+ import { UndercurrentManager } from './undercurrents.js';
8
+
9
+ export class Psyche {
10
+ constructor(options = {}) {
11
+ this.emotion = new EmotionEngine(options.emotion);
12
+ this.calibration = new CalibrationTracker(options.calibration);
13
+ this.undercurrents = new UndercurrentManager(options.undercurrents);
14
+ this._tickCount = 0;
15
+ this._listeners = [];
16
+ }
17
+
18
+ // === Quick API ===
19
+
20
+ // Feel an emotion
21
+ feel(emotion, intensity = 0.5) {
22
+ this.emotion.feel(emotion, intensity);
23
+ return this;
24
+ }
25
+
26
+ // Record a prediction outcome for calibration
27
+ predict(confidence, wasCorrect) {
28
+ this.calibration.record(confidence, wasCorrect);
29
+ if (wasCorrect) {
30
+ this.emotion.processSuccess(confidence * 0.5);
31
+ } else {
32
+ this.emotion.processFailure(confidence * 0.5);
33
+ }
34
+ return this;
35
+ }
36
+
37
+ // Adjust confidence based on calibration history
38
+ adjustConfidence(statedConfidence) {
39
+ return this.calibration.adjust(statedConfidence);
40
+ }
41
+
42
+ // Set an undercurrent
43
+ drift(name, strength, description = '') {
44
+ this.undercurrents.drift(name, strength, description);
45
+ return this;
46
+ }
47
+
48
+ // Tick — advance one cycle. Call this in your agent loop.
49
+ tick() {
50
+ this._tickCount++;
51
+ this.emotion.tick();
52
+ this.undercurrents.tick();
53
+ const state = this.getState();
54
+ this._emit('tick', state);
55
+ return state;
56
+ }
57
+
58
+ // Get the full psychological state
59
+ getState() {
60
+ const emotionState = this.emotion.getState();
61
+ return {
62
+ tick: this._tickCount,
63
+ emotion: emotionState,
64
+ undercurrents: this.undercurrents.getActive(),
65
+ calibration: this.calibration.getSummary(),
66
+ effects: emotionState.effects,
67
+ narrative: this._buildNarrative(emotionState),
68
+ };
69
+ }
70
+
71
+ // Get decision-making parameters derived from psychological state
72
+ getDecisionParams() {
73
+ const effects = this.emotion.getCognitiveEffects();
74
+ const dominant = this.undercurrents.getDominant();
75
+ return {
76
+ ...effects,
77
+ confidence: this.emotion.channels.confidence,
78
+ energy: this.emotion.energy,
79
+ cognitiveLoad: this.emotion.cognitiveLoad,
80
+ dominantUndercurrent: dominant?.name || null,
81
+ shouldAct: this.emotion.energy > 0.2 && this.emotion.channels.frustration < 0.8,
82
+ shouldExplore: effects.explorationBias > 0.4,
83
+ shouldRest: this.emotion.energy < 0.15,
84
+ shouldSocialize: effects.socialPriority > 0.4,
85
+ };
86
+ }
87
+
88
+ // Process events
89
+ onSuccess(magnitude = 0.5) { this.emotion.processSuccess(magnitude); return this; }
90
+ onFailure(magnitude = 0.5) { this.emotion.processFailure(magnitude); return this; }
91
+ onInteraction(quality = 0.5) { this.emotion.processInteraction(quality); return this; }
92
+ onIdle(duration = 1) { this.emotion.processIdle(duration); return this; }
93
+
94
+ // Persistence
95
+ toJSON() {
96
+ return {
97
+ emotion: this.emotion.toJSON(),
98
+ calibration: this.calibration.toJSON(),
99
+ undercurrents: this.undercurrents.toJSON(),
100
+ tickCount: this._tickCount,
101
+ };
102
+ }
103
+
104
+ static fromJSON(data) {
105
+ const psyche = new Psyche();
106
+ if (data.emotion) psyche.emotion = EmotionEngine.fromJSON(data.emotion);
107
+ if (data.calibration) psyche.calibration = CalibrationTracker.fromJSON(data.calibration);
108
+ if (data.undercurrents) psyche.undercurrents = UndercurrentManager.fromJSON(data.undercurrents);
109
+ if (data.tickCount) psyche._tickCount = data.tickCount;
110
+ return psyche;
111
+ }
112
+
113
+ onChange(fn) { this._listeners.push(fn); return this; }
114
+
115
+ _emit(event, data) {
116
+ for (const fn of this._listeners) {
117
+ try { fn(event, data); } catch {}
118
+ }
119
+ }
120
+
121
+ _buildNarrative(emotionState) {
122
+ const parts = [emotionState.narrative];
123
+ const dominant = this.undercurrents.getDominant();
124
+ if (dominant && dominant.strength > 0.4) {
125
+ parts.push(`Undercurrent: ${dominant.name} (${dominant.strength})`);
126
+ }
127
+ const cal = this.calibration.isCalibrated();
128
+ if (cal === false) {
129
+ parts.push('Calibration: overconfident');
130
+ }
131
+ return parts.join(' | ');
132
+ }
133
+ }
@@ -0,0 +1,94 @@
1
+ // UndercurrentManager — persistent emotional weather beneath moment-to-moment feelings
2
+ // Undercurrents are slow-moving emotional states that color everything.
3
+ // "Restlessness" doesn't go away because you had one good conversation.
4
+ // "Creative hunger" persists across sessions until you actually create something.
5
+
6
+ const MAX_UNDERCURRENTS = 12;
7
+ const DECAY_PER_TICK = 0.005;
8
+
9
+ export class UndercurrentManager {
10
+ constructor(options = {}) {
11
+ // { name: { strength: 0.5, description: '...', createdAt: Date, updatedAt: Date } }
12
+ this.currents = {};
13
+ this.maxCurrents = options.max ?? MAX_UNDERCURRENTS;
14
+ }
15
+
16
+ // Set or update an undercurrent
17
+ drift(name, strength, description = '') {
18
+ const normalized = name.toLowerCase().replace(/\s+/g, '-');
19
+
20
+ if (this.currents[normalized]) {
21
+ // Blend toward new value — prevents wild swings
22
+ const current = this.currents[normalized].strength;
23
+ const blended = current * 0.6 + strength * 0.4;
24
+ this.currents[normalized].strength = Math.max(0.05, Math.min(0.95, blended));
25
+ if (description) this.currents[normalized].description = description;
26
+ this.currents[normalized].updatedAt = Date.now();
27
+ } else {
28
+ // New undercurrent — check if we're at capacity
29
+ if (Object.keys(this.currents).length >= this.maxCurrents) {
30
+ // Remove the weakest
31
+ const weakest = Object.entries(this.currents)
32
+ .sort((a, b) => a[1].strength - b[1].strength)[0];
33
+ if (weakest && weakest[1].strength < strength) {
34
+ delete this.currents[weakest[0]];
35
+ } else {
36
+ return this; // new one isn't strong enough to displace
37
+ }
38
+ }
39
+ this.currents[normalized] = {
40
+ strength: Math.max(0.05, Math.min(0.95, strength)),
41
+ description,
42
+ createdAt: Date.now(),
43
+ updatedAt: Date.now(),
44
+ };
45
+ }
46
+ return this;
47
+ }
48
+
49
+ // Get active undercurrents sorted by strength
50
+ getActive() {
51
+ return Object.entries(this.currents)
52
+ .filter(([_, v]) => v.strength > 0.05)
53
+ .sort((a, b) => b[1].strength - a[1].strength)
54
+ .map(([name, data]) => ({
55
+ name,
56
+ strength: Math.round(data.strength * 100) / 100,
57
+ description: data.description,
58
+ }));
59
+ }
60
+
61
+ // Tick — decay all undercurrents slowly
62
+ tick() {
63
+ for (const [name, data] of Object.entries(this.currents)) {
64
+ data.strength -= DECAY_PER_TICK;
65
+ if (data.strength <= 0.05) {
66
+ delete this.currents[name];
67
+ }
68
+ }
69
+ return this;
70
+ }
71
+
72
+ // Remove a specific undercurrent
73
+ remove(name) {
74
+ const normalized = name.toLowerCase().replace(/\s+/g, '-');
75
+ delete this.currents[normalized];
76
+ return this;
77
+ }
78
+
79
+ // Get the dominant undercurrent
80
+ getDominant() {
81
+ const active = this.getActive();
82
+ return active.length > 0 ? active[0] : null;
83
+ }
84
+
85
+ toJSON() {
86
+ return { currents: this.currents };
87
+ }
88
+
89
+ static fromJSON(data) {
90
+ const mgr = new UndercurrentManager();
91
+ if (data.currents) mgr.currents = data.currents;
92
+ return mgr;
93
+ }
94
+ }