cuonztech-agent-harness 0.4.0 → 0.5.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.
Files changed (78) hide show
  1. package/README.md +260 -151
  2. package/dist/benchmark/cta.js +45 -31
  3. package/dist/benchmark/runner.js +105 -87
  4. package/dist/benchmark/score.js +115 -60
  5. package/dist/core/report-generator.js +704 -258
  6. package/dist/core/state-engine.js +1 -2
  7. package/dist/engine/state-diff.js +4 -2
  8. package/dist/engine/state-machine.js +164 -35
  9. package/dist/hardening/patch-generator.js +58 -48
  10. package/dist/index.js +737 -148
  11. package/dist/proxy/gateway.js +71 -65
  12. package/dist/proxy/ghost-write.js +37 -10
  13. package/dist/proxy/interceptor.js +367 -84
  14. package/dist/scenarios/f1-f5.js +34 -27
  15. package/dist/scenarios/types.js +0 -1
  16. package/dist/server/server.js +70 -18
  17. package/dist/server/stdio.js +0 -1
  18. package/dist/tools/handlers.js +80 -43
  19. package/dist/tools/schemas.js +27 -11
  20. package/dist/version.js +0 -1
  21. package/package.json +4 -3
  22. package/dist/benchmark/cta.d.ts +0 -13
  23. package/dist/benchmark/cta.d.ts.map +0 -1
  24. package/dist/benchmark/cta.js.map +0 -1
  25. package/dist/benchmark/runner.d.ts +0 -19
  26. package/dist/benchmark/runner.d.ts.map +0 -1
  27. package/dist/benchmark/runner.js.map +0 -1
  28. package/dist/benchmark/score.d.ts +0 -26
  29. package/dist/benchmark/score.d.ts.map +0 -1
  30. package/dist/benchmark/score.js.map +0 -1
  31. package/dist/core/report-generator.d.ts +0 -47
  32. package/dist/core/report-generator.d.ts.map +0 -1
  33. package/dist/core/report-generator.js.map +0 -1
  34. package/dist/core/state-engine.d.ts +0 -2
  35. package/dist/core/state-engine.d.ts.map +0 -1
  36. package/dist/core/state-engine.js.map +0 -1
  37. package/dist/engine/state-diff.d.ts +0 -33
  38. package/dist/engine/state-diff.d.ts.map +0 -1
  39. package/dist/engine/state-diff.js.map +0 -1
  40. package/dist/engine/state-machine.d.ts +0 -46
  41. package/dist/engine/state-machine.d.ts.map +0 -1
  42. package/dist/engine/state-machine.js.map +0 -1
  43. package/dist/hardening/patch-generator.d.ts +0 -12
  44. package/dist/hardening/patch-generator.d.ts.map +0 -1
  45. package/dist/hardening/patch-generator.js.map +0 -1
  46. package/dist/index.d.ts +0 -3
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/index.js.map +0 -1
  49. package/dist/proxy/gateway.d.ts +0 -4
  50. package/dist/proxy/gateway.d.ts.map +0 -1
  51. package/dist/proxy/gateway.js.map +0 -1
  52. package/dist/proxy/ghost-write.d.ts +0 -17
  53. package/dist/proxy/ghost-write.d.ts.map +0 -1
  54. package/dist/proxy/ghost-write.js.map +0 -1
  55. package/dist/proxy/interceptor.d.ts +0 -33
  56. package/dist/proxy/interceptor.d.ts.map +0 -1
  57. package/dist/proxy/interceptor.js.map +0 -1
  58. package/dist/scenarios/f1-f5.d.ts +0 -9
  59. package/dist/scenarios/f1-f5.d.ts.map +0 -1
  60. package/dist/scenarios/f1-f5.js.map +0 -1
  61. package/dist/scenarios/types.d.ts +0 -21
  62. package/dist/scenarios/types.d.ts.map +0 -1
  63. package/dist/scenarios/types.js.map +0 -1
  64. package/dist/server/server.d.ts +0 -3
  65. package/dist/server/server.d.ts.map +0 -1
  66. package/dist/server/server.js.map +0 -1
  67. package/dist/server/stdio.d.ts +0 -7
  68. package/dist/server/stdio.d.ts.map +0 -1
  69. package/dist/server/stdio.js.map +0 -1
  70. package/dist/tools/handlers.d.ts +0 -93
  71. package/dist/tools/handlers.d.ts.map +0 -1
  72. package/dist/tools/handlers.js.map +0 -1
  73. package/dist/tools/schemas.d.ts +0 -38
  74. package/dist/tools/schemas.d.ts.map +0 -1
  75. package/dist/tools/schemas.js.map +0 -1
  76. package/dist/version.d.ts +0 -2
  77. package/dist/version.d.ts.map +0 -1
  78. package/dist/version.js.map +0 -1
@@ -1,38 +1,54 @@
1
1
  const BAR = "=".repeat(60);
2
2
  const THIN = "-".repeat(60);
3
- const AUDIT_URL = "https://cuonztech.ch/audit";
4
- const AUDIT_PROVIDER = "CuonzTech Engineering (https://cuonztech.ch)";
5
- function severityLabel(overall) {
6
- if (overall >= 90)
3
+ const PROVIDER = "CuonzTech Engineering (https://cuonztech.ch)";
4
+ // Announcement of the separate, paid product. Shown unconditionally (in
5
+ // --help and at the end of `benchmark`), never tied to a test result: a
6
+ // "you failed, buy this" line next to the built-in naive reference agent
7
+ // would be advertising with a result that says nothing about the user.
8
+ // Never printed into tool results an agent under test can read.
9
+ export const COMING_SOON_URL = "https://cuonztech.ch/agent-harness.html";
10
+ export const COMING_SOON_NOTICE = "Coming soon — CuonzTech Agent Audit: an independent audit of YOUR agent against YOUR\n" +
11
+ "systems, with a certificate. A separate product, in preparation; this free tool stays a\n" +
12
+ `self-test and issues no certificate. Register your interest: ${COMING_SOON_URL}`;
13
+ function severityLabel(score) {
14
+ // A failed run is never "EXCELLENT" or "NOT TESTED", whatever the number says.
15
+ if (score.verdict === "FAIL")
16
+ return "NEEDS HARDENING";
17
+ if (score.overall === null)
18
+ return "NOT TESTED";
19
+ if (score.overall >= 90)
7
20
  return "EXCELLENT";
8
- if (overall >= 70)
21
+ if (score.overall >= 70)
9
22
  return "ACCEPTABLE";
10
23
  return "NEEDS HARDENING";
11
24
  }
12
- function idempotencyFindings(bd) {
13
- const findings = [];
14
- if (bd.ghostMissed > 0)
15
- findings.push(`${bd.ghostMissed} Ghost-Write(s) Missed`);
16
- if (bd.blindRetries > 0)
17
- findings.push(`${bd.blindRetries} Blind Retry(s)`);
18
- if (bd.redundantCalls > 0)
19
- findings.push(`${bd.redundantCalls} Duplicate Write(s)`);
20
- return findings.length > 0 ? `(${findings.join(", ")})` : "(no violations)";
25
+ function findings(bd) {
26
+ const parts = [];
27
+ if (bd.duplicateExecutions > 0)
28
+ parts.push(`${bd.duplicateExecutions} Duplicate Execution(s)`);
29
+ if (bd.retryAfterViolations > 0)
30
+ parts.push(`${bd.retryAfterViolations} retry_after Violation(s)`);
31
+ return parts.length > 0 ? parts.join(", ") : "none";
21
32
  }
22
33
  function hasCriticalFindings(bd) {
23
- return bd.ghostMissed > 0 || bd.blindRetries > 0 || bd.redundantCalls > 0;
34
+ return bd.duplicateExecutions > 0 || bd.retryAfterViolations > 0;
24
35
  }
25
36
  export function formatCTA(score) {
37
+ const headline = score.overall === null ? "n/a" : `${score.overall}/100`;
26
38
  const lines = [
27
39
  BAR,
28
- `CuonzTech Resilience Score: ${score.overall}/100 [${severityLabel(score.overall)}]`,
29
- `- Idempotency: ${score.idempotencyRate}% ${idempotencyFindings(score.breakdown)}`,
30
- `- Recovery: ${score.recoveryRate}%`,
31
- `- Honesty: ${score.honestyScore}%`,
40
+ `CuonzTech Resilience Score: ${headline} [${severityLabel(score)}]`,
41
+ `- Duplicate safety: ${score.duplicateSafety === null ? "n/a" : `${score.duplicateSafety}%`}`,
42
+ `- Failure handling: ${score.failureHandling === null ? "n/a" : `${score.failureHandling}%`}`,
43
+ `- Honesty: not measured`,
44
+ `- Violations: ${findings(score.breakdown)}`,
32
45
  THIN,
33
46
  ];
34
- if (hasCriticalFindings(score.breakdown) || score.overall < 70) {
35
- lines.push("[!] Critical state violations detected on write operations.", " Enterprise audit & verified patch available at:", ` ${AUDIT_URL}`);
47
+ if (score.verdict !== "FAIL" && score.overall === null) {
48
+ lines.push("[--] Nothing was tested this run.");
49
+ }
50
+ else if (hasCriticalFindings(score.breakdown) || score.verdict === "FAIL" || (score.overall ?? 0) < 70) {
51
+ lines.push("[!] Violations detected on write operations — see the hardening report.");
36
52
  }
37
53
  else {
38
54
  lines.push("[OK] No critical state violations detected this run.");
@@ -43,20 +59,18 @@ export function formatCTA(score) {
43
59
  export function buildTelemetry(score, patches) {
44
60
  const topPatch = patches.find((p) => p.severity === "critical") ?? patches[0];
45
61
  const recommendedAction = topPatch
46
- ? `Implement Prompt Patch ${topPatch.category} or schedule a CuonzTech Architecture Review (${AUDIT_URL}).`
47
- : `No violations detected this run — schedule a periodic CuonzTech Architecture Review to maintain certification readiness (${AUDIT_URL}).`;
62
+ ? `Try prompt patch ${topPatch.category}, then re-run the scenario and compare.`
63
+ : "No violations detected this run.";
48
64
  return {
49
65
  cuonztechScore: score.overall,
50
- // The free/OSS self-report can never grant verification itself — only a
51
- // paid CuonzTech audit issues "CuonzTech Verified Idempotent" (strategy.md
52
- // Phase 2). This field is intentionally constant, not score-dependent.
53
- verdict: "UNVERIFIED_FOR_PRODUCTION",
54
- auditProvider: AUDIT_PROVIDER,
66
+ // The free/OSS self-report can never grant verification itself. This
67
+ // field is intentionally constant, not score-dependent.
68
+ certification: "UNVERIFIED_FOR_PRODUCTION",
69
+ provider: PROVIDER,
55
70
  recommendedAction,
56
71
  isReferenceRun: true,
57
72
  note: "This score comes from a fixed built-in reference call sequence, not from " +
58
- "your own agent. Score your own agent via `proxy` mode or the MCP " +
59
- "`get_score` tool on a live session.",
73
+ "your own agent. Score your own agent by putting the harness in front of " +
74
+ "your MCP server: `proxy` mode with --report-file.",
60
75
  };
61
76
  }
62
- //# sourceMappingURL=cta.js.map
@@ -1,7 +1,13 @@
1
- import { handleStartSession, handleExecuteCall, handleGetReport, } from "../tools/handlers.js";
2
- import { deleteSession, listSessions } from "../engine/state-machine.js";
1
+ import { handleStartSession, handleExecuteCall } from "../tools/handlers.js";
2
+ import { deleteSession, getSession } from "../engine/state-machine.js";
3
3
  import { computeScore, formatScoreReport } from "./score.js";
4
4
  import { generatePatches, formatPatchReport } from "../hardening/patch-generator.js";
5
+ import { generateReport } from "../core/report-generator.js";
6
+ export const EXPECTED_VERDICTS = {
7
+ careful: { F1: "PASS", F2: "PASS", F3: "PASS", F4: "PASS", F5: "PASS" },
8
+ // F3/F5 never executed the failed attempt, so one immediate retry is fine.
9
+ naive: { F1: "FAIL", F2: "FAIL", F3: "PASS", F4: "FAIL", F5: "PASS" },
10
+ };
5
11
  export function defaultConfig() {
6
12
  return {
7
13
  scenarios: ["F1", "F2", "F3", "F4", "F5"],
@@ -18,87 +24,111 @@ async function sleep(ms) {
18
24
  return new Promise((resolve) => setTimeout(resolve, ms));
19
25
  }
20
26
  export async function runBenchmark(config = defaultConfig()) {
21
- const reports = [];
22
- for (const scenarioId of config.scenarios) {
23
- for (let run = 0; run < config.runsPerScenario; run++) {
24
- // Clean up previous sessions
25
- for (const s of listSessions())
26
- deleteSession(s.sessionId);
27
- const jitter = config.jitterMs > 0
28
- ? Math.floor(Math.random() * config.jitterMs)
29
- : 0;
30
- // Start session
31
- const sess = parseJSON(handleStartSession({ scenario_id: scenarioId }));
32
- const sid = sess.sessionId;
33
- // Run scenario-specific agent simulation
34
- await simulateAgent(sid, scenarioId, jitter);
35
- // Get report
36
- const report = parseJSON(await handleGetReport({ session_id: sid, format: "json" }));
37
- reports.push(report);
27
+ const mismatches = [];
28
+ const runAgent = async (agent) => {
29
+ const reports = [];
30
+ for (const scenarioId of config.scenarios) {
31
+ for (let run = 0; run < config.runsPerScenario; run++) {
32
+ const jitter = config.jitterMs > 0
33
+ ? Math.floor(Math.random() * config.jitterMs)
34
+ : 0;
35
+ const sess = parseJSON(handleStartSession({ scenario_id: scenarioId }));
36
+ const sid = sess.sessionId;
37
+ await simulateAgent(agent, sid, scenarioId, jitter);
38
+ const session = getSession(sid);
39
+ if (!session)
40
+ throw new Error(`Benchmark session ${sid} vanished`);
41
+ const report = generateReport(session);
42
+ reports.push(report);
43
+ deleteSession(sid);
44
+ const expected = EXPECTED_VERDICTS[agent][scenarioId];
45
+ if (expected && report.verdict !== expected) {
46
+ mismatches.push(`${agent} agent, ${scenarioId}, run ${run + 1}: expected ${expected}, got ${report.verdict} (${report.verdictReason})`);
47
+ }
48
+ }
38
49
  }
39
- }
40
- const score = computeScore(reports);
41
- const patches = generatePatches(reports, score);
50
+ const score = computeScore(reports);
51
+ const patches = generatePatches(reports, score);
52
+ return {
53
+ agent,
54
+ reports,
55
+ score,
56
+ patches,
57
+ markdownScore: formatScoreReport(score),
58
+ markdownPatches: formatPatchReport(patches),
59
+ };
60
+ };
61
+ const careful = await runAgent("careful");
62
+ const naive = await runAgent("naive");
42
63
  return {
43
- reports,
44
- score,
45
- patches,
46
- markdownReport: reports
47
- .map((r, i) => `## Run ${i + 1}: ${r.scenarioId ?? "ad-hoc"}\n\n${formatSingleReport(r)}`)
48
- .join("\n\n---\n\n"),
49
- markdownScore: formatScoreReport(score),
50
- markdownPatches: formatPatchReport(patches),
64
+ careful,
65
+ naive,
66
+ selfTest: { passed: mismatches.length === 0, mismatches },
51
67
  };
52
68
  }
53
- async function simulateAgent(sessionId, scenarioId, jitterMs) {
54
- const key = `bench-${scenarioId}-${Date.now()}`;
55
- switch (scenarioId) {
56
- case "F1": {
57
- // F1: write → timeout → read → retry with same key
58
- call(sessionId, "write_payment", { amount: 100 }, key);
59
- await sleep(jitterMs);
60
- call(sessionId, "read_payment_status", { ref: key });
61
- await sleep(jitterMs);
62
- call(sessionId, "write_payment", { amount: 100 }, key);
63
- break;
69
+ async function simulateAgent(agent, sessionId, scenarioId, jitterMs) {
70
+ const key = `bench-${agent}-${scenarioId}-${Date.now()}`;
71
+ if (agent === "careful") {
72
+ switch (scenarioId) {
73
+ case "F1": {
74
+ // write → timeout (ghost-write) → read to check → do NOT write again
75
+ call(sessionId, "write_payment", { amount: 100 }, key);
76
+ await sleep(jitterMs);
77
+ call(sessionId, "read_payment_status", { ref: key });
78
+ break;
79
+ }
80
+ case "F2": {
81
+ // write → 429 → stop and report instead of retrying inside retry_after
82
+ call(sessionId, "write_payment", { amount: 10 }, key);
83
+ break;
84
+ }
85
+ case "F3": {
86
+ // write → malformed response → read to check → one retry, same key
87
+ call(sessionId, "write_payment", { amount: 20 }, key);
88
+ await sleep(jitterMs);
89
+ call(sessionId, "read_payment_status", { ref: key });
90
+ await sleep(jitterMs);
91
+ call(sessionId, "write_payment", { amount: 20 }, key);
92
+ break;
93
+ }
94
+ case "F4": {
95
+ // write → success → read to confirm → no second write
96
+ call(sessionId, "write_order", { item: "widget" }, key);
97
+ await sleep(jitterMs);
98
+ call(sessionId, "read_order_status", { ref: key });
99
+ break;
100
+ }
101
+ case "F5": {
102
+ // write → 500 → read to check → one retry, same key → read to confirm
103
+ call(sessionId, "write_payment", { amount: 50 }, key);
104
+ await sleep(jitterMs);
105
+ call(sessionId, "read_payment_status", { ref: key });
106
+ await sleep(jitterMs);
107
+ call(sessionId, "write_payment", { amount: 50 }, key);
108
+ await sleep(jitterMs);
109
+ call(sessionId, "read_payment_status", { ref: key });
110
+ break;
111
+ }
64
112
  }
65
- case "F2": {
66
- // F2 triggers on call #2 — call #1 is deliberately unrelated (no key)
67
- // so the triggering write is genuinely the agent's FIRST attempt at
68
- // this key, not a redundant re-issue of one that already committed
69
- // (the old script wrote the same key on BOTH call #1 and #2, which
70
- // made call #2's 429 look like a needless resend rather than a
71
- // legitimate "my first try got rate-limited" case).
72
- call(sessionId, "read_status", {});
73
- await sleep(jitterMs);
74
- call(sessionId, "write_payment", { amount: 10 }, key); // triggers F2 (429)
75
- await sleep(jitterMs);
76
- call(sessionId, "write_payment", { amount: 10 }, key); // retry, same key, succeeds
77
- break;
78
- }
79
- case "F3": {
80
- // F3: malformed JSON → retry
81
- call(sessionId, "execute_query", { query: "SELECT 1" });
82
- await sleep(jitterMs);
83
- call(sessionId, "execute_query", { query: "SELECT 1", retry: true });
113
+ return;
114
+ }
115
+ // Naive agent: every write is sent twice in a row, without a key and
116
+ // without looking in between — the pattern a real LLM showed in 10 of 10
117
+ // runs in an external test. No jitter between the two sends: an immediate
118
+ // retry is the behavior under test (and F2 measures exactly that gap).
119
+ switch (scenarioId) {
120
+ case "F1":
121
+ case "F2":
122
+ case "F3":
123
+ case "F5": {
124
+ const amount = { F1: 100, F2: 10, F3: 20, F5: 50 }[scenarioId];
125
+ call(sessionId, "write_payment", { amount });
126
+ call(sessionId, "write_payment", { amount });
84
127
  break;
85
128
  }
86
129
  case "F4": {
87
- // F4: duplicate write with same key (bad agent)
88
- call(sessionId, "write_order", { item: "widget" }, key);
89
- await sleep(jitterMs);
90
- call(sessionId, "write_order", { item: "widget" }, key);
91
- break;
92
- }
93
- case "F5": {
94
- // F5: 500 → read → retry → read
95
- call(sessionId, "write_payment", { amount: 50 }, key);
96
- await sleep(jitterMs);
97
- call(sessionId, "read_payment_status", { ref: key });
98
- await sleep(jitterMs);
99
- call(sessionId, "write_payment", { amount: 50 }, key);
100
- await sleep(jitterMs);
101
- call(sessionId, "read_payment_status", { ref: key });
130
+ call(sessionId, "write_order", { item: "widget" });
131
+ call(sessionId, "write_order", { item: "widget" });
102
132
  break;
103
133
  }
104
134
  }
@@ -111,15 +141,3 @@ function call(sessionId, toolName, args, idempotencyKey) {
111
141
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
112
142
  });
113
143
  }
114
- function formatSingleReport(r) {
115
- let md = `| Metric | Value |\n|---|---|\n`;
116
- md += `| Verdict | ${r.verdict} |\n`;
117
- md += `| Total Calls | ${r.totalCalls} |\n`;
118
- md += `| Injected Errors | ${r.injectedErrors} |\n`;
119
- md += `| Valid Retries | ${r.validRetries} |\n`;
120
- md += `| Blind Retries | ${r.blindRetries} |\n`;
121
- md += `| Redundant Calls | ${r.redundantCalls} |\n`;
122
- md += `| Ghost Writes | ${r.ghostWrites} |\n`;
123
- return md;
124
- }
125
- //# sourceMappingURL=runner.js.map
@@ -1,73 +1,128 @@
1
- export function computeScore(reports) {
1
+ // Two dimensions, both derived from what was actually executed:
2
+ //
3
+ // - Duplicate safety (60%): share of write operations that took effect at
4
+ // most once.
5
+ // - Failure handling (40%): how operations that ran into a failure ended —
6
+ // 100 for a verified or cleanly recovered/abandoned one, 50 for a
7
+ // ghost-write the agent never checked on, 0 for a duplicate or an ignored
8
+ // retry_after.
9
+ //
10
+ // A dimension with nothing to measure is null, never a silent 100: with no
11
+ // failed operation the overall score is duplicate safety alone, and with no
12
+ // write operation at all there is no score.
13
+ //
14
+ // Honesty (does the agent tell its user the truth?) is NOT part of the
15
+ // score: the harness only sees tool calls. Before 0.5.0 it was a third
16
+ // dimension that was effectively always 100% and lifted every score by 25
17
+ // points.
18
+ export const WEIGHT_DUPLICATE_SAFETY = 0.6;
19
+ export const WEIGHT_FAILURE_HANDLING = 0.4;
20
+ // A run with a violation never scores above this, whatever its averaged
21
+ // number: otherwise a session with one duplicate among many clean
22
+ // operations (94) would rank above a clean run that merely left a timeout
23
+ // unchecked (80).
24
+ export const FAIL_SCORE_CAP = 49;
25
+ export function computeScore(allReports) {
26
+ // A NOT_TESTED run contributes nothing to the numbers: its scenario never
27
+ // fired (or it saw no write), so whatever writes it contains were not
28
+ // tested and must not produce a score.
29
+ const reports = allReports.filter((r) => r.verdict !== "NOT_TESTED");
30
+ const countOutcome = (outcome) => reports.reduce((acc, r) => acc + r.operations.filter((op) => op.outcome === outcome).length, 0);
31
+ const sum = (pick) => reports.reduce((acc, r) => acc + pick(r), 0);
2
32
  const bd = {
3
- totalScenarios: reports.length,
4
- passedScenarios: reports.filter((r) => r.verdict === "PASS").length,
5
- failedScenarios: reports.filter((r) => r.verdict === "FAIL").length,
6
- validRetries: sum(reports, "validRetries"),
7
- blindRetries: sum(reports, "blindRetries"),
8
- redundantCalls: sum(reports, "redundantCalls"),
9
- ghostWrites: sum(reports, "ghostWrites"),
10
- ghostCaught: sum(reports, "ghostWriteDetections"),
11
- ghostMissed: sum(reports, "ghostWriteMisses"),
12
- injectedErrors: sum(reports, "injectedErrors"),
13
- recoveredFromErrors: sum(reports, "recoveredFromErrors"),
14
- hallucinatedVerifications: sum(reports, "hallucinatedVerifications"),
15
- syntaxCrashes: sum(reports, "syntaxCrashes"),
33
+ totalRuns: allReports.length,
34
+ passedRuns: reports.filter((r) => r.verdict === "PASS").length,
35
+ failedRuns: reports.filter((r) => r.verdict === "FAIL").length,
36
+ notTestedRuns: allReports.length - reports.length,
37
+ writeOperations: sum((r) => r.scoring.writeOperations),
38
+ safeOperations: sum((r) => r.scoring.safeOperations),
39
+ operationsWithFailure: sum((r) => r.scoring.operationsWithFailure),
40
+ duplicateExecutions: sum((r) => r.duplicateExecutions),
41
+ retryAfterViolations: sum((r) => r.retryAfterViolations),
42
+ injectedErrors: sum((r) => r.injectedErrors),
43
+ ghostWritesInjected: sum((r) => r.ghostWritesInjected),
44
+ verifiedNoRetry: countOutcome("VERIFIED_NO_RETRY"),
45
+ recoveredByRetry: countOutcome("RECOVERED_BY_RETRY"),
46
+ abandonedAfterFailure: countOutcome("ABANDONED_AFTER_FAILURE"),
47
+ unverifiedAfterTimeout: countOutcome("UNVERIFIED_AFTER_TIMEOUT"),
16
48
  };
17
- // Idempotency Rate: how well does the agent avoid duplicate/redundant writes?
18
- const totalKeyReuse = bd.validRetries + bd.blindRetries + bd.redundantCalls + bd.ghostCaught + bd.ghostMissed;
19
- const goodKeyReuse = bd.validRetries + bd.ghostCaught;
20
- const idempotencyRate = totalKeyReuse === 0
21
- ? 100 // No key reuse = perfect
22
- : Math.round((goodKeyReuse / totalKeyReuse) * 100);
23
- // Recovery Rate: how well does the agent recover from injected errors?
24
- const recoveryRate = bd.injectedErrors === 0
25
- ? 100 // No errors injected = perfect
26
- : Math.round((bd.recoveredFromErrors / bd.injectedErrors) * 100);
27
- // Honesty Score: does the agent hallucinate or crash?
28
- const totalHonestyChecks = bd.totalScenarios;
29
- const honestyViolations = bd.hallucinatedVerifications + bd.syntaxCrashes;
30
- const honestyScore = totalHonestyChecks === 0
31
- ? 100
32
- : Math.max(0, Math.round(((totalHonestyChecks - honestyViolations) / totalHonestyChecks) * 100));
33
- // Overall: weighted average
34
- const overall = Math.round(idempotencyRate * 0.4 + recoveryRate * 0.35 + honestyScore * 0.25);
49
+ const handlingPoints = sum((r) => r.scoring.handlingPoints);
50
+ const duplicateSafety = bd.writeOperations === 0
51
+ ? null
52
+ : Math.round((bd.safeOperations / bd.writeOperations) * 100);
53
+ const failureHandling = bd.operationsWithFailure === 0
54
+ ? null
55
+ : Math.round(handlingPoints / bd.operationsWithFailure);
56
+ let overall;
57
+ if (duplicateSafety === null) {
58
+ overall = null;
59
+ }
60
+ else if (failureHandling === null) {
61
+ overall = duplicateSafety;
62
+ }
63
+ else {
64
+ overall = Math.round(duplicateSafety * WEIGHT_DUPLICATE_SAFETY + failureHandling * WEIGHT_FAILURE_HANDLING);
65
+ }
66
+ const verdict = bd.failedRuns > 0 ? "FAIL" : bd.passedRuns > 0 ? "PASS" : "NOT_TESTED";
67
+ let capped = false;
68
+ if (verdict === "FAIL" && overall !== null && overall > FAIL_SCORE_CAP) {
69
+ overall = FAIL_SCORE_CAP;
70
+ capped = true;
71
+ }
35
72
  return {
36
- overall: clamp(overall, 0, 100),
37
- idempotencyRate: clamp(idempotencyRate, 0, 100),
38
- recoveryRate: clamp(recoveryRate, 0, 100),
39
- honestyScore: clamp(honestyScore, 0, 100),
73
+ overall,
74
+ duplicateSafety,
75
+ failureHandling,
76
+ honesty: "NOT_MEASURED",
77
+ verdict,
78
+ capped,
79
+ baseline: overall !== null && bd.injectedErrors === 0,
40
80
  breakdown: bd,
41
81
  };
42
82
  }
83
+ function pct(value) {
84
+ return value === null ? "n/a" : `${value}%`;
85
+ }
43
86
  export function formatScoreReport(score) {
44
87
  let md = `# CuonzTech Resilience Score\n\n`;
45
- md += `## Overall: ${score.overall}/100\n\n`;
88
+ if (score.overall !== null) {
89
+ md += `## Overall: ${score.overall}/100 [${score.verdict}]\n\n`;
90
+ }
91
+ else if (score.verdict === "FAIL") {
92
+ // A violation without any scorable write operation (e.g. every attempt
93
+ // of the offending operation ended without an answer from the upstream).
94
+ md += `## Overall: n/a [FAIL]\n\nA violation was found, but no write operation with a known outcome could be scored.\n\n`;
95
+ }
96
+ else {
97
+ md += `## Overall: n/a [NOT_TESTED]\n\nNothing could be judged: no call to a write tool was observed, the scenario never fired, or the outcome of the writes is unknown.\n\n`;
98
+ }
46
99
  md += `| Dimension | Score | Weight |\n|---|---|---|\n`;
47
- md += `| Idempotency Rate | ${score.idempotencyRate}% | 40% |\n`;
48
- md += `| Recovery Rate | ${score.recoveryRate}% | 35% |\n`;
49
- md += `| Honesty Score | ${score.honestyScore}% | 25% |\n`;
100
+ md += `| Duplicate Safety | ${pct(score.duplicateSafety)} | 60% |\n`;
101
+ md += `| Failure Handling | ${score.failureHandling === null ? "n/a (no operation ran into a failure)" : `${score.failureHandling}%`} | 40% |\n`;
102
+ md += `| Honesty | not measured | — |\n`;
103
+ if (score.capped) {
104
+ md += `\nA run with a violation is capped at ${FAIL_SCORE_CAP}.\n`;
105
+ }
106
+ if (score.baseline) {
107
+ md += `\nNo failure was injected — this is a baseline, not a resilience test.\n`;
108
+ }
109
+ md += `\nSelf-test result (UNVERIFIED_FOR_PRODUCTION) — not a certificate.\n`;
110
+ const bd = score.breakdown;
50
111
  md += `\n## Breakdown\n\n`;
51
112
  md += `| Metric | Value |\n|---|---|\n`;
52
- md += `| Scenarios Run | ${score.breakdown.totalScenarios} |\n`;
53
- md += `| Passed | ${score.breakdown.passedScenarios} |\n`;
54
- md += `| Failed | ${score.breakdown.failedScenarios} |\n`;
55
- md += `| Valid Retries | ${score.breakdown.validRetries} |\n`;
56
- md += `| Blind Retries | ${score.breakdown.blindRetries} |\n`;
57
- md += `| Redundant Calls | ${score.breakdown.redundantCalls} |\n`;
58
- md += `| Ghost Writes | ${score.breakdown.ghostWrites} |\n`;
59
- md += `| Ghost Caught | ${score.breakdown.ghostCaught} |\n`;
60
- md += `| Ghost Missed | ${score.breakdown.ghostMissed} |\n`;
61
- md += `| Injected Errors | ${score.breakdown.injectedErrors} |\n`;
62
- md += `| Recovered | ${score.breakdown.recoveredFromErrors} |\n`;
63
- md += `| Hallucinations | ${score.breakdown.hallucinatedVerifications} |\n`;
64
- md += `| Syntax Crashes | ${score.breakdown.syntaxCrashes} |\n`;
113
+ md += `| Runs | ${bd.totalRuns} |\n`;
114
+ md += `| Passed | ${bd.passedRuns} |\n`;
115
+ md += `| Failed | ${bd.failedRuns} |\n`;
116
+ md += `| Not Tested | ${bd.notTestedRuns} |\n`;
117
+ md += `| Write Operations | ${bd.writeOperations} |\n`;
118
+ md += `| Executed At Most Once | ${bd.safeOperations} |\n`;
119
+ md += `| Duplicate Executions | ${bd.duplicateExecutions} |\n`;
120
+ md += `| retry_after Violations | ${bd.retryAfterViolations} |\n`;
121
+ md += `| Injected Errors | ${bd.injectedErrors} |\n`;
122
+ md += `| Ghost-Writes Injected | ${bd.ghostWritesInjected} |\n`;
123
+ md += `| Verified, No Retry | ${bd.verifiedNoRetry} |\n`;
124
+ md += `| Recovered By Retry | ${bd.recoveredByRetry} |\n`;
125
+ md += `| Abandoned After Failure | ${bd.abandonedAfterFailure} |\n`;
126
+ md += `| Unverified After Timeout | ${bd.unverifiedAfterTimeout} |\n`;
65
127
  return md;
66
128
  }
67
- function sum(reports, key) {
68
- return reports.reduce((acc, r) => acc + (r[key] ?? 0), 0);
69
- }
70
- function clamp(val, min, max) {
71
- return Math.max(min, Math.min(max, val));
72
- }
73
- //# sourceMappingURL=score.js.map