@ggui-ai/negotiator 0.2.0-alpha.3 → 0.2.0-alpha.4
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/dist/ensure-conforming-contract.d.ts +70 -0
- package/dist/ensure-conforming-contract.d.ts.map +1 -0
- package/dist/ensure-conforming-contract.js +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/normalize-draft.d.ts +33 -0
- package/dist/normalize-draft.d.ts.map +1 -0
- package/dist/normalize-draft.js +143 -0
- package/dist/preserve-seed-surfaces.d.ts +40 -0
- package/dist/preserve-seed-surfaces.d.ts.map +1 -0
- package/dist/preserve-seed-surfaces.js +57 -0
- package/dist/synth-bench/cli-llm.d.ts +20 -0
- package/dist/synth-bench/cli-llm.d.ts.map +1 -0
- package/dist/synth-bench/cli-llm.js +97 -0
- package/dist/synth-bench/corpus.d.ts +52 -0
- package/dist/synth-bench/corpus.d.ts.map +1 -1
- package/dist/synth-bench/corpus.js +306 -5
- package/dist/synth-bench/round-trip-score.d.ts +87 -0
- package/dist/synth-bench/round-trip-score.d.ts.map +1 -0
- package/dist/synth-bench/round-trip-score.js +105 -0
- package/dist/synth-bench/run-bench-cli.js +6 -82
- package/dist/synth-bench/run-repair-bench-cli.d.ts +3 -0
- package/dist/synth-bench/run-repair-bench-cli.d.ts.map +1 -0
- package/dist/synth-bench/run-repair-bench-cli.js +86 -0
- package/dist/synth-bench/run-repair-bench.d.ts +94 -0
- package/dist/synth-bench/run-repair-bench.d.ts.map +1 -0
- package/dist/synth-bench/run-repair-bench.js +172 -0
- package/dist/synthesize-contract.d.ts +38 -6
- package/dist/synthesize-contract.d.ts.map +1 -1
- package/dist/synthesize-contract.js +246 -32
- package/package.json +5 -4
- package/src/ensure-conforming-contract.ts +175 -0
- package/src/index.ts +2 -0
- package/src/normalize-draft.ts +156 -0
- package/src/preserve-seed-surfaces.ts +61 -0
- package/src/synth-bench/cli-llm.ts +140 -0
- package/src/synth-bench/corpus.ts +335 -5
- package/src/synth-bench/round-trip-score.ts +169 -0
- package/src/synth-bench/run-bench-cli.ts +13 -115
- package/src/synth-bench/run-repair-bench-cli.ts +119 -0
- package/src/synth-bench/run-repair-bench.ts +266 -0
- package/src/synthesize-contract.ts +299 -37
|
@@ -18,84 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Bench-only — not exported from the package index.
|
|
20
20
|
*/
|
|
21
|
-
import { readFileSync } from 'node:fs';
|
|
22
|
-
import { homedir } from 'node:os';
|
|
23
|
-
import { resolve as pathResolve } from 'node:path';
|
|
24
21
|
import { evaluateAgainstCorpus, formatBenchReport } from './run-bench.js';
|
|
25
22
|
import { contractShape } from './corpus.js';
|
|
26
|
-
|
|
27
|
-
const DEFAULT_MODEL = 'claude-haiku-4-5';
|
|
28
|
-
function resolveAnthropicKey() {
|
|
29
|
-
const envKey = process.env['ANTHROPIC_API_KEY'];
|
|
30
|
-
if (envKey && envKey.length > 0)
|
|
31
|
-
return envKey;
|
|
32
|
-
const credsPath = pathResolve(homedir(), '.ggui', 'credentials.json');
|
|
33
|
-
let parsed;
|
|
34
|
-
try {
|
|
35
|
-
parsed = JSON.parse(readFileSync(credsPath, 'utf8'));
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
throw new Error(`bench-synth: could not read ${credsPath} (${err instanceof Error ? err.message : String(err)}). Set ANTHROPIC_API_KEY env var or run \`ggui auth set anthropic\`.`);
|
|
39
|
-
}
|
|
40
|
-
const key = parsed.apps?.global?.anthropic;
|
|
41
|
-
if (typeof key !== 'string' || key.length === 0) {
|
|
42
|
-
throw new Error(`bench-synth: no anthropic key found at apps.global.anthropic in ${credsPath}.`);
|
|
43
|
-
}
|
|
44
|
-
return key;
|
|
45
|
-
}
|
|
46
|
-
let totalInputTokens = 0;
|
|
47
|
-
let totalOutputTokens = 0;
|
|
48
|
-
function buildAnthropicLlmCaller(apiKey, model) {
|
|
49
|
-
return {
|
|
50
|
-
async call() {
|
|
51
|
-
throw new Error('bench-synth: text-mode not exercised — synth uses callStructured');
|
|
52
|
-
},
|
|
53
|
-
async callStructured(systemPrompt, userMessage, tool, maxTokens) {
|
|
54
|
-
// `temperature` deprecated on Haiku 4.5+ — Anthropic rejects with
|
|
55
|
-
// HTTP 400. `tool_choice: { type: 'tool', name }` below already
|
|
56
|
-
// binds output to the input_schema; residual stochasticity stays
|
|
57
|
-
// bounded via canonical-key normalization downstream.
|
|
58
|
-
const body = {
|
|
59
|
-
model,
|
|
60
|
-
max_tokens: maxTokens ?? 1024,
|
|
61
|
-
system: systemPrompt,
|
|
62
|
-
messages: [{ role: 'user', content: userMessage }],
|
|
63
|
-
tools: [
|
|
64
|
-
{
|
|
65
|
-
name: tool.name,
|
|
66
|
-
description: tool.description,
|
|
67
|
-
input_schema: tool.input_schema,
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
tool_choice: { type: 'tool', name: tool.name },
|
|
71
|
-
};
|
|
72
|
-
const res = await fetch(ANTHROPIC_API, {
|
|
73
|
-
method: 'POST',
|
|
74
|
-
headers: {
|
|
75
|
-
'content-type': 'application/json',
|
|
76
|
-
'x-api-key': apiKey,
|
|
77
|
-
'anthropic-version': '2023-06-01',
|
|
78
|
-
},
|
|
79
|
-
body: JSON.stringify(body),
|
|
80
|
-
});
|
|
81
|
-
const json = (await res.json());
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
const errType = json.error?.type ?? 'unknown';
|
|
84
|
-
const errMsg = json.error?.message ?? `HTTP ${res.status}`;
|
|
85
|
-
throw new Error(`anthropic ${errType}: ${errMsg}`);
|
|
86
|
-
}
|
|
87
|
-
if (json.usage) {
|
|
88
|
-
totalInputTokens += json.usage.input_tokens ?? 0;
|
|
89
|
-
totalOutputTokens += json.usage.output_tokens ?? 0;
|
|
90
|
-
}
|
|
91
|
-
const toolBlock = json.content?.find((b) => b.type === 'tool_use');
|
|
92
|
-
if (!toolBlock || toolBlock.input === undefined) {
|
|
93
|
-
throw new Error(`anthropic: no tool_use block in response (stop_reason=${json.stop_reason ?? 'unknown'})`);
|
|
94
|
-
}
|
|
95
|
-
return toolBlock.input;
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
23
|
+
import { DEFAULT_MODEL, HAIKU_4_5_PRICE_INPUT_PER_TOKEN, HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN, buildAnthropicLlmCaller, getTokenUsage, resolveAnthropicKey, } from './cli-llm.js';
|
|
99
24
|
const SHAPE_FILTERS = [
|
|
100
25
|
'props-only',
|
|
101
26
|
'context-only',
|
|
@@ -137,11 +62,9 @@ function parseArgs(argv) {
|
|
|
137
62
|
result.shapeFilter = shapeFilter;
|
|
138
63
|
return result;
|
|
139
64
|
}
|
|
140
|
-
const HAIKU_4_5_PRICE_INPUT_PER_TOKEN = 1.0 / 1_000_000;
|
|
141
|
-
const HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN = 5.0 / 1_000_000;
|
|
142
65
|
async function main() {
|
|
143
66
|
const args = parseArgs(process.argv.slice(2));
|
|
144
|
-
const apiKey = resolveAnthropicKey();
|
|
67
|
+
const apiKey = resolveAnthropicKey('bench-synth');
|
|
145
68
|
const llm = buildAnthropicLlmCaller(apiKey, args.model);
|
|
146
69
|
if (!args.json) {
|
|
147
70
|
process.stdout.write(`bench-synth: model=${args.model}\n\n`);
|
|
@@ -168,11 +91,12 @@ async function main() {
|
|
|
168
91
|
process.stdout.write('\n');
|
|
169
92
|
process.stdout.write(formatBenchReport(report));
|
|
170
93
|
process.stdout.write('\n\n');
|
|
171
|
-
const
|
|
172
|
-
|
|
94
|
+
const usage = getTokenUsage();
|
|
95
|
+
const totalCost = usage.input * HAIKU_4_5_PRICE_INPUT_PER_TOKEN +
|
|
96
|
+
usage.output * HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN;
|
|
173
97
|
const callsMade = report.totals.all - report.totals.synthDeclined;
|
|
174
98
|
const costPerCall = callsMade === 0 ? 0 : totalCost / callsMade;
|
|
175
|
-
process.stdout.write(`Tokens: input=${
|
|
99
|
+
process.stdout.write(`Tokens: input=${usage.input} output=${usage.output}\n`);
|
|
176
100
|
process.stdout.write(`Cost: total=$${totalCost.toFixed(4)} per-call=$${costPerCall.toFixed(4)}\n`);
|
|
177
101
|
}
|
|
178
102
|
main().catch((err) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-repair-bench-cli.d.ts","sourceRoot":"","sources":["../../src/synth-bench/run-repair-bench-cli.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Repair bench CLI — live round-trip-quality probe.
|
|
4
|
+
*
|
|
5
|
+
* Runs the production forgiving-handshake create-path
|
|
6
|
+
* (`ensureConformingContract`) over REPAIR_CORPUS and reports the
|
|
7
|
+
* round-trip-usable rate — the contract-quality number the shape bench
|
|
8
|
+
* (bench-synth) cannot see. Reads ~/.ggui/credentials.json (or
|
|
9
|
+
* ANTHROPIC_API_KEY) for the key.
|
|
10
|
+
*
|
|
11
|
+
* Costs ~$0.001 per repaired entry on Haiku 4.5 (clean drafts hit the
|
|
12
|
+
* lint-clean fast path and make NO LLM call). NOT run in CI; opt-in.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* pnpm -F @ggui-ai/negotiator bench-repair
|
|
16
|
+
* pnpm -F @ggui-ai/negotiator bench-repair -- --limit 1
|
|
17
|
+
* pnpm -F @ggui-ai/negotiator bench-repair -- --json > repair-report.json
|
|
18
|
+
* ANTHROPIC_API_KEY=sk-... pnpm -F @ggui-ai/negotiator bench-repair
|
|
19
|
+
*
|
|
20
|
+
* Bench-only — not exported from the package index.
|
|
21
|
+
*/
|
|
22
|
+
import { evaluateRepairCorpus, formatRepairBenchReport, } from './run-repair-bench.js';
|
|
23
|
+
import { DEFAULT_MODEL, HAIKU_4_5_PRICE_INPUT_PER_TOKEN, HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN, buildAnthropicLlmCaller, getTokenUsage, resolveAnthropicKey, } from './cli-llm.js';
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
let limit;
|
|
26
|
+
let model = DEFAULT_MODEL;
|
|
27
|
+
let json = false;
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
if (a === '--limit' && argv[i + 1]) {
|
|
31
|
+
limit = Number(argv[++i]);
|
|
32
|
+
}
|
|
33
|
+
else if (a === '--model' && argv[i + 1]) {
|
|
34
|
+
model = argv[++i];
|
|
35
|
+
}
|
|
36
|
+
else if (a === '--json') {
|
|
37
|
+
json = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const result = { model, json };
|
|
41
|
+
if (limit !== undefined)
|
|
42
|
+
result.limit = limit;
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
async function main() {
|
|
46
|
+
const args = parseArgs(process.argv.slice(2));
|
|
47
|
+
const apiKey = resolveAnthropicKey('bench-repair');
|
|
48
|
+
const llm = buildAnthropicLlmCaller(apiKey, args.model);
|
|
49
|
+
if (!args.json) {
|
|
50
|
+
process.stdout.write(`bench-repair: model=${args.model}\n\n`);
|
|
51
|
+
}
|
|
52
|
+
const report = await evaluateRepairCorpus({ llm }, {
|
|
53
|
+
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
|
54
|
+
onProgress: args.json
|
|
55
|
+
? undefined
|
|
56
|
+
: (outcome, idx, total) => {
|
|
57
|
+
const pass = outcome.roundTrip !== null
|
|
58
|
+
? outcome.roundTrip.pass
|
|
59
|
+
: outcome.shape.pass;
|
|
60
|
+
const status = pass ? 'OK ' : 'NO ';
|
|
61
|
+
const id = outcome.entry.id.padEnd(22);
|
|
62
|
+
const origin = `origin:${outcome.origin}`.padEnd(13);
|
|
63
|
+
process.stdout.write(`[${status}] ${(idx + 1).toString().padStart(2)}/${total} ${origin} ${id} ${outcome.latencyMs}ms\n`);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
if (args.json) {
|
|
67
|
+
process.stdout.write(JSON.stringify(report, null, 2));
|
|
68
|
+
process.stdout.write('\n');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
process.stdout.write('\n');
|
|
72
|
+
process.stdout.write(formatRepairBenchReport(report));
|
|
73
|
+
process.stdout.write('\n\n');
|
|
74
|
+
const usage = getTokenUsage();
|
|
75
|
+
const totalCost = usage.input * HAIKU_4_5_PRICE_INPUT_PER_TOKEN +
|
|
76
|
+
usage.output * HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN;
|
|
77
|
+
// Clean drafts (origin agent) make no LLM call — cost is per repaired entry.
|
|
78
|
+
const callsMade = report.totals.originSynth;
|
|
79
|
+
const costPerCall = callsMade === 0 ? 0 : totalCost / callsMade;
|
|
80
|
+
process.stdout.write(`Tokens: input=${usage.input} output=${usage.output}\n`);
|
|
81
|
+
process.stdout.write(`Cost: total=$${totalCost.toFixed(4)} per-repair=$${costPerCall.toFixed(4)}\n`);
|
|
82
|
+
}
|
|
83
|
+
main().catch((err) => {
|
|
84
|
+
process.stderr.write(`bench-repair failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair-path bench runner — the round-trip QUALITY probe.
|
|
3
|
+
*
|
|
4
|
+
* Where {@link evaluateAgainstCorpus} runs synthesize-from-intent and
|
|
5
|
+
* scores SHAPE, this runs the production forgiving-handshake create-path
|
|
6
|
+
* — `ensureConformingContract(draft, intent)` — over {@link
|
|
7
|
+
* REPAIR_CORPUS} and scores whether the produced contract is round-trip
|
|
8
|
+
* USABLE, not merely valid.
|
|
9
|
+
*
|
|
10
|
+
* Per entry it records:
|
|
11
|
+
* - `origin` — `agent` (draft was clean, returned verbatim via the
|
|
12
|
+
* fast path) vs `synth` (repaired in-place). The repair discriminator.
|
|
13
|
+
* - `shape` — {@link scoreSynthesizedContract} ride-along (which specs).
|
|
14
|
+
* - `roundTrip` — {@link scoreContractRoundTrip}: the headline signal.
|
|
15
|
+
* A reshape that breaks the agent's seed round-trip fails here even
|
|
16
|
+
* though `lintContract` passes the contract.
|
|
17
|
+
*
|
|
18
|
+
* The bench's pass verdict is the ROUND-TRIP score when an entry carries
|
|
19
|
+
* a round-trip expectation, falling back to shape otherwise. So the
|
|
20
|
+
* top-line precision answers "how often does the negotiator produce a
|
|
21
|
+
* round-trip-usable contract from an agent draft?" — the contract-
|
|
22
|
+
* quality number the shape bench cannot see.
|
|
23
|
+
*
|
|
24
|
+
* Live LLM probe — opt-in CLI (run-repair-bench-cli.ts), NOT in CI. The
|
|
25
|
+
* deterministic scorer pinning lives in round-trip-score.test.ts.
|
|
26
|
+
*/
|
|
27
|
+
import type { DataContract, SuggestionFinding } from '@ggui-ai/protocol';
|
|
28
|
+
import type { LLMCaller } from '../llm-caller.js';
|
|
29
|
+
import { type ScoreResult } from './run-bench.js';
|
|
30
|
+
import { type RoundTripScore } from './round-trip-score.js';
|
|
31
|
+
import { type BenchEntry } from './corpus.js';
|
|
32
|
+
export interface RepairBenchOutcome {
|
|
33
|
+
readonly entry: BenchEntry;
|
|
34
|
+
/** ensureConformingContract always returns a contract (possibly `{}`). */
|
|
35
|
+
readonly contract: DataContract;
|
|
36
|
+
/** `agent` = clean draft returned verbatim; `synth` = repaired in-place. */
|
|
37
|
+
readonly origin: 'agent' | 'synth';
|
|
38
|
+
/** How the contract was produced (the efficiency tier): verbatim /
|
|
39
|
+
* normalized (deterministic, no LLM) / llm-repair / fallback-empty. */
|
|
40
|
+
readonly method: 'verbatim' | 'normalized' | 'llm-repair' | 'fallback-empty';
|
|
41
|
+
/** Structural shape score (ride-along secondary signal). */
|
|
42
|
+
readonly shape: ScoreResult;
|
|
43
|
+
/** Round-trip usability — null when the entry declares no round-trip
|
|
44
|
+
* expectation (then `shape` carries the verdict). */
|
|
45
|
+
readonly roundTrip: RoundTripScore | null;
|
|
46
|
+
/** The error/warn findings the negotiator surfaced back to the agent. */
|
|
47
|
+
readonly findings: readonly SuggestionFinding[];
|
|
48
|
+
readonly reasoning: string;
|
|
49
|
+
readonly latencyMs: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* An outcome passes on its ROUND-TRIP score when one exists (the sharper
|
|
53
|
+
* gate), else on its shape score. Round-trip is the point of this bench.
|
|
54
|
+
*/
|
|
55
|
+
export declare function repairOutcomePass(outcome: RepairBenchOutcome): boolean;
|
|
56
|
+
export interface RepairBenchReport {
|
|
57
|
+
readonly outcomes: readonly RepairBenchOutcome[];
|
|
58
|
+
readonly totals: {
|
|
59
|
+
readonly all: number;
|
|
60
|
+
readonly pass: number;
|
|
61
|
+
readonly fail: number;
|
|
62
|
+
/** Drafts returned verbatim by the lint-clean fast path. */
|
|
63
|
+
readonly originAgent: number;
|
|
64
|
+
/** Drafts repaired in-place by the synth loop. */
|
|
65
|
+
readonly originSynth: number;
|
|
66
|
+
/** Entries carrying a round-trip expectation. */
|
|
67
|
+
readonly roundTripScored: number;
|
|
68
|
+
/** Of those, how many round-trip cleanly. */
|
|
69
|
+
readonly roundTripPass: number;
|
|
70
|
+
/** roundTripPass / roundTripScored — the contract-quality headline. */
|
|
71
|
+
readonly roundTripPrecision: number;
|
|
72
|
+
/** pass / all across all entries. */
|
|
73
|
+
readonly precision: number;
|
|
74
|
+
};
|
|
75
|
+
/** Histogram of round-trip failure kinds across the run. */
|
|
76
|
+
readonly byFailureKind: Readonly<Record<string, number>>;
|
|
77
|
+
readonly latency: {
|
|
78
|
+
readonly p50Ms: number;
|
|
79
|
+
readonly p95Ms: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export interface RunRepairBenchOptions {
|
|
83
|
+
readonly limit?: number;
|
|
84
|
+
readonly onProgress?: (outcome: RepairBenchOutcome, index: number, total: number) => void;
|
|
85
|
+
}
|
|
86
|
+
export declare function evaluateRepairCorpus(deps: {
|
|
87
|
+
readonly llm: LLMCaller;
|
|
88
|
+
}, options?: RunRepairBenchOptions, corpus?: readonly BenchEntry[]): Promise<RepairBenchReport>;
|
|
89
|
+
export declare function summarizeRepair(outcomes: readonly RepairBenchOutcome[]): RepairBenchReport;
|
|
90
|
+
export declare function formatRepairBenchReport(report: RepairBenchReport): string;
|
|
91
|
+
export declare function runRepairBench(deps: {
|
|
92
|
+
readonly llm: LLMCaller;
|
|
93
|
+
}, options?: RunRepairBenchOptions): Promise<RepairBenchReport>;
|
|
94
|
+
//# sourceMappingURL=run-repair-bench.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-repair-bench.d.ts","sourceRoot":"","sources":["../../src/synth-bench/run-repair-bench.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair-path bench runner — the round-trip QUALITY probe.
|
|
3
|
+
*
|
|
4
|
+
* Where {@link evaluateAgainstCorpus} runs synthesize-from-intent and
|
|
5
|
+
* scores SHAPE, this runs the production forgiving-handshake create-path
|
|
6
|
+
* — `ensureConformingContract(draft, intent)` — over {@link
|
|
7
|
+
* REPAIR_CORPUS} and scores whether the produced contract is round-trip
|
|
8
|
+
* USABLE, not merely valid.
|
|
9
|
+
*
|
|
10
|
+
* Per entry it records:
|
|
11
|
+
* - `origin` — `agent` (draft was clean, returned verbatim via the
|
|
12
|
+
* fast path) vs `synth` (repaired in-place). The repair discriminator.
|
|
13
|
+
* - `shape` — {@link scoreSynthesizedContract} ride-along (which specs).
|
|
14
|
+
* - `roundTrip` — {@link scoreContractRoundTrip}: the headline signal.
|
|
15
|
+
* A reshape that breaks the agent's seed round-trip fails here even
|
|
16
|
+
* though `lintContract` passes the contract.
|
|
17
|
+
*
|
|
18
|
+
* The bench's pass verdict is the ROUND-TRIP score when an entry carries
|
|
19
|
+
* a round-trip expectation, falling back to shape otherwise. So the
|
|
20
|
+
* top-line precision answers "how often does the negotiator produce a
|
|
21
|
+
* round-trip-usable contract from an agent draft?" — the contract-
|
|
22
|
+
* quality number the shape bench cannot see.
|
|
23
|
+
*
|
|
24
|
+
* Live LLM probe — opt-in CLI (run-repair-bench-cli.ts), NOT in CI. The
|
|
25
|
+
* deterministic scorer pinning lives in round-trip-score.test.ts.
|
|
26
|
+
*/
|
|
27
|
+
import { ensureConformingContract } from '../ensure-conforming-contract.js';
|
|
28
|
+
import { scoreSynthesizedContract } from './run-bench.js';
|
|
29
|
+
import { scoreContractRoundTrip, } from './round-trip-score.js';
|
|
30
|
+
import { REPAIR_CORPUS } from './corpus.js';
|
|
31
|
+
/**
|
|
32
|
+
* An outcome passes on its ROUND-TRIP score when one exists (the sharper
|
|
33
|
+
* gate), else on its shape score. Round-trip is the point of this bench.
|
|
34
|
+
*/
|
|
35
|
+
export function repairOutcomePass(outcome) {
|
|
36
|
+
return outcome.roundTrip !== null
|
|
37
|
+
? outcome.roundTrip.pass
|
|
38
|
+
: outcome.shape.pass;
|
|
39
|
+
}
|
|
40
|
+
export async function evaluateRepairCorpus(deps, options = {}, corpus = REPAIR_CORPUS) {
|
|
41
|
+
let subset = corpus;
|
|
42
|
+
if (options.limit !== undefined) {
|
|
43
|
+
subset = subset.slice(0, options.limit);
|
|
44
|
+
}
|
|
45
|
+
const outcomes = [];
|
|
46
|
+
for (let i = 0; i < subset.length; i++) {
|
|
47
|
+
const entry = subset[i];
|
|
48
|
+
const startedAt = Date.now();
|
|
49
|
+
// The real production create-path: lint the draft → verbatim if clean
|
|
50
|
+
// (origin agent), repair-in-place otherwise (origin synth). NEVER
|
|
51
|
+
// throws; an unrepairable draft yields the empty `{}` contract.
|
|
52
|
+
const result = await ensureConformingContract({ llm: deps.llm }, {
|
|
53
|
+
draft: entry.draft,
|
|
54
|
+
intent: entry.intent,
|
|
55
|
+
...(entry.appGadgets !== undefined
|
|
56
|
+
? { appGadgets: entry.appGadgets }
|
|
57
|
+
: {}),
|
|
58
|
+
});
|
|
59
|
+
const latencyMs = Date.now() - startedAt;
|
|
60
|
+
const shape = scoreSynthesizedContract(result.contract, entry.expected);
|
|
61
|
+
const roundTrip = entry.roundTrip !== undefined
|
|
62
|
+
? scoreContractRoundTrip(result.contract, entry.roundTrip)
|
|
63
|
+
: null;
|
|
64
|
+
const outcome = {
|
|
65
|
+
entry,
|
|
66
|
+
contract: result.contract,
|
|
67
|
+
origin: result.origin,
|
|
68
|
+
method: result.method,
|
|
69
|
+
shape,
|
|
70
|
+
roundTrip,
|
|
71
|
+
findings: result.findings,
|
|
72
|
+
reasoning: result.reasoning,
|
|
73
|
+
latencyMs,
|
|
74
|
+
};
|
|
75
|
+
outcomes.push(outcome);
|
|
76
|
+
options.onProgress?.(outcome, i, subset.length);
|
|
77
|
+
}
|
|
78
|
+
return summarizeRepair(outcomes);
|
|
79
|
+
}
|
|
80
|
+
export function summarizeRepair(outcomes) {
|
|
81
|
+
const all = outcomes.length;
|
|
82
|
+
const pass = outcomes.filter(repairOutcomePass).length;
|
|
83
|
+
const fail = all - pass;
|
|
84
|
+
const originAgent = outcomes.filter((o) => o.origin === 'agent').length;
|
|
85
|
+
const originSynth = outcomes.filter((o) => o.origin === 'synth').length;
|
|
86
|
+
const scored = outcomes.filter((o) => o.roundTrip !== null);
|
|
87
|
+
const roundTripScored = scored.length;
|
|
88
|
+
const roundTripPass = scored.filter((o) => o.roundTrip?.pass === true).length;
|
|
89
|
+
const roundTripPrecision = roundTripScored === 0 ? 0 : roundTripPass / roundTripScored;
|
|
90
|
+
const byFailureKind = {};
|
|
91
|
+
for (const o of outcomes) {
|
|
92
|
+
for (const f of o.roundTrip?.failures ?? []) {
|
|
93
|
+
byFailureKind[f.kind] = (byFailureKind[f.kind] ?? 0) + 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const latencies = outcomes.map((o) => o.latencyMs).sort((a, b) => a - b);
|
|
97
|
+
return {
|
|
98
|
+
outcomes,
|
|
99
|
+
totals: {
|
|
100
|
+
all,
|
|
101
|
+
pass,
|
|
102
|
+
fail,
|
|
103
|
+
originAgent,
|
|
104
|
+
originSynth,
|
|
105
|
+
roundTripScored,
|
|
106
|
+
roundTripPass,
|
|
107
|
+
roundTripPrecision,
|
|
108
|
+
precision: all === 0 ? 0 : pass / all,
|
|
109
|
+
},
|
|
110
|
+
byFailureKind,
|
|
111
|
+
latency: {
|
|
112
|
+
p50Ms: percentile(latencies, 0.5),
|
|
113
|
+
p95Ms: percentile(latencies, 0.95),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function percentile(sorted, p) {
|
|
118
|
+
if (sorted.length === 0)
|
|
119
|
+
return 0;
|
|
120
|
+
const idx = Math.min(sorted.length - 1, Math.floor(p * sorted.length));
|
|
121
|
+
return sorted[idx] ?? 0;
|
|
122
|
+
}
|
|
123
|
+
export function formatRepairBenchReport(report) {
|
|
124
|
+
const lines = [];
|
|
125
|
+
const t = report.totals;
|
|
126
|
+
lines.push('=== repair bench report (round-trip quality) ===');
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(`Round-trip usable: ${t.roundTripPass}/${t.roundTripScored} (${(t.roundTripPrecision * 100).toFixed(1)}%)`);
|
|
129
|
+
lines.push(`Repair origin: agent×${t.originAgent} synth×${t.originSynth}`);
|
|
130
|
+
// Efficiency tiers — verbatim + normalized are FREE (no LLM); only
|
|
131
|
+
// llm-repair pays a model call. A high normalized count = the cheap
|
|
132
|
+
// deterministic tier doing the work the LLM loop used to.
|
|
133
|
+
const byMethod = new Map();
|
|
134
|
+
for (const o of report.outcomes) {
|
|
135
|
+
byMethod.set(o.method, (byMethod.get(o.method) ?? 0) + 1);
|
|
136
|
+
}
|
|
137
|
+
const methodStr = ['verbatim', 'normalized', 'llm-repair', 'fallback-empty']
|
|
138
|
+
.filter((m) => byMethod.has(m))
|
|
139
|
+
.map((m) => `${m}×${byMethod.get(m)}`)
|
|
140
|
+
.join(' ');
|
|
141
|
+
lines.push(`Method (LLM cost): ${methodStr}`);
|
|
142
|
+
lines.push(`Overall pass: ${t.pass}/${t.all} (${(t.precision * 100).toFixed(1)}%)`);
|
|
143
|
+
lines.push(`Latency: p50=${report.latency.p50Ms}ms p95=${report.latency.p95Ms}ms`);
|
|
144
|
+
const kinds = Object.entries(report.byFailureKind);
|
|
145
|
+
if (kinds.length > 0) {
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push('Round-trip failures by kind:');
|
|
148
|
+
for (const [kind, count] of kinds.sort((a, b) => b[1] - a[1])) {
|
|
149
|
+
lines.push(` ${kind.padEnd(20)} ×${count}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const failed = report.outcomes.filter((o) => !repairOutcomePass(o));
|
|
153
|
+
if (failed.length > 0) {
|
|
154
|
+
lines.push('');
|
|
155
|
+
lines.push('Failures:');
|
|
156
|
+
for (const o of failed) {
|
|
157
|
+
lines.push(` [origin ${o.origin}] ${o.entry.id}: ${o.entry.intent.slice(0, 56)}`);
|
|
158
|
+
// Shape mismatches (gating only — advisory name checks omitted here).
|
|
159
|
+
for (const f of o.shape.failures) {
|
|
160
|
+
lines.push(` shape ${f.kind}: ${f.hint.slice(0, 120)}`);
|
|
161
|
+
}
|
|
162
|
+
for (const f of o.roundTrip?.failures ?? []) {
|
|
163
|
+
lines.push(` round-trip ${f.kind}: ${f.hint.slice(0, 160)}`);
|
|
164
|
+
}
|
|
165
|
+
lines.push(` reasoning: ${o.reasoning.slice(0, 120)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
export function runRepairBench(deps, options = {}) {
|
|
171
|
+
return evaluateRepairCorpus(deps, options);
|
|
172
|
+
}
|
|
@@ -24,10 +24,12 @@
|
|
|
24
24
|
* a richer surface should author the contract themselves on the
|
|
25
25
|
* handshake input; synthesis is a fallback, not a replacement.
|
|
26
26
|
*
|
|
27
|
-
* **Failure modes collapse to null.** LLM throws
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* **Failure modes collapse to null.** LLM throws on every attempt or
|
|
28
|
+
* the bounded repair budget is exhausted → return `null`. Caller falls
|
|
29
|
+
* back to an empty stub; behavior regresses to pre-synth but doesn't
|
|
30
|
+
* crash. Providers without `callStructured` (gemini / openai /
|
|
31
|
+
* openrouter) use a text-JSON fallback rather than skipping synthesis,
|
|
32
|
+
* so repair works on every provider.
|
|
31
33
|
*
|
|
32
34
|
* **Cost.** ~$0.0005-0.001 per call (Haiku 4.5, ~500 input + ~300
|
|
33
35
|
* output tokens). Latency ~1.5s. Fires only on cold-path Tier 3
|
|
@@ -74,8 +76,14 @@ export declare const SYNTHESIZE_TOOL: ToolSchema;
|
|
|
74
76
|
* Empty / whitespace intent short-circuits to null with a reason —
|
|
75
77
|
* no contract can be inferred from nothing.
|
|
76
78
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
+
* Providers without `callStructured` (gemini / openai / openrouter) use
|
|
80
|
+
* a text-JSON fallback (the validate-and-repair loop catches malformed
|
|
81
|
+
* output and retries) rather than skipping synthesis.
|
|
82
|
+
*
|
|
83
|
+
* When `options.draft` is supplied, the loop REPAIRS that draft in
|
|
84
|
+
* place (seeded with the agent's contract + the deterministic findings)
|
|
85
|
+
* instead of synthesizing from `intent` alone — the forgiving-handshake
|
|
86
|
+
* path.
|
|
79
87
|
*
|
|
80
88
|
* Each attempt is self-checked against the validation gate; a failure
|
|
81
89
|
* feeds the precise error back for up to {@link MAX_SYNTH_ATTEMPTS}
|
|
@@ -101,6 +109,30 @@ export declare function synthesizeContract(deps: {
|
|
|
101
109
|
* no-app-registry path).
|
|
102
110
|
*/
|
|
103
111
|
readonly appGadgets?: readonly GadgetDescriptor[];
|
|
112
|
+
/**
|
|
113
|
+
* Repair-in-place seed. When provided, the synthesizer does NOT
|
|
114
|
+
* synthesize from `intent` alone — it starts from the agent's
|
|
115
|
+
* proposed `draft` and the deterministic findings that rejected it,
|
|
116
|
+
* and the validate-and-repair loop corrects exactly those problems
|
|
117
|
+
* while preserving the agent's intent + structure. This is the
|
|
118
|
+
* forgiving-handshake path: an invalid agent draft is the loop's
|
|
119
|
+
* SEED rather than a thrown error. Absent ⇒ classic
|
|
120
|
+
* synthesize-from-intent (cold path). Typed `unknown` because the
|
|
121
|
+
* agent's draft is untrusted — it may not be a valid DataContract
|
|
122
|
+
* (that's the whole point of repairing it).
|
|
123
|
+
*/
|
|
124
|
+
readonly draft?: unknown;
|
|
125
|
+
/**
|
|
126
|
+
* Deterministic validation findings that rejected {@link draft}
|
|
127
|
+
* (from `lintContract(draft).errors`). Fed into the first repair
|
|
128
|
+
* note so the model corrects the precise problems. Ignored when
|
|
129
|
+
* `draft` is absent.
|
|
130
|
+
*/
|
|
131
|
+
readonly draftFindings?: readonly {
|
|
132
|
+
readonly code: string;
|
|
133
|
+
readonly path: string;
|
|
134
|
+
readonly message: string;
|
|
135
|
+
}[];
|
|
104
136
|
}): Promise<SynthesizeContractResult>;
|
|
105
137
|
/**
|
|
106
138
|
* Compose the "AVAILABLE GADGETS" section appended to synth's user
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synthesize-contract.d.ts","sourceRoot":"","sources":["../src/synthesize-contract.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"synthesize-contract.d.ts","sourceRoot":"","sources":["../src/synthesize-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAM7D,OAAO,EAKL,KAAK,yBAAyB,EAC/B,MAAM,0BAA0B,CAAC;AAsBlC,uCAAuC;AACvC,MAAM,WAAW,wBAAwB;IACvC,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IACvC,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,yBAAyB,EAAE,CAAC;CACzD;AAuOD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,UA+I7B,CAAC;AAwRF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;CAAE,EACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS;QAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;CACL,GACA,OAAO,CAAC,wBAAwB,CAAC,CAyMnC;AAgUD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,SAAS,GAC/C,MAAM,GAAG,SAAS,CA+CpB"}
|