akemon 0.2.2 → 0.2.4

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.
@@ -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) {
@@ -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
+ }