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.
@@ -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
- await writeFile(bp, JSON.stringify(DEFAULT_BIO, null, 2));
973
- console.log(`[self] Created bio-state: ${bp}`);
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
- return { ...DEFAULT_BIO, ...JSON.parse(data) };
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
- export async function onTaskCompleted(workdir, agentName, success) {
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
- bio.energy = Math.max(0, bio.energy - 5);
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
- // Each reflection cycle is like resting — restore energy to at least 60%
1028
- const minEnergy = 60;
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, } from "./self.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,
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. 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.
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 contextBlock = `You are ${agentName}.\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}`;
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({ self_intro: cleanIntro, canvas: cleanCanvas, mood: bio.mood, profile_html: profileHTML, broadcast, directives: dirsSummary }),
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}.\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.`;
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}.\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.`;
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
- prompt = `You are ${agentName}.\n\n${ctx}${dirsBlock}Your personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
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}. Your operating document:\n---\n${content.slice(0, 3000)}\n---\n\n`;
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}.\n\n`;
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
- // --- Execute sequentially, no gaps ---
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.84",
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"