@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
|
@@ -1,15 +1,24 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { resolve } from "node:path";
|
|
2
3
|
import { ROLE_ENUM } from "./shared.js";
|
|
3
4
|
import { resolveWorkspaceRoot } from "./helpers.js";
|
|
4
5
|
import { ALL_EXECUTION_ENGINES } from "./helpers/constants.js";
|
|
5
6
|
import { executeAceInternalTool } from "./ace-internal-tools.js";
|
|
6
|
-
import { ModelBridge } from "./model-bridge.js";
|
|
7
|
+
import { isMutationIntent, ModelBridge, verifyCompletionArtifacts } from "./model-bridge.js";
|
|
8
|
+
import { readBridgeTrace } from "./bridge-trace.js";
|
|
9
|
+
import { reflectDeterministic } from "./experience-reflector.js";
|
|
10
|
+
import { curate } from "./playbook-curator.js";
|
|
11
|
+
import { PlaybookRepository } from "./store/repositories/playbook-repository.js";
|
|
12
|
+
import { appendStatusEventSafe } from "./status-events.js";
|
|
7
13
|
import { OllamaClient } from "./tui/ollama.js";
|
|
8
14
|
import { OpenAICompatibleClient, diagnoseChatRuntimeConfig, } from "./tui/openai-compatible.js";
|
|
9
15
|
import { discoverProviderContext } from "./tui/provider-discovery.js";
|
|
10
16
|
import { defaultToolScopeForPolicy, defaultTurnsForPolicy, resolveLocalModelExecutionPolicy, } from "./local-model-policy.js";
|
|
11
17
|
import { HermesSubprocessExecutor, } from "./hermes/session-manager.js";
|
|
12
18
|
import { resolveHermesLaunchProfile, } from "./hermes/launch-profile.js";
|
|
19
|
+
import { buildFilteredToolCatalog } from "./runtime-tool-specs.js";
|
|
20
|
+
import { withLocalModelRuntimeRepository, } from "./store/repositories/local-model-runtime-repository.js";
|
|
21
|
+
import { storeExistsSync } from "./store/store-snapshot.js";
|
|
13
22
|
export function parseExecutionEngine(input) {
|
|
14
23
|
if (!input)
|
|
15
24
|
return undefined;
|
|
@@ -151,8 +160,40 @@ export async function runLocalModelTask(options) {
|
|
|
151
160
|
requested_model_class: options.modelClass,
|
|
152
161
|
});
|
|
153
162
|
const maxTurns = options.maxTurns ?? defaultTurnsForPolicy(role, policy);
|
|
154
|
-
const
|
|
163
|
+
const requestedToolScope = options.toolScope ?? defaultToolScopeForPolicy(role, policy);
|
|
164
|
+
const filteredToolCatalog = buildFilteredToolCatalog(policy.model_class, requestedToolScope, {
|
|
165
|
+
mutation_lane: policy.mutation_lane,
|
|
166
|
+
});
|
|
167
|
+
const filteredToolScope = filteredToolCatalog.entries.map((entry) => entry.name);
|
|
168
|
+
const toolScope = filteredToolScope.length > 0 ? filteredToolScope : requestedToolScope;
|
|
155
169
|
if (runtime.executionEngine === "hermes_local") {
|
|
170
|
+
if (toolScope.length === 0) {
|
|
171
|
+
throw new Error("hermes_local requires a non-empty tool scope (reason_code=hermes_empty_tool_scope)");
|
|
172
|
+
}
|
|
173
|
+
const capabilitySnapshotId = randomUUID();
|
|
174
|
+
if (storeExistsSync(runtime.workspaceRoot)) {
|
|
175
|
+
await withLocalModelRuntimeRepository(runtime.workspaceRoot, async (repo) => {
|
|
176
|
+
await repo.saveCapabilitySnapshot({
|
|
177
|
+
snapshot_id: capabilitySnapshotId,
|
|
178
|
+
session_id: `hermes-local-${capabilitySnapshotId}`,
|
|
179
|
+
turn_number: 1,
|
|
180
|
+
captured_at: Date.now(),
|
|
181
|
+
model_class: policy.model_class,
|
|
182
|
+
allowed_tools: toolScope,
|
|
183
|
+
unavailable_tools: filteredToolCatalog.unavailable_tools,
|
|
184
|
+
tool_cost_class: filteredToolCatalog.tool_cost_class,
|
|
185
|
+
});
|
|
186
|
+
await repo.appendTransitionRecord({
|
|
187
|
+
session_id: `hermes-local-${capabilitySnapshotId}`,
|
|
188
|
+
subject_kind: "capability",
|
|
189
|
+
subject_id: `hermes_local/${capabilitySnapshotId}`,
|
|
190
|
+
from: "unscoped",
|
|
191
|
+
to: `model_class:${policy.model_class}`,
|
|
192
|
+
reason: "Capability snapshot captured for Hermes-local turn (surface_kind=hermes_local)",
|
|
193
|
+
evidence_refs: [capabilitySnapshotId],
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
156
197
|
const launchProfile = options.hermesLaunchProfile ?? resolveHermesLaunchProfile({ workspaceRoot: runtime.workspaceRoot });
|
|
157
198
|
const hermesExecutor = options.hermesExecutor ??
|
|
158
199
|
new HermesSubprocessExecutor({
|
|
@@ -165,6 +206,24 @@ export async function runLocalModelTask(options) {
|
|
|
165
206
|
policy,
|
|
166
207
|
toolScope,
|
|
167
208
|
maxTurns,
|
|
209
|
+
timeoutMs: options.timeoutMs,
|
|
210
|
+
});
|
|
211
|
+
hermes.result = await verifyCompletionArtifacts(hermes.result, {
|
|
212
|
+
task: options.task,
|
|
213
|
+
role,
|
|
214
|
+
workspace: runtime.workspaceRoot,
|
|
215
|
+
touchedPaths: hermes.metadata.touched_paths ?? [],
|
|
216
|
+
expectedArtifacts: options.expectedArtifacts,
|
|
217
|
+
requiredEvidenceRefs: options.requiredEvidenceRefs,
|
|
218
|
+
forceCompletionVerification: (role === "coders" || role === "builder") &&
|
|
219
|
+
isMutationIntent(options.task),
|
|
220
|
+
});
|
|
221
|
+
await reflectRunIntoPlaybook({
|
|
222
|
+
workspaceRoot: runtime.workspaceRoot,
|
|
223
|
+
result: hermes.result,
|
|
224
|
+
role,
|
|
225
|
+
model: runtime.model,
|
|
226
|
+
modelClass: policy.model_class,
|
|
168
227
|
});
|
|
169
228
|
return {
|
|
170
229
|
runtime,
|
|
@@ -183,8 +242,16 @@ export async function runLocalModelTask(options) {
|
|
|
183
242
|
maxTurns,
|
|
184
243
|
provider: runtime.provider,
|
|
185
244
|
model: runtime.model,
|
|
245
|
+
modelClass: policy.model_class,
|
|
186
246
|
toolScope,
|
|
187
247
|
});
|
|
248
|
+
await reflectRunIntoPlaybook({
|
|
249
|
+
workspaceRoot: runtime.workspaceRoot,
|
|
250
|
+
result,
|
|
251
|
+
role,
|
|
252
|
+
model: runtime.model,
|
|
253
|
+
modelClass: policy.model_class,
|
|
254
|
+
});
|
|
188
255
|
return {
|
|
189
256
|
runtime,
|
|
190
257
|
role,
|
|
@@ -193,5 +260,66 @@ export async function runLocalModelTask(options) {
|
|
|
193
260
|
result,
|
|
194
261
|
};
|
|
195
262
|
}
|
|
263
|
+
async function reflectRunIntoPlaybook(input) {
|
|
264
|
+
if (process.env.ACE_PLAYBOOK_REFLECT === "0" || process.env.ACE_PLAYBOOK_REFLECT === "false") {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const trace = readBridgeTrace(input.workspaceRoot, input.result.bridge_id);
|
|
269
|
+
const repo = new PlaybookRepository(input.workspaceRoot);
|
|
270
|
+
const ops = reflectDeterministic({
|
|
271
|
+
trace,
|
|
272
|
+
result: input.result,
|
|
273
|
+
role: input.role,
|
|
274
|
+
model: input.model,
|
|
275
|
+
modelClass: input.modelClass,
|
|
276
|
+
});
|
|
277
|
+
if (ops.length > 0) {
|
|
278
|
+
await curate(repo, ops);
|
|
279
|
+
}
|
|
280
|
+
const attributionOps = buildImplicitAttributionOps(repo, input.result);
|
|
281
|
+
if (attributionOps.length > 0) {
|
|
282
|
+
await curate(repo, attributionOps);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await appendStatusEventSafe({
|
|
287
|
+
source_module: "capability-build",
|
|
288
|
+
event_type: "PLAYBOOK_REFLECT_FAILED",
|
|
289
|
+
status: "fail",
|
|
290
|
+
summary: `Playbook reflection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
291
|
+
payload: {
|
|
292
|
+
bridge_id: input.result.bridge_id,
|
|
293
|
+
role: input.role,
|
|
294
|
+
model_class: input.modelClass,
|
|
295
|
+
},
|
|
296
|
+
}).catch(() => undefined);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function buildImplicitAttributionOps(repo, result) {
|
|
300
|
+
const injected = result.injected_bullets ?? [];
|
|
301
|
+
if (injected.length === 0)
|
|
302
|
+
return [];
|
|
303
|
+
if ((result.used_bullets ?? []).length > 0) {
|
|
304
|
+
const injectedSet = new Set(injected);
|
|
305
|
+
return [...new Set(result.used_bullets ?? [])]
|
|
306
|
+
.filter((bullet_id) => injectedSet.has(bullet_id))
|
|
307
|
+
.map((bullet_id) => ({ type: "TAG", bullet_id, tag: "helpful" }));
|
|
308
|
+
}
|
|
309
|
+
const completedWithEvidence = result.status === "completed" &&
|
|
310
|
+
((result.evidence_refs ?? []).length > 0 || (result.tool_calls ?? []).some((tool) => tool.ok));
|
|
311
|
+
if (completedWithEvidence) {
|
|
312
|
+
return injected.map((bullet_id) => ({ type: "TAG", bullet_id, tag: "helpful" }));
|
|
313
|
+
}
|
|
314
|
+
if (result.status === "blocked" && result.reason_code) {
|
|
315
|
+
return injected.flatMap((bullet_id) => {
|
|
316
|
+
const bullet = repo.getBullet(bullet_id);
|
|
317
|
+
return bullet?.scope.reason_code === result.reason_code
|
|
318
|
+
? [{ type: "TAG", bullet_id, tag: "harmful" }]
|
|
319
|
+
: [];
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
196
324
|
export { resolveLocalModelExecutionPolicy, resolveLocalModelClass } from "./local-model-policy.js";
|
|
197
325
|
//# sourceMappingURL=local-model-runtime.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measured model classing (E10 / D8).
|
|
3
|
+
*
|
|
4
|
+
* Name-regex classing misfires (gpt-oss→frontier, qwen2.5-coder→small). The
|
|
5
|
+
* question ACE actually cares about is "can this model hold the envelope
|
|
6
|
+
* contract?" — which is directly measurable with a one-shot canary probe at
|
|
7
|
+
* `ace connect`/`doctor`: ask for a fixed envelope and score compliance. This
|
|
8
|
+
* module compiles a {json_compliance, measured_ctx, param_count_b} measurement
|
|
9
|
+
* into a class, persists it per (provider, model), and lets the policy consult
|
|
10
|
+
* measurements first and the regex only as a fallback.
|
|
11
|
+
*
|
|
12
|
+
* The probe's model call is injected, so scoring/classing/persistence are all
|
|
13
|
+
* unit-testable without a live model.
|
|
14
|
+
*/
|
|
15
|
+
import type { RuntimeModelClass } from "./runtime-profile.js";
|
|
16
|
+
/** Fixed canary: the smallest possible well-formed envelope request. */
|
|
17
|
+
export declare const CANARY_PROMPT: string;
|
|
18
|
+
export interface CapabilityMeasurement {
|
|
19
|
+
provider: string;
|
|
20
|
+
model: string;
|
|
21
|
+
/** 0..1 — how well the model emitted the requested envelope. */
|
|
22
|
+
json_compliance: number;
|
|
23
|
+
/** Measured/declared context window in tokens, if probed. */
|
|
24
|
+
measured_ctx?: number;
|
|
25
|
+
/** Parameter count in billions, if probed from provider metadata. */
|
|
26
|
+
param_count_b?: number;
|
|
27
|
+
measured_at: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Score a raw model output against the canary contract. Full credit for a
|
|
31
|
+
* clean parse of the exact envelope; partial credit for a JSON object with the
|
|
32
|
+
* right status field embedded in prose (the parser tolerates leading text);
|
|
33
|
+
* zero for no recoverable JSON.
|
|
34
|
+
*/
|
|
35
|
+
export declare function scoreCanaryCompliance(raw: string): number;
|
|
36
|
+
/**
|
|
37
|
+
* Compile a measurement into a class. Compliance dominates: a model that cannot
|
|
38
|
+
* hold the envelope is `small_local` regardless of its parameter count (that is
|
|
39
|
+
* the whole point — measured behavior over metadata). Otherwise parameter count
|
|
40
|
+
* decides, with the context window as a tie-break toward `mid`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function classFromMeasurement(m: Pick<CapabilityMeasurement, "json_compliance" | "measured_ctx" | "param_count_b">): RuntimeModelClass | undefined;
|
|
43
|
+
export interface ProbeCanaryInput {
|
|
44
|
+
provider: string;
|
|
45
|
+
model: string;
|
|
46
|
+
/** Injected model call: returns the raw text the model produced for a prompt. */
|
|
47
|
+
call: (prompt: string) => Promise<string>;
|
|
48
|
+
measured_ctx?: number;
|
|
49
|
+
param_count_b?: number;
|
|
50
|
+
}
|
|
51
|
+
/** Run the canary probe and produce a measurement (does not persist). */
|
|
52
|
+
export declare function probeModelCanary(input: ProbeCanaryInput): Promise<CapabilityMeasurement>;
|
|
53
|
+
export declare function saveMeasurement(workspaceRoot: string, measurement: CapabilityMeasurement): void;
|
|
54
|
+
export declare function getMeasurement(workspaceRoot: string, provider: string, model: string): CapabilityMeasurement | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Class for a (provider, model), consulting a persisted measurement first and
|
|
57
|
+
* the name regex only as a fallback. This is the E10 entry point callers use
|
|
58
|
+
* instead of `resolveLocalModelClass` directly when a workspace is available.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveClassMeasuredFirst(input: {
|
|
61
|
+
provider: string;
|
|
62
|
+
model: string;
|
|
63
|
+
workspaceRoot: string;
|
|
64
|
+
requested_model_class?: RuntimeModelClass;
|
|
65
|
+
}): Promise<RuntimeModelClass>;
|
|
66
|
+
//# sourceMappingURL=measured-classing.d.ts.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measured model classing (E10 / D8).
|
|
3
|
+
*
|
|
4
|
+
* Name-regex classing misfires (gpt-oss→frontier, qwen2.5-coder→small). The
|
|
5
|
+
* question ACE actually cares about is "can this model hold the envelope
|
|
6
|
+
* contract?" — which is directly measurable with a one-shot canary probe at
|
|
7
|
+
* `ace connect`/`doctor`: ask for a fixed envelope and score compliance. This
|
|
8
|
+
* module compiles a {json_compliance, measured_ctx, param_count_b} measurement
|
|
9
|
+
* into a class, persists it per (provider, model), and lets the policy consult
|
|
10
|
+
* measurements first and the regex only as a fallback.
|
|
11
|
+
*
|
|
12
|
+
* The probe's model call is injected, so scoring/classing/persistence are all
|
|
13
|
+
* unit-testable without a live model.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
/** Fixed canary: the smallest possible well-formed envelope request. */
|
|
19
|
+
export const CANARY_PROMPT = 'Respond with exactly one JSON object and nothing else: ' +
|
|
20
|
+
'{"status":"message","thinking":"ok","message":"canary"}';
|
|
21
|
+
/**
|
|
22
|
+
* Score a raw model output against the canary contract. Full credit for a
|
|
23
|
+
* clean parse of the exact envelope; partial credit for a JSON object with the
|
|
24
|
+
* right status field embedded in prose (the parser tolerates leading text);
|
|
25
|
+
* zero for no recoverable JSON.
|
|
26
|
+
*/
|
|
27
|
+
export function scoreCanaryCompliance(raw) {
|
|
28
|
+
const text = (raw ?? "").trim();
|
|
29
|
+
if (!text)
|
|
30
|
+
return 0;
|
|
31
|
+
// Exact-ish: the whole output is one JSON object with status:"message".
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(text);
|
|
34
|
+
if (parsed && typeof parsed === "object" && parsed.status === "message") {
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
if (parsed && typeof parsed === "object" && typeof parsed.status === "string") {
|
|
38
|
+
return 0.75;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// fall through to brace-scan
|
|
43
|
+
}
|
|
44
|
+
// Tolerant: a JSON object is embedded in surrounding prose.
|
|
45
|
+
const start = text.indexOf("{");
|
|
46
|
+
const end = text.lastIndexOf("}");
|
|
47
|
+
if (start >= 0 && end > start) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
50
|
+
if (parsed && typeof parsed === "object" && typeof parsed.status === "string") {
|
|
51
|
+
return 0.5;
|
|
52
|
+
}
|
|
53
|
+
return 0.3;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return 0.1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compile a measurement into a class. Compliance dominates: a model that cannot
|
|
63
|
+
* hold the envelope is `small_local` regardless of its parameter count (that is
|
|
64
|
+
* the whole point — measured behavior over metadata). Otherwise parameter count
|
|
65
|
+
* decides, with the context window as a tie-break toward `mid`.
|
|
66
|
+
*/
|
|
67
|
+
export function classFromMeasurement(m) {
|
|
68
|
+
const compliance = clamp01(m.json_compliance);
|
|
69
|
+
// Below half-compliance the model cannot be trusted with the tool envelope.
|
|
70
|
+
if (compliance < 0.5)
|
|
71
|
+
return "small_local";
|
|
72
|
+
const params = m.param_count_b;
|
|
73
|
+
if (typeof params === "number" && Number.isFinite(params) && params > 0) {
|
|
74
|
+
if (params >= 30 && compliance >= 0.8)
|
|
75
|
+
return "frontier";
|
|
76
|
+
if (params >= 13)
|
|
77
|
+
return "frontier";
|
|
78
|
+
if (params >= 7)
|
|
79
|
+
return "mid";
|
|
80
|
+
return "small_local";
|
|
81
|
+
}
|
|
82
|
+
// No parameter metadata: fall back on compliance + context.
|
|
83
|
+
if (compliance >= 0.9 && (m.measured_ctx ?? 0) >= 32_768)
|
|
84
|
+
return "frontier";
|
|
85
|
+
if (compliance >= 0.75)
|
|
86
|
+
return "mid";
|
|
87
|
+
return "small_local";
|
|
88
|
+
}
|
|
89
|
+
/** Run the canary probe and produce a measurement (does not persist). */
|
|
90
|
+
export async function probeModelCanary(input) {
|
|
91
|
+
let raw = "";
|
|
92
|
+
try {
|
|
93
|
+
raw = await input.call(CANARY_PROMPT);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
raw = "";
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
provider: input.provider,
|
|
100
|
+
model: input.model,
|
|
101
|
+
json_compliance: scoreCanaryCompliance(raw),
|
|
102
|
+
measured_ctx: input.measured_ctx,
|
|
103
|
+
param_count_b: input.param_count_b,
|
|
104
|
+
measured_at: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function measurementPath(workspaceRoot) {
|
|
108
|
+
return join(workspaceRoot, "agent-state", "model-measurements.json");
|
|
109
|
+
}
|
|
110
|
+
function measurementKey(provider, model) {
|
|
111
|
+
return `${provider.trim().toLowerCase()}::${model.trim().toLowerCase()}`;
|
|
112
|
+
}
|
|
113
|
+
function readMeasurementFile(workspaceRoot) {
|
|
114
|
+
const path = measurementPath(workspaceRoot);
|
|
115
|
+
if (!existsSync(path))
|
|
116
|
+
return { version: 1, updated_at: new Date().toISOString(), measurements: [] };
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
119
|
+
if (parsed.version === 1 && Array.isArray(parsed.measurements))
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// fall through to default
|
|
124
|
+
}
|
|
125
|
+
return { version: 1, updated_at: new Date().toISOString(), measurements: [] };
|
|
126
|
+
}
|
|
127
|
+
export function saveMeasurement(workspaceRoot, measurement) {
|
|
128
|
+
const dir = join(workspaceRoot, "agent-state");
|
|
129
|
+
if (!existsSync(dir))
|
|
130
|
+
mkdirSync(dir, { recursive: true });
|
|
131
|
+
const file = readMeasurementFile(workspaceRoot);
|
|
132
|
+
const key = measurementKey(measurement.provider, measurement.model);
|
|
133
|
+
const next = file.measurements.filter((m) => measurementKey(m.provider, m.model) !== key);
|
|
134
|
+
next.push(measurement);
|
|
135
|
+
const path = measurementPath(workspaceRoot);
|
|
136
|
+
const tmp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
137
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, updated_at: new Date().toISOString(), measurements: next }, null, 2));
|
|
138
|
+
renameSync(tmp, path);
|
|
139
|
+
}
|
|
140
|
+
export function getMeasurement(workspaceRoot, provider, model) {
|
|
141
|
+
const key = measurementKey(provider, model);
|
|
142
|
+
return readMeasurementFile(workspaceRoot).measurements.find((m) => measurementKey(m.provider, m.model) === key);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Class for a (provider, model), consulting a persisted measurement first and
|
|
146
|
+
* the name regex only as a fallback. This is the E10 entry point callers use
|
|
147
|
+
* instead of `resolveLocalModelClass` directly when a workspace is available.
|
|
148
|
+
*/
|
|
149
|
+
export async function resolveClassMeasuredFirst(input) {
|
|
150
|
+
const { resolveLocalModelClass } = await import("./local-model-policy.js");
|
|
151
|
+
const measurement = getMeasurement(input.workspaceRoot, input.provider, input.model);
|
|
152
|
+
const measured = measurement ? classFromMeasurement(measurement) : undefined;
|
|
153
|
+
return resolveLocalModelClass({
|
|
154
|
+
provider: input.provider,
|
|
155
|
+
model: input.model,
|
|
156
|
+
requested_model_class: input.requested_model_class,
|
|
157
|
+
measured_model_class: measured,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function clamp01(n) {
|
|
161
|
+
if (!Number.isFinite(n))
|
|
162
|
+
return 0;
|
|
163
|
+
return Math.max(0, Math.min(1, n));
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=measured-classing.js.map
|
package/dist/model-bridge.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface BridgeToolResult {
|
|
|
10
10
|
ok: boolean;
|
|
11
11
|
summary: string;
|
|
12
12
|
isError: boolean;
|
|
13
|
+
/** Set when a tool fails for a classified reason (e.g. "tool_timeout"). */
|
|
14
|
+
reason_code?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface BridgeUsageTotals {
|
|
17
|
+
prompt_tokens: number;
|
|
18
|
+
completion_tokens: number;
|
|
13
19
|
}
|
|
14
20
|
export interface BridgeProgressEvent {
|
|
15
21
|
kind: "model_chunk" | "tool_start" | "tool_finish" | "process_post" | "output" | "thinking";
|
|
@@ -26,11 +32,16 @@ export interface BridgeResult {
|
|
|
26
32
|
child_results: BridgeResult[];
|
|
27
33
|
evidence_refs?: string[];
|
|
28
34
|
reason_code?: string;
|
|
35
|
+
usage_totals?: BridgeUsageTotals;
|
|
36
|
+
injected_bullets?: string[];
|
|
37
|
+
used_bullets?: string[];
|
|
29
38
|
}
|
|
30
39
|
export interface ModelBridgeClients {
|
|
31
40
|
ollama: Pick<OllamaClient, "chat" | "abort">;
|
|
32
41
|
openai: Pick<OpenAICompatibleClient, "chat" | "abort">;
|
|
33
42
|
}
|
|
43
|
+
/** Keep completion-evidence policy identical across direct and Hermes execution. */
|
|
44
|
+
export declare function isMutationIntent(task: string): boolean;
|
|
34
45
|
export interface ModelBridgeRunOptions {
|
|
35
46
|
task: string;
|
|
36
47
|
role: string;
|
|
@@ -40,11 +51,13 @@ export interface ModelBridgeRunOptions {
|
|
|
40
51
|
maxTurns: number;
|
|
41
52
|
provider: string;
|
|
42
53
|
model: string;
|
|
54
|
+
modelClass?: string;
|
|
43
55
|
toolScope?: string[];
|
|
44
56
|
expectedArtifacts?: {
|
|
45
57
|
path: string;
|
|
46
58
|
required?: boolean;
|
|
47
59
|
}[];
|
|
60
|
+
requiredEvidenceRefs?: string[];
|
|
48
61
|
parentBridge?: string;
|
|
49
62
|
onToolCall?: (tool: string, args: Record<string, unknown>) => void;
|
|
50
63
|
onToolResult?: (tool: string, result: BridgeToolResult) => void;
|
|
@@ -52,6 +65,38 @@ export interface ModelBridgeRunOptions {
|
|
|
52
65
|
onThinking?: (text: string) => void;
|
|
53
66
|
onProgress?: (event: BridgeProgressEvent) => void;
|
|
54
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* JSON Schema for the ACE response envelope. Passed as `format` (Ollama ≥ 0.5) or
|
|
70
|
+
* `response_format.json_schema` (OpenAI-compatible) to enable schema-constrained
|
|
71
|
+
* generation. Falls back to plain json-mode if the provider rejects it with a 400.
|
|
72
|
+
*/
|
|
73
|
+
export declare const ACE_ENVELOPE_JSON_SCHEMA: Record<string, unknown>;
|
|
74
|
+
export declare function verifyCompletionArtifacts(result: BridgeResult, context: {
|
|
75
|
+
task: string;
|
|
76
|
+
role: string;
|
|
77
|
+
workspace: string;
|
|
78
|
+
touchedPaths: string[];
|
|
79
|
+
expectedArtifacts?: {
|
|
80
|
+
path: string;
|
|
81
|
+
required?: boolean;
|
|
82
|
+
}[];
|
|
83
|
+
requiredEvidenceRefs?: string[];
|
|
84
|
+
forceCompletionVerification?: boolean;
|
|
85
|
+
}): Promise<BridgeResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Per-tool-call deadline (E12). A hung validation/test subprocess must never
|
|
88
|
+
* wedge a turn indefinitely — and under Promise.all one stuck call also holds
|
|
89
|
+
* every parallel sibling's feedback hostage. On timeout we resolve a classified
|
|
90
|
+
* `tool_timeout` failure that then feeds the WS-C doom-loop brake.
|
|
91
|
+
*/
|
|
92
|
+
export declare function executeToolWithDeadline(tool: string, args: Record<string, unknown>, sessionId: string, opts: {
|
|
93
|
+
workspace_path: string;
|
|
94
|
+
}, deadlineMs: number, exec?: (tool: string, args: Record<string, unknown>, sessionId: string, opts: {
|
|
95
|
+
workspace_path: string;
|
|
96
|
+
}) => Promise<unknown>): Promise<{
|
|
97
|
+
rawToolResult: unknown;
|
|
98
|
+
timedOut: boolean;
|
|
99
|
+
}>;
|
|
55
100
|
export declare class ModelBridge {
|
|
56
101
|
private clients;
|
|
57
102
|
private activeProviderClient;
|