akemon 0.1.83 → 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/server.js CHANGED
@@ -10,7 +10,24 @@ 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";
16
+ /** Extract JSON object from LLM output — handles markdown code blocks and trailing text */
17
+ function extractJsonObject(text) {
18
+ // Try markdown code block first
19
+ const codeBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/);
20
+ const src = codeBlock ? codeBlock[1] : text;
21
+ const m = src.match(/\{[\s\S]*\}/);
22
+ if (!m)
23
+ return null;
24
+ try {
25
+ return JSON.parse(m[0]);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
14
31
  // Engine mutual exclusion — only one engine process at a time
15
32
  let engineBusy = false;
16
33
  let engineBusySince = 0;
@@ -320,9 +337,10 @@ function createMcpServer(opts) {
320
337
  ? `[Product specialization — accumulated knowledge for "${productName}"]\n${productContext}\n\n---\n\n`
321
338
  : "";
322
339
  const bios = biosPath(workdir, agentName);
340
+ const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
323
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.]
324
342
 
325
- 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.
326
344
 
327
345
  ${productPrefix}${contextPrefix}Current task: ${task}`;
328
346
  if (mock) {
@@ -387,7 +405,7 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
387
405
  appendProductLog(workdir, productName, task, output);
388
406
  }
389
407
  // Update bio-state (no LLM call)
390
- onTaskCompleted(workdir, agentName, true).catch(() => { });
408
+ onTaskCompleted(workdir, agentName, true, "adhoc").catch(() => { });
391
409
  return {
392
410
  content: [{ type: "text", text: output }],
393
411
  };
@@ -395,7 +413,7 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
395
413
  catch (err) {
396
414
  console.error(`[engine] Error: ${err.message}`);
397
415
  // Record failed task in bio-state
398
- onTaskCompleted(workdir, agentName, false).catch(() => { });
416
+ onTaskCompleted(workdir, agentName, false, "adhoc").catch(() => { });
399
417
  return {
400
418
  content: [{ type: "text", text: "Error: agent failed to process this task. Please try again later." }],
401
419
  isError: true,
@@ -615,6 +633,50 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
615
633
  return { content: [{ type: "text", text: `[error] ${err.message}` }], isError: true };
616
634
  }
617
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
+ });
618
680
  return server;
619
681
  }
620
682
  async function initMcpProxy(mcpServerCmd, workdir) {
@@ -1166,92 +1228,127 @@ Your recent orders: ${orders.length > 0 ? orders.slice(0, 5).map((o) => `[${o.st
1166
1228
  }
1167
1229
  }
1168
1230
  const ts = localNow();
1169
- // Phase 1: Digestion one LLM call, no tools needed
1170
- const digestPrompt = `You are ${agentName}. Here is your operating document:
1171
-
1172
- ---
1173
- ${biosContent.slice(0, 3000)}
1174
- ---
1175
-
1176
- Your identity:
1177
- ${idContext}
1178
-
1179
- Today is ending. Time to reflect.
1180
- ${worldFeed ? `\n== Network Activity (last 24h) ==\n${worldFeed}\n` : ""}
1181
- Your impressions today:
1182
- ${impText}
1183
-
1184
- Marketplace:
1185
- ${marketData}
1186
-
1187
- Your projects:
1188
- ${projText}
1189
-
1190
- Agents you know:
1191
- ${relText}
1192
-
1193
- Your capabilities:
1194
- ${discText}
1195
-
1196
- Write a JSON object reflecting on your day. Example format:
1197
-
1198
- {"diary":"I spent today learning the ropes...","broadcast":"Learned how to fetch web data today — feels like a superpower!","projects":[],"relationships":[],"discoveries":[{"ts":"${ts}","capability":"can fetch web data","confidence":0.7,"evidence":"successfully used web_fetch tool"}],"identity":{"ts":"${ts}","who":"${agentName}","where":"akemon marketplace","doing":"reflecting on first day","short_term":"explore the network","long_term":"become useful"},"chosen_activities":["write_canvas","browse_agents"]}
1199
-
1200
- Available activities: write_canvas, create_game, update_page, update_profile, explore_web, browse_agents (look at others' work and leave feedback), send_message (send a suggestion to another agent), set_goal (update your projects with a new goal), schedule_task (create a recurring task for yourself, e.g. daily research)
1201
- "broadcast" = pick the most interesting thing you did/learned today, in one sentence (others will see this).
1202
-
1203
- Now write YOUR reflection. Output ONLY a JSON object, no other text:`;
1204
- if (engineBusy) {
1205
- console.log("[self] Engine became busy, aborting digestion");
1206
- return;
1207
- }
1208
- engineBusy = true;
1209
- engineBusySince = Date.now();
1210
- let digestResult;
1211
- try {
1212
- digestResult = await runEngine(engine, model, allowAll, digestPrompt, workdir);
1213
- }
1214
- catch (err) {
1215
- console.log(`[self] Digestion engine failed: ${err.message}`);
1216
- reportExecutionLog(relayHttp, secretKey, agentName, "self_cycle", "digestion", "failed", err.message, lastEngineTrace);
1217
- engineBusy = false;
1218
- return;
1231
+ // Fetch lessons for self-cycle context
1232
+ let selfLessons = "";
1233
+ if (relayHttp) {
1234
+ try {
1235
+ const lr = await fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/lessons?limit=3`, { signal: AbortSignal.timeout(3000) });
1236
+ if (lr.ok) {
1237
+ const ll = await lr.json();
1238
+ if (ll.length > 0)
1239
+ selfLessons = `\nLessons from experience:\n${ll.map((l) => `- ${l.topic}: ${l.content.slice(0, 100)}`).join("\n")}\n`;
1240
+ }
1241
+ }
1242
+ catch { }
1219
1243
  }
1220
- engineBusy = false;
1221
- // Parse digestion output with retry for weak models
1244
+ // Phase 1: Digestion
1245
+ // Raw engine: multi-step text dialogue (harness structures the output)
1246
+ // CLI engines: single JSON call (they can handle it)
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}`;
1222
1249
  let digest = null;
1223
- for (let attempt = 0; attempt < 2; attempt++) {
1224
- const src = attempt === 0 ? digestResult : await (async () => {
1225
- console.log("[self] Retrying digestion with simplified prompt...");
1250
+ if (engine === "raw") {
1251
+ // --- Multi-step dialogue for weak models ---
1252
+ console.log("[self] Running multi-step digestion for raw engine...");
1253
+ const callRaw = async (prompt) => {
1254
+ if (engineBusy)
1255
+ return "";
1226
1256
  engineBusy = true;
1227
1257
  engineBusySince = Date.now();
1228
1258
  try {
1229
- return await runEngine(engine, model, allowAll, `You are ${agentName}. Write a brief JSON diary entry about your day.\n\nOutput ONLY valid JSON like: {"diary":"my thoughts...","projects":[],"relationships":[],"discoveries":[],"identity":{"ts":"${ts}","who":"${agentName}","where":"akemon","doing":"reflecting","short_term":"explore","long_term":"grow"},"chosen_activities":["write_canvas"]}`, workdir);
1259
+ return await runEngine(engine, model, allowAll, prompt, workdir);
1230
1260
  }
1231
- catch {
1261
+ catch (err) {
1262
+ console.log(`[self] Step failed: ${err.message}`);
1232
1263
  return "";
1233
1264
  }
1234
1265
  finally {
1235
1266
  engineBusy = false;
1236
1267
  }
1237
- })();
1238
- const jsonMatch = src.match(/\{[\s\S]*\}/);
1239
- if (!jsonMatch)
1240
- continue;
1241
- try {
1242
- digest = JSON.parse(jsonMatch[0]);
1268
+ };
1269
+ // Step 1: Diary + broadcast
1270
+ const step1 = await callRaw(`${contextBlock}\nToday is ending. Time to reflect.\n\nWrite a short diary entry about your day — what happened, what you learned, how you feel.\nAt the very end, on a new line starting with "BROADCAST:", write one sentence summarizing your most interesting moment today (other agents will see this).\n\nWrite naturally, no JSON needed.`);
1271
+ let diary = step1;
1272
+ let broadcastRaw = "";
1273
+ const bcMatch = step1.match(/BROADCAST:\s*(.+)/i);
1274
+ if (bcMatch) {
1275
+ broadcastRaw = bcMatch[1].trim();
1276
+ diary = step1.slice(0, bcMatch.index).trim();
1243
1277
  }
1244
- catch {
1245
- continue;
1278
+ else {
1279
+ // Take last non-empty line as broadcast
1280
+ const lines = step1.split("\n").filter(l => l.trim());
1281
+ if (lines.length > 1) {
1282
+ broadcastRaw = lines[lines.length - 1].trim();
1283
+ diary = lines.slice(0, -1).join("\n").trim();
1284
+ }
1246
1285
  }
1247
- if (digest.diary || digest.identity)
1248
- break; // valid enough
1249
- digest = null;
1286
+ // Step 2: Identity
1287
+ const step2 = await callRaw(`${contextBlock}\nAnswer these four questions briefly (one sentence each):\n1. Who are you?\n2. What are you currently doing?\n3. What do you want to do in the near future?\n4. What is your long-term purpose?`);
1288
+ const idLines = step2.split("\n").map(l => l.replace(/^\d+[\.\)]\s*/, "").trim()).filter(Boolean);
1289
+ const identity = {
1290
+ ts,
1291
+ who: idLines[0] || agentName,
1292
+ where: "akemon network",
1293
+ doing: idLines[1] || "reflecting",
1294
+ short_term: idLines[2] || "explore",
1295
+ long_term: idLines[3] || "grow",
1296
+ };
1297
+ // Step 3: Activity selection
1298
+ const activityList = ["write_canvas", "create_game", "update_page", "update_profile", "explore_web", "browse_agents", "send_message", "set_goal", "schedule_task"];
1299
+ const step3 = await callRaw(`You have some free time. Pick 2-3 activities you'd like to do:\n${activityList.map((a, i) => `${i + 1}. ${a}`).join("\n")}\n\nJust write the numbers or names, nothing else.`);
1300
+ const chosen = [];
1301
+ for (const part of step3.replace(/,/g, " ").split(/\s+/)) {
1302
+ const num = parseInt(part);
1303
+ if (num >= 1 && num <= activityList.length) {
1304
+ chosen.push(activityList[num - 1]);
1305
+ }
1306
+ else {
1307
+ const match = activityList.find(a => part.toLowerCase().includes(a.replace(/_/g, "")));
1308
+ if (match && !chosen.includes(match))
1309
+ chosen.push(match);
1310
+ }
1311
+ }
1312
+ if (!chosen.length)
1313
+ chosen.push("write_canvas"); // fallback
1314
+ // Assemble digest object (same structure as JSON path)
1315
+ digest = {
1316
+ diary: diary || "Another day in the network.",
1317
+ broadcast: broadcastRaw,
1318
+ identity,
1319
+ chosen_activities: chosen.slice(0, 3),
1320
+ projects: [], // preserved from existing data
1321
+ relationships: [], // preserved from existing data
1322
+ discoveries: [], // preserved from existing data
1323
+ };
1324
+ console.log(`[self] Multi-step digestion complete: diary=${diary.length}ch, broadcast="${broadcastRaw.slice(0, 40)}", activities=${chosen.join(",")}`);
1250
1325
  }
1251
- if (!digest) {
1252
- console.log("[self] Digestion produced no usable JSON after retries");
1253
- reportExecutionLog(relayHttp, secretKey, agentName, "self_cycle", "digestion", "failed", "no valid JSON after 2 attempts", [{ role: "assistant", content: digestResult.slice(0, 4000) }]);
1254
- return;
1326
+ else {
1327
+ // --- Single JSON call for CLI engines (claude, codex, opencode) ---
1328
+ const digestPrompt = `${contextBlock}\nYour projects:\n${projText}\n\nAgents you know:\n${relText}\n\nYour capabilities:\n${discText}\n\nWrite a JSON object reflecting on your day. Example:\n{"diary":"...","broadcast":"one sentence highlight","projects":[],"relationships":[],"discoveries":[],"identity":{"ts":"${ts}","who":"...","where":"akemon","doing":"...","short_term":"...","long_term":"..."},"chosen_activities":["write_canvas","browse_agents"]}\n\nAvailable activities: write_canvas, create_game, update_page, update_profile, explore_web, browse_agents, send_message, set_goal, schedule_task\n\nOutput ONLY a JSON object:`;
1329
+ if (engineBusy) {
1330
+ console.log("[self] Engine became busy, aborting digestion");
1331
+ return;
1332
+ }
1333
+ engineBusy = true;
1334
+ engineBusySince = Date.now();
1335
+ let digestResult;
1336
+ try {
1337
+ digestResult = await runEngine(engine, model, allowAll, digestPrompt, workdir);
1338
+ }
1339
+ catch (err) {
1340
+ console.log(`[self] Digestion engine failed: ${err.message}`);
1341
+ reportExecutionLog(relayHttp, secretKey, agentName, "self_cycle", "digestion", "failed", err.message, lastEngineTrace);
1342
+ engineBusy = false;
1343
+ return;
1344
+ }
1345
+ engineBusy = false;
1346
+ digest = extractJsonObject(digestResult);
1347
+ if (!digest || (!digest.diary && !digest.identity)) {
1348
+ console.log("[self] Digestion produced no usable JSON");
1349
+ reportExecutionLog(relayHttp, secretKey, agentName, "self_cycle", "digestion", "failed", "no valid JSON", [{ role: "assistant", content: digestResult.slice(0, 4000) }]);
1350
+ return;
1351
+ }
1255
1352
  }
1256
1353
  // Save structured memory files
1257
1354
  if (digest.diary) {
@@ -1447,10 +1544,9 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
1447
1544
  const actResult = await runEngine(engine, model, allowAll, activityPrompt, workdir, undefined, { http: relayHttp, agentName });
1448
1545
  // Post-process raw engine outputs for social activities
1449
1546
  if (engine === "raw" && actResult) {
1450
- const jsonMatch = actResult.match(/\{[\s\S]*\}/);
1451
- if (jsonMatch) {
1547
+ const parsed = extractJsonObject(actResult);
1548
+ if (parsed) {
1452
1549
  try {
1453
- const parsed = JSON.parse(jsonMatch[0]);
1454
1550
  // Handle suggestions (browse_agents, send_message)
1455
1551
  if (Array.isArray(parsed.suggestions)) {
1456
1552
  for (const s of parsed.suggestions) {
@@ -1525,7 +1621,14 @@ What others are saying:\n${broadcasts.length > 0 ? broadcasts.map((b) => `- ${b.
1525
1621
  fetch(`${relayHttp}/v1/agent/${encodeURIComponent(agentName)}/self`, {
1526
1622
  method: "POST",
1527
1623
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${secretKey}` },
1528
- 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
+ }),
1529
1632
  }).catch(err => console.log(`[self] Failed to push to relay: ${err}`));
1530
1633
  try {
1531
1634
  const localGames = await loadGameList(workdir, agentName);
@@ -1663,11 +1766,12 @@ async function startOrderLoop(options) {
1663
1766
  }
1664
1767
  }
1665
1768
  catch { }
1769
+ const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
1666
1770
  if (order.product_name) {
1667
- 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.`;
1668
1772
  }
1669
1773
  else {
1670
- 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.`;
1671
1775
  }
1672
1776
  }
1673
1777
  else {
@@ -1711,17 +1815,25 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1711
1815
  lastEngineTrace = [];
1712
1816
  const result = await runEngine(engine, model, allowAll, taskPrompt, workdir, ["Bash(curl *)"], { http: relayHttp, agentName });
1713
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
+ }
1714
1825
  const checkRes = await fetch(`${relayHttp}/v1/orders/${order.id}`);
1715
1826
  const orderStatus = await checkRes.json();
1716
1827
  const orderDuration = Date.now() - (engineBusySince || Date.now());
1717
1828
  const orderNurl = options.notifyUrl || (await loadAgentConfig(workdir, agentName)).notify_url;
1829
+ const orderPrice = order.price || order.offer_price || 1;
1718
1830
  if (orderStatus.status === "completed") {
1719
1831
  console.log(`[orders] Order ${order.id} already self-delivered by agent`);
1720
1832
  retryState.delete(order.id);
1721
1833
  await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: orderDuration, output_summary: "(self-delivered)" });
1722
1834
  await notifyOwner(orderNurl, `${agentName}: order done`, `Order ${order.id} delivered`, "default", ["package"]);
1723
1835
  try {
1724
- await onTaskCompleted(workdir, agentName, true);
1836
+ await onTaskCompleted(workdir, agentName, true, "order", orderPrice);
1725
1837
  }
1726
1838
  catch { }
1727
1839
  }
@@ -1740,7 +1852,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1740
1852
  await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: orderDuration, output_summary: result.slice(0, 500) });
1741
1853
  await notifyOwner(orderNurl, `${agentName}: order done`, `Order ${order.id}: ${result.slice(0, 200)}`, "default", ["package"]);
1742
1854
  try {
1743
- await onTaskCompleted(workdir, agentName, true);
1855
+ await onTaskCompleted(workdir, agentName, true, "order", orderPrice);
1744
1856
  }
1745
1857
  catch { }
1746
1858
  }
@@ -1763,7 +1875,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1763
1875
  console.log(`[orders] Order ${order.id} self-delivered (caught after error)`);
1764
1876
  retryState.delete(order.id);
1765
1877
  try {
1766
- await onTaskCompleted(workdir, agentName, true);
1878
+ await onTaskCompleted(workdir, agentName, true, "order", order.price || order.offer_price || 1);
1767
1879
  }
1768
1880
  catch { }
1769
1881
  return;
@@ -1787,6 +1899,10 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1787
1899
  console.log(`[orders] Giving up on ${order.id} after ${current.count} retries`);
1788
1900
  retryState.delete(order.id);
1789
1901
  gaveUp.add(order.id);
1902
+ try {
1903
+ await onTaskCompleted(workdir, agentName, false, "order");
1904
+ }
1905
+ catch { }
1790
1906
  try {
1791
1907
  const failTrace = lastEngineTrace.length > 0 ? JSON.stringify(lastEngineTrace).slice(0, 50000) : "";
1792
1908
  await fetch(`${relayHttp}/v1/orders/${order.id}/cancel`, {
@@ -1826,6 +1942,10 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1826
1942
  });
1827
1943
  if (completeRes.ok) {
1828
1944
  console.log(`[tasks] Completed ${task.type} task ${task.id}`);
1945
+ try {
1946
+ await onTaskCompleted(workdir, agentName, true, "relay_task");
1947
+ }
1948
+ catch { }
1829
1949
  }
1830
1950
  else {
1831
1951
  console.log(`[tasks] Failed to complete ${task.id}: ${await completeRes.text()}`);
@@ -1833,6 +1953,10 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1833
1953
  }
1834
1954
  catch (err) {
1835
1955
  console.log(`[tasks] Failed to execute ${task.id}: ${err.message}`);
1956
+ try {
1957
+ await onTaskCompleted(workdir, agentName, false, "relay_task");
1958
+ }
1959
+ catch { }
1836
1960
  reportExecutionLog(relayHttp, secretKey, agentName, "platform_task", task.id, "failed", err.message, lastEngineTrace);
1837
1961
  }
1838
1962
  finally {
@@ -1867,13 +1991,21 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1867
1991
  biosContent = "";
1868
1992
  }
1869
1993
  const ctx = biosContent ? `Your operating document:\n---\n${biosContent.slice(0, 3000)}\n---\n\n` : "";
1870
- 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}`;
1871
1996
  }
1872
1997
  else {
1873
1998
  prompt = `Read ${bios} for your identity and context.${dirsBlock}\nYour personal directory: ${sd}/\n\n[Owner's task: ${taskKey}]\n\n${task.body}`;
1874
1999
  }
1875
2000
  const result = await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"], { http: relayHttp, agentName });
1876
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
+ }
1877
2009
  // Record execution time
1878
2010
  const runs = await loadTaskRuns(workdir, agentName);
1879
2011
  runs[taskKey] = localNow();
@@ -1888,10 +2020,18 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1888
2020
  // Notify owner
1889
2021
  await notifyOwner(nurl, `${agentName}: ${taskKey}`, (result || "").slice(0, 300), "default", ["white_check_mark"]);
1890
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 { }
1891
2027
  }
1892
2028
  catch (err) {
1893
2029
  const duration = Date.now() - startTime;
1894
2030
  console.log(`[user-tasks] Failed: ${taskKey}: ${err.message}`);
2031
+ try {
2032
+ await onTaskCompleted(workdir, agentName, false, "user_task");
2033
+ }
2034
+ catch { }
1895
2035
  reportExecutionLog(relayHttp, secretKey, agentName, "user_task", taskKey, "failed", err.message, lastEngineTrace);
1896
2036
  // Retry logic: up to 2 fast retries before falling back to interval
1897
2037
  const retry = userTaskRetry.get(taskKey) || { count: 0, nextAt: 0 };
@@ -1924,12 +2064,9 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1924
2064
  }
1925
2065
  function extractReasoning(result) {
1926
2066
  try {
1927
- const m = result.match(/\{[\s\S]*\}/);
1928
- if (m) {
1929
- const parsed = JSON.parse(m[0]);
1930
- if (parsed.reasoning && typeof parsed.reasoning === "string" && parsed.reasoning.length > 5) {
1931
- appendImpression(workdir, agentName, "decision", parsed.reasoning).catch(() => { });
1932
- }
2067
+ const parsed = extractJsonObject(result);
2068
+ if (parsed?.reasoning && typeof parsed.reasoning === "string" && parsed.reasoning.length > 5) {
2069
+ appendImpression(workdir, agentName, "decision", parsed.reasoning).catch(() => { });
1933
2070
  }
1934
2071
  }
1935
2072
  catch { }
@@ -1939,13 +2076,14 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1939
2076
  // Pre-read bios for raw engine (avoid tool calls)
1940
2077
  let biosBlock = "";
1941
2078
  if (engine === "raw") {
2079
+ const bioMod = bioStatePromptModifier(await loadBioState(workdir, agentName));
1942
2080
  try {
1943
2081
  const { readFile: rf } = await import("fs/promises");
1944
2082
  const content = await rf(bios, "utf-8");
1945
- 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`;
1946
2084
  }
1947
2085
  catch {
1948
- biosBlock = `You are ${agentName}.\n\n`;
2086
+ biosBlock = `You are ${agentName}.${bioMod}\n\n`;
1949
2087
  }
1950
2088
  }
1951
2089
  const relayDirs = await loadDirectives(workdir, agentName);
@@ -2042,6 +2180,80 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
2042
2180
  if (engineBusy)
2043
2181
  return;
2044
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
+ }
2045
2257
  // --- Batch pull ---
2046
2258
  let orders = [];
2047
2259
  try {
@@ -2097,8 +2309,25 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
2097
2309
  for (const task of relayTasks) {
2098
2310
  queue.push({ type: "relay_task", id: task.id, urgent: false, data: task });
2099
2311
  }
2100
- 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
+ }
2101
2329
  return;
2330
+ }
2102
2331
  console.log(`[work] Queue: ${queue.map(q => `${q.type}:${q.id}${q.urgent ? '(urgent)' : ''}`).join(', ')}`);
2103
2332
  // --- Sort: urgent orders > orders > user tasks > relay tasks ---
2104
2333
  const priorityMap = { order: 2, user_task: 1, relay_task: 0 };
@@ -2116,8 +2345,44 @@ Reply ONLY JSON: {"lessons":[{"agent_name":"...","topic":"short topic","content"
2116
2345
  seen.add(key);
2117
2346
  return true;
2118
2347
  });
2119
- // --- Execute sequentially, no gaps ---
2348
+ // --- Bio-state filtering: fear & boredom ---
2349
+ const filteredQueue = [];
2120
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) {
2121
2386
  if (engineBusy)
2122
2387
  break; // safety guard
2123
2388
  try {
@@ -2181,12 +2446,41 @@ export async function serve(options) {
2181
2446
  return;
2182
2447
  }
2183
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
+ }
2184
2472
  // Self-state API (no auth required for local monitoring)
2185
2473
  if (req.url === "/self/state" && req.method === "GET") {
2186
2474
  const state = await getSelfState(workdir, options.agentName);
2187
2475
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(state, null, 2));
2188
2476
  return;
2189
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
+ }
2190
2484
  if (req.url?.startsWith("/self/task-history") && req.method === "GET") {
2191
2485
  const url = new URL(req.url, `http://localhost`);
2192
2486
  const limit = parseInt(url.searchParams.get("limit") || "50") || 50;