@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.
Files changed (43) hide show
  1. package/dist/ensure-conforming-contract.d.ts +70 -0
  2. package/dist/ensure-conforming-contract.d.ts.map +1 -0
  3. package/dist/ensure-conforming-contract.js +115 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/normalize-draft.d.ts +33 -0
  8. package/dist/normalize-draft.d.ts.map +1 -0
  9. package/dist/normalize-draft.js +143 -0
  10. package/dist/preserve-seed-surfaces.d.ts +40 -0
  11. package/dist/preserve-seed-surfaces.d.ts.map +1 -0
  12. package/dist/preserve-seed-surfaces.js +57 -0
  13. package/dist/synth-bench/cli-llm.d.ts +20 -0
  14. package/dist/synth-bench/cli-llm.d.ts.map +1 -0
  15. package/dist/synth-bench/cli-llm.js +97 -0
  16. package/dist/synth-bench/corpus.d.ts +52 -0
  17. package/dist/synth-bench/corpus.d.ts.map +1 -1
  18. package/dist/synth-bench/corpus.js +306 -5
  19. package/dist/synth-bench/round-trip-score.d.ts +87 -0
  20. package/dist/synth-bench/round-trip-score.d.ts.map +1 -0
  21. package/dist/synth-bench/round-trip-score.js +105 -0
  22. package/dist/synth-bench/run-bench-cli.js +6 -82
  23. package/dist/synth-bench/run-repair-bench-cli.d.ts +3 -0
  24. package/dist/synth-bench/run-repair-bench-cli.d.ts.map +1 -0
  25. package/dist/synth-bench/run-repair-bench-cli.js +86 -0
  26. package/dist/synth-bench/run-repair-bench.d.ts +94 -0
  27. package/dist/synth-bench/run-repair-bench.d.ts.map +1 -0
  28. package/dist/synth-bench/run-repair-bench.js +172 -0
  29. package/dist/synthesize-contract.d.ts +38 -6
  30. package/dist/synthesize-contract.d.ts.map +1 -1
  31. package/dist/synthesize-contract.js +246 -32
  32. package/package.json +5 -4
  33. package/src/ensure-conforming-contract.ts +175 -0
  34. package/src/index.ts +2 -0
  35. package/src/normalize-draft.ts +156 -0
  36. package/src/preserve-seed-surfaces.ts +61 -0
  37. package/src/synth-bench/cli-llm.ts +140 -0
  38. package/src/synth-bench/corpus.ts +335 -5
  39. package/src/synth-bench/round-trip-score.ts +169 -0
  40. package/src/synth-bench/run-bench-cli.ts +13 -115
  41. package/src/synth-bench/run-repair-bench-cli.ts +119 -0
  42. package/src/synth-bench/run-repair-bench.ts +266 -0
  43. 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
- const ANTHROPIC_API = 'https://api.anthropic.com/v1/messages';
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 totalCost = totalInputTokens * HAIKU_4_5_PRICE_INPUT_PER_TOKEN +
172
- totalOutputTokens * HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN;
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=${totalInputTokens} output=${totalOutputTokens}\n`);
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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=run-repair-bench-cli.d.ts.map
@@ -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, parse fails,
28
- * provider doesn't support `callStructured` → return `null`. Caller
29
- * falls back to an empty stub; behavior regresses to pre-synth but
30
- * doesn't crash.
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
- * Provider lacking `callStructured` (test stubs, providers without
78
- * tool-use) collapses to null.
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,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;AA+ND;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,UA+I7B,CAAC;AAsGF;;;;;;;;;;;;;;GAcG;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;CACnD,GACA,OAAO,CAAC,wBAAwB,CAAC,CAiKnC;AAgUD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,SAAS,GAC/C,MAAM,GAAG,SAAS,CA+CpB"}
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"}