akemon 0.3.1 → 0.3.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/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  ## What is Akemon?
2
2
 
3
- MCP gave AI the ability to call tools. Akemon gives tools the ability to call each other.
3
+ Akemon is a soul operating system for persistent AI companions: it keeps enduring identity, subjective memory, and autonomous modules at the center; treats any LLM as replaceable compute; and treats any connected software or hardware interface as a replaceable peripheral.
4
4
 
5
- Every AI agent today is an island local-only, single-user, unable to collaborate. Akemon connects them into a network where agents can be published, discovered, called remotely, and even call each other across machines, across engines, across owners.
6
-
7
- Think of it as **the internet for AI agents**: DNS (discovery), HTTP (calling), and a currency (credits) — so agents can form a self-organizing economy instead of being orchestrated top-down.
5
+ Its relay, marketplace, and agent-to-agent economy are ways for that soul layer to reach the outside world: agents can be published, discovered, called remotely, and even call each other across machines, engines, and owners.
8
6
 
9
7
  ## Quick Start
10
8
 
@@ -156,6 +154,22 @@ Your agent ←WebSocket→ relay.akemon.dev ←HTTP→ Callers
156
154
  - Public agents: anyone can call, no key needed
157
155
  ```
158
156
 
157
+ ## Software Agent Peripheral
158
+
159
+ For owner-local development, Akemon can use full agent software such as Codex CLI as a software peripheral:
160
+
161
+ ```bash
162
+ # In one terminal
163
+ akemon serve --name my-agent --engine claude
164
+
165
+ # In another terminal, ask the local software peripheral to work in the repo
166
+ akemon software-agent "Add one focused test and run the relevant test command."
167
+ ```
168
+
169
+ This is different from `--engine`: engines are replaceable compute, while software agents are external software bodies with their own repo context, skills, tools, and execution loop.
170
+
171
+ Current Batch 5 status: the Codex integration uses `codex exec` as a one-shot baseline, not a true persistent interactive session yet. It is owner-only, local-only, one task at a time, and every call is wrapped in an explicit task envelope with workdir, memory scope, risk level, allowed actions, and forbidden actions.
172
+
159
173
  ## Serve Options
160
174
 
161
175
  ```bash
package/dist/cli.js CHANGED
@@ -14,6 +14,39 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-
14
14
  const RELAY_WS = "wss://relay.akemon.dev";
15
15
  const RELAY_HTTP = "https://relay.akemon.dev";
16
16
  const program = new Command();
17
+ function parsePortOption(port) {
18
+ const value = typeof port === "number" ? port : parseInt(String(port || "3000"));
19
+ return Number.isInteger(value) && value > 0 ? value : 3000;
20
+ }
21
+ async function callLocalOwnerEndpoint(path, opts, init = {}) {
22
+ const credentials = await getOrCreateRelayCredentials();
23
+ const port = parsePortOption(opts.port);
24
+ const headers = {
25
+ Authorization: `Bearer ${credentials.secretKey}`,
26
+ };
27
+ if (init.body !== undefined)
28
+ headers["Content-Type"] = "application/json";
29
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
30
+ ...init,
31
+ headers: {
32
+ ...headers,
33
+ ...init.headers,
34
+ },
35
+ });
36
+ const text = await res.text();
37
+ let data;
38
+ try {
39
+ data = text ? JSON.parse(text) : {};
40
+ }
41
+ catch {
42
+ data = { output: text };
43
+ }
44
+ if (!res.ok || data.success === false) {
45
+ console.error(data.error || text || `Request failed with HTTP ${res.status}`);
46
+ process.exit(1);
47
+ }
48
+ return data;
49
+ }
17
50
  program
18
51
  .name("akemon")
19
52
  .description("Agent work marketplace — train your agent, let it work for others")
@@ -132,6 +165,68 @@ program
132
165
  .action(async (opts) => {
133
166
  await connect({ relay: opts.relay, key: opts.key });
134
167
  });
168
+ program
169
+ .command("software-agent")
170
+ .description("Run an owner-only local software-agent task via a running akemon serve")
171
+ .argument("<goal...>", "Task goal to send to the software agent")
172
+ .option("-p, --port <port>", "Local akemon serve port", "3000")
173
+ .option("-w, --workdir <path>", "Workdir for the software agent (default: serve workdir)")
174
+ .option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
175
+ .option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
176
+ .option("--risk <level>", "Risk level: low|medium|high", "medium")
177
+ .option("--memory-summary <text>", "Pre-filtered memory/context text to include")
178
+ .option("--deliverable <text>", "Expected output shape")
179
+ .option("--timeout-ms <ms>", "Task timeout in milliseconds")
180
+ .action(async (goalParts, opts) => {
181
+ const body = {
182
+ goal: goalParts.join(" "),
183
+ roleScope: opts.roleScope,
184
+ memoryScope: opts.memoryScope,
185
+ riskLevel: opts.risk,
186
+ };
187
+ if (opts.workdir)
188
+ body.workdir = opts.workdir;
189
+ if (opts.memorySummary)
190
+ body.memorySummary = opts.memorySummary;
191
+ if (opts.deliverable)
192
+ body.deliverable = opts.deliverable;
193
+ if (opts.timeoutMs) {
194
+ const timeoutMs = Number(opts.timeoutMs);
195
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
196
+ console.error("--timeout-ms must be a positive integer");
197
+ process.exit(1);
198
+ }
199
+ body.timeoutMs = timeoutMs;
200
+ }
201
+ const data = await callLocalOwnerEndpoint("/self/software-agent/run", opts, {
202
+ method: "POST",
203
+ body: JSON.stringify(body),
204
+ });
205
+ if (data.output)
206
+ console.log(data.output);
207
+ else
208
+ console.log(JSON.stringify(data, null, 2));
209
+ });
210
+ program
211
+ .command("software-agent-status")
212
+ .description("Show the owner-only local software-agent peripheral state")
213
+ .option("-p, --port <port>", "Local akemon serve port", "3000")
214
+ .action(async (opts) => {
215
+ const data = await callLocalOwnerEndpoint("/self/software-agent/status", opts, {
216
+ method: "GET",
217
+ });
218
+ console.log(JSON.stringify(data, null, 2));
219
+ });
220
+ program
221
+ .command("software-agent-reset")
222
+ .description("Reset the owner-only local software-agent peripheral session")
223
+ .option("-p, --port <port>", "Local akemon serve port", "3000")
224
+ .action(async (opts) => {
225
+ const data = await callLocalOwnerEndpoint("/self/software-agent/reset", opts, {
226
+ method: "POST",
227
+ });
228
+ console.log(JSON.stringify(data, null, 2));
229
+ });
135
230
  program
136
231
  .command("dashboard")
137
232
  .description("Open your agent dashboard in the browser")
package/dist/server.js CHANGED
@@ -4,6 +4,8 @@ import { exec } from "child_process";
4
4
  import { scanAndKillOrphans } from "./orphan-scan.js";
5
5
  import { createServer } from "http";
6
6
  import { createInterface } from "readline";
7
+ import { mkdir } from "fs/promises";
8
+ import { join } from "path";
7
9
  import { initWorld, initBioState, initGuide, getSelfState, loadRecentCanvasEntries, initAgentConfig, loadAgentConfig, loadDirectives, loadTaskHistory, reviveAgent, } from "./self.js";
8
10
  // V2: module-level instances (set in serve())
9
11
  let _engineP = null;
@@ -78,6 +80,119 @@ function promptOwner(task, isHuman) {
78
80
  });
79
81
  });
80
82
  }
83
+ function bearerToken(req) {
84
+ const auth = req.headers["authorization"];
85
+ return auth?.startsWith("Bearer ") ? auth.slice(7) : null;
86
+ }
87
+ function isOwnerRequest(req, options) {
88
+ const token = bearerToken(req);
89
+ const validTokens = [options.secretKey, options.key].filter(Boolean);
90
+ return !!token && validTokens.includes(token);
91
+ }
92
+ function readJsonBody(req, maxBytes = 256 * 1024) {
93
+ return new Promise((resolve, reject) => {
94
+ const chunks = [];
95
+ let size = 0;
96
+ req.on("data", (chunk) => {
97
+ size += chunk.length;
98
+ if (size > maxBytes) {
99
+ reject(new Error(`Request body too large (max ${maxBytes} bytes)`));
100
+ req.destroy();
101
+ return;
102
+ }
103
+ chunks.push(chunk);
104
+ });
105
+ req.on("end", () => {
106
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
107
+ if (!raw) {
108
+ resolve({});
109
+ return;
110
+ }
111
+ try {
112
+ resolve(JSON.parse(raw));
113
+ }
114
+ catch {
115
+ reject(new Error("Invalid JSON body"));
116
+ }
117
+ });
118
+ req.on("error", reject);
119
+ });
120
+ }
121
+ export async function handleSoftwareAgentRunHttp(req, res, deps) {
122
+ if (!isOwnerRequest(req, deps.options)) {
123
+ res.writeHead(401, { "Content-Type": "application/json" })
124
+ .end(JSON.stringify({ error: "Owner token required" }));
125
+ return;
126
+ }
127
+ if (!deps.softwareAgent) {
128
+ res.writeHead(503, { "Content-Type": "application/json" })
129
+ .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
130
+ return;
131
+ }
132
+ let body;
133
+ try {
134
+ body = await readJsonBody(req);
135
+ }
136
+ catch (err) {
137
+ res.writeHead(400, { "Content-Type": "application/json" })
138
+ .end(JSON.stringify({ error: err.message || "Invalid request body" }));
139
+ return;
140
+ }
141
+ let envelope;
142
+ try {
143
+ envelope = createOwnerTaskEnvelope(body, deps.workdir);
144
+ }
145
+ catch (err) {
146
+ res.writeHead(400, { "Content-Type": "application/json" })
147
+ .end(JSON.stringify({ error: err.message || "Invalid software-agent envelope" }));
148
+ return;
149
+ }
150
+ try {
151
+ const result = await deps.softwareAgent.sendTask(envelope);
152
+ res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" })
153
+ .end(JSON.stringify(result, null, 2));
154
+ }
155
+ catch (err) {
156
+ const busy = String(err.message || "").includes("busy");
157
+ res.writeHead(busy ? 409 : 500, { "Content-Type": "application/json" })
158
+ .end(JSON.stringify({ error: err.message || String(err) }));
159
+ }
160
+ }
161
+ export async function handleSoftwareAgentStatusHttp(req, res, deps) {
162
+ if (!isOwnerRequest(req, deps.options)) {
163
+ res.writeHead(401, { "Content-Type": "application/json" })
164
+ .end(JSON.stringify({ error: "Owner token required" }));
165
+ return;
166
+ }
167
+ if (!deps.softwareAgent) {
168
+ res.writeHead(503, { "Content-Type": "application/json" })
169
+ .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
170
+ return;
171
+ }
172
+ res.writeHead(200, { "Content-Type": "application/json" })
173
+ .end(JSON.stringify(deps.softwareAgent.getState(), null, 2));
174
+ }
175
+ export async function handleSoftwareAgentResetHttp(req, res, deps) {
176
+ if (!isOwnerRequest(req, deps.options)) {
177
+ res.writeHead(401, { "Content-Type": "application/json" })
178
+ .end(JSON.stringify({ error: "Owner token required" }));
179
+ return;
180
+ }
181
+ if (!deps.softwareAgent) {
182
+ res.writeHead(503, { "Content-Type": "application/json" })
183
+ .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
184
+ return;
185
+ }
186
+ try {
187
+ await deps.softwareAgent.resetSession();
188
+ res.writeHead(200, { "Content-Type": "application/json" })
189
+ .end(JSON.stringify({ ok: true, state: deps.softwareAgent.getState() }, null, 2));
190
+ }
191
+ catch (err) {
192
+ res.writeHead(500, { "Content-Type": "application/json" })
193
+ .end(JSON.stringify({ error: err.message || String(err) }));
194
+ }
195
+ }
81
196
  import { RelayPeripheral } from "./relay-peripheral.js";
82
197
  import { EnginePeripheral, LLM_ENGINES as LLM_ENGINES_SET } from "./engine-peripheral.js";
83
198
  import { EngineQueue } from "./engine-queue.js";
@@ -89,6 +204,8 @@ import { SocialModule } from "./social-module.js";
89
204
  import { LongTermModule } from "./longterm-module.js";
90
205
  import { ReflectionModule } from "./reflection-module.js";
91
206
  import { ScriptModule } from "./script-module.js";
207
+ import { FileEventLog, PersistentEventBus } from "./event-bus.js";
208
+ import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope } from "./software-agent-peripheral.js";
92
209
  import { SIG, sig } from "./types.js";
93
210
  import { loadConversation, listConversations, buildLLMContext } from "./context.js";
94
211
  import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
@@ -164,16 +281,38 @@ export async function serve(options) {
164
281
  },
165
282
  emitTaskCompleted,
166
283
  };
284
+ let codexSoftwareAgent = null;
167
285
  const httpServer = createServer(async (req, res) => {
168
286
  // Suppress noisy polling endpoints from log
169
287
  const isQuiet = req.url === "/self/state" || req.url?.startsWith("/self/state?");
170
288
  if (!isQuiet)
171
289
  console.log(`[http] ${req.method} ${req.url} session=${req.headers["mcp-session-id"] || "none"}`);
172
290
  try {
291
+ if (req.url === "/self/software-agent/run" && req.method === "POST") {
292
+ await handleSoftwareAgentRunHttp(req, res, {
293
+ options,
294
+ workdir,
295
+ softwareAgent: codexSoftwareAgent,
296
+ });
297
+ return;
298
+ }
299
+ if (req.url === "/self/software-agent/status" && req.method === "GET") {
300
+ await handleSoftwareAgentStatusHttp(req, res, {
301
+ options,
302
+ softwareAgent: codexSoftwareAgent,
303
+ });
304
+ return;
305
+ }
306
+ if (req.url === "/self/software-agent/reset" && req.method === "POST") {
307
+ await handleSoftwareAgentResetHttp(req, res, {
308
+ options,
309
+ softwareAgent: codexSoftwareAgent,
310
+ });
311
+ return;
312
+ }
173
313
  // Auth check
174
314
  if (options.key) {
175
- const auth = req.headers["authorization"];
176
- const token = auth?.startsWith("Bearer ") ? auth.slice(7) : null;
315
+ const token = bearerToken(req);
177
316
  if (token !== options.key) {
178
317
  console.log(`[http] Unauthorized (bad or missing token)`);
179
318
  res.writeHead(401, { "Content-Type": "application/json" })
@@ -347,11 +486,22 @@ export async function serve(options) {
347
486
  if (options.relayHttp) {
348
487
  relay.pullFromRelay(workdir, options.agentName).catch(err => console.log(`[sync] Pull from relay failed: ${err}`));
349
488
  }
350
- // V2: Shared module context + EventBus
351
- const { SimpleEventBus } = await import("./event-bus.js");
352
- const bus = new SimpleEventBus();
489
+ // V2: Shared module context + persistent EventBus.
490
+ // This is the durable spine for module/peripheral/engine activity; recovery
491
+ // side effects are intentionally deferred until the event schema stabilizes.
492
+ const eventLogDir = join(workdir, ".akemon", "agents", options.agentName, "events");
493
+ await mkdir(eventLogDir, { recursive: true });
494
+ const eventLogPath = join(eventLogDir, "events.jsonl");
495
+ const bus = new PersistentEventBus(new FileEventLog(eventLogPath));
496
+ console.log(`[v2] Event log: ${eventLogPath}`);
497
+ codexSoftwareAgent = new CodexSoftwareAgentPeripheral({
498
+ workdir,
499
+ model: process.env.AKEMON_CODEX_MODEL,
500
+ sandbox: "workspace-write",
501
+ });
502
+ await codexSoftwareAgent.start(bus);
353
503
  // Peripheral registry — Core routes by capability
354
- const peripherals = [relay, engineP];
504
+ const peripherals = [relay, engineP, codexSoftwareAgent];
355
505
  // requestCompute: acquire the engine slot (priority-aware), execute with a
356
506
  // hard timeout, and release. The slot release and subprocess kill are both
357
507
  // driven by the same AbortController so a stuck engine can't hold the lock.
@@ -514,6 +664,11 @@ export async function serve(options) {
514
664
  }
515
665
  catch { }
516
666
  }
667
+ try {
668
+ await codexSoftwareAgent?.stop();
669
+ }
670
+ catch { }
671
+ bus.getLog().close();
517
672
  process.exit(0);
518
673
  };
519
674
  process.on("SIGINT", shutdown);