@voybio/ace-swarm 2.4.0 → 2.4.2

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 (80) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +502 -56
  3. package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
  4. package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
  5. package/assets/agent-state/runtime-tool-specs.json +70 -2
  6. package/assets/instructions/ACE_Coder.instructions.md +13 -0
  7. package/assets/instructions/ACE_UI.instructions.md +11 -0
  8. package/dist/ace-context.js +70 -11
  9. package/dist/ace-internal-tools.d.ts +3 -1
  10. package/dist/ace-internal-tools.js +10 -2
  11. package/dist/agent-runtime/role-adapters.d.ts +18 -1
  12. package/dist/agent-runtime/role-adapters.js +49 -5
  13. package/dist/astgrep-index.d.ts +48 -0
  14. package/dist/astgrep-index.js +126 -1
  15. package/dist/cli.js +487 -17
  16. package/dist/discovery-runtime-wrappers.d.ts +108 -0
  17. package/dist/discovery-runtime-wrappers.js +615 -0
  18. package/dist/helpers/bootstrap.js +1 -1
  19. package/dist/helpers/constants.d.ts +4 -2
  20. package/dist/helpers/constants.js +8 -0
  21. package/dist/helpers/path-utils.d.ts +8 -1
  22. package/dist/helpers/path-utils.js +27 -8
  23. package/dist/helpers/store-resolution.js +7 -3
  24. package/dist/hermes/bridge-protocol.d.ts +41 -0
  25. package/dist/hermes/bridge-protocol.js +70 -0
  26. package/dist/hermes/launch-profile.d.ts +19 -0
  27. package/dist/hermes/launch-profile.js +81 -0
  28. package/dist/hermes/session-manager.d.ts +42 -0
  29. package/dist/hermes/session-manager.js +187 -0
  30. package/dist/job-scheduler.js +30 -4
  31. package/dist/json-sanitizer.d.ts +16 -0
  32. package/dist/json-sanitizer.js +26 -0
  33. package/dist/local-model-policy.d.ts +27 -0
  34. package/dist/local-model-policy.js +84 -0
  35. package/dist/local-model-runtime.d.ts +17 -0
  36. package/dist/local-model-runtime.js +77 -20
  37. package/dist/model-bridge.d.ts +6 -1
  38. package/dist/model-bridge.js +338 -21
  39. package/dist/orchestrator-supervisor.d.ts +42 -0
  40. package/dist/orchestrator-supervisor.js +110 -3
  41. package/dist/plan-proposal.d.ts +115 -0
  42. package/dist/plan-proposal.js +1073 -0
  43. package/dist/runtime-executor.d.ts +6 -1
  44. package/dist/runtime-executor.js +72 -5
  45. package/dist/runtime-tool-specs.d.ts +19 -1
  46. package/dist/runtime-tool-specs.js +67 -26
  47. package/dist/schemas.js +30 -1
  48. package/dist/server.d.ts +3 -0
  49. package/dist/server.js +73 -4
  50. package/dist/shared.d.ts +1 -0
  51. package/dist/shared.js +2 -0
  52. package/dist/store/bootstrap-store.d.ts +1 -0
  53. package/dist/store/bootstrap-store.js +8 -2
  54. package/dist/store/materializers/vericify-projector.js +3 -0
  55. package/dist/store/repositories/local-model-runtime-repository.d.ts +13 -1
  56. package/dist/store/repositories/local-model-runtime-repository.js +4 -1
  57. package/dist/store/repositories/vericify-repository.d.ts +1 -1
  58. package/dist/tools-agent.d.ts +20 -0
  59. package/dist/tools-agent.js +544 -29
  60. package/dist/tools-discovery.js +135 -0
  61. package/dist/tools-files.js +768 -66
  62. package/dist/tools-framework.js +80 -61
  63. package/dist/tools.d.ts +4 -1
  64. package/dist/tools.js +35 -13
  65. package/dist/tui/chat.d.ts +8 -0
  66. package/dist/tui/chat.js +74 -0
  67. package/dist/tui/index.d.ts +7 -0
  68. package/dist/tui/index.js +45 -2
  69. package/dist/tui/layout.d.ts +1 -0
  70. package/dist/tui/layout.js +4 -1
  71. package/dist/tui/ollama.d.ts +8 -1
  72. package/dist/tui/ollama.js +53 -12
  73. package/dist/tui/openai-compatible.d.ts +13 -0
  74. package/dist/tui/openai-compatible.js +305 -5
  75. package/dist/tui/provider-discovery.d.ts +1 -0
  76. package/dist/tui/provider-discovery.js +50 -24
  77. package/dist/vericify-bridge.d.ts +4 -1
  78. package/dist/vericify-bridge.js +3 -0
  79. package/package.json +2 -1
  80. package/scripts/hermes_bridge_worker.py +136 -0
@@ -53,6 +53,7 @@ export const ALL_LLM_PROVIDERS = [
53
53
  "gemini",
54
54
  "copilot",
55
55
  ];
56
+ export const ALL_EXECUTION_ENGINES = ["direct", "hermes_local"];
56
57
  export const ALL_AGENTS = [
57
58
  "orchestrator",
58
59
  "vos",
@@ -71,6 +72,7 @@ export const ALL_AGENTS = [
71
72
  "observability",
72
73
  "eval",
73
74
  "release",
75
+ "planner",
74
76
  ];
75
77
  export const SWARM_AGENTS = ["orchestrator", "vos", "ui", "coders"];
76
78
  export const COMPOSABLE_AGENTS = [
@@ -87,6 +89,7 @@ export const COMPOSABLE_AGENTS = [
87
89
  "observability",
88
90
  "eval",
89
91
  "release",
92
+ "planner",
90
93
  ];
91
94
  export const SWARM_SUBAGENT_MAP = {
92
95
  orchestrator: COMPOSABLE_AGENTS,
@@ -145,6 +148,7 @@ export const AGENT_FILES = {
145
148
  ],
146
149
  eval: [".agents/ACE/agent-eval/instructions.md", ".agents/ACE/agent-eval/AGENTS.md"],
147
150
  release: [".agents/ACE/agent-release/instructions.md", ".agents/ACE/agent-release/AGENTS.md"],
151
+ planner: [".agents/ACE/agent-planner/instructions.md", ".agents/ACE/agent-planner/AGENTS.md"],
148
152
  };
149
153
  export const AGENT_MANIFEST_FILES = {
150
154
  orchestrator: [
@@ -167,6 +171,7 @@ export const AGENT_MANIFEST_FILES = {
167
171
  observability: [".agents/ACE/agent-observability/AGENTS.md"],
168
172
  eval: [".agents/ACE/agent-eval/AGENTS.md"],
169
173
  release: [".agents/ACE/agent-release/AGENTS.md"],
174
+ planner: [".agents/ACE/agent-planner/AGENTS.md"],
170
175
  };
171
176
  export const TASK_FILES = {
172
177
  todo: [`${ACE_TASKS_ROOT_REL}/todo.md`],
@@ -208,6 +213,7 @@ export const DEFAULT_AGENT_INSTRUCTION_FILES = {
208
213
  observability: resolve(DEFAULTS_ROOT, ".agents", "ACE", "agent-observability", "instructions.md"),
209
214
  eval: resolve(DEFAULTS_ROOT, ".agents", "ACE", "agent-eval", "instructions.md"),
210
215
  release: resolve(DEFAULTS_ROOT, ".agents", "ACE", "agent-release", "instructions.md"),
216
+ planner: resolve(DEFAULTS_ROOT, ".agents", "ACE", "agent-planner", "instructions.md"),
211
217
  };
212
218
  export const DEFAULT_AGENT_MANIFEST_FILES = {
213
219
  orchestrator: resolve(DEFAULTS_ROOT, ".agents", "ACE", "orchestrator", "AGENTS.md"),
@@ -227,6 +233,7 @@ export const DEFAULT_AGENT_MANIFEST_FILES = {
227
233
  observability: resolve(DEFAULTS_ROOT, ".agents", "ACE", "agent-observability", "AGENTS.md"),
228
234
  eval: resolve(DEFAULTS_ROOT, ".agents", "ACE", "agent-eval", "AGENTS.md"),
229
235
  release: resolve(DEFAULTS_ROOT, ".agents", "ACE", "agent-release", "AGENTS.md"),
236
+ planner: resolve(DEFAULTS_ROOT, ".agents", "ACE", "agent-planner", "AGENTS.md"),
230
237
  };
231
238
  export const DEFAULT_TASK_FILES = {
232
239
  todo: resolve(DEFAULTS_TASKS_ROOT, "todo.md"),
@@ -269,6 +276,7 @@ export const STORE_AGENT_FILES = {
269
276
  },
270
277
  eval: { agent: "eval", instructions: ["instructions.md", "AGENTS.md"], manifests: ["AGENTS.md"] },
271
278
  release: { agent: "release", instructions: ["instructions.md", "AGENTS.md"], manifests: ["AGENTS.md"] },
279
+ planner: { agent: "planner", instructions: ["instructions.md", "AGENTS.md"], manifests: ["AGENTS.md"] },
272
280
  };
273
281
  export const STORE_TASK_FILES = {
274
282
  todo: "knowledge/tasks/todo.md",
@@ -8,7 +8,14 @@ export declare function normalizePathForValidation(path: string): string;
8
8
  export declare function isSwarmRole(role: string): role is (typeof SWARM_AGENTS)[number];
9
9
  export declare function atomicWrite(filePath: string, content: string): string;
10
10
  export declare function fileExists(filePath: string): boolean;
11
- export declare function resolveWorkspaceWritePath(filePath: string): string;
11
+ export interface WorkspacePathError extends Error {
12
+ reason_code: "path_escape" | "missing_workspace_path";
13
+ payload: {
14
+ reason_code: "path_escape" | "missing_workspace_path";
15
+ message: string;
16
+ };
17
+ }
18
+ export declare function resolveWorkspaceWritePath(filePath: string, workspaceRoot?: string): string;
12
19
  export declare function resolveWorkspaceReadPath(filePath: string): string;
13
20
  export declare function resolveWorkspaceArtifactPath(filePath: string, mode?: "read" | "write"): string;
14
21
  export declare function toAbsoluteWorkspaceCandidates(candidates: string[]): string[];
@@ -54,18 +54,37 @@ export function fileExists(filePath) {
54
54
  const abs = isAbsolute(filePath) ? filePath : resolveWorkspaceReadPath(filePath);
55
55
  return existsSync(abs);
56
56
  }
57
- export function resolveWorkspaceWritePath(filePath) {
58
- const root = currentWorkspaceRoot();
57
+ function workspacePathError(reasonCode, message) {
58
+ const error = new Error(message);
59
+ error.reason_code = reasonCode;
60
+ error.payload = {
61
+ reason_code: reasonCode,
62
+ message,
63
+ };
64
+ return error;
65
+ }
66
+ function looksLikeWindowsAbsolutePath(filePath) {
67
+ return /^[a-zA-Z]:[\\/]/.test(filePath) || /^\\\\/.test(filePath);
68
+ }
69
+ export function resolveWorkspaceWritePath(filePath, workspaceRoot = currentWorkspaceRoot()) {
70
+ const root = resolve(workspaceRoot);
59
71
  if (dirname(root) === root) {
60
72
  throw new Error("Workspace root resolved to filesystem root. Set ACE_WORKSPACE_ROOT or run ACE from the target workspace.");
61
73
  }
62
- const abs = isAbsolute(filePath)
63
- ? filePath
64
- : resolve(root, mapAceWorkspaceRelativePath(filePath));
65
- if (!isInside(root, abs)) {
66
- throw new Error(`Path escapes workspace root: ${filePath}`);
74
+ const trimmed = filePath.trim();
75
+ if (!trimmed) {
76
+ throw workspacePathError("path_escape", "Workspace write path is empty.");
67
77
  }
68
- return abs;
78
+ if (trimmed.startsWith("~") || isAbsolute(trimmed) || looksLikeWindowsAbsolutePath(trimmed)) {
79
+ throw workspacePathError("path_escape", `Workspace write path must be relative to workspace root: ${filePath}`);
80
+ }
81
+ const normalizedPath = mapAceWorkspaceRelativePath(normalizeRelPath(trimmed));
82
+ const target = resolve(root, normalizedPath);
83
+ const rel = relative(root, target);
84
+ if (rel.startsWith("..") || isAbsolute(rel) || !isInside(root, target)) {
85
+ throw workspacePathError("path_escape", `Path escapes workspace root: ${filePath}`);
86
+ }
87
+ return target;
69
88
  }
70
89
  export function resolveWorkspaceReadPath(filePath) {
71
90
  const root = currentWorkspaceRoot();
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
2
2
  import { mkdir, rename, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
3
+ import { dirname, isAbsolute } from "node:path";
4
4
  import { isInside, normalizeRelPath } from "../shared.js";
5
5
  import { getWorkspaceStorePath, listStoreKeysSync, parseVirtualStorePath, readStoreBlobSync, readVirtualStorePathSync, toVirtualStorePath, } from "../store/store-snapshot.js";
6
6
  import { isOperationalArtifactPath, operationalArtifactKey, writeOperationalArtifactQueued, writeStoreBlobQueued, writeStoreBlobSync, } from "../store/store-artifacts.js";
@@ -234,7 +234,9 @@ export function safeWrite(filePath, content) {
234
234
  return storeVirtualPath;
235
235
  }
236
236
  }
237
- const abs = resolveWorkspaceWritePath(filePath);
237
+ const abs = isAbsolute(filePath) && isInside(root, filePath)
238
+ ? filePath
239
+ : resolveWorkspaceWritePath(filePath, root);
238
240
  mkdirSync(dirname(abs), { recursive: true });
239
241
  const tmpPath = `${abs}.${process.pid}.${Date.now()}.tmp`;
240
242
  writeFileSync(tmpPath, content, "utf-8");
@@ -271,7 +273,9 @@ export async function safeWriteAsync(filePath, content) {
271
273
  return storeVirtualPath;
272
274
  }
273
275
  }
274
- const abs = resolveWorkspaceWritePath(filePath);
276
+ const abs = isAbsolute(filePath) && isInside(root, filePath)
277
+ ? filePath
278
+ : resolveWorkspaceWritePath(filePath, root);
275
279
  await mkdir(dirname(abs), { recursive: true });
276
280
  const tmpPath = `${abs}.${process.pid}.${Date.now()}.tmp`;
277
281
  await writeFile(tmpPath, content, "utf-8");
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ export declare const HERMES_BRIDGE_PROTOCOL_VERSION = "1.0.0";
3
+ declare const bridgeEventSchema: z.ZodObject<{
4
+ type: z.ZodEnum<{
5
+ error: "error";
6
+ status: "status";
7
+ tool_start: "tool_start";
8
+ session_open: "session_open";
9
+ turn_run: "turn_run";
10
+ interrupt: "interrupt";
11
+ session_close: "session_close";
12
+ delta: "delta";
13
+ reasoning: "reasoning";
14
+ assistant_interim: "assistant_interim";
15
+ tool_progress: "tool_progress";
16
+ tool_complete: "tool_complete";
17
+ final: "final";
18
+ }>;
19
+ session_id: z.ZodOptional<z.ZodString>;
20
+ turn_id: z.ZodOptional<z.ZodString>;
21
+ sequence: z.ZodOptional<z.ZodNumber>;
22
+ }, z.core.$loose>;
23
+ export type HermesBridgeEvent = z.infer<typeof bridgeEventSchema>;
24
+ export interface DecodedHermesBridgeFrame {
25
+ ok: true;
26
+ event: HermesBridgeEvent;
27
+ }
28
+ export interface RejectedHermesBridgeFrame {
29
+ ok: false;
30
+ reason: string;
31
+ raw: string;
32
+ }
33
+ export type HermesBridgeFrame = DecodedHermesBridgeFrame | RejectedHermesBridgeFrame;
34
+ export declare class HermesBridgeFrameDecoder {
35
+ private decoder;
36
+ private buffer;
37
+ push(chunk: Uint8Array, end?: boolean): HermesBridgeFrame[];
38
+ }
39
+ export declare function decodeHermesBridgeLine(line: string): HermesBridgeFrame;
40
+ export {};
41
+ //# sourceMappingURL=bridge-protocol.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { TextDecoder } from "node:util";
2
+ import { z } from "zod";
3
+ export const HERMES_BRIDGE_PROTOCOL_VERSION = "1.0.0";
4
+ const bridgeEventBaseSchema = z.object({
5
+ type: z.enum([
6
+ "session_open",
7
+ "turn_run",
8
+ "interrupt",
9
+ "session_close",
10
+ "status",
11
+ "delta",
12
+ "reasoning",
13
+ "assistant_interim",
14
+ "tool_start",
15
+ "tool_progress",
16
+ "tool_complete",
17
+ "final",
18
+ "error",
19
+ ]),
20
+ session_id: z.string().optional(),
21
+ turn_id: z.string().optional(),
22
+ sequence: z.number().int().nonnegative().optional(),
23
+ });
24
+ const bridgeEventSchema = bridgeEventBaseSchema.passthrough();
25
+ function sanitizeBridgeLine(line) {
26
+ if (line.includes("\u0000"))
27
+ return undefined;
28
+ const stripped = line.replace(/[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
29
+ const trimmed = stripped.trim();
30
+ return trimmed ? trimmed : undefined;
31
+ }
32
+ export class HermesBridgeFrameDecoder {
33
+ decoder = new TextDecoder("utf-8", { fatal: false });
34
+ buffer = "";
35
+ push(chunk, end = false) {
36
+ this.buffer += this.decoder.decode(chunk, { stream: !end });
37
+ const lines = this.buffer.split(/\r?\n/);
38
+ this.buffer = end ? "" : (lines.pop() ?? "");
39
+ const frames = [];
40
+ for (const line of lines) {
41
+ if (!line.trim())
42
+ continue;
43
+ frames.push(decodeHermesBridgeLine(line));
44
+ }
45
+ if (end && this.buffer.trim()) {
46
+ frames.push(decodeHermesBridgeLine(this.buffer));
47
+ this.buffer = "";
48
+ }
49
+ return frames;
50
+ }
51
+ }
52
+ export function decodeHermesBridgeLine(line) {
53
+ const sanitized = sanitizeBridgeLine(line);
54
+ if (!sanitized) {
55
+ return { ok: false, reason: "bridge_frame_control_bytes", raw: line };
56
+ }
57
+ try {
58
+ const parsed = JSON.parse(sanitized);
59
+ const event = bridgeEventSchema.parse(parsed);
60
+ return { ok: true, event };
61
+ }
62
+ catch (error) {
63
+ return {
64
+ ok: false,
65
+ reason: error instanceof Error ? error.message : "bridge_frame_invalid_json",
66
+ raw: sanitized,
67
+ };
68
+ }
69
+ }
70
+ //# sourceMappingURL=bridge-protocol.js.map
@@ -0,0 +1,19 @@
1
+ export interface HermesLaunchProfile {
2
+ enabled: boolean;
3
+ mode: "ephemeral_subprocess" | "external_command";
4
+ hermes_root?: string;
5
+ command: readonly string[];
6
+ worker_path: string;
7
+ source: "cli" | "env" | "store" | "default";
8
+ diagnostics: string[];
9
+ }
10
+ export interface ResolveHermesLaunchProfileInput {
11
+ workspaceRoot: string;
12
+ cliHermesRoot?: string;
13
+ cliHermesPython?: string;
14
+ cliHermesUvProject?: string;
15
+ cliHermesCommandJson?: string;
16
+ env?: NodeJS.ProcessEnv;
17
+ }
18
+ export declare function resolveHermesLaunchProfile(input: ResolveHermesLaunchProfileInput): HermesLaunchProfile;
19
+ //# sourceMappingURL=launch-profile.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readStoreJsonSync } from "../store/store-snapshot.js";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const DIST_ROOT = resolve(__dirname, "..");
8
+ const PACKAGE_ROOT = resolve(DIST_ROOT, "..");
9
+ function parseCommandJson(raw, source) {
10
+ if (!raw?.trim())
11
+ return undefined;
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(raw);
15
+ }
16
+ catch (error) {
17
+ throw new Error(`${source} must be a JSON array of command tokens.`);
18
+ }
19
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((item) => typeof item !== "string" || !item.trim())) {
20
+ throw new Error(`${source} must be a non-empty JSON array of non-empty strings.`);
21
+ }
22
+ return parsed.map((item) => item.trim());
23
+ }
24
+ function defaultHermesRoot() {
25
+ const candidate = resolve(PACKAGE_ROOT, "..", ".tmp", "hermes-agent");
26
+ return existsSync(candidate) ? candidate : undefined;
27
+ }
28
+ export function resolveHermesLaunchProfile(input) {
29
+ const env = input.env ?? process.env;
30
+ const diagnostics = [];
31
+ const workerPath = resolve(PACKAGE_ROOT, "scripts", "hermes_bridge_worker.py");
32
+ const stored = readStoreJsonSync(input.workspaceRoot, "state/runtime/hermes_launch_profile") ??
33
+ undefined;
34
+ const hermesRoot = input.cliHermesRoot ?? env.ACE_HERMES_ROOT ?? stored?.hermes_root ?? defaultHermesRoot();
35
+ const commandJson = input.cliHermesCommandJson ?? env.ACE_HERMES_COMMAND_JSON ?? stored?.hermes_command_json;
36
+ const uvProject = input.cliHermesUvProject ?? env.ACE_HERMES_UV_PROJECT ?? stored?.hermes_uv_project;
37
+ const python = input.cliHermesPython ?? env.HERMES_PYTHON ?? stored?.hermes_python;
38
+ let command;
39
+ let source = "default";
40
+ if (commandJson) {
41
+ command = parseCommandJson(commandJson, input.cliHermesCommandJson ? "--hermes-command-json" : "ACE_HERMES_COMMAND_JSON");
42
+ source = input.cliHermesCommandJson ? "cli" : "env";
43
+ if (!input.cliHermesCommandJson && !env.ACE_HERMES_COMMAND_JSON) {
44
+ source = "store";
45
+ }
46
+ }
47
+ else if (uvProject) {
48
+ command = ["uv", "run", "--project", uvProject, "python"];
49
+ source = input.cliHermesUvProject ? "cli" : "env";
50
+ if (!input.cliHermesUvProject && !env.ACE_HERMES_UV_PROJECT) {
51
+ source = "store";
52
+ }
53
+ }
54
+ else if (python) {
55
+ command = [python];
56
+ source = input.cliHermesPython ? "cli" : "env";
57
+ if (!input.cliHermesPython && !env.HERMES_PYTHON) {
58
+ source = "store";
59
+ }
60
+ }
61
+ else {
62
+ command = ["python3"];
63
+ diagnostics.push("HERMES_PYTHON not set; using python3 fallback.");
64
+ }
65
+ if (!hermesRoot) {
66
+ diagnostics.push("ACE_HERMES_ROOT not set and no default Hermes checkout was found.");
67
+ }
68
+ if (!existsSync(workerPath)) {
69
+ diagnostics.push(`Hermes bridge worker is missing at ${workerPath}.`);
70
+ }
71
+ return {
72
+ enabled: true,
73
+ mode: commandJson ? "external_command" : "ephemeral_subprocess",
74
+ hermes_root: hermesRoot,
75
+ command,
76
+ worker_path: workerPath,
77
+ source,
78
+ diagnostics,
79
+ };
80
+ }
81
+ //# sourceMappingURL=launch-profile.js.map
@@ -0,0 +1,42 @@
1
+ import type { BridgeResult } from "../model-bridge.js";
2
+ import type { LocalModelRuntimeConfig } from "../local-model-runtime.js";
3
+ import type { LocalModelExecutionPolicy } from "../local-model-policy.js";
4
+ import { type HermesBridgeEvent } from "./bridge-protocol.js";
5
+ import { type HermesLaunchProfile } from "./launch-profile.js";
6
+ export interface HermesLocalTurnOptions {
7
+ task: string;
8
+ role: string;
9
+ runtime: LocalModelRuntimeConfig;
10
+ policy: LocalModelExecutionPolicy;
11
+ toolScope?: string[];
12
+ maxTurns: number;
13
+ }
14
+ export interface HermesLocalTurnResult {
15
+ result: BridgeResult;
16
+ metadata: {
17
+ bridge_protocol_version: string;
18
+ hermes_session_id: string;
19
+ shadow_mcp_session_id: string;
20
+ execution_engine: "hermes_local";
21
+ provider: string;
22
+ model: string;
23
+ events: HermesBridgeEvent[];
24
+ };
25
+ }
26
+ export interface HermesLocalExecutor {
27
+ runTurn(options: HermesLocalTurnOptions): Promise<HermesLocalTurnResult>;
28
+ }
29
+ export interface HermesSubprocessExecutorOptions {
30
+ hermesRoot?: string;
31
+ workerPath?: string;
32
+ pythonCommand?: string;
33
+ command?: readonly string[];
34
+ launchProfile?: HermesLaunchProfile;
35
+ keepTemp?: boolean;
36
+ }
37
+ export declare class HermesSubprocessExecutor implements HermesLocalExecutor {
38
+ private options;
39
+ constructor(options?: HermesSubprocessExecutorOptions);
40
+ runTurn(options: HermesLocalTurnOptions): Promise<HermesLocalTurnResult>;
41
+ }
42
+ //# sourceMappingURL=session-manager.d.ts.map
@@ -0,0 +1,187 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawn } from "node:child_process";
7
+ import { HERMES_BRIDGE_PROTOCOL_VERSION, HermesBridgeFrameDecoder, } from "./bridge-protocol.js";
8
+ import { resolveHermesLaunchProfile, } from "./launch-profile.js";
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const DIST_ROOT = resolve(__dirname, "..");
12
+ const PACKAGE_ROOT = resolve(DIST_ROOT, "..");
13
+ const MIN_HERMES_LOCAL_CONTEXT_LENGTH = 65536;
14
+ function summarizeToolResult(value) {
15
+ if (typeof value === "string")
16
+ return value.slice(0, 500);
17
+ try {
18
+ return JSON.stringify(value).slice(0, 500);
19
+ }
20
+ catch {
21
+ return String(value).slice(0, 500);
22
+ }
23
+ }
24
+ function eventText(event) {
25
+ const value = event.text ?? event.message;
26
+ return typeof value === "string" ? value : "";
27
+ }
28
+ function hermesBaseUrl(provider, baseUrl) {
29
+ const normalized = String(baseUrl ?? "").replace(/\/+$/, "");
30
+ if (provider === "ollama" && normalized && !normalized.endsWith("/v1")) {
31
+ return `${normalized}/v1`;
32
+ }
33
+ return normalized;
34
+ }
35
+ export class HermesSubprocessExecutor {
36
+ options;
37
+ constructor(options = {}) {
38
+ this.options = options;
39
+ }
40
+ async runTurn(options) {
41
+ const sessionId = `hermes-${randomUUID()}`;
42
+ const shadowId = `shadow-${randomUUID()}`;
43
+ const hermesHome = mkdtempSync(resolve(tmpdir(), "ace-hermes-home-"));
44
+ mkdirSync(hermesHome, { recursive: true });
45
+ const toolScope = options.toolScope ?? [];
46
+ const cliPath = resolve(DIST_ROOT, "cli.js");
47
+ const baseUrl = hermesBaseUrl(options.runtime.provider, options.runtime.baseUrl ?? options.runtime.ollamaUrl);
48
+ writeFileSync(resolve(hermesHome, "config.yaml"), [
49
+ "model:",
50
+ ` default: ${JSON.stringify(options.runtime.model)}`,
51
+ " provider: custom",
52
+ ` base_url: ${JSON.stringify(baseUrl)}`,
53
+ " api_key: no-key-required",
54
+ ` context_length: ${MIN_HERMES_LOCAL_CONTEXT_LENGTH}`,
55
+ "",
56
+ "auxiliary:",
57
+ " compression:",
58
+ ` model: ${JSON.stringify(options.runtime.model)}`,
59
+ " provider: custom",
60
+ ` base_url: ${JSON.stringify(baseUrl)}`,
61
+ " api_key: no-key-required",
62
+ ` context_length: ${MIN_HERMES_LOCAL_CONTEXT_LENGTH}`,
63
+ "",
64
+ "mcp_servers:",
65
+ " ace_shadow:",
66
+ ` command: ${JSON.stringify(process.execPath)}`,
67
+ " args:",
68
+ ` - ${JSON.stringify(cliPath)}`,
69
+ " - mcp-shadow",
70
+ ` - ${JSON.stringify("--tools")}`,
71
+ ` - ${JSON.stringify(toolScope.join(","))}`,
72
+ " timeout: 30",
73
+ "",
74
+ ].join("\n"), "utf-8");
75
+ const launchProfile = this.options.launchProfile ??
76
+ resolveHermesLaunchProfile({
77
+ workspaceRoot: options.runtime.workspaceRoot,
78
+ cliHermesRoot: this.options.hermesRoot,
79
+ cliHermesPython: this.options.pythonCommand,
80
+ });
81
+ const workerPath = this.options.workerPath ?? launchProfile.worker_path;
82
+ const hermesRoot = this.options.hermesRoot ?? launchProfile.hermes_root;
83
+ const command = this.options.command ?? launchProfile.command;
84
+ const [bin, ...args] = command;
85
+ if (!bin) {
86
+ throw new Error("Hermes launch command must contain at least one executable argument.");
87
+ }
88
+ const child = spawn(bin, [...args, workerPath], {
89
+ cwd: PACKAGE_ROOT,
90
+ env: {
91
+ ...process.env,
92
+ HERMES_HOME: hermesHome,
93
+ ACE_WORKSPACE_ROOT: options.runtime.workspaceRoot,
94
+ },
95
+ stdio: ["pipe", "pipe", "pipe"],
96
+ });
97
+ const decoder = new HermesBridgeFrameDecoder();
98
+ const events = [];
99
+ const malformed = [];
100
+ const stderr = [];
101
+ let finalText = "";
102
+ let turns = 0;
103
+ let sawError = "";
104
+ child.stdout.on("data", (chunk) => {
105
+ for (const frame of decoder.push(chunk)) {
106
+ if (frame.ok) {
107
+ events.push(frame.event);
108
+ if (frame.event.type === "final") {
109
+ finalText = eventText(frame.event);
110
+ const rawTurns = frame.event.turns;
111
+ turns = typeof rawTurns === "number" ? rawTurns : turns;
112
+ }
113
+ if (frame.event.type === "error") {
114
+ sawError = eventText(frame.event) || "Hermes bridge error";
115
+ }
116
+ }
117
+ else {
118
+ malformed.push(frame.reason);
119
+ }
120
+ }
121
+ });
122
+ child.stderr.on("data", (chunk) => {
123
+ stderr.push(chunk.toString("utf8"));
124
+ });
125
+ const request = {
126
+ session_id: sessionId,
127
+ turn_id: randomUUID(),
128
+ hermes_root: hermesRoot ?? "",
129
+ task: options.task,
130
+ role: options.role,
131
+ provider: options.runtime.provider,
132
+ model: options.runtime.model,
133
+ base_url: options.runtime.baseUrl ?? options.runtime.ollamaUrl,
134
+ max_turns: options.maxTurns,
135
+ system_prompt: "You are running inside ACE Hermes-local mode. Use only the imported ACE MCP toolset for tool calls. Do not use plain text as executable authority.",
136
+ };
137
+ child.stdin.end(`${JSON.stringify(request)}\n`);
138
+ const exitCode = await new Promise((resolvePromise, reject) => {
139
+ child.on("error", reject);
140
+ child.on("close", resolvePromise);
141
+ });
142
+ for (const frame of decoder.push(new Uint8Array(), true)) {
143
+ if (frame.ok)
144
+ events.push(frame.event);
145
+ else
146
+ malformed.push(frame.reason);
147
+ }
148
+ if (!this.options.keepTemp) {
149
+ rmSync(hermesHome, { recursive: true, force: true });
150
+ }
151
+ const toolCalls = events
152
+ .filter((event) => event.type === "tool_complete")
153
+ .map((event) => ({
154
+ tool: String(event.tool ?? "unknown"),
155
+ ok: !event.is_error,
156
+ summary: summarizeToolResult(event.result ?? eventText(event)),
157
+ isError: Boolean(event.is_error),
158
+ }));
159
+ const status = sawError || malformed.length > 0 || exitCode !== 0 ? "failed" : "completed";
160
+ const summary = status === "failed"
161
+ ? sawError ||
162
+ `Hermes bridge failed${malformed.length ? `; malformed_frames=${malformed.join(",")}` : ""}${stderr.length ? `; stderr=${stderr.join("").slice(0, 500)}` : ""}`
163
+ : finalText || "Hermes-local turn completed.";
164
+ return {
165
+ result: {
166
+ bridge_id: sessionId,
167
+ role: options.role,
168
+ status,
169
+ summary,
170
+ turns,
171
+ tool_calls: toolCalls,
172
+ child_results: [],
173
+ evidence_refs: toolCalls.map((tool) => `tool:${tool.tool}`),
174
+ },
175
+ metadata: {
176
+ bridge_protocol_version: HERMES_BRIDGE_PROTOCOL_VERSION,
177
+ hermes_session_id: sessionId,
178
+ shadow_mcp_session_id: shadowId,
179
+ execution_engine: "hermes_local",
180
+ provider: options.runtime.provider,
181
+ model: options.runtime.model,
182
+ events,
183
+ },
184
+ };
185
+ }
186
+ }
187
+ //# sourceMappingURL=session-manager.js.map
@@ -114,6 +114,30 @@ function defaultJobLockFile() {
114
114
  locks: [],
115
115
  };
116
116
  }
117
+ function coerceJobQueueFile(value) {
118
+ if (!value || typeof value !== "object" || Array.isArray(value))
119
+ return undefined;
120
+ const candidate = value;
121
+ if (candidate.version !== 1)
122
+ return undefined;
123
+ return {
124
+ version: 1,
125
+ updated_at: typeof candidate.updated_at === "string" ? candidate.updated_at : nowIso(),
126
+ jobs: Array.isArray(candidate.jobs) ? candidate.jobs : [],
127
+ };
128
+ }
129
+ function coerceJobLockTableFile(value) {
130
+ if (!value || typeof value !== "object" || Array.isArray(value))
131
+ return undefined;
132
+ const candidate = value;
133
+ if (candidate.version !== 1)
134
+ return undefined;
135
+ return {
136
+ version: 1,
137
+ updated_at: typeof candidate.updated_at === "string" ? candidate.updated_at : nowIso(),
138
+ locks: Array.isArray(candidate.locks) ? candidate.locks : [],
139
+ };
140
+ }
117
141
  function defaultLease(owner = "scheduler") {
118
142
  const now = new Date();
119
143
  const expires = new Date(now.getTime() + 30_000);
@@ -278,8 +302,9 @@ export function readJobQueue() {
278
302
  const root = resolveWorkspaceRoot();
279
303
  if (storeExistsSync(root)) {
280
304
  const stored = readStoreJsonSync(root, "state/scheduler/queue");
281
- if (stored)
282
- return stored;
305
+ const normalized = coerceJobQueueFile(stored);
306
+ if (normalized)
307
+ return normalized;
283
308
  }
284
309
  const raw = safeRead(JOB_QUEUE_REL);
285
310
  if (isReadError(raw))
@@ -290,8 +315,9 @@ export function readJobLockTable() {
290
315
  const root = resolveWorkspaceRoot();
291
316
  if (storeExistsSync(root)) {
292
317
  const stored = readStoreJsonSync(root, "state/scheduler/locks");
293
- if (stored)
294
- return stored;
318
+ const normalized = coerceJobLockTableFile(stored);
319
+ if (normalized)
320
+ return normalized;
295
321
  }
296
322
  const raw = safeRead(JOB_LOCK_TABLE_REL);
297
323
  if (isReadError(raw))
@@ -0,0 +1,16 @@
1
+ export interface JsonTextSanitizationResult {
2
+ text: string;
3
+ removed_control_bytes: number;
4
+ had_bom: boolean;
5
+ }
6
+ export declare function sanitizeJsonLikeText(raw: string): JsonTextSanitizationResult;
7
+ export declare function parseJsonLikeText(raw: string): {
8
+ ok: true;
9
+ value: unknown;
10
+ sanitized: JsonTextSanitizationResult;
11
+ } | {
12
+ ok: false;
13
+ error: string;
14
+ sanitized: JsonTextSanitizationResult;
15
+ };
16
+ //# sourceMappingURL=json-sanitizer.d.ts.map