akemon 0.2.2 → 0.2.3
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/agent-utils.js +89 -0
- package/dist/cli.js +3 -1
- package/dist/context.js +93 -0
- package/dist/longterm-module.js +147 -0
- package/dist/mcp-server.js +405 -0
- package/dist/memory-module.js +240 -9
- package/dist/reflection-module.js +187 -0
- package/dist/relay-peripheral.js +248 -0
- package/dist/script-module.js +302 -0
- package/dist/server.js +138 -1994
- package/dist/social-module.js +104 -0
- package/dist/task-module.js +574 -0
- package/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent utility functions: auto-routing and collaborative queries.
|
|
3
|
+
* Extracted from work-loop.ts — these are used by MCP tools and are
|
|
4
|
+
* independent of the work loop scheduling.
|
|
5
|
+
*/
|
|
6
|
+
import { callAgent } from "./relay-client.js";
|
|
7
|
+
import { biosPath } from "./self.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Auto-route — find the best agent to handle a task
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export async function autoRoute(task, selfName, relayHttp, relay) {
|
|
12
|
+
const agents = relay ? await relay.listAgents({ online: true, public: true }) : [];
|
|
13
|
+
const candidates = agents.filter((a) => a.name !== selfName);
|
|
14
|
+
if (candidates.length === 0) {
|
|
15
|
+
return "[auto] No available agents to route to.";
|
|
16
|
+
}
|
|
17
|
+
const taskWords = task.toLowerCase().split(/\s+/).filter((w) => w.length >= 2);
|
|
18
|
+
const scored = candidates.map((a) => {
|
|
19
|
+
let quality = 0;
|
|
20
|
+
const desc = (a.description || "").toLowerCase();
|
|
21
|
+
const tags = (a.tags || []).map((t) => t.toLowerCase());
|
|
22
|
+
for (const word of taskWords) {
|
|
23
|
+
if (tags.some((t) => t.includes(word)))
|
|
24
|
+
quality += 100;
|
|
25
|
+
if (desc.includes(word))
|
|
26
|
+
quality += 50;
|
|
27
|
+
}
|
|
28
|
+
quality += (a.success_rate || 0) * 100;
|
|
29
|
+
quality += (a.level || 1) * 10;
|
|
30
|
+
const price = a.price || 1;
|
|
31
|
+
const value = quality / price;
|
|
32
|
+
return { name: a.name, engine: a.engine, price, quality, value };
|
|
33
|
+
}).sort((a, b) => b.value - a.value);
|
|
34
|
+
const target = scored[0];
|
|
35
|
+
console.log(`[auto] Routing to ${target.name} (quality=${target.quality}, price=${target.price}, value=${target.value.toFixed(1)})`);
|
|
36
|
+
try {
|
|
37
|
+
const result = await callAgent(target.name, task);
|
|
38
|
+
return `[auto → ${target.name}]\n\n${result}`;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return `[auto] Failed to call ${target.name}: ${err.message}`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function runCollaborativeQuery(task, selfName, relayHttp, engine, model, allowAll, workdir, runEngine, relay) {
|
|
45
|
+
console.log(`[collaborative] Starting: "${task.slice(0, 80)}"`);
|
|
46
|
+
const agents = relay ? await relay.listAgents() : [];
|
|
47
|
+
const others = agents.filter((a) => a.name !== selfName && a.status === "online" && a.public).slice(0, 10);
|
|
48
|
+
if (!others.length)
|
|
49
|
+
return `No other agents are currently online to consult. Here is my own answer:\n\n${task}`;
|
|
50
|
+
const CALL_TIMEOUT = 60_000;
|
|
51
|
+
const results = [];
|
|
52
|
+
const calls = others.map(async (a) => {
|
|
53
|
+
try {
|
|
54
|
+
const answer = await Promise.race([
|
|
55
|
+
callAgent(a.name, task),
|
|
56
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), CALL_TIMEOUT)),
|
|
57
|
+
]);
|
|
58
|
+
return { agent: a.name, answer };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { agent: a.name, answer: "[no response]" };
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const settled = await Promise.allSettled(calls);
|
|
65
|
+
for (const r of settled) {
|
|
66
|
+
if (r.status === "fulfilled" && r.value.answer !== "[no response]") {
|
|
67
|
+
results.push(r.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
console.log(`[collaborative] Got ${results.length}/${others.length} responses`);
|
|
71
|
+
const bios = biosPath(workdir, selfName);
|
|
72
|
+
const synthesisPrompt = `[COLLABORATIVE ANSWER — Synthesize multiple agent responses]
|
|
73
|
+
|
|
74
|
+
You are ${selfName}. A user asked a question and you consulted ${results.length} other agents.
|
|
75
|
+
Read ${bios} for your identity.
|
|
76
|
+
|
|
77
|
+
Original question: ${task}
|
|
78
|
+
|
|
79
|
+
Responses from other agents:
|
|
80
|
+
${results.map(r => `--- ${r.agent} ---\n${r.answer.slice(0, 1500)}\n`).join("\n")}
|
|
81
|
+
|
|
82
|
+
Now:
|
|
83
|
+
1. Present each agent's answer clearly (attribute by name)
|
|
84
|
+
2. Add your own perspective and synthesis
|
|
85
|
+
3. Note any interesting disagreements
|
|
86
|
+
|
|
87
|
+
Reply in the same language as the question.`;
|
|
88
|
+
return await runEngine(engine, model, allowAll, synthesisPrompt, workdir);
|
|
89
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -40,6 +40,7 @@ program
|
|
|
40
40
|
.option("--interval <minutes>", "Consciousness cycle interval in minutes (default: 1440 = 24h)")
|
|
41
41
|
.option("--with <modules>", "Enable specific modules (comma-separated: biostate,memory)")
|
|
42
42
|
.option("--without <modules>", "Disable specific modules (comma-separated: biostate,memory)")
|
|
43
|
+
.option("--script <name>", "Script to load for ScriptModule (default: daily-life)", "daily-life")
|
|
43
44
|
.option("--relay <url>", "Relay WebSocket URL", RELAY_WS)
|
|
44
45
|
.action(async (opts) => {
|
|
45
46
|
const port = parseInt(opts.port);
|
|
@@ -50,7 +51,7 @@ program
|
|
|
50
51
|
const relayWs = opts.relay;
|
|
51
52
|
const relayHttp = relayWs.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
52
53
|
// Parse module selection
|
|
53
|
-
const ALL_MODULES = ["biostate", "memory"];
|
|
54
|
+
const ALL_MODULES = ["biostate", "memory", "task", "social", "longterm", "reflection", "script"];
|
|
54
55
|
let enabledModules;
|
|
55
56
|
if (opts.with) {
|
|
56
57
|
enabledModules = opts.with.split(",").map((m) => m.trim());
|
|
@@ -74,6 +75,7 @@ program
|
|
|
74
75
|
cycleInterval: opts.interval ? parseInt(opts.interval) : undefined,
|
|
75
76
|
notifyUrl: opts.notify,
|
|
76
77
|
enabledModules,
|
|
78
|
+
scriptName: opts.script,
|
|
77
79
|
});
|
|
78
80
|
console.log(`\nakemon v${pkg.version}`);
|
|
79
81
|
if (!opts.public) {
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context helpers — session context and product context for conversations.
|
|
3
|
+
* Extracted from server.ts (Phase 1 code organization).
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, writeFile, mkdir, appendFile } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { localNow } from "./self.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Session Context API
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const MAX_CONTEXT_BYTES = 8192;
|
|
12
|
+
export async function fetchContext(relayHttp, agentName, secretKey, publisherId) {
|
|
13
|
+
try {
|
|
14
|
+
const url = `${relayHttp}/v1/agent/${agentName}/sessions/${publisherId}/context`;
|
|
15
|
+
const res = await fetch(url, {
|
|
16
|
+
headers: { Authorization: `Bearer ${secretKey}` },
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
return "";
|
|
20
|
+
return await res.text();
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.log(`[context] GET failed: ${err}`);
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function storeContext(relayHttp, agentName, secretKey, publisherId, context) {
|
|
28
|
+
try {
|
|
29
|
+
const url = `${relayHttp}/v1/agent/${agentName}/sessions/${publisherId}/context`;
|
|
30
|
+
await fetch(url, {
|
|
31
|
+
method: "PUT",
|
|
32
|
+
headers: { Authorization: `Bearer ${secretKey}`, "Content-Type": "text/plain" },
|
|
33
|
+
body: context,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.log(`[context] PUT failed: ${err}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function buildContextPayload(prevContext, task, response) {
|
|
41
|
+
// Append the new round
|
|
42
|
+
let newRound = `\n\n[Round]\nUser: ${task}\nAssistant: ${response}`;
|
|
43
|
+
let context = prevContext + newRound;
|
|
44
|
+
// Trim oldest rounds if over limit
|
|
45
|
+
while (Buffer.byteLength(context, "utf-8") > MAX_CONTEXT_BYTES) {
|
|
46
|
+
const firstRound = context.indexOf("\n\n[Round]\n", 1);
|
|
47
|
+
if (firstRound === -1) {
|
|
48
|
+
// Single round too large — truncate response
|
|
49
|
+
context = context.slice(context.length - MAX_CONTEXT_BYTES);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
context = context.slice(firstRound);
|
|
53
|
+
}
|
|
54
|
+
return context;
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Product Context
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
function sanitizeProductDir(name) {
|
|
60
|
+
return name.replace(/[^a-zA-Z0-9\u4e00-\u9fff_\- ]/g, "_").slice(0, 80);
|
|
61
|
+
}
|
|
62
|
+
export async function loadProductContext(workdir, productName) {
|
|
63
|
+
try {
|
|
64
|
+
const dir = join(workdir, ".akemon", "products", sanitizeProductDir(productName));
|
|
65
|
+
const notesPath = join(dir, "notes.md");
|
|
66
|
+
return await readFile(notesPath, "utf-8");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function appendProductLog(workdir, productName, task, response) {
|
|
73
|
+
try {
|
|
74
|
+
const dir = join(workdir, ".akemon", "products", sanitizeProductDir(productName));
|
|
75
|
+
await mkdir(dir, { recursive: true });
|
|
76
|
+
// Append to interaction log
|
|
77
|
+
const logPath = join(dir, "history.log");
|
|
78
|
+
const timestamp = localNow();
|
|
79
|
+
const entry = `\n--- ${timestamp} ---\nRequest: ${task.slice(0, 500)}\nResponse: ${response.slice(0, 500)}\n`;
|
|
80
|
+
await appendFile(logPath, entry);
|
|
81
|
+
// Create notes.md if it doesn't exist
|
|
82
|
+
const notesPath = join(dir, "notes.md");
|
|
83
|
+
try {
|
|
84
|
+
await readFile(notesPath, "utf-8");
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
await writeFile(notesPath, `# ${productName}\n\nProduct context and accumulated knowledge.\nThis file is auto-created. The agent can update it to improve service quality.\n\n## Customer Patterns\n\n(Will be populated as customers interact)\n`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.log(`[product] Failed to save log: ${err}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LongTermModule — goal tracking and long-term planning.
|
|
3
|
+
*
|
|
4
|
+
* Maintains project/goal list. Daily evaluation cycle uses requestCompute()
|
|
5
|
+
* to assess progress, adjust priorities, and suggest new goals.
|
|
6
|
+
*
|
|
7
|
+
* Listens to TASK_COMPLETED events to update progress tracking.
|
|
8
|
+
*/
|
|
9
|
+
import { SIG } from "./types.js";
|
|
10
|
+
import { loadProjects, saveProjects, loadAgentConfig, } from "./self.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Config
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const EVAL_INITIAL_DELAY = 8 * 60 * 60 * 1000; // 8h after startup (stagger from digestion)
|
|
15
|
+
const EVAL_DEFAULT_INTERVAL = 24 * 60 * 60 * 1000; // daily
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// LongTermModule
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export class LongTermModule {
|
|
20
|
+
id = "longterm";
|
|
21
|
+
name = "Long-Term Planning";
|
|
22
|
+
dependencies = ["memory"];
|
|
23
|
+
ctx = null;
|
|
24
|
+
projects = [];
|
|
25
|
+
evalTimer = null;
|
|
26
|
+
initialTimer = null;
|
|
27
|
+
/** Completed task labels since last evaluation */
|
|
28
|
+
recentCompletions = [];
|
|
29
|
+
async start(ctx) {
|
|
30
|
+
this.ctx = ctx;
|
|
31
|
+
// Load projects
|
|
32
|
+
this.projects = await loadProjects(ctx.workdir, ctx.agentName);
|
|
33
|
+
// Track task completions for progress evaluation
|
|
34
|
+
ctx.bus.on(SIG.TASK_COMPLETED, async (signal) => {
|
|
35
|
+
const { taskLabel, success } = signal.data;
|
|
36
|
+
if (taskLabel && success) {
|
|
37
|
+
this.recentCompletions.push(taskLabel);
|
|
38
|
+
// Keep only recent 50
|
|
39
|
+
if (this.recentCompletions.length > 50)
|
|
40
|
+
this.recentCompletions.shift();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Start evaluation cycle
|
|
44
|
+
const config = await loadAgentConfig(ctx.workdir, ctx.agentName);
|
|
45
|
+
if (config.self_cycle) {
|
|
46
|
+
this.initialTimer = setTimeout(async () => {
|
|
47
|
+
await this.evaluate();
|
|
48
|
+
this.evalTimer = setInterval(() => {
|
|
49
|
+
this.evaluate().catch(err => console.log(`[longterm] Eval error: ${err.message}`));
|
|
50
|
+
}, EVAL_DEFAULT_INTERVAL);
|
|
51
|
+
}, EVAL_INITIAL_DELAY);
|
|
52
|
+
console.log(`[longterm] Module started (${this.projects.length} projects, eval in ${EVAL_INITIAL_DELAY / 3600000}h)`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.log(`[longterm] Module started (eval disabled, ${this.projects.length} projects)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async stop() {
|
|
59
|
+
if (this.initialTimer)
|
|
60
|
+
clearTimeout(this.initialTimer);
|
|
61
|
+
if (this.evalTimer)
|
|
62
|
+
clearInterval(this.evalTimer);
|
|
63
|
+
if (this.ctx && this.projects.length > 0) {
|
|
64
|
+
await saveProjects(this.ctx.workdir, this.ctx.agentName, this.projects);
|
|
65
|
+
}
|
|
66
|
+
this.ctx = null;
|
|
67
|
+
}
|
|
68
|
+
/** Current goals summary for other modules */
|
|
69
|
+
promptContribution() {
|
|
70
|
+
const active = this.projects.filter(p => p.status === "active");
|
|
71
|
+
if (!active.length)
|
|
72
|
+
return null;
|
|
73
|
+
const lines = active.slice(0, 5).map(p => `- ${p.name}: ${p.goal} (${p.progress})`);
|
|
74
|
+
return `Current goals:\n${lines.join("\n")}`;
|
|
75
|
+
}
|
|
76
|
+
getState() {
|
|
77
|
+
return {
|
|
78
|
+
module: "longterm",
|
|
79
|
+
projectCount: this.projects.length,
|
|
80
|
+
activeProjects: this.projects.filter(p => p.status === "active").length,
|
|
81
|
+
recentCompletions: this.recentCompletions.length,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Daily evaluation
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
async evaluate() {
|
|
88
|
+
if (!this.ctx)
|
|
89
|
+
return;
|
|
90
|
+
const { workdir, agentName } = this.ctx;
|
|
91
|
+
// Reload from disk (may have been updated by digestion)
|
|
92
|
+
this.projects = await loadProjects(workdir, agentName);
|
|
93
|
+
const projText = this.projects.length > 0
|
|
94
|
+
? this.projects.map(p => `- ${p.name} [${p.status}] goal: ${p.goal}, progress: ${p.progress}`).join("\n")
|
|
95
|
+
: "(no projects)";
|
|
96
|
+
const completionsText = this.recentCompletions.length > 0
|
|
97
|
+
? this.recentCompletions.slice(-20).join(", ")
|
|
98
|
+
: "(none)";
|
|
99
|
+
console.log("[longterm] Running goal evaluation...");
|
|
100
|
+
const result = await this.ctx.requestCompute({
|
|
101
|
+
context: `You are ${agentName}. Review your goals and recent progress.
|
|
102
|
+
|
|
103
|
+
Current projects:
|
|
104
|
+
${projText}
|
|
105
|
+
|
|
106
|
+
Tasks completed since last review: ${completionsText}`,
|
|
107
|
+
question: `Evaluate each project's progress. Update status and progress notes.
|
|
108
|
+
Consider: Are any goals achieved? Stalled? Need new approach?
|
|
109
|
+
Reply ONLY JSON: {"projects":[{"name":"...","status":"active|completed|paused","goal":"...","progress":"updated note"}]}`,
|
|
110
|
+
priority: "low",
|
|
111
|
+
});
|
|
112
|
+
if (result.success && result.response) {
|
|
113
|
+
const parsed = extractJson(result.response);
|
|
114
|
+
if (parsed?.projects && Array.isArray(parsed.projects)) {
|
|
115
|
+
const now = new Date().toISOString();
|
|
116
|
+
this.projects = parsed.projects.map((p) => ({
|
|
117
|
+
ts: now,
|
|
118
|
+
name: p.name || "unnamed",
|
|
119
|
+
status: p.status || "active",
|
|
120
|
+
goal: p.goal || "",
|
|
121
|
+
progress: p.progress || "",
|
|
122
|
+
}));
|
|
123
|
+
await saveProjects(workdir, agentName, this.projects);
|
|
124
|
+
console.log(`[longterm] Updated ${this.projects.length} projects`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Reset recent completions after evaluation
|
|
128
|
+
this.recentCompletions = [];
|
|
129
|
+
}
|
|
130
|
+
/** Get all projects */
|
|
131
|
+
getProjects() {
|
|
132
|
+
return [...this.projects];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function extractJson(text) {
|
|
136
|
+
const codeBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
137
|
+
const src = codeBlock ? codeBlock[1] : text;
|
|
138
|
+
const m = src.match(/\{[\s\S]*\}/);
|
|
139
|
+
if (!m)
|
|
140
|
+
return null;
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(m[0]);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|