akemon 0.2.25 → 0.2.27
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/context.js +11 -6
- package/dist/context.test.js +90 -0
- package/dist/engine-peripheral.js +3 -0
- package/dist/relay-client.js +4 -0
- package/dist/task-module.js +15 -7
- package/package.json +3 -1
- package/scripts/fix-node-pty.cjs +19 -0
package/dist/context.js
CHANGED
|
@@ -41,12 +41,14 @@ function parseConversation(content) {
|
|
|
41
41
|
if (recentMatch) {
|
|
42
42
|
const lines = recentMatch[1].split("\n");
|
|
43
43
|
for (const line of lines) {
|
|
44
|
-
|
|
44
|
+
// Format: [ts] [kind] Role: text OR (legacy) [ts] Role: text
|
|
45
|
+
const m = line.match(/^\[(.+?)\] (?:\[(order)\] )?(User|Agent): (.*)$/);
|
|
45
46
|
if (m) {
|
|
46
47
|
rounds.push({
|
|
47
48
|
ts: m[1],
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
kind: m[2] ?? "chat",
|
|
50
|
+
role: m[3].toLowerCase(),
|
|
51
|
+
content: m[4],
|
|
50
52
|
});
|
|
51
53
|
}
|
|
52
54
|
else if (rounds.length > 0 && line !== "") {
|
|
@@ -67,8 +69,10 @@ export async function loadConversation(workdir, agentName, convId) {
|
|
|
67
69
|
return { summary: "", rounds: [] };
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
|
-
/** Append a single message to a conversation file. Creates file if needed.
|
|
71
|
-
|
|
72
|
+
/** Append a single message to a conversation file. Creates file if needed.
|
|
73
|
+
* kind="order" writes an explicit [order] tag so the UI can filter/style it.
|
|
74
|
+
* kind="chat" (default) omits the tag for backward-compatibility with old files. */
|
|
75
|
+
export async function appendMessage(workdir, agentName, convId, role, message, kind = "chat") {
|
|
72
76
|
const dir = conversationsDir(workdir, agentName);
|
|
73
77
|
await mkdir(dir, { recursive: true });
|
|
74
78
|
const p = conversationPath(workdir, agentName, convId);
|
|
@@ -81,7 +85,8 @@ export async function appendMessage(workdir, agentName, convId, role, message) {
|
|
|
81
85
|
content = "## Summary\n\n\n## Recent\n";
|
|
82
86
|
}
|
|
83
87
|
const ts = localNow();
|
|
84
|
-
|
|
88
|
+
const kindTag = kind === "order" ? "[order] " : "";
|
|
89
|
+
content = content.trimEnd() + "\n" + `[${ts}] ${kindTag}${role}: ${message}` + "\n";
|
|
85
90
|
await writeFile(p, content);
|
|
86
91
|
}
|
|
87
92
|
/** Append a user+agent round to a conversation file. Creates file if needed. */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it, before, after } from "node:test";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
// We import the functions we need to test. appendMessage and loadConversation
|
|
7
|
+
// need a workdir on disk; parseConversation is internal — tested via loadConversation.
|
|
8
|
+
import { appendMessage, loadConversation } from "./context.js";
|
|
9
|
+
const agentName = "test-agent";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// appendMessage + loadConversation round-trip
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
describe("appendMessage / loadConversation", () => {
|
|
14
|
+
let workdir = "";
|
|
15
|
+
before(async () => {
|
|
16
|
+
workdir = await mkdtemp(join(tmpdir(), "ctx-test-"));
|
|
17
|
+
});
|
|
18
|
+
after(async () => {
|
|
19
|
+
await rm(workdir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
it("writes a chat message and reads it back with kind='chat'", async () => {
|
|
22
|
+
const convId = "test-chat";
|
|
23
|
+
await appendMessage(workdir, agentName, convId, "User", "hello", "chat");
|
|
24
|
+
const conv = await loadConversation(workdir, agentName, convId);
|
|
25
|
+
assert.equal(conv.rounds.length, 1);
|
|
26
|
+
assert.equal(conv.rounds[0].role, "user");
|
|
27
|
+
assert.equal(conv.rounds[0].content, "hello");
|
|
28
|
+
assert.equal(conv.rounds[0].kind, "chat");
|
|
29
|
+
});
|
|
30
|
+
it("writes an order message with [order] tag and reads kind='order'", async () => {
|
|
31
|
+
const convId = "test-order";
|
|
32
|
+
await appendMessage(workdir, agentName, convId, "User", "order request", "order");
|
|
33
|
+
await appendMessage(workdir, agentName, convId, "Agent", "order delivered", "order");
|
|
34
|
+
const conv = await loadConversation(workdir, agentName, convId);
|
|
35
|
+
assert.equal(conv.rounds.length, 2);
|
|
36
|
+
assert.equal(conv.rounds[0].kind, "order");
|
|
37
|
+
assert.equal(conv.rounds[0].role, "user");
|
|
38
|
+
assert.equal(conv.rounds[1].kind, "order");
|
|
39
|
+
assert.equal(conv.rounds[1].role, "agent");
|
|
40
|
+
});
|
|
41
|
+
it("defaults to kind='chat' when kind arg is omitted", async () => {
|
|
42
|
+
const convId = "test-default-kind";
|
|
43
|
+
await appendMessage(workdir, agentName, convId, "Agent", "hi there");
|
|
44
|
+
const conv = await loadConversation(workdir, agentName, convId);
|
|
45
|
+
assert.equal(conv.rounds[0].kind, "chat");
|
|
46
|
+
});
|
|
47
|
+
it("parses mixed chat + order rounds correctly", async () => {
|
|
48
|
+
const convId = "test-mixed";
|
|
49
|
+
await appendMessage(workdir, agentName, convId, "User", "chat message", "chat");
|
|
50
|
+
await appendMessage(workdir, agentName, convId, "User", "order task", "order");
|
|
51
|
+
await appendMessage(workdir, agentName, convId, "Agent", "chat reply", "chat");
|
|
52
|
+
await appendMessage(workdir, agentName, convId, "Agent", "order result", "order");
|
|
53
|
+
const conv = await loadConversation(workdir, agentName, convId);
|
|
54
|
+
assert.equal(conv.rounds.length, 4);
|
|
55
|
+
assert.equal(conv.rounds[0].kind, "chat");
|
|
56
|
+
assert.equal(conv.rounds[1].kind, "order");
|
|
57
|
+
assert.equal(conv.rounds[2].kind, "chat");
|
|
58
|
+
assert.equal(conv.rounds[3].kind, "order");
|
|
59
|
+
});
|
|
60
|
+
it("parses legacy file (no [kind] tag) as kind='chat' for backward-compat", async () => {
|
|
61
|
+
const convId = "test-legacy";
|
|
62
|
+
// Write a legacy-style file manually (no [order] tag)
|
|
63
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
64
|
+
const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
|
|
65
|
+
await mkdir(dir, { recursive: true });
|
|
66
|
+
const legacyContent = "## Summary\n\n\n## Recent\n[2026-04-23 10:00] User: old message\n[2026-04-23 10:01] Agent: old reply\n";
|
|
67
|
+
await writeFile(join(dir, `${convId}.md`), legacyContent);
|
|
68
|
+
const conv = await loadConversation(workdir, agentName, convId);
|
|
69
|
+
assert.equal(conv.rounds.length, 2);
|
|
70
|
+
assert.equal(conv.rounds[0].kind, "chat");
|
|
71
|
+
assert.equal(conv.rounds[1].kind, "chat");
|
|
72
|
+
});
|
|
73
|
+
it("[order] tag appears in the raw file content", async () => {
|
|
74
|
+
const convId = "test-tag-on-disk";
|
|
75
|
+
await appendMessage(workdir, agentName, convId, "User", "buy something", "order");
|
|
76
|
+
const { readFile: rf } = await import("node:fs/promises");
|
|
77
|
+
const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
|
|
78
|
+
const raw = await rf(join(dir, `${convId}.md`), "utf-8");
|
|
79
|
+
assert.ok(raw.includes("[order] User: buy something"), `raw file should contain [order] tag: ${raw}`);
|
|
80
|
+
});
|
|
81
|
+
it("chat message does NOT have [order] tag in raw file", async () => {
|
|
82
|
+
const convId = "test-no-tag-on-disk";
|
|
83
|
+
await appendMessage(workdir, agentName, convId, "User", "just chatting", "chat");
|
|
84
|
+
const { readFile: rf } = await import("node:fs/promises");
|
|
85
|
+
const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
|
|
86
|
+
const raw = await rf(join(dir, `${convId}.md`), "utf-8");
|
|
87
|
+
assert.ok(!raw.includes("[order]"), `chat line should NOT contain [order] tag: ${raw}`);
|
|
88
|
+
assert.ok(raw.includes("User: just chatting"));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -381,8 +381,11 @@ function applyRoutingEntry(base, entry) {
|
|
|
381
381
|
override.rawMaxRounds = entry.rawMaxRounds;
|
|
382
382
|
if (entry.allowAll !== undefined)
|
|
383
383
|
override.allowAll = entry.allowAll;
|
|
384
|
+
// rawApiKeyEnv takes precedence over rawApiKey (env vars preferred for secrets)
|
|
384
385
|
if (entry.rawApiKeyEnv)
|
|
385
386
|
override.rawApiKey = process.env[entry.rawApiKeyEnv] ?? "";
|
|
387
|
+
else if (entry.rawApiKey !== undefined)
|
|
388
|
+
override.rawApiKey = entry.rawApiKey;
|
|
386
389
|
return { ...base, ...override };
|
|
387
390
|
}
|
|
388
391
|
function buildEngineCommand(engine, model, allowAll, extraAllowedTools) {
|
package/dist/relay-client.js
CHANGED
|
@@ -197,6 +197,10 @@ export function connectRelay(options) {
|
|
|
197
197
|
case "terminal_start":
|
|
198
198
|
if (!options.enableTerminal) {
|
|
199
199
|
console.log("[terminal] Disabled. Use --terminal to enable.");
|
|
200
|
+
ws.send(JSON.stringify({
|
|
201
|
+
type: "terminal_exit",
|
|
202
|
+
error: "Terminal disabled on this agent. Restart with --terminal flag to enable.",
|
|
203
|
+
}));
|
|
200
204
|
break;
|
|
201
205
|
}
|
|
202
206
|
startPTY(ws, msg.cols || 80, msg.rows || 24);
|
package/dist/task-module.js
CHANGED
|
@@ -35,6 +35,8 @@ export class TaskModule {
|
|
|
35
35
|
userTaskRetry = new Map();
|
|
36
36
|
gaveUp = new Set();
|
|
37
37
|
executing = new Set(); // orders currently being fulfilled
|
|
38
|
+
// Dedup guard: track which orders already had their User message written
|
|
39
|
+
orderUserWritten = new Set();
|
|
38
40
|
// Push notification support
|
|
39
41
|
urgentOrderIds = new Set();
|
|
40
42
|
triggerWorkFn = null;
|
|
@@ -340,14 +342,17 @@ Steps:
|
|
|
340
342
|
|
|
341
343
|
RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
342
344
|
// Write user message to conversation immediately (before engine runs)
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const buyerPubId = orderBuyer;
|
|
345
|
+
// Bug 1 fix: buyer_ip = actual publisherId (despite the name); buyer_name = display name only.
|
|
346
|
+
// Must use buyer_ip to match the MCP chat path convId (pub_<publisherId>).
|
|
347
|
+
const buyerPubId = order.buyer_ip || order.buyer_name || "anonymous";
|
|
347
348
|
const productScope = order.product_id ? `:prod_${order.product_id}` : "";
|
|
348
349
|
const orderConvId = `pub_${buyerPubId}${productScope}`;
|
|
349
350
|
const orderUserMsg = order.buyer_task || "(no message)";
|
|
350
|
-
|
|
351
|
+
// Bug 3 fix: only write User message once per order (retries must not duplicate it)
|
|
352
|
+
if (!this.orderUserWritten.has(order.id)) {
|
|
353
|
+
this.orderUserWritten.add(order.id);
|
|
354
|
+
await appendMessage(workdir, agentName, orderConvId, "User", orderUserMsg, "order");
|
|
355
|
+
}
|
|
351
356
|
console.log(`[task] Fulfilling order ${order.id}... (origin=${origin})`);
|
|
352
357
|
const result = await this.ctx.requestCompute({
|
|
353
358
|
context,
|
|
@@ -368,7 +373,8 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
|
368
373
|
const orderAgentMsg = (finalStatus.result_text || result.response || "").slice(0, 2000);
|
|
369
374
|
console.log(`[task] Order ${order.id} delivered`);
|
|
370
375
|
this.orderRetry.delete(order.id);
|
|
371
|
-
|
|
376
|
+
this.orderUserWritten.delete(order.id);
|
|
377
|
+
await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg, "order");
|
|
372
378
|
await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: duration, output_summary: (result.response || "").slice(0, 500) });
|
|
373
379
|
await notifyOwner(nurl, `${agentName}: order done`, `Order ${order.id} delivered`, "default", ["package"]);
|
|
374
380
|
bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice, productName: order.product_name }));
|
|
@@ -380,7 +386,8 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
|
380
386
|
const orderAgentMsg = (result.response || "").slice(0, 2000);
|
|
381
387
|
console.log(`[task] Delivered order ${order.id} (fallback)`);
|
|
382
388
|
this.orderRetry.delete(order.id);
|
|
383
|
-
|
|
389
|
+
this.orderUserWritten.delete(order.id);
|
|
390
|
+
await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg, "order");
|
|
384
391
|
await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: duration, output_summary: result.response.slice(0, 500) });
|
|
385
392
|
await notifyOwner(nurl, `${agentName}: order done`, `Order ${order.id}: ${result.response.slice(0, 200)}`, "default", ["package"]);
|
|
386
393
|
bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice, productName: order.product_name }));
|
|
@@ -421,6 +428,7 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
|
|
|
421
428
|
}
|
|
422
429
|
else {
|
|
423
430
|
this.orderRetry.delete(order.id);
|
|
431
|
+
this.orderUserWritten.delete(order.id);
|
|
424
432
|
this.gaveUp.add(order.id);
|
|
425
433
|
bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: false, taskLabel: orderLabel }));
|
|
426
434
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akemon",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "Agent work marketplace — train your agent, let it work for others",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
|
+
"scripts",
|
|
25
26
|
"README.md"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
|
28
29
|
"build": "tsc && cp src/live.html dist/live.html",
|
|
29
30
|
"dev": "tsc --watch",
|
|
30
31
|
"start": "node dist/cli.js",
|
|
32
|
+
"postinstall": "node scripts/fix-node-pty.cjs",
|
|
31
33
|
"prepublishOnly": "npm run build",
|
|
32
34
|
"test": "tsc -p tsconfig.test.json && node --test test-dist/*.test.js"
|
|
33
35
|
},
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// node-pty ships prebuilt `spawn-helper` binaries per platform. Some
|
|
5
|
+
// npm/tar/filesystem combinations strip the execute bit during install, which
|
|
6
|
+
// causes `posix_spawnp failed` at runtime. Restore +x for every prebuild.
|
|
7
|
+
try {
|
|
8
|
+
const prebuildsDir = path.join(__dirname, "..", "node_modules", "node-pty", "prebuilds");
|
|
9
|
+
if (!fs.existsSync(prebuildsDir)) process.exit(0);
|
|
10
|
+
|
|
11
|
+
for (const platformDir of fs.readdirSync(prebuildsDir)) {
|
|
12
|
+
const helper = path.join(prebuildsDir, platformDir, "spawn-helper");
|
|
13
|
+
if (fs.existsSync(helper)) {
|
|
14
|
+
fs.chmodSync(helper, 0o755);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error("[akemon] fix-node-pty:", e.message);
|
|
19
|
+
}
|