@ggui-ai/negotiator 0.2.0-alpha.3 → 0.3.0-rc.0

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 (101) hide show
  1. package/README.md +25 -19
  2. package/dist/contract-validators.d.ts +20 -16
  3. package/dist/contract-validators.d.ts.map +1 -1
  4. package/dist/contract-validators.js +11 -8
  5. package/dist/ensure-conforming-contract.d.ts +70 -0
  6. package/dist/ensure-conforming-contract.d.ts.map +1 -0
  7. package/dist/ensure-conforming-contract.js +115 -0
  8. package/dist/index.d.ts +22 -20
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +21 -14
  11. package/dist/llm-caller.d.ts +9 -9
  12. package/dist/llm-caller.d.ts.map +1 -1
  13. package/dist/llm-caller.js +8 -8
  14. package/dist/llm-rerank.d.ts +9 -8
  15. package/dist/llm-rerank.d.ts.map +1 -1
  16. package/dist/llm-rerank.js +4 -25
  17. package/dist/normalize-draft.d.ts +33 -0
  18. package/dist/normalize-draft.d.ts.map +1 -0
  19. package/dist/normalize-draft.js +167 -0
  20. package/dist/normalize-schema.d.ts.map +1 -1
  21. package/dist/normalize-schema.js +12 -0
  22. package/dist/preserve-seed-surfaces.d.ts +40 -0
  23. package/dist/preserve-seed-surfaces.d.ts.map +1 -0
  24. package/dist/preserve-seed-surfaces.js +57 -0
  25. package/dist/synth-bench/cache-key-probe.d.ts +3 -0
  26. package/dist/synth-bench/cache-key-probe.d.ts.map +1 -0
  27. package/dist/synth-bench/cache-key-probe.js +177 -0
  28. package/dist/synth-bench/cli-llm.d.ts +20 -0
  29. package/dist/synth-bench/cli-llm.d.ts.map +1 -0
  30. package/dist/synth-bench/cli-llm.js +97 -0
  31. package/dist/synth-bench/corpus.d.ts +52 -0
  32. package/dist/synth-bench/corpus.d.ts.map +1 -1
  33. package/dist/synth-bench/corpus.js +314 -5
  34. package/dist/synth-bench/round-trip-score.d.ts +87 -0
  35. package/dist/synth-bench/round-trip-score.d.ts.map +1 -0
  36. package/dist/synth-bench/round-trip-score.js +105 -0
  37. package/dist/synth-bench/run-bench-cli.js +6 -82
  38. package/dist/synth-bench/run-bench.js +2 -2
  39. package/dist/synth-bench/run-repair-bench-cli.d.ts +3 -0
  40. package/dist/synth-bench/run-repair-bench-cli.d.ts.map +1 -0
  41. package/dist/synth-bench/run-repair-bench-cli.js +86 -0
  42. package/dist/synth-bench/run-repair-bench.d.ts +94 -0
  43. package/dist/synth-bench/run-repair-bench.d.ts.map +1 -0
  44. package/dist/synth-bench/run-repair-bench.js +172 -0
  45. package/dist/synthesize-contract.d.ts +40 -9
  46. package/dist/synthesize-contract.d.ts.map +1 -1
  47. package/dist/synthesize-contract.js +266 -47
  48. package/package.json +9 -7
  49. package/src/contract-validators.ts +21 -17
  50. package/src/ensure-conforming-contract.ts +175 -0
  51. package/src/index.ts +22 -33
  52. package/src/llm-caller.ts +9 -9
  53. package/src/llm-rerank.ts +12 -17
  54. package/src/normalize-draft.ts +182 -0
  55. package/src/normalize-schema.ts +11 -0
  56. package/src/preserve-seed-surfaces.ts +61 -0
  57. package/src/synth-bench/cache-key-probe.ts +213 -0
  58. package/src/synth-bench/cli-llm.ts +140 -0
  59. package/src/synth-bench/corpus.ts +343 -5
  60. package/src/synth-bench/round-trip-score.ts +169 -0
  61. package/src/synth-bench/run-bench-cli.ts +13 -115
  62. package/src/synth-bench/run-bench.ts +2 -2
  63. package/src/synth-bench/run-repair-bench-cli.ts +119 -0
  64. package/src/synth-bench/run-repair-bench.ts +266 -0
  65. package/src/synthesize-contract.ts +323 -57
  66. package/dist/decision-input.d.ts +0 -51
  67. package/dist/decision-input.d.ts.map +0 -1
  68. package/dist/decision-input.js +0 -17
  69. package/dist/decision.d.ts +0 -58
  70. package/dist/decision.d.ts.map +0 -1
  71. package/dist/decision.js +0 -501
  72. package/dist/intent.d.ts +0 -22
  73. package/dist/intent.d.ts.map +0 -1
  74. package/dist/intent.js +0 -28
  75. package/dist/negotiate.d.ts +0 -142
  76. package/dist/negotiate.d.ts.map +0 -1
  77. package/dist/negotiate.js +0 -163
  78. package/dist/pure.d.ts +0 -30
  79. package/dist/pure.d.ts.map +0 -1
  80. package/dist/pure.js +0 -43
  81. package/dist/rag-search.d.ts +0 -73
  82. package/dist/rag-search.d.ts.map +0 -1
  83. package/dist/rag-search.js +0 -192
  84. package/dist/render.d.ts +0 -57
  85. package/dist/render.d.ts.map +0 -1
  86. package/dist/render.js +0 -25
  87. package/dist/suggestion.d.ts +0 -38
  88. package/dist/suggestion.d.ts.map +0 -1
  89. package/dist/suggestion.js +0 -47
  90. package/dist/types.d.ts +0 -30
  91. package/dist/types.d.ts.map +0 -1
  92. package/dist/types.js +0 -13
  93. package/src/decision-input.ts +0 -52
  94. package/src/decision.ts +0 -581
  95. package/src/intent.ts +0 -37
  96. package/src/negotiate.ts +0 -314
  97. package/src/pure.ts +0 -46
  98. package/src/rag-search.ts +0 -274
  99. package/src/render.ts +0 -56
  100. package/src/suggestion.ts +0 -73
  101. package/src/types.ts +0 -31
@@ -1 +1 @@
1
- {"version":3,"file":"llm-rerank.d.ts","sourceRoot":"","sources":["../src/llm-rerank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,QAAA,MAAM,oBAAoB,spCAQkM,CAAC;AAE7N,QAAA,MAAM,WAAW,EAAE,UA4BlB,CAAC;AAsCF,OAAO,EAAE,iBAAiB,EAAE,CAAC;AA8C7B;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;CAAE,EACjC,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,SAAS,eAAe,EAAE,GACrC,OAAO,CAAC,cAAc,CAAC,CAwDzB;AAGD,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"llm-rerank.d.ts","sourceRoot":"","sources":["../src/llm-rerank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,QAAA,MAAM,oBAAoB,03DAQ6U,CAAC;AAExW,QAAA,MAAM,WAAW,EAAE,UA4BlB,CAAC;AA6EF;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;CAAE,EACjC,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,SAAS,eAAe,EAAE,GACrC,OAAO,CAAC,cAAc,CAAC,CAwDzB;AAGD,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,CAAC"}
@@ -1,27 +1,12 @@
1
- /**
2
- * LLM rerank — Tier-2 precision oracle for the blueprint registry.
3
- *
4
- * Given a user's UI request (intent + contract structure) and a set
5
- * of candidate cached blueprints retrieved by RAG, ask a fast LLM
6
- * (Haiku 4.5) which candidate (if any) matches. Returns a structured
7
- * decision so the caller can branch deterministically.
8
- *
9
- * This module is the precision half of the blueprint-first
10
- * architecture: RAG retrieval is high-recall but low-precision (bge-
11
- * small confuses topic-similar but UI-divergent prompts); the LLM
12
- * judge restores precision. Combined break-even hit rate is ~10%;
13
- * realistic workloads observe 30-70%.
14
- */
15
- import { summarizeContract } from '@ggui-ai/protocol';
16
- const RERANK_SYSTEM_PROMPT = `You match user UI requests against previously-generated UI blueprints. Each blueprint was produced for a past request and stored. Decide whether any candidate produces the SAME USEFUL UI for the current request.
1
+ const RERANK_SYSTEM_PROMPT = `You match user UI requests against previously-generated UI blueprints. Each blueprint was produced for a past request and stored. Decide whether any candidate belongs to the SAME FAMILY as the current request — i.e. it would serve as a reasonable starting point that the requester can refine, not a pixel-exact replica.
17
2
 
18
- MATCH means the candidate would correctly satisfy the user's current request — same UI shape (component types, layout pattern), same wire surface (slot names, action names), same intended user task, and same load-bearing parameters (dates, months, ranges, enum values).
3
+ MATCH means the candidate is the same intended user task AND the same broad UI shape (component types, layout pattern). A candidate still MATCHES when the current request adds or omits fields, slots, or actions relative to the cached blueprint — a superset, a subset, or an overlapping wire surface all still match. Added or omitted fields/slots/actions DO NOT block a match and are NOT yours to judge: those wire-surface deltas are reconciled and reported to the agent separately, after you decide. Judge similarity of task and shape, never coverage of fields.
19
4
 
20
- NO-MATCH means the candidate would NOT satisfy the user's current request — different task (haiku composer vs tweet draft, login vs signup), different UI shape (form vs list vs dashboard), or load-bearing parameters differ (calendar-Jan vs calendar-Mar — same contract, different value).
5
+ NO-MATCH means the candidate is a fundamentally different thing — a different intended task (haiku composer vs tweet draft, login vs signup), a different UI shape (form vs list vs dashboard), OR a conflicting load-bearing fixed VALUE baked into the blueprint (calendar pinned to Jan vs a request for Mar — same contract shape, but the fixed value conflicts). Reserve NO-MATCH for these; do not decline a candidate merely because its fields, slots, or actions differ from the request.
21
6
 
22
7
  Visual style differences alone (minimal vs ornate, dense vs spacious) DO NOT block a match — the user can refine those after they get a working UI.
23
8
 
24
- Output exactly ONE tool call with your decision. confidence is a number in [0, 1]. matchId is a string from the candidate ids, or null when no candidate matches. reason is a short sentence the operator can use to debug.`;
9
+ Output exactly ONE tool call with your decision. confidence is a number in [0, 1] measuring how strongly the candidate is in the same task-and-shape family as the request (not how completely its fields cover the request). matchId is a string from the candidate ids, or null when no candidate matches. reason is a short sentence the operator can use to debug.`;
25
10
  const RERANK_TOOL = {
26
11
  name: 'submit_rerank_decision',
27
12
  description: 'Submit your match-vs-no-match decision over the candidates.',
@@ -72,12 +57,6 @@ function truncate(text, max) {
72
57
  return text;
73
58
  return `${text.slice(0, max - 1)}…`;
74
59
  }
75
- // Re-export `summarizeContract` from protocol for backwards-compatible
76
- // access through `@ggui-ai/negotiator/llm-rerank` consumers (the
77
- // canonical home is `@ggui-ai/protocol` — both the registry storage
78
- // and the rerank prompt depend on it). Kept as a re-export so the
79
- // existing test imports here keep working.
80
- export { summarizeContract };
81
60
  function clampConfidence(value) {
82
61
  if (typeof value !== 'number' || !Number.isFinite(value))
83
62
  return 0;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Deterministic draft normalization — the cheap, faithful repair tier.
3
+ *
4
+ * Most agent-draft malformations are MECHANICAL: a stray illegal key on
5
+ * a spec wrapper (a JSON-Schema-reflex `required: [...]` array on the
6
+ * propsSpec wrapper, an `additionalProperties` key), or a non-canonical
7
+ * schema `type` spelling (`"enum"`, `"integer"`). None of these needs an
8
+ * LLM to fix — and routing them through the LLM repair loop is both
9
+ * wasteful (a full regeneration) and RISKY (the model may re-author and
10
+ * reshape a 95%-correct draft, e.g. drop a propsSpec seed surface).
11
+ *
12
+ * This pass fixes the mechanical classes deterministically, preserving
13
+ * the agent's intent exactly:
14
+ * - strips keys the protocol's `.strict()` spec schemas would reject,
15
+ * keeping only the allowed keys at each wrapper / entry level;
16
+ * - canonicalizes every inner JSON Schema via {@link normalizeSchema}
17
+ * (the same normalizer `buildContract` runs on synth output).
18
+ *
19
+ * The caller (`ensureConformingContract`) re-lints the result: if it now
20
+ * passes the gate, the draft is returned WITHOUT ever calling the LLM.
21
+ * Semantic deficiencies (wrong placement, missing data surface, dangling
22
+ * cross-refs) are deliberately out of scope — those still go to the
23
+ * repair loop, where reasoning earns its keep.
24
+ */
25
+ /**
26
+ * Return a structurally-normalized copy of an untrusted draft: illegal
27
+ * wrapper/entry keys stripped, inner schemas canonicalized. Pure — never
28
+ * mutates the input, never throws. Unknown top-level fields ride through
29
+ * (the top-level DataContract schema is `.passthrough()`); only the
30
+ * `.strict()` spec wrappers and entries are cleaned.
31
+ */
32
+ export declare function normalizeDraft(draft: unknown): unknown;
33
+ //# sourceMappingURL=normalize-draft.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-draft.d.ts","sourceRoot":"","sources":["../src/normalize-draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA2GH;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA4CtD"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Deterministic draft normalization — the cheap, faithful repair tier.
3
+ *
4
+ * Most agent-draft malformations are MECHANICAL: a stray illegal key on
5
+ * a spec wrapper (a JSON-Schema-reflex `required: [...]` array on the
6
+ * propsSpec wrapper, an `additionalProperties` key), or a non-canonical
7
+ * schema `type` spelling (`"enum"`, `"integer"`). None of these needs an
8
+ * LLM to fix — and routing them through the LLM repair loop is both
9
+ * wasteful (a full regeneration) and RISKY (the model may re-author and
10
+ * reshape a 95%-correct draft, e.g. drop a propsSpec seed surface).
11
+ *
12
+ * This pass fixes the mechanical classes deterministically, preserving
13
+ * the agent's intent exactly:
14
+ * - strips keys the protocol's `.strict()` spec schemas would reject,
15
+ * keeping only the allowed keys at each wrapper / entry level;
16
+ * - canonicalizes every inner JSON Schema via {@link normalizeSchema}
17
+ * (the same normalizer `buildContract` runs on synth output).
18
+ *
19
+ * The caller (`ensureConformingContract`) re-lints the result: if it now
20
+ * passes the gate, the draft is returned WITHOUT ever calling the LLM.
21
+ * Semantic deficiencies (wrong placement, missing data surface, dangling
22
+ * cross-refs) are deliberately out of scope — those still go to the
23
+ * repair loop, where reasoning earns its keep.
24
+ */
25
+ import { normalizeSchema } from './normalize-schema.js';
26
+ // Allowed-key sets mirror the protocol `.strict()` schemas
27
+ // (schemas/data-contract.ts). Stripping anything outside these is safe:
28
+ // the strict schema would reject it as CTR_SHAPE_UNRECOGNIZED_KEYS.
29
+ const PROPS_WRAPPER_KEYS = new Set(['description', 'properties']);
30
+ const PROP_ENTRY_KEYS = new Set([
31
+ 'description',
32
+ 'schema',
33
+ 'required',
34
+ 'default',
35
+ 'example',
36
+ 'sourceTool',
37
+ ]);
38
+ const CONTEXT_ENTRY_KEYS = new Set([
39
+ 'description',
40
+ 'schema',
41
+ 'default',
42
+ 'debounceMs',
43
+ 'example',
44
+ ]);
45
+ const ACTION_ENTRY_KEYS = new Set([
46
+ 'description',
47
+ 'label',
48
+ 'schema',
49
+ 'example',
50
+ 'icon',
51
+ 'confirm',
52
+ 'nextStep',
53
+ ]);
54
+ const STREAM_ENTRY_KEYS = new Set(['description', 'schema', 'source']);
55
+ const AGENT_TOOL_KEYS = new Set(['serverInfo', 'toolInfo', 'usage', 'example']);
56
+ /** Inner keys of an {@link AgentToolEntry.toolInfo} (the MCP descriptor). */
57
+ const AGENT_TOOL_INFO_KEYS = new Set([
58
+ 'inputSchema',
59
+ 'description',
60
+ 'outputSchema',
61
+ ]);
62
+ function isRecord(value) {
63
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
64
+ }
65
+ /** Keep only `allowed` keys; normalize the schema-bearing fields named
66
+ * in `schemaFields`. Non-record entries pass through untouched. */
67
+ function cleanEntry(entry, allowed, schemaFields) {
68
+ if (!isRecord(entry))
69
+ return entry;
70
+ const out = {};
71
+ for (const [key, value] of Object.entries(entry)) {
72
+ if (!allowed.has(key))
73
+ continue; // strip the illegal key
74
+ out[key] =
75
+ schemaFields.includes(key) && value !== undefined
76
+ ? normalizeSchema(value)
77
+ : value;
78
+ }
79
+ return out;
80
+ }
81
+ /** Apply {@link cleanEntry} across a `Record<name, entry>` spec map. */
82
+ function cleanEntryMap(map, allowed, schemaFields) {
83
+ const out = {};
84
+ for (const [name, entry] of Object.entries(map)) {
85
+ out[name] = cleanEntry(entry, allowed, schemaFields);
86
+ }
87
+ return out;
88
+ }
89
+ /** Clean a single `AgentToolEntry`: keep only the allowed outer keys
90
+ * ({@link AGENT_TOOL_KEYS}), then clean the nested `toolInfo` to its
91
+ * inner keys ({@link AGENT_TOOL_INFO_KEYS}) and normalize
92
+ * `toolInfo.inputSchema` / `toolInfo.outputSchema` so the `.strict()`
93
+ * schema doesn't reject a stray nested key. Non-record entries pass
94
+ * through untouched. */
95
+ function cleanAgentToolEntry(entry) {
96
+ if (!isRecord(entry))
97
+ return entry;
98
+ const out = {};
99
+ for (const [key, value] of Object.entries(entry)) {
100
+ if (!AGENT_TOOL_KEYS.has(key))
101
+ continue; // strip the illegal outer key
102
+ out[key] =
103
+ key === 'toolInfo'
104
+ ? cleanEntry(value, AGENT_TOOL_INFO_KEYS, ['inputSchema', 'outputSchema'])
105
+ : value;
106
+ }
107
+ return out;
108
+ }
109
+ /** Apply {@link cleanAgentToolEntry} across the agent-tool catalog. */
110
+ function cleanAgentToolMap(map) {
111
+ const out = {};
112
+ for (const [name, entry] of Object.entries(map)) {
113
+ out[name] = cleanAgentToolEntry(entry);
114
+ }
115
+ return out;
116
+ }
117
+ /**
118
+ * Return a structurally-normalized copy of an untrusted draft: illegal
119
+ * wrapper/entry keys stripped, inner schemas canonicalized. Pure — never
120
+ * mutates the input, never throws. Unknown top-level fields ride through
121
+ * (the top-level DataContract schema is `.passthrough()`); only the
122
+ * `.strict()` spec wrappers and entries are cleaned.
123
+ */
124
+ export function normalizeDraft(draft) {
125
+ if (!isRecord(draft))
126
+ return draft;
127
+ const out = { ...draft };
128
+ // propsSpec wrapper: keep {description, properties}; clean each PropEntry.
129
+ if (isRecord(out['propsSpec'])) {
130
+ const ps = {};
131
+ for (const [key, value] of Object.entries(out['propsSpec'])) {
132
+ if (PROPS_WRAPPER_KEYS.has(key))
133
+ ps[key] = value;
134
+ }
135
+ if (isRecord(ps['properties'])) {
136
+ ps['properties'] = cleanEntryMap(ps['properties'], PROP_ENTRY_KEYS, [
137
+ 'schema',
138
+ ]);
139
+ }
140
+ out['propsSpec'] = ps;
141
+ }
142
+ if (isRecord(out['contextSpec'])) {
143
+ out['contextSpec'] = cleanEntryMap(out['contextSpec'], CONTEXT_ENTRY_KEYS, [
144
+ 'schema',
145
+ ]);
146
+ }
147
+ if (isRecord(out['actionSpec'])) {
148
+ out['actionSpec'] = cleanEntryMap(out['actionSpec'], ACTION_ENTRY_KEYS, [
149
+ 'schema',
150
+ ]);
151
+ }
152
+ if (isRecord(out['streamSpec'])) {
153
+ out['streamSpec'] = cleanEntryMap(out['streamSpec'], STREAM_ENTRY_KEYS, [
154
+ 'schema',
155
+ ]);
156
+ }
157
+ if (isRecord(out['agentCapabilities'])) {
158
+ const ac = out['agentCapabilities'];
159
+ if (isRecord(ac['tools'])) {
160
+ out['agentCapabilities'] = {
161
+ ...ac,
162
+ tools: cleanAgentToolMap(ac['tools']),
163
+ };
164
+ }
165
+ }
166
+ return out;
167
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"normalize-schema.d.ts","sourceRoot":"","sources":["../src/normalize-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAkJH;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CA2BxD"}
1
+ {"version":3,"file":"normalize-schema.d.ts","sourceRoot":"","sources":["../src/normalize-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA6JH;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CA2BxD"}
@@ -106,6 +106,18 @@ function normalizeTypeValue(value, enumSibling) {
106
106
  const alias = TYPE_ALIASES[lower];
107
107
  if (alias !== undefined)
108
108
  return alias;
109
+ if (lower.includes('|')) {
110
+ // Pipe-union string (`"STRING|null"`). Some models emit the union
111
+ // as one pipe-delimited string rather than the JSON Schema array
112
+ // form; recover the first valid non-null member, mirroring the
113
+ // array branch below (drops the nullable arm).
114
+ for (const member of lower.split('|')) {
115
+ const norm = normalizeTypeValue(member, enumSibling);
116
+ if (norm !== undefined && norm !== 'null')
117
+ return norm;
118
+ }
119
+ return undefined;
120
+ }
109
121
  // Unrecognized garbage — drop the constraint rather than guess.
110
122
  return undefined;
111
123
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Seed-surface preservation — the deterministic backstop that keeps the
3
+ * repair loop FAITHFUL.
4
+ *
5
+ * The agent's draft declares its agent-owned render-time data as
6
+ * `propsSpec.properties` (the only agent→client seed channel —
7
+ * `contextSpec` has no runtime seed path). A repair that drops or
8
+ * reshapes one of those keys away from propsSpec (the canonical
9
+ * `propsSpec.X → contextSpec.X` regression) produces a contract that is
10
+ * VALID (`lintContract` passes) yet round-trip-BROKEN: the agent can no
11
+ * longer seed X at render, so the UI renders empty.
12
+ *
13
+ * `lintContract` + the placement validators can't catch this — none of
14
+ * them sees the agent's DRAFT. These helpers do: they compare the
15
+ * repaired candidate against the draft and report which agent-owned seed
16
+ * surfaces went missing, so the synth loop can drive a corrective retry.
17
+ * Model-independent — it works even when a weak model keeps reshaping.
18
+ */
19
+ import type { DataContract } from '@ggui-ai/protocol';
20
+ /**
21
+ * The `propsSpec.properties` keys an agent declared on a (possibly
22
+ * malformed) draft — its agent-owned render-time SEED surfaces.
23
+ * Defensive: the draft is untrusted, so every level is probed before
24
+ * access. Returns `[]` for any non-propsSpec-bearing draft.
25
+ */
26
+ export declare function draftSeedPropKeys(draft: unknown): string[];
27
+ /**
28
+ * Agent-owned seed surfaces (propsSpec property keys) present in `draft`
29
+ * that the repaired `candidate` DROPPED — i.e. they are no longer
30
+ * seedable as a propsSpec property. Reshaping `propsSpec.X` to
31
+ * `contextSpec.X`, or dropping it entirely, both surface here (contextSpec
32
+ * is not an agent-seedable home). Returns `[]` when every declared seed
33
+ * surface survived (preservation holds).
34
+ *
35
+ * Preservation-biased on purpose: keeping a seed key the intent turned
36
+ * out not to need is harmless (an unsent optional prop); DROPPING one the
37
+ * agent relies on is the round-trip break. So we only ever flag drops.
38
+ */
39
+ export declare function findDroppedSeedSurfaces(draft: unknown, candidate: DataContract): string[];
40
+ //# sourceMappingURL=preserve-seed-surfaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preserve-seed-surfaces.d.ts","sourceRoot":"","sources":["../src/preserve-seed-surfaces.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAMtD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAO1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,YAAY,GACtB,MAAM,EAAE,CAKV"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Seed-surface preservation — the deterministic backstop that keeps the
3
+ * repair loop FAITHFUL.
4
+ *
5
+ * The agent's draft declares its agent-owned render-time data as
6
+ * `propsSpec.properties` (the only agent→client seed channel —
7
+ * `contextSpec` has no runtime seed path). A repair that drops or
8
+ * reshapes one of those keys away from propsSpec (the canonical
9
+ * `propsSpec.X → contextSpec.X` regression) produces a contract that is
10
+ * VALID (`lintContract` passes) yet round-trip-BROKEN: the agent can no
11
+ * longer seed X at render, so the UI renders empty.
12
+ *
13
+ * `lintContract` + the placement validators can't catch this — none of
14
+ * them sees the agent's DRAFT. These helpers do: they compare the
15
+ * repaired candidate against the draft and report which agent-owned seed
16
+ * surfaces went missing, so the synth loop can drive a corrective retry.
17
+ * Model-independent — it works even when a weak model keeps reshaping.
18
+ */
19
+ function isRecord(value) {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
21
+ }
22
+ /**
23
+ * The `propsSpec.properties` keys an agent declared on a (possibly
24
+ * malformed) draft — its agent-owned render-time SEED surfaces.
25
+ * Defensive: the draft is untrusted, so every level is probed before
26
+ * access. Returns `[]` for any non-propsSpec-bearing draft.
27
+ */
28
+ export function draftSeedPropKeys(draft) {
29
+ if (!isRecord(draft))
30
+ return [];
31
+ const propsSpec = draft['propsSpec'];
32
+ if (!isRecord(propsSpec))
33
+ return [];
34
+ const properties = propsSpec['properties'];
35
+ if (!isRecord(properties))
36
+ return [];
37
+ return Object.keys(properties);
38
+ }
39
+ /**
40
+ * Agent-owned seed surfaces (propsSpec property keys) present in `draft`
41
+ * that the repaired `candidate` DROPPED — i.e. they are no longer
42
+ * seedable as a propsSpec property. Reshaping `propsSpec.X` to
43
+ * `contextSpec.X`, or dropping it entirely, both surface here (contextSpec
44
+ * is not an agent-seedable home). Returns `[]` when every declared seed
45
+ * surface survived (preservation holds).
46
+ *
47
+ * Preservation-biased on purpose: keeping a seed key the intent turned
48
+ * out not to need is harmless (an unsent optional prop); DROPPING one the
49
+ * agent relies on is the round-trip break. So we only ever flag drops.
50
+ */
51
+ export function findDroppedSeedSurfaces(draft, candidate) {
52
+ const seedKeys = draftSeedPropKeys(draft);
53
+ if (seedKeys.length === 0)
54
+ return [];
55
+ const candidateProps = candidate.propsSpec?.properties ?? {};
56
+ return seedKeys.filter((key) => candidateProps[key] === undefined);
57
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cache-key-probe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-key-probe.d.ts","sourceRoot":"","sources":["../../src/synth-bench/cache-key-probe.ts"],"names":[],"mappings":""}
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Blueprint cache-key stability probe.
4
+ *
5
+ * The handshake blueprint cache hits ONLY on exact canonical-contract
6
+ * equality: `blueprintKey(contract)` = sha256 of `canonicalizeContracts`
7
+ * (RFC 8785), which preserves spec placement, action/slot/tool NAMES and
8
+ * schema shapes (it only normalizes key order / whitespace and strips
9
+ * `description`/`usage`). The semantic/fuzzy match is disabled whenever a
10
+ * contract is present. So two requests for the SAME intent reuse a cached
11
+ * blueprint only if the LLM authored a byte-identical canonical contract
12
+ * both times.
13
+ *
14
+ * This probe synthesizes one intent N times and reports:
15
+ * 1. how many DISTINCT blueprintKeys result (distinct > 1 ⇒ the
16
+ * exact-key cache structurally cannot hit on re-runs);
17
+ * 2. the KEY-AFFECTING diff — every leaf path where the runs disagree
18
+ * (descriptions/usage stripped, since the key ignores them) — so we
19
+ * can see WHETHER the variance is trivial (normalizable: required
20
+ * order, additionalProperties, optional fields) or semantic
21
+ * (property names / structure → only a semantic match can bridge it).
22
+ *
23
+ * Usage:
24
+ * pnpm -F @ggui-ai/negotiator cache-probe -- --intent "my todo items" --n 5
25
+ * pnpm -F @ggui-ai/negotiator cache-probe -- --n 5 --dump /tmp/cache-probe.json
26
+ */
27
+ import { writeFileSync } from 'node:fs';
28
+ import { blueprintKey } from '@ggui-ai/protocol/blueprint-key';
29
+ import { synthesizeContract } from '../synthesize-contract.js';
30
+ import { DEFAULT_MODEL, buildAnthropicLlmCaller, resolveAnthropicKey, } from './cli-llm.js';
31
+ function parseArgs(argv) {
32
+ let intent = 'my todo items';
33
+ let n = 3;
34
+ let model = DEFAULT_MODEL;
35
+ let dump;
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a === '--intent' && argv[i + 1])
39
+ intent = argv[++i];
40
+ else if (a === '--n' && argv[i + 1])
41
+ n = Number(argv[++i]);
42
+ else if (a === '--model' && argv[i + 1])
43
+ model = argv[++i];
44
+ else if (a === '--dump' && argv[i + 1])
45
+ dump = argv[++i];
46
+ }
47
+ return { intent, n, model, ...(dump !== undefined ? { dump } : {}) };
48
+ }
49
+ /** One-line canonical-surface summary — the bits blueprintKey is sensitive to. */
50
+ function shapeSummary(c) {
51
+ const parts = [];
52
+ const props = Object.keys(c.propsSpec?.properties ?? {});
53
+ const ctx = Object.keys(c.contextSpec ?? {});
54
+ const act = Object.keys(c.actionSpec ?? {});
55
+ const str = Object.keys(c.streamSpec ?? {});
56
+ const tools = Object.keys(c.agentCapabilities?.tools ?? {});
57
+ if (props.length)
58
+ parts.push(`props{${props.join(',')}}`);
59
+ if (ctx.length)
60
+ parts.push(`context{${ctx.join(',')}}`);
61
+ if (act.length)
62
+ parts.push(`action{${act.join(',')}}`);
63
+ if (str.length)
64
+ parts.push(`stream{${str.join(',')}}`);
65
+ if (tools.length)
66
+ parts.push(`tools{${tools.join(',')}}`);
67
+ return parts.join(' ') || '(empty)';
68
+ }
69
+ /**
70
+ * Flatten a contract to `path → json-encoded leaf`, applying the same
71
+ * domain rule the cache key does: drop string-valued `description` /
72
+ * `usage` (informational — `canonicalizeContracts` strips them). Objects
73
+ * recurse; arrays are encoded whole (order is key-affecting under JCS,
74
+ * which sorts object keys but NOT array elements).
75
+ */
76
+ function flattenKeyAffecting(value, prefix, out) {
77
+ if (Array.isArray(value)) {
78
+ out.set(prefix, JSON.stringify(value));
79
+ return;
80
+ }
81
+ if (value !== null && typeof value === 'object') {
82
+ for (const [k, v] of Object.entries(value)) {
83
+ if ((k === 'description' || k === 'usage') && typeof v === 'string') {
84
+ continue; // stripped by the cache key
85
+ }
86
+ flattenKeyAffecting(v, prefix ? `${prefix}.${k}` : k, out);
87
+ }
88
+ return;
89
+ }
90
+ out.set(prefix, JSON.stringify(value));
91
+ }
92
+ /** Every leaf path where the N contracts do not all agree, with the
93
+ * distinct per-run values (or `∅` when a run omits the path). */
94
+ function crossRunDiff(contracts) {
95
+ const flat = contracts.map((c) => {
96
+ const m = new Map();
97
+ flattenKeyAffecting(c, '', m);
98
+ return m;
99
+ });
100
+ const allPaths = new Set();
101
+ for (const m of flat)
102
+ for (const p of m.keys())
103
+ allPaths.add(p);
104
+ const divergent = [];
105
+ for (const path of [...allPaths].sort()) {
106
+ const values = flat.map((m) => m.get(path) ?? '∅');
107
+ if (new Set(values).size > 1)
108
+ divergent.push({ path, values });
109
+ }
110
+ return divergent;
111
+ }
112
+ async function main() {
113
+ const args = parseArgs(process.argv.slice(2));
114
+ const llm = buildAnthropicLlmCaller(resolveAnthropicKey('cache-probe'), args.model);
115
+ process.stdout.write(`cache-probe: intent=${JSON.stringify(args.intent)} n=${args.n} model=${args.model}\n\n`);
116
+ const contracts = [];
117
+ const keys = [];
118
+ for (let i = 0; i < args.n; i++) {
119
+ const synth = await synthesizeContract({ llm }, args.intent);
120
+ if (synth.contract === null) {
121
+ process.stdout.write(` run ${i + 1}: synth declined — ${synth.reason}\n`);
122
+ continue;
123
+ }
124
+ const key = blueprintKey(synth.contract);
125
+ contracts.push(synth.contract);
126
+ keys.push(key);
127
+ process.stdout.write(` run ${i + 1}: ${key} ${shapeSummary(synth.contract)}\n`);
128
+ }
129
+ const distinct = new Set(keys);
130
+ process.stdout.write(`\nDistinct blueprintKeys: ${distinct.size}/${keys.length}\n`);
131
+ if (distinct.size <= 1 && keys.length > 1) {
132
+ process.stdout.write('STABLE → the exact-key cache WOULD hit on a re-run of this intent.\n');
133
+ }
134
+ else if (keys.length > 1) {
135
+ process.stdout.write('UNSTABLE → same intent yields different canonical contracts → the exact-key\n' +
136
+ 'cache cannot hit; each request cold-gens a new blueprint.\n');
137
+ }
138
+ // Key-affecting cross-run diff — the "what varies" the user asked for.
139
+ if (contracts.length > 1) {
140
+ const diff = crossRunDiff(contracts);
141
+ process.stdout.write(`\n=== KEY-AFFECTING DIFF (${diff.length} divergent paths; descriptions stripped) ===\n`);
142
+ for (const { path, values } of diff) {
143
+ process.stdout.write(` ${path}\n`);
144
+ values.forEach((v, i) => {
145
+ const capped = v.length > 100 ? `${v.slice(0, 100)}…` : v;
146
+ process.stdout.write(` run${i + 1}: ${capped}\n`);
147
+ });
148
+ }
149
+ // Heuristic taxonomy hint.
150
+ const isArrayOrderOnly = diff.every(({ values }) => {
151
+ const parsed = values
152
+ .filter((v) => v !== '∅')
153
+ .map((v) => {
154
+ try {
155
+ return JSON.stringify([...JSON.parse(v)].sort());
156
+ }
157
+ catch {
158
+ return v;
159
+ }
160
+ });
161
+ return new Set(parsed).size === 1;
162
+ });
163
+ process.stdout.write(`\nHint: ${diff.length === 0
164
+ ? 'no key-affecting differences — variance must be elsewhere'
165
+ : isArrayOrderOnly
166
+ ? 'ALL divergent paths are arrays differing only in ORDER → a canonical sort in canonicalizeContracts would collapse these keys (cheap fix, no semantic match needed).'
167
+ : 'divergent paths include non-array-order differences (names / types / structure / presence) → a semantic intent-match is needed to bridge them (canonicalization alone cannot).'}\n`);
168
+ }
169
+ if (args.dump !== undefined) {
170
+ writeFileSync(args.dump, JSON.stringify({ intent: args.intent, model: args.model, keys, contracts }, null, 2));
171
+ process.stdout.write(`\nDumped ${contracts.length} contracts → ${args.dump}\n`);
172
+ }
173
+ }
174
+ main().catch((err) => {
175
+ process.stderr.write(`cache-probe failed: ${err instanceof Error ? err.message : String(err)}\n`);
176
+ process.exit(1);
177
+ });
@@ -0,0 +1,20 @@
1
+ import type { LLMCaller } from '../llm-caller.js';
2
+ /** Default bench model — matches the `ui-gen-default-haiku-4-5` slug. */
3
+ export declare const DEFAULT_MODEL = "claude-haiku-4-5";
4
+ /** Haiku 4.5 token pricing (USD per token) for the cost report. */
5
+ export declare const HAIKU_4_5_PRICE_INPUT_PER_TOKEN: number;
6
+ export declare const HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN: number;
7
+ /**
8
+ * Resolve the Anthropic API key: `ANTHROPIC_API_KEY` env var first, then
9
+ * `~/.ggui/credentials.json` at `apps.global.anthropic`. `scriptName`
10
+ * prefixes the error hints so a failure names the bench that needs it.
11
+ */
12
+ export declare function resolveAnthropicKey(scriptName: string): string;
13
+ /** Running token usage accumulated across `callStructured` calls in this
14
+ * process — read after a run for the cost line. */
15
+ export declare function getTokenUsage(): {
16
+ readonly input: number;
17
+ readonly output: number;
18
+ };
19
+ export declare function buildAnthropicLlmCaller(apiKey: string, model: string): LLMCaller;
20
+ //# sourceMappingURL=cli-llm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-llm.d.ts","sourceRoot":"","sources":["../../src/synth-bench/cli-llm.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,kBAAkB,CAAC;AAI9D,yEAAyE;AACzE,eAAO,MAAM,aAAa,qBAAqB,CAAC;AAEhD,mEAAmE;AACnE,eAAO,MAAM,+BAA+B,QAAkB,CAAC;AAC/D,eAAO,MAAM,gCAAgC,QAAkB,CAAC;AAMhE;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAmB9D;AAmBD;oDACoD;AACpD,wBAAgB,aAAa,IAAI;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAEA;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,SAAS,CA2DX"}