akemon 0.3.5 → 0.3.7

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.
Files changed (54) hide show
  1. package/DATA_POLICY.md +128 -0
  2. package/README.md +156 -19
  3. package/TRADEMARK.md +74 -0
  4. package/dist/akemon-home.js +56 -0
  5. package/dist/akemon-message.js +107 -0
  6. package/dist/best-effort.js +8 -0
  7. package/dist/cli.js +1411 -132
  8. package/dist/cognitive-artifact-store.js +101 -0
  9. package/dist/cognitive-event-log.js +47 -0
  10. package/dist/config.js +45 -9
  11. package/dist/context.js +27 -6
  12. package/dist/core/contracts/layers.js +1 -0
  13. package/dist/core/contracts/permission.js +1 -0
  14. package/dist/core/contracts/workspace.js +1 -0
  15. package/dist/core-cognitive-module.js +768 -0
  16. package/dist/engine-peripheral.js +127 -26
  17. package/dist/engine-routing.js +58 -17
  18. package/dist/interactive-session.js +361 -0
  19. package/dist/local-interconnect.js +156 -0
  20. package/dist/local-registry.js +178 -0
  21. package/dist/mcp-server.js +4 -1
  22. package/dist/memory-proposal.js +379 -0
  23. package/dist/memory-recorder.js +368 -0
  24. package/dist/orphan-scan.js +36 -24
  25. package/dist/passive-reflection-cognitive-module.js +172 -0
  26. package/dist/peripheral-registry.js +235 -0
  27. package/dist/permission-audit.js +132 -0
  28. package/dist/relay-client.js +68 -9
  29. package/dist/relay-mode.js +34 -0
  30. package/dist/relay-peripheral.js +139 -49
  31. package/dist/runtime-platform.js +122 -0
  32. package/dist/secretariat/client.js +87 -0
  33. package/dist/self.js +15 -6
  34. package/dist/server.js +3695 -439
  35. package/dist/social-discovery.js +231 -0
  36. package/dist/software-agent-peripheral.js +314 -235
  37. package/dist/software-agent-result-cli.js +69 -0
  38. package/dist/software-agent-stream-cli.js +23 -0
  39. package/dist/software-agent-transport.js +177 -0
  40. package/dist/task-module.js +243 -0
  41. package/dist/task-registry.js +756 -0
  42. package/dist/vendor/xterm/addon-fit.js +2 -0
  43. package/dist/vendor/xterm/addon-search.js +2 -0
  44. package/dist/vendor/xterm/addon-web-links.js +2 -0
  45. package/dist/vendor/xterm/xterm.css +285 -0
  46. package/dist/vendor/xterm/xterm.js +2 -0
  47. package/dist/work-memory.js +339 -0
  48. package/dist/workbench-peripheral-guide.js +79 -0
  49. package/dist/workbench-session.js +1074 -0
  50. package/dist/workbench.html +4011 -0
  51. package/package.json +11 -4
  52. package/scripts/build.cjs +24 -0
  53. package/scripts/check-architecture-baseline.cjs +68 -0
  54. package/scripts/test.cjs +38 -0
@@ -17,8 +17,20 @@ 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 { shouldDetachChildProcess, terminateProcessTree } from "./runtime-platform.js";
20
21
  import { resolveEngineRoute, } from "./engine-routing.js";
21
22
  export const LLM_ENGINES = new Set(["claude", "codex", "opencode", "gemini", "raw"]);
23
+ export function normalizeEngineName(value) {
24
+ const raw = String(value ?? "").trim();
25
+ const cleaned = raw.replace(/[\x00-\x1f\x7f]/g, "").trim();
26
+ if (!cleaned)
27
+ throw new Error("Invalid engine: must not be empty");
28
+ return cleaned;
29
+ }
30
+ function engineIdSegment(engine) {
31
+ const cleaned = engine.replace(/[^a-zA-Z0-9._:-]+/g, "_").replace(/^_+|_+$/g, "");
32
+ return cleaned || "custom";
33
+ }
22
34
  const defaultTaskRelay = {
23
35
  sendTaskStart,
24
36
  sendTaskStream,
@@ -30,12 +42,15 @@ const defaultTaskRelay = {
30
42
  export class EnginePeripheral {
31
43
  id;
32
44
  name;
45
+ engineName;
33
46
  capabilities = ["text-in", "text-out"];
34
47
  tags = ["engine", "llm"];
35
48
  config;
36
49
  bus = null;
37
50
  /** Last execution trace (for error reporting) */
38
51
  lastTrace = [];
52
+ /** Last selected invocation target, used by compute audit logging. */
53
+ lastInvocation = null;
39
54
  /** Active CLI child processes — tracked so SIGTERM handler can kill them. */
40
55
  activeChildren = new Set();
41
56
  /**
@@ -49,18 +64,17 @@ export class EnginePeripheral {
49
64
  for (const child of this.activeChildren) {
50
65
  if (!child.pid)
51
66
  continue;
52
- console.log(`[engine] shutdown: killing pgid=-${child.pid}`);
53
- try {
54
- process.kill(-child.pid, "SIGKILL");
55
- }
56
- catch { }
67
+ console.log(`[engine] shutdown: terminating process tree pid=${child.pid}`);
68
+ terminateProcessTree(child.pid, { signal: "SIGKILL" });
57
69
  }
58
70
  this.activeChildren.clear();
59
71
  }
60
72
  constructor(config) {
61
- this.config = config;
62
- this.id = `engine:${config.engine}`;
63
- this.name = config.engine === "raw" ? "Local Raw Engine" : `${config.engine} CLI`;
73
+ const engine = normalizeEngineName(config.engine);
74
+ this.config = { ...config, engine };
75
+ this.engineName = engine;
76
+ this.id = `engine:${engineIdSegment(engine)}`;
77
+ this.name = engine === "raw" ? "Local Raw Engine" : `${engine} CLI`;
64
78
  }
65
79
  get connected() {
66
80
  return true; // engines are always "available"
@@ -100,19 +114,27 @@ export class EnginePeripheral {
100
114
  // ---------------------------------------------------------------------------
101
115
  // Unified engine runner
102
116
  // ---------------------------------------------------------------------------
103
- async runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId, routeRequest) {
117
+ async runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId, routeRequest, engineResources) {
104
118
  const resolution = resolveEngineRoute(routing, { origin, ...routeRequest });
105
119
  const entry = resolution.entry;
106
- const cfg = entry ? applyRoutingEntry(this.config, entry) : this.config;
120
+ const cfg = entry ? applyRoutingEntry(this.config, entry, engineResources) : this.config;
121
+ this.lastInvocation = describeEngineInvocation(cfg, resolution.routeId);
107
122
  if (origin && entry) {
108
- console.log(`[engine] using ${cfg.engine}${cfg.model ? `/${cfg.model}` : ""} (origin=${origin}, source=${resolution.source})`);
123
+ const target = cfg.resourceId || cfg.engine;
124
+ console.log(`[engine] using ${target}${cfg.model ? `/${cfg.model}` : ""} (origin=${origin}, source=${resolution.source})`);
109
125
  }
110
126
  const t0 = Date.now();
111
127
  try {
112
128
  if (cfg.engine === "raw") {
113
129
  return await this.runRawEngine(task, cfg);
114
130
  }
115
- const cmd = buildEngineCommand(cfg.engine, cfg.model, allowAll ?? cfg.allowAll, extraAllowedTools);
131
+ const cmd = cfg.cliCommand
132
+ ? buildResourceCliCommand(cfg, allowAll ?? cfg.allowAll, extraAllowedTools)
133
+ : buildEngineCommand(cfg.engine, cfg.model, allowAll ?? cfg.allowAll, extraAllowedTools);
134
+ this.lastInvocation = {
135
+ ...this.lastInvocation,
136
+ command: [cmd.cmd, ...cmd.args].join(" "),
137
+ };
116
138
  return await runCommand(cmd.cmd, cmd.args, task, cfg.workdir, cmd.stdinMode, signal, this.activeChildren, origin, taskId, cfg.taskRelay ?? defaultTaskRelay, cfg.spawnImpl ?? spawn);
117
139
  }
118
140
  finally {
@@ -379,8 +401,38 @@ export const RAW_TOOLS = [
379
401
  * Resolves rawApiKeyEnv → rawApiKey from environment at call time.
380
402
  * Never mutates the base config.
381
403
  */
382
- function applyRoutingEntry(base, entry) {
383
- const override = { engine: entry.engine };
404
+ function applyRoutingEntry(base, entry, engineResources) {
405
+ const resource = entry.resource ? engineResources?.[entry.resource] : undefined;
406
+ const override = entry.engine ? { engine: entry.engine } : {};
407
+ if (resource) {
408
+ override.resourceId = entry.resource;
409
+ override.resourceKind = resource.kind;
410
+ if (resource.kind === "cli") {
411
+ override.engine = `cli:${entry.resource}`;
412
+ override.cliCommand = resource.command;
413
+ override.cliArgs = resource.args || [];
414
+ override.cliStdin = resource.stdin !== false;
415
+ override.cliModelArg = resource.modelArg;
416
+ if (resource.allowAll !== undefined)
417
+ override.allowAll = resource.allowAll;
418
+ override.model = entry.model ?? resource.defaultModel;
419
+ }
420
+ else {
421
+ override.engine = "raw";
422
+ override.provider = resource.provider;
423
+ override.protocol = resource.protocol || "openai_chat_completions";
424
+ override.rawApiUrl = resource.baseUrl;
425
+ if (resource.rawMaxRounds !== undefined)
426
+ override.rawMaxRounds = resource.rawMaxRounds;
427
+ override.model = entry.model ?? resource.defaultModel;
428
+ if (resource.apiKeyEnv)
429
+ override.rawApiKey = process.env[resource.apiKeyEnv] ?? "";
430
+ else if (resource.apiKey !== undefined)
431
+ override.rawApiKey = resource.apiKey;
432
+ else if (resource.credential)
433
+ override.rawApiKey = process.env[credentialEnvName(resource.credential)] ?? "";
434
+ }
435
+ }
384
436
  if (entry.model !== undefined)
385
437
  override.model = entry.model ?? undefined;
386
438
  if (entry.rawApiUrl !== undefined)
@@ -389,6 +441,10 @@ function applyRoutingEntry(base, entry) {
389
441
  override.rawMaxRounds = entry.rawMaxRounds;
390
442
  if (entry.allowAll !== undefined)
391
443
  override.allowAll = entry.allowAll;
444
+ if (entry.reasoningEffort !== undefined)
445
+ override.reasoningEffort = entry.reasoningEffort;
446
+ if (entry.verbosity !== undefined)
447
+ override.verbosity = entry.verbosity;
392
448
  // rawApiKeyEnv takes precedence over rawApiKey (env vars preferred for secrets)
393
449
  if (entry.rawApiKeyEnv)
394
450
  override.rawApiKey = process.env[entry.rawApiKeyEnv] ?? "";
@@ -396,6 +452,9 @@ function applyRoutingEntry(base, entry) {
396
452
  override.rawApiKey = entry.rawApiKey;
397
453
  return { ...base, ...override };
398
454
  }
455
+ function credentialEnvName(credential) {
456
+ return `${credential.trim().replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase()}_API_KEY`;
457
+ }
399
458
  function buildEngineCommand(engine, model, allowAll, extraAllowedTools) {
400
459
  switch (engine) {
401
460
  case "claude": {
@@ -428,6 +487,57 @@ function buildEngineCommand(engine, model, allowAll, extraAllowedTools) {
428
487
  return { cmd: engine, args: [], stdinMode: true };
429
488
  }
430
489
  }
490
+ function buildResourceCliCommand(cfg, _allowAll, _extraAllowedTools) {
491
+ const args = [...(cfg.cliArgs || [])];
492
+ if (cfg.model && cfg.cliModelArg)
493
+ args.push(cfg.cliModelArg, cfg.model);
494
+ return {
495
+ cmd: cfg.cliCommand || cfg.engine,
496
+ args,
497
+ stdinMode: cfg.cliStdin !== false,
498
+ };
499
+ }
500
+ function describeEngineInvocation(cfg, routeId) {
501
+ if (cfg.engine === "raw") {
502
+ const apiUrl = `${cfg.rawApiUrl || "http://localhost:11434/v1"}/chat/completions`;
503
+ return {
504
+ routeId,
505
+ resourceId: cfg.resourceId,
506
+ resourceKind: cfg.resourceKind,
507
+ provider: cfg.provider,
508
+ protocol: cfg.protocol || "openai_chat_completions",
509
+ engine: cfg.engine,
510
+ model: cfg.model,
511
+ reasoningEffort: cfg.reasoningEffort,
512
+ verbosity: cfg.verbosity,
513
+ command: `POST ${apiUrl}`,
514
+ };
515
+ }
516
+ if (cfg.cliCommand) {
517
+ const cmd = buildResourceCliCommand(cfg);
518
+ return {
519
+ routeId,
520
+ resourceId: cfg.resourceId,
521
+ resourceKind: cfg.resourceKind,
522
+ engine: cfg.engine,
523
+ model: cfg.model,
524
+ reasoningEffort: cfg.reasoningEffort,
525
+ verbosity: cfg.verbosity,
526
+ command: [cmd.cmd, ...cmd.args].join(" "),
527
+ };
528
+ }
529
+ const cmd = buildEngineCommand(cfg.engine, cfg.model, cfg.allowAll);
530
+ return {
531
+ routeId,
532
+ resourceId: cfg.resourceId,
533
+ resourceKind: cfg.resourceKind || "cli",
534
+ engine: cfg.engine,
535
+ model: cfg.model,
536
+ reasoningEffort: cfg.reasoningEffort,
537
+ verbosity: cfg.verbosity,
538
+ command: [cmd.cmd, ...cmd.args].join(" "),
539
+ };
540
+ }
431
541
  function runCommand(cmd, args, task, cwd, stdinMode = true, signal, activeChildren, origin, taskId, taskRelay = defaultTaskRelay, spawnImpl = spawn) {
432
542
  return new Promise((resolve, reject) => {
433
543
  const { CLAUDECODE, ...cleanEnv } = process.env;
@@ -450,7 +560,7 @@ function runCommand(cmd, args, task, cwd, stdinMode = true, signal, activeChildr
450
560
  cwd,
451
561
  env: cleanEnv,
452
562
  stdio: [stdinMode ? "pipe" : "ignore", "pipe", "pipe"],
453
- detached: true, // child becomes process-group leader; enables pgid kill
563
+ detached: shouldDetachChildProcess(),
454
564
  });
455
565
  }
456
566
  catch (err) {
@@ -470,18 +580,9 @@ function runCommand(cmd, args, task, cwd, stdinMode = true, signal, activeChildr
470
580
  if (aborted || !child.pid)
471
581
  return;
472
582
  aborted = true;
473
- console.log(`[${cmd}] aborted, killing pgid=-${child.pid}`);
583
+ console.log(`[${cmd}] aborted, terminating process tree pid=${child.pid}`);
474
584
  sendFailureEvent("engine_abort", cmd, "engine subprocess aborted via signal");
475
- try {
476
- process.kill(-child.pid, "SIGTERM");
477
- }
478
- catch { }
479
- setTimeout(() => {
480
- try {
481
- process.kill(-child.pid, "SIGKILL");
482
- }
483
- catch { }
484
- }, 3000).unref();
585
+ terminateProcessTree(child.pid, { signal: "SIGTERM", forceAfterMs: 3000 });
485
586
  };
486
587
  if (signal) {
487
588
  if (signal.aborted)
@@ -42,15 +42,20 @@ export function resolveEngineRoute(routing, request = {}) {
42
42
  }
43
43
  const route = selectRoute(routing.routes, request);
44
44
  if (route) {
45
+ const entry = stripRouteMetadata(route);
45
46
  return {
46
- entry: stripRouteMetadata(route),
47
+ entry,
48
+ routeId: route.id,
49
+ resourceId: entry.resource,
47
50
  source: "route",
48
51
  reason: "matched registry route",
49
52
  };
50
53
  }
51
54
  if (request.origin && routing[request.origin]) {
55
+ const entry = routing[request.origin];
52
56
  return {
53
- entry: routing[request.origin],
57
+ entry,
58
+ resourceId: entry.resource,
54
59
  source: "origin",
55
60
  reason: `matched legacy origin route ${request.origin}`,
56
61
  };
@@ -58,10 +63,19 @@ export function resolveEngineRoute(routing, request = {}) {
58
63
  if (routing.default) {
59
64
  return {
60
65
  entry: routing.default,
66
+ resourceId: routing.default.resource,
61
67
  source: "default",
62
68
  reason: "matched legacy default route",
63
69
  };
64
70
  }
71
+ if (routing.defaultResource) {
72
+ return {
73
+ entry: { resource: routing.defaultResource },
74
+ resourceId: routing.defaultResource,
75
+ source: "default",
76
+ reason: "matched default resource",
77
+ };
78
+ }
65
79
  return { entry: null, source: "none", reason: "no matching route" };
66
80
  }
67
81
  /**
@@ -103,15 +117,24 @@ function selectRoute(routes, request) {
103
117
  return best?.route ?? null;
104
118
  }
105
119
  function routeMatches(route, request) {
106
- if (request.origin && route.origins?.length && !route.origins.includes(request.origin))
120
+ const match = route.match;
121
+ const origins = match?.origins ?? route.origins;
122
+ const taskKinds = match?.taskKinds ?? route.taskKinds;
123
+ const capabilities = match?.requiredCapabilities ?? route.capabilities;
124
+ const privacy = match?.privacy ?? route.privacy;
125
+ const maxCost = match?.maxCost ?? route.cost;
126
+ const maxLatency = match?.maxLatency ?? route.latency;
127
+ if (request.origin && origins?.length && !origins.includes(request.origin))
128
+ return false;
129
+ if (taskKinds?.length && (!request.taskKind || !taskKinds.includes(request.taskKind)))
107
130
  return false;
108
- if (!hasRequiredCapabilities(route.capabilities, request.requiredCapabilities))
131
+ if (!hasRequiredCapabilities(capabilities, request.requiredCapabilities))
109
132
  return false;
110
- if (request.privacy && route.privacy && route.privacy !== request.privacy)
133
+ if (request.privacy && privacy && privacy !== request.privacy)
111
134
  return false;
112
- if (request.maxCost && route.cost && tierRank(route.cost) > tierRank(request.maxCost))
135
+ if (request.maxCost && maxCost && tierRank(maxCost) > tierRank(request.maxCost))
113
136
  return false;
114
- if (request.maxLatency && route.latency && tierRank(route.latency) > tierRank(request.maxLatency))
137
+ if (request.maxLatency && maxLatency && tierRank(maxLatency) > tierRank(request.maxLatency))
115
138
  return false;
116
139
  return true;
117
140
  }
@@ -124,23 +147,41 @@ function hasRequiredCapabilities(available, required) {
124
147
  }
125
148
  function scoreRoute(route, request) {
126
149
  let score = route.priority ?? 0;
127
- if (request.origin && route.origins?.includes(request.origin))
150
+ const match = route.match;
151
+ const origins = match?.origins ?? route.origins;
152
+ const taskKinds = match?.taskKinds ?? route.taskKinds;
153
+ const capabilities = match?.requiredCapabilities ?? route.capabilities;
154
+ const privacy = match?.privacy ?? route.privacy;
155
+ const cost = match?.maxCost ?? route.cost;
156
+ const latency = match?.maxLatency ?? route.latency;
157
+ if (request.taskKind && taskKinds?.includes(request.taskKind))
158
+ score += 120;
159
+ if (request.origin && origins?.includes(request.origin))
128
160
  score += 100;
129
- if (request.origin && !route.origins?.length)
161
+ if (request.origin && !origins?.length)
130
162
  score += 10;
131
163
  if (request.requiredCapabilities?.length)
132
- score += (route.capabilities?.length || 0) * 2;
133
- if (request.privacy && route.privacy === request.privacy)
164
+ score += (capabilities?.length || 0) * 2;
165
+ if (request.privacy && privacy === request.privacy)
134
166
  score += 20;
135
- if (route.cost)
136
- score += 6 - tierRank(route.cost);
137
- if (route.latency)
138
- score += 6 - tierRank(route.latency);
167
+ if (cost)
168
+ score += 6 - tierRank(cost);
169
+ if (latency)
170
+ score += 6 - tierRank(latency);
139
171
  return score;
140
172
  }
141
173
  function stripRouteMetadata(route) {
142
- const { origins: _origins, priority: _priority, ...entry } = route;
143
- return entry;
174
+ const { id, origins: _origins, taskKinds: _taskKinds, priority: _priority, match: _match, use, ...entry } = route;
175
+ return {
176
+ ...entry,
177
+ ...(id ? { routeId: id } : {}),
178
+ ...(use?.resource ? { resource: use.resource } : {}),
179
+ ...(use?.model !== undefined ? { model: use.model } : {}),
180
+ ...(use?.allowAll !== undefined ? { allowAll: use.allowAll } : {}),
181
+ ...(use?.rawMaxRounds !== undefined ? { rawMaxRounds: use.rawMaxRounds } : {}),
182
+ ...(use?.reasoningEffort !== undefined ? { reasoningEffort: use.reasoningEffort } : {}),
183
+ ...(use?.verbosity !== undefined ? { verbosity: use.verbosity } : {}),
184
+ };
144
185
  }
145
186
  function tierRank(tier) {
146
187
  switch (tier) {