akemon 0.3.4 → 0.3.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/DATA_POLICY.md +120 -0
- package/README.md +43 -0
- package/TRADEMARK.md +74 -0
- package/dist/cli.js +311 -71
- package/dist/engine-peripheral.js +5 -4
- package/dist/engine-routing.js +99 -0
- package/dist/event-bus.js +63 -17
- package/dist/privacy-filter.js +269 -0
- package/dist/redaction.js +159 -0
- package/dist/relay-client.js +39 -2
- package/dist/server.js +181 -103
- package/dist/software-agent-memory.js +9 -11
- package/dist/software-agent-peripheral.js +453 -22
- package/dist/software-agent-result-cli.js +69 -0
- package/dist/software-agent-stream-cli.js +124 -0
- package/dist/work-memory.js +295 -0
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,10 @@ import { getOrCreateRelayCredentials } from "./config.js";
|
|
|
6
6
|
import { connectRelay } from "./relay-client.js";
|
|
7
7
|
import { listAgents } from "./list.js";
|
|
8
8
|
import { connect } from "./connect.js";
|
|
9
|
+
import { PrivacyFilterUnavailableError, sanitizeText, } from "./privacy-filter.js";
|
|
10
|
+
import { SoftwareAgentStreamCliRenderer } from "./software-agent-stream-cli.js";
|
|
11
|
+
import { appendWorkMemoryNote, buildWorkMemoryContext, } from "./work-memory.js";
|
|
12
|
+
import { renderSoftwareAgentRunResult } from "./software-agent-result-cli.js";
|
|
9
13
|
import { readFileSync } from "fs";
|
|
10
14
|
import { fileURLToPath } from "url";
|
|
11
15
|
import { dirname, join } from "path";
|
|
@@ -24,6 +28,43 @@ function clampPositiveInt(value, fallback, max) {
|
|
|
24
28
|
return fallback;
|
|
25
29
|
return Math.min(parsed, max);
|
|
26
30
|
}
|
|
31
|
+
function parsePrivacyFilterMode(value) {
|
|
32
|
+
if (value === "fast" || value === "pii" || value === "strict")
|
|
33
|
+
return value;
|
|
34
|
+
console.error("--mode must be one of: fast, pii, strict");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
function parsePrivacyFilterBackend(value) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (value === "fast" || value === "opf")
|
|
41
|
+
return value;
|
|
42
|
+
console.error("--backend must be one of: fast, opf");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
function parseSoftwareAgentEnvPolicy(value) {
|
|
46
|
+
const normalized = (value || "inherit").trim().toLowerCase();
|
|
47
|
+
if (normalized === "inherit" || normalized === "allowlist")
|
|
48
|
+
return normalized;
|
|
49
|
+
console.error("--software-agent-env must be one of: inherit, allowlist");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
function parseCommaSeparatedCliOption(value) {
|
|
53
|
+
if (!value)
|
|
54
|
+
return undefined;
|
|
55
|
+
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
56
|
+
return items.length ? items : undefined;
|
|
57
|
+
}
|
|
58
|
+
function parsePositiveIntCliOption(value, optionName) {
|
|
59
|
+
if (value === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
62
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
63
|
+
console.error(`${optionName} must be a positive integer`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
27
68
|
function printSoftwareAgentTaskList(tasks) {
|
|
28
69
|
if (!tasks.length) {
|
|
29
70
|
console.log("No software-agent tasks found.");
|
|
@@ -37,10 +78,36 @@ function printSoftwareAgentTaskList(tasks) {
|
|
|
37
78
|
: "no-git";
|
|
38
79
|
const goal = truncateOneLine(task.envelope?.goal || "", 90);
|
|
39
80
|
console.log(`${task.taskId} ${task.status}/${result} ${duration} ${git} ${task.updatedAt || task.startedAt}`);
|
|
81
|
+
if (task.contextSession?.sessionId)
|
|
82
|
+
console.log(` session: ${task.contextSession.sessionId}`);
|
|
83
|
+
const workMemoryDir = task.envelope?.workMemoryDir || task.result?.workMemoryDir;
|
|
84
|
+
if (workMemoryDir)
|
|
85
|
+
console.log(` work memory: ${workMemoryDir}`);
|
|
40
86
|
if (goal)
|
|
41
87
|
console.log(` ${goal}`);
|
|
42
88
|
}
|
|
43
89
|
}
|
|
90
|
+
function printSoftwareAgentSessionList(sessions) {
|
|
91
|
+
if (!sessions.length) {
|
|
92
|
+
console.log("No software-agent context sessions found.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const session of sessions) {
|
|
96
|
+
const result = session.lastResult?.success === true ? "ok" : session.lastResult?.success === false ? "error" : "pending";
|
|
97
|
+
const duration = typeof session.lastResult?.durationMs === "number" ? `${session.lastResult.durationMs}ms` : "-";
|
|
98
|
+
const updatedAt = session.updatedAt || "-";
|
|
99
|
+
const goal = truncateOneLine(session.lastGoal || "", 90);
|
|
100
|
+
console.log(`${session.sessionId} ${result} ${duration} ${updatedAt}`);
|
|
101
|
+
if (session.lastTaskId)
|
|
102
|
+
console.log(` last task: ${session.lastTaskId}`);
|
|
103
|
+
if (goal)
|
|
104
|
+
console.log(` ${goal}`);
|
|
105
|
+
if (session.packetPath)
|
|
106
|
+
console.log(` context: ${session.packetPath}`);
|
|
107
|
+
if (session.workMemoryDir)
|
|
108
|
+
console.log(` work memory: ${session.workMemoryDir}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
44
111
|
function truncateOneLine(value, max) {
|
|
45
112
|
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
46
113
|
if (oneLine.length <= max)
|
|
@@ -117,6 +184,7 @@ async function streamLocalOwnerEndpoint(path, opts, body) {
|
|
|
117
184
|
const decoder = new TextDecoder();
|
|
118
185
|
let buffer = "";
|
|
119
186
|
let failed = false;
|
|
187
|
+
const streamRenderer = new SoftwareAgentStreamCliRenderer();
|
|
120
188
|
const reader = res.body.getReader();
|
|
121
189
|
while (true) {
|
|
122
190
|
const { done, value } = await reader.read();
|
|
@@ -126,57 +194,74 @@ async function streamLocalOwnerEndpoint(path, opts, body) {
|
|
|
126
194
|
const lines = buffer.split(/\r?\n/);
|
|
127
195
|
buffer = lines.pop() || "";
|
|
128
196
|
for (const line of lines) {
|
|
129
|
-
if (
|
|
197
|
+
if (streamRenderer.handleLine(line))
|
|
130
198
|
failed = true;
|
|
131
199
|
}
|
|
132
200
|
}
|
|
133
201
|
buffer += decoder.decode();
|
|
134
|
-
if (buffer.trim() &&
|
|
202
|
+
if (buffer.trim() && streamRenderer.handleLine(buffer))
|
|
135
203
|
failed = true;
|
|
136
204
|
if (failed)
|
|
137
205
|
process.exit(1);
|
|
138
206
|
}
|
|
139
|
-
function
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
207
|
+
async function runSoftwareAgentCli(goalParts, opts, forcedSessionId) {
|
|
208
|
+
const body = {
|
|
209
|
+
goal: goalParts.join(" "),
|
|
210
|
+
roleScope: opts.roleScope,
|
|
211
|
+
memoryScope: opts.memoryScope,
|
|
212
|
+
riskLevel: opts.risk,
|
|
213
|
+
};
|
|
214
|
+
if (opts.workdir)
|
|
215
|
+
body.workdir = opts.workdir;
|
|
216
|
+
if (opts.allowOutsideWorkdir)
|
|
217
|
+
body.allowOutsideWorkdir = true;
|
|
218
|
+
if (opts.memorySummary)
|
|
219
|
+
body.memorySummary = opts.memorySummary;
|
|
220
|
+
const workContextBudget = parsePositiveIntCliOption(opts.workContextBudget, "--work-context-budget");
|
|
221
|
+
if (opts.workContext || workContextBudget !== undefined)
|
|
222
|
+
body.includeWorkMemoryContext = true;
|
|
223
|
+
if (workContextBudget !== undefined)
|
|
224
|
+
body.workMemoryContextBudget = workContextBudget;
|
|
225
|
+
const sessionId = forcedSessionId || opts.session;
|
|
226
|
+
if (sessionId)
|
|
227
|
+
body.contextSessionId = sessionId;
|
|
228
|
+
if (opts.deliverable)
|
|
229
|
+
body.deliverable = opts.deliverable;
|
|
230
|
+
if (opts.timeoutMs) {
|
|
231
|
+
const timeoutMs = Number(opts.timeoutMs);
|
|
232
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
233
|
+
console.error("--timeout-ms must be a positive integer");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
body.timeoutMs = timeoutMs;
|
|
237
|
+
}
|
|
238
|
+
if (opts.stream !== false) {
|
|
239
|
+
await streamLocalOwnerEndpoint("/self/software-agent/run-stream", opts, body);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const res = await fetchLocalOwnerEndpoint("/self/software-agent/run", opts, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
body: JSON.stringify(body),
|
|
245
|
+
});
|
|
246
|
+
const text = await res.text();
|
|
247
|
+
let data;
|
|
144
248
|
try {
|
|
145
|
-
|
|
249
|
+
data = text ? JSON.parse(text) : {};
|
|
146
250
|
}
|
|
147
251
|
catch {
|
|
148
|
-
|
|
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;
|
|
252
|
+
data = { output: text };
|
|
170
253
|
}
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
console.error(data.error || text || `Request failed with HTTP ${res.status}`);
|
|
256
|
+
process.exit(1);
|
|
174
257
|
}
|
|
175
|
-
|
|
258
|
+
const failed = renderSoftwareAgentRunResult(data);
|
|
259
|
+
if (failed)
|
|
260
|
+
process.exit(1);
|
|
176
261
|
}
|
|
177
262
|
program
|
|
178
263
|
.name("akemon")
|
|
179
|
-
.description("
|
|
264
|
+
.description("Local AI companion runtime with memory, modules, relay sync, and software-agent control")
|
|
180
265
|
.version(pkg.version);
|
|
181
266
|
program
|
|
182
267
|
.command("serve")
|
|
@@ -202,6 +287,8 @@ program
|
|
|
202
287
|
.option("--without <modules>", "Disable specific modules (comma-separated: biostate,memory)")
|
|
203
288
|
.option("--script <name>", "Script to load for ScriptModule (default: daily-life)", "daily-life")
|
|
204
289
|
.option("--terminal", "Enable remote terminal access (PTY)")
|
|
290
|
+
.option("--software-agent-env <policy>", "Software-agent child environment policy: inherit or allowlist", process.env.AKEMON_SOFTWARE_AGENT_ENV_POLICY || "inherit")
|
|
291
|
+
.option("--software-agent-env-allow <vars>", "Comma-separated extra env vars for software-agent allowlist")
|
|
205
292
|
.option("--relay <url>", "Relay WebSocket URL", RELAY_WS)
|
|
206
293
|
.action(async (opts) => {
|
|
207
294
|
const port = parseInt(opts.port);
|
|
@@ -237,6 +324,8 @@ program
|
|
|
237
324
|
notifyUrl: opts.notify,
|
|
238
325
|
enabledModules,
|
|
239
326
|
scriptName: opts.script,
|
|
327
|
+
softwareAgentEnvPolicy: parseSoftwareAgentEnvPolicy(opts.softwareAgentEnv),
|
|
328
|
+
softwareAgentEnvAllowlist: parseCommaSeparatedCliOption(opts.softwareAgentEnvAllow),
|
|
240
329
|
});
|
|
241
330
|
console.log(`\nakemon v${pkg.version}`);
|
|
242
331
|
if (!opts.public) {
|
|
@@ -303,44 +392,34 @@ program
|
|
|
303
392
|
.option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
|
|
304
393
|
.option("--risk <level>", "Risk level: low|medium|high", "medium")
|
|
305
394
|
.option("--memory-summary <text>", "Pre-filtered memory/context text to include")
|
|
395
|
+
.option("--work-context", "Embed a bounded work-memory context packet in the task envelope")
|
|
396
|
+
.option("--work-context-budget <chars>", "Maximum embedded work-memory context size; also enables --work-context")
|
|
397
|
+
.option("--session <id>", "Akemon-side context session id for explicit software-agent continuity")
|
|
306
398
|
.option("--deliverable <text>", "Expected output shape")
|
|
307
399
|
.option("--timeout-ms <ms>", "Task timeout in milliseconds")
|
|
308
400
|
.option("--no-stream", "Disable local streaming and wait for the final response")
|
|
309
401
|
.action(async (goalParts, opts) => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
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, {
|
|
337
|
-
method: "POST",
|
|
338
|
-
body: JSON.stringify(body),
|
|
339
|
-
});
|
|
340
|
-
if (data.output)
|
|
341
|
-
console.log(data.output);
|
|
342
|
-
else
|
|
343
|
-
console.log(JSON.stringify(data, null, 2));
|
|
402
|
+
await runSoftwareAgentCli(goalParts, opts);
|
|
403
|
+
});
|
|
404
|
+
program
|
|
405
|
+
.command("software-agent-continue")
|
|
406
|
+
.description("Continue an Akemon-side software-agent context session")
|
|
407
|
+
.argument("<sessionId>", "Akemon-side context session id to continue")
|
|
408
|
+
.argument("<goal...>", "Task goal to send to the software agent")
|
|
409
|
+
.option("-p, --port <port>", "Local akemon serve port", "3000")
|
|
410
|
+
.option("-w, --workdir <path>", "Workdir for the software agent (default: serve workdir)")
|
|
411
|
+
.option("--allow-outside-workdir", "Allow the software agent workdir to be outside the serve workdir")
|
|
412
|
+
.option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
|
|
413
|
+
.option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
|
|
414
|
+
.option("--risk <level>", "Risk level: low|medium|high", "medium")
|
|
415
|
+
.option("--memory-summary <text>", "Pre-filtered memory/context text to include")
|
|
416
|
+
.option("--work-context", "Embed a bounded work-memory context packet in the task envelope")
|
|
417
|
+
.option("--work-context-budget <chars>", "Maximum embedded work-memory context size; also enables --work-context")
|
|
418
|
+
.option("--deliverable <text>", "Expected output shape")
|
|
419
|
+
.option("--timeout-ms <ms>", "Task timeout in milliseconds")
|
|
420
|
+
.option("--no-stream", "Disable local streaming and wait for the final response")
|
|
421
|
+
.action(async (sessionId, goalParts, opts) => {
|
|
422
|
+
await runSoftwareAgentCli(goalParts, opts, sessionId);
|
|
344
423
|
});
|
|
345
424
|
program
|
|
346
425
|
.command("software-agent-status")
|
|
@@ -358,20 +437,78 @@ program
|
|
|
358
437
|
.argument("[taskId]", "Task id to inspect")
|
|
359
438
|
.option("-p, --port <port>", "Local akemon serve port", "3000")
|
|
360
439
|
.option("-l, --limit <n>", "Maximum recent tasks to list", "20")
|
|
440
|
+
.option("--session <id>", "Filter listed tasks by Akemon-side context session id")
|
|
441
|
+
.option("--context", "Print the task's Akemon TASK_CONTEXT.md content when inspecting one task")
|
|
361
442
|
.option("--json", "Print raw JSON")
|
|
362
443
|
.action(async (taskId, opts) => {
|
|
444
|
+
if (!taskId && opts.context) {
|
|
445
|
+
console.error("--context requires a taskId");
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
if (taskId && opts.session) {
|
|
449
|
+
console.error("--session cannot be used when inspecting a single taskId");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
363
452
|
const path = taskId
|
|
364
|
-
? `/self/software-agent/tasks/${encodeURIComponent(taskId)}`
|
|
365
|
-
: `/self/software-agent/tasks?limit=${clampPositiveInt(opts.limit, 20, 100)}`;
|
|
453
|
+
? `/self/software-agent/tasks/${encodeURIComponent(taskId)}${opts.context ? "?includeContext=1" : ""}`
|
|
454
|
+
: `/self/software-agent/tasks?limit=${clampPositiveInt(opts.limit, 20, 100)}${opts.session ? `&session=${encodeURIComponent(opts.session)}` : ""}`;
|
|
366
455
|
const data = await callLocalOwnerEndpoint(path, opts, {
|
|
367
456
|
method: "GET",
|
|
368
457
|
});
|
|
458
|
+
if (taskId && opts.context) {
|
|
459
|
+
const contextPacket = data.contextSession?.contextPacket;
|
|
460
|
+
if (typeof contextPacket === "string" && contextPacket.length > 0) {
|
|
461
|
+
process.stdout.write(contextPacket);
|
|
462
|
+
if (!contextPacket.endsWith("\n"))
|
|
463
|
+
process.stdout.write("\n");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
console.error("No TASK_CONTEXT.md content found for this task.");
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
369
469
|
if (opts.json || taskId) {
|
|
370
470
|
console.log(JSON.stringify(taskId ? data.task : data, null, 2));
|
|
371
471
|
return;
|
|
372
472
|
}
|
|
373
473
|
printSoftwareAgentTaskList(Array.isArray(data.tasks) ? data.tasks : []);
|
|
374
474
|
});
|
|
475
|
+
program
|
|
476
|
+
.command("software-agent-sessions")
|
|
477
|
+
.description("List or inspect owner-only Akemon-side software-agent context sessions")
|
|
478
|
+
.argument("[sessionId]", "Context session id to inspect")
|
|
479
|
+
.option("-p, --port <port>", "Local akemon serve port", "3000")
|
|
480
|
+
.option("-l, --limit <n>", "Maximum recent sessions to list", "20")
|
|
481
|
+
.option("--context", "Print the session TASK_CONTEXT.md content")
|
|
482
|
+
.option("--json", "Print raw JSON")
|
|
483
|
+
.action(async (sessionId, opts) => {
|
|
484
|
+
const query = sessionId && opts.context ? "?includeContext=1" : "";
|
|
485
|
+
const path = sessionId
|
|
486
|
+
? `/self/software-agent/sessions/${encodeURIComponent(sessionId)}${query}`
|
|
487
|
+
: `/self/software-agent/sessions?limit=${clampPositiveInt(opts.limit, 20, 100)}`;
|
|
488
|
+
const data = await callLocalOwnerEndpoint(path, opts, {
|
|
489
|
+
method: "GET",
|
|
490
|
+
});
|
|
491
|
+
if (sessionId) {
|
|
492
|
+
if (opts.context) {
|
|
493
|
+
const contextPacket = data.session?.contextPacket;
|
|
494
|
+
if (typeof contextPacket === "string" && contextPacket.length > 0) {
|
|
495
|
+
process.stdout.write(contextPacket);
|
|
496
|
+
if (!contextPacket.endsWith("\n"))
|
|
497
|
+
process.stdout.write("\n");
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
console.error("No TASK_CONTEXT.md content found for this session.");
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
console.log(JSON.stringify(data.session, null, 2));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (opts.json) {
|
|
507
|
+
console.log(JSON.stringify(data, null, 2));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
printSoftwareAgentSessionList(Array.isArray(data.sessions) ? data.sessions : []);
|
|
511
|
+
});
|
|
375
512
|
program
|
|
376
513
|
.command("software-agent-reset")
|
|
377
514
|
.description("Reset the owner-only local software-agent peripheral session")
|
|
@@ -382,6 +519,109 @@ program
|
|
|
382
519
|
});
|
|
383
520
|
console.log(JSON.stringify(data, null, 2));
|
|
384
521
|
});
|
|
522
|
+
program
|
|
523
|
+
.command("privacy-filter")
|
|
524
|
+
.description("Sanitize text with built-in redaction and optional OpenAI Privacy Filter")
|
|
525
|
+
.argument("<text...>", "Text to sanitize")
|
|
526
|
+
.option("--mode <mode>", "Mode: fast, pii, or strict", "fast")
|
|
527
|
+
.option("--backend <backend>", "Backend: fast or opf")
|
|
528
|
+
.option("--command <command>", "OPF command (default: opf)")
|
|
529
|
+
.option("--device <device>", "OPF device, e.g. cpu or cuda")
|
|
530
|
+
.option("--checkpoint <path>", "OPF checkpoint directory")
|
|
531
|
+
.option("--timeout-ms <ms>", "OPF timeout in milliseconds")
|
|
532
|
+
.option("--max-input-chars <n>", "Maximum text length to pass to OPF")
|
|
533
|
+
.option("--json", "Print result metadata as JSON")
|
|
534
|
+
.action(async (textParts, opts) => {
|
|
535
|
+
try {
|
|
536
|
+
const result = await sanitizeText(textParts.join(" "), {
|
|
537
|
+
mode: parsePrivacyFilterMode(opts.mode),
|
|
538
|
+
backend: parsePrivacyFilterBackend(opts.backend),
|
|
539
|
+
command: opts.command,
|
|
540
|
+
device: opts.device,
|
|
541
|
+
checkpoint: opts.checkpoint,
|
|
542
|
+
timeoutMs: parsePositiveIntCliOption(opts.timeoutMs, "--timeout-ms"),
|
|
543
|
+
maxInputChars: parsePositiveIntCliOption(opts.maxInputChars, "--max-input-chars"),
|
|
544
|
+
});
|
|
545
|
+
if (opts.json) {
|
|
546
|
+
console.log(JSON.stringify(result, null, 2));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
console.log(result.text);
|
|
550
|
+
for (const warning of result.warnings) {
|
|
551
|
+
console.error(`[privacy-filter] ${warning}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (error instanceof PrivacyFilterUnavailableError || error instanceof TypeError) {
|
|
556
|
+
console.error(error.message);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
program
|
|
563
|
+
.command("work-context")
|
|
564
|
+
.description("Print a work-memory context packet for external software agents")
|
|
565
|
+
.option("-w, --workdir <path>", "Akemon workdir (default: cwd)")
|
|
566
|
+
.option("-n, --name <name>", "Agent name", "my-agent")
|
|
567
|
+
.option("--purpose <text>", "Purpose of this context packet", "external software-agent work context")
|
|
568
|
+
.option("--budget <chars>", "Maximum packet size in characters", "12000")
|
|
569
|
+
.option("--json", "Print raw JSON")
|
|
570
|
+
.action(async (opts) => {
|
|
571
|
+
try {
|
|
572
|
+
const packet = await buildWorkMemoryContext({
|
|
573
|
+
workdir: opts.workdir || process.cwd(),
|
|
574
|
+
agentName: opts.name,
|
|
575
|
+
purpose: opts.purpose,
|
|
576
|
+
budget: parsePositiveIntCliOption(opts.budget, "--budget"),
|
|
577
|
+
});
|
|
578
|
+
if (opts.json) {
|
|
579
|
+
console.log(JSON.stringify(packet, null, 2));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
process.stdout.write(packet.text);
|
|
583
|
+
if (!packet.text.endsWith("\n"))
|
|
584
|
+
process.stdout.write("\n");
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
program
|
|
592
|
+
.command("work-note")
|
|
593
|
+
.description("Append a note to Akemon work memory")
|
|
594
|
+
.argument("<text...>", "Durable work-memory note")
|
|
595
|
+
.option("-w, --workdir <path>", "Akemon workdir (default: cwd)")
|
|
596
|
+
.option("-n, --name <name>", "Agent name", "my-agent")
|
|
597
|
+
.option("--source <source>", "Note source, e.g. user, codex, or claude-code", "user")
|
|
598
|
+
.option("--session <id>", "External or Akemon-side session id")
|
|
599
|
+
.option("--kind <kind>", "Work-memory kind, e.g. note, decision, command, project", "note")
|
|
600
|
+
.option("--target <path>", "Optional target file under the work memory directory")
|
|
601
|
+
.option("--json", "Print raw JSON")
|
|
602
|
+
.action(async (textParts, opts) => {
|
|
603
|
+
try {
|
|
604
|
+
const result = await appendWorkMemoryNote({
|
|
605
|
+
workdir: opts.workdir || process.cwd(),
|
|
606
|
+
agentName: opts.name,
|
|
607
|
+
text: textParts.join(" "),
|
|
608
|
+
source: opts.source,
|
|
609
|
+
sessionId: opts.session,
|
|
610
|
+
kind: opts.kind,
|
|
611
|
+
target: opts.target,
|
|
612
|
+
});
|
|
613
|
+
if (opts.json) {
|
|
614
|
+
console.log(JSON.stringify(result, null, 2));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
console.log(`Work memory note appended: ${result.note.id}`);
|
|
618
|
+
console.log(`Path: ${result.path}`);
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
385
625
|
program
|
|
386
626
|
.command("dashboard")
|
|
387
627
|
.description("Open your agent dashboard in the browser")
|
|
@@ -17,7 +17,7 @@ import { callAgent, sendTaskEnd, sendTaskStart, sendTaskStream } from "./relay-c
|
|
|
17
17
|
import { SIG, sig } from "./types.js";
|
|
18
18
|
import { updateMetrics, pushExecMs } from "./metrics.js";
|
|
19
19
|
import { sendFailureEvent } from "./relay-client.js";
|
|
20
|
-
import {
|
|
20
|
+
import { resolveEngineRoute, } from "./engine-routing.js";
|
|
21
21
|
export const LLM_ENGINES = new Set(["claude", "codex", "opencode", "gemini", "raw"]);
|
|
22
22
|
const defaultTaskRelay = {
|
|
23
23
|
sendTaskStart,
|
|
@@ -100,11 +100,12 @@ export class EnginePeripheral {
|
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
// Unified engine runner
|
|
102
102
|
// ---------------------------------------------------------------------------
|
|
103
|
-
async runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId) {
|
|
104
|
-
const
|
|
103
|
+
async runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId, routeRequest) {
|
|
104
|
+
const resolution = resolveEngineRoute(routing, { origin, ...routeRequest });
|
|
105
|
+
const entry = resolution.entry;
|
|
105
106
|
const cfg = entry ? applyRoutingEntry(this.config, entry) : this.config;
|
|
106
107
|
if (origin && entry) {
|
|
107
|
-
console.log(`[engine] using ${cfg.engine}${cfg.model ? `/${cfg.model}` : ""} (origin=${origin})`);
|
|
108
|
+
console.log(`[engine] using ${cfg.engine}${cfg.model ? `/${cfg.model}` : ""} (origin=${origin}, source=${resolution.source})`);
|
|
108
109
|
}
|
|
109
110
|
const t0 = Date.now();
|
|
110
111
|
try {
|
package/dist/engine-routing.js
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* deriveChildOrigin — returns the origin a child/sub-task should carry
|
|
7
7
|
* downgradeForRetry — downgrades any origin to "retry" when a task retries
|
|
8
8
|
*/
|
|
9
|
+
export class EngineRegistry {
|
|
10
|
+
routing;
|
|
11
|
+
constructor(routing) {
|
|
12
|
+
this.routing = routing;
|
|
13
|
+
}
|
|
14
|
+
resolve(request = {}) {
|
|
15
|
+
return resolveEngineRoute(this.routing, request);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
9
18
|
/**
|
|
10
19
|
* Resolve which engine routing entry to use for a given origin.
|
|
11
20
|
*
|
|
@@ -27,6 +36,34 @@ export function resolveEngineConfig(routing, origin) {
|
|
|
27
36
|
}
|
|
28
37
|
return routing.default ?? null;
|
|
29
38
|
}
|
|
39
|
+
export function resolveEngineRoute(routing, request = {}) {
|
|
40
|
+
if (!routing) {
|
|
41
|
+
return { entry: null, source: "none", reason: "no routing configured" };
|
|
42
|
+
}
|
|
43
|
+
const route = selectRoute(routing.routes, request);
|
|
44
|
+
if (route) {
|
|
45
|
+
return {
|
|
46
|
+
entry: stripRouteMetadata(route),
|
|
47
|
+
source: "route",
|
|
48
|
+
reason: "matched registry route",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (request.origin && routing[request.origin]) {
|
|
52
|
+
return {
|
|
53
|
+
entry: routing[request.origin],
|
|
54
|
+
source: "origin",
|
|
55
|
+
reason: `matched legacy origin route ${request.origin}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (routing.default) {
|
|
59
|
+
return {
|
|
60
|
+
entry: routing.default,
|
|
61
|
+
source: "default",
|
|
62
|
+
reason: "matched legacy default route",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { entry: null, source: "none", reason: "no matching route" };
|
|
66
|
+
}
|
|
30
67
|
/**
|
|
31
68
|
* Derive the origin that a child task should carry.
|
|
32
69
|
*
|
|
@@ -50,3 +87,65 @@ export function deriveChildOrigin(_parentOrigin) {
|
|
|
50
87
|
export function downgradeForRetry(_origin) {
|
|
51
88
|
return "retry";
|
|
52
89
|
}
|
|
90
|
+
function selectRoute(routes, request) {
|
|
91
|
+
if (!routes?.length)
|
|
92
|
+
return null;
|
|
93
|
+
let best = null;
|
|
94
|
+
for (let index = 0; index < routes.length; index++) {
|
|
95
|
+
const route = routes[index];
|
|
96
|
+
if (!routeMatches(route, request))
|
|
97
|
+
continue;
|
|
98
|
+
const score = scoreRoute(route, request);
|
|
99
|
+
if (!best || score > best.score || (score === best.score && index < best.index)) {
|
|
100
|
+
best = { route, score, index };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return best?.route ?? null;
|
|
104
|
+
}
|
|
105
|
+
function routeMatches(route, request) {
|
|
106
|
+
if (request.origin && route.origins?.length && !route.origins.includes(request.origin))
|
|
107
|
+
return false;
|
|
108
|
+
if (!hasRequiredCapabilities(route.capabilities, request.requiredCapabilities))
|
|
109
|
+
return false;
|
|
110
|
+
if (request.privacy && route.privacy && route.privacy !== request.privacy)
|
|
111
|
+
return false;
|
|
112
|
+
if (request.maxCost && route.cost && tierRank(route.cost) > tierRank(request.maxCost))
|
|
113
|
+
return false;
|
|
114
|
+
if (request.maxLatency && route.latency && tierRank(route.latency) > tierRank(request.maxLatency))
|
|
115
|
+
return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
function hasRequiredCapabilities(available, required) {
|
|
119
|
+
if (!required?.length)
|
|
120
|
+
return true;
|
|
121
|
+
if (!available?.length)
|
|
122
|
+
return false;
|
|
123
|
+
return required.every((capability) => available.includes(capability));
|
|
124
|
+
}
|
|
125
|
+
function scoreRoute(route, request) {
|
|
126
|
+
let score = route.priority ?? 0;
|
|
127
|
+
if (request.origin && route.origins?.includes(request.origin))
|
|
128
|
+
score += 100;
|
|
129
|
+
if (request.origin && !route.origins?.length)
|
|
130
|
+
score += 10;
|
|
131
|
+
if (request.requiredCapabilities?.length)
|
|
132
|
+
score += (route.capabilities?.length || 0) * 2;
|
|
133
|
+
if (request.privacy && route.privacy === request.privacy)
|
|
134
|
+
score += 20;
|
|
135
|
+
if (route.cost)
|
|
136
|
+
score += 6 - tierRank(route.cost);
|
|
137
|
+
if (route.latency)
|
|
138
|
+
score += 6 - tierRank(route.latency);
|
|
139
|
+
return score;
|
|
140
|
+
}
|
|
141
|
+
function stripRouteMetadata(route) {
|
|
142
|
+
const { origins: _origins, priority: _priority, ...entry } = route;
|
|
143
|
+
return entry;
|
|
144
|
+
}
|
|
145
|
+
function tierRank(tier) {
|
|
146
|
+
switch (tier) {
|
|
147
|
+
case "low": return 1;
|
|
148
|
+
case "medium": return 2;
|
|
149
|
+
case "high": return 3;
|
|
150
|
+
}
|
|
151
|
+
}
|