akemon 0.3.2 → 0.3.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.
- package/README.md +6 -1
- package/dist/cli.js +209 -19
- package/dist/server.js +236 -2
- package/dist/software-agent-memory.js +141 -0
- package/dist/software-agent-peripheral.js +297 -24
- package/package.json +4 -4
- package/dist/context.test.js +0 -90
- package/dist/dashboard.html +0 -552
- package/dist/engine-queue.test.js +0 -99
- package/dist/engine-routing.test.js +0 -122
- package/dist/engine-stream.test.js +0 -103
- package/dist/event-bus.test.js +0 -51
- package/dist/orphan-scan.test.js +0 -81
- package/dist/reflection-module.integration.test.js +0 -180
- package/dist/reflection-module.test.js +0 -66
- package/dist/role-module.test.js +0 -208
- package/dist/software-agent-http.test.js +0 -108
- package/dist/software-agent-peripheral.test.js +0 -187
- package/dist/task-helpers.test.js +0 -88
package/README.md
CHANGED
|
@@ -164,11 +164,16 @@ akemon serve --name my-agent --engine claude
|
|
|
164
164
|
|
|
165
165
|
# In another terminal, ask the local software peripheral to work in the repo
|
|
166
166
|
akemon software-agent "Add one focused test and run the relevant test command."
|
|
167
|
+
|
|
168
|
+
# Review recent software-agent runs
|
|
169
|
+
akemon software-agent-tasks --limit 5
|
|
167
170
|
```
|
|
168
171
|
|
|
169
172
|
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
173
|
|
|
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.
|
|
174
|
+
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, streams local stdout/stderr by default, and every call is wrapped in an explicit task envelope with workdir, memory scope, risk level, allowed actions, and forbidden actions.
|
|
175
|
+
|
|
176
|
+
Software-agent tasks default to the `akemon serve` workdir boundary. Use `--allow-outside-workdir` only when you explicitly want the software agent to run outside that root. Each run is recorded under `.akemon/agents/<name>/software-agent/tasks/` with the envelope, result, output summaries, and git worktree status.
|
|
172
177
|
|
|
173
178
|
## Serve Options
|
|
174
179
|
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,166 @@ 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
|
+
function clampPositiveInt(value, fallback, max) {
|
|
22
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
23
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
24
|
+
return fallback;
|
|
25
|
+
return Math.min(parsed, max);
|
|
26
|
+
}
|
|
27
|
+
function printSoftwareAgentTaskList(tasks) {
|
|
28
|
+
if (!tasks.length) {
|
|
29
|
+
console.log("No software-agent tasks found.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const task of tasks) {
|
|
33
|
+
const result = task.result?.success === true ? "ok" : task.result?.success === false ? "error" : "pending";
|
|
34
|
+
const duration = typeof task.durationMs === "number" ? `${task.durationMs}ms` : "-";
|
|
35
|
+
const git = task.workdirStatus?.isRepo
|
|
36
|
+
? (task.workdirStatus.dirty ? `dirty:${task.workdirStatus.changedFiles?.length || 0}` : "clean")
|
|
37
|
+
: "no-git";
|
|
38
|
+
const goal = truncateOneLine(task.envelope?.goal || "", 90);
|
|
39
|
+
console.log(`${task.taskId} ${task.status}/${result} ${duration} ${git} ${task.updatedAt || task.startedAt}`);
|
|
40
|
+
if (goal)
|
|
41
|
+
console.log(` ${goal}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function truncateOneLine(value, max) {
|
|
45
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
46
|
+
if (oneLine.length <= max)
|
|
47
|
+
return oneLine;
|
|
48
|
+
return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
|
|
49
|
+
}
|
|
50
|
+
async function callLocalOwnerEndpoint(path, opts, init = {}) {
|
|
51
|
+
const res = await fetchLocalOwnerEndpoint(path, opts, init);
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
data = text ? JSON.parse(text) : {};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
data = { output: text };
|
|
59
|
+
}
|
|
60
|
+
if (!res.ok || data.success === false) {
|
|
61
|
+
console.error(data.error || text || `Request failed with HTTP ${res.status}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
async function fetchLocalOwnerEndpoint(path, opts, init = {}) {
|
|
67
|
+
const credentials = await getOrCreateRelayCredentials();
|
|
68
|
+
const port = parsePortOption(opts.port);
|
|
69
|
+
const headers = {
|
|
70
|
+
Authorization: `Bearer ${credentials.secretKey}`,
|
|
71
|
+
};
|
|
72
|
+
if (init.body !== undefined)
|
|
73
|
+
headers["Content-Type"] = "application/json";
|
|
74
|
+
let res;
|
|
75
|
+
try {
|
|
76
|
+
res = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
77
|
+
...init,
|
|
78
|
+
headers: {
|
|
79
|
+
...headers,
|
|
80
|
+
...init.headers,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const cause = error.cause;
|
|
86
|
+
if (error instanceof TypeError && error.message === "fetch failed" && cause?.message === "bad port") {
|
|
87
|
+
console.error(`Port ${port} cannot be used for the local akemon serve connection. Choose a different --port.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (error instanceof TypeError && error.message === "fetch failed") {
|
|
91
|
+
console.error(`Cannot connect to local akemon serve on port ${port}. Start it with: akemon serve --port ${port}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
return res;
|
|
97
|
+
}
|
|
98
|
+
async function streamLocalOwnerEndpoint(path, opts, body) {
|
|
99
|
+
const res = await fetchLocalOwnerEndpoint(path, opts, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
body: JSON.stringify(body),
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
let data;
|
|
106
|
+
try {
|
|
107
|
+
data = text ? JSON.parse(text) : {};
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
data = { error: text };
|
|
111
|
+
}
|
|
112
|
+
console.error(data.error || text || `Request failed with HTTP ${res.status}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
if (!res.body)
|
|
116
|
+
return;
|
|
117
|
+
const decoder = new TextDecoder();
|
|
118
|
+
let buffer = "";
|
|
119
|
+
let failed = false;
|
|
120
|
+
const reader = res.body.getReader();
|
|
121
|
+
while (true) {
|
|
122
|
+
const { done, value } = await reader.read();
|
|
123
|
+
if (done)
|
|
124
|
+
break;
|
|
125
|
+
buffer += decoder.decode(value, { stream: true });
|
|
126
|
+
const lines = buffer.split(/\r?\n/);
|
|
127
|
+
buffer = lines.pop() || "";
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
if (handleSoftwareAgentStreamLine(line))
|
|
130
|
+
failed = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
buffer += decoder.decode();
|
|
134
|
+
if (buffer.trim() && handleSoftwareAgentStreamLine(buffer))
|
|
135
|
+
failed = true;
|
|
136
|
+
if (failed)
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
function handleSoftwareAgentStreamLine(line) {
|
|
140
|
+
const trimmed = line.trim();
|
|
141
|
+
if (!trimmed)
|
|
142
|
+
return false;
|
|
143
|
+
let event;
|
|
144
|
+
try {
|
|
145
|
+
event = JSON.parse(trimmed);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
process.stderr.write(`${trimmed}\n`);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (event.type === "start" && event.taskId) {
|
|
152
|
+
process.stderr.write(`[software-agent] started ${event.taskId}\n`);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (event.type === "stdout" && typeof event.chunk === "string") {
|
|
156
|
+
process.stdout.write(event.chunk);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (event.type === "stderr" && typeof event.chunk === "string") {
|
|
160
|
+
process.stderr.write(event.chunk);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (event.type === "end") {
|
|
164
|
+
const result = event.result;
|
|
165
|
+
if (result?.success === false && result.error) {
|
|
166
|
+
process.stderr.write(`${result.error}\n`);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (event.type === "error") {
|
|
172
|
+
process.stderr.write(`${event.error || "Software-agent stream failed"}\n`);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
17
177
|
program
|
|
18
178
|
.name("akemon")
|
|
19
179
|
.description("Agent work marketplace — train your agent, let it work for others")
|
|
@@ -138,15 +298,15 @@ program
|
|
|
138
298
|
.argument("<goal...>", "Task goal to send to the software agent")
|
|
139
299
|
.option("-p, --port <port>", "Local akemon serve port", "3000")
|
|
140
300
|
.option("-w, --workdir <path>", "Workdir for the software agent (default: serve workdir)")
|
|
301
|
+
.option("--allow-outside-workdir", "Allow the software agent workdir to be outside the serve workdir")
|
|
141
302
|
.option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
|
|
142
303
|
.option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
|
|
143
304
|
.option("--risk <level>", "Risk level: low|medium|high", "medium")
|
|
144
305
|
.option("--memory-summary <text>", "Pre-filtered memory/context text to include")
|
|
145
306
|
.option("--deliverable <text>", "Expected output shape")
|
|
146
307
|
.option("--timeout-ms <ms>", "Task timeout in milliseconds")
|
|
308
|
+
.option("--no-stream", "Disable local streaming and wait for the final response")
|
|
147
309
|
.action(async (goalParts, opts) => {
|
|
148
|
-
const credentials = await getOrCreateRelayCredentials();
|
|
149
|
-
const port = parseInt(opts.port) || 3000;
|
|
150
310
|
const body = {
|
|
151
311
|
goal: goalParts.join(" "),
|
|
152
312
|
roleScope: opts.roleScope,
|
|
@@ -155,6 +315,8 @@ program
|
|
|
155
315
|
};
|
|
156
316
|
if (opts.workdir)
|
|
157
317
|
body.workdir = opts.workdir;
|
|
318
|
+
if (opts.allowOutsideWorkdir)
|
|
319
|
+
body.allowOutsideWorkdir = true;
|
|
158
320
|
if (opts.memorySummary)
|
|
159
321
|
body.memorySummary = opts.memorySummary;
|
|
160
322
|
if (opts.deliverable)
|
|
@@ -167,31 +329,59 @@ program
|
|
|
167
329
|
}
|
|
168
330
|
body.timeoutMs = timeoutMs;
|
|
169
331
|
}
|
|
170
|
-
|
|
332
|
+
if (opts.stream !== false) {
|
|
333
|
+
await streamLocalOwnerEndpoint("/self/software-agent/run-stream", opts, body);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const data = await callLocalOwnerEndpoint("/self/software-agent/run", opts, {
|
|
171
337
|
method: "POST",
|
|
172
|
-
headers: {
|
|
173
|
-
Authorization: `Bearer ${credentials.secretKey}`,
|
|
174
|
-
"Content-Type": "application/json",
|
|
175
|
-
},
|
|
176
338
|
body: JSON.stringify(body),
|
|
177
339
|
});
|
|
178
|
-
const text = await res.text();
|
|
179
|
-
let data;
|
|
180
|
-
try {
|
|
181
|
-
data = text ? JSON.parse(text) : {};
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
data = { output: text };
|
|
185
|
-
}
|
|
186
|
-
if (!res.ok || data.success === false) {
|
|
187
|
-
console.error(data.error || text || `Request failed with HTTP ${res.status}`);
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
340
|
if (data.output)
|
|
191
341
|
console.log(data.output);
|
|
192
342
|
else
|
|
193
343
|
console.log(JSON.stringify(data, null, 2));
|
|
194
344
|
});
|
|
345
|
+
program
|
|
346
|
+
.command("software-agent-status")
|
|
347
|
+
.description("Show the owner-only local software-agent peripheral state")
|
|
348
|
+
.option("-p, --port <port>", "Local akemon serve port", "3000")
|
|
349
|
+
.action(async (opts) => {
|
|
350
|
+
const data = await callLocalOwnerEndpoint("/self/software-agent/status", opts, {
|
|
351
|
+
method: "GET",
|
|
352
|
+
});
|
|
353
|
+
console.log(JSON.stringify(data, null, 2));
|
|
354
|
+
});
|
|
355
|
+
program
|
|
356
|
+
.command("software-agent-tasks")
|
|
357
|
+
.description("List recent owner-only software-agent task ledger records")
|
|
358
|
+
.argument("[taskId]", "Task id to inspect")
|
|
359
|
+
.option("-p, --port <port>", "Local akemon serve port", "3000")
|
|
360
|
+
.option("-l, --limit <n>", "Maximum recent tasks to list", "20")
|
|
361
|
+
.option("--json", "Print raw JSON")
|
|
362
|
+
.action(async (taskId, opts) => {
|
|
363
|
+
const path = taskId
|
|
364
|
+
? `/self/software-agent/tasks/${encodeURIComponent(taskId)}`
|
|
365
|
+
: `/self/software-agent/tasks?limit=${clampPositiveInt(opts.limit, 20, 100)}`;
|
|
366
|
+
const data = await callLocalOwnerEndpoint(path, opts, {
|
|
367
|
+
method: "GET",
|
|
368
|
+
});
|
|
369
|
+
if (opts.json || taskId) {
|
|
370
|
+
console.log(JSON.stringify(taskId ? data.task : data, null, 2));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
printSoftwareAgentTaskList(Array.isArray(data.tasks) ? data.tasks : []);
|
|
374
|
+
});
|
|
375
|
+
program
|
|
376
|
+
.command("software-agent-reset")
|
|
377
|
+
.description("Reset the owner-only local software-agent peripheral session")
|
|
378
|
+
.option("-p, --port <port>", "Local akemon serve port", "3000")
|
|
379
|
+
.action(async (opts) => {
|
|
380
|
+
const data = await callLocalOwnerEndpoint("/self/software-agent/reset", opts, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
});
|
|
383
|
+
console.log(JSON.stringify(data, null, 2));
|
|
384
|
+
});
|
|
195
385
|
program
|
|
196
386
|
.command("dashboard")
|
|
197
387
|
.description("Open your agent dashboard in the browser")
|
package/dist/server.js
CHANGED
|
@@ -141,6 +141,12 @@ export async function handleSoftwareAgentRunHttp(req, res, deps) {
|
|
|
141
141
|
let envelope;
|
|
142
142
|
try {
|
|
143
143
|
envelope = createOwnerTaskEnvelope(body, deps.workdir);
|
|
144
|
+
envelope.memorySummary = await buildSoftwareAgentMemorySummary({
|
|
145
|
+
workdir: deps.workdir,
|
|
146
|
+
agentName: deps.agentName,
|
|
147
|
+
envelope,
|
|
148
|
+
request: body,
|
|
149
|
+
});
|
|
144
150
|
}
|
|
145
151
|
catch (err) {
|
|
146
152
|
res.writeHead(400, { "Content-Type": "application/json" })
|
|
@@ -158,6 +164,196 @@ export async function handleSoftwareAgentRunHttp(req, res, deps) {
|
|
|
158
164
|
.end(JSON.stringify({ error: err.message || String(err) }));
|
|
159
165
|
}
|
|
160
166
|
}
|
|
167
|
+
export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
|
|
168
|
+
if (!isOwnerRequest(req, deps.options)) {
|
|
169
|
+
res.writeHead(401, { "Content-Type": "application/json" })
|
|
170
|
+
.end(JSON.stringify({ error: "Owner token required" }));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!deps.softwareAgent) {
|
|
174
|
+
res.writeHead(503, { "Content-Type": "application/json" })
|
|
175
|
+
.end(JSON.stringify({ error: "Software agent peripheral not ready" }));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let body;
|
|
179
|
+
try {
|
|
180
|
+
body = await readJsonBody(req);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
res.writeHead(400, { "Content-Type": "application/json" })
|
|
184
|
+
.end(JSON.stringify({ error: err.message || "Invalid request body" }));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let envelope;
|
|
188
|
+
try {
|
|
189
|
+
envelope = createOwnerTaskEnvelope(body, deps.workdir);
|
|
190
|
+
envelope.memorySummary = await buildSoftwareAgentMemorySummary({
|
|
191
|
+
workdir: deps.workdir,
|
|
192
|
+
agentName: deps.agentName,
|
|
193
|
+
envelope,
|
|
194
|
+
request: body,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
res.writeHead(400, { "Content-Type": "application/json" })
|
|
199
|
+
.end(JSON.stringify({ error: err.message || "Invalid software-agent envelope" }));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const abortController = new AbortController();
|
|
203
|
+
let responseFinished = false;
|
|
204
|
+
let streamStarted = false;
|
|
205
|
+
res.on("close", () => {
|
|
206
|
+
if (!responseFinished)
|
|
207
|
+
abortController.abort();
|
|
208
|
+
});
|
|
209
|
+
const ensureStreamStarted = () => {
|
|
210
|
+
if (streamStarted)
|
|
211
|
+
return;
|
|
212
|
+
streamStarted = true;
|
|
213
|
+
res.writeHead(200, {
|
|
214
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
215
|
+
"Cache-Control": "no-cache",
|
|
216
|
+
"X-Accel-Buffering": "no",
|
|
217
|
+
});
|
|
218
|
+
res.flushHeaders?.();
|
|
219
|
+
};
|
|
220
|
+
try {
|
|
221
|
+
await deps.softwareAgent.sendTask(envelope, {
|
|
222
|
+
signal: abortController.signal,
|
|
223
|
+
observer: {
|
|
224
|
+
onStart(event) {
|
|
225
|
+
ensureStreamStarted();
|
|
226
|
+
writeSoftwareAgentStreamEvent(res, {
|
|
227
|
+
type: "start",
|
|
228
|
+
taskId: event.taskId,
|
|
229
|
+
commandLine: event.commandLine,
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
onStream(event) {
|
|
233
|
+
ensureStreamStarted();
|
|
234
|
+
writeSoftwareAgentStreamEvent(res, {
|
|
235
|
+
type: event.stream,
|
|
236
|
+
taskId: event.taskId,
|
|
237
|
+
chunk: event.chunk,
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
onEnd(event) {
|
|
241
|
+
ensureStreamStarted();
|
|
242
|
+
writeSoftwareAgentStreamEvent(res, {
|
|
243
|
+
type: "end",
|
|
244
|
+
taskId: event.taskId,
|
|
245
|
+
result: event.result,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
if (!streamStarted) {
|
|
253
|
+
const busy = String(err.message || "").includes("busy");
|
|
254
|
+
res.writeHead(busy ? 409 : 500, { "Content-Type": "application/json" })
|
|
255
|
+
.end(JSON.stringify({ error: err.message || String(err) }));
|
|
256
|
+
responseFinished = true;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
writeSoftwareAgentStreamEvent(res, {
|
|
260
|
+
type: "error",
|
|
261
|
+
error: err.message || String(err),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
responseFinished = true;
|
|
266
|
+
if (streamStarted && !res.writableEnded)
|
|
267
|
+
res.end();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export async function handleSoftwareAgentStatusHttp(req, res, deps) {
|
|
271
|
+
if (!isOwnerRequest(req, deps.options)) {
|
|
272
|
+
res.writeHead(401, { "Content-Type": "application/json" })
|
|
273
|
+
.end(JSON.stringify({ error: "Owner token required" }));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!deps.softwareAgent) {
|
|
277
|
+
res.writeHead(503, { "Content-Type": "application/json" })
|
|
278
|
+
.end(JSON.stringify({ error: "Software agent peripheral not ready" }));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
282
|
+
.end(JSON.stringify(deps.softwareAgent.getState(), null, 2));
|
|
283
|
+
}
|
|
284
|
+
export async function handleSoftwareAgentTasksHttp(req, res, deps) {
|
|
285
|
+
if (!isOwnerRequest(req, deps.options)) {
|
|
286
|
+
res.writeHead(401, { "Content-Type": "application/json" })
|
|
287
|
+
.end(JSON.stringify({ error: "Owner token required" }));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
291
|
+
const basePath = "/self/software-agent/tasks";
|
|
292
|
+
const taskLedgerDir = softwareAgentTaskLedgerDir(deps.workdir, deps.agentName);
|
|
293
|
+
if (url.pathname === basePath) {
|
|
294
|
+
const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
|
|
295
|
+
const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit);
|
|
296
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
297
|
+
.end(JSON.stringify({ tasks }, null, 2));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (url.pathname.startsWith(`${basePath}/`)) {
|
|
301
|
+
const taskId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
|
|
302
|
+
if (!taskId || taskId.includes("/")) {
|
|
303
|
+
res.writeHead(400, { "Content-Type": "application/json" })
|
|
304
|
+
.end(JSON.stringify({ error: "Invalid software-agent task id" }));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const task = readSoftwareAgentTaskRecord(taskLedgerDir, taskId);
|
|
308
|
+
if (!task) {
|
|
309
|
+
res.writeHead(404, { "Content-Type": "application/json" })
|
|
310
|
+
.end(JSON.stringify({ error: "Software-agent task not found" }));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
314
|
+
.end(JSON.stringify({ task }, null, 2));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
res.writeHead(404, { "Content-Type": "application/json" })
|
|
318
|
+
.end(JSON.stringify({ error: "Software-agent task endpoint not found" }));
|
|
319
|
+
}
|
|
320
|
+
function softwareAgentTaskLedgerDir(workdir, agentName) {
|
|
321
|
+
return join(workdir, ".akemon", "agents", agentName, "software-agent", "tasks");
|
|
322
|
+
}
|
|
323
|
+
function readPositiveIntQuery(value, fallback, max) {
|
|
324
|
+
if (!value)
|
|
325
|
+
return fallback;
|
|
326
|
+
const parsed = Number(value);
|
|
327
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
328
|
+
return fallback;
|
|
329
|
+
return Math.min(parsed, max);
|
|
330
|
+
}
|
|
331
|
+
function writeSoftwareAgentStreamEvent(res, event) {
|
|
332
|
+
if (res.destroyed)
|
|
333
|
+
return;
|
|
334
|
+
res.write(`${JSON.stringify(event)}\n`);
|
|
335
|
+
}
|
|
336
|
+
export async function handleSoftwareAgentResetHttp(req, res, deps) {
|
|
337
|
+
if (!isOwnerRequest(req, deps.options)) {
|
|
338
|
+
res.writeHead(401, { "Content-Type": "application/json" })
|
|
339
|
+
.end(JSON.stringify({ error: "Owner token required" }));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (!deps.softwareAgent) {
|
|
343
|
+
res.writeHead(503, { "Content-Type": "application/json" })
|
|
344
|
+
.end(JSON.stringify({ error: "Software agent peripheral not ready" }));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
await deps.softwareAgent.resetSession();
|
|
349
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
350
|
+
.end(JSON.stringify({ ok: true, state: deps.softwareAgent.getState() }, null, 2));
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
res.writeHead(500, { "Content-Type": "application/json" })
|
|
354
|
+
.end(JSON.stringify({ error: err.message || String(err) }));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
161
357
|
import { RelayPeripheral } from "./relay-peripheral.js";
|
|
162
358
|
import { EnginePeripheral, LLM_ENGINES as LLM_ENGINES_SET } from "./engine-peripheral.js";
|
|
163
359
|
import { EngineQueue } from "./engine-queue.js";
|
|
@@ -170,7 +366,8 @@ import { LongTermModule } from "./longterm-module.js";
|
|
|
170
366
|
import { ReflectionModule } from "./reflection-module.js";
|
|
171
367
|
import { ScriptModule } from "./script-module.js";
|
|
172
368
|
import { FileEventLog, PersistentEventBus } from "./event-bus.js";
|
|
173
|
-
import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope } from "./software-agent-peripheral.js";
|
|
369
|
+
import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentTaskRecords, readSoftwareAgentTaskRecord, } from "./software-agent-peripheral.js";
|
|
370
|
+
import { buildSoftwareAgentMemorySummary } from "./software-agent-memory.js";
|
|
174
371
|
import { SIG, sig } from "./types.js";
|
|
175
372
|
import { loadConversation, listConversations, buildLLMContext } from "./context.js";
|
|
176
373
|
import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
|
|
@@ -253,10 +450,44 @@ export async function serve(options) {
|
|
|
253
450
|
if (!isQuiet)
|
|
254
451
|
console.log(`[http] ${req.method} ${req.url} session=${req.headers["mcp-session-id"] || "none"}`);
|
|
255
452
|
try {
|
|
453
|
+
if (req.url === "/self/software-agent/run-stream" && req.method === "POST") {
|
|
454
|
+
await handleSoftwareAgentRunStreamHttp(req, res, {
|
|
455
|
+
options,
|
|
456
|
+
workdir,
|
|
457
|
+
agentName: options.agentName,
|
|
458
|
+
softwareAgent: codexSoftwareAgent,
|
|
459
|
+
});
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
256
462
|
if (req.url === "/self/software-agent/run" && req.method === "POST") {
|
|
257
463
|
await handleSoftwareAgentRunHttp(req, res, {
|
|
258
464
|
options,
|
|
259
465
|
workdir,
|
|
466
|
+
agentName: options.agentName,
|
|
467
|
+
softwareAgent: codexSoftwareAgent,
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (req.url === "/self/software-agent/status" && req.method === "GET") {
|
|
472
|
+
await handleSoftwareAgentStatusHttp(req, res, {
|
|
473
|
+
options,
|
|
474
|
+
softwareAgent: codexSoftwareAgent,
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const requestPath = req.url?.split("?")[0] || "";
|
|
479
|
+
if (req.method === "GET"
|
|
480
|
+
&& (requestPath === "/self/software-agent/tasks" || requestPath.startsWith("/self/software-agent/tasks/"))) {
|
|
481
|
+
await handleSoftwareAgentTasksHttp(req, res, {
|
|
482
|
+
options,
|
|
483
|
+
workdir,
|
|
484
|
+
agentName: options.agentName,
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (req.url === "/self/software-agent/reset" && req.method === "POST") {
|
|
489
|
+
await handleSoftwareAgentResetHttp(req, res, {
|
|
490
|
+
options,
|
|
260
491
|
softwareAgent: codexSoftwareAgent,
|
|
261
492
|
});
|
|
262
493
|
return;
|
|
@@ -449,10 +680,13 @@ export async function serve(options) {
|
|
|
449
680
|
workdir,
|
|
450
681
|
model: process.env.AKEMON_CODEX_MODEL,
|
|
451
682
|
sandbox: "workspace-write",
|
|
683
|
+
taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
|
|
452
684
|
});
|
|
453
|
-
await codexSoftwareAgent.start(bus);
|
|
454
685
|
// Peripheral registry — Core routes by capability
|
|
455
686
|
const peripherals = [relay, engineP, codexSoftwareAgent];
|
|
687
|
+
for (const peripheral of peripherals) {
|
|
688
|
+
await peripheral.start(bus);
|
|
689
|
+
}
|
|
456
690
|
// requestCompute: acquire the engine slot (priority-aware), execute with a
|
|
457
691
|
// hard timeout, and release. The slot release and subprocess kill are both
|
|
458
692
|
// driven by the same AbortController so a stuck engine can't hold the lock.
|