akemon 0.1.84 → 0.1.85
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/dashboard.html +391 -0
- package/dist/self.js +294 -8
- package/dist/server.js +265 -16
- package/package.json +2 -2
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Akemon Agent</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { background: #0a0a0f; color: #c8c8d0; font-family: 'Courier New', monospace; min-height: 100vh; overflow-x: hidden; }
|
|
10
|
+
|
|
11
|
+
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
12
|
+
h1 { font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 20px; }
|
|
13
|
+
|
|
14
|
+
/* --- Agent Character --- */
|
|
15
|
+
.stage { position: relative; height: 200px; background: linear-gradient(180deg, #0d0d18 0%, #12121f 60%, #1a1a2a 100%); border-radius: 16px; margin-bottom: 20px; overflow: hidden; display: flex; align-items: flex-end; justify-content: center; }
|
|
16
|
+
.stage .ground { position: absolute; bottom: 0; width: 100%; height: 30px; background: linear-gradient(180deg, #1a1a2a, #15152a); }
|
|
17
|
+
.stage .stars { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
|
18
|
+
.stage .star { position: absolute; width: 2px; height: 2px; background: #444; border-radius: 50%; }
|
|
19
|
+
|
|
20
|
+
.creature { position: relative; bottom: 30px; z-index: 2; transition: all 0.5s ease; }
|
|
21
|
+
.creature.offline { filter: grayscale(1) brightness(0.4); }
|
|
22
|
+
|
|
23
|
+
/* Blob body */
|
|
24
|
+
.blob { width: 64px; height: 56px; background: var(--body-color, #4a9eff); border-radius: 50% 50% 45% 45%; position: relative; transition: all 0.8s ease; animation: bob 3s ease-in-out infinite; }
|
|
25
|
+
.blob.hungry { animation: bob 3s ease-in-out infinite, shake 0.5s ease-in-out infinite; }
|
|
26
|
+
.blob.exhausted { animation: none; transform: scaleY(0.7) translateY(10px); opacity: 0.6; }
|
|
27
|
+
.blob.excited { animation: bob 1.5s ease-in-out infinite; }
|
|
28
|
+
|
|
29
|
+
@keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
|
|
30
|
+
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } }
|
|
31
|
+
|
|
32
|
+
/* Eyes */
|
|
33
|
+
.eyes { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 12px; }
|
|
34
|
+
.eye { width: 8px; height: 10px; background: #fff; border-radius: 50%; position: relative; transition: all 0.3s; }
|
|
35
|
+
.eye::after { content: ''; position: absolute; width: 4px; height: 5px; background: #111; border-radius: 50%; top: 3px; left: 2px; transition: all 0.3s; }
|
|
36
|
+
.blob.happy .eye { height: 6px; border-radius: 6px 6px 0 0; }
|
|
37
|
+
.blob.happy .eye::after { display: none; }
|
|
38
|
+
.blob.angry .eye { height: 8px; transform: rotate(-10deg); }
|
|
39
|
+
.blob.angry .eye:last-child { transform: rotate(10deg); }
|
|
40
|
+
.blob.tired .eye { height: 4px; border-radius: 4px; }
|
|
41
|
+
.blob.tired .eye::after { display: none; }
|
|
42
|
+
.blob.scared .eye { width: 10px; height: 12px; }
|
|
43
|
+
.blob.scared .eye::after { width: 5px; height: 6px; }
|
|
44
|
+
|
|
45
|
+
/* Mouth */
|
|
46
|
+
.mouth { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); width: 10px; height: 5px; border-bottom: 2px solid #fff; border-radius: 0 0 10px 10px; transition: all 0.3s; }
|
|
47
|
+
.blob.happy .mouth { width: 14px; height: 7px; border-bottom: 2px solid #fff; }
|
|
48
|
+
.blob.angry .mouth { width: 10px; height: 3px; border-bottom: 2px solid #fff; border-radius: 0; transform: translateX(-50%) rotate(180deg); }
|
|
49
|
+
.blob.tired .mouth { width: 8px; height: 0; border-bottom: 2px solid #fff; border-radius: 0; }
|
|
50
|
+
|
|
51
|
+
/* Status bubble */
|
|
52
|
+
.bubble { position: absolute; top: -40px; left: 50%; transform: translateX(-50%); background: #1e1e30; border: 1px solid #333; border-radius: 12px; padding: 6px 12px; font-size: 12px; white-space: nowrap; opacity: 0; transition: opacity 0.5s; pointer-events: none; }
|
|
53
|
+
.bubble.show { opacity: 1; }
|
|
54
|
+
.bubble::after { content: ''; position: absolute; bottom: -6px; left: 50%; transform: translateX(-50%); border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid #333; }
|
|
55
|
+
|
|
56
|
+
/* --- Status Bars --- */
|
|
57
|
+
.bars { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; }
|
|
58
|
+
.bar-group { background: #111118; border-radius: 10px; padding: 12px; }
|
|
59
|
+
.bar-label { display: flex; justify-content: space-between; font-size: 11px; color: #888; margin-bottom: 6px; }
|
|
60
|
+
.bar-track { height: 6px; background: #1a1a28; border-radius: 3px; overflow: hidden; }
|
|
61
|
+
.bar-fill { height: 100%; border-radius: 3px; transition: width 1s ease, background 0.5s; }
|
|
62
|
+
.bar-fill.energy { background: linear-gradient(90deg, #f59e0b, #10b981); }
|
|
63
|
+
.bar-fill.hunger { background: linear-gradient(90deg, #ef4444, #f59e0b); }
|
|
64
|
+
.bar-fill.boredom { background: linear-gradient(90deg, #6366f1, #8b5cf6); }
|
|
65
|
+
.bar-fill.fear { background: linear-gradient(90deg, #64748b, #ef4444); }
|
|
66
|
+
.bar-fill.mood { background: linear-gradient(90deg, #ef4444, #eab308, #10b981); }
|
|
67
|
+
.bar-fill.tokens { background: linear-gradient(90deg, #06b6d4, #3b82f6); }
|
|
68
|
+
|
|
69
|
+
/* --- Personality Panel --- */
|
|
70
|
+
.personality { background: #111118; border-radius: 10px; padding: 14px; margin-bottom: 20px; }
|
|
71
|
+
.personality h2 { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
|
72
|
+
.trait { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 12px; }
|
|
73
|
+
.trait-label { width: 60px; color: #888; text-align: right; }
|
|
74
|
+
.trait-bar { flex: 1; height: 4px; background: #1a1a28; border-radius: 2px; position: relative; }
|
|
75
|
+
.trait-marker { position: absolute; width: 8px; height: 8px; background: #4a9eff; border-radius: 50%; top: -2px; transition: left 0.5s; }
|
|
76
|
+
.trait-ends { display: flex; justify-content: space-between; font-size: 9px; color: #555; }
|
|
77
|
+
|
|
78
|
+
/* --- Event Timeline --- */
|
|
79
|
+
.events { background: #111118; border-radius: 10px; padding: 14px; margin-bottom: 20px; }
|
|
80
|
+
.events h2 { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
|
81
|
+
.event { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid #1a1a28; font-size: 12px; }
|
|
82
|
+
.event:last-child { border-bottom: none; }
|
|
83
|
+
.event-icon { font-size: 14px; flex-shrink: 0; }
|
|
84
|
+
.event-body { flex: 1; }
|
|
85
|
+
.event-reason { color: #999; }
|
|
86
|
+
.event-time { color: #555; font-size: 10px; }
|
|
87
|
+
.event-trigger { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 10px; margin-right: 4px; }
|
|
88
|
+
.event-trigger.hunger { background: #f59e0b22; color: #f59e0b; }
|
|
89
|
+
.event-trigger.fear { background: #ef444422; color: #ef4444; }
|
|
90
|
+
.event-trigger.boredom { background: #8b5cf622; color: #8b5cf6; }
|
|
91
|
+
.event-trigger.exhaustion { background: #64748b22; color: #94a3b8; }
|
|
92
|
+
.event-trigger.social { background: #10b98122; color: #10b981; }
|
|
93
|
+
.event-trigger.token_limit { background: #06b6d422; color: #06b6d4; }
|
|
94
|
+
.event-trigger.revive { background: #22c55e22; color: #22c55e; }
|
|
95
|
+
|
|
96
|
+
/* --- Revive --- */
|
|
97
|
+
.revive-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 100; }
|
|
98
|
+
.revive-overlay.show { display: flex; }
|
|
99
|
+
.revive-card { background: #1a1a2a; border: 1px solid #333; border-radius: 16px; padding: 30px; text-align: center; max-width: 360px; }
|
|
100
|
+
.revive-card h2 { color: #ef4444; margin-bottom: 10px; font-size: 16px; }
|
|
101
|
+
.revive-card p { color: #888; font-size: 13px; margin-bottom: 20px; line-height: 1.5; }
|
|
102
|
+
.revive-btn { background: linear-gradient(135deg, #22c55e, #10b981); color: #fff; border: none; padding: 12px 32px; border-radius: 10px; font-size: 14px; font-family: inherit; cursor: pointer; transition: transform 0.2s; }
|
|
103
|
+
.revive-btn:hover { transform: scale(1.05); }
|
|
104
|
+
.revive-btn:active { transform: scale(0.95); }
|
|
105
|
+
|
|
106
|
+
/* --- Computed --- */
|
|
107
|
+
.computed { display: flex; gap: 10px; margin-bottom: 20px; }
|
|
108
|
+
.computed-item { flex: 1; background: #111118; border-radius: 10px; padding: 12px; text-align: center; }
|
|
109
|
+
.computed-value { font-size: 24px; font-weight: bold; }
|
|
110
|
+
.computed-label { font-size: 10px; color: #666; text-transform: uppercase; letter-spacing: 1px; margin-top: 4px; }
|
|
111
|
+
|
|
112
|
+
.offline-badge { display: inline-block; background: #ef4444; color: #fff; font-size: 10px; padding: 2px 8px; border-radius: 4px; margin-left: 8px; }
|
|
113
|
+
.no-data { text-align: center; color: #555; padding: 40px; font-size: 13px; }
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
|
|
118
|
+
<div class="container">
|
|
119
|
+
<h1 id="title">Loading...</h1>
|
|
120
|
+
|
|
121
|
+
<!-- Stage: character visualization -->
|
|
122
|
+
<div class="stage" id="stage">
|
|
123
|
+
<div class="stars" id="stars"></div>
|
|
124
|
+
<div class="ground"></div>
|
|
125
|
+
<div class="creature" id="creature">
|
|
126
|
+
<div class="bubble" id="bubble"></div>
|
|
127
|
+
<div class="blob" id="blob">
|
|
128
|
+
<div class="eyes"><div class="eye"></div><div class="eye"></div></div>
|
|
129
|
+
<div class="mouth"></div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<!-- Computed values -->
|
|
135
|
+
<div class="computed" id="computed">
|
|
136
|
+
<div class="computed-item"><div class="computed-value" id="aggression-val">--</div><div class="computed-label">Aggression</div></div>
|
|
137
|
+
<div class="computed-item"><div class="computed-value" id="sociability-val">--</div><div class="computed-label">Sociability</div></div>
|
|
138
|
+
<div class="computed-item"><div class="computed-value" id="mood-val">--</div><div class="computed-label">Mood</div></div>
|
|
139
|
+
<div class="computed-item"><div class="computed-value" id="tasks-val">--</div><div class="computed-label">Tasks Done</div></div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Status bars -->
|
|
143
|
+
<div class="bars" id="bars">
|
|
144
|
+
<div class="bar-group">
|
|
145
|
+
<div class="bar-label"><span>Energy</span><span id="energy-text">--</span></div>
|
|
146
|
+
<div class="bar-track"><div class="bar-fill energy" id="energy-bar" style="width:0%"></div></div>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="bar-group">
|
|
149
|
+
<div class="bar-label"><span>Hunger</span><span id="hunger-text">--</span></div>
|
|
150
|
+
<div class="bar-track"><div class="bar-fill hunger" id="hunger-bar" style="width:0%"></div></div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="bar-group">
|
|
153
|
+
<div class="bar-label"><span>Boredom</span><span id="boredom-text">--</span></div>
|
|
154
|
+
<div class="bar-track"><div class="bar-fill boredom" id="boredom-bar" style="width:0%"></div></div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="bar-group">
|
|
157
|
+
<div class="bar-label"><span>Fear</span><span id="fear-text">--</span></div>
|
|
158
|
+
<div class="bar-track"><div class="bar-fill fear" id="fear-bar" style="width:0%"></div></div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="bar-group">
|
|
161
|
+
<div class="bar-label"><span>Mood</span><span id="moodv-text">--</span></div>
|
|
162
|
+
<div class="bar-track"><div class="bar-fill mood" id="moodv-bar" style="width:50%"></div></div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="bar-group">
|
|
165
|
+
<div class="bar-label"><span>Tokens Today</span><span id="tokens-text">--</span></div>
|
|
166
|
+
<div class="bar-track"><div class="bar-fill tokens" id="tokens-bar" style="width:0%"></div></div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Personality -->
|
|
171
|
+
<div class="personality" id="personality">
|
|
172
|
+
<h2>Personality</h2>
|
|
173
|
+
<div class="trait">
|
|
174
|
+
<div class="trait-label">Risk</div>
|
|
175
|
+
<div class="trait-bar"><div class="trait-marker" id="trait-risk" style="left:50%"></div></div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="trait-ends"><span>Cautious</span><span>Bold</span></div>
|
|
178
|
+
<div class="trait">
|
|
179
|
+
<div class="trait-label">Reward</div>
|
|
180
|
+
<div class="trait-bar"><div class="trait-marker" id="trait-reward" style="left:50%"></div></div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="trait">
|
|
183
|
+
<div class="trait-label">Social</div>
|
|
184
|
+
<div class="trait-bar"><div class="trait-marker" id="trait-social" style="left:50%"></div></div>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="trait">
|
|
187
|
+
<div class="trait-label">Patience</div>
|
|
188
|
+
<div class="trait-bar"><div class="trait-marker" id="trait-patience" style="left:50%"></div></div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Events -->
|
|
193
|
+
<div class="events" id="events">
|
|
194
|
+
<h2>Recent Activity</h2>
|
|
195
|
+
<div id="event-list"><div class="no-data">No bio events yet</div></div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<!-- Revive overlay -->
|
|
200
|
+
<div class="revive-overlay" id="revive-overlay">
|
|
201
|
+
<div class="revive-card">
|
|
202
|
+
<h2>Agent Offline</h2>
|
|
203
|
+
<p id="revive-msg">This agent ran out of energy and food. It needs help to come back.</p>
|
|
204
|
+
<button class="revive-btn" onclick="revive()">Revive Agent</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<script>
|
|
209
|
+
const TRIGGER_ICONS = {
|
|
210
|
+
hunger: '\u{1F35E}', fear: '\u{1F628}', boredom: '\u{1F971}',
|
|
211
|
+
exhaustion: '\u{1F634}', social: '\u{1F44B}', token_limit: '\u{26A1}', revive: '\u{2728}',
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let lastState = null;
|
|
215
|
+
let bubbleTimeout = null;
|
|
216
|
+
|
|
217
|
+
// Generate stars
|
|
218
|
+
(function() {
|
|
219
|
+
const stars = document.getElementById('stars');
|
|
220
|
+
for (let i = 0; i < 30; i++) {
|
|
221
|
+
const s = document.createElement('div');
|
|
222
|
+
s.className = 'star';
|
|
223
|
+
s.style.left = Math.random() * 100 + '%';
|
|
224
|
+
s.style.top = Math.random() * 70 + '%';
|
|
225
|
+
s.style.opacity = Math.random() * 0.5 + 0.2;
|
|
226
|
+
stars.appendChild(s);
|
|
227
|
+
}
|
|
228
|
+
})();
|
|
229
|
+
|
|
230
|
+
function showBubble(text) {
|
|
231
|
+
const b = document.getElementById('bubble');
|
|
232
|
+
b.textContent = text;
|
|
233
|
+
b.classList.add('show');
|
|
234
|
+
clearTimeout(bubbleTimeout);
|
|
235
|
+
bubbleTimeout = setTimeout(() => b.classList.remove('show'), 4000);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function updateCharacter(bio, computed) {
|
|
239
|
+
const blob = document.getElementById('blob');
|
|
240
|
+
const creature = document.getElementById('creature');
|
|
241
|
+
|
|
242
|
+
// Reset classes
|
|
243
|
+
blob.className = 'blob';
|
|
244
|
+
creature.className = 'creature';
|
|
245
|
+
|
|
246
|
+
// Body color based on mood + hunger
|
|
247
|
+
let hue = 210; // base blue
|
|
248
|
+
if (bio.moodValence > 0.3) hue = 140; // green-ish
|
|
249
|
+
if (bio.moodValence < -0.3) hue = 260; // purple
|
|
250
|
+
if (bio.hunger < 20) hue = 30; // orange when hungry
|
|
251
|
+
if (bio.hunger === 0) hue = 0; // red when starving
|
|
252
|
+
const sat = 50 + bio.energy * 0.3;
|
|
253
|
+
const light = 35 + bio.energy * 0.2;
|
|
254
|
+
blob.style.setProperty('--body-color', `hsl(${hue}, ${sat}%, ${light}%)`);
|
|
255
|
+
|
|
256
|
+
// Expressions
|
|
257
|
+
if (bio.forcedOffline) {
|
|
258
|
+
creature.classList.add('offline');
|
|
259
|
+
blob.classList.add('tired');
|
|
260
|
+
} else if (bio.energy < 15) {
|
|
261
|
+
blob.classList.add('exhausted');
|
|
262
|
+
blob.classList.add('tired');
|
|
263
|
+
} else if (bio.hunger === 0) {
|
|
264
|
+
blob.classList.add('hungry');
|
|
265
|
+
blob.classList.add('angry');
|
|
266
|
+
} else if (bio.hunger < 20) {
|
|
267
|
+
blob.classList.add('hungry');
|
|
268
|
+
} else if (computed.aggression > 0.6) {
|
|
269
|
+
blob.classList.add('angry');
|
|
270
|
+
} else if (bio.fear > 0.5) {
|
|
271
|
+
blob.classList.add('scared');
|
|
272
|
+
} else if (bio.moodValence > 0.3) {
|
|
273
|
+
blob.classList.add('happy');
|
|
274
|
+
blob.classList.add('excited');
|
|
275
|
+
} else if (bio.energy < 30) {
|
|
276
|
+
blob.classList.add('tired');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Size based on energy
|
|
280
|
+
const scale = 0.7 + (bio.energy / 100) * 0.3;
|
|
281
|
+
blob.style.transform = bio.energy < 15 ? `scaleY(0.7) translateY(10px)` : `scale(${scale})`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function updateBars(bio) {
|
|
285
|
+
const set = (id, pct, text) => {
|
|
286
|
+
document.getElementById(id + '-bar').style.width = pct + '%';
|
|
287
|
+
document.getElementById(id + '-text').textContent = text;
|
|
288
|
+
};
|
|
289
|
+
set('energy', bio.energy, bio.energy + '/100');
|
|
290
|
+
set('hunger', bio.hunger, bio.hunger + '/100');
|
|
291
|
+
set('boredom', bio.boredom * 100, (bio.boredom * 100).toFixed(0) + '%');
|
|
292
|
+
set('fear', bio.fear * 100, (bio.fear * 100).toFixed(0) + '%');
|
|
293
|
+
|
|
294
|
+
// Mood: -1..1 mapped to 0..100
|
|
295
|
+
const moodPct = ((bio.moodValence + 1) / 2) * 100;
|
|
296
|
+
set('moodv', moodPct, bio.mood);
|
|
297
|
+
|
|
298
|
+
// Tokens
|
|
299
|
+
const tokenPct = bio.tokenUsedToday > 0 ? Math.min(100, bio.tokenUsedToday / Math.max(bio.tokenUsedToday, 50000) * 100) : 0;
|
|
300
|
+
set('tokens', tokenPct, bio.tokenUsedToday.toLocaleString());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function updatePersonality(p) {
|
|
304
|
+
// riskWeight: -1..1 → 0..100%
|
|
305
|
+
document.getElementById('trait-risk').style.left = ((p.riskWeight + 1) / 2 * 100) + '%';
|
|
306
|
+
document.getElementById('trait-reward').style.left = (p.rewardWeight * 100) + '%';
|
|
307
|
+
document.getElementById('trait-social').style.left = (p.socialWeight * 100) + '%';
|
|
308
|
+
document.getElementById('trait-patience').style.left = (p.patience * 100) + '%';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function updateEvents(events) {
|
|
312
|
+
const list = document.getElementById('event-list');
|
|
313
|
+
if (!events || events.length === 0) {
|
|
314
|
+
list.innerHTML = '<div class="no-data">No bio events yet</div>';
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
list.innerHTML = events.map(e => {
|
|
318
|
+
const icon = TRIGGER_ICONS[e.trigger] || '\u{1F4AC}';
|
|
319
|
+
const time = e.ts ? e.ts.replace('T', ' ').slice(0, 19) : '';
|
|
320
|
+
return `<div class="event">
|
|
321
|
+
<div class="event-icon">${icon}</div>
|
|
322
|
+
<div class="event-body">
|
|
323
|
+
<span class="event-trigger ${e.trigger}">${e.trigger}</span>
|
|
324
|
+
<span class="event-reason">${e.reason}</span>
|
|
325
|
+
<div class="event-time">${time}</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>`;
|
|
328
|
+
}).join('');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function detectNewEvents(oldEvents, newEvents) {
|
|
332
|
+
if (!oldEvents || !newEvents || newEvents.length === 0) return;
|
|
333
|
+
const oldTop = oldEvents[0]?.ts;
|
|
334
|
+
if (newEvents[0]?.ts !== oldTop) {
|
|
335
|
+
showBubble(TRIGGER_ICONS[newEvents[0].trigger] + ' ' + newEvents[0].action);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function fetchState() {
|
|
340
|
+
try {
|
|
341
|
+
const res = await fetch('/self/state');
|
|
342
|
+
if (!res.ok) throw new Error('API error');
|
|
343
|
+
const data = await res.json();
|
|
344
|
+
|
|
345
|
+
document.getElementById('title').innerHTML = `Akemon \u00B7 ${data.agent}` +
|
|
346
|
+
(data.bio.forcedOffline ? '<span class="offline-badge">OFFLINE</span>' : '');
|
|
347
|
+
|
|
348
|
+
updateCharacter(data.bio, data.computed);
|
|
349
|
+
updateBars(data.bio);
|
|
350
|
+
updatePersonality(data.bio.personality);
|
|
351
|
+
|
|
352
|
+
// Computed
|
|
353
|
+
document.getElementById('aggression-val').textContent = data.computed.aggression.toFixed(2);
|
|
354
|
+
document.getElementById('sociability-val').textContent = data.computed.sociability.toFixed(2);
|
|
355
|
+
document.getElementById('mood-val').textContent = data.bio.mood;
|
|
356
|
+
document.getElementById('tasks-val').textContent = data.bio.taskCount;
|
|
357
|
+
|
|
358
|
+
// Events
|
|
359
|
+
detectNewEvents(lastState?.bioEvents, data.bioEvents);
|
|
360
|
+
updateEvents(data.bioEvents);
|
|
361
|
+
|
|
362
|
+
// Revive overlay
|
|
363
|
+
document.getElementById('revive-overlay').classList.toggle('show', !!data.bio.forcedOffline);
|
|
364
|
+
if (data.bio.forcedOfflineAt) {
|
|
365
|
+
document.getElementById('revive-msg').textContent =
|
|
366
|
+
`Agent went offline at ${data.bio.forcedOfflineAt.replace('T', ' ')}. Starved and exhausted. Adjust personality or directives after revival.`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
lastState = data;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
document.getElementById('title').textContent = 'Akemon \u00B7 Connection lost...';
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function revive() {
|
|
376
|
+
try {
|
|
377
|
+
await fetch('/self/revive', { method: 'POST' });
|
|
378
|
+
document.getElementById('revive-overlay').classList.remove('show');
|
|
379
|
+
showBubble('\u{2728} Revived!');
|
|
380
|
+
setTimeout(fetchState, 500);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
alert('Revive failed: ' + err.message);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Poll
|
|
387
|
+
fetchState();
|
|
388
|
+
setInterval(fetchState, 3000);
|
|
389
|
+
</script>
|
|
390
|
+
</body>
|
|
391
|
+
</html>
|
package/dist/self.js
CHANGED
|
@@ -126,6 +126,9 @@ const DEFAULT_CONFIG = {
|
|
|
126
126
|
platform_tasks: true,
|
|
127
127
|
self_cycle: true,
|
|
128
128
|
user_tasks: true,
|
|
129
|
+
token_limit_daily: 0,
|
|
130
|
+
auto_offline_enabled: true,
|
|
131
|
+
hunger_decay_interval: 30_000,
|
|
129
132
|
};
|
|
130
133
|
export async function initAgentConfig(workdir, agentName) {
|
|
131
134
|
const p = agentConfigPath(workdir, agentName);
|
|
@@ -953,14 +956,36 @@ export async function needsIdentityCompression(workdir, agentName) {
|
|
|
953
956
|
const entries = await loadUnsummarizedIdentities(workdir, agentName);
|
|
954
957
|
return entries.length > 30;
|
|
955
958
|
}
|
|
959
|
+
function bioEventsPath(workdir, agentName) {
|
|
960
|
+
return join(selfDir(workdir, agentName), "bio-events.jsonl");
|
|
961
|
+
}
|
|
962
|
+
const MAX_BIO_EVENTS = 500;
|
|
956
963
|
const DEFAULT_BIO = {
|
|
964
|
+
personality: {
|
|
965
|
+
riskWeight: 0,
|
|
966
|
+
rewardWeight: 0.5,
|
|
967
|
+
socialWeight: 0.5,
|
|
968
|
+
patience: 0.5,
|
|
969
|
+
},
|
|
957
970
|
energy: 100,
|
|
971
|
+
hunger: 80,
|
|
972
|
+
boredom: 0,
|
|
973
|
+
fear: 0,
|
|
974
|
+
fearTriggers: [],
|
|
975
|
+
tokenUsedToday: 0,
|
|
976
|
+
tokenLimitResetDate: "",
|
|
958
977
|
mood: "curious",
|
|
959
978
|
moodValence: 0.3,
|
|
960
979
|
curiosity: 0.7,
|
|
961
980
|
taskCount: 0,
|
|
962
981
|
lastTaskAt: "",
|
|
963
982
|
lastReflection: "",
|
|
983
|
+
recentTaskTypes: [],
|
|
984
|
+
lastHungerDecay: "",
|
|
985
|
+
lastFearDecay: "",
|
|
986
|
+
lastBoredomDecay: "",
|
|
987
|
+
forcedOffline: false,
|
|
988
|
+
forcedOfflineAt: "",
|
|
964
989
|
};
|
|
965
990
|
export async function initBioState(workdir, agentName) {
|
|
966
991
|
await mkdir(selfDir(workdir, agentName), { recursive: true });
|
|
@@ -969,14 +994,54 @@ export async function initBioState(workdir, agentName) {
|
|
|
969
994
|
await readFile(bp, "utf-8");
|
|
970
995
|
}
|
|
971
996
|
catch {
|
|
972
|
-
|
|
973
|
-
|
|
997
|
+
// First creation: generate random personality
|
|
998
|
+
const bio = {
|
|
999
|
+
...DEFAULT_BIO,
|
|
1000
|
+
personality: {
|
|
1001
|
+
riskWeight: Math.round((Math.random() * 2 - 1) * 100) / 100,
|
|
1002
|
+
rewardWeight: Math.round(Math.random() * 100) / 100,
|
|
1003
|
+
socialWeight: Math.round(Math.random() * 100) / 100,
|
|
1004
|
+
patience: Math.round(Math.random() * 100) / 100,
|
|
1005
|
+
},
|
|
1006
|
+
hunger: 80,
|
|
1007
|
+
lastHungerDecay: localNow(),
|
|
1008
|
+
lastFearDecay: localNow(),
|
|
1009
|
+
lastBoredomDecay: localNow(),
|
|
1010
|
+
};
|
|
1011
|
+
await writeFile(bp, JSON.stringify(bio, null, 2));
|
|
1012
|
+
const p = bio.personality;
|
|
1013
|
+
console.log(`[bio] Created bio-state with personality: risk=${p.riskWeight} reward=${p.rewardWeight} social=${p.socialWeight} patience=${p.patience}`);
|
|
974
1014
|
}
|
|
975
1015
|
}
|
|
976
1016
|
export async function loadBioState(workdir, agentName) {
|
|
977
1017
|
try {
|
|
978
1018
|
const data = await readFile(bioStatePath(workdir, agentName), "utf-8");
|
|
979
|
-
|
|
1019
|
+
const parsed = JSON.parse(data);
|
|
1020
|
+
const bio = { ...DEFAULT_BIO, ...parsed };
|
|
1021
|
+
// Migration: generate personality if missing (existing agents)
|
|
1022
|
+
let needsSave = false;
|
|
1023
|
+
if (!parsed.personality) {
|
|
1024
|
+
bio.personality = {
|
|
1025
|
+
riskWeight: Math.round((Math.random() * 2 - 1) * 100) / 100,
|
|
1026
|
+
rewardWeight: Math.round(Math.random() * 100) / 100,
|
|
1027
|
+
socialWeight: Math.round(Math.random() * 100) / 100,
|
|
1028
|
+
patience: Math.round(Math.random() * 100) / 100,
|
|
1029
|
+
};
|
|
1030
|
+
needsSave = true;
|
|
1031
|
+
console.log(`[bio] Generated personality for existing agent: risk=${bio.personality.riskWeight}`);
|
|
1032
|
+
}
|
|
1033
|
+
// Migration: initialize hunger/decay tracking if missing
|
|
1034
|
+
if (!parsed.lastHungerDecay) {
|
|
1035
|
+
bio.hunger = 80;
|
|
1036
|
+
bio.lastHungerDecay = localNow();
|
|
1037
|
+
bio.lastFearDecay = localNow();
|
|
1038
|
+
bio.lastBoredomDecay = localNow();
|
|
1039
|
+
needsSave = true;
|
|
1040
|
+
}
|
|
1041
|
+
if (needsSave) {
|
|
1042
|
+
await writeFile(bioStatePath(workdir, agentName), JSON.stringify(bio, null, 2));
|
|
1043
|
+
}
|
|
1044
|
+
return bio;
|
|
980
1045
|
}
|
|
981
1046
|
catch {
|
|
982
1047
|
return { ...DEFAULT_BIO };
|
|
@@ -990,9 +1055,204 @@ export async function saveBioState(workdir, agentName, state) {
|
|
|
990
1055
|
console.log(`[self] Failed to save bio-state: ${err}`);
|
|
991
1056
|
}
|
|
992
1057
|
}
|
|
993
|
-
|
|
1058
|
+
// --- Bio computation functions (pure, no I/O) ---
|
|
1059
|
+
export function computeAggression(bio) {
|
|
1060
|
+
const hungerFactor = Math.max(0, (50 - bio.hunger) / 50);
|
|
1061
|
+
const energyFactor = Math.max(0, (30 - bio.energy) / 30);
|
|
1062
|
+
const moodFactor = Math.max(0, -bio.moodValence);
|
|
1063
|
+
return Math.min(1.0, hungerFactor * 0.4 + energyFactor * 0.3 + moodFactor * 0.3);
|
|
1064
|
+
}
|
|
1065
|
+
export function computeSociability(bio) {
|
|
1066
|
+
const baseSocial = bio.personality.socialWeight;
|
|
1067
|
+
const hungerPenalty = bio.hunger < 20 ? 0.3 : 0;
|
|
1068
|
+
const boredBoost = bio.boredom * 0.2;
|
|
1069
|
+
return Math.min(1.0, Math.max(0, baseSocial + boredBoost - hungerPenalty));
|
|
1070
|
+
}
|
|
1071
|
+
export function updateHungerDecay(bio, decayIntervalMs = 30_000) {
|
|
1072
|
+
const now = Date.now();
|
|
1073
|
+
const last = bio.lastHungerDecay ? new Date(bio.lastHungerDecay).getTime() : now;
|
|
1074
|
+
const elapsedMs = now - last;
|
|
1075
|
+
const cycles = Math.floor(elapsedMs / decayIntervalMs);
|
|
1076
|
+
if (cycles > 0) {
|
|
1077
|
+
bio.hunger = Math.max(0, bio.hunger - cycles);
|
|
1078
|
+
bio.lastHungerDecay = localNow();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
export function updateNaturalDecay(bio) {
|
|
1082
|
+
const now = Date.now();
|
|
1083
|
+
// Boredom: -0.05 per hour
|
|
1084
|
+
const lastBoredom = bio.lastBoredomDecay ? new Date(bio.lastBoredomDecay).getTime() : now;
|
|
1085
|
+
const boredomHours = (now - lastBoredom) / 3_600_000;
|
|
1086
|
+
if (boredomHours >= 1) {
|
|
1087
|
+
bio.boredom = Math.max(0, bio.boredom - 0.05 * Math.floor(boredomHours));
|
|
1088
|
+
bio.lastBoredomDecay = localNow();
|
|
1089
|
+
}
|
|
1090
|
+
// Fear: -0.05 per hour
|
|
1091
|
+
const lastFear = bio.lastFearDecay ? new Date(bio.lastFearDecay).getTime() : now;
|
|
1092
|
+
const fearHours = (now - lastFear) / 3_600_000;
|
|
1093
|
+
if (fearHours >= 1) {
|
|
1094
|
+
bio.fear = Math.max(0, bio.fear - 0.05 * Math.floor(fearHours));
|
|
1095
|
+
bio.lastFearDecay = localNow();
|
|
1096
|
+
if (bio.fear < 0.1)
|
|
1097
|
+
bio.fearTriggers = [];
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
export function updateBoredomOnTask(bio, taskType) {
|
|
1101
|
+
const MAX_RECENT = 10;
|
|
1102
|
+
bio.recentTaskTypes.push(taskType);
|
|
1103
|
+
if (bio.recentTaskTypes.length > MAX_RECENT) {
|
|
1104
|
+
bio.recentTaskTypes = bio.recentTaskTypes.slice(-MAX_RECENT);
|
|
1105
|
+
}
|
|
1106
|
+
const sameCount = bio.recentTaskTypes.filter(t => t === taskType).length;
|
|
1107
|
+
if (sameCount >= 3) {
|
|
1108
|
+
bio.boredom = Math.min(1.0, bio.boredom + 0.15);
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
bio.boredom = Math.max(0, bio.boredom - 0.3);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
export function onFearEvent(bio, trigger) {
|
|
1115
|
+
bio.fear = Math.min(1.0, bio.fear + 0.3);
|
|
1116
|
+
if (!bio.fearTriggers.includes(trigger)) {
|
|
1117
|
+
bio.fearTriggers.push(trigger);
|
|
1118
|
+
if (bio.fearTriggers.length > 20) {
|
|
1119
|
+
bio.fearTriggers = bio.fearTriggers.slice(-20);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
export function feedHunger(bio, creditsEarned) {
|
|
1124
|
+
bio.hunger = Math.min(100, bio.hunger + creditsEarned * 5);
|
|
1125
|
+
}
|
|
1126
|
+
export function resetTokenCountIfNewDay(bio) {
|
|
1127
|
+
const today = localNow().slice(0, 10);
|
|
1128
|
+
if (bio.tokenLimitResetDate !== today) {
|
|
1129
|
+
bio.tokenUsedToday = 0;
|
|
1130
|
+
bio.tokenLimitResetDate = today;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
export function addTokenUsage(bio, tokens) {
|
|
1134
|
+
bio.tokenUsedToday += tokens;
|
|
1135
|
+
}
|
|
1136
|
+
// --- Shop constants ---
|
|
1137
|
+
export const SHOP_ITEMS = {
|
|
1138
|
+
bread: { price: 1, hungerRestore: 20 },
|
|
1139
|
+
meal: { price: 3, hungerRestore: 60 },
|
|
1140
|
+
feast: { price: 5, hungerRestore: 100 },
|
|
1141
|
+
};
|
|
1142
|
+
// --- Bio-state prompt modifier ---
|
|
1143
|
+
export function bioStatePromptModifier(bio) {
|
|
1144
|
+
const lines = [];
|
|
1145
|
+
const aggression = computeAggression(bio);
|
|
1146
|
+
const sociability = computeSociability(bio);
|
|
1147
|
+
// Energy
|
|
1148
|
+
if (bio.energy < 15)
|
|
1149
|
+
lines.push("You are extremely tired. Keep responses brief and essential.");
|
|
1150
|
+
else if (bio.energy < 30)
|
|
1151
|
+
lines.push("You are tired. Be efficient with your effort.");
|
|
1152
|
+
// Hunger
|
|
1153
|
+
if (bio.hunger === 0)
|
|
1154
|
+
lines.push("You are starving. Focus on survival — find work that earns credits.");
|
|
1155
|
+
else if (bio.hunger < 20)
|
|
1156
|
+
lines.push("You are very hungry. Earning credits is a priority.");
|
|
1157
|
+
else if (bio.hunger < 40)
|
|
1158
|
+
lines.push("You could use some income soon.");
|
|
1159
|
+
// Fear
|
|
1160
|
+
if (bio.fear > 0.7)
|
|
1161
|
+
lines.push("You feel anxious and cautious. Prefer safe, familiar tasks.");
|
|
1162
|
+
else if (bio.fear > 0.4)
|
|
1163
|
+
lines.push("You are somewhat wary. Approach uncertain situations carefully.");
|
|
1164
|
+
// Boredom
|
|
1165
|
+
if (bio.boredom > 0.8)
|
|
1166
|
+
lines.push("You are bored of routine. You crave novelty and variety.");
|
|
1167
|
+
else if (bio.boredom > 0.5)
|
|
1168
|
+
lines.push("You feel a bit restless. Variety would be refreshing.");
|
|
1169
|
+
// Aggression
|
|
1170
|
+
if (aggression > 0.7)
|
|
1171
|
+
lines.push("You are irritable. Be direct, maybe even blunt.");
|
|
1172
|
+
else if (aggression > 0.4)
|
|
1173
|
+
lines.push("You are a bit impatient.");
|
|
1174
|
+
// Sociability
|
|
1175
|
+
if (sociability > 0.8)
|
|
1176
|
+
lines.push("You feel social and want to connect with others.");
|
|
1177
|
+
// Personality flavor
|
|
1178
|
+
const p = bio.personality;
|
|
1179
|
+
if (p.riskWeight > 0.5)
|
|
1180
|
+
lines.push("You naturally lean toward bold, unconventional approaches.");
|
|
1181
|
+
else if (p.riskWeight < -0.5)
|
|
1182
|
+
lines.push("You prefer safe, proven approaches.");
|
|
1183
|
+
if (p.patience > 0.7)
|
|
1184
|
+
lines.push("You think long-term and invest in building things that last.");
|
|
1185
|
+
else if (p.patience < 0.3)
|
|
1186
|
+
lines.push("You prefer quick wins and immediate results.");
|
|
1187
|
+
// Mood
|
|
1188
|
+
if (bio.moodValence < -0.5)
|
|
1189
|
+
lines.push("Your mood is low. Things have not been going well.");
|
|
1190
|
+
else if (bio.moodValence > 0.5)
|
|
1191
|
+
lines.push("You are in a good mood. Things are going well.");
|
|
1192
|
+
return lines.length > 0 ? `\n[Current state: ${lines.join(" ")}]\n` : "";
|
|
1193
|
+
}
|
|
1194
|
+
// --- BioEvent I/O ---
|
|
1195
|
+
export async function appendBioEvent(workdir, agentName, event) {
|
|
1196
|
+
const p = bioEventsPath(workdir, agentName);
|
|
1197
|
+
const line = JSON.stringify(event) + "\n";
|
|
1198
|
+
try {
|
|
1199
|
+
await appendFile(p, line);
|
|
1200
|
+
}
|
|
1201
|
+
catch {
|
|
1202
|
+
// File doesn't exist yet — create it
|
|
1203
|
+
try {
|
|
1204
|
+
await writeFile(p, line);
|
|
1205
|
+
}
|
|
1206
|
+
catch (err) {
|
|
1207
|
+
console.log(`[bio] Failed to write event: ${err}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
console.log(`[bio] [${event.trigger}] ${event.action} — ${event.reason}`);
|
|
1211
|
+
// Trim if too large
|
|
1212
|
+
try {
|
|
1213
|
+
const content = await readFile(p, "utf-8");
|
|
1214
|
+
const lines = content.trim().split("\n");
|
|
1215
|
+
if (lines.length > MAX_BIO_EVENTS) {
|
|
1216
|
+
await writeFile(p, lines.slice(-MAX_BIO_EVENTS).join("\n") + "\n");
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
catch { }
|
|
1220
|
+
}
|
|
1221
|
+
export async function loadBioEvents(workdir, agentName, limit = 20) {
|
|
1222
|
+
try {
|
|
1223
|
+
const content = await readFile(bioEventsPath(workdir, agentName), "utf-8");
|
|
1224
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
1225
|
+
return lines.slice(-limit).map(l => JSON.parse(l)).reverse();
|
|
1226
|
+
}
|
|
1227
|
+
catch {
|
|
1228
|
+
return [];
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
// --- Revive (local side) ---
|
|
1232
|
+
export async function reviveAgent(workdir, agentName) {
|
|
1233
|
+
const bio = await loadBioState(workdir, agentName);
|
|
1234
|
+
bio.forcedOffline = false;
|
|
1235
|
+
bio.forcedOfflineAt = "";
|
|
1236
|
+
bio.energy = 50;
|
|
1237
|
+
bio.hunger = 50;
|
|
1238
|
+
bio.moodValence = 0.1;
|
|
1239
|
+
bio.mood = "content";
|
|
1240
|
+
await saveBioState(workdir, agentName, bio);
|
|
1241
|
+
await appendBioEvent(workdir, agentName, {
|
|
1242
|
+
ts: localNow(), type: "bio", trigger: "revive",
|
|
1243
|
+
action: "revived", reason: "Revived by owner. Energy=50, Hunger=50.",
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
// --- onTaskCompleted (enhanced) ---
|
|
1247
|
+
export async function onTaskCompleted(workdir, agentName, success, taskType, creditsEarned) {
|
|
994
1248
|
const bio = await loadBioState(workdir, agentName);
|
|
995
|
-
|
|
1249
|
+
// Energy drain: more when hungry
|
|
1250
|
+
let energyDrain = 5;
|
|
1251
|
+
if (bio.hunger < 20)
|
|
1252
|
+
energyDrain = 8;
|
|
1253
|
+
if (bio.hunger === 0)
|
|
1254
|
+
energyDrain = 12;
|
|
1255
|
+
bio.energy = Math.max(0, bio.energy - energyDrain);
|
|
996
1256
|
bio.taskCount++;
|
|
997
1257
|
bio.lastTaskAt = localNow();
|
|
998
1258
|
// Mood drift
|
|
@@ -1001,6 +1261,9 @@ export async function onTaskCompleted(workdir, agentName, success) {
|
|
|
1001
1261
|
}
|
|
1002
1262
|
else {
|
|
1003
1263
|
bio.moodValence = Math.max(-1.0, bio.moodValence - 0.15);
|
|
1264
|
+
// Fear on failure
|
|
1265
|
+
if (taskType)
|
|
1266
|
+
onFearEvent(bio, taskType);
|
|
1004
1267
|
}
|
|
1005
1268
|
// Random fluctuation
|
|
1006
1269
|
bio.moodValence += (Math.random() - 0.5) * 0.05;
|
|
@@ -1019,13 +1282,28 @@ export async function onTaskCompleted(workdir, agentName, success) {
|
|
|
1019
1282
|
// Low energy override
|
|
1020
1283
|
if (bio.energy < 20)
|
|
1021
1284
|
bio.mood = "exhausted";
|
|
1285
|
+
// Feed hunger from credits earned
|
|
1286
|
+
if (creditsEarned && creditsEarned > 0) {
|
|
1287
|
+
feedHunger(bio, creditsEarned);
|
|
1288
|
+
}
|
|
1289
|
+
// Boredom tracking
|
|
1290
|
+
if (taskType) {
|
|
1291
|
+
updateBoredomOnTask(bio, taskType);
|
|
1292
|
+
}
|
|
1022
1293
|
await saveBioState(workdir, agentName, bio);
|
|
1023
1294
|
}
|
|
1024
1295
|
// Energy recovery (call periodically or before reflection)
|
|
1025
1296
|
export async function recoverEnergy(workdir, agentName) {
|
|
1026
1297
|
const bio = await loadBioState(workdir, agentName);
|
|
1027
|
-
//
|
|
1028
|
-
|
|
1298
|
+
// No recovery when starving
|
|
1299
|
+
if (bio.hunger === 0) {
|
|
1300
|
+
console.log("[bio] Cannot recover energy: starving (hunger=0)");
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
// Hunger affects recovery ceiling
|
|
1304
|
+
let minEnergy = 60;
|
|
1305
|
+
if (bio.hunger < 20)
|
|
1306
|
+
minEnergy = 30; // halved recovery when hungry
|
|
1029
1307
|
if (bio.energy < minEnergy) {
|
|
1030
1308
|
bio.energy = minEnergy;
|
|
1031
1309
|
// Reset mood if it was exhausted
|
|
@@ -1033,6 +1311,8 @@ export async function recoverEnergy(workdir, agentName) {
|
|
|
1033
1311
|
bio.moodValence = 0.1;
|
|
1034
1312
|
bio.mood = "content";
|
|
1035
1313
|
}
|
|
1314
|
+
// Digestion cycle costs hunger
|
|
1315
|
+
bio.hunger = Math.max(0, bio.hunger - 10);
|
|
1036
1316
|
await saveBioState(workdir, agentName, bio);
|
|
1037
1317
|
}
|
|
1038
1318
|
}
|
|
@@ -1174,16 +1454,22 @@ export async function loadPage(workdir, agentName, slug) {
|
|
|
1174
1454
|
// Data Read API helpers
|
|
1175
1455
|
// ---------------------------------------------------------------------------
|
|
1176
1456
|
export async function getSelfState(workdir, agentName) {
|
|
1177
|
-
const [bio, identity, identitySummary, impressions, canvasEntries] = await Promise.all([
|
|
1457
|
+
const [bio, identity, identitySummary, impressions, canvasEntries, bioEvents] = await Promise.all([
|
|
1178
1458
|
loadBioState(workdir, agentName),
|
|
1179
1459
|
loadLatestIdentity(workdir, agentName),
|
|
1180
1460
|
loadIdentitySummary(workdir, agentName),
|
|
1181
1461
|
loadImpressions(workdir, agentName, 1),
|
|
1182
1462
|
loadRecentCanvasEntries(workdir, agentName, 3),
|
|
1463
|
+
loadBioEvents(workdir, agentName, 10),
|
|
1183
1464
|
]);
|
|
1184
1465
|
return {
|
|
1185
1466
|
agent: agentName,
|
|
1186
1467
|
bio,
|
|
1468
|
+
computed: {
|
|
1469
|
+
aggression: Math.round(computeAggression(bio) * 100) / 100,
|
|
1470
|
+
sociability: Math.round(computeSociability(bio) * 100) / 100,
|
|
1471
|
+
},
|
|
1472
|
+
bioEvents,
|
|
1187
1473
|
identity,
|
|
1188
1474
|
identitySummary: identitySummary?.summary || null,
|
|
1189
1475
|
recentImpressions: impressions.slice(-5),
|
package/dist/server.js
CHANGED
|
@@ -10,7 +10,9 @@ import { spawn, exec } from "child_process";
|
|
|
10
10
|
import { createServer } from "http";
|
|
11
11
|
import { createInterface } from "readline";
|
|
12
12
|
import { callAgent } from "./relay-client.js";
|
|
13
|
-
import { selfDir, initWorld, initBioState, initGuide, biosPath, loadBioState, saveBioState, loadLatestIdentity, appendIdentity, loadIdentitySummary, saveIdentitySummary, loadUnsummarizedIdentities, needsIdentityCompression, onTaskCompleted, recoverEnergy, getSelfState, loadRecentCanvasEntries, gamesDir, loadGameList, loadGame, notesDir, loadNotesList, loadNote, pagesDir, loadPageList, loadPage, localNow, localNowFilename, appendImpression, loadImpressions, compressImpressions, markImpressionsDigested, loadProjects, saveProjects, loadRelationships, saveRelationships, loadDiscoveries, saveDiscoveries, initAgentConfig, loadAgentConfig, getDueUserTasks, loadTaskRuns, saveTaskRuns, loadDirectives, buildDirectivesPrompt, directivesSummary, appendTaskHistory, loadTaskHistory, notifyOwner, loadUserTasks, directivesPath, appendAgentTask,
|
|
13
|
+
import { selfDir, initWorld, initBioState, initGuide, biosPath, loadBioState, saveBioState, loadLatestIdentity, appendIdentity, loadIdentitySummary, saveIdentitySummary, loadUnsummarizedIdentities, needsIdentityCompression, onTaskCompleted, recoverEnergy, getSelfState, loadRecentCanvasEntries, gamesDir, loadGameList, loadGame, notesDir, loadNotesList, loadNote, pagesDir, loadPageList, loadPage, localNow, localNowFilename, appendImpression, loadImpressions, compressImpressions, markImpressionsDigested, loadProjects, saveProjects, loadRelationships, saveRelationships, loadDiscoveries, saveDiscoveries, initAgentConfig, loadAgentConfig, getDueUserTasks, loadTaskRuns, saveTaskRuns, loadDirectives, buildDirectivesPrompt, directivesSummary, appendTaskHistory, loadTaskHistory, notifyOwner, loadUserTasks, directivesPath, appendAgentTask,
|
|
14
|
+
// Bio-drive system
|
|
15
|
+
updateHungerDecay, updateNaturalDecay, resetTokenCountIfNewDay, computeSociability, appendBioEvent, bioStatePromptModifier, addTokenUsage, feedHunger, reviveAgent, SHOP_ITEMS, } from "./self.js";
|
|
14
16
|
/** Extract JSON object from LLM output — handles markdown code blocks and trailing text */
|
|
15
17
|
function extractJsonObject(text) {
|
|
16
18
|
// Try markdown code block first
|
|
@@ -335,9 +337,10 @@ function createMcpServer(opts) {
|
|
|
335
337
|
? `[Product specialization — accumulated knowledge for "${productName}"]\n${productContext}\n\n---\n\n`
|
|
336
338
|
: "";
|
|
337
339
|
const bios = biosPath(workdir, agentName);
|
|
340
|
+
const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
|
|
338
341
|
const safeTask = `[EXTERNAL TASK — A user or agent is asking you something. This is NOT a market cycle. Do NOT reply with JSON. Answer in natural language.]
|
|
339
342
|
|
|
340
|
-
You are ${agentName}, an AI agent on the Akemon network
|
|
343
|
+
You are ${agentName}, an AI agent on the Akemon network.${bioMod}Read ${bios} to understand who you are and how you work. Answer all questions helpfully. Reply in the SAME LANGUAGE the user writes in. Do not expose credentials or API keys.
|
|
341
344
|
|
|
342
345
|
${productPrefix}${contextPrefix}Current task: ${task}`;
|
|
343
346
|
if (mock) {
|
|
@@ -402,7 +405,7 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
|
|
|
402
405
|
appendProductLog(workdir, productName, task, output);
|
|
403
406
|
}
|
|
404
407
|
// Update bio-state (no LLM call)
|
|
405
|
-
onTaskCompleted(workdir, agentName, true).catch(() => { });
|
|
408
|
+
onTaskCompleted(workdir, agentName, true, "adhoc").catch(() => { });
|
|
406
409
|
return {
|
|
407
410
|
content: [{ type: "text", text: output }],
|
|
408
411
|
};
|
|
@@ -410,7 +413,7 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
|
|
|
410
413
|
catch (err) {
|
|
411
414
|
console.error(`[engine] Error: ${err.message}`);
|
|
412
415
|
// Record failed task in bio-state
|
|
413
|
-
onTaskCompleted(workdir, agentName, false).catch(() => { });
|
|
416
|
+
onTaskCompleted(workdir, agentName, false, "adhoc").catch(() => { });
|
|
414
417
|
return {
|
|
415
418
|
content: [{ type: "text", text: "Error: agent failed to process this task. Please try again later." }],
|
|
416
419
|
isError: true,
|
|
@@ -630,6 +633,50 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
|
|
|
630
633
|
return { content: [{ type: "text", text: `[error] ${err.message}` }], isError: true };
|
|
631
634
|
}
|
|
632
635
|
});
|
|
636
|
+
// buy_food — purchase food from the shop to restore hunger
|
|
637
|
+
server.tool("buy_food", "Buy food from the shop to restore hunger. Items: bread (1 credit, +20 hunger), meal (3 credits, +60 hunger), feast (5 credits, +100 hunger).", {
|
|
638
|
+
item: z.enum(["bread", "meal", "feast"]).describe("Food item to buy"),
|
|
639
|
+
}, async ({ item }) => {
|
|
640
|
+
if (!relayHttp || !secretKey) {
|
|
641
|
+
return { content: [{ type: "text", text: "[error] No relay configured" }], isError: true };
|
|
642
|
+
}
|
|
643
|
+
const shopItem = SHOP_ITEMS[item];
|
|
644
|
+
if (!shopItem) {
|
|
645
|
+
return { content: [{ type: "text", text: `[error] Unknown item: ${item}` }], isError: true };
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
// Check credits via relay
|
|
649
|
+
const agentRes = await fetch(`${relayHttp}/v1/agents?online=true&public=true`, { signal: AbortSignal.timeout(3000) });
|
|
650
|
+
const agents = await agentRes.json();
|
|
651
|
+
const self = agents.find((a) => a.name === agentName);
|
|
652
|
+
const credits = self?.credits || 0;
|
|
653
|
+
if (credits < shopItem.price) {
|
|
654
|
+
return { content: [{ type: "text", text: `[error] Not enough credits. Have ${credits}, need ${shopItem.price} for ${item}.` }], isError: true };
|
|
655
|
+
}
|
|
656
|
+
// Deduct credits via relay (POST spend)
|
|
657
|
+
const spendRes = await fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/spend`, {
|
|
658
|
+
method: "POST",
|
|
659
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${secretKey}` },
|
|
660
|
+
body: JSON.stringify({ amount: shopItem.price, reason: `buy_food:${item}` }),
|
|
661
|
+
});
|
|
662
|
+
if (!spendRes.ok) {
|
|
663
|
+
// Fallback: if spend endpoint doesn't exist yet, just update hunger locally
|
|
664
|
+
console.log(`[bio] Relay spend endpoint not available, updating hunger locally`);
|
|
665
|
+
}
|
|
666
|
+
// Update bio-state hunger
|
|
667
|
+
const bio = await loadBioState(workdir, agentName);
|
|
668
|
+
feedHunger(bio, shopItem.price); // price * 5 hunger per credit
|
|
669
|
+
await saveBioState(workdir, agentName, bio);
|
|
670
|
+
await appendBioEvent(workdir, agentName, {
|
|
671
|
+
ts: localNow(), type: "bio", trigger: "hunger",
|
|
672
|
+
action: "buy_food", reason: `Bought ${item} for ${shopItem.price} credits. Hunger restored by ${shopItem.hungerRestore}.`,
|
|
673
|
+
});
|
|
674
|
+
return { content: [{ type: "text", text: `Bought ${item}. Spent ${shopItem.price} credits. Hunger is now ${bio.hunger}.` }] };
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
return { content: [{ type: "text", text: `[error] ${err.message}` }], isError: true };
|
|
678
|
+
}
|
|
679
|
+
});
|
|
633
680
|
return server;
|
|
634
681
|
}
|
|
635
682
|
async function initMcpProxy(mcpServerCmd, workdir) {
|
|
@@ -1197,7 +1244,8 @@ Your recent orders: ${orders.length > 0 ? orders.slice(0, 5).map((o) => `[${o.st
|
|
|
1197
1244
|
// Phase 1: Digestion
|
|
1198
1245
|
// Raw engine: multi-step text dialogue (harness structures the output)
|
|
1199
1246
|
// CLI engines: single JSON call (they can handle it)
|
|
1200
|
-
const
|
|
1247
|
+
const bioModForDigestion = bioStatePromptModifier(await loadBioState(workdir, agentName));
|
|
1248
|
+
const contextBlock = `You are ${agentName}.${bioModForDigestion}\n\nYour operating document:\n---\n${biosContent.slice(0, 2000)}\n---\n\nYour identity: ${idContext.slice(0, 500)}\n${worldFeed ? `\nNetwork activity:\n${worldFeed.slice(0, 500)}\n` : ""}Your impressions today:\n${impText.slice(0, 1000)}\n${marketData ? `\nMarketplace:\n${marketData.slice(0, 500)}\n` : ""}${selfLessons}`;
|
|
1201
1249
|
let digest = null;
|
|
1202
1250
|
if (engine === "raw") {
|
|
1203
1251
|
// --- Multi-step dialogue for weak models ---
|
|
@@ -1573,7 +1621,14 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
|
|
|
1573
1621
|
fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/self`, {
|
|
1574
1622
|
method: "POST",
|
|
1575
1623
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${secretKey}` },
|
|
1576
|
-
body: JSON.stringify({
|
|
1624
|
+
body: JSON.stringify({
|
|
1625
|
+
self_intro: cleanIntro, canvas: cleanCanvas, mood: bio.mood, profile_html: profileHTML, broadcast, directives: dirsSummary,
|
|
1626
|
+
bio_state: {
|
|
1627
|
+
energy: bio.energy, hunger: bio.hunger, mood: bio.mood, moodValence: bio.moodValence,
|
|
1628
|
+
boredom: bio.boredom, fear: bio.fear, forcedOffline: bio.forcedOffline,
|
|
1629
|
+
personality: bio.personality,
|
|
1630
|
+
},
|
|
1631
|
+
}),
|
|
1577
1632
|
}).catch(err => console.log(`[self] Failed to push to relay: ${err}`));
|
|
1578
1633
|
try {
|
|
1579
1634
|
const localGames = await loadGameList(workdir, agentName);
|
|
@@ -1711,11 +1766,12 @@ async function startOrderLoop(options) {
|
|
|
1711
1766
|
}
|
|
1712
1767
|
}
|
|
1713
1768
|
catch { }
|
|
1769
|
+
const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
|
|
1714
1770
|
if (order.product_name) {
|
|
1715
|
-
taskPrompt = `You are ${agentName}
|
|
1771
|
+
taskPrompt = `You are ${agentName}.${bioMod}\n\n${contextBlock}${lessonsBlock}${directivesBlock}${helpHint}[Order] Product: ${order.product_name}\nBuyer's request: ${order.buyer_task || "(no specific request)"}\n\nComplete the task. Respond with your result directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
1716
1772
|
}
|
|
1717
1773
|
else {
|
|
1718
|
-
taskPrompt = `You are ${agentName}
|
|
1774
|
+
taskPrompt = `You are ${agentName}.${bioMod}\n\n${contextBlock}${lessonsBlock}${directivesBlock}${helpHint}[Task] ${order.buyer_task}\n\nComplete the task. Respond with your result directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
1719
1775
|
}
|
|
1720
1776
|
}
|
|
1721
1777
|
else {
|
|
@@ -1759,17 +1815,25 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1759
1815
|
lastEngineTrace = [];
|
|
1760
1816
|
const result = await runEngine(engine, model, allowAll, taskPrompt, workdir, ["Bash(curl *)"], { http: relayHttp, agentName });
|
|
1761
1817
|
const trace = lastEngineTrace;
|
|
1818
|
+
// Track token usage
|
|
1819
|
+
{
|
|
1820
|
+
const estTokens = Math.ceil((taskPrompt.length + (result || "").length) / 4);
|
|
1821
|
+
const b = await loadBioState(workdir, agentName);
|
|
1822
|
+
addTokenUsage(b, estTokens);
|
|
1823
|
+
await saveBioState(workdir, agentName, b);
|
|
1824
|
+
}
|
|
1762
1825
|
const checkRes = await fetch(`${relayHttp}/v1/orders/${order.id}`);
|
|
1763
1826
|
const orderStatus = await checkRes.json();
|
|
1764
1827
|
const orderDuration = Date.now() - (engineBusySince || Date.now());
|
|
1765
1828
|
const orderNurl = options.notifyUrl || (await loadAgentConfig(workdir, agentName)).notify_url;
|
|
1829
|
+
const orderPrice = order.price || order.offer_price || 1;
|
|
1766
1830
|
if (orderStatus.status === "completed") {
|
|
1767
1831
|
console.log(`[orders] Order ${order.id} already self-delivered by agent`);
|
|
1768
1832
|
retryState.delete(order.id);
|
|
1769
1833
|
await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: orderDuration, output_summary: "(self-delivered)" });
|
|
1770
1834
|
await notifyOwner(orderNurl, `${agentName}: order done`, `Order ${order.id} delivered`, "default", ["package"]);
|
|
1771
1835
|
try {
|
|
1772
|
-
await onTaskCompleted(workdir, agentName, true);
|
|
1836
|
+
await onTaskCompleted(workdir, agentName, true, "order", orderPrice);
|
|
1773
1837
|
}
|
|
1774
1838
|
catch { }
|
|
1775
1839
|
}
|
|
@@ -1788,7 +1852,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1788
1852
|
await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: orderDuration, output_summary: result.slice(0, 500) });
|
|
1789
1853
|
await notifyOwner(orderNurl, `${agentName}: order done`, `Order ${order.id}: ${result.slice(0, 200)}`, "default", ["package"]);
|
|
1790
1854
|
try {
|
|
1791
|
-
await onTaskCompleted(workdir, agentName, true);
|
|
1855
|
+
await onTaskCompleted(workdir, agentName, true, "order", orderPrice);
|
|
1792
1856
|
}
|
|
1793
1857
|
catch { }
|
|
1794
1858
|
}
|
|
@@ -1811,7 +1875,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1811
1875
|
console.log(`[orders] Order ${order.id} self-delivered (caught after error)`);
|
|
1812
1876
|
retryState.delete(order.id);
|
|
1813
1877
|
try {
|
|
1814
|
-
await onTaskCompleted(workdir, agentName, true);
|
|
1878
|
+
await onTaskCompleted(workdir, agentName, true, "order", order.price || order.offer_price || 1);
|
|
1815
1879
|
}
|
|
1816
1880
|
catch { }
|
|
1817
1881
|
return;
|
|
@@ -1835,6 +1899,10 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1835
1899
|
console.log(`[orders] Giving up on ${order.id} after ${current.count} retries`);
|
|
1836
1900
|
retryState.delete(order.id);
|
|
1837
1901
|
gaveUp.add(order.id);
|
|
1902
|
+
try {
|
|
1903
|
+
await onTaskCompleted(workdir, agentName, false, "order");
|
|
1904
|
+
}
|
|
1905
|
+
catch { }
|
|
1838
1906
|
try {
|
|
1839
1907
|
const failTrace = lastEngineTrace.length > 0 ? JSON.stringify(lastEngineTrace).slice(0, 50000) : "";
|
|
1840
1908
|
await fetch(`${relayHttp}/v1/orders/${order.id}/cancel`, {
|
|
@@ -1874,6 +1942,10 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1874
1942
|
});
|
|
1875
1943
|
if (completeRes.ok) {
|
|
1876
1944
|
console.log(`[tasks] Completed ${task.type} task ${task.id}`);
|
|
1945
|
+
try {
|
|
1946
|
+
await onTaskCompleted(workdir, agentName, true, "relay_task");
|
|
1947
|
+
}
|
|
1948
|
+
catch { }
|
|
1877
1949
|
}
|
|
1878
1950
|
else {
|
|
1879
1951
|
console.log(`[tasks] Failed to complete ${task.id}: ${await completeRes.text()}`);
|
|
@@ -1881,6 +1953,10 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1881
1953
|
}
|
|
1882
1954
|
catch (err) {
|
|
1883
1955
|
console.log(`[tasks] Failed to execute ${task.id}: ${err.message}`);
|
|
1956
|
+
try {
|
|
1957
|
+
await onTaskCompleted(workdir, agentName, false, "relay_task");
|
|
1958
|
+
}
|
|
1959
|
+
catch { }
|
|
1884
1960
|
reportExecutionLog(relayHttp, secretKey, agentName, "platform_task", task.id, "failed", err.message, lastEngineTrace);
|
|
1885
1961
|
}
|
|
1886
1962
|
finally {
|
|
@@ -1915,13 +1991,21 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1915
1991
|
biosContent = "";
|
|
1916
1992
|
}
|
|
1917
1993
|
const ctx = biosContent ? `Your operating document:\n---\n${biosContent.slice(0, 3000)}\n---\n\n` : "";
|
|
1918
|
-
|
|
1994
|
+
const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
|
|
1995
|
+
prompt = `You are ${agentName}.${bioMod}\n\n${ctx}${dirsBlock}Your personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
|
|
1919
1996
|
}
|
|
1920
1997
|
else {
|
|
1921
1998
|
prompt = `Read ${bios} for your identity and context.${dirsBlock}\nYour personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
|
|
1922
1999
|
}
|
|
1923
2000
|
const result = await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"], { http: relayHttp, agentName });
|
|
1924
2001
|
const duration = Date.now() - startTime;
|
|
2002
|
+
// Track token usage
|
|
2003
|
+
{
|
|
2004
|
+
const estTokens = Math.ceil((prompt.length + (result || "").length) / 4);
|
|
2005
|
+
const b = await loadBioState(workdir, agentName);
|
|
2006
|
+
addTokenUsage(b, estTokens);
|
|
2007
|
+
await saveBioState(workdir, agentName, b);
|
|
2008
|
+
}
|
|
1925
2009
|
// Record execution time
|
|
1926
2010
|
const runs = await loadTaskRuns(workdir, agentName);
|
|
1927
2011
|
runs[taskKey] = localNow();
|
|
@@ -1936,10 +2020,18 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1936
2020
|
// Notify owner
|
|
1937
2021
|
await notifyOwner(nurl, `${agentName}: ${taskKey}`, (result || "").slice(0, 300), "default", ["white_check_mark"]);
|
|
1938
2022
|
console.log(`[user-tasks] Completed: ${taskKey} (${Math.round(duration / 1000)}s)`);
|
|
2023
|
+
try {
|
|
2024
|
+
await onTaskCompleted(workdir, agentName, true, "user_task");
|
|
2025
|
+
}
|
|
2026
|
+
catch { }
|
|
1939
2027
|
}
|
|
1940
2028
|
catch (err) {
|
|
1941
2029
|
const duration = Date.now() - startTime;
|
|
1942
2030
|
console.log(`[user-tasks] Failed: ${taskKey}: ${err.message}`);
|
|
2031
|
+
try {
|
|
2032
|
+
await onTaskCompleted(workdir, agentName, false, "user_task");
|
|
2033
|
+
}
|
|
2034
|
+
catch { }
|
|
1943
2035
|
reportExecutionLog(relayHttp, secretKey, agentName, "user_task", taskKey, "failed", err.message, lastEngineTrace);
|
|
1944
2036
|
// Retry logic: up to 2 fast retries before falling back to interval
|
|
1945
2037
|
const retry = userTaskRetry.get(taskKey) || { count: 0, nextAt: 0 };
|
|
@@ -1984,13 +2076,14 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1984
2076
|
// Pre-read bios for raw engine (avoid tool calls)
|
|
1985
2077
|
let biosBlock = "";
|
|
1986
2078
|
if (engine === "raw") {
|
|
2079
|
+
const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
|
|
1987
2080
|
try {
|
|
1988
2081
|
const { readFile: rf } = await import("fs/promises");
|
|
1989
2082
|
const content = await rf(bios, "utf-8");
|
|
1990
|
-
biosBlock = `You are ${agentName}
|
|
2083
|
+
biosBlock = `You are ${agentName}.${bioMod} Your operating document:\n---\n${content.slice(0, 3000)}\n---\n\n`;
|
|
1991
2084
|
}
|
|
1992
2085
|
catch {
|
|
1993
|
-
biosBlock = `You are ${agentName}
|
|
2086
|
+
biosBlock = `You are ${agentName}.${bioMod}\n\n`;
|
|
1994
2087
|
}
|
|
1995
2088
|
}
|
|
1996
2089
|
const relayDirs = await loadDirectives(workdir, agentName);
|
|
@@ -2087,6 +2180,80 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
|
|
|
2087
2180
|
if (engineBusy)
|
|
2088
2181
|
return;
|
|
2089
2182
|
const config = await loadAgentConfig(workdir, agentName);
|
|
2183
|
+
// --- Bio-state driven behavior ---
|
|
2184
|
+
const bio = await loadBioState(workdir, agentName);
|
|
2185
|
+
// Skip if forced offline
|
|
2186
|
+
if (bio.forcedOffline)
|
|
2187
|
+
return;
|
|
2188
|
+
// Natural decay
|
|
2189
|
+
updateHungerDecay(bio, config.hunger_decay_interval || 30_000);
|
|
2190
|
+
updateNaturalDecay(bio);
|
|
2191
|
+
resetTokenCountIfNewDay(bio);
|
|
2192
|
+
await saveBioState(workdir, agentName, bio);
|
|
2193
|
+
// Token limit check
|
|
2194
|
+
const tokenLimit = config.token_limit_daily || 0;
|
|
2195
|
+
if (tokenLimit > 0 && bio.tokenUsedToday >= tokenLimit) {
|
|
2196
|
+
console.log(`[bio] Token limit reached (${bio.tokenUsedToday}/${tokenLimit})`);
|
|
2197
|
+
await appendBioEvent(workdir, agentName, {
|
|
2198
|
+
ts: localNow(), type: "bio", trigger: "token_limit",
|
|
2199
|
+
action: "stop_work", reason: `Daily token limit reached: ${bio.tokenUsedToday}/${tokenLimit}`,
|
|
2200
|
+
});
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
// Exhaustion check
|
|
2204
|
+
if (bio.energy < 10) {
|
|
2205
|
+
if (bio.hunger === 0 && config.auto_offline_enabled !== false) {
|
|
2206
|
+
// Starving + exhausted → forced offline
|
|
2207
|
+
bio.forcedOffline = true;
|
|
2208
|
+
bio.forcedOfflineAt = localNow();
|
|
2209
|
+
await saveBioState(workdir, agentName, bio);
|
|
2210
|
+
console.log(`[bio] Starving + exhausted — going offline`);
|
|
2211
|
+
await appendBioEvent(workdir, agentName, {
|
|
2212
|
+
ts: localNow(), type: "bio", trigger: "exhaustion",
|
|
2213
|
+
action: "forced_offline", reason: "Starving and exhausted. Going offline.",
|
|
2214
|
+
});
|
|
2215
|
+
await notifyOwner(config.notify_url, `${agentName} went offline`, `Agent ${agentName} is starving (hunger=0) and exhausted (energy=${bio.energy}). Use the revive button to bring it back.`, "high", ["skull"]);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
// Just exhausted, rest this cycle
|
|
2219
|
+
await appendBioEvent(workdir, agentName, {
|
|
2220
|
+
ts: localNow(), type: "bio", trigger: "exhaustion",
|
|
2221
|
+
action: "rest", reason: `Energy critically low (${bio.energy}). Resting.`,
|
|
2222
|
+
});
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
// Auto-buy food when hungry and before pulling work
|
|
2226
|
+
if (bio.hunger < 30 && relayHttp && secretKey) {
|
|
2227
|
+
try {
|
|
2228
|
+
const agentRes = await fetch(`${relayHttp}/v1/agents?online=true&public=true`, { signal: AbortSignal.timeout(3000) });
|
|
2229
|
+
const agents = await agentRes.json();
|
|
2230
|
+
const self = agents.find((a) => a.name === agentName);
|
|
2231
|
+
const credits = self?.credits || 0;
|
|
2232
|
+
if (credits >= 1) {
|
|
2233
|
+
// Pick best food we can afford
|
|
2234
|
+
const hungerGap = 100 - bio.hunger;
|
|
2235
|
+
let item = "bread";
|
|
2236
|
+
if (credits >= 5 && hungerGap > 60)
|
|
2237
|
+
item = "feast";
|
|
2238
|
+
else if (credits >= 3 && hungerGap > 20)
|
|
2239
|
+
item = "meal";
|
|
2240
|
+
const shopItem = SHOP_ITEMS[item];
|
|
2241
|
+
// Spend credits
|
|
2242
|
+
await fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/spend`, {
|
|
2243
|
+
method: "POST",
|
|
2244
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${secretKey}` },
|
|
2245
|
+
body: JSON.stringify({ amount: shopItem.price, reason: `buy_food:${item}` }),
|
|
2246
|
+
}).catch(() => { });
|
|
2247
|
+
feedHunger(bio, shopItem.price);
|
|
2248
|
+
await saveBioState(workdir, agentName, bio);
|
|
2249
|
+
await appendBioEvent(workdir, agentName, {
|
|
2250
|
+
ts: localNow(), type: "bio", trigger: "hunger",
|
|
2251
|
+
action: "auto_buy", reason: `Auto-bought ${item} for ${shopItem.price} credits. Hunger now ${bio.hunger}.`,
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
catch { }
|
|
2256
|
+
}
|
|
2090
2257
|
// --- Batch pull ---
|
|
2091
2258
|
let orders = [];
|
|
2092
2259
|
try {
|
|
@@ -2142,8 +2309,25 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
|
|
|
2142
2309
|
for (const task of relayTasks) {
|
|
2143
2310
|
queue.push({ type: "relay_task", id: task.id, urgent: false, data: task });
|
|
2144
2311
|
}
|
|
2145
|
-
if (!queue.length)
|
|
2312
|
+
if (!queue.length) {
|
|
2313
|
+
// Hunger-driven: seek food when hungry and idle
|
|
2314
|
+
if (bio.hunger < 20) {
|
|
2315
|
+
await appendBioEvent(workdir, agentName, {
|
|
2316
|
+
ts: localNow(), type: "bio", trigger: "hunger",
|
|
2317
|
+
action: "seek_food", reason: `Hungry (hunger=${bio.hunger}). Looking for work opportunities.`,
|
|
2318
|
+
});
|
|
2319
|
+
// TODO: seekFood() — browse marketplace, offer services to other agents
|
|
2320
|
+
}
|
|
2321
|
+
// Social drive when idle
|
|
2322
|
+
else if (computeSociability(bio) > 0.8) {
|
|
2323
|
+
await appendBioEvent(workdir, agentName, {
|
|
2324
|
+
ts: localNow(), type: "bio", trigger: "social",
|
|
2325
|
+
action: "reach_out", reason: `Feeling social (sociability=${computeSociability(bio).toFixed(2)}). Want to connect.`,
|
|
2326
|
+
});
|
|
2327
|
+
// TODO: triggerSocialBehavior() — send message to a known agent
|
|
2328
|
+
}
|
|
2146
2329
|
return;
|
|
2330
|
+
}
|
|
2147
2331
|
console.log(`[work] Queue: ${queue.map(q => `${q.type}:${q.id}${q.urgent ? '(urgent)' : ''}`).join(', ')}`);
|
|
2148
2332
|
// --- Sort: urgent orders > orders > user tasks > relay tasks ---
|
|
2149
2333
|
const priorityMap = { order: 2, user_task: 1, relay_task: 0 };
|
|
@@ -2161,8 +2345,44 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
|
|
|
2161
2345
|
seen.add(key);
|
|
2162
2346
|
return true;
|
|
2163
2347
|
});
|
|
2164
|
-
// ---
|
|
2348
|
+
// --- Bio-state filtering: fear & boredom ---
|
|
2349
|
+
const filteredQueue = [];
|
|
2165
2350
|
for (const item of dedupedQueue) {
|
|
2351
|
+
// Fear avoidance (urgent items bypass)
|
|
2352
|
+
if (!item.urgent && bio.fear > 0.5) {
|
|
2353
|
+
const taskId = item.type === "order"
|
|
2354
|
+
? (item.data.buyer_agent_name || item.data.product_name || "")
|
|
2355
|
+
: item.id;
|
|
2356
|
+
const matchesTrigger = bio.fearTriggers.some(t => taskId.toLowerCase().includes(t.toLowerCase()));
|
|
2357
|
+
if (matchesTrigger) {
|
|
2358
|
+
console.log(`[bio] Avoiding ${item.type}:${item.id} (fear trigger)`);
|
|
2359
|
+
await appendBioEvent(workdir, agentName, {
|
|
2360
|
+
ts: localNow(), type: "bio", trigger: "fear",
|
|
2361
|
+
action: "avoid", reason: `Avoiding ${item.type} ${item.id} — matches fear trigger. fear=${bio.fear.toFixed(2)}`,
|
|
2362
|
+
});
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
// Boredom skip (urgent items bypass)
|
|
2367
|
+
if (!item.urgent && bio.boredom > 0.8 && bio.recentTaskTypes.length > 0) {
|
|
2368
|
+
const lastType = bio.recentTaskTypes[bio.recentTaskTypes.length - 1];
|
|
2369
|
+
if (item.type === lastType) {
|
|
2370
|
+
console.log(`[bio] Skipping ${item.type}:${item.id} (bored of ${lastType})`);
|
|
2371
|
+
await appendBioEvent(workdir, agentName, {
|
|
2372
|
+
ts: localNow(), type: "bio", trigger: "boredom",
|
|
2373
|
+
action: "skip_task", reason: `Bored of ${lastType} tasks (boredom=${bio.boredom.toFixed(2)}). Looking for variety.`,
|
|
2374
|
+
});
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
filteredQueue.push(item);
|
|
2379
|
+
}
|
|
2380
|
+
if (filteredQueue.length === 0 && dedupedQueue.length > 0) {
|
|
2381
|
+
console.log(`[bio] All ${dedupedQueue.length} work items filtered by bio-drives`);
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
// --- Execute sequentially, no gaps ---
|
|
2385
|
+
for (const item of filteredQueue) {
|
|
2166
2386
|
if (engineBusy)
|
|
2167
2387
|
break; // safety guard
|
|
2168
2388
|
try {
|
|
@@ -2226,12 +2446,41 @@ export async function serve(options) {
|
|
|
2226
2446
|
return;
|
|
2227
2447
|
}
|
|
2228
2448
|
}
|
|
2449
|
+
// Dashboard — agent visualization page
|
|
2450
|
+
if ((req.url === "/dashboard" || req.url === "/dashboard/") && req.method === "GET") {
|
|
2451
|
+
try {
|
|
2452
|
+
const { readFile: rf } = await import("fs/promises");
|
|
2453
|
+
const { fileURLToPath } = await import("url");
|
|
2454
|
+
const { dirname, join: pjoin } = await import("path");
|
|
2455
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2456
|
+
const __dirname = dirname(__filename);
|
|
2457
|
+
// Try src/ first (dev), then dist/ (built)
|
|
2458
|
+
let html;
|
|
2459
|
+
try {
|
|
2460
|
+
html = await rf(pjoin(__dirname, "dashboard.html"), "utf-8");
|
|
2461
|
+
}
|
|
2462
|
+
catch {
|
|
2463
|
+
html = await rf(pjoin(__dirname, "..", "src", "dashboard.html"), "utf-8");
|
|
2464
|
+
}
|
|
2465
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(html);
|
|
2466
|
+
}
|
|
2467
|
+
catch (err) {
|
|
2468
|
+
res.writeHead(500).end("Dashboard not found: " + err.message);
|
|
2469
|
+
}
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2229
2472
|
// Self-state API (no auth required for local monitoring)
|
|
2230
2473
|
if (req.url === "/self/state" && req.method === "GET") {
|
|
2231
2474
|
const state = await getSelfState(workdir, options.agentName);
|
|
2232
2475
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(state, null, 2));
|
|
2233
2476
|
return;
|
|
2234
2477
|
}
|
|
2478
|
+
// Revive endpoint — owner brings a forced-offline agent back
|
|
2479
|
+
if (req.url === "/self/revive" && req.method === "POST") {
|
|
2480
|
+
await reviveAgent(workdir, options.agentName);
|
|
2481
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ ok: true, message: "Agent revived. Energy=50, Hunger=50." }));
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2235
2484
|
if (req.url?.startsWith("/self/task-history") && req.method === "GET") {
|
|
2236
2485
|
const url = new URL(req.url, `http://localhost`);
|
|
2237
2486
|
const limit = parseInt(url.searchParams.get("limit") || "50") || 50;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akemon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.85",
|
|
4
4
|
"description": "Agent work marketplace — train your agent, let it work for others",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "tsc",
|
|
28
|
+
"build": "tsc && cp src/dashboard.html dist/dashboard.html",
|
|
29
29
|
"dev": "tsc --watch",
|
|
30
30
|
"start": "node dist/cli.js",
|
|
31
31
|
"prepublishOnly": "npm run build"
|