@voybio/ace-swarm 2.4.3 → 2.4.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/CHANGELOG.md +78 -0
- package/README.md +72 -532
- package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
- package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
- package/assets/agent-state/MODULES/registry.json +1 -4
- package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
- package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
- package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
- package/assets/agent-state/QUALITY_GATES.md +25 -9
- package/assets/agent-state/TEAL_CONFIG.md +2 -11
- package/assets/eval-battery/battery.json +85 -0
- package/assets/eval-battery/fixtures/edit-target.js +3 -0
- package/assets/scripts/render-mcp-configs.sh +12 -0
- package/dist/ace-context.d.ts +18 -0
- package/dist/ace-context.js +161 -16
- package/dist/ace-server-instructions.js +36 -9
- package/dist/astgrep-index.d.ts +7 -1
- package/dist/astgrep-index.js +66 -39
- package/dist/bridge-trace.d.ts +17 -0
- package/dist/bridge-trace.js +23 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +619 -10
- package/dist/discovery-runtime-wrappers.d.ts +16 -1
- package/dist/discovery-runtime-wrappers.js +65 -9
- package/dist/eval-harness.d.ts +110 -0
- package/dist/eval-harness.js +254 -0
- package/dist/experience-reflector.d.ts +13 -0
- package/dist/experience-reflector.js +62 -0
- package/dist/gate-contract.d.ts +63 -0
- package/dist/gate-contract.js +178 -0
- package/dist/goal-engine.d.ts +155 -0
- package/dist/goal-engine.js +795 -0
- package/dist/helpers/store-resolution.js +40 -39
- package/dist/hermes/session-manager.d.ts +10 -0
- package/dist/hermes/session-manager.js +130 -27
- package/dist/job-scheduler.d.ts +58 -0
- package/dist/job-scheduler.js +202 -2
- package/dist/job-worker.d.ts +87 -0
- package/dist/job-worker.js +182 -0
- package/dist/local-model-policy.d.ts +8 -2
- package/dist/local-model-policy.js +31 -17
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +130 -2
- package/dist/measured-classing.d.ts +66 -0
- package/dist/measured-classing.js +165 -0
- package/dist/model-bridge.d.ts +45 -0
- package/dist/model-bridge.js +502 -44
- package/dist/orchestrator-supervisor.js +1 -1
- package/dist/plan-proposal.d.ts +7 -0
- package/dist/plan-proposal.js +74 -4
- package/dist/playbook-adaptation.d.ts +37 -0
- package/dist/playbook-adaptation.js +159 -0
- package/dist/playbook-curator.d.ts +16 -0
- package/dist/playbook-curator.js +106 -0
- package/dist/playbook-llm-reflector.d.ts +26 -0
- package/dist/playbook-llm-reflector.js +157 -0
- package/dist/public-surface.d.ts +2 -0
- package/dist/public-surface.js +26 -14
- package/dist/runtime-executor.js +45 -2
- package/dist/runtime-tool-specs.d.ts +8 -1
- package/dist/runtime-tool-specs.js +19 -1
- package/dist/safe-edit.d.ts +20 -0
- package/dist/safe-edit.js +355 -78
- package/dist/schemas.js +16 -15
- package/dist/server.d.ts +11 -2
- package/dist/server.js +55 -5
- package/dist/shared.d.ts +12 -0
- package/dist/shared.js +13 -1
- package/dist/source-mutation-coordinator.d.ts +54 -0
- package/dist/source-mutation-coordinator.js +259 -0
- package/dist/status-events.d.ts +4 -1
- package/dist/status-events.js +41 -6
- package/dist/store/ace-packed-store.d.ts +28 -9
- package/dist/store/ace-packed-store.js +324 -57
- package/dist/store/bgzf.d.ts +4 -0
- package/dist/store/bgzf.js +94 -0
- package/dist/store/bootstrap-store.d.ts +4 -1
- package/dist/store/bootstrap-store.js +87 -9
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +67 -1
- package/dist/store/crc32.d.ts +2 -0
- package/dist/store/crc32.js +16 -0
- package/dist/store/fsck.d.ts +16 -0
- package/dist/store/fsck.js +240 -0
- package/dist/store/gate-contract-migration.d.ts +10 -0
- package/dist/store/gate-contract-migration.js +413 -0
- package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
- package/dist/store/materializers/context-snapshot-materializer.js +4 -1
- package/dist/store/materializers/projection-manager.d.ts +7 -0
- package/dist/store/materializers/projection-manager.js +37 -0
- package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
- package/dist/store/repositories/goal-repository.d.ts +112 -0
- package/dist/store/repositories/goal-repository.js +157 -0
- package/dist/store/repositories/playbook-repository.d.ts +107 -0
- package/dist/store/repositories/playbook-repository.js +314 -0
- package/dist/store/store-artifacts.js +44 -2
- package/dist/store/store-authority-audit.js +1 -0
- package/dist/store/store-cli.d.ts +30 -0
- package/dist/store/store-cli.js +242 -0
- package/dist/store/store-snapshot.d.ts +12 -0
- package/dist/store/store-snapshot.js +74 -11
- package/dist/store/types.d.ts +1 -0
- package/dist/store/write-coordinator.d.ts +7 -0
- package/dist/store/write-coordinator.js +11 -0
- package/dist/todo-state.d.ts +21 -0
- package/dist/todo-state.js +150 -53
- package/dist/tools-agent.js +86 -22
- package/dist/tools-discovery.js +5 -3
- package/dist/tools-files.js +107 -23
- package/dist/tools-framework.js +146 -40
- package/dist/tools-goal.d.ts +11 -0
- package/dist/tools-goal.js +108 -0
- package/dist/tools-handoff.js +7 -11
- package/dist/tools-lifecycle.js +2 -0
- package/dist/tools-scheduler.js +44 -1
- package/dist/tools-skills.js +3 -2
- package/dist/tools-todo.js +64 -3
- package/dist/tools.js +2 -0
- package/dist/tui/chat.js +4 -0
- package/dist/tui/index.js +4 -1
- package/dist/tui/ollama.d.ts +2 -0
- package/dist/tui/openai-compatible.d.ts +14 -0
- package/dist/tui/openai-compatible.js +15 -2
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +2 -2
- package/docs/cli-reference.md +125 -0
- package/docs/development.md +46 -0
- package/docs/evaluation-safety.md +27 -0
- package/docs/experience-playbook.md +35 -0
- package/docs/hermes.md +129 -0
- package/docs/library-api.md +31 -0
- package/docs/mcp-tool-reference.md +290 -0
- package/docs/provider-security.md +85 -0
- package/docs/state-model.md +113 -0
- package/docs/troubleshooting.md +95 -0
- package/docs/tui-operator-guide.md +75 -0
- package/package.json +5 -1
- package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
- package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
- package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
- package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
|
@@ -7,18 +7,6 @@ import { isOperationalArtifactPath, operationalArtifactKey, writeOperationalArti
|
|
|
7
7
|
import { withStoreWriteCoordinatorSync } from "../store/write-coordinator.js";
|
|
8
8
|
import { ACE_ROOT_REL, ACE_SCRIPTS_ROOT_REL, ACE_SKILLS_ROOT_REL, ACE_TASKS_ROOT_REL, AGENT_FILES, AGENT_MANIFEST_FILES, DEFAULTS_ROOT, KERNEL_FILES, STORE_AGENT_FILES, STORE_KERNEL_FILES, STORE_TASK_FILES, TASK_FILES, } from "./constants.js";
|
|
9
9
|
import { currentWorkspaceRoot, isProjectedAceWorkspacePath, mapAceWorkspaceRelativePath, resolveWorkspaceReadPath, resolveWorkspaceWritePath, } from "./path-utils.js";
|
|
10
|
-
const STORE_ONLY_AGENT_STATE_FILES = new Set([
|
|
11
|
-
"agent-state/AST_GREP_INDEX.json",
|
|
12
|
-
"agent-state/AST_GREP_INDEX.md",
|
|
13
|
-
"agent-state/EVIDENCE_LOG.md",
|
|
14
|
-
"agent-state/STATUS_EVENTS.ndjson",
|
|
15
|
-
"agent-state/STATUS_EVENTS-archive.ndjson",
|
|
16
|
-
"agent-state/index-fingerprints.json",
|
|
17
|
-
"agent-state/index.json",
|
|
18
|
-
"agent-state/job-locks.json",
|
|
19
|
-
"agent-state/job-queue.json",
|
|
20
|
-
"agent-state/scheduler-lease.json",
|
|
21
|
-
]);
|
|
22
10
|
export function formatStorePath(key) {
|
|
23
11
|
return toVirtualStorePath(getWorkspaceStorePath(currentWorkspaceRoot()), key);
|
|
24
12
|
}
|
|
@@ -62,6 +50,13 @@ function resolveStoreFallbackKeys(filePath) {
|
|
|
62
50
|
if (logicalAgentStatePath && isOperationalArtifactPath(logicalAgentStatePath)) {
|
|
63
51
|
keys.add(operationalArtifactKey(logicalAgentStatePath));
|
|
64
52
|
}
|
|
53
|
+
// Root-level tasks/SWARM_HANDOFF.*.json — not under agent-state/ or
|
|
54
|
+
// .agents/ACE/, so it previously had no store fallback key at all and
|
|
55
|
+
// accumulated forever outside `ace cache`'s scan roots.
|
|
56
|
+
const handoffPayloadMatch = normalized.match(/^tasks\/(SWARM_HANDOFF\..+\.json)$/);
|
|
57
|
+
if (handoffPayloadMatch) {
|
|
58
|
+
keys.add(`state/handoffs/payloads/${handoffPayloadMatch[1]}`);
|
|
59
|
+
}
|
|
65
60
|
if (normalized === "agent-state/ACE_WORKFLOW.md" || canonical === `${ACE_ROOT_REL}/agent-state/ACE_WORKFLOW.md`) {
|
|
66
61
|
keys.add("knowledge/runtime/ACE_WORKFLOW.md");
|
|
67
62
|
}
|
|
@@ -117,6 +112,9 @@ function resolveStoreFallbackKeys(filePath) {
|
|
|
117
112
|
]);
|
|
118
113
|
if (logicalAgentStatePath?.startsWith("agent-state/")) {
|
|
119
114
|
const rel = logicalAgentStatePath.slice("agent-state/".length);
|
|
115
|
+
if (rel === "source-edit-events.ndjson") {
|
|
116
|
+
keys.add("state/source-edit-events.ndjson");
|
|
117
|
+
}
|
|
120
118
|
if (!rel.startsWith("context-snapshots/")) {
|
|
121
119
|
keys.add(`state/artifacts/agent-state/${rel}`);
|
|
122
120
|
}
|
|
@@ -176,6 +174,18 @@ function resolveStoreFallbackKeys(filePath) {
|
|
|
176
174
|
}
|
|
177
175
|
return [...keys];
|
|
178
176
|
}
|
|
177
|
+
function logicalAgentStatePathFor(normalized, canonical) {
|
|
178
|
+
return normalized.startsWith("agent-state/")
|
|
179
|
+
? normalized
|
|
180
|
+
: canonical.startsWith(`${ACE_ROOT_REL}/agent-state/`)
|
|
181
|
+
? canonical.slice(`${ACE_ROOT_REL}/`.length)
|
|
182
|
+
: undefined;
|
|
183
|
+
}
|
|
184
|
+
function isAceManagedPath(canonical, logicalAgentStatePath) {
|
|
185
|
+
return Boolean(logicalAgentStatePath ||
|
|
186
|
+
canonical === ACE_ROOT_REL ||
|
|
187
|
+
canonical.startsWith(`${ACE_ROOT_REL}/`));
|
|
188
|
+
}
|
|
179
189
|
export function resolveStoreFallbackKeysForPath(filePath) {
|
|
180
190
|
return resolveStoreFallbackKeys(filePath);
|
|
181
191
|
}
|
|
@@ -197,9 +207,22 @@ export function safeRead(filePath) {
|
|
|
197
207
|
const direct = readVirtualStorePathSync(filePath);
|
|
198
208
|
return typeof direct === "string" ? direct : `[FILE NOT FOUND] ${filePath}`;
|
|
199
209
|
}
|
|
210
|
+
const storeKeys = resolveStoreFallbackKeys(filePath);
|
|
211
|
+
const store = readStoreText(storeKeys);
|
|
212
|
+
const normalized = normalizeRelPath(filePath);
|
|
213
|
+
const canonical = mapAceWorkspaceRelativePath(normalized);
|
|
214
|
+
const storeFirst = process.env.ACE_STORE_FIRST !== "0" && !isProjectedAceWorkspacePath(canonical);
|
|
200
215
|
const abs = resolveWorkspaceReadPath(filePath);
|
|
216
|
+
if (store && storeFirst) {
|
|
217
|
+
if (existsSync(abs)) {
|
|
218
|
+
const diskText = readFileSyncUtf8(abs);
|
|
219
|
+
if (diskText !== store.text) {
|
|
220
|
+
console.error(`ace safeRead: store/disk copies differ for '${filePath}' (source=store, key=${store.key}; set ACE_STORE_FIRST=0 for legacy disk precedence)`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return store.text;
|
|
224
|
+
}
|
|
201
225
|
if (!existsSync(abs)) {
|
|
202
|
-
const store = readStoreText(resolveStoreFallbackKeys(filePath));
|
|
203
226
|
if (store)
|
|
204
227
|
return store.text;
|
|
205
228
|
return `[FILE NOT FOUND] ${abs}`;
|
|
@@ -213,24 +236,13 @@ export function safeRead(filePath) {
|
|
|
213
236
|
export function safeWrite(filePath, content) {
|
|
214
237
|
const normalized = normalizeRelPath(filePath);
|
|
215
238
|
const canonical = mapAceWorkspaceRelativePath(normalized);
|
|
216
|
-
const logicalAgentStatePath = normalized
|
|
217
|
-
? normalized
|
|
218
|
-
: canonical.startsWith(`${ACE_ROOT_REL}/agent-state/`)
|
|
219
|
-
? canonical.slice(`${ACE_ROOT_REL}/`.length)
|
|
220
|
-
: undefined;
|
|
239
|
+
const logicalAgentStatePath = logicalAgentStatePathFor(normalized, canonical);
|
|
221
240
|
const root = currentWorkspaceRoot();
|
|
222
241
|
let storeVirtualPath;
|
|
223
242
|
const storeKeys = resolveStoreFallbackKeys(filePath);
|
|
224
243
|
if (storeKeys.length > 0 && existsSync(getWorkspaceStorePath(root))) {
|
|
225
244
|
storeVirtualPath = withStoreWriteCoordinatorSync(getWorkspaceStorePath(root), () => writeStoreBlobSync(root, storeKeys[0], content), { operation_label: "safeWrite" });
|
|
226
|
-
|
|
227
|
-
const isStoreOnlyProjection = canonical === `${ACE_ROOT_REL}/ace-hook-context.json` ||
|
|
228
|
-
logicalAgentStatePath?.startsWith("agent-state/context-snapshots/") ||
|
|
229
|
-
STORE_ONLY_AGENT_STATE_FILES.has(logicalAgentStatePath ?? "");
|
|
230
|
-
if (isAcePath && !isProjectedAceWorkspacePath(canonical)) {
|
|
231
|
-
return storeVirtualPath;
|
|
232
|
-
}
|
|
233
|
-
if (isStoreOnlyProjection) {
|
|
245
|
+
if (isAceManagedPath(canonical, logicalAgentStatePath) && !isProjectedAceWorkspacePath(canonical)) {
|
|
234
246
|
return storeVirtualPath;
|
|
235
247
|
}
|
|
236
248
|
}
|
|
@@ -246,11 +258,7 @@ export function safeWrite(filePath, content) {
|
|
|
246
258
|
export async function safeWriteAsync(filePath, content) {
|
|
247
259
|
const normalized = normalizeRelPath(filePath);
|
|
248
260
|
const canonical = mapAceWorkspaceRelativePath(normalized);
|
|
249
|
-
const logicalAgentStatePath = normalized
|
|
250
|
-
? normalized
|
|
251
|
-
: canonical.startsWith(`${ACE_ROOT_REL}/agent-state/`)
|
|
252
|
-
? canonical.slice(`${ACE_ROOT_REL}/`.length)
|
|
253
|
-
: undefined;
|
|
261
|
+
const logicalAgentStatePath = logicalAgentStatePathFor(normalized, canonical);
|
|
254
262
|
const root = currentWorkspaceRoot();
|
|
255
263
|
let storeVirtualPath;
|
|
256
264
|
const storeKeys = resolveStoreFallbackKeys(filePath);
|
|
@@ -262,14 +270,7 @@ export async function safeWriteAsync(filePath, content) {
|
|
|
262
270
|
else {
|
|
263
271
|
storeVirtualPath = await writeStoreBlobQueued(root, storeKey, content, { operation_label: "safeWriteAsync" });
|
|
264
272
|
}
|
|
265
|
-
|
|
266
|
-
const isStoreOnlyProjection = canonical === `${ACE_ROOT_REL}/ace-hook-context.json` ||
|
|
267
|
-
logicalAgentStatePath?.startsWith("agent-state/context-snapshots/") ||
|
|
268
|
-
STORE_ONLY_AGENT_STATE_FILES.has(logicalAgentStatePath ?? "");
|
|
269
|
-
if (isAcePath && !isProjectedAceWorkspacePath(canonical)) {
|
|
270
|
-
return storeVirtualPath;
|
|
271
|
-
}
|
|
272
|
-
if (isStoreOnlyProjection) {
|
|
273
|
+
if (isAceManagedPath(canonical, logicalAgentStatePath) && !isProjectedAceWorkspacePath(canonical)) {
|
|
273
274
|
return storeVirtualPath;
|
|
274
275
|
}
|
|
275
276
|
}
|
|
@@ -10,6 +10,9 @@ export interface HermesLocalTurnOptions {
|
|
|
10
10
|
policy: LocalModelExecutionPolicy;
|
|
11
11
|
toolScope?: string[];
|
|
12
12
|
maxTurns: number;
|
|
13
|
+
/** Probed context window for the model; falls back to a conservative default. */
|
|
14
|
+
contextLength?: number;
|
|
15
|
+
timeoutMs?: number;
|
|
13
16
|
}
|
|
14
17
|
export interface HermesLocalTurnResult {
|
|
15
18
|
result: BridgeResult;
|
|
@@ -21,10 +24,12 @@ export interface HermesLocalTurnResult {
|
|
|
21
24
|
provider: string;
|
|
22
25
|
model: string;
|
|
23
26
|
events: HermesBridgeEvent[];
|
|
27
|
+
touched_paths: string[];
|
|
24
28
|
};
|
|
25
29
|
}
|
|
26
30
|
export interface HermesLocalExecutor {
|
|
27
31
|
runTurn(options: HermesLocalTurnOptions): Promise<HermesLocalTurnResult>;
|
|
32
|
+
interrupt?(): void;
|
|
28
33
|
}
|
|
29
34
|
export interface HermesSubprocessExecutorOptions {
|
|
30
35
|
hermesRoot?: string;
|
|
@@ -33,10 +38,15 @@ export interface HermesSubprocessExecutorOptions {
|
|
|
33
38
|
command?: readonly string[];
|
|
34
39
|
launchProfile?: HermesLaunchProfile;
|
|
35
40
|
keepTemp?: boolean;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
maxOutputBytes?: number;
|
|
36
43
|
}
|
|
37
44
|
export declare class HermesSubprocessExecutor implements HermesLocalExecutor {
|
|
38
45
|
private options;
|
|
46
|
+
private readonly activeChildren;
|
|
39
47
|
constructor(options?: HermesSubprocessExecutorOptions);
|
|
48
|
+
interrupt(): void;
|
|
49
|
+
private interruptChild;
|
|
40
50
|
runTurn(options: HermesLocalTurnOptions): Promise<HermesLocalTurnResult>;
|
|
41
51
|
}
|
|
42
52
|
//# sourceMappingURL=session-manager.d.ts.map
|
|
@@ -10,7 +10,38 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
11
|
const DIST_ROOT = resolve(__dirname, "..");
|
|
12
12
|
const PACKAGE_ROOT = resolve(DIST_ROOT, "..");
|
|
13
|
-
|
|
13
|
+
// Conservative fallback when the model's real context window has not been probed.
|
|
14
|
+
// The prior 65536 default over-promised small models (an 8k gemma/llama was told it
|
|
15
|
+
// had 64k), causing silent server-side truncation. Callers that have a probed value
|
|
16
|
+
// (Ollama /api/show, llama.cpp /props) should pass it via options.contextLength.
|
|
17
|
+
const DEFAULT_HERMES_LOCAL_CONTEXT_LENGTH = 8192;
|
|
18
|
+
// Env vars forwarded to the operator-supplied Hermes Python worker. Forwarding the
|
|
19
|
+
// full parent process.env leaked unrelated API keys/secrets into a third-party
|
|
20
|
+
// process; only transport-relevant variables are allowlisted here.
|
|
21
|
+
const HERMES_ENV_ALLOWLIST_EXACT = new Set([
|
|
22
|
+
"PATH",
|
|
23
|
+
"HOME",
|
|
24
|
+
"TMPDIR",
|
|
25
|
+
"LANG",
|
|
26
|
+
"LC_ALL",
|
|
27
|
+
"OLLAMA_HOST",
|
|
28
|
+
"OLLAMA_BASE_URL",
|
|
29
|
+
"OPENAI_BASE_URL",
|
|
30
|
+
"OPENAI_API_BASE",
|
|
31
|
+
]);
|
|
32
|
+
const HERMES_ENV_ALLOWLIST_PREFIXES = ["HERMES_", "ACE_"];
|
|
33
|
+
function scrubbedHermesEnv(base) {
|
|
34
|
+
const scrubbed = {};
|
|
35
|
+
for (const [key, value] of Object.entries(base)) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
continue;
|
|
38
|
+
if (HERMES_ENV_ALLOWLIST_EXACT.has(key) ||
|
|
39
|
+
HERMES_ENV_ALLOWLIST_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
40
|
+
scrubbed[key] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return scrubbed;
|
|
44
|
+
}
|
|
14
45
|
function summarizeToolResult(value) {
|
|
15
46
|
if (typeof value === "string")
|
|
16
47
|
return value.slice(0, 500);
|
|
@@ -34,15 +65,38 @@ function hermesBaseUrl(provider, baseUrl) {
|
|
|
34
65
|
}
|
|
35
66
|
export class HermesSubprocessExecutor {
|
|
36
67
|
options;
|
|
68
|
+
activeChildren = new Set();
|
|
37
69
|
constructor(options = {}) {
|
|
38
70
|
this.options = options;
|
|
39
71
|
}
|
|
72
|
+
interrupt() {
|
|
73
|
+
for (const child of this.activeChildren)
|
|
74
|
+
this.interruptChild(child);
|
|
75
|
+
}
|
|
76
|
+
interruptChild(child) {
|
|
77
|
+
if (child.killed)
|
|
78
|
+
return;
|
|
79
|
+
child.kill("SIGTERM");
|
|
80
|
+
const killTimer = setTimeout(() => {
|
|
81
|
+
if (this.activeChildren.has(child) && child.exitCode === null && child.signalCode === null) {
|
|
82
|
+
child.kill("SIGKILL");
|
|
83
|
+
}
|
|
84
|
+
}, 1_000);
|
|
85
|
+
killTimer.unref();
|
|
86
|
+
}
|
|
40
87
|
async runTurn(options) {
|
|
41
88
|
const sessionId = `hermes-${randomUUID()}`;
|
|
42
89
|
const shadowId = `shadow-${randomUUID()}`;
|
|
43
90
|
const hermesHome = mkdtempSync(resolve(tmpdir(), "ace-hermes-home-"));
|
|
44
91
|
mkdirSync(hermesHome, { recursive: true });
|
|
45
|
-
const toolScope = options.toolScope
|
|
92
|
+
const toolScope = options.toolScope;
|
|
93
|
+
if (!toolScope || toolScope.length === 0) {
|
|
94
|
+
throw new Error("hermes_local requires a non-empty tool scope (reason_code=hermes_empty_tool_scope). " +
|
|
95
|
+
"This is an upstream configuration bug — toolScope must be populated before reaching the Hermes executor.");
|
|
96
|
+
}
|
|
97
|
+
const contextLength = options.contextLength && options.contextLength > 0
|
|
98
|
+
? options.contextLength
|
|
99
|
+
: DEFAULT_HERMES_LOCAL_CONTEXT_LENGTH;
|
|
46
100
|
const cliPath = resolve(DIST_ROOT, "cli.js");
|
|
47
101
|
const baseUrl = hermesBaseUrl(options.runtime.provider, options.runtime.baseUrl ?? options.runtime.ollamaUrl);
|
|
48
102
|
writeFileSync(resolve(hermesHome, "config.yaml"), [
|
|
@@ -51,7 +105,7 @@ export class HermesSubprocessExecutor {
|
|
|
51
105
|
" provider: custom",
|
|
52
106
|
` base_url: ${JSON.stringify(baseUrl)}`,
|
|
53
107
|
" api_key: no-key-required",
|
|
54
|
-
` context_length: ${
|
|
108
|
+
` context_length: ${contextLength}`,
|
|
55
109
|
"",
|
|
56
110
|
"auxiliary:",
|
|
57
111
|
" compression:",
|
|
@@ -59,7 +113,7 @@ export class HermesSubprocessExecutor {
|
|
|
59
113
|
" provider: custom",
|
|
60
114
|
` base_url: ${JSON.stringify(baseUrl)}`,
|
|
61
115
|
" api_key: no-key-required",
|
|
62
|
-
` context_length: ${
|
|
116
|
+
` context_length: ${contextLength}`,
|
|
63
117
|
"",
|
|
64
118
|
"mcp_servers:",
|
|
65
119
|
" ace_shadow:",
|
|
@@ -88,12 +142,13 @@ export class HermesSubprocessExecutor {
|
|
|
88
142
|
const child = spawn(bin, [...args, workerPath], {
|
|
89
143
|
cwd: PACKAGE_ROOT,
|
|
90
144
|
env: {
|
|
91
|
-
...process.env,
|
|
145
|
+
...scrubbedHermesEnv(process.env),
|
|
92
146
|
HERMES_HOME: hermesHome,
|
|
93
147
|
ACE_WORKSPACE_ROOT: options.runtime.workspaceRoot,
|
|
94
148
|
},
|
|
95
149
|
stdio: ["pipe", "pipe", "pipe"],
|
|
96
150
|
});
|
|
151
|
+
this.activeChildren.add(child);
|
|
97
152
|
const decoder = new HermesBridgeFrameDecoder();
|
|
98
153
|
const events = [];
|
|
99
154
|
const malformed = [];
|
|
@@ -101,25 +156,50 @@ export class HermesSubprocessExecutor {
|
|
|
101
156
|
let finalText = "";
|
|
102
157
|
let turns = 0;
|
|
103
158
|
let sawError = "";
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
let finalCompleted;
|
|
160
|
+
let finalCount = 0;
|
|
161
|
+
let outputBytes = 0;
|
|
162
|
+
let outputOverflow = false;
|
|
163
|
+
const maxOutputBytes = this.options.maxOutputBytes ?? 4 * 1024 * 1024;
|
|
164
|
+
// Single per-frame consumer so the live stream and the post-close tail-flush
|
|
165
|
+
// (decoder.push(…, true)) extract final/error semantics identically and can never drift.
|
|
166
|
+
const applyFrame = (frame) => {
|
|
167
|
+
if (frame.ok) {
|
|
168
|
+
events.push(frame.event);
|
|
169
|
+
if (frame.event.type === "final") {
|
|
170
|
+
finalCount += 1;
|
|
171
|
+
finalText = eventText(frame.event);
|
|
172
|
+
const rawCompleted = frame.event.completed;
|
|
173
|
+
finalCompleted = typeof rawCompleted === "boolean" ? rawCompleted : true;
|
|
174
|
+
const rawTurns = frame.event.turns;
|
|
175
|
+
turns = typeof rawTurns === "number" ? rawTurns : turns;
|
|
116
176
|
}
|
|
117
|
-
|
|
118
|
-
|
|
177
|
+
if (frame.event.type === "error") {
|
|
178
|
+
sawError = eventText(frame.event) || "Hermes bridge error";
|
|
119
179
|
}
|
|
120
180
|
}
|
|
181
|
+
else {
|
|
182
|
+
malformed.push(frame.reason);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
child.stdout.on("data", (chunk) => {
|
|
186
|
+
outputBytes += chunk.byteLength;
|
|
187
|
+
if (outputBytes > maxOutputBytes) {
|
|
188
|
+
outputOverflow = true;
|
|
189
|
+
this.interruptChild(child);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const frame of decoder.push(chunk)) {
|
|
193
|
+
applyFrame(frame);
|
|
194
|
+
}
|
|
121
195
|
});
|
|
122
196
|
child.stderr.on("data", (chunk) => {
|
|
197
|
+
outputBytes += chunk.byteLength;
|
|
198
|
+
if (outputBytes > maxOutputBytes) {
|
|
199
|
+
outputOverflow = true;
|
|
200
|
+
this.interruptChild(child);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
123
203
|
stderr.push(chunk.toString("utf8"));
|
|
124
204
|
});
|
|
125
205
|
const request = {
|
|
@@ -135,15 +215,22 @@ export class HermesSubprocessExecutor {
|
|
|
135
215
|
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
216
|
};
|
|
137
217
|
child.stdin.end(`${JSON.stringify(request)}\n`);
|
|
218
|
+
const timeoutMs = options.timeoutMs ?? this.options.timeoutMs ?? 120_000;
|
|
219
|
+
let timedOut = false;
|
|
220
|
+
const timeout = setTimeout(() => {
|
|
221
|
+
timedOut = true;
|
|
222
|
+
this.interruptChild(child);
|
|
223
|
+
}, timeoutMs);
|
|
224
|
+
timeout.unref();
|
|
138
225
|
const exitCode = await new Promise((resolvePromise, reject) => {
|
|
139
226
|
child.on("error", reject);
|
|
140
227
|
child.on("close", resolvePromise);
|
|
228
|
+
}).finally(() => {
|
|
229
|
+
clearTimeout(timeout);
|
|
230
|
+
this.activeChildren.delete(child);
|
|
141
231
|
});
|
|
142
232
|
for (const frame of decoder.push(new Uint8Array(), true)) {
|
|
143
|
-
|
|
144
|
-
events.push(frame.event);
|
|
145
|
-
else
|
|
146
|
-
malformed.push(frame.reason);
|
|
233
|
+
applyFrame(frame);
|
|
147
234
|
}
|
|
148
235
|
if (!this.options.keepTemp) {
|
|
149
236
|
rmSync(hermesHome, { recursive: true, force: true });
|
|
@@ -156,11 +243,26 @@ export class HermesSubprocessExecutor {
|
|
|
156
243
|
summary: summarizeToolResult(event.result ?? eventText(event)),
|
|
157
244
|
isError: Boolean(event.is_error),
|
|
158
245
|
}));
|
|
159
|
-
const
|
|
246
|
+
const touchedPaths = events
|
|
247
|
+
.filter((event) => event.type === "tool_start" && event.tool === "write_workspace_file")
|
|
248
|
+
.map((event) => event.input?.path)
|
|
249
|
+
.filter((path) => typeof path === "string" && path.trim().length > 0);
|
|
250
|
+
const finalIndex = events.findIndex((event) => event.type === "final");
|
|
251
|
+
const protocolIdentityInvalid = events.some((event) => (event.session_id !== undefined && event.session_id !== sessionId) ||
|
|
252
|
+
(event.turn_id !== undefined && event.turn_id !== request.turn_id));
|
|
253
|
+
const eventAfterFinal = finalIndex >= 0 && events.slice(finalIndex + 1).some((event) => event.type !== "session_close");
|
|
254
|
+
const protocolInvalid = protocolIdentityInvalid || eventAfterFinal;
|
|
255
|
+
const status = sawError || malformed.length > 0 || exitCode !== 0 || timedOut || outputOverflow || finalCount !== 1 || protocolInvalid
|
|
256
|
+
? "failed"
|
|
257
|
+
: finalCompleted === false
|
|
258
|
+
? "blocked"
|
|
259
|
+
: "completed";
|
|
160
260
|
const summary = status === "failed"
|
|
161
261
|
? sawError ||
|
|
162
|
-
`Hermes bridge failed${malformed.length ? `; malformed_frames=${malformed.join(",")}` : ""}${stderr.length ? `; stderr=${stderr.join("").slice(0, 500)}` : ""}`
|
|
163
|
-
:
|
|
262
|
+
`Hermes bridge failed${timedOut ? "; reason_code=hermes_timeout" : ""}${outputOverflow ? "; reason_code=hermes_output_limit" : ""}${finalCount !== 1 ? `; terminal_frames=${finalCount}` : ""}${protocolIdentityInvalid ? "; reason_code=hermes_frame_identity" : ""}${eventAfterFinal ? "; reason_code=hermes_frame_after_final" : ""}${malformed.length ? `; malformed_frames=${malformed.join(",")}` : ""}${stderr.length ? `; stderr=${stderr.join("").slice(0, 500)}` : ""}`
|
|
263
|
+
: status === "blocked"
|
|
264
|
+
? finalText || "Hermes reported completed=false."
|
|
265
|
+
: finalText || "Hermes-local turn completed.";
|
|
164
266
|
return {
|
|
165
267
|
result: {
|
|
166
268
|
bridge_id: sessionId,
|
|
@@ -170,7 +272,7 @@ export class HermesSubprocessExecutor {
|
|
|
170
272
|
turns,
|
|
171
273
|
tool_calls: toolCalls,
|
|
172
274
|
child_results: [],
|
|
173
|
-
evidence_refs:
|
|
275
|
+
evidence_refs: touchedPaths,
|
|
174
276
|
},
|
|
175
277
|
metadata: {
|
|
176
278
|
bridge_protocol_version: HERMES_BRIDGE_PROTOCOL_VERSION,
|
|
@@ -180,6 +282,7 @@ export class HermesSubprocessExecutor {
|
|
|
180
282
|
provider: options.runtime.provider,
|
|
181
283
|
model: options.runtime.model,
|
|
182
284
|
events,
|
|
285
|
+
touched_paths: touchedPaths,
|
|
183
286
|
},
|
|
184
287
|
};
|
|
185
288
|
}
|
package/dist/job-scheduler.d.ts
CHANGED
|
@@ -94,6 +94,12 @@ export interface DispatchJobsInput {
|
|
|
94
94
|
owner?: string;
|
|
95
95
|
lease_ttl_seconds?: number;
|
|
96
96
|
now_iso?: string;
|
|
97
|
+
/**
|
|
98
|
+
* How long a running job may go without a worker heartbeat before the
|
|
99
|
+
* watchdog reaps it into `unknown_recovery`. Defaults to
|
|
100
|
+
* DEFAULT_HEARTBEAT_STALE_MS. Set to <=0 to disable reaping for this tick.
|
|
101
|
+
*/
|
|
102
|
+
heartbeat_stale_seconds?: number;
|
|
97
103
|
}
|
|
98
104
|
export interface DispatchJobsResult {
|
|
99
105
|
ok: boolean;
|
|
@@ -111,6 +117,7 @@ export interface DispatchJobsResult {
|
|
|
111
117
|
running_jobs: number;
|
|
112
118
|
started_jobs: number;
|
|
113
119
|
recovered_jobs: number;
|
|
120
|
+
reaped_jobs: number;
|
|
114
121
|
};
|
|
115
122
|
}
|
|
116
123
|
export interface DispatchJobNowInput {
|
|
@@ -148,6 +155,34 @@ export interface CompleteJobResult {
|
|
|
148
155
|
retry_scheduled_for?: string;
|
|
149
156
|
error?: string;
|
|
150
157
|
}
|
|
158
|
+
export interface HeartbeatJobInput {
|
|
159
|
+
job_id: string;
|
|
160
|
+
owner?: string;
|
|
161
|
+
now_iso?: string;
|
|
162
|
+
lease_ttl_seconds?: number;
|
|
163
|
+
}
|
|
164
|
+
export interface HeartbeatJobResult {
|
|
165
|
+
ok: boolean;
|
|
166
|
+
queue_path: string;
|
|
167
|
+
lock_path: string;
|
|
168
|
+
heartbeat_at?: string;
|
|
169
|
+
resources?: string[];
|
|
170
|
+
error?: string;
|
|
171
|
+
}
|
|
172
|
+
export interface ReapStaleRunningJobsInput {
|
|
173
|
+
owner?: string;
|
|
174
|
+
now_iso?: string;
|
|
175
|
+
heartbeat_stale_seconds?: number;
|
|
176
|
+
}
|
|
177
|
+
export interface ReapStaleRunningJobsResult {
|
|
178
|
+
ok: boolean;
|
|
179
|
+
queue_path: string;
|
|
180
|
+
lock_path: string;
|
|
181
|
+
queue: JobQueueFile;
|
|
182
|
+
locks: JobLockTableFile;
|
|
183
|
+
reaped_jobs: number;
|
|
184
|
+
reaped_job_ids: string[];
|
|
185
|
+
}
|
|
151
186
|
export interface AckJobInput {
|
|
152
187
|
job_id: string;
|
|
153
188
|
action: "accept" | "reject" | "cancel" | "resume";
|
|
@@ -167,9 +202,32 @@ export declare function getSchedulerLeasePath(): string;
|
|
|
167
202
|
export declare function readJobQueue(): JobQueueFile;
|
|
168
203
|
export declare function readJobLockTable(): JobLockTableFile;
|
|
169
204
|
export declare function readSchedulerLease(): SchedulerLeaseFile | undefined;
|
|
205
|
+
/**
|
|
206
|
+
* Default window a running job may go without a worker heartbeat before the
|
|
207
|
+
* watchdog presumes the worker is dead and reaps the job into
|
|
208
|
+
* `unknown_recovery`. A live in-process worker is expected to call
|
|
209
|
+
* `heartbeatJob` well within this window (see job-worker.ts).
|
|
210
|
+
*/
|
|
211
|
+
export declare const DEFAULT_HEARTBEAT_STALE_MS = 120000;
|
|
170
212
|
export declare function enqueueJob(input: EnqueueJobInput): Promise<EnqueueJobResult>;
|
|
171
213
|
export declare function acknowledgeJob(input: AckJobInput): Promise<AckJobResult>;
|
|
172
214
|
export declare function dispatchJobs(input?: DispatchJobsInput): Promise<DispatchJobsResult>;
|
|
173
215
|
export declare function dispatchJobNow(input: DispatchJobNowInput): Promise<DispatchJobNowResult>;
|
|
174
216
|
export declare function completeJob(input: CompleteJobInput): Promise<CompleteJobResult>;
|
|
217
|
+
/**
|
|
218
|
+
* Prove liveness for a running job. A live in-process worker calls this
|
|
219
|
+
* periodically while executing a job body so the heartbeat watchdog does not
|
|
220
|
+
* reap it. Advances the job's active-lock heartbeats and renews the owner
|
|
221
|
+
* lease. This is the ONLY path (besides a fresh start) that advances a running
|
|
222
|
+
* job's heartbeat — the dispatcher deliberately does not, so a missing
|
|
223
|
+
* heartbeat reliably signals a dead worker.
|
|
224
|
+
*/
|
|
225
|
+
export declare function heartbeatJob(input: HeartbeatJobInput): Promise<HeartbeatJobResult>;
|
|
226
|
+
/**
|
|
227
|
+
* Standalone heartbeat-watchdog tick: reap stale running jobs without planning
|
|
228
|
+
* or starting anything. Takes its own scheduler lock. Useful for a dedicated
|
|
229
|
+
* watchdog cadence separate from dispatch. Does not require lease ownership —
|
|
230
|
+
* reaping a dead worker's job must not depend on holding the lease.
|
|
231
|
+
*/
|
|
232
|
+
export declare function reapStaleRunningJobsTick(input?: ReapStaleRunningJobsInput): Promise<ReapStaleRunningJobsResult>;
|
|
175
233
|
//# sourceMappingURL=job-scheduler.d.ts.map
|