@voybio/ace-swarm 2.4.4 → 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 +69 -0
- package/README.md +72 -532
- 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/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/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/safe-edit.d.ts +20 -0
- package/dist/safe-edit.js +355 -78
- 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 +57 -3
- 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/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 +39 -18
- package/dist/tools-framework.js +31 -3
- 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 +3 -1
- package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
package/dist/model-bridge.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, realpathSync } from "node:fs";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync, realpathSync } from "node:fs";
|
|
3
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";
|
|
@@ -8,6 +8,11 @@ import { appendRunLedgerEntrySafe } from "./run-ledger.js";
|
|
|
8
8
|
import { appendStatusEventSafe } from "./status-events.js";
|
|
9
9
|
import { normalizeRelPath } from "./helpers.js";
|
|
10
10
|
import { sanitizeJsonLikeText } from "./json-sanitizer.js";
|
|
11
|
+
const MUTATION_INTENT = /\b(write|create|mutate|edit|persist|save|generate|implement|fix|update|modify|patch|delete|remove)\b/i;
|
|
12
|
+
/** Keep completion-evidence policy identical across direct and Hermes execution. */
|
|
13
|
+
export function isMutationIntent(task) {
|
|
14
|
+
return MUTATION_INTENT.test(task);
|
|
15
|
+
}
|
|
11
16
|
/**
|
|
12
17
|
* Roles that MUST produce a valid JSON envelope in every response.
|
|
13
18
|
* A parse_error from these roles is a contract failure, not a plain-text fallback.
|
|
@@ -15,6 +20,33 @@ import { sanitizeJsonLikeText } from "./json-sanitizer.js";
|
|
|
15
20
|
* marking the run as failed with status "role_contract_violation".
|
|
16
21
|
*/
|
|
17
22
|
const ROLES_REQUIRING_JSON_ENVELOPE = new Set(["coders", "builder", "qa"]);
|
|
23
|
+
/**
|
|
24
|
+
* JSON Schema for the ACE response envelope. Passed as `format` (Ollama ≥ 0.5) or
|
|
25
|
+
* `response_format.json_schema` (OpenAI-compatible) to enable schema-constrained
|
|
26
|
+
* generation. Falls back to plain json-mode if the provider rejects it with a 400.
|
|
27
|
+
*/
|
|
28
|
+
export const ACE_ENVELOPE_JSON_SCHEMA = {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
status: { type: "string", enum: ["tool", "message", "complete", "need_input"] },
|
|
32
|
+
thinking: { type: "string" },
|
|
33
|
+
message: { type: "string" },
|
|
34
|
+
summary: { type: "string" },
|
|
35
|
+
tool_calls: {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
tool: { type: "string" },
|
|
41
|
+
input: { type: "object" },
|
|
42
|
+
},
|
|
43
|
+
required: ["tool"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
evidence_refs: { type: "array", items: { type: "string" } },
|
|
47
|
+
},
|
|
48
|
+
required: ["status"],
|
|
49
|
+
};
|
|
18
50
|
const VALID_ENVELOPE_STATUSES = new Set(["tool", "message", "complete", "need_input"]);
|
|
19
51
|
const MAX_PARSE_REPAIR_ATTEMPTS = 2;
|
|
20
52
|
const MAX_OUTPUT_DRIFT_REPAIRS = 1;
|
|
@@ -93,6 +125,15 @@ function parseEnvelope(raw) {
|
|
|
93
125
|
.map((ref) => ref.trim())
|
|
94
126
|
.filter(Boolean);
|
|
95
127
|
}
|
|
128
|
+
if (parsed.used_bullets !== undefined) {
|
|
129
|
+
if (!Array.isArray(parsed.used_bullets)) {
|
|
130
|
+
throw new Error("used_bullets must be an array");
|
|
131
|
+
}
|
|
132
|
+
parsed.used_bullets = parsed.used_bullets
|
|
133
|
+
.filter((id) => typeof id === "string")
|
|
134
|
+
.map((id) => id.trim())
|
|
135
|
+
.filter((id) => /^pb-[a-fA-F0-9]{8}$/.test(id));
|
|
136
|
+
}
|
|
96
137
|
return parsed;
|
|
97
138
|
}
|
|
98
139
|
catch {
|
|
@@ -310,13 +351,14 @@ function mergeEvidenceRefs(...groups) {
|
|
|
310
351
|
return merged.length > 0 ? [...new Set(merged)] : undefined;
|
|
311
352
|
}
|
|
312
353
|
async function appendFalseCompletionEvidence(input) {
|
|
354
|
+
const reasonCode = input.reason_code ?? "false_completion_no_evidence";
|
|
313
355
|
await appendRunLedgerEntrySafe({
|
|
314
356
|
tool: "model-bridge",
|
|
315
357
|
category: "regression",
|
|
316
358
|
message: input.summary,
|
|
317
359
|
artifacts: input.evidence_refs ?? [],
|
|
318
360
|
metadata: {
|
|
319
|
-
reason_code:
|
|
361
|
+
reason_code: reasonCode,
|
|
320
362
|
role: input.role,
|
|
321
363
|
workspace: input.workspace,
|
|
322
364
|
evidence_refs: input.evidence_refs ?? [],
|
|
@@ -329,20 +371,23 @@ async function appendFalseCompletionEvidence(input) {
|
|
|
329
371
|
summary: input.summary,
|
|
330
372
|
objective_id: "model-bridge-completion-verification",
|
|
331
373
|
payload: {
|
|
332
|
-
reason_code:
|
|
374
|
+
reason_code: reasonCode,
|
|
333
375
|
role: input.role,
|
|
334
376
|
workspace: input.workspace,
|
|
335
377
|
evidence_refs: input.evidence_refs ?? [],
|
|
336
378
|
},
|
|
337
379
|
}).catch(() => undefined);
|
|
338
380
|
}
|
|
339
|
-
async function verifyCompletionArtifacts(result, context) {
|
|
381
|
+
export async function verifyCompletionArtifacts(result, context) {
|
|
340
382
|
if (result.status !== "completed")
|
|
341
383
|
return result;
|
|
342
|
-
const expectedArtifacts = context.expectedArtifacts ?? [];
|
|
343
|
-
const
|
|
344
|
-
const
|
|
384
|
+
const expectedArtifacts = (context.expectedArtifacts ?? []).filter((artifact) => artifact.required !== false);
|
|
385
|
+
const requiredEvidenceRefs = (context.requiredEvidenceRefs ?? []).filter((ref) => ref.trim().length > 0);
|
|
386
|
+
const mutationIntent = isMutationIntent(context.task);
|
|
387
|
+
const shouldVerify = context.forceCompletionVerification === true ||
|
|
388
|
+
((context.role === "coders" || context.role === "builder") && mutationIntent) ||
|
|
345
389
|
expectedArtifacts.length > 0 ||
|
|
390
|
+
requiredEvidenceRefs.length > 0 ||
|
|
346
391
|
result.tool_calls.some((toolCall) => toolCall.tool === "write_workspace_file");
|
|
347
392
|
if (!shouldVerify)
|
|
348
393
|
return result;
|
|
@@ -351,10 +396,42 @@ async function verifyCompletionArtifacts(result, context) {
|
|
|
351
396
|
const evidenceRefsOk = (result.evidence_refs ?? []).length > 0 &&
|
|
352
397
|
(result.evidence_refs ?? []).some((ref) => workspaceEvidenceExists(ref, context.workspace));
|
|
353
398
|
const expectedArtifactsOk = expectedArtifacts.length > 0 &&
|
|
354
|
-
expectedArtifacts
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (
|
|
399
|
+
expectedArtifacts.every((artifact) => workspaceEvidenceExists(artifact.path, context.workspace));
|
|
400
|
+
const requiredEvidenceRefsOk = requiredEvidenceRefs.length > 0 &&
|
|
401
|
+
requiredEvidenceRefs.every((required) => (result.evidence_refs ?? []).some((ref) => ref.includes(required)));
|
|
402
|
+
if (expectedArtifacts.length > 0 && !expectedArtifactsOk) {
|
|
403
|
+
const summary = "Model claimed completion but declared expected artifacts are missing.";
|
|
404
|
+
await appendFalseCompletionEvidence({
|
|
405
|
+
role: context.role,
|
|
406
|
+
workspace: context.workspace,
|
|
407
|
+
summary,
|
|
408
|
+
evidence_refs: result.evidence_refs,
|
|
409
|
+
reason_code: "forward_pacing_incomplete",
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
...result,
|
|
413
|
+
status: "blocked",
|
|
414
|
+
reason_code: "forward_pacing_incomplete",
|
|
415
|
+
summary,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (requiredEvidenceRefs.length > 0 && !requiredEvidenceRefsOk) {
|
|
419
|
+
const summary = "Model claimed completion but required evidence refs are missing.";
|
|
420
|
+
await appendFalseCompletionEvidence({
|
|
421
|
+
role: context.role,
|
|
422
|
+
workspace: context.workspace,
|
|
423
|
+
summary,
|
|
424
|
+
evidence_refs: result.evidence_refs,
|
|
425
|
+
reason_code: "forward_pacing_incomplete",
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
...result,
|
|
429
|
+
status: "blocked",
|
|
430
|
+
reason_code: "forward_pacing_incomplete",
|
|
431
|
+
summary,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (expectedArtifactsOk || requiredEvidenceRefsOk || writeEvidenceOk || evidenceRefsOk)
|
|
358
435
|
return result;
|
|
359
436
|
const summary = "Model claimed completion but no persisted evidence or tool-calls found.";
|
|
360
437
|
await appendFalseCompletionEvidence({
|
|
@@ -370,9 +447,34 @@ async function verifyCompletionArtifacts(result, context) {
|
|
|
370
447
|
summary,
|
|
371
448
|
};
|
|
372
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Returns true when a provider rejected our schema-constrained format (400) and
|
|
452
|
+
* the bridge should retry with plain json-mode. Applies only when `jsonSchema` was
|
|
453
|
+
* set — any 400 from a schema request is treated as a format-rejection to degrade
|
|
454
|
+
* gracefully on older Ollama / backends that don't support json_schema yet.
|
|
455
|
+
*/
|
|
456
|
+
function isSchemaFormatRejectedError(error) {
|
|
457
|
+
const statusCode = typeof error?.statusCode === "number"
|
|
458
|
+
? error.statusCode
|
|
459
|
+
: undefined;
|
|
460
|
+
return statusCode === 400;
|
|
461
|
+
}
|
|
373
462
|
function isRetryableProviderError(error) {
|
|
374
463
|
const message = formatErrorMessage(error).toLowerCase();
|
|
375
|
-
|
|
464
|
+
if (/(abort|aborted|cancelled|canceled|interrupted)/.test(message))
|
|
465
|
+
return false;
|
|
466
|
+
const statusCode = typeof error?.statusCode === "number"
|
|
467
|
+
? error.statusCode
|
|
468
|
+
: undefined;
|
|
469
|
+
if (statusCode !== undefined) {
|
|
470
|
+
if (statusCode >= 500)
|
|
471
|
+
return true;
|
|
472
|
+
if (statusCode === 408 || statusCode === 429)
|
|
473
|
+
return true;
|
|
474
|
+
if (statusCode >= 400 && statusCode < 500)
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
return /(econnreset|econnrefused|etimedout|timeout|timed out|socket|network|fetch failed)/.test(message);
|
|
376
478
|
}
|
|
377
479
|
function formatToolFeedback(results) {
|
|
378
480
|
return [
|
|
@@ -381,20 +483,124 @@ function formatToolFeedback(results) {
|
|
|
381
483
|
"Continue with another JSON response.",
|
|
382
484
|
].join("\n");
|
|
383
485
|
}
|
|
486
|
+
function stableJson(value) {
|
|
487
|
+
if (value === null || typeof value !== "object")
|
|
488
|
+
return JSON.stringify(value);
|
|
489
|
+
if (Array.isArray(value))
|
|
490
|
+
return `[${value.map((entry) => stableJson(entry)).join(",")}]`;
|
|
491
|
+
const record = value;
|
|
492
|
+
const volatileKeys = new Set(["session_id", "sessionId", "timestamp", "updated_at", "created_at", "at"]);
|
|
493
|
+
return `{${Object.keys(record)
|
|
494
|
+
.filter((key) => !volatileKeys.has(key))
|
|
495
|
+
.sort()
|
|
496
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
|
|
497
|
+
.join(",")}}`;
|
|
498
|
+
}
|
|
499
|
+
function sha256Text(value) {
|
|
500
|
+
return createHash("sha256").update(value).digest("hex");
|
|
501
|
+
}
|
|
502
|
+
function toolDoomLoopKey(toolCall, result) {
|
|
503
|
+
return sha256Text([
|
|
504
|
+
toolCall.tool,
|
|
505
|
+
stableJson(toolCall.input ?? {}),
|
|
506
|
+
result.ok ? "ok" : "error",
|
|
507
|
+
sha256Text(result.summary),
|
|
508
|
+
].join("\n"));
|
|
509
|
+
}
|
|
510
|
+
function defaultOutputTokenCap(numCtx) {
|
|
511
|
+
return Math.max(1, Math.min(2048, Math.floor(numCtx / 4)));
|
|
512
|
+
}
|
|
513
|
+
function addUsageTotals(total, usage) {
|
|
514
|
+
if (!usage)
|
|
515
|
+
return;
|
|
516
|
+
total.prompt_tokens += usage.prompt_tokens;
|
|
517
|
+
total.completion_tokens += usage.completion_tokens;
|
|
518
|
+
}
|
|
519
|
+
function withUsageTotals(result, usageTotals) {
|
|
520
|
+
if (usageTotals.prompt_tokens === 0 && usageTotals.completion_tokens === 0)
|
|
521
|
+
return result;
|
|
522
|
+
return {
|
|
523
|
+
...result,
|
|
524
|
+
usage_totals: { ...usageTotals },
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function contextOverflowSuspected(usage, numCtx) {
|
|
528
|
+
return Boolean(usage && usage.prompt_tokens >= Math.floor(numCtx * 0.98));
|
|
529
|
+
}
|
|
530
|
+
const DEFAULT_TOOL_DEADLINE_MS = 120_000;
|
|
531
|
+
function toolDeadlineMs() {
|
|
532
|
+
const raw = Number(process.env.ACE_TOOL_DEADLINE_MS);
|
|
533
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_TOOL_DEADLINE_MS;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Per-tool-call deadline (E12). A hung validation/test subprocess must never
|
|
537
|
+
* wedge a turn indefinitely — and under Promise.all one stuck call also holds
|
|
538
|
+
* every parallel sibling's feedback hostage. On timeout we resolve a classified
|
|
539
|
+
* `tool_timeout` failure that then feeds the WS-C doom-loop brake.
|
|
540
|
+
*/
|
|
541
|
+
export async function executeToolWithDeadline(tool, args, sessionId, opts, deadlineMs, exec = executeAceInternalTool) {
|
|
542
|
+
let timer;
|
|
543
|
+
const timeout = new Promise((resolve) => {
|
|
544
|
+
timer = setTimeout(() => resolve({ rawToolResult: undefined, timedOut: true }), deadlineMs);
|
|
545
|
+
timer.unref?.();
|
|
546
|
+
});
|
|
547
|
+
try {
|
|
548
|
+
return await Promise.race([
|
|
549
|
+
exec(tool, args, sessionId, opts).then((rawToolResult) => ({
|
|
550
|
+
rawToolResult,
|
|
551
|
+
timedOut: false,
|
|
552
|
+
})),
|
|
553
|
+
timeout,
|
|
554
|
+
]);
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
if (timer)
|
|
558
|
+
clearTimeout(timer);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function appendBridgeTrace(input) {
|
|
562
|
+
const safeRunId = input.runId.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
563
|
+
const traceDir = resolve(input.workspace, "agent-state", "bridge-traces");
|
|
564
|
+
mkdirSync(traceDir, { recursive: true });
|
|
565
|
+
appendFileSync(resolve(traceDir, `${safeRunId}.ndjson`), `${JSON.stringify({
|
|
566
|
+
turn: input.turn,
|
|
567
|
+
envelope_status: input.envelopeStatus,
|
|
568
|
+
parse_repairs: input.parseRepairAttempts,
|
|
569
|
+
drift_repairs: input.outputDriftRepairs,
|
|
570
|
+
tool_calls: (input.toolCalls ?? []).map((toolCall) => ({
|
|
571
|
+
tool: toolCall.tool,
|
|
572
|
+
ok: toolCall.ok,
|
|
573
|
+
reason_code: toolCall.reason_code ?? (toolCall.ok ? undefined : "tool_error"),
|
|
574
|
+
})),
|
|
575
|
+
usage: input.usage,
|
|
576
|
+
compressed: input.compressed,
|
|
577
|
+
})}\n`, "utf-8");
|
|
578
|
+
}
|
|
384
579
|
async function collectOllamaResponse(client, model, messages, options) {
|
|
385
580
|
let combined = "";
|
|
581
|
+
let usage;
|
|
582
|
+
// Prefer schema-constrained (Ollama ≥ 0.5) over plain json-mode; caller degrades on 400.
|
|
583
|
+
const ollamaFormat = options?.jsonSchema ? options.jsonSchema : options?.jsonMode ? "json" : undefined;
|
|
386
584
|
for await (const chunk of client.chat({
|
|
387
585
|
model,
|
|
388
586
|
messages,
|
|
389
587
|
stream: true,
|
|
588
|
+
...(ollamaFormat !== undefined ? { format: ollamaFormat } : {}),
|
|
390
589
|
options: {
|
|
391
590
|
temperature: 0.2,
|
|
392
591
|
top_p: 0.9,
|
|
393
592
|
num_ctx: options?.num_ctx ?? 8192,
|
|
593
|
+
num_predict: options?.num_predict ?? defaultOutputTokenCap(options?.num_ctx ?? 8192),
|
|
394
594
|
},
|
|
395
595
|
})) {
|
|
396
596
|
const text = chunk.message?.content ?? "";
|
|
397
597
|
combined += text;
|
|
598
|
+
if (typeof chunk.prompt_eval_count === "number" || typeof chunk.eval_count === "number") {
|
|
599
|
+
usage = {
|
|
600
|
+
prompt_tokens: chunk.prompt_eval_count ?? usage?.prompt_tokens ?? 0,
|
|
601
|
+
completion_tokens: chunk.eval_count ?? usage?.completion_tokens ?? 0,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
398
604
|
if (text || chunk.done) {
|
|
399
605
|
options?.onProgress?.({
|
|
400
606
|
kind: "model_chunk",
|
|
@@ -405,16 +611,25 @@ async function collectOllamaResponse(client, model, messages, options) {
|
|
|
405
611
|
if (chunk.done)
|
|
406
612
|
break;
|
|
407
613
|
}
|
|
408
|
-
return combined;
|
|
614
|
+
return { text: combined, usage };
|
|
409
615
|
}
|
|
410
|
-
async function collectOpenAiCompatibleResponse(client, provider, model, messages, onProgress) {
|
|
616
|
+
async function collectOpenAiCompatibleResponse(client, provider, model, messages, maxTokens, jsonMode, jsonSchema, onProgress) {
|
|
411
617
|
let combined = "";
|
|
618
|
+
let usage;
|
|
619
|
+
// Prefer schema-constrained (OpenAI json_schema) over json_object; caller degrades on 400.
|
|
620
|
+
const openAiResponseFormat = jsonSchema
|
|
621
|
+
? { type: "json_schema", json_schema: { name: "ace_envelope", schema: jsonSchema, strict: false } }
|
|
622
|
+
: jsonMode
|
|
623
|
+
? { type: "json_object" }
|
|
624
|
+
: undefined;
|
|
412
625
|
for await (const chunk of client.chat({
|
|
413
626
|
provider,
|
|
414
627
|
model,
|
|
415
628
|
messages,
|
|
416
629
|
temperature: 0.2,
|
|
417
630
|
topP: 0.9,
|
|
631
|
+
maxTokens,
|
|
632
|
+
...(openAiResponseFormat ? { responseFormat: openAiResponseFormat } : {}),
|
|
418
633
|
onProviderEvent: (event) => {
|
|
419
634
|
onProgress?.({
|
|
420
635
|
kind: "thinking",
|
|
@@ -427,6 +642,12 @@ async function collectOpenAiCompatibleResponse(client, provider, model, messages
|
|
|
427
642
|
},
|
|
428
643
|
})) {
|
|
429
644
|
combined += chunk.text;
|
|
645
|
+
if (typeof chunk.promptTokens === "number" || typeof chunk.completionTokens === "number") {
|
|
646
|
+
usage = {
|
|
647
|
+
prompt_tokens: chunk.promptTokens ?? usage?.prompt_tokens ?? 0,
|
|
648
|
+
completion_tokens: chunk.completionTokens ?? usage?.completion_tokens ?? 0,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
430
651
|
if (chunk.text || chunk.done) {
|
|
431
652
|
onProgress?.({
|
|
432
653
|
kind: "model_chunk",
|
|
@@ -441,21 +662,37 @@ async function collectOpenAiCompatibleResponse(client, provider, model, messages
|
|
|
441
662
|
if (chunk.done)
|
|
442
663
|
break;
|
|
443
664
|
}
|
|
444
|
-
return combined;
|
|
665
|
+
return { text: combined, usage };
|
|
445
666
|
}
|
|
446
667
|
async function collectProviderResponse(clients, provider, options) {
|
|
447
668
|
return provider.client === "ollama"
|
|
448
669
|
? collectOllamaResponse(clients.ollama, options.model, options.messages, {
|
|
449
670
|
num_ctx: options.numCtx,
|
|
671
|
+
num_predict: defaultOutputTokenCap(options.numCtx),
|
|
672
|
+
jsonMode: options.jsonMode,
|
|
673
|
+
jsonSchema: options.jsonSchema,
|
|
450
674
|
onProgress: options.onProgress,
|
|
451
675
|
})
|
|
452
|
-
: collectOpenAiCompatibleResponse(clients.openai, provider.display, options.model, options.messages, options.onProgress);
|
|
676
|
+
: collectOpenAiCompatibleResponse(clients.openai, provider.display, options.model, options.messages, defaultOutputTokenCap(options.numCtx), options.jsonMode, options.jsonSchema, options.onProgress);
|
|
453
677
|
}
|
|
454
678
|
async function collectProviderResponseWithRetry(clients, provider, options, onThinking) {
|
|
455
679
|
try {
|
|
456
680
|
return await collectProviderResponse(clients, provider, options);
|
|
457
681
|
}
|
|
458
682
|
catch (error) {
|
|
683
|
+
// Schema-constrained format rejected by provider → degrade to plain json-mode.
|
|
684
|
+
// Any 400 from a json_schema request is treated as a format-rejection; if the
|
|
685
|
+
// underlying issue is something else (bad model name, etc.) json-mode will also
|
|
686
|
+
// fail and propagate the error normally.
|
|
687
|
+
if (options.jsonSchema && isSchemaFormatRejectedError(error)) {
|
|
688
|
+
onThinking?.(`[schema-degrade] provider rejected schema-constrained format — retrying with json-mode`);
|
|
689
|
+
options.onProgress?.({
|
|
690
|
+
kind: "thinking",
|
|
691
|
+
at: Date.now(),
|
|
692
|
+
detail: { reason: "schema_format_degrade" },
|
|
693
|
+
});
|
|
694
|
+
return collectProviderResponse(clients, provider, { ...options, jsonSchema: undefined });
|
|
695
|
+
}
|
|
459
696
|
if (!isRetryableProviderError(error)) {
|
|
460
697
|
throw error;
|
|
461
698
|
}
|
|
@@ -529,7 +766,7 @@ export class ModelBridge {
|
|
|
529
766
|
messages: selectionMessages,
|
|
530
767
|
numCtx: Math.min(input.numCtx, 8192),
|
|
531
768
|
});
|
|
532
|
-
const parsed = parseToolPlan(rawSelection, plan.catalog);
|
|
769
|
+
const parsed = parseToolPlan(rawSelection.text, plan.catalog);
|
|
533
770
|
const scoped = parsed.selected_tools.slice(0, plan.selection_limit);
|
|
534
771
|
if (scoped.length === 0)
|
|
535
772
|
return undefined;
|
|
@@ -571,6 +808,8 @@ export class ModelBridge {
|
|
|
571
808
|
workspace: options.workspace,
|
|
572
809
|
tier: requestedTier,
|
|
573
810
|
tools: selectedToolScope,
|
|
811
|
+
modelClass: options.modelClass,
|
|
812
|
+
model: options.model,
|
|
574
813
|
});
|
|
575
814
|
const promptBudget = calculateContextBudget(numCtx);
|
|
576
815
|
for (const candidateTier of buildTierFallbackOrder(requestedTier)) {
|
|
@@ -580,6 +819,8 @@ export class ModelBridge {
|
|
|
580
819
|
workspace: options.workspace,
|
|
581
820
|
tier: candidateTier,
|
|
582
821
|
tools: selectedToolScope,
|
|
822
|
+
modelClass: options.modelClass,
|
|
823
|
+
model: options.model,
|
|
583
824
|
});
|
|
584
825
|
aceContext = candidateContext;
|
|
585
826
|
if (candidateContext.tokenEstimate <= promptBudget || candidateTier === "brief") {
|
|
@@ -597,7 +838,18 @@ export class ModelBridge {
|
|
|
597
838
|
const childResults = [];
|
|
598
839
|
const touchedPaths = [];
|
|
599
840
|
const declaredEvidenceRefs = [];
|
|
841
|
+
const usageTotals = { prompt_tokens: 0, completion_tokens: 0 };
|
|
600
842
|
const evidenceRefs = () => mergeEvidenceRefs(touchedPaths, declaredEvidenceRefs);
|
|
843
|
+
const attachPlaybookMetadata = (result, usedBullets) => {
|
|
844
|
+
const cleanUsed = (usedBullets ?? []).filter((id) => aceContext.injected_ids.includes(id));
|
|
845
|
+
if (aceContext.injected_ids.length === 0 && cleanUsed.length === 0)
|
|
846
|
+
return result;
|
|
847
|
+
return {
|
|
848
|
+
...result,
|
|
849
|
+
...(aceContext.injected_ids.length > 0 ? { injected_bullets: [...aceContext.injected_ids] } : {}),
|
|
850
|
+
...(cleanUsed.length > 0 ? { used_bullets: [...new Set(cleanUsed)] } : {}),
|
|
851
|
+
};
|
|
852
|
+
};
|
|
601
853
|
const sessionId = this.bridgeId;
|
|
602
854
|
const refs = deriveWorkspaceVericifyRunRef({
|
|
603
855
|
session_id: this.bridgeId,
|
|
@@ -613,6 +865,11 @@ export class ModelBridge {
|
|
|
613
865
|
let parseRepairAttempts = 0;
|
|
614
866
|
// Tracks correction attempts for roles that produce semantically drifted output.
|
|
615
867
|
let outputDriftRepairs = 0;
|
|
868
|
+
const toolDoomLoopCounts = new Map();
|
|
869
|
+
// Inter-turn text-loop brake (E2): catches a model that keeps emitting the same
|
|
870
|
+
// non-terminating output (repeated identical tool envelopes / parse errors). The
|
|
871
|
+
// intra-turn runaway is bounded separately by the num_predict output cap.
|
|
872
|
+
const textLoopCounts = new Map();
|
|
616
873
|
try {
|
|
617
874
|
await appendVericifyProcessPostSafe({
|
|
618
875
|
run_id: refs.run_id,
|
|
@@ -632,12 +889,17 @@ export class ModelBridge {
|
|
|
632
889
|
noteProgress("thinking", { reason: "context_compressed", turn });
|
|
633
890
|
}
|
|
634
891
|
this.activeProviderClient = provider.client;
|
|
635
|
-
const
|
|
892
|
+
const useJsonEnvelope = ROLES_REQUIRING_JSON_ENVELOPE.has(role);
|
|
893
|
+
const providerResponse = await collectProviderResponseWithRetry(this.clients, provider, {
|
|
636
894
|
model: options.model,
|
|
637
895
|
messages,
|
|
638
896
|
numCtx,
|
|
897
|
+
jsonMode: useJsonEnvelope,
|
|
898
|
+
jsonSchema: useJsonEnvelope ? ACE_ENVELOPE_JSON_SCHEMA : undefined,
|
|
639
899
|
onProgress: options.onProgress,
|
|
640
900
|
}, options.onThinking);
|
|
901
|
+
addUsageTotals(usageTotals, providerResponse.usage);
|
|
902
|
+
const rawResponse = providerResponse.text;
|
|
641
903
|
this.activeProviderClient = null;
|
|
642
904
|
const envelope = parseEnvelope(rawResponse);
|
|
643
905
|
if (envelope.status !== "parse_error") {
|
|
@@ -650,7 +912,108 @@ export class ModelBridge {
|
|
|
650
912
|
noteProgress("thinking", { turn });
|
|
651
913
|
}
|
|
652
914
|
messages.push({ role: "assistant", content: rawResponse });
|
|
915
|
+
// Text-loop brake (WS-C+, E2): a model stuck repeating the same non-terminating
|
|
916
|
+
// output. This fires only on turns that execute NO tool — turns that run tools are
|
|
917
|
+
// governed by the tool-doom-loop brake below (which exempts legitimate polling:
|
|
918
|
+
// same call, changing results). The gap this closes is an envelope loop that
|
|
919
|
+
// performs no work, e.g. repeated `{"status":"tool","tool_calls":[]}`, which the
|
|
920
|
+
// tool brake never sees. Terminal statuses (message/complete/need_input) return
|
|
921
|
+
// before a repeat can accumulate.
|
|
922
|
+
const turnExecutesTools = envelope.status === "tool" &&
|
|
923
|
+
Array.isArray(envelope.tool_calls) &&
|
|
924
|
+
envelope.tool_calls.length > 0;
|
|
925
|
+
if (!turnExecutesTools) {
|
|
926
|
+
const textLoopKey = sha256Text(rawResponse.trim());
|
|
927
|
+
const textLoopCount = (textLoopCounts.get(textLoopKey) ?? 0) + 1;
|
|
928
|
+
textLoopCounts.set(textLoopKey, textLoopCount);
|
|
929
|
+
if (textLoopCount === 3) {
|
|
930
|
+
messages.push({
|
|
931
|
+
role: "user",
|
|
932
|
+
content: "You have produced the same response three times without making progress. " +
|
|
933
|
+
"Change approach, call a tool, or report status:need_input — do not repeat yourself.",
|
|
934
|
+
});
|
|
935
|
+
noteProgress("thinking", { reason: "bridge_text_doom_loop_warning", count: textLoopCount });
|
|
936
|
+
}
|
|
937
|
+
if (textLoopCount >= 4) {
|
|
938
|
+
const summary = "Bridge blocked after repeating an identical response four times without progress.";
|
|
939
|
+
await appendVericifyProcessPostSafe({
|
|
940
|
+
run_id: refs.run_id,
|
|
941
|
+
branch_id: refs.branch_id,
|
|
942
|
+
lane_id: refs.lane_id,
|
|
943
|
+
agent_id: `agent-${role}`,
|
|
944
|
+
kind: "blocker",
|
|
945
|
+
summary,
|
|
946
|
+
tool_refs: toolResults.map((entry) => entry.tool),
|
|
947
|
+
blocker_category: "bridge_text_doom_loop",
|
|
948
|
+
});
|
|
949
|
+
noteProgress("process_post", { kind: "blocker", reason: "bridge_text_doom_loop" });
|
|
950
|
+
return attachPlaybookMetadata(withUsageTotals({
|
|
951
|
+
bridge_id: this.bridgeId,
|
|
952
|
+
role,
|
|
953
|
+
status: "blocked",
|
|
954
|
+
reason_code: "bridge_text_doom_loop",
|
|
955
|
+
summary,
|
|
956
|
+
turns: turn,
|
|
957
|
+
tool_calls: toolResults,
|
|
958
|
+
child_results: childResults,
|
|
959
|
+
evidence_refs: evidenceRefs(),
|
|
960
|
+
}, usageTotals));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (contextOverflowSuspected(providerResponse.usage, numCtx)) {
|
|
964
|
+
appendBridgeTrace({
|
|
965
|
+
workspace: options.workspace,
|
|
966
|
+
runId: refs.run_id,
|
|
967
|
+
turn,
|
|
968
|
+
envelopeStatus: envelope.status,
|
|
969
|
+
parseRepairAttempts,
|
|
970
|
+
outputDriftRepairs,
|
|
971
|
+
compressed: compressed.compressed,
|
|
972
|
+
usage: providerResponse.usage,
|
|
973
|
+
});
|
|
974
|
+
const summary = `Context overflow suspected: provider reported ${providerResponse.usage?.prompt_tokens ?? 0} ` +
|
|
975
|
+
`prompt tokens for num_ctx ${numCtx}.`;
|
|
976
|
+
await appendStatusEventSafe({
|
|
977
|
+
source_module: "capability-qa",
|
|
978
|
+
event_type: "MODEL_CONTEXT_OVERFLOW_SUSPECTED",
|
|
979
|
+
status: "blocked",
|
|
980
|
+
summary,
|
|
981
|
+
objective_id: "model-bridge-context-overflow",
|
|
982
|
+
payload: {
|
|
983
|
+
reason_code: "context_overflow",
|
|
984
|
+
role,
|
|
985
|
+
provider: provider.display,
|
|
986
|
+
model: options.model,
|
|
987
|
+
prompt_tokens: providerResponse.usage?.prompt_tokens,
|
|
988
|
+
completion_tokens: providerResponse.usage?.completion_tokens,
|
|
989
|
+
num_ctx: numCtx,
|
|
990
|
+
threshold_ratio: 0.98,
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
noteProgress("output", { status: "blocked", reason: "context_overflow" });
|
|
994
|
+
return attachPlaybookMetadata(withUsageTotals({
|
|
995
|
+
bridge_id: this.bridgeId,
|
|
996
|
+
role,
|
|
997
|
+
status: "blocked",
|
|
998
|
+
reason_code: "context_overflow",
|
|
999
|
+
summary,
|
|
1000
|
+
turns: turn,
|
|
1001
|
+
tool_calls: toolResults,
|
|
1002
|
+
child_results: childResults,
|
|
1003
|
+
evidence_refs: evidenceRefs(),
|
|
1004
|
+
}, usageTotals));
|
|
1005
|
+
}
|
|
653
1006
|
if (envelope.status === "parse_error") {
|
|
1007
|
+
appendBridgeTrace({
|
|
1008
|
+
workspace: options.workspace,
|
|
1009
|
+
runId: refs.run_id,
|
|
1010
|
+
turn,
|
|
1011
|
+
envelopeStatus: envelope.status,
|
|
1012
|
+
parseRepairAttempts,
|
|
1013
|
+
outputDriftRepairs,
|
|
1014
|
+
compressed: compressed.compressed,
|
|
1015
|
+
usage: providerResponse.usage,
|
|
1016
|
+
});
|
|
654
1017
|
// Plain-text fallback: accepted only for roles that do NOT require a JSON envelope
|
|
655
1018
|
// and only when they have no tool scope (i.e. they legitimately produce prose output).
|
|
656
1019
|
if ((selectedToolScope ?? []).length === 0 && !ROLES_REQUIRING_JSON_ENVELOPE.has(role)) {
|
|
@@ -667,7 +1030,7 @@ export class ModelBridge {
|
|
|
667
1030
|
tool_refs: [],
|
|
668
1031
|
});
|
|
669
1032
|
noteProgress("process_post", { kind: "completion", fallback: "plain_text" });
|
|
670
|
-
return verifyCompletionArtifacts({
|
|
1033
|
+
return verifyCompletionArtifacts(attachPlaybookMetadata(withUsageTotals({
|
|
671
1034
|
bridge_id: this.bridgeId,
|
|
672
1035
|
role,
|
|
673
1036
|
status: "completed",
|
|
@@ -676,12 +1039,13 @@ export class ModelBridge {
|
|
|
676
1039
|
tool_calls: toolResults,
|
|
677
1040
|
child_results: childResults,
|
|
678
1041
|
evidence_refs: evidenceRefs(),
|
|
679
|
-
}, {
|
|
1042
|
+
}, usageTotals)), {
|
|
680
1043
|
role,
|
|
681
1044
|
task: options.task,
|
|
682
1045
|
workspace: options.workspace,
|
|
683
1046
|
touchedPaths,
|
|
684
1047
|
expectedArtifacts: options.expectedArtifacts,
|
|
1048
|
+
requiredEvidenceRefs: options.requiredEvidenceRefs,
|
|
685
1049
|
});
|
|
686
1050
|
}
|
|
687
1051
|
// Repair path: roles that require a JSON envelope get up to MAX_PARSE_REPAIR_ATTEMPTS
|
|
@@ -723,7 +1087,7 @@ export class ModelBridge {
|
|
|
723
1087
|
tool_refs: toolResults.map((entry) => entry.tool),
|
|
724
1088
|
});
|
|
725
1089
|
noteProgress("process_post", { kind: "blocker" });
|
|
726
|
-
return {
|
|
1090
|
+
return attachPlaybookMetadata(withUsageTotals({
|
|
727
1091
|
bridge_id: this.bridgeId,
|
|
728
1092
|
role,
|
|
729
1093
|
status: "failed",
|
|
@@ -732,7 +1096,7 @@ export class ModelBridge {
|
|
|
732
1096
|
tool_calls: toolResults,
|
|
733
1097
|
child_results: childResults,
|
|
734
1098
|
evidence_refs: evidenceRefs(),
|
|
735
|
-
};
|
|
1099
|
+
}, usageTotals));
|
|
736
1100
|
}
|
|
737
1101
|
if (envelope.status === "tool" &&
|
|
738
1102
|
Array.isArray(envelope.tool_calls) &&
|
|
@@ -776,19 +1140,26 @@ export class ModelBridge {
|
|
|
776
1140
|
});
|
|
777
1141
|
continue;
|
|
778
1142
|
}
|
|
779
|
-
const
|
|
1143
|
+
const executedPairs = await Promise.all(envelope.tool_calls.map(async (toolCall) => {
|
|
780
1144
|
const args = toolCall.input ?? {};
|
|
781
1145
|
options.onToolCall?.(toolCall.tool, args);
|
|
782
1146
|
noteProgress("tool_start", { tool: toolCall.tool });
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1147
|
+
const deadlineMs = toolDeadlineMs();
|
|
1148
|
+
const outcome = await executeToolWithDeadline(toolCall.tool, args, sessionId, { workspace_path: options.workspace }, deadlineMs);
|
|
1149
|
+
const result = outcome.timedOut
|
|
1150
|
+
? {
|
|
1151
|
+
tool: toolCall.tool,
|
|
1152
|
+
ok: false,
|
|
1153
|
+
isError: true,
|
|
1154
|
+
summary: `tool_timeout: ${toolCall.tool} exceeded ${deadlineMs}ms deadline`,
|
|
1155
|
+
reason_code: "tool_timeout",
|
|
1156
|
+
}
|
|
1157
|
+
: truncateToolResult({
|
|
1158
|
+
tool: toolCall.tool,
|
|
1159
|
+
ok: !Boolean(outcome.rawToolResult?.isError),
|
|
1160
|
+
isError: Boolean(outcome.rawToolResult?.isError),
|
|
1161
|
+
summary: summarizeToolText(outcome.rawToolResult),
|
|
1162
|
+
}, options.workspace);
|
|
792
1163
|
options.onToolResult?.(toolCall.tool, result);
|
|
793
1164
|
noteProgress("tool_finish", { tool: toolCall.tool, ok: result.ok });
|
|
794
1165
|
toolResults.push(result);
|
|
@@ -801,8 +1172,63 @@ export class ModelBridge {
|
|
|
801
1172
|
if (pathArg)
|
|
802
1173
|
touchedPaths.push(pathArg);
|
|
803
1174
|
}
|
|
804
|
-
return result;
|
|
1175
|
+
return { toolCall, result };
|
|
805
1176
|
}));
|
|
1177
|
+
const executed = executedPairs.map((entry) => entry.result);
|
|
1178
|
+
appendBridgeTrace({
|
|
1179
|
+
workspace: options.workspace,
|
|
1180
|
+
runId: refs.run_id,
|
|
1181
|
+
turn,
|
|
1182
|
+
envelopeStatus: envelope.status,
|
|
1183
|
+
parseRepairAttempts,
|
|
1184
|
+
outputDriftRepairs,
|
|
1185
|
+
compressed: compressed.compressed,
|
|
1186
|
+
usage: providerResponse.usage,
|
|
1187
|
+
toolCalls: executed,
|
|
1188
|
+
});
|
|
1189
|
+
for (const { toolCall, result } of executedPairs) {
|
|
1190
|
+
const loopKey = toolDoomLoopKey(toolCall, result);
|
|
1191
|
+
const count = (toolDoomLoopCounts.get(loopKey) ?? 0) + 1;
|
|
1192
|
+
toolDoomLoopCounts.set(loopKey, count);
|
|
1193
|
+
if (count === 3) {
|
|
1194
|
+
messages.push({
|
|
1195
|
+
role: "user",
|
|
1196
|
+
content: "You have repeated the same tool call with the same result three times. " +
|
|
1197
|
+
"Change approach or report blocked; do not repeat the same failing action.",
|
|
1198
|
+
});
|
|
1199
|
+
noteProgress("thinking", {
|
|
1200
|
+
reason: "bridge_tool_doom_loop_warning",
|
|
1201
|
+
tool: toolCall.tool,
|
|
1202
|
+
count,
|
|
1203
|
+
});
|
|
1204
|
+
options.onThinking?.(`[doom-loop] repeated ${toolCall.tool} with identical result three times; change approach or report blocked.`);
|
|
1205
|
+
}
|
|
1206
|
+
if (count >= 4) {
|
|
1207
|
+
const summary = `Bridge blocked after repeated identical tool result for ${toolCall.tool}.`;
|
|
1208
|
+
await appendVericifyProcessPostSafe({
|
|
1209
|
+
run_id: refs.run_id,
|
|
1210
|
+
branch_id: refs.branch_id,
|
|
1211
|
+
lane_id: refs.lane_id,
|
|
1212
|
+
agent_id: `agent-${role}`,
|
|
1213
|
+
kind: "blocker",
|
|
1214
|
+
summary,
|
|
1215
|
+
tool_refs: toolResults.map((entry) => entry.tool),
|
|
1216
|
+
blocker_category: "bridge_tool_doom_loop",
|
|
1217
|
+
});
|
|
1218
|
+
noteProgress("process_post", { kind: "blocker", reason: "bridge_tool_doom_loop" });
|
|
1219
|
+
return attachPlaybookMetadata(withUsageTotals({
|
|
1220
|
+
bridge_id: this.bridgeId,
|
|
1221
|
+
role,
|
|
1222
|
+
status: "blocked",
|
|
1223
|
+
reason_code: "bridge_tool_doom_loop",
|
|
1224
|
+
summary,
|
|
1225
|
+
turns: turn,
|
|
1226
|
+
tool_calls: toolResults,
|
|
1227
|
+
child_results: childResults,
|
|
1228
|
+
evidence_refs: evidenceRefs(),
|
|
1229
|
+
}, usageTotals));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
806
1232
|
messages.push({
|
|
807
1233
|
role: "user",
|
|
808
1234
|
content: amendedTools.length > 0
|
|
@@ -812,6 +1238,16 @@ export class ModelBridge {
|
|
|
812
1238
|
continue;
|
|
813
1239
|
}
|
|
814
1240
|
if (envelope.status === "message") {
|
|
1241
|
+
appendBridgeTrace({
|
|
1242
|
+
workspace: options.workspace,
|
|
1243
|
+
runId: refs.run_id,
|
|
1244
|
+
turn,
|
|
1245
|
+
envelopeStatus: envelope.status,
|
|
1246
|
+
parseRepairAttempts,
|
|
1247
|
+
outputDriftRepairs,
|
|
1248
|
+
compressed: compressed.compressed,
|
|
1249
|
+
usage: providerResponse.usage,
|
|
1250
|
+
});
|
|
815
1251
|
const message = readEnvelopeText(envelope.message) || rawResponse.trim();
|
|
816
1252
|
options.onOutput?.(message);
|
|
817
1253
|
noteProgress("output", { status: "message" });
|
|
@@ -825,7 +1261,7 @@ export class ModelBridge {
|
|
|
825
1261
|
tool_refs: [],
|
|
826
1262
|
});
|
|
827
1263
|
noteProgress("process_post", { kind: "progress" });
|
|
828
|
-
return verifyCompletionArtifacts({
|
|
1264
|
+
return verifyCompletionArtifacts(attachPlaybookMetadata(withUsageTotals({
|
|
829
1265
|
bridge_id: this.bridgeId,
|
|
830
1266
|
role,
|
|
831
1267
|
status: "completed",
|
|
@@ -834,15 +1270,26 @@ export class ModelBridge {
|
|
|
834
1270
|
tool_calls: toolResults,
|
|
835
1271
|
child_results: childResults,
|
|
836
1272
|
evidence_refs: evidenceRefs(),
|
|
837
|
-
}, {
|
|
1273
|
+
}, usageTotals), envelope.used_bullets), {
|
|
838
1274
|
role,
|
|
839
1275
|
task: options.task,
|
|
840
1276
|
workspace: options.workspace,
|
|
841
1277
|
touchedPaths,
|
|
842
1278
|
expectedArtifacts: options.expectedArtifacts,
|
|
1279
|
+
requiredEvidenceRefs: options.requiredEvidenceRefs,
|
|
843
1280
|
});
|
|
844
1281
|
}
|
|
845
1282
|
if (envelope.status === "need_input") {
|
|
1283
|
+
appendBridgeTrace({
|
|
1284
|
+
workspace: options.workspace,
|
|
1285
|
+
runId: refs.run_id,
|
|
1286
|
+
turn,
|
|
1287
|
+
envelopeStatus: envelope.status,
|
|
1288
|
+
parseRepairAttempts,
|
|
1289
|
+
outputDriftRepairs,
|
|
1290
|
+
compressed: compressed.compressed,
|
|
1291
|
+
usage: providerResponse.usage,
|
|
1292
|
+
});
|
|
846
1293
|
const message = readEnvelopeText(envelope.message) || "Additional operator input required.";
|
|
847
1294
|
options.onOutput?.(message);
|
|
848
1295
|
noteProgress("output", { status: "need_input" });
|
|
@@ -856,7 +1303,7 @@ export class ModelBridge {
|
|
|
856
1303
|
tool_refs: [],
|
|
857
1304
|
});
|
|
858
1305
|
noteProgress("process_post", { kind: "blocker" });
|
|
859
|
-
return {
|
|
1306
|
+
return attachPlaybookMetadata(withUsageTotals({
|
|
860
1307
|
bridge_id: this.bridgeId,
|
|
861
1308
|
role,
|
|
862
1309
|
status: "needs_input",
|
|
@@ -865,10 +1312,20 @@ export class ModelBridge {
|
|
|
865
1312
|
tool_calls: toolResults,
|
|
866
1313
|
child_results: childResults,
|
|
867
1314
|
evidence_refs: evidenceRefs(),
|
|
868
|
-
};
|
|
1315
|
+
}, usageTotals));
|
|
869
1316
|
}
|
|
870
1317
|
if (envelope.status === "complete") {
|
|
871
1318
|
const summary = readEnvelopeText(envelope.summary) || "Bridge completed.";
|
|
1319
|
+
appendBridgeTrace({
|
|
1320
|
+
workspace: options.workspace,
|
|
1321
|
+
runId: refs.run_id,
|
|
1322
|
+
turn,
|
|
1323
|
+
envelopeStatus: envelope.status,
|
|
1324
|
+
parseRepairAttempts,
|
|
1325
|
+
outputDriftRepairs,
|
|
1326
|
+
compressed: compressed.compressed,
|
|
1327
|
+
usage: providerResponse.usage,
|
|
1328
|
+
});
|
|
872
1329
|
// Output shape drift check: detect semantic violations before accepting the result.
|
|
873
1330
|
const driftViolation = checkOutputShapeDrift(role, summary, toolResults);
|
|
874
1331
|
if (driftViolation) {
|
|
@@ -901,7 +1358,7 @@ export class ModelBridge {
|
|
|
901
1358
|
tool_refs: toolResults.map((entry) => entry.tool),
|
|
902
1359
|
});
|
|
903
1360
|
noteProgress("process_post", { kind: "blocker" });
|
|
904
|
-
return {
|
|
1361
|
+
return attachPlaybookMetadata(withUsageTotals({
|
|
905
1362
|
bridge_id: this.bridgeId,
|
|
906
1363
|
role,
|
|
907
1364
|
status: "failed",
|
|
@@ -910,7 +1367,7 @@ export class ModelBridge {
|
|
|
910
1367
|
tool_calls: toolResults,
|
|
911
1368
|
child_results: childResults,
|
|
912
1369
|
evidence_refs: evidenceRefs(),
|
|
913
|
-
};
|
|
1370
|
+
}, usageTotals));
|
|
914
1371
|
}
|
|
915
1372
|
// Clean output — accept the completion.
|
|
916
1373
|
options.onOutput?.(summary);
|
|
@@ -925,7 +1382,7 @@ export class ModelBridge {
|
|
|
925
1382
|
tool_refs: toolResults.map((entry) => entry.tool),
|
|
926
1383
|
});
|
|
927
1384
|
noteProgress("process_post", { kind: "completion" });
|
|
928
|
-
return verifyCompletionArtifacts({
|
|
1385
|
+
return verifyCompletionArtifacts(attachPlaybookMetadata(withUsageTotals({
|
|
929
1386
|
bridge_id: this.bridgeId,
|
|
930
1387
|
role,
|
|
931
1388
|
status: "completed",
|
|
@@ -934,12 +1391,13 @@ export class ModelBridge {
|
|
|
934
1391
|
tool_calls: toolResults,
|
|
935
1392
|
child_results: childResults,
|
|
936
1393
|
evidence_refs: evidenceRefs(),
|
|
937
|
-
}, {
|
|
1394
|
+
}, usageTotals), envelope.used_bullets), {
|
|
938
1395
|
role,
|
|
939
1396
|
task: options.task,
|
|
940
1397
|
workspace: options.workspace,
|
|
941
1398
|
touchedPaths,
|
|
942
1399
|
expectedArtifacts: options.expectedArtifacts,
|
|
1400
|
+
requiredEvidenceRefs: options.requiredEvidenceRefs,
|
|
943
1401
|
});
|
|
944
1402
|
}
|
|
945
1403
|
}
|
|
@@ -954,7 +1412,7 @@ export class ModelBridge {
|
|
|
954
1412
|
tool_refs: toolResults.map((entry) => entry.tool),
|
|
955
1413
|
});
|
|
956
1414
|
noteProgress("process_post", { kind: "blocker" });
|
|
957
|
-
return {
|
|
1415
|
+
return attachPlaybookMetadata(withUsageTotals({
|
|
958
1416
|
bridge_id: this.bridgeId,
|
|
959
1417
|
role,
|
|
960
1418
|
status: "max_turns",
|
|
@@ -963,7 +1421,7 @@ export class ModelBridge {
|
|
|
963
1421
|
tool_calls: toolResults,
|
|
964
1422
|
child_results: childResults,
|
|
965
1423
|
evidence_refs: evidenceRefs(),
|
|
966
|
-
};
|
|
1424
|
+
}, usageTotals));
|
|
967
1425
|
}
|
|
968
1426
|
finally {
|
|
969
1427
|
this.activeProviderClient = null;
|