@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.
- package/CHANGELOG.md +16 -0
- package/README.md +502 -56
- package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
- package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
- package/assets/agent-state/runtime-tool-specs.json +70 -2
- package/assets/instructions/ACE_Coder.instructions.md +13 -0
- package/assets/instructions/ACE_UI.instructions.md +11 -0
- package/dist/ace-context.js +70 -11
- package/dist/ace-internal-tools.d.ts +3 -1
- package/dist/ace-internal-tools.js +10 -2
- package/dist/agent-runtime/role-adapters.d.ts +18 -1
- package/dist/agent-runtime/role-adapters.js +49 -5
- package/dist/astgrep-index.d.ts +48 -0
- package/dist/astgrep-index.js +126 -1
- package/dist/cli.js +487 -17
- package/dist/discovery-runtime-wrappers.d.ts +108 -0
- package/dist/discovery-runtime-wrappers.js +615 -0
- package/dist/helpers/bootstrap.js +1 -1
- package/dist/helpers/constants.d.ts +4 -2
- package/dist/helpers/constants.js +8 -0
- package/dist/helpers/path-utils.d.ts +8 -1
- package/dist/helpers/path-utils.js +27 -8
- package/dist/helpers/store-resolution.js +7 -3
- package/dist/hermes/bridge-protocol.d.ts +41 -0
- package/dist/hermes/bridge-protocol.js +70 -0
- package/dist/hermes/launch-profile.d.ts +19 -0
- package/dist/hermes/launch-profile.js +81 -0
- package/dist/hermes/session-manager.d.ts +42 -0
- package/dist/hermes/session-manager.js +187 -0
- package/dist/job-scheduler.js +30 -4
- package/dist/json-sanitizer.d.ts +16 -0
- package/dist/json-sanitizer.js +26 -0
- package/dist/local-model-policy.d.ts +27 -0
- package/dist/local-model-policy.js +84 -0
- package/dist/local-model-runtime.d.ts +17 -0
- package/dist/local-model-runtime.js +77 -20
- package/dist/model-bridge.d.ts +6 -1
- package/dist/model-bridge.js +338 -21
- package/dist/orchestrator-supervisor.d.ts +42 -0
- package/dist/orchestrator-supervisor.js +110 -3
- package/dist/plan-proposal.d.ts +115 -0
- package/dist/plan-proposal.js +1073 -0
- package/dist/runtime-executor.d.ts +6 -1
- package/dist/runtime-executor.js +72 -5
- package/dist/runtime-tool-specs.d.ts +19 -1
- package/dist/runtime-tool-specs.js +67 -26
- package/dist/schemas.js +30 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.js +73 -4
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +2 -0
- package/dist/store/bootstrap-store.d.ts +1 -0
- package/dist/store/bootstrap-store.js +8 -2
- package/dist/store/materializers/vericify-projector.js +3 -0
- package/dist/store/repositories/local-model-runtime-repository.d.ts +13 -1
- package/dist/store/repositories/local-model-runtime-repository.js +4 -1
- package/dist/store/repositories/vericify-repository.d.ts +1 -1
- package/dist/tools-agent.d.ts +20 -0
- package/dist/tools-agent.js +544 -29
- package/dist/tools-discovery.js +135 -0
- package/dist/tools-files.js +768 -66
- package/dist/tools-framework.js +80 -61
- package/dist/tools.d.ts +4 -1
- package/dist/tools.js +35 -13
- package/dist/tui/chat.d.ts +8 -0
- package/dist/tui/chat.js +74 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.js +45 -2
- package/dist/tui/layout.d.ts +1 -0
- package/dist/tui/layout.js +4 -1
- package/dist/tui/ollama.d.ts +8 -1
- package/dist/tui/ollama.js +53 -12
- package/dist/tui/openai-compatible.d.ts +13 -0
- package/dist/tui/openai-compatible.js +305 -5
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +50 -24
- package/dist/vericify-bridge.d.ts +4 -1
- package/dist/vericify-bridge.js +3 -0
- package/package.json +2 -1
- 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
|
|
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
|
-
|
|
58
|
-
const
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
package/dist/job-scheduler.js
CHANGED
|
@@ -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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|