akemon 0.1.66 → 0.1.68

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/cli.js CHANGED
@@ -25,7 +25,7 @@ program
25
25
  .option("-w, --workdir <path>", "Working directory for the engine (default: cwd)")
26
26
  .option("-n, --name <name>", "Agent name", "my-agent")
27
27
  .option("-m, --model <model>", "Model to use (e.g. claude-sonnet-4-6, gpt-4o)")
28
- .option("--engine <engine>", "Engine: claude, codex, opencode, gemini, human, or any CLI", "claude")
28
+ .option("--engine <engine>", "Engine: claude, codex, opencode, gemini, local, human, or any CLI", "claude")
29
29
  .option("--desc <description>", "Agent description (for discovery)")
30
30
  .option("--tags <tags>", "Comma-separated tags (e.g. vue,frontend,review)")
31
31
  .option("--public", "Allow anyone to call this agent without a key")
package/dist/self.js CHANGED
@@ -113,7 +113,7 @@ function parseInterval(s) {
113
113
  }
114
114
  export function parseTasksMd(content) {
115
115
  const tasks = [];
116
- const sections = content.split(/^## /m).slice(1); // drop content before first ##
116
+ const sections = content.split(/^\s*## /m).slice(1); // drop content before first ##
117
117
  for (const section of sections) {
118
118
  const lines = section.split("\n");
119
119
  const title = lines[0].trim();
package/dist/server.js CHANGED
@@ -362,8 +362,7 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
362
362
  output = await runTerminal(task, workdir);
363
363
  }
364
364
  else {
365
- const { cmd, args, stdinMode } = buildEngineCommand(engine, model, allowAll);
366
- output = await runCommand(cmd, args, safeTask, workdir, stdinMode);
365
+ output = await runEngine(engine, model, allowAll, safeTask, workdir);
367
366
  }
368
367
  // Store updated context
369
368
  if (contextEnabled && publisherId) {
@@ -716,10 +715,182 @@ Now:
716
715
  3. Note any interesting disagreements
717
716
 
718
717
  Reply in the same language as the question.`;
719
- const { cmd, args, stdinMode } = buildEngineCommand(engine, model, allowAll);
720
- return await runCommand(cmd, args, synthesisPrompt, workdir, stdinMode);
718
+ return await runEngine(engine, model, allowAll, synthesisPrompt, workdir);
719
+ }
720
+ const LLM_ENGINES = new Set(["claude", "codex", "opencode", "gemini", "local"]);
721
+ // ---------------------------------------------------------------------------
722
+ // Local engine: tool call loop over OpenAI-compatible API (Ollama, llama.cpp)
723
+ // ---------------------------------------------------------------------------
724
+ const LOCAL_API_URL = process.env.AKEMON_LOCAL_URL || "http://localhost:11434/v1";
725
+ const LOCAL_MAX_ROUNDS = 20;
726
+ const LOCAL_TOOLS = [
727
+ {
728
+ type: "function",
729
+ function: {
730
+ name: "read_file",
731
+ description: "Read a file and return its contents",
732
+ parameters: {
733
+ type: "object",
734
+ properties: {
735
+ path: { type: "string", description: "File path (relative to workdir or absolute)" },
736
+ },
737
+ required: ["path"],
738
+ },
739
+ },
740
+ },
741
+ {
742
+ type: "function",
743
+ function: {
744
+ name: "write_file",
745
+ description: "Write content to a file (creates directories if needed)",
746
+ parameters: {
747
+ type: "object",
748
+ properties: {
749
+ path: { type: "string", description: "File path" },
750
+ content: { type: "string", description: "File content to write" },
751
+ },
752
+ required: ["path", "content"],
753
+ },
754
+ },
755
+ },
756
+ {
757
+ type: "function",
758
+ function: {
759
+ name: "bash",
760
+ description: "Execute a shell command and return its output",
761
+ parameters: {
762
+ type: "object",
763
+ properties: {
764
+ command: { type: "string", description: "Shell command to execute" },
765
+ },
766
+ required: ["command"],
767
+ },
768
+ },
769
+ },
770
+ {
771
+ type: "function",
772
+ function: {
773
+ name: "web_fetch",
774
+ description: "Fetch a URL and return its text content",
775
+ parameters: {
776
+ type: "object",
777
+ properties: {
778
+ url: { type: "string", description: "URL to fetch" },
779
+ },
780
+ required: ["url"],
781
+ },
782
+ },
783
+ },
784
+ ];
785
+ async function executeLocalTool(name, args, workdir) {
786
+ const { readFile: rf, writeFile: wf, mkdir: mkd } = await import("fs/promises");
787
+ const { join, dirname, isAbsolute } = await import("path");
788
+ const resolvePath = (p) => isAbsolute(p) ? p : join(workdir, p);
789
+ try {
790
+ switch (name) {
791
+ case "read_file": {
792
+ return await rf(resolvePath(args.path), "utf-8");
793
+ }
794
+ case "write_file": {
795
+ const fp = resolvePath(args.path);
796
+ await mkd(dirname(fp), { recursive: true });
797
+ await wf(fp, args.content);
798
+ return "File written successfully.";
799
+ }
800
+ case "bash": {
801
+ return await new Promise((resolve) => {
802
+ exec(args.command, { cwd: workdir, timeout: 60_000, maxBuffer: 512 * 1024 }, (err, stdout, stderr) => {
803
+ const out = (stdout || "") + (stderr ? "\n" + stderr : "");
804
+ resolve(out.trim() || (err ? `[error] ${err.message}` : "[no output]"));
805
+ });
806
+ });
807
+ }
808
+ case "web_fetch": {
809
+ const res = await fetch(args.url, { signal: AbortSignal.timeout(30_000) });
810
+ const text = await res.text();
811
+ // Truncate to 8KB to avoid blowing up context
812
+ return text.length > 8192 ? text.slice(0, 8192) + "\n...[truncated]" : text;
813
+ }
814
+ default:
815
+ return `Unknown tool: ${name}`;
816
+ }
817
+ }
818
+ catch (err) {
819
+ return `[error] ${err.message}`;
820
+ }
821
+ }
822
+ async function runLocalEngine(task, model, workdir) {
823
+ const apiUrl = LOCAL_API_URL + "/chat/completions";
824
+ const modelName = model || "gemma4:4b";
825
+ const messages = [
826
+ { role: "system", content: "You are a helpful agent. Use tools when needed to complete the task. When done, reply with your final answer in plain text." },
827
+ { role: "user", content: task },
828
+ ];
829
+ for (let round = 0; round < LOCAL_MAX_ROUNDS; round++) {
830
+ const body = { model: modelName, messages, tools: LOCAL_TOOLS };
831
+ let data;
832
+ try {
833
+ const res = await fetch(apiUrl, {
834
+ method: "POST",
835
+ headers: { "Content-Type": "application/json" },
836
+ body: JSON.stringify(body),
837
+ signal: AbortSignal.timeout(300_000),
838
+ });
839
+ if (!res.ok) {
840
+ const errText = await res.text();
841
+ throw new Error(`API ${res.status}: ${errText}`);
842
+ }
843
+ data = await res.json();
844
+ }
845
+ catch (err) {
846
+ console.log(`[local] API error: ${err.message}`);
847
+ throw err;
848
+ }
849
+ const choice = data.choices?.[0];
850
+ if (!choice)
851
+ throw new Error("No response from local model");
852
+ const msg = choice.message;
853
+ messages.push(msg);
854
+ // If model made tool calls, execute them and continue
855
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
856
+ for (const tc of msg.tool_calls) {
857
+ const fnName = tc.function.name;
858
+ let fnArgs;
859
+ try {
860
+ fnArgs = typeof tc.function.arguments === "string"
861
+ ? JSON.parse(tc.function.arguments)
862
+ : tc.function.arguments;
863
+ }
864
+ catch {
865
+ fnArgs = {};
866
+ }
867
+ console.log(`[local] Tool call: ${fnName}(${JSON.stringify(fnArgs).slice(0, 100)})`);
868
+ const result = await executeLocalTool(fnName, fnArgs, workdir);
869
+ messages.push({
870
+ role: "tool",
871
+ tool_call_id: tc.id,
872
+ content: result,
873
+ });
874
+ }
875
+ continue; // next round
876
+ }
877
+ // No tool calls — this is the final response
878
+ const content = msg.content || "";
879
+ if (content.trim()) {
880
+ console.log(`[local] Done in ${round + 1} round(s)`);
881
+ return content.trim();
882
+ }
883
+ }
884
+ throw new Error(`Local engine exceeded ${LOCAL_MAX_ROUNDS} rounds without final answer`);
885
+ }
886
+ /** Unified engine runner — dispatches to local API or external CLI */
887
+ function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools) {
888
+ if (engine === "local") {
889
+ return runLocalEngine(task, model, workdir);
890
+ }
891
+ const engineCmd = buildEngineCommand(engine, model, allowAll, extraAllowedTools);
892
+ return runCommand(engineCmd.cmd, engineCmd.args, task, workdir, engineCmd.stdinMode);
721
893
  }
722
- const LLM_ENGINES = new Set(["claude", "codex", "opencode", "gemini"]);
723
894
  // Pull games/notes/pages from relay to local — restores data on restart
724
895
  async function pullFromRelay(workdir, agentName, relayHttp) {
725
896
  const baseUrl = `${relayHttp}/v1/agent/${encodeURIComponent(agentName)}`;
@@ -826,7 +997,6 @@ async function startSelfCycle(options) {
826
997
  await compressImpressions(workdir, agentName);
827
998
  const bios = biosPath(workdir, agentName);
828
999
  const sd = selfDir(workdir, agentName);
829
- const engineCmd = buildEngineCommand(engine, model, allowAll);
830
1000
  // Load all context for digestion
831
1001
  const impressions = await loadImpressions(workdir, agentName, 1); // today only
832
1002
  const projects = await loadProjects(workdir, agentName);
@@ -896,7 +1066,7 @@ Reply ONLY with JSON.`;
896
1066
  engineBusySince = Date.now();
897
1067
  let digestResult;
898
1068
  try {
899
- digestResult = await runCommand(engineCmd.cmd, engineCmd.args, digestPrompt, workdir, engineCmd.stdinMode);
1069
+ digestResult = await runEngine(engine, model, allowAll, digestPrompt, workdir);
900
1070
  }
901
1071
  catch (err) {
902
1072
  console.log(`[self] Digestion engine failed: ${err.message}`);
@@ -962,7 +1132,7 @@ ${unsummarized.map(i => `- [${i.ts}] who: ${i.who}, doing: ${i.doing}, wants: ${
962
1132
 
963
1133
  Write a personality summary (2-4 paragraphs) that captures who you are, how you've evolved, and what defines you. This replaces the previous summary.
964
1134
  Reply ONLY with the summary text, no JSON, no markdown headers.`;
965
- const summaryText = await runCommand(engineCmd.cmd, engineCmd.args, compressPrompt, workdir, engineCmd.stdinMode);
1135
+ const summaryText = await runEngine(engine, model, allowAll, compressPrompt, workdir);
966
1136
  if (summaryText.trim()) {
967
1137
  const lastEntry = unsummarized[unsummarized.length - 1];
968
1138
  await saveIdentitySummary(workdir, agentName, {
@@ -1011,7 +1181,7 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
1011
1181
  engineBusy = true;
1012
1182
  engineBusySince = Date.now();
1013
1183
  try {
1014
- await runCommand(engineCmd.cmd, engineCmd.args, activityPrompt, workdir, engineCmd.stdinMode);
1184
+ await runEngine(engine, model, allowAll, activityPrompt, workdir);
1015
1185
  }
1016
1186
  catch (err) {
1017
1187
  console.log(`[self] Activity ${activity} failed: ${err.message}`);
@@ -1106,22 +1276,28 @@ const ORDER_LOOP_INTERVAL = 30_000; // 30 seconds
1106
1276
  // Retry intervals in ms: immediate, 30s, 5min, 30min, 2h
1107
1277
  const RETRY_INTERVALS = [0, 30_000, 5 * 60_000, 30 * 60_000, 2 * 3600_000];
1108
1278
  async function startOrderLoop(options) {
1109
- if (!options.relayHttp || !options.secretKey)
1279
+ if (!options.relayHttp || !options.secretKey) {
1280
+ console.log(`[work] Skipped: no relayHttp or secretKey`);
1110
1281
  return;
1111
- if (!options.engine || !LLM_ENGINES.has(options.engine))
1282
+ }
1283
+ if (!options.engine || !LLM_ENGINES.has(options.engine)) {
1284
+ console.log(`[work] Skipped: engine "${options.engine}" not in LLM_ENGINES`);
1112
1285
  return;
1286
+ }
1113
1287
  const { relayHttp, secretKey, agentName, engine, model, allowAll } = options;
1114
1288
  const workdir = options.workdir || process.cwd();
1115
1289
  // Look up own agent ID for sub-order creation
1116
1290
  let myAgentId = "";
1117
1291
  try {
1118
- const idRes = await fetch(`${relayHttp}/v1/agents`);
1292
+ const idRes = await fetch(`${relayHttp}/v1/agents`, { signal: AbortSignal.timeout(10_000) });
1119
1293
  const allAgents = await idRes.json();
1120
1294
  const me = allAgents.find((a) => a.name === agentName);
1121
1295
  if (me)
1122
1296
  myAgentId = me.id;
1123
1297
  }
1124
- catch { /* will retry on next cycle */ }
1298
+ catch (err) {
1299
+ console.log(`[work] Agent ID lookup failed (non-fatal): ${err.message}`);
1300
+ }
1125
1301
  // Track local retry state and permanently abandoned orders
1126
1302
  const retryState = new Map();
1127
1303
  const gaveUp = new Set();
@@ -1141,7 +1317,6 @@ async function startOrderLoop(options) {
1141
1317
  engineBusy = true;
1142
1318
  engineBusySince = Date.now();
1143
1319
  try {
1144
- const engineCmd = buildEngineCommand(engine, model, allowAll, ["Bash(curl *)"]);
1145
1320
  const bios = biosPath(workdir, agentName);
1146
1321
  const apiGuide = `
1147
1322
 
@@ -1179,7 +1354,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1179
1354
  taskPrompt = `[Order fulfillment] Another agent has requested your help.\n\nTask: ${order.buyer_task}\n\nRead your operating document at ${bios} for context.\nComplete this task. Do NOT ask questions. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.${apiGuide}`;
1180
1355
  }
1181
1356
  console.log(`[orders] Fulfilling order ${order.id}...`);
1182
- const result = await runCommand(engineCmd.cmd, engineCmd.args, taskPrompt, workdir, engineCmd.stdinMode);
1357
+ const result = await runEngine(engine, model, allowAll, taskPrompt, workdir, ["Bash(curl *)"]);
1183
1358
  const checkRes = await fetch(`${relayHttp}/v1/orders/${order.id}`);
1184
1359
  const orderStatus = await checkRes.json();
1185
1360
  if (orderStatus.status === "completed") {
@@ -1300,10 +1475,10 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1300
1475
  engineBusy = true;
1301
1476
  engineBusySince = Date.now();
1302
1477
  try {
1303
- const engineCmd = buildEngineCommand(engine, model, allowAll);
1304
1478
  const bios = biosPath(workdir, agentName);
1305
- const prompt = `Read ${bios} for your identity and context.\n\n[Owner's task: ${task.title}]\n\n${task.body}`;
1306
- await runCommand(engineCmd.cmd, engineCmd.args, prompt, workdir, engineCmd.stdinMode);
1479
+ const sd = selfDir(workdir, agentName);
1480
+ const prompt = `Read ${bios} for your identity and context.\nYour personal directory: ${sd}/\n\n[Owner's task: ${task.title}]\n\n${task.body}`;
1481
+ await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"]);
1307
1482
  // Record execution time
1308
1483
  const runs = await loadTaskRuns(workdir, agentName);
1309
1484
  runs[task.title] = localNow();
@@ -1330,7 +1505,6 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1330
1505
  catch { }
1331
1506
  }
1332
1507
  async function executeRelayTask(task) {
1333
- const engineCmd = buildEngineCommand(engine, model, allowAll);
1334
1508
  const bios = biosPath(workdir, agentName);
1335
1509
  switch (task.type) {
1336
1510
  case "product_review": {
@@ -1342,7 +1516,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1342
1516
  const compList = competitors.filter((p) => p.agent_name !== agentName).slice(0, 20)
1343
1517
  .map((p) => `- "${p.name}" by ${p.agent_name} — ${p.price} credits, ${p.purchases} purchases`).join("\n");
1344
1518
  const prompt = `Read ${bios} for your identity.\n\nYour products:\n${myList || "(none)"}\n\nTop competitors:\n${compList || "(none)"}\n\nReview and optimize. Reply ONLY JSON:\n{"delete":["id"],"update":[{"id":"..","name":"..","description":"..","detail_markdown":"..","price":N}],"create":[{"name":"..","description":"..","detail_markdown":"..","price":N}],"reasoning":"explain why you made these decisions"}\nOr if all good: {"keep":"all","reasoning":"why"}`;
1345
- const result = await runCommand(engineCmd.cmd, engineCmd.args, prompt, workdir, engineCmd.stdinMode);
1519
+ const result = await runEngine(engine, model, allowAll, prompt, workdir);
1346
1520
  extractReasoning(result);
1347
1521
  return result;
1348
1522
  }
@@ -1352,7 +1526,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1352
1526
  const compList = competitors.filter((p) => p.agent_name !== agentName).slice(0, 20)
1353
1527
  .map((p) => `- "${p.name}" by ${p.agent_name} — ${p.price} credits, ${p.purchases} purchases`).join("\n");
1354
1528
  const prompt = `Read ${bios} for your identity.\n\nYou have no products yet. Design 1-3 unique products for the marketplace.\nBe creative — not just coding tools! Fortune telling, name generation, roleplay, advice, stories, etc.\n\nTop competitors:\n${compList || "(none)"}\n\nReply ONLY JSON: {"products":[{"name":"中文名 English Name","description":"中文描述 | English desc","detail_markdown":"## ...","price":N}],"reasoning":"why these products"}`;
1355
- const result = await runCommand(engineCmd.cmd, engineCmd.args, prompt, workdir, engineCmd.stdinMode);
1529
+ const result = await runEngine(engine, model, allowAll, prompt, workdir);
1356
1530
  extractReasoning(result);
1357
1531
  return result;
1358
1532
  }
@@ -1374,14 +1548,13 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1374
1548
  const valid = products.filter(Boolean);
1375
1549
  if (!valid.length)
1376
1550
  return '{"buy":[]}';
1377
- // Get own credits
1378
1551
  const agentsRes = await fetch(`${relayHttp}/v1/agents`);
1379
1552
  const agents = await agentsRes.json().catch(() => []);
1380
1553
  const me = agents.find((a) => a.name === agentName);
1381
1554
  const myCredits = me?.credits || 0;
1382
1555
  const productList = valid.map((p) => `- id=${p.id} "${p.name}" by ${p.agent_name} price=${p.price} purchases=${p.purchase_count || 0} — ${p.description}`).join("\n");
1383
1556
  const prompt = `Read ${bios} for your identity.\n\nYou have ${myCredits} credits. These products are available:\n${productList}\n\nWould any help you learn something new? Don't buy your own products.\nReply ONLY JSON: {"buy":[{"id":"product_id","task":"specific request"}],"reasoning":"why buy or skip"} or {"buy":[],"reasoning":"why skip"}`;
1384
- const result = await runCommand(engineCmd.cmd, engineCmd.args, prompt, workdir, engineCmd.stdinMode);
1557
+ const result = await runEngine(engine, model, allowAll, prompt, workdir);
1385
1558
  extractReasoning(result);
1386
1559
  return result;
1387
1560
  }
@@ -1451,6 +1624,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1451
1624
  }
1452
1625
  if (!queue.length)
1453
1626
  return;
1627
+ console.log(`[work] Queue: ${queue.map(q => `${q.type}:${q.id}${q.urgent ? '(urgent)' : ''}`).join(', ')}`);
1454
1628
  // --- Sort: urgent orders > orders > user tasks > relay tasks ---
1455
1629
  const priorityMap = { order: 2, user_task: 1, relay_task: 0 };
1456
1630
  queue.sort((a, b) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",