@voybio/ace-swarm 0.2.5 → 2.4.1
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 +19 -1
- package/README.md +21 -13
- package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
- package/assets/agent-state/EVIDENCE_LOG.md +1 -1
- package/assets/agent-state/MODULES/roles/capability-framework.json +41 -0
- package/assets/agent-state/MODULES/roles/capability-git.json +33 -0
- package/assets/agent-state/MODULES/roles/capability-safety.json +37 -0
- package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +21 -0
- package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +43 -0
- package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
- package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +11 -0
- package/assets/agent-state/STATUS.md +2 -2
- 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/assets/scripts/ace-hook-dispatch.mjs +70 -6
- package/assets/scripts/render-mcp-configs.sh +19 -5
- package/dist/ace-context.js +91 -11
- package/dist/ace-internal-tools.d.ts +3 -1
- package/dist/ace-internal-tools.js +10 -2
- package/dist/ace-server-instructions.js +3 -3
- package/dist/ace-state-resolver.js +5 -3
- 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 +57 -1
- package/dist/astgrep-index.js +140 -4
- package/dist/cli.js +232 -35
- package/dist/discovery-runtime-wrappers.d.ts +108 -0
- package/dist/discovery-runtime-wrappers.js +615 -0
- package/dist/handoff-registry.js +5 -5
- package/dist/helpers/artifacts.d.ts +19 -0
- package/dist/helpers/artifacts.js +152 -0
- package/dist/helpers/bootstrap.d.ts +24 -0
- package/dist/helpers/bootstrap.js +894 -0
- package/dist/helpers/constants.d.ts +53 -0
- package/dist/helpers/constants.js +295 -0
- package/dist/helpers/drift.d.ts +13 -0
- package/dist/helpers/drift.js +45 -0
- package/dist/helpers/path-utils.d.ts +24 -0
- package/dist/helpers/path-utils.js +123 -0
- package/dist/helpers/store-resolution.d.ts +19 -0
- package/dist/helpers/store-resolution.js +305 -0
- package/dist/helpers/workspace-root.d.ts +3 -0
- package/dist/helpers/workspace-root.js +80 -0
- package/dist/helpers.d.ts +8 -125
- package/dist/helpers.js +8 -1768
- package/dist/job-scheduler.js +33 -7
- 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 +6 -0
- package/dist/local-model-runtime.js +33 -21
- package/dist/model-bridge.d.ts +13 -1
- package/dist/model-bridge.js +410 -23
- package/dist/orchestrator-supervisor.d.ts +56 -0
- package/dist/orchestrator-supervisor.js +179 -1
- package/dist/plan-proposal.d.ts +115 -0
- package/dist/plan-proposal.js +1073 -0
- package/dist/run-ledger.js +3 -3
- package/dist/runtime-command.d.ts +8 -0
- package/dist/runtime-command.js +38 -6
- package/dist/runtime-executor.d.ts +20 -1
- package/dist/runtime-executor.js +737 -172
- package/dist/runtime-profile.d.ts +32 -0
- package/dist/runtime-profile.js +89 -13
- package/dist/runtime-tool-specs.d.ts +39 -0
- package/dist/runtime-tool-specs.js +144 -28
- package/dist/safe-edit.d.ts +7 -0
- package/dist/safe-edit.js +163 -37
- package/dist/schemas.js +48 -1
- package/dist/server.js +51 -0
- package/dist/shared.d.ts +3 -2
- package/dist/shared.js +2 -0
- package/dist/status-events.js +9 -6
- package/dist/store/ace-packed-store.d.ts +3 -2
- package/dist/store/ace-packed-store.js +188 -110
- package/dist/store/bootstrap-store.d.ts +2 -1
- package/dist/store/bootstrap-store.js +102 -83
- package/dist/store/cache-workspace.js +11 -5
- package/dist/store/materializers/context-snapshot-materializer.js +6 -2
- package/dist/store/materializers/hook-context-materializer.d.ts +6 -9
- package/dist/store/materializers/hook-context-materializer.js +11 -21
- package/dist/store/materializers/host-file-materializer.js +6 -0
- package/dist/store/materializers/projection-manager.d.ts +0 -1
- package/dist/store/materializers/projection-manager.js +5 -13
- package/dist/store/materializers/scheduler-projection-materializer.js +1 -1
- package/dist/store/materializers/vericify-projector.d.ts +7 -7
- package/dist/store/materializers/vericify-projector.js +11 -11
- package/dist/store/repositories/local-model-runtime-repository.d.ts +120 -3
- package/dist/store/repositories/local-model-runtime-repository.js +242 -6
- package/dist/store/repositories/vericify-repository.d.ts +1 -1
- package/dist/store/skills-install.d.ts +4 -0
- package/dist/store/skills-install.js +21 -12
- package/dist/store/state-reader.d.ts +2 -0
- package/dist/store/state-reader.js +20 -0
- package/dist/store/store-artifacts.d.ts +7 -0
- package/dist/store/store-artifacts.js +27 -1
- package/dist/store/store-authority-audit.d.ts +18 -1
- package/dist/store/store-authority-audit.js +115 -5
- package/dist/store/store-snapshot.d.ts +3 -0
- package/dist/store/store-snapshot.js +22 -2
- package/dist/store/workspace-store-paths.d.ts +39 -0
- package/dist/store/workspace-store-paths.js +94 -0
- package/dist/store/write-coordinator.d.ts +65 -0
- package/dist/store/write-coordinator.js +386 -0
- package/dist/todo-state.js +5 -5
- package/dist/tools-agent.d.ts +20 -0
- package/dist/tools-agent.js +789 -25
- package/dist/tools-discovery.js +136 -1
- package/dist/tools-files.d.ts +7 -0
- package/dist/tools-files.js +1002 -11
- package/dist/tools-framework.js +105 -66
- package/dist/tools-handoff.js +2 -2
- package/dist/tools-lifecycle.js +4 -4
- package/dist/tools-memory.js +6 -6
- package/dist/tools-todo.js +2 -2
- package/dist/tracker-adapters.d.ts +1 -1
- package/dist/tracker-adapters.js +13 -18
- package/dist/tracker-sync.js +5 -3
- package/dist/tui/agent-runner.js +3 -1
- package/dist/tui/chat.js +103 -7
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/tui/dashboard.js +43 -0
- package/dist/tui/index.js +10 -1
- package/dist/tui/layout.d.ts +20 -0
- package/dist/tui/layout.js +31 -1
- package/dist/tui/local-model-contract.d.ts +6 -2
- package/dist/tui/local-model-contract.js +16 -3
- 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 +35 -11
- package/dist/vericify-bridge.d.ts +6 -1
- package/dist/vericify-bridge.js +27 -3
- package/dist/workspace-manager.d.ts +30 -3
- package/dist/workspace-manager.js +257 -27
- package/package.json +1 -2
- package/dist/internal-tool-runtime.d.ts +0 -21
- package/dist/internal-tool-runtime.js +0 -136
- package/dist/store/workspace-snapshot.d.ts +0 -26
- package/dist/store/workspace-snapshot.js +0 -107
package/dist/model-bridge.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
4
4
|
import { executeAceInternalTool, listAceInternalToolCatalog, } from "./ace-internal-tools.js";
|
|
5
5
|
import { appendVericifyProcessPostSafe, deriveWorkspaceVericifyRunRef, } from "./vericify-bridge.js";
|
|
6
6
|
import { buildToolPlan, renderAceContext } from "./ace-context.js";
|
|
7
|
+
import { appendRunLedgerEntrySafe } from "./run-ledger.js";
|
|
8
|
+
import { appendStatusEventSafe } from "./status-events.js";
|
|
9
|
+
import { normalizeRelPath } from "./helpers.js";
|
|
10
|
+
import { sanitizeJsonLikeText } from "./json-sanitizer.js";
|
|
11
|
+
/**
|
|
12
|
+
* Roles that MUST produce a valid JSON envelope in every response.
|
|
13
|
+
* A parse_error from these roles is a contract failure, not a plain-text fallback.
|
|
14
|
+
* The bridge will inject a repair prompt (up to MAX_PARSE_REPAIR_ATTEMPTS) before
|
|
15
|
+
* marking the run as failed with status "role_contract_violation".
|
|
16
|
+
*/
|
|
17
|
+
const ROLES_REQUIRING_JSON_ENVELOPE = new Set(["coders", "builder", "qa"]);
|
|
18
|
+
const VALID_ENVELOPE_STATUSES = new Set(["tool", "message", "complete", "need_input"]);
|
|
19
|
+
const MAX_PARSE_REPAIR_ATTEMPTS = 2;
|
|
20
|
+
const MAX_OUTPUT_DRIFT_REPAIRS = 1;
|
|
7
21
|
function resolveProviderClient(input) {
|
|
8
22
|
const display = input.trim() || "ollama";
|
|
9
23
|
const normalized = display.toLowerCase();
|
|
@@ -11,8 +25,11 @@ function resolveProviderClient(input) {
|
|
|
11
25
|
? { display, client: "ollama" }
|
|
12
26
|
: { display, client: "openai-compatible" };
|
|
13
27
|
}
|
|
28
|
+
function sanitizeModelOutput(raw) {
|
|
29
|
+
return sanitizeJsonLikeText(raw).text;
|
|
30
|
+
}
|
|
14
31
|
function extractJsonEnvelope(raw) {
|
|
15
|
-
const trimmed = raw.trim();
|
|
32
|
+
const trimmed = sanitizeModelOutput(raw).trim();
|
|
16
33
|
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
17
34
|
return trimmed;
|
|
18
35
|
}
|
|
@@ -29,11 +46,11 @@ function extractJsonEnvelope(raw) {
|
|
|
29
46
|
let escape = false;
|
|
30
47
|
for (let index = firstBrace; index < trimmed.length; index += 1) {
|
|
31
48
|
const ch = trimmed[index];
|
|
32
|
-
if (escape) {
|
|
49
|
+
if (inString && escape) {
|
|
33
50
|
escape = false;
|
|
34
51
|
continue;
|
|
35
52
|
}
|
|
36
|
-
if (ch === "\\") {
|
|
53
|
+
if (inString && ch === "\\") {
|
|
37
54
|
escape = true;
|
|
38
55
|
continue;
|
|
39
56
|
}
|
|
@@ -61,18 +78,60 @@ function parseEnvelope(raw) {
|
|
|
61
78
|
if (!parsed || typeof parsed !== "object") {
|
|
62
79
|
throw new Error("response is not an object");
|
|
63
80
|
}
|
|
64
|
-
if (!parsed.status) {
|
|
65
|
-
throw new Error("missing status");
|
|
81
|
+
if (typeof parsed.status !== "string" || !VALID_ENVELOPE_STATUSES.has(parsed.status)) {
|
|
82
|
+
throw new Error("missing or invalid status");
|
|
83
|
+
}
|
|
84
|
+
if (parsed.tool_calls !== undefined && !Array.isArray(parsed.tool_calls)) {
|
|
85
|
+
throw new Error("tool_calls must be an array");
|
|
86
|
+
}
|
|
87
|
+
if (parsed.evidence_refs !== undefined) {
|
|
88
|
+
if (!Array.isArray(parsed.evidence_refs)) {
|
|
89
|
+
throw new Error("evidence_refs must be an array");
|
|
90
|
+
}
|
|
91
|
+
parsed.evidence_refs = parsed.evidence_refs
|
|
92
|
+
.filter((ref) => typeof ref === "string")
|
|
93
|
+
.map((ref) => ref.trim())
|
|
94
|
+
.filter(Boolean);
|
|
66
95
|
}
|
|
67
96
|
return parsed;
|
|
68
97
|
}
|
|
69
98
|
catch {
|
|
70
99
|
return {
|
|
71
|
-
status: "
|
|
72
|
-
message: raw.trim(),
|
|
100
|
+
status: "parse_error",
|
|
101
|
+
message: summarizeSnippet(sanitizeModelOutput(raw).trim() || "[empty response]", 240),
|
|
73
102
|
};
|
|
74
103
|
}
|
|
75
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Validates the semantic shape of a completed bridge output against role-specific
|
|
107
|
+
* output contracts. Returns a violation description if drift is detected, or null
|
|
108
|
+
* if the output is clean.
|
|
109
|
+
*
|
|
110
|
+
* Thresholds are conservative to avoid false positives on legitimate inline snippets.
|
|
111
|
+
*/
|
|
112
|
+
function checkOutputShapeDrift(role, text, _toolResults) {
|
|
113
|
+
if (role === "qa") {
|
|
114
|
+
// qa must return a short structured verdict, not a rewritten artifact.
|
|
115
|
+
// Large code fences in the output strongly suggest artifact rewriting.
|
|
116
|
+
const codeBlocks = [...text.matchAll(/```[\s\S]*?```/g)];
|
|
117
|
+
const totalCodeChars = codeBlocks.reduce((sum, match) => sum + match[0].length, 0);
|
|
118
|
+
if (totalCodeChars > 800) {
|
|
119
|
+
return ("output contains large code blocks — qa must return a short structured verdict, " +
|
|
120
|
+
"not a rewritten artifact. Include a one-paragraph verdict and failure classification, " +
|
|
121
|
+
"not the full file content.");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (role === "vos" || role === "ui") {
|
|
125
|
+
// vos/ui primary output is prose; high HTML-tag density indicates a full document
|
|
126
|
+
// was produced instead of planning prose.
|
|
127
|
+
const htmlTagCount = (text.match(/<[a-z][^>]*>/gi) ?? []).length;
|
|
128
|
+
if (htmlTagCount > 8) {
|
|
129
|
+
return (`${role} output contains ${htmlTagCount} HTML opening tags — primary output must be ` +
|
|
130
|
+
`prose. HTML authoring is the coders role's responsibility. Restate as plain prose.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
76
135
|
function parseToolPlan(raw, catalog) {
|
|
77
136
|
const candidate = extractJsonEnvelope(raw);
|
|
78
137
|
const allowedTools = new Set(catalog.map((tool) => tool.name));
|
|
@@ -127,6 +186,9 @@ function summarizeSnippet(text, maxChars = 240) {
|
|
|
127
186
|
return normalized;
|
|
128
187
|
return `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
|
|
129
188
|
}
|
|
189
|
+
function readEnvelopeText(value) {
|
|
190
|
+
return typeof value === "string" ? value.trim() : "";
|
|
191
|
+
}
|
|
130
192
|
function buildHistorySummary(messages, maxChars) {
|
|
131
193
|
const lines = messages.slice(-8).map((message) => {
|
|
132
194
|
const content = summarizeSnippet(messageText(message).replace(/^Conversation summary:\s*/i, ""), 220);
|
|
@@ -209,6 +271,105 @@ function truncateToolResult(result, workspace, maxChars = 3000) {
|
|
|
209
271
|
function formatErrorMessage(error) {
|
|
210
272
|
return error instanceof Error ? error.message : String(error);
|
|
211
273
|
}
|
|
274
|
+
function normalizeEvidenceRefPath(ref) {
|
|
275
|
+
const pathPart = ref.split("#", 1)[0]?.trim() ?? "";
|
|
276
|
+
if (!pathPart || pathPart.includes("\0") || pathPart.startsWith("~"))
|
|
277
|
+
return undefined;
|
|
278
|
+
if (isAbsolute(pathPart))
|
|
279
|
+
return undefined;
|
|
280
|
+
const normalized = normalizeRelPath(pathPart);
|
|
281
|
+
if (!normalized || normalized === ".." || normalized.startsWith("../"))
|
|
282
|
+
return undefined;
|
|
283
|
+
return normalized;
|
|
284
|
+
}
|
|
285
|
+
function workspaceEvidenceExists(ref, workspace) {
|
|
286
|
+
const normalized = normalizeEvidenceRefPath(ref);
|
|
287
|
+
if (!normalized)
|
|
288
|
+
return false;
|
|
289
|
+
try {
|
|
290
|
+
const root = resolve(workspace);
|
|
291
|
+
const candidate = resolve(root, normalized);
|
|
292
|
+
// Prevent symlink escape: compare real (resolved) filesystem paths
|
|
293
|
+
const realRoot = realpathSync(root);
|
|
294
|
+
const realCandidate = realpathSync(candidate);
|
|
295
|
+
// Accept candidate only if its real path is equal to the root or nested under it
|
|
296
|
+
if (realCandidate === realRoot || realCandidate.startsWith(realRoot + sep)) {
|
|
297
|
+
return existsSync(realCandidate);
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function mergeEvidenceRefs(...groups) {
|
|
306
|
+
const merged = groups
|
|
307
|
+
.flatMap((group) => group ?? [])
|
|
308
|
+
.map((ref) => ref.trim())
|
|
309
|
+
.filter(Boolean);
|
|
310
|
+
return merged.length > 0 ? [...new Set(merged)] : undefined;
|
|
311
|
+
}
|
|
312
|
+
async function appendFalseCompletionEvidence(input) {
|
|
313
|
+
await appendRunLedgerEntrySafe({
|
|
314
|
+
tool: "model-bridge",
|
|
315
|
+
category: "regression",
|
|
316
|
+
message: input.summary,
|
|
317
|
+
artifacts: input.evidence_refs ?? [],
|
|
318
|
+
metadata: {
|
|
319
|
+
reason_code: "false_completion_no_evidence",
|
|
320
|
+
role: input.role,
|
|
321
|
+
workspace: input.workspace,
|
|
322
|
+
evidence_refs: input.evidence_refs ?? [],
|
|
323
|
+
},
|
|
324
|
+
}).catch(() => undefined);
|
|
325
|
+
await appendStatusEventSafe({
|
|
326
|
+
source_module: "capability-qa",
|
|
327
|
+
event_type: "MODEL_BRIDGE_COMPLETION_BLOCKED",
|
|
328
|
+
status: "blocked",
|
|
329
|
+
summary: input.summary,
|
|
330
|
+
objective_id: "model-bridge-completion-verification",
|
|
331
|
+
payload: {
|
|
332
|
+
reason_code: "false_completion_no_evidence",
|
|
333
|
+
role: input.role,
|
|
334
|
+
workspace: input.workspace,
|
|
335
|
+
evidence_refs: input.evidence_refs ?? [],
|
|
336
|
+
},
|
|
337
|
+
}).catch(() => undefined);
|
|
338
|
+
}
|
|
339
|
+
async function verifyCompletionArtifacts(result, context) {
|
|
340
|
+
if (result.status !== "completed")
|
|
341
|
+
return result;
|
|
342
|
+
const expectedArtifacts = context.expectedArtifacts ?? [];
|
|
343
|
+
const mutationIntent = /\b(write|create|mutate|edit|persist|save|generate)\b/i.test(context.task);
|
|
344
|
+
const shouldVerify = ((context.role === "coders" || context.role === "builder") && mutationIntent) ||
|
|
345
|
+
expectedArtifacts.length > 0 ||
|
|
346
|
+
result.tool_calls.some((toolCall) => toolCall.tool === "write_workspace_file");
|
|
347
|
+
if (!shouldVerify)
|
|
348
|
+
return result;
|
|
349
|
+
const writeEvidenceOk = result.tool_calls.some((toolCall) => toolCall.tool === "write_workspace_file" && toolCall.ok) &&
|
|
350
|
+
context.touchedPaths.some((path) => workspaceEvidenceExists(path, context.workspace));
|
|
351
|
+
const evidenceRefsOk = (result.evidence_refs ?? []).length > 0 &&
|
|
352
|
+
(result.evidence_refs ?? []).some((ref) => workspaceEvidenceExists(ref, context.workspace));
|
|
353
|
+
const expectedArtifactsOk = expectedArtifacts.length > 0 &&
|
|
354
|
+
expectedArtifacts
|
|
355
|
+
.filter((artifact) => artifact.required !== false)
|
|
356
|
+
.every((artifact) => workspaceEvidenceExists(artifact.path, context.workspace));
|
|
357
|
+
if (writeEvidenceOk || evidenceRefsOk || expectedArtifactsOk)
|
|
358
|
+
return result;
|
|
359
|
+
const summary = "Model claimed completion but no persisted evidence or tool-calls found.";
|
|
360
|
+
await appendFalseCompletionEvidence({
|
|
361
|
+
role: context.role,
|
|
362
|
+
workspace: context.workspace,
|
|
363
|
+
summary,
|
|
364
|
+
evidence_refs: result.evidence_refs,
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
...result,
|
|
368
|
+
status: "blocked",
|
|
369
|
+
reason_code: "false_completion_no_evidence",
|
|
370
|
+
summary,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
212
373
|
function isRetryableProviderError(error) {
|
|
213
374
|
const message = formatErrorMessage(error).toLowerCase();
|
|
214
375
|
return !/(abort|aborted|cancelled|canceled|interrupted)/.test(message);
|
|
@@ -232,13 +393,21 @@ async function collectOllamaResponse(client, model, messages, options) {
|
|
|
232
393
|
num_ctx: options?.num_ctx ?? 8192,
|
|
233
394
|
},
|
|
234
395
|
})) {
|
|
235
|
-
|
|
396
|
+
const text = chunk.message?.content ?? "";
|
|
397
|
+
combined += text;
|
|
398
|
+
if (text || chunk.done) {
|
|
399
|
+
options?.onProgress?.({
|
|
400
|
+
kind: "model_chunk",
|
|
401
|
+
at: Date.now(),
|
|
402
|
+
detail: { provider: "ollama", done: Boolean(chunk.done), bytes: Buffer.byteLength(text) },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
236
405
|
if (chunk.done)
|
|
237
406
|
break;
|
|
238
407
|
}
|
|
239
408
|
return combined;
|
|
240
409
|
}
|
|
241
|
-
async function collectOpenAiCompatibleResponse(client, provider, model, messages) {
|
|
410
|
+
async function collectOpenAiCompatibleResponse(client, provider, model, messages, onProgress) {
|
|
242
411
|
let combined = "";
|
|
243
412
|
for await (const chunk of client.chat({
|
|
244
413
|
provider,
|
|
@@ -246,8 +415,29 @@ async function collectOpenAiCompatibleResponse(client, provider, model, messages
|
|
|
246
415
|
messages,
|
|
247
416
|
temperature: 0.2,
|
|
248
417
|
topP: 0.9,
|
|
418
|
+
onProviderEvent: (event) => {
|
|
419
|
+
onProgress?.({
|
|
420
|
+
kind: "thinking",
|
|
421
|
+
at: Date.now(),
|
|
422
|
+
detail: {
|
|
423
|
+
reason: "provider_adapter_event",
|
|
424
|
+
...event,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
},
|
|
249
428
|
})) {
|
|
250
429
|
combined += chunk.text;
|
|
430
|
+
if (chunk.text || chunk.done) {
|
|
431
|
+
onProgress?.({
|
|
432
|
+
kind: "model_chunk",
|
|
433
|
+
at: Date.now(),
|
|
434
|
+
detail: {
|
|
435
|
+
provider,
|
|
436
|
+
done: Boolean(chunk.done),
|
|
437
|
+
bytes: Buffer.byteLength(chunk.text),
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
251
441
|
if (chunk.done)
|
|
252
442
|
break;
|
|
253
443
|
}
|
|
@@ -257,8 +447,9 @@ async function collectProviderResponse(clients, provider, options) {
|
|
|
257
447
|
return provider.client === "ollama"
|
|
258
448
|
? collectOllamaResponse(clients.ollama, options.model, options.messages, {
|
|
259
449
|
num_ctx: options.numCtx,
|
|
450
|
+
onProgress: options.onProgress,
|
|
260
451
|
})
|
|
261
|
-
: collectOpenAiCompatibleResponse(clients.openai, provider.display, options.model, options.messages);
|
|
452
|
+
: collectOpenAiCompatibleResponse(clients.openai, provider.display, options.model, options.messages, options.onProgress);
|
|
262
453
|
}
|
|
263
454
|
async function collectProviderResponseWithRetry(clients, provider, options, onThinking) {
|
|
264
455
|
try {
|
|
@@ -269,6 +460,11 @@ async function collectProviderResponseWithRetry(clients, provider, options, onTh
|
|
|
269
460
|
throw error;
|
|
270
461
|
}
|
|
271
462
|
onThinking?.(`Provider error, retrying once: ${formatErrorMessage(error)}`);
|
|
463
|
+
options.onProgress?.({
|
|
464
|
+
kind: "thinking",
|
|
465
|
+
at: Date.now(),
|
|
466
|
+
detail: { reason: "provider_retry" },
|
|
467
|
+
});
|
|
272
468
|
return collectProviderResponse(clients, provider, options);
|
|
273
469
|
}
|
|
274
470
|
}
|
|
@@ -353,9 +549,12 @@ export class ModelBridge {
|
|
|
353
549
|
const provider = resolveProviderClient(options.provider);
|
|
354
550
|
const numCtx = options.numCtx ??
|
|
355
551
|
(requestedTier === "brief" ? 4096 : requestedTier === "compressed" ? 8192 : 16384);
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
552
|
+
const toolScopeProvided = Array.isArray(options.toolScope);
|
|
553
|
+
const explicitToolScope = toolScopeProvided
|
|
554
|
+
? options.toolScope.map((tool) => tool.trim()).filter((tool) => tool.length > 0)
|
|
555
|
+
: [];
|
|
556
|
+
const toolScopeLocked = toolScopeProvided;
|
|
557
|
+
const selectedToolScope = toolScopeProvided
|
|
359
558
|
? explicitToolScope
|
|
360
559
|
: await this.selectToolScope({
|
|
361
560
|
task: options.task,
|
|
@@ -396,6 +595,9 @@ export class ModelBridge {
|
|
|
396
595
|
];
|
|
397
596
|
const toolResults = [];
|
|
398
597
|
const childResults = [];
|
|
598
|
+
const touchedPaths = [];
|
|
599
|
+
const declaredEvidenceRefs = [];
|
|
600
|
+
const evidenceRefs = () => mergeEvidenceRefs(touchedPaths, declaredEvidenceRefs);
|
|
399
601
|
const sessionId = this.bridgeId;
|
|
400
602
|
const refs = deriveWorkspaceVericifyRunRef({
|
|
401
603
|
session_id: this.bridgeId,
|
|
@@ -404,6 +606,13 @@ export class ModelBridge {
|
|
|
404
606
|
const availableTools = new Set(listAceInternalToolCatalog().map((tool) => tool.name));
|
|
405
607
|
const allowedTools = new Set(aceContext.tools.map((tool) => tool.name));
|
|
406
608
|
this.currentRunChildResults = childResults;
|
|
609
|
+
const noteProgress = (kind, detail) => {
|
|
610
|
+
options.onProgress?.({ kind, at: Date.now(), detail });
|
|
611
|
+
};
|
|
612
|
+
// Tracks repair attempts for roles that require a JSON envelope (coders, builder, qa).
|
|
613
|
+
let parseRepairAttempts = 0;
|
|
614
|
+
// Tracks correction attempts for roles that produce semantically drifted output.
|
|
615
|
+
let outputDriftRepairs = 0;
|
|
407
616
|
try {
|
|
408
617
|
await appendVericifyProcessPostSafe({
|
|
409
618
|
run_id: refs.run_id,
|
|
@@ -414,24 +623,117 @@ export class ModelBridge {
|
|
|
414
623
|
summary: `Bridge started for ${role} via ${provider.display}: ${options.task}`,
|
|
415
624
|
tool_refs: aceContext.tools.map((tool) => tool.name),
|
|
416
625
|
});
|
|
626
|
+
noteProgress("process_post", { kind: "intent" });
|
|
417
627
|
for (let turn = 1; turn <= options.maxTurns; turn += 1) {
|
|
418
628
|
const compressed = compressConversationHistory(messages, numCtx);
|
|
419
629
|
if (compressed.compressed) {
|
|
420
630
|
messages.splice(0, messages.length, ...compressed.messages);
|
|
421
631
|
options.onThinking?.(`Context window compressed to ~${compressed.promptTokens} tokens.`);
|
|
632
|
+
noteProgress("thinking", { reason: "context_compressed", turn });
|
|
422
633
|
}
|
|
423
634
|
this.activeProviderClient = provider.client;
|
|
424
635
|
const rawResponse = await collectProviderResponseWithRetry(this.clients, provider, {
|
|
425
636
|
model: options.model,
|
|
426
637
|
messages,
|
|
427
638
|
numCtx,
|
|
639
|
+
onProgress: options.onProgress,
|
|
428
640
|
}, options.onThinking);
|
|
429
641
|
this.activeProviderClient = null;
|
|
430
642
|
const envelope = parseEnvelope(rawResponse);
|
|
643
|
+
if (envelope.status !== "parse_error") {
|
|
644
|
+
const refs = mergeEvidenceRefs(envelope.evidence_refs);
|
|
645
|
+
if (refs)
|
|
646
|
+
declaredEvidenceRefs.push(...refs);
|
|
647
|
+
}
|
|
431
648
|
if (envelope.thinking) {
|
|
432
649
|
options.onThinking?.(envelope.thinking);
|
|
650
|
+
noteProgress("thinking", { turn });
|
|
433
651
|
}
|
|
434
652
|
messages.push({ role: "assistant", content: rawResponse });
|
|
653
|
+
if (envelope.status === "parse_error") {
|
|
654
|
+
// Plain-text fallback: accepted only for roles that do NOT require a JSON envelope
|
|
655
|
+
// and only when they have no tool scope (i.e. they legitimately produce prose output).
|
|
656
|
+
if ((selectedToolScope ?? []).length === 0 && !ROLES_REQUIRING_JSON_ENVELOPE.has(role)) {
|
|
657
|
+
const summary = rawResponse.trim() || "Bridge completed.";
|
|
658
|
+
options.onOutput?.(summary);
|
|
659
|
+
noteProgress("output", { status: "complete", fallback: "plain_text" });
|
|
660
|
+
await appendVericifyProcessPostSafe({
|
|
661
|
+
run_id: refs.run_id,
|
|
662
|
+
branch_id: refs.branch_id,
|
|
663
|
+
lane_id: refs.lane_id,
|
|
664
|
+
agent_id: `agent-${role}`,
|
|
665
|
+
kind: "completion",
|
|
666
|
+
summary,
|
|
667
|
+
tool_refs: [],
|
|
668
|
+
});
|
|
669
|
+
noteProgress("process_post", { kind: "completion", fallback: "plain_text" });
|
|
670
|
+
return verifyCompletionArtifacts({
|
|
671
|
+
bridge_id: this.bridgeId,
|
|
672
|
+
role,
|
|
673
|
+
status: "completed",
|
|
674
|
+
summary,
|
|
675
|
+
turns: turn,
|
|
676
|
+
tool_calls: toolResults,
|
|
677
|
+
child_results: childResults,
|
|
678
|
+
evidence_refs: evidenceRefs(),
|
|
679
|
+
}, {
|
|
680
|
+
role,
|
|
681
|
+
task: options.task,
|
|
682
|
+
workspace: options.workspace,
|
|
683
|
+
touchedPaths,
|
|
684
|
+
expectedArtifacts: options.expectedArtifacts,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
// Repair path: roles that require a JSON envelope get up to MAX_PARSE_REPAIR_ATTEMPTS
|
|
688
|
+
// chances to emit a valid response before the run is marked as failed.
|
|
689
|
+
if (ROLES_REQUIRING_JSON_ENVELOPE.has(role) && parseRepairAttempts < MAX_PARSE_REPAIR_ATTEMPTS) {
|
|
690
|
+
parseRepairAttempts += 1;
|
|
691
|
+
const repairPrompt = `Your previous response was not a valid JSON envelope and cannot be accepted. ` +
|
|
692
|
+
`As the ${role} role, every response MUST be a JSON object with a "status" key. ` +
|
|
693
|
+
`Valid response shapes:\n` +
|
|
694
|
+
` {"status":"tool","tool_calls":[{"tool":"name","input":{}}]}\n` +
|
|
695
|
+
` {"status":"complete","summary":"what you accomplished"}\n` +
|
|
696
|
+
`Do NOT output plain text, HTML, markdown, or code outside a JSON envelope. ` +
|
|
697
|
+
`Respond with valid JSON only. (repair attempt ${parseRepairAttempts}/${MAX_PARSE_REPAIR_ATTEMPTS})`;
|
|
698
|
+
messages.push({ role: "user", content: repairPrompt });
|
|
699
|
+
options.onThinking?.(`[drift-repair] ${role} parse_error — injecting repair prompt (attempt ${parseRepairAttempts}/${MAX_PARSE_REPAIR_ATTEMPTS})`);
|
|
700
|
+
noteProgress("thinking", {
|
|
701
|
+
reason: "role_contract_repair",
|
|
702
|
+
role,
|
|
703
|
+
attempt: parseRepairAttempts,
|
|
704
|
+
});
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const isContractViolation = ROLES_REQUIRING_JSON_ENVELOPE.has(role);
|
|
708
|
+
const summary = isContractViolation
|
|
709
|
+
? `[role_contract_violation] ${role} returned malformed JSON after ${parseRepairAttempts} repair attempt(s): ${envelope.message ?? "[empty response]"}`
|
|
710
|
+
: `Model bridge returned malformed or non-JSON output: ${envelope.message ?? "[empty response]"}`;
|
|
711
|
+
options.onOutput?.(summary);
|
|
712
|
+
noteProgress("output", {
|
|
713
|
+
status: "parse_error",
|
|
714
|
+
role_contract_violation: isContractViolation,
|
|
715
|
+
});
|
|
716
|
+
await appendVericifyProcessPostSafe({
|
|
717
|
+
run_id: refs.run_id,
|
|
718
|
+
branch_id: refs.branch_id,
|
|
719
|
+
lane_id: refs.lane_id,
|
|
720
|
+
agent_id: `agent-${role}`,
|
|
721
|
+
kind: "blocker",
|
|
722
|
+
summary,
|
|
723
|
+
tool_refs: toolResults.map((entry) => entry.tool),
|
|
724
|
+
});
|
|
725
|
+
noteProgress("process_post", { kind: "blocker" });
|
|
726
|
+
return {
|
|
727
|
+
bridge_id: this.bridgeId,
|
|
728
|
+
role,
|
|
729
|
+
status: "failed",
|
|
730
|
+
summary,
|
|
731
|
+
turns: turn,
|
|
732
|
+
tool_calls: toolResults,
|
|
733
|
+
child_results: childResults,
|
|
734
|
+
evidence_refs: evidenceRefs(),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
435
737
|
if (envelope.status === "tool" &&
|
|
436
738
|
Array.isArray(envelope.tool_calls) &&
|
|
437
739
|
envelope.tool_calls.length > 0) {
|
|
@@ -460,6 +762,11 @@ export class ModelBridge {
|
|
|
460
762
|
: `Tool '${toolCall.tool}' is not available in the active ACE catalog.`,
|
|
461
763
|
};
|
|
462
764
|
options.onToolResult?.(toolCall.tool, result);
|
|
765
|
+
noteProgress("tool_finish", {
|
|
766
|
+
tool: toolCall.tool,
|
|
767
|
+
ok: false,
|
|
768
|
+
blocked: true,
|
|
769
|
+
});
|
|
463
770
|
toolResults.push(result);
|
|
464
771
|
return result;
|
|
465
772
|
});
|
|
@@ -472,7 +779,10 @@ export class ModelBridge {
|
|
|
472
779
|
const executed = await Promise.all(envelope.tool_calls.map(async (toolCall) => {
|
|
473
780
|
const args = toolCall.input ?? {};
|
|
474
781
|
options.onToolCall?.(toolCall.tool, args);
|
|
475
|
-
|
|
782
|
+
noteProgress("tool_start", { tool: toolCall.tool });
|
|
783
|
+
const rawToolResult = await executeAceInternalTool(toolCall.tool, args, sessionId, {
|
|
784
|
+
workspace_path: options.workspace,
|
|
785
|
+
});
|
|
476
786
|
const result = truncateToolResult({
|
|
477
787
|
tool: toolCall.tool,
|
|
478
788
|
ok: !Boolean(rawToolResult?.isError),
|
|
@@ -480,7 +790,17 @@ export class ModelBridge {
|
|
|
480
790
|
summary: summarizeToolText(rawToolResult),
|
|
481
791
|
}, options.workspace);
|
|
482
792
|
options.onToolResult?.(toolCall.tool, result);
|
|
793
|
+
noteProgress("tool_finish", { tool: toolCall.tool, ok: result.ok });
|
|
483
794
|
toolResults.push(result);
|
|
795
|
+
if (result.ok) {
|
|
796
|
+
const pathArg = typeof args.path === "string"
|
|
797
|
+
? args.path
|
|
798
|
+
: typeof args.file_path === "string"
|
|
799
|
+
? args.file_path
|
|
800
|
+
: undefined;
|
|
801
|
+
if (pathArg)
|
|
802
|
+
touchedPaths.push(pathArg);
|
|
803
|
+
}
|
|
484
804
|
return result;
|
|
485
805
|
}));
|
|
486
806
|
messages.push({
|
|
@@ -492,8 +812,9 @@ export class ModelBridge {
|
|
|
492
812
|
continue;
|
|
493
813
|
}
|
|
494
814
|
if (envelope.status === "message") {
|
|
495
|
-
const message = envelope.message
|
|
815
|
+
const message = readEnvelopeText(envelope.message) || rawResponse.trim();
|
|
496
816
|
options.onOutput?.(message);
|
|
817
|
+
noteProgress("output", { status: "message" });
|
|
497
818
|
await appendVericifyProcessPostSafe({
|
|
498
819
|
run_id: refs.run_id,
|
|
499
820
|
branch_id: refs.branch_id,
|
|
@@ -503,7 +824,8 @@ export class ModelBridge {
|
|
|
503
824
|
summary: message,
|
|
504
825
|
tool_refs: [],
|
|
505
826
|
});
|
|
506
|
-
|
|
827
|
+
noteProgress("process_post", { kind: "progress" });
|
|
828
|
+
return verifyCompletionArtifacts({
|
|
507
829
|
bridge_id: this.bridgeId,
|
|
508
830
|
role,
|
|
509
831
|
status: "completed",
|
|
@@ -511,11 +833,19 @@ export class ModelBridge {
|
|
|
511
833
|
turns: turn,
|
|
512
834
|
tool_calls: toolResults,
|
|
513
835
|
child_results: childResults,
|
|
514
|
-
|
|
836
|
+
evidence_refs: evidenceRefs(),
|
|
837
|
+
}, {
|
|
838
|
+
role,
|
|
839
|
+
task: options.task,
|
|
840
|
+
workspace: options.workspace,
|
|
841
|
+
touchedPaths,
|
|
842
|
+
expectedArtifacts: options.expectedArtifacts,
|
|
843
|
+
});
|
|
515
844
|
}
|
|
516
845
|
if (envelope.status === "need_input") {
|
|
517
|
-
const message = envelope.message
|
|
846
|
+
const message = readEnvelopeText(envelope.message) || "Additional operator input required.";
|
|
518
847
|
options.onOutput?.(message);
|
|
848
|
+
noteProgress("output", { status: "need_input" });
|
|
519
849
|
await appendVericifyProcessPostSafe({
|
|
520
850
|
run_id: refs.run_id,
|
|
521
851
|
branch_id: refs.branch_id,
|
|
@@ -525,6 +855,7 @@ export class ModelBridge {
|
|
|
525
855
|
summary: message,
|
|
526
856
|
tool_refs: [],
|
|
527
857
|
});
|
|
858
|
+
noteProgress("process_post", { kind: "blocker" });
|
|
528
859
|
return {
|
|
529
860
|
bridge_id: this.bridgeId,
|
|
530
861
|
role,
|
|
@@ -533,11 +864,57 @@ export class ModelBridge {
|
|
|
533
864
|
turns: turn,
|
|
534
865
|
tool_calls: toolResults,
|
|
535
866
|
child_results: childResults,
|
|
867
|
+
evidence_refs: evidenceRefs(),
|
|
536
868
|
};
|
|
537
869
|
}
|
|
538
870
|
if (envelope.status === "complete") {
|
|
539
|
-
const summary = envelope.summary
|
|
871
|
+
const summary = readEnvelopeText(envelope.summary) || "Bridge completed.";
|
|
872
|
+
// Output shape drift check: detect semantic violations before accepting the result.
|
|
873
|
+
const driftViolation = checkOutputShapeDrift(role, summary, toolResults);
|
|
874
|
+
if (driftViolation) {
|
|
875
|
+
if (outputDriftRepairs < MAX_OUTPUT_DRIFT_REPAIRS) {
|
|
876
|
+
outputDriftRepairs += 1;
|
|
877
|
+
const correctionPrompt = `Your previous completion violated the output contract for the ${role} role: ` +
|
|
878
|
+
`${driftViolation} Please restate your output, correcting the violation.`;
|
|
879
|
+
messages.push({ role: "user", content: correctionPrompt });
|
|
880
|
+
options.onThinking?.(`[drift-correction] ${role} output drift — injecting correction ` +
|
|
881
|
+
`(attempt ${outputDriftRepairs}/${MAX_OUTPUT_DRIFT_REPAIRS}): ${driftViolation}`);
|
|
882
|
+
noteProgress("thinking", {
|
|
883
|
+
reason: "output_drift_correction",
|
|
884
|
+
role,
|
|
885
|
+
attempt: outputDriftRepairs,
|
|
886
|
+
violation: driftViolation,
|
|
887
|
+
});
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
// Correction exhausted — reject the drifted output.
|
|
891
|
+
const driftSummary = `[output_drift_violation] ${role}: ${driftViolation}`;
|
|
892
|
+
options.onOutput?.(driftSummary);
|
|
893
|
+
noteProgress("output", { status: "output_drift_violation" });
|
|
894
|
+
await appendVericifyProcessPostSafe({
|
|
895
|
+
run_id: refs.run_id,
|
|
896
|
+
branch_id: refs.branch_id,
|
|
897
|
+
lane_id: refs.lane_id,
|
|
898
|
+
agent_id: `agent-${role}`,
|
|
899
|
+
kind: "blocker",
|
|
900
|
+
summary: driftSummary,
|
|
901
|
+
tool_refs: toolResults.map((entry) => entry.tool),
|
|
902
|
+
});
|
|
903
|
+
noteProgress("process_post", { kind: "blocker" });
|
|
904
|
+
return {
|
|
905
|
+
bridge_id: this.bridgeId,
|
|
906
|
+
role,
|
|
907
|
+
status: "failed",
|
|
908
|
+
summary: driftSummary,
|
|
909
|
+
turns: turn,
|
|
910
|
+
tool_calls: toolResults,
|
|
911
|
+
child_results: childResults,
|
|
912
|
+
evidence_refs: evidenceRefs(),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
// Clean output — accept the completion.
|
|
540
916
|
options.onOutput?.(summary);
|
|
917
|
+
noteProgress("output", { status: "complete" });
|
|
541
918
|
await appendVericifyProcessPostSafe({
|
|
542
919
|
run_id: refs.run_id,
|
|
543
920
|
branch_id: refs.branch_id,
|
|
@@ -547,7 +924,8 @@ export class ModelBridge {
|
|
|
547
924
|
summary,
|
|
548
925
|
tool_refs: toolResults.map((entry) => entry.tool),
|
|
549
926
|
});
|
|
550
|
-
|
|
927
|
+
noteProgress("process_post", { kind: "completion" });
|
|
928
|
+
return verifyCompletionArtifacts({
|
|
551
929
|
bridge_id: this.bridgeId,
|
|
552
930
|
role,
|
|
553
931
|
status: "completed",
|
|
@@ -555,7 +933,14 @@ export class ModelBridge {
|
|
|
555
933
|
turns: turn,
|
|
556
934
|
tool_calls: toolResults,
|
|
557
935
|
child_results: childResults,
|
|
558
|
-
|
|
936
|
+
evidence_refs: evidenceRefs(),
|
|
937
|
+
}, {
|
|
938
|
+
role,
|
|
939
|
+
task: options.task,
|
|
940
|
+
workspace: options.workspace,
|
|
941
|
+
touchedPaths,
|
|
942
|
+
expectedArtifacts: options.expectedArtifacts,
|
|
943
|
+
});
|
|
559
944
|
}
|
|
560
945
|
}
|
|
561
946
|
const summary = "Bridge stopped after reaching max turns.";
|
|
@@ -568,6 +953,7 @@ export class ModelBridge {
|
|
|
568
953
|
summary,
|
|
569
954
|
tool_refs: toolResults.map((entry) => entry.tool),
|
|
570
955
|
});
|
|
956
|
+
noteProgress("process_post", { kind: "blocker" });
|
|
571
957
|
return {
|
|
572
958
|
bridge_id: this.bridgeId,
|
|
573
959
|
role,
|
|
@@ -576,6 +962,7 @@ export class ModelBridge {
|
|
|
576
962
|
turns: options.maxTurns,
|
|
577
963
|
tool_calls: toolResults,
|
|
578
964
|
child_results: childResults,
|
|
965
|
+
evidence_refs: evidenceRefs(),
|
|
579
966
|
};
|
|
580
967
|
}
|
|
581
968
|
finally {
|