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 +21 -0
- package/README.md +160 -0
- package/package.json +41 -0
- package/src/calibration.js +125 -0
- package/src/cli.js +58 -0
- package/src/emotion.js +199 -0
- package/src/index.js +7 -0
- package/src/psyche.js +133 -0
- package/src/undercurrents.js +94 -0
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
|
+
}
|