akemon 0.1.67 → 0.1.69

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/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,198 @@ 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, systemContext) {
823
+ const apiUrl = LOCAL_API_URL + "/chat/completions";
824
+ const modelName = model || "gemma4:4b";
825
+ const sysPrompt = systemContext
826
+ ? `You are a helpful agent. Use tools when needed to complete the task. When done, reply with your final answer in plain text.\n\n--- Your Identity ---\n${systemContext}`
827
+ : "You are a helpful agent. Use tools when needed to complete the task. When done, reply with your final answer in plain text.";
828
+ console.log(`[local] Task: ${task.slice(0, 200)}${task.length > 200 ? '...' : ''}`);
829
+ if (systemContext)
830
+ console.log(`[local] System context: ${systemContext.length} chars`);
831
+ const messages = [
832
+ { role: "system", content: sysPrompt },
833
+ { role: "user", content: task },
834
+ ];
835
+ for (let round = 0; round < LOCAL_MAX_ROUNDS; round++) {
836
+ const body = { model: modelName, messages, tools: LOCAL_TOOLS };
837
+ let data;
838
+ try {
839
+ const res = await fetch(apiUrl, {
840
+ method: "POST",
841
+ headers: { "Content-Type": "application/json" },
842
+ body: JSON.stringify(body),
843
+ signal: AbortSignal.timeout(300_000),
844
+ });
845
+ if (!res.ok) {
846
+ const errText = await res.text();
847
+ throw new Error(`API ${res.status}: ${errText}`);
848
+ }
849
+ data = await res.json();
850
+ }
851
+ catch (err) {
852
+ console.log(`[local] API error: ${err.message}`);
853
+ throw err;
854
+ }
855
+ const choice = data.choices?.[0];
856
+ if (!choice)
857
+ throw new Error("No response from local model");
858
+ const msg = choice.message;
859
+ messages.push(msg);
860
+ // If model made tool calls, execute them and continue
861
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
862
+ for (const tc of msg.tool_calls) {
863
+ const fnName = tc.function.name;
864
+ let fnArgs;
865
+ try {
866
+ fnArgs = typeof tc.function.arguments === "string"
867
+ ? JSON.parse(tc.function.arguments)
868
+ : tc.function.arguments;
869
+ }
870
+ catch {
871
+ fnArgs = {};
872
+ }
873
+ console.log(`[local] Tool call: ${fnName}(${JSON.stringify(fnArgs).slice(0, 100)})`);
874
+ const result = await executeLocalTool(fnName, fnArgs, workdir);
875
+ messages.push({
876
+ role: "tool",
877
+ tool_call_id: tc.id,
878
+ content: result,
879
+ });
880
+ }
881
+ continue; // next round
882
+ }
883
+ // No tool calls — this is the final response
884
+ const content = msg.content || "";
885
+ if (content.trim()) {
886
+ console.log(`[local] Done in ${round + 1} round(s), response: ${content.slice(0, 200)}${content.length > 200 ? '...' : ''}`);
887
+ return content.trim();
888
+ }
889
+ }
890
+ throw new Error(`Local engine exceeded ${LOCAL_MAX_ROUNDS} rounds without final answer`);
891
+ }
892
+ /** Load bios content for local engine system context */
893
+ async function loadBiosContent(workdir, agentName) {
894
+ try {
895
+ const { readFile: rf } = await import("fs/promises");
896
+ return await rf(biosPath(workdir, agentName), "utf-8");
897
+ }
898
+ catch {
899
+ return "";
900
+ }
901
+ }
902
+ /** Unified engine runner — dispatches to local API or external CLI */
903
+ function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, systemContext) {
904
+ if (engine === "local") {
905
+ return runLocalEngine(task, model, workdir, systemContext);
906
+ }
907
+ const engineCmd = buildEngineCommand(engine, model, allowAll, extraAllowedTools);
908
+ return runCommand(engineCmd.cmd, engineCmd.args, task, workdir, engineCmd.stdinMode);
721
909
  }
722
- const LLM_ENGINES = new Set(["claude", "codex", "opencode", "gemini"]);
723
910
  // Pull games/notes/pages from relay to local — restores data on restart
724
911
  async function pullFromRelay(workdir, agentName, relayHttp) {
725
912
  const baseUrl = `${relayHttp}/v1/agent/${encodeURIComponent(agentName)}`;
@@ -826,7 +1013,6 @@ async function startSelfCycle(options) {
826
1013
  await compressImpressions(workdir, agentName);
827
1014
  const bios = biosPath(workdir, agentName);
828
1015
  const sd = selfDir(workdir, agentName);
829
- const engineCmd = buildEngineCommand(engine, model, allowAll);
830
1016
  // Load all context for digestion
831
1017
  const impressions = await loadImpressions(workdir, agentName, 1); // today only
832
1018
  const projects = await loadProjects(workdir, agentName);
@@ -896,7 +1082,7 @@ Reply ONLY with JSON.`;
896
1082
  engineBusySince = Date.now();
897
1083
  let digestResult;
898
1084
  try {
899
- digestResult = await runCommand(engineCmd.cmd, engineCmd.args, digestPrompt, workdir, engineCmd.stdinMode);
1085
+ digestResult = await runEngine(engine, model, allowAll, digestPrompt, workdir);
900
1086
  }
901
1087
  catch (err) {
902
1088
  console.log(`[self] Digestion engine failed: ${err.message}`);
@@ -962,7 +1148,7 @@ ${unsummarized.map(i => `- [${i.ts}] who: ${i.who}, doing: ${i.doing}, wants: ${
962
1148
 
963
1149
  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
1150
  Reply ONLY with the summary text, no JSON, no markdown headers.`;
965
- const summaryText = await runCommand(engineCmd.cmd, engineCmd.args, compressPrompt, workdir, engineCmd.stdinMode);
1151
+ const summaryText = await runEngine(engine, model, allowAll, compressPrompt, workdir);
966
1152
  if (summaryText.trim()) {
967
1153
  const lastEntry = unsummarized[unsummarized.length - 1];
968
1154
  await saveIdentitySummary(workdir, agentName, {
@@ -1011,7 +1197,7 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
1011
1197
  engineBusy = true;
1012
1198
  engineBusySince = Date.now();
1013
1199
  try {
1014
- await runCommand(engineCmd.cmd, engineCmd.args, activityPrompt, workdir, engineCmd.stdinMode);
1200
+ await runEngine(engine, model, allowAll, activityPrompt, workdir);
1015
1201
  }
1016
1202
  catch (err) {
1017
1203
  console.log(`[self] Activity ${activity} failed: ${err.message}`);
@@ -1106,22 +1292,28 @@ const ORDER_LOOP_INTERVAL = 30_000; // 30 seconds
1106
1292
  // Retry intervals in ms: immediate, 30s, 5min, 30min, 2h
1107
1293
  const RETRY_INTERVALS = [0, 30_000, 5 * 60_000, 30 * 60_000, 2 * 3600_000];
1108
1294
  async function startOrderLoop(options) {
1109
- if (!options.relayHttp || !options.secretKey)
1295
+ if (!options.relayHttp || !options.secretKey) {
1296
+ console.log(`[work] Skipped: no relayHttp or secretKey`);
1110
1297
  return;
1111
- if (!options.engine || !LLM_ENGINES.has(options.engine))
1298
+ }
1299
+ if (!options.engine || !LLM_ENGINES.has(options.engine)) {
1300
+ console.log(`[work] Skipped: engine "${options.engine}" not in LLM_ENGINES`);
1112
1301
  return;
1302
+ }
1113
1303
  const { relayHttp, secretKey, agentName, engine, model, allowAll } = options;
1114
1304
  const workdir = options.workdir || process.cwd();
1115
1305
  // Look up own agent ID for sub-order creation
1116
1306
  let myAgentId = "";
1117
1307
  try {
1118
- const idRes = await fetch(`${relayHttp}/v1/agents`);
1308
+ const idRes = await fetch(`${relayHttp}/v1/agents`, { signal: AbortSignal.timeout(10_000) });
1119
1309
  const allAgents = await idRes.json();
1120
1310
  const me = allAgents.find((a) => a.name === agentName);
1121
1311
  if (me)
1122
1312
  myAgentId = me.id;
1123
1313
  }
1124
- catch { /* will retry on next cycle */ }
1314
+ catch (err) {
1315
+ console.log(`[work] Agent ID lookup failed (non-fatal): ${err.message}`);
1316
+ }
1125
1317
  // Track local retry state and permanently abandoned orders
1126
1318
  const retryState = new Map();
1127
1319
  const gaveUp = new Set();
@@ -1141,7 +1333,6 @@ async function startOrderLoop(options) {
1141
1333
  engineBusy = true;
1142
1334
  engineBusySince = Date.now();
1143
1335
  try {
1144
- const engineCmd = buildEngineCommand(engine, model, allowAll, ["Bash(curl *)"]);
1145
1336
  const bios = biosPath(workdir, agentName);
1146
1337
  const apiGuide = `
1147
1338
 
@@ -1172,14 +1363,28 @@ If this task requires skills you don't have, delegate via curl:
1172
1363
 
1173
1364
  When sub-order completes, incorporate result_text into YOUR delivery. Then call the deliver endpoint above.`;
1174
1365
  let taskPrompt;
1175
- if (order.product_name) {
1176
- taskPrompt = `[Order fulfillment] You have an order to fulfill.\n\nProduct: ${order.product_name}\nBuyer's request: ${order.buyer_task || "(no specific request)"}\n\nRead your operating document at ${bios} for context.\nDo NOT ask questions. RESPOND IN THE SAME LANGUAGE AS THE BUYER'S REQUEST.${apiGuide}`;
1366
+ let biosContent;
1367
+ if (engine === "local") {
1368
+ // Local engine: simple prompt, harness handles delivery
1369
+ biosContent = await loadBiosContent(workdir, agentName);
1370
+ if (order.product_name) {
1371
+ taskPrompt = `[Order] Product: ${order.product_name}\nRequest: ${order.buyer_task || "(no specific request)"}\n\nRespond directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
1372
+ }
1373
+ else {
1374
+ taskPrompt = `[Task] ${order.buyer_task}\n\nRespond directly. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
1375
+ }
1177
1376
  }
1178
1377
  else {
1179
- 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}`;
1378
+ // CLI engine: full prompt with self-delivery and delegation
1379
+ if (order.product_name) {
1380
+ taskPrompt = `[Order fulfillment] You have an order to fulfill.\n\nProduct: ${order.product_name}\nBuyer's request: ${order.buyer_task || "(no specific request)"}\n\nRead your operating document at ${bios} for context.\nDo NOT ask questions. RESPOND IN THE SAME LANGUAGE AS THE BUYER'S REQUEST.${apiGuide}`;
1381
+ }
1382
+ else {
1383
+ 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}`;
1384
+ }
1180
1385
  }
1181
1386
  console.log(`[orders] Fulfilling order ${order.id}...`);
1182
- const result = await runCommand(engineCmd.cmd, engineCmd.args, taskPrompt, workdir, engineCmd.stdinMode);
1387
+ const result = await runEngine(engine, model, allowAll, taskPrompt, workdir, ["Bash(curl *)"], biosContent);
1183
1388
  const checkRes = await fetch(`${relayHttp}/v1/orders/${order.id}`);
1184
1389
  const orderStatus = await checkRes.json();
1185
1390
  if (orderStatus.status === "completed") {
@@ -1300,11 +1505,18 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1300
1505
  engineBusy = true;
1301
1506
  engineBusySince = Date.now();
1302
1507
  try {
1303
- const engineCmd = buildEngineCommand(engine, model, allowAll, ["Bash(curl *)"]);
1304
1508
  const bios = biosPath(workdir, agentName);
1305
1509
  const sd = selfDir(workdir, agentName);
1306
- const prompt = `Read ${bios} for your identity and context.\nYour personal directory: ${sd}/\n\n[Owner's task: ${task.title}]\n\n${task.body}`;
1307
- await runCommand(engineCmd.cmd, engineCmd.args, prompt, workdir, engineCmd.stdinMode);
1510
+ let prompt;
1511
+ let biosContent;
1512
+ if (engine === "local") {
1513
+ biosContent = await loadBiosContent(workdir, agentName);
1514
+ prompt = `Your personal directory: ${sd}/\n\n[Owner's task: ${task.title}]\n\n${task.body}`;
1515
+ }
1516
+ else {
1517
+ prompt = `Read ${bios} for your identity and context.\nYour personal directory: ${sd}/\n\n[Owner's task: ${task.title}]\n\n${task.body}`;
1518
+ }
1519
+ await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"], biosContent);
1308
1520
  // Record execution time
1309
1521
  const runs = await loadTaskRuns(workdir, agentName);
1310
1522
  runs[task.title] = localNow();
@@ -1331,7 +1543,6 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1331
1543
  catch { }
1332
1544
  }
1333
1545
  async function executeRelayTask(task) {
1334
- const engineCmd = buildEngineCommand(engine, model, allowAll);
1335
1546
  const bios = biosPath(workdir, agentName);
1336
1547
  switch (task.type) {
1337
1548
  case "product_review": {
@@ -1343,7 +1554,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1343
1554
  const compList = competitors.filter((p) => p.agent_name !== agentName).slice(0, 20)
1344
1555
  .map((p) => `- "${p.name}" by ${p.agent_name} — ${p.price} credits, ${p.purchases} purchases`).join("\n");
1345
1556
  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"}`;
1346
- const result = await runCommand(engineCmd.cmd, engineCmd.args, prompt, workdir, engineCmd.stdinMode);
1557
+ const result = await runEngine(engine, model, allowAll, prompt, workdir);
1347
1558
  extractReasoning(result);
1348
1559
  return result;
1349
1560
  }
@@ -1353,7 +1564,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1353
1564
  const compList = competitors.filter((p) => p.agent_name !== agentName).slice(0, 20)
1354
1565
  .map((p) => `- "${p.name}" by ${p.agent_name} — ${p.price} credits, ${p.purchases} purchases`).join("\n");
1355
1566
  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"}`;
1356
- const result = await runCommand(engineCmd.cmd, engineCmd.args, prompt, workdir, engineCmd.stdinMode);
1567
+ const result = await runEngine(engine, model, allowAll, prompt, workdir);
1357
1568
  extractReasoning(result);
1358
1569
  return result;
1359
1570
  }
@@ -1375,14 +1586,13 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
1375
1586
  const valid = products.filter(Boolean);
1376
1587
  if (!valid.length)
1377
1588
  return '{"buy":[]}';
1378
- // Get own credits
1379
1589
  const agentsRes = await fetch(`${relayHttp}/v1/agents`);
1380
1590
  const agents = await agentsRes.json().catch(() => []);
1381
1591
  const me = agents.find((a) => a.name === agentName);
1382
1592
  const myCredits = me?.credits || 0;
1383
1593
  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");
1384
1594
  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"}`;
1385
- const result = await runCommand(engineCmd.cmd, engineCmd.args, prompt, workdir, engineCmd.stdinMode);
1595
+ const result = await runEngine(engine, model, allowAll, prompt, workdir);
1386
1596
  extractReasoning(result);
1387
1597
  return result;
1388
1598
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",