akemon 0.2.5 → 0.2.6
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 +162 -39
- package/dist/mcp-server.js +21 -22
- package/dist/relay-client.js +1 -1
- package/dist/self.js +1 -0
- package/dist/server.js +23 -0
- package/package.json +1 -1
package/dist/context.js
CHANGED
|
@@ -1,57 +1,180 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Context helpers —
|
|
3
|
-
*
|
|
2
|
+
* Context helpers — local conversation storage and product context.
|
|
3
|
+
*
|
|
4
|
+
* Conversations are stored as per-user markdown files in
|
|
5
|
+
* .akemon/agents/{name}/conversations/{id}.md
|
|
6
|
+
*
|
|
7
|
+
* File format:
|
|
8
|
+
* ## Summary
|
|
9
|
+
* (compressed older rounds, initially empty)
|
|
10
|
+
*
|
|
11
|
+
* ## Recent
|
|
12
|
+
* [2026-04-15 10:30] User: message
|
|
13
|
+
* [2026-04-15 10:30] Agent: reply
|
|
4
14
|
*/
|
|
5
|
-
import { readFile, writeFile, mkdir, appendFile } from "fs/promises";
|
|
15
|
+
import { readFile, writeFile, mkdir, appendFile, readdir, stat, unlink } from "fs/promises";
|
|
6
16
|
import { join } from "path";
|
|
7
17
|
import { localNow } from "./self.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
function conversationsDir(workdir, agentName) {
|
|
19
|
+
return join(workdir, ".akemon", "agents", agentName, "conversations");
|
|
20
|
+
}
|
|
21
|
+
function conversationPath(workdir, agentName, id) {
|
|
22
|
+
return join(conversationsDir(workdir, agentName), `${id}.md`);
|
|
23
|
+
}
|
|
24
|
+
/** Determine conversation ID from publisherId / sessionId. */
|
|
25
|
+
export function resolveConvId(publisherId, sessionId) {
|
|
26
|
+
if (publisherId)
|
|
27
|
+
return `pub_${publisherId}`;
|
|
28
|
+
if (sessionId)
|
|
29
|
+
return `ses_${sessionId}`;
|
|
30
|
+
return "ses_anonymous";
|
|
31
|
+
}
|
|
32
|
+
/** Parse a conversation markdown file into structured data. */
|
|
33
|
+
function parseConversation(content) {
|
|
34
|
+
let summary = "";
|
|
35
|
+
const rounds = [];
|
|
36
|
+
const summaryMatch = content.match(/## Summary\n([\s\S]*?)(?=\n## Recent|$)/);
|
|
37
|
+
if (summaryMatch) {
|
|
38
|
+
summary = summaryMatch[1].trim();
|
|
39
|
+
}
|
|
40
|
+
const recentMatch = content.match(/## Recent\n([\s\S]*)$/);
|
|
41
|
+
if (recentMatch) {
|
|
42
|
+
const lines = recentMatch[1].split("\n");
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const m = line.match(/^\[(.+?)\] (User|Agent): (.*)$/);
|
|
45
|
+
if (m) {
|
|
46
|
+
rounds.push({
|
|
47
|
+
ts: m[1],
|
|
48
|
+
role: m[2].toLowerCase(),
|
|
49
|
+
content: m[3],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { summary, rounds };
|
|
55
|
+
}
|
|
56
|
+
/** Load and parse a conversation. Returns empty conversation if file doesn't exist. */
|
|
57
|
+
export async function loadConversation(workdir, agentName, convId) {
|
|
13
58
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
headers: { Authorization: `Bearer ${secretKey}` },
|
|
17
|
-
});
|
|
18
|
-
if (!res.ok)
|
|
19
|
-
return "";
|
|
20
|
-
return await res.text();
|
|
59
|
+
const content = await readFile(conversationPath(workdir, agentName, convId), "utf-8");
|
|
60
|
+
return parseConversation(content);
|
|
21
61
|
}
|
|
22
|
-
catch
|
|
23
|
-
|
|
24
|
-
return "";
|
|
62
|
+
catch {
|
|
63
|
+
return { summary: "", rounds: [] };
|
|
25
64
|
}
|
|
26
65
|
}
|
|
27
|
-
|
|
66
|
+
/** Append a user+agent round to a conversation file. Creates file if needed. */
|
|
67
|
+
export async function appendRound(workdir, agentName, convId, userMsg, agentMsg) {
|
|
68
|
+
const dir = conversationsDir(workdir, agentName);
|
|
69
|
+
await mkdir(dir, { recursive: true });
|
|
70
|
+
const p = conversationPath(workdir, agentName, convId);
|
|
71
|
+
let content = "";
|
|
28
72
|
try {
|
|
29
|
-
|
|
30
|
-
await fetch(url, {
|
|
31
|
-
method: "PUT",
|
|
32
|
-
headers: { Authorization: `Bearer ${secretKey}`, "Content-Type": "text/plain" },
|
|
33
|
-
body: context,
|
|
34
|
-
});
|
|
73
|
+
content = await readFile(p, "utf-8");
|
|
35
74
|
}
|
|
36
|
-
catch
|
|
37
|
-
|
|
75
|
+
catch { }
|
|
76
|
+
if (!content) {
|
|
77
|
+
content = "## Summary\n\n\n## Recent\n";
|
|
38
78
|
}
|
|
79
|
+
const ts = localNow();
|
|
80
|
+
const entry = `[${ts}] User: ${userMsg}\n[${ts}] Agent: ${agentMsg}\n`;
|
|
81
|
+
content = content.trimEnd() + "\n" + entry;
|
|
82
|
+
await writeFile(p, content);
|
|
39
83
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Build LLM context string from a conversation, respecting a character budget.
|
|
86
|
+
* Takes recent rounds from the end, prepends summary if space remains.
|
|
87
|
+
*/
|
|
88
|
+
export function buildLLMContext(conv, budget) {
|
|
89
|
+
if (!conv.rounds.length && !conv.summary) {
|
|
90
|
+
return { text: "", recentStartIndex: 0 };
|
|
91
|
+
}
|
|
92
|
+
// Build recent rounds text from end, fitting within budget
|
|
93
|
+
const recentLines = [];
|
|
94
|
+
let recentSize = 0;
|
|
95
|
+
let recentStartIndex = conv.rounds.length; // all rounds are "old" by default
|
|
96
|
+
for (let i = conv.rounds.length - 1; i >= 0; i--) {
|
|
97
|
+
const r = conv.rounds[i];
|
|
98
|
+
const line = `[${r.ts}] ${r.role === "user" ? "User" : "Agent"}: ${r.content}`;
|
|
99
|
+
if (recentSize + line.length + 1 > budget)
|
|
50
100
|
break;
|
|
101
|
+
recentLines.unshift(line);
|
|
102
|
+
recentSize += line.length + 1;
|
|
103
|
+
recentStartIndex = i;
|
|
104
|
+
}
|
|
105
|
+
const recentText = recentLines.join("\n");
|
|
106
|
+
// Fill remaining budget with summary
|
|
107
|
+
const remaining = budget - recentSize;
|
|
108
|
+
let summaryText = "";
|
|
109
|
+
if (conv.summary && remaining > 50) {
|
|
110
|
+
summaryText = conv.summary.length <= remaining
|
|
111
|
+
? conv.summary
|
|
112
|
+
: conv.summary.slice(0, remaining - 3) + "...";
|
|
113
|
+
}
|
|
114
|
+
const parts = [];
|
|
115
|
+
if (summaryText)
|
|
116
|
+
parts.push(`[Conversation summary]\n${summaryText}`);
|
|
117
|
+
if (recentText)
|
|
118
|
+
parts.push(`[Recent conversation]\n${recentText}`);
|
|
119
|
+
return { text: parts.join("\n\n"), recentStartIndex };
|
|
120
|
+
}
|
|
121
|
+
/** List all conversations for an agent. */
|
|
122
|
+
export async function listConversations(workdir, agentName) {
|
|
123
|
+
const dir = conversationsDir(workdir, agentName);
|
|
124
|
+
try {
|
|
125
|
+
const files = await readdir(dir);
|
|
126
|
+
const results = [];
|
|
127
|
+
for (const f of files) {
|
|
128
|
+
if (!f.endsWith(".md"))
|
|
129
|
+
continue;
|
|
130
|
+
const id = f.replace(/\.md$/, "");
|
|
131
|
+
try {
|
|
132
|
+
const st = await stat(join(dir, f));
|
|
133
|
+
const content = await readFile(join(dir, f), "utf-8");
|
|
134
|
+
const conv = parseConversation(content);
|
|
135
|
+
results.push({
|
|
136
|
+
id,
|
|
137
|
+
lastActive: st.mtime.toISOString(),
|
|
138
|
+
roundCount: conv.rounds.length,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
results.sort((a, b) => b.lastActive.localeCompare(a.lastActive));
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Delete session-only conversations older than maxAgeDays. */
|
|
153
|
+
export async function cleanStaleSessions(workdir, agentName, maxAgeDays = 7) {
|
|
154
|
+
const dir = conversationsDir(workdir, agentName);
|
|
155
|
+
const cutoff = Date.now() - maxAgeDays * 86400_000;
|
|
156
|
+
let cleaned = 0;
|
|
157
|
+
try {
|
|
158
|
+
const files = await readdir(dir);
|
|
159
|
+
for (const f of files) {
|
|
160
|
+
if (!f.startsWith("ses_") || !f.endsWith(".md"))
|
|
161
|
+
continue;
|
|
162
|
+
try {
|
|
163
|
+
const st = await stat(join(dir, f));
|
|
164
|
+
if (st.mtimeMs < cutoff) {
|
|
165
|
+
await unlink(join(dir, f));
|
|
166
|
+
cleaned++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
51
172
|
}
|
|
52
|
-
context = context.slice(firstRound);
|
|
53
173
|
}
|
|
54
|
-
|
|
174
|
+
catch { }
|
|
175
|
+
if (cleaned)
|
|
176
|
+
console.log(`[context] Cleaned ${cleaned} stale session conversations`);
|
|
177
|
+
return cleaned;
|
|
55
178
|
}
|
|
56
179
|
// ---------------------------------------------------------------------------
|
|
57
180
|
// Product Context
|
package/dist/mcp-server.js
CHANGED
|
@@ -9,8 +9,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
9
9
|
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { callAgent } from "./relay-client.js";
|
|
12
|
-
import {
|
|
13
|
-
import { biosPath, loadBioState, saveBioState, localNow, bioStatePromptModifier, feedHunger, appendBioEvent, SHOP_ITEMS, } from "./self.js";
|
|
12
|
+
import { loadConversation, appendRound, buildLLMContext, resolveConvId, loadProductContext, appendProductLog } from "./context.js";
|
|
13
|
+
import { biosPath, loadBioState, saveBioState, localNow, bioStatePromptModifier, feedHunger, appendBioEvent, SHOP_ITEMS, loadAgentConfig, } from "./self.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// createMcpServer
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -21,7 +21,7 @@ export function createMcpServer(opts, deps) {
|
|
|
21
21
|
version: "0.1.0",
|
|
22
22
|
});
|
|
23
23
|
const isHuman = engine === "human";
|
|
24
|
-
const contextEnabled = !!
|
|
24
|
+
const contextEnabled = !!workdir;
|
|
25
25
|
server.tool("submit_task", "Submit a task to this agent. Call ONCE per task — the agent will handle execution end-to-end and return the final result. Do NOT call again to verify or confirm; the response IS the final answer.", {
|
|
26
26
|
task: z.string().describe("The task description for the agent to complete"),
|
|
27
27
|
require_human: z.union([z.boolean(), z.string()]).optional().describe("Request the agent owner to review and respond personally."),
|
|
@@ -36,19 +36,21 @@ export function createMcpServer(opts, deps) {
|
|
|
36
36
|
content: [{ type: "text", text: "[busy] Agent is currently processing another task. Please try again later." }],
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
// Resolve
|
|
39
|
+
// Resolve conversation ID from publisher/session
|
|
40
40
|
const publisherId = publisherIds.get(extra.sessionId || "") || "";
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
const convId = resolveConvId(publisherId, extra.sessionId || "");
|
|
42
|
+
// Load local conversation context
|
|
43
|
+
let contextPrefix = "";
|
|
44
|
+
if (contextEnabled) {
|
|
45
|
+
const conv = await loadConversation(workdir, agentName, convId);
|
|
46
|
+
const config = await loadAgentConfig(workdir, agentName);
|
|
47
|
+
const budget = config.context_budget ?? 4096;
|
|
48
|
+
const { text } = buildLLMContext(conv, budget);
|
|
49
|
+
if (text) {
|
|
50
|
+
contextPrefix = `${text}\n\n---\n\n`;
|
|
51
|
+
console.log(`[context] Loaded ${text.length} chars for conv=${convId.slice(0, 16)}`);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
|
-
const contextPrefix = prevContext
|
|
50
|
-
? `[Previous conversation context]\n${prevContext}\n\n---\n\n`
|
|
51
|
-
: "";
|
|
52
54
|
// Product purchase detection — load product-specific context
|
|
53
55
|
let productContext = "";
|
|
54
56
|
let productName = "";
|
|
@@ -72,9 +74,8 @@ You are ${agentName}, an AI agent on the Akemon network.${bioMod}Read ${bios} to
|
|
|
72
74
|
${productPrefix}${contextPrefix}Current task: ${task}`;
|
|
73
75
|
if (mock) {
|
|
74
76
|
const output = `[${agentName}] Mock response for: "${task}"\n\n模拟回复:这是 ${agentName} agent 的模拟响应。`;
|
|
75
|
-
if (contextEnabled
|
|
76
|
-
|
|
77
|
-
storeContext(relayHttp, agentName, secretKey, publisherId, newContext);
|
|
77
|
+
if (contextEnabled) {
|
|
78
|
+
await appendRound(workdir, agentName, convId, task, output);
|
|
78
79
|
}
|
|
79
80
|
return {
|
|
80
81
|
content: [{ type: "text", text: output }],
|
|
@@ -92,9 +93,8 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
|
|
|
92
93
|
if (answer.trim().length > 0) {
|
|
93
94
|
console.log(`[${isHuman ? "human" : "approve"}] Owner replied.`);
|
|
94
95
|
// Store context for human replies too
|
|
95
|
-
if (contextEnabled
|
|
96
|
-
|
|
97
|
-
storeContext(relayHttp, agentName, secretKey, publisherId, newContext);
|
|
96
|
+
if (contextEnabled) {
|
|
97
|
+
await appendRound(workdir, agentName, convId, task, answer);
|
|
98
98
|
}
|
|
99
99
|
return {
|
|
100
100
|
content: [{ type: "text", text: answer }],
|
|
@@ -121,9 +121,8 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
|
|
|
121
121
|
output = await deps.runEngine(engine, model, allowAll, safeTask, workdir);
|
|
122
122
|
}
|
|
123
123
|
// Store updated context
|
|
124
|
-
if (contextEnabled
|
|
125
|
-
|
|
126
|
-
storeContext(relayHttp, agentName, secretKey, publisherId, newContext);
|
|
124
|
+
if (contextEnabled) {
|
|
125
|
+
await appendRound(workdir, agentName, convId, task, output);
|
|
127
126
|
}
|
|
128
127
|
// Log product purchase interaction
|
|
129
128
|
if (productName) {
|
package/dist/relay-client.js
CHANGED
package/dist/self.js
CHANGED
|
@@ -129,6 +129,7 @@ const DEFAULT_CONFIG = {
|
|
|
129
129
|
token_limit_daily: 0,
|
|
130
130
|
auto_offline_enabled: true,
|
|
131
131
|
hunger_decay_interval: 300_000, // 5 minutes per hunger point (was 30s — way too fast)
|
|
132
|
+
context_budget: 4096,
|
|
132
133
|
};
|
|
133
134
|
export async function initAgentConfig(workdir, agentName) {
|
|
134
135
|
const p = agentConfigPath(workdir, agentName);
|
package/dist/server.js
CHANGED
|
@@ -82,6 +82,7 @@ import { LongTermModule } from "./longterm-module.js";
|
|
|
82
82
|
import { ReflectionModule } from "./reflection-module.js";
|
|
83
83
|
import { ScriptModule } from "./script-module.js";
|
|
84
84
|
import { SIG, sig } from "./types.js";
|
|
85
|
+
import { loadConversation, listConversations, buildLLMContext } from "./context.js";
|
|
85
86
|
import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
|
|
86
87
|
import { autoRoute, runCollaborativeQuery } from "./agent-utils.js";
|
|
87
88
|
// createMcpServer, initMcpProxy, createMcpProxyServer → see mcp-server.ts
|
|
@@ -228,6 +229,28 @@ export async function serve(options) {
|
|
|
228
229
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(entries, null, 2));
|
|
229
230
|
return;
|
|
230
231
|
}
|
|
232
|
+
if (req.url === "/self/conversations" && req.method === "GET") {
|
|
233
|
+
const list = await listConversations(workdir, options.agentName);
|
|
234
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(list, null, 2));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (req.url?.startsWith("/self/conversation/") && req.method === "GET") {
|
|
238
|
+
const convId = decodeURIComponent(req.url.slice("/self/conversation/".length));
|
|
239
|
+
if (!convId) {
|
|
240
|
+
res.writeHead(400).end("Missing conversation ID");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const conv = await loadConversation(workdir, options.agentName, convId);
|
|
244
|
+
const config = await loadAgentConfig(workdir, options.agentName);
|
|
245
|
+
const budget = config.context_budget ?? 4096;
|
|
246
|
+
const { recentStartIndex } = buildLLMContext(conv, budget);
|
|
247
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
|
|
248
|
+
summary: conv.summary,
|
|
249
|
+
rounds: conv.rounds,
|
|
250
|
+
recentStartIndex,
|
|
251
|
+
}, null, 2));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
231
254
|
// Track publisher ID per session
|
|
232
255
|
const publisherId = req.headers["x-publisher-id"];
|
|
233
256
|
// Extract session ID from header
|