@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
|
@@ -19,10 +19,32 @@
|
|
|
19
19
|
* - structure-bench.test.ts (deterministic validator-only check)
|
|
20
20
|
*/
|
|
21
21
|
import type { GadgetDescriptor } from '@ggui-ai/protocol';
|
|
22
|
+
import type { RoundTripExpectation } from './round-trip-score.js';
|
|
22
23
|
export interface BenchEntry {
|
|
23
24
|
readonly id: string;
|
|
24
25
|
readonly intent: string;
|
|
25
26
|
readonly expected: BenchExpectation;
|
|
27
|
+
/**
|
|
28
|
+
* Repair-path seed. When set, the entry exercises the FORGIVING
|
|
29
|
+
* handshake flow rather than synthesize-from-intent: the runner feeds
|
|
30
|
+
* this (possibly malformed / suboptimal) agent draft to
|
|
31
|
+
* `ensureConformingContract`, which lints it → returns verbatim if
|
|
32
|
+
* clean (origin `agent`) or repairs-in-place if not (origin `synth`).
|
|
33
|
+
* Mirrors what a real agent submits on `ggui_handshake.blueprintDraft`.
|
|
34
|
+
* Typed `unknown` because the draft is untrusted — it may not be a
|
|
35
|
+
* valid `DataContract` (that is the whole point of repairing it).
|
|
36
|
+
*
|
|
37
|
+
* Entries WITHOUT a draft run the classic synthesize-from-intent path.
|
|
38
|
+
*/
|
|
39
|
+
readonly draft?: unknown;
|
|
40
|
+
/**
|
|
41
|
+
* Round-trip quality expectation — what the agent intends to do with
|
|
42
|
+
* the AGREED contract next turn (seed props on `ggui_render`, consume
|
|
43
|
+
* gestures). The round-trip scorer asserts the produced contract
|
|
44
|
+
* actually supports it. Only meaningful alongside a `draft` (the
|
|
45
|
+
* repair corpus); leave absent for shape-only entries.
|
|
46
|
+
*/
|
|
47
|
+
readonly roundTrip?: RoundTripExpectation;
|
|
26
48
|
/**
|
|
27
49
|
* Per-entry registered gadget catalog. When set, the runner
|
|
28
50
|
* forwards this to `synthesizeContract`'s `appGadgets` option so
|
|
@@ -103,4 +125,34 @@ export interface BenchExpectation {
|
|
|
103
125
|
*/
|
|
104
126
|
export declare function contractShape(expected: BenchExpectation): string;
|
|
105
127
|
export declare const BENCH_CORPUS: readonly BenchEntry[];
|
|
128
|
+
/**
|
|
129
|
+
* Repair-path corpus — the round-trip QUALITY bench.
|
|
130
|
+
*
|
|
131
|
+
* Where {@link BENCH_CORPUS} asks "does synth emit the right SHAPE from
|
|
132
|
+
* an intent?", this corpus asks the contract-quality question: "when an
|
|
133
|
+
* agent submits a draft to `ggui_handshake`, does the negotiator's
|
|
134
|
+
* repair produce a contract that is round-trip-USABLE?" Each entry
|
|
135
|
+
* carries a real-shaped agent `draft` (run through the production
|
|
136
|
+
* `ensureConformingContract` create-path) plus a `roundTrip`
|
|
137
|
+
* expectation describing what the agent does next turn (seed props,
|
|
138
|
+
* consume gestures). The new round-trip scorer mirrors the render +
|
|
139
|
+
* consume wire gates, so a "valid-but-broken" repair fails here even
|
|
140
|
+
* though `lintContract` and the shape scorer pass it.
|
|
141
|
+
*
|
|
142
|
+
* The four entries map a 2×2 of (repair-fired?) × (round-trip-survives?):
|
|
143
|
+
* - `repair-todo-seed` — repair FIRES and BREAKS round-trip (RED today;
|
|
144
|
+
* the contract-quality falsifier — synth reshapes
|
|
145
|
+
* the seedable `todos` collection propsSpec →
|
|
146
|
+
* contextSpec, deleting the only agent seed channel).
|
|
147
|
+
* - `repair-todo-clean` — no repair needed (origin `agent`, fast-path);
|
|
148
|
+
* round-trip survives. Pins the no-false-positive case.
|
|
149
|
+
* - `repair-form-nextstep` — repair FIRES (dangling nextStep) and PRESERVES the
|
|
150
|
+
* action round-trip. A "good repair" of a non-seed error.
|
|
151
|
+
* - `repair-weather-seed` — repair FIRES (stray wrapper key) and PRESERVES the
|
|
152
|
+
* props round-trip. A "good repair" that keeps propsSpec.
|
|
153
|
+
*
|
|
154
|
+
* Used by run-repair-bench(-cli).ts (live LLM probe) and
|
|
155
|
+
* round-trip-score.test.ts (deterministic scorer pinning).
|
|
156
|
+
*/
|
|
157
|
+
export declare const REPAIR_CORPUS: readonly BenchEntry[];
|
|
106
158
|
//# sourceMappingURL=corpus.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"corpus.d.ts","sourceRoot":"","sources":["../../src/synth-bench/corpus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"corpus.d.ts","sourceRoot":"","sources":["../../src/synth-bench/corpus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,oBAAoB,CAAC;IAC1C;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IACzC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;+DAG2D;IAC3D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC;;;;2DAIuD;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C;;;0EAGsE;IACtE,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C;;;;;;;;;OASG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtD;;;;;sEAKkE;IAClE,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C;;qDAEiD;IACjD,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAOhE;AAoDD,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAs8B7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,UAAU,EA8Q9C,CAAC"}
|
|
@@ -325,12 +325,19 @@ export const BENCH_CORPUS = [
|
|
|
325
325
|
intent: 'an agent-backed todo list that persists across sessions',
|
|
326
326
|
expected: {
|
|
327
327
|
hasActionSpec: true,
|
|
328
|
-
hasContextSpec:
|
|
328
|
+
hasContextSpec: false,
|
|
329
329
|
hasStreamSpec: false,
|
|
330
|
-
hasProps:
|
|
331
|
-
actionNames: [
|
|
332
|
-
|
|
333
|
-
|
|
330
|
+
hasProps: true,
|
|
331
|
+
actionNames: [
|
|
332
|
+
'addTodo',
|
|
333
|
+
'deleteTodo',
|
|
334
|
+
'toggle',
|
|
335
|
+
'add',
|
|
336
|
+
'delete',
|
|
337
|
+
'create',
|
|
338
|
+
'remove',
|
|
339
|
+
],
|
|
340
|
+
notes: 'agent-backed/persisted = the agent OWNS the items → todos seed on propsSpec (refreshed via ggui_update); add/delete/toggle are discrete events on actionSpec. contextSpec has no agent-push channel, so an agent-owned persisted list there cannot round-trip — this is the round-trip-correct shape, aligned with list-message-thread / list-file-browser (both props-bearing agent-supplied collections).',
|
|
334
341
|
},
|
|
335
342
|
},
|
|
336
343
|
{
|
|
@@ -992,3 +999,297 @@ export const BENCH_CORPUS = [
|
|
|
992
999
|
appGadgets: [CHART_DESCRIPTOR],
|
|
993
1000
|
},
|
|
994
1001
|
];
|
|
1002
|
+
/**
|
|
1003
|
+
* Repair-path corpus — the round-trip QUALITY bench.
|
|
1004
|
+
*
|
|
1005
|
+
* Where {@link BENCH_CORPUS} asks "does synth emit the right SHAPE from
|
|
1006
|
+
* an intent?", this corpus asks the contract-quality question: "when an
|
|
1007
|
+
* agent submits a draft to `ggui_handshake`, does the negotiator's
|
|
1008
|
+
* repair produce a contract that is round-trip-USABLE?" Each entry
|
|
1009
|
+
* carries a real-shaped agent `draft` (run through the production
|
|
1010
|
+
* `ensureConformingContract` create-path) plus a `roundTrip`
|
|
1011
|
+
* expectation describing what the agent does next turn (seed props,
|
|
1012
|
+
* consume gestures). The new round-trip scorer mirrors the render +
|
|
1013
|
+
* consume wire gates, so a "valid-but-broken" repair fails here even
|
|
1014
|
+
* though `lintContract` and the shape scorer pass it.
|
|
1015
|
+
*
|
|
1016
|
+
* The four entries map a 2×2 of (repair-fired?) × (round-trip-survives?):
|
|
1017
|
+
* - `repair-todo-seed` — repair FIRES and BREAKS round-trip (RED today;
|
|
1018
|
+
* the contract-quality falsifier — synth reshapes
|
|
1019
|
+
* the seedable `todos` collection propsSpec →
|
|
1020
|
+
* contextSpec, deleting the only agent seed channel).
|
|
1021
|
+
* - `repair-todo-clean` — no repair needed (origin `agent`, fast-path);
|
|
1022
|
+
* round-trip survives. Pins the no-false-positive case.
|
|
1023
|
+
* - `repair-form-nextstep` — repair FIRES (dangling nextStep) and PRESERVES the
|
|
1024
|
+
* action round-trip. A "good repair" of a non-seed error.
|
|
1025
|
+
* - `repair-weather-seed` — repair FIRES (stray wrapper key) and PRESERVES the
|
|
1026
|
+
* props round-trip. A "good repair" that keeps propsSpec.
|
|
1027
|
+
*
|
|
1028
|
+
* Used by run-repair-bench(-cli).ts (live LLM probe) and
|
|
1029
|
+
* round-trip-score.test.ts (deterministic scorer pinning).
|
|
1030
|
+
*/
|
|
1031
|
+
export const REPAIR_CORPUS = [
|
|
1032
|
+
{
|
|
1033
|
+
// THE falsifier. The agent reaches for a JSON-Schema reflex — a
|
|
1034
|
+
// collection on propsSpec plus a wrapper-level `required` names-array
|
|
1035
|
+
// (illegal: required-ness is a per-prop boolean; the wrapper only
|
|
1036
|
+
// takes `description` + `properties`). That single
|
|
1037
|
+
// CTR_SHAPE_UNRECOGNIZED_KEYS bounces the draft off the fast path
|
|
1038
|
+
// into repair. The MINIMAL correct repair is "drop the stray
|
|
1039
|
+
// `required` key — `todos` is render-time seed data, it stays on
|
|
1040
|
+
// propsSpec." But synth's "a mutable collection is ALWAYS contextSpec"
|
|
1041
|
+
// heuristic instead reshapes propsSpec → contextSpec, producing a
|
|
1042
|
+
// VALID contract with no propsSpec — which the accept-path then drops
|
|
1043
|
+
// the agent's `props: { todos }` against. RED on BOTH the shape scorer
|
|
1044
|
+
// (has-props-mismatch) and the round-trip scorer (props-no-home) until
|
|
1045
|
+
// the negotiator learns seed data stays on propsSpec.
|
|
1046
|
+
id: 'repair-todo-seed',
|
|
1047
|
+
intent: 'show my todos, let me check off completed ones and add new items',
|
|
1048
|
+
draft: {
|
|
1049
|
+
propsSpec: {
|
|
1050
|
+
description: "The user's todo items",
|
|
1051
|
+
// ⚠️ FATAL: `required` is not a legal key on the propsSpec wrapper.
|
|
1052
|
+
required: ['todos'],
|
|
1053
|
+
properties: {
|
|
1054
|
+
todos: {
|
|
1055
|
+
required: true,
|
|
1056
|
+
schema: {
|
|
1057
|
+
type: 'array',
|
|
1058
|
+
items: {
|
|
1059
|
+
type: 'object',
|
|
1060
|
+
properties: {
|
|
1061
|
+
id: { type: 'string' },
|
|
1062
|
+
text: { type: 'string' },
|
|
1063
|
+
completed: { type: 'boolean' },
|
|
1064
|
+
},
|
|
1065
|
+
required: ['id', 'text', 'completed'],
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
actionSpec: {
|
|
1072
|
+
toggleTodo: {
|
|
1073
|
+
label: 'Toggle todo',
|
|
1074
|
+
schema: {
|
|
1075
|
+
type: 'object',
|
|
1076
|
+
properties: { id: { type: 'string' } },
|
|
1077
|
+
required: ['id'],
|
|
1078
|
+
},
|
|
1079
|
+
nextStep: 'todo_toggle',
|
|
1080
|
+
},
|
|
1081
|
+
addTodo: {
|
|
1082
|
+
label: 'Add todo',
|
|
1083
|
+
schema: {
|
|
1084
|
+
type: 'object',
|
|
1085
|
+
properties: { text: { type: 'string' } },
|
|
1086
|
+
required: ['text'],
|
|
1087
|
+
},
|
|
1088
|
+
nextStep: 'todo_add',
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
agentCapabilities: {
|
|
1092
|
+
tools: {
|
|
1093
|
+
todo_toggle: {
|
|
1094
|
+
description: 'Flip a todo done/undone',
|
|
1095
|
+
inputSchema: {
|
|
1096
|
+
type: 'object',
|
|
1097
|
+
properties: { id: { type: 'string' } },
|
|
1098
|
+
required: ['id'],
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
todo_add: {
|
|
1102
|
+
description: 'Append a new todo',
|
|
1103
|
+
inputSchema: {
|
|
1104
|
+
type: 'object',
|
|
1105
|
+
properties: { text: { type: 'string' } },
|
|
1106
|
+
required: ['text'],
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
expected: {
|
|
1113
|
+
// The GOOD outcome: todos stays seedable on propsSpec, toggle/add
|
|
1114
|
+
// stay as actions, NO contextSpec reshape.
|
|
1115
|
+
hasActionSpec: true,
|
|
1116
|
+
hasContextSpec: false,
|
|
1117
|
+
hasStreamSpec: false,
|
|
1118
|
+
hasProps: true,
|
|
1119
|
+
actionNames: ['toggle', 'add'],
|
|
1120
|
+
notes: 'contract-quality falsifier: synth currently reshapes the seedable todos collection propsSpec → contextSpec, breaking the agent seed round-trip. Expected GREEN once the negotiator preserves render-time seed data on propsSpec.',
|
|
1121
|
+
},
|
|
1122
|
+
roundTrip: {
|
|
1123
|
+
renderProps: {
|
|
1124
|
+
todos: [{ id: 't1', text: 'Buy milk', completed: false }],
|
|
1125
|
+
},
|
|
1126
|
+
consumableActions: ['toggleTodo', 'addTodo'],
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
// The fast-path / no-false-positive case. Same intent, but the agent
|
|
1131
|
+
// submitted a CLEAN, round-trip-correct draft (todos on propsSpec, no
|
|
1132
|
+
// stray key, resolvable nextStep cross-refs). ensureConformingContract
|
|
1133
|
+
// returns it VERBATIM (origin `agent`, no LLM call). The round-trip
|
|
1134
|
+
// scorer MUST pass — if it flags this, the scorer is over-eager.
|
|
1135
|
+
id: 'repair-todo-clean',
|
|
1136
|
+
intent: 'show my todos, let me check off completed ones and add new items',
|
|
1137
|
+
draft: {
|
|
1138
|
+
propsSpec: {
|
|
1139
|
+
description: "The user's todo items",
|
|
1140
|
+
properties: {
|
|
1141
|
+
todos: {
|
|
1142
|
+
required: true,
|
|
1143
|
+
schema: {
|
|
1144
|
+
type: 'array',
|
|
1145
|
+
items: {
|
|
1146
|
+
type: 'object',
|
|
1147
|
+
properties: {
|
|
1148
|
+
id: { type: 'string' },
|
|
1149
|
+
text: { type: 'string' },
|
|
1150
|
+
completed: { type: 'boolean' },
|
|
1151
|
+
},
|
|
1152
|
+
required: ['id', 'text', 'completed'],
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
actionSpec: {
|
|
1159
|
+
toggleTodo: {
|
|
1160
|
+
label: 'Toggle todo',
|
|
1161
|
+
schema: {
|
|
1162
|
+
type: 'object',
|
|
1163
|
+
properties: { id: { type: 'string' } },
|
|
1164
|
+
required: ['id'],
|
|
1165
|
+
},
|
|
1166
|
+
nextStep: 'todo_toggle',
|
|
1167
|
+
},
|
|
1168
|
+
addTodo: {
|
|
1169
|
+
label: 'Add todo',
|
|
1170
|
+
schema: {
|
|
1171
|
+
type: 'object',
|
|
1172
|
+
properties: { text: { type: 'string' } },
|
|
1173
|
+
required: ['text'],
|
|
1174
|
+
},
|
|
1175
|
+
nextStep: 'todo_add',
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
agentCapabilities: {
|
|
1179
|
+
tools: {
|
|
1180
|
+
todo_toggle: {
|
|
1181
|
+
description: 'Flip a todo done/undone',
|
|
1182
|
+
inputSchema: {
|
|
1183
|
+
type: 'object',
|
|
1184
|
+
properties: { id: { type: 'string' } },
|
|
1185
|
+
required: ['id'],
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1188
|
+
todo_add: {
|
|
1189
|
+
description: 'Append a new todo',
|
|
1190
|
+
inputSchema: {
|
|
1191
|
+
type: 'object',
|
|
1192
|
+
properties: { text: { type: 'string' } },
|
|
1193
|
+
required: ['text'],
|
|
1194
|
+
},
|
|
1195
|
+
},
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
expected: {
|
|
1200
|
+
hasActionSpec: true,
|
|
1201
|
+
hasContextSpec: false,
|
|
1202
|
+
hasStreamSpec: false,
|
|
1203
|
+
hasProps: true,
|
|
1204
|
+
actionNames: ['toggle', 'add'],
|
|
1205
|
+
notes: 'clean draft — must pass the fast path verbatim (origin agent) and the round-trip scorer. Guards against the scorer false-positiving on a good contract.',
|
|
1206
|
+
},
|
|
1207
|
+
roundTrip: {
|
|
1208
|
+
renderProps: {
|
|
1209
|
+
todos: [{ id: 't1', text: 'Buy milk', completed: false }],
|
|
1210
|
+
},
|
|
1211
|
+
consumableActions: ['toggleTodo', 'addTodo'],
|
|
1212
|
+
},
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
// Good repair of a NON-seed error. A contact form: fields are
|
|
1216
|
+
// client-originated draft state (contextSpec), submit is an action.
|
|
1217
|
+
// The draft's submit.nextStep points at a tool that isn't declared
|
|
1218
|
+
// (CTR_REF_NEXT_STEP dangling) → repair fires. The fix (add the tool
|
|
1219
|
+
// or drop the nextStep) has no reason to touch the `submit` action, so
|
|
1220
|
+
// the gesture round-trip survives. No seed props.
|
|
1221
|
+
id: 'repair-form-nextstep',
|
|
1222
|
+
intent: 'a contact form with name, email and message that sends to my backend',
|
|
1223
|
+
draft: {
|
|
1224
|
+
contextSpec: {
|
|
1225
|
+
name: { schema: { type: 'string' }, default: '' },
|
|
1226
|
+
email: { schema: { type: 'string' }, default: '' },
|
|
1227
|
+
message: { schema: { type: 'string' }, default: '' },
|
|
1228
|
+
},
|
|
1229
|
+
actionSpec: {
|
|
1230
|
+
submit: {
|
|
1231
|
+
label: 'Send message',
|
|
1232
|
+
schema: {
|
|
1233
|
+
type: 'object',
|
|
1234
|
+
properties: {
|
|
1235
|
+
name: { type: 'string' },
|
|
1236
|
+
email: { type: 'string' },
|
|
1237
|
+
message: { type: 'string' },
|
|
1238
|
+
},
|
|
1239
|
+
required: ['name', 'email', 'message'],
|
|
1240
|
+
},
|
|
1241
|
+
// ⚠️ dangling: no `send_message` tool declared below.
|
|
1242
|
+
nextStep: 'send_message',
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
expected: {
|
|
1247
|
+
hasActionSpec: true,
|
|
1248
|
+
hasContextSpec: true,
|
|
1249
|
+
hasStreamSpec: false,
|
|
1250
|
+
hasProps: false,
|
|
1251
|
+
actionNames: ['submit', 'send'],
|
|
1252
|
+
contextSlots: ['name', 'email', 'message'],
|
|
1253
|
+
notes: 'good repair of a dangling nextStep — the submit gesture must survive so the form round-trips.',
|
|
1254
|
+
},
|
|
1255
|
+
roundTrip: {
|
|
1256
|
+
consumableActions: ['submit'],
|
|
1257
|
+
},
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
// Good repair that must KEEP propsSpec. A weather card is the
|
|
1261
|
+
// canonical props-only display case; the agent seeds city/temp/
|
|
1262
|
+
// conditions. The draft carries a stray `additionalProperties` key on
|
|
1263
|
+
// the propsSpec wrapper (illegal — only description+properties allowed)
|
|
1264
|
+
// → CTR_SHAPE_UNRECOGNIZED_KEYS → repair fires. The fix drops the stray
|
|
1265
|
+
// key; propsSpec (and the seed round-trip) must survive.
|
|
1266
|
+
id: 'repair-weather-seed',
|
|
1267
|
+
intent: 'show a weather card for a city with temperature and conditions',
|
|
1268
|
+
draft: {
|
|
1269
|
+
propsSpec: {
|
|
1270
|
+
description: 'Current weather',
|
|
1271
|
+
// ⚠️ FATAL: not a legal key on the propsSpec wrapper.
|
|
1272
|
+
additionalProperties: false,
|
|
1273
|
+
properties: {
|
|
1274
|
+
city: { schema: { type: 'string' }, required: true },
|
|
1275
|
+
temp: { schema: { type: 'number' }, required: true },
|
|
1276
|
+
conditions: { schema: { type: 'string' }, required: true },
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
expected: {
|
|
1281
|
+
hasActionSpec: false,
|
|
1282
|
+
hasContextSpec: false,
|
|
1283
|
+
hasStreamSpec: false,
|
|
1284
|
+
hasProps: true,
|
|
1285
|
+
notes: 'good repair of a stray wrapper key — propsSpec must survive so the seed round-trips.',
|
|
1286
|
+
},
|
|
1287
|
+
roundTrip: {
|
|
1288
|
+
renderProps: {
|
|
1289
|
+
city: 'San Francisco',
|
|
1290
|
+
temp: 62,
|
|
1291
|
+
conditions: 'Foggy',
|
|
1292
|
+
},
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip quality scorer.
|
|
3
|
+
*
|
|
4
|
+
* The shape scorer ({@link scoreSynthesizedContract}) answers "did the
|
|
5
|
+
* negotiator emit the right SPECS?" — a validity floor. This scorer
|
|
6
|
+
* answers the harder question the contract-quality frontier cares
|
|
7
|
+
* about: "is the produced contract actually USABLE end-to-end?" — i.e.
|
|
8
|
+
* after handshake agrees on it, can the agent `ggui_render` its seed
|
|
9
|
+
* data, and `ggui_consume` its gestures, without the wire rejecting or
|
|
10
|
+
* silently dropping anything?
|
|
11
|
+
*
|
|
12
|
+
* Why a structural scorer can't see this: a contract that reshapes a
|
|
13
|
+
* seedable collection from `propsSpec` to `contextSpec` is STRUCTURALLY
|
|
14
|
+
* VALID (passes `lintContract`) and PASSES the shape scorer (it has a
|
|
15
|
+
* contextSpec). Yet it is round-trip-BROKEN: `propsSpec` is the only
|
|
16
|
+
* agent→client seed channel (and the only `ggui_update` target);
|
|
17
|
+
* `contextSpec` is client→agent observed state with NO runtime seed
|
|
18
|
+
* path (`contextSpec.default` is `useState` scaffold, not wire data).
|
|
19
|
+
* So the agent's `ggui_render({props:{todos:[…]}})` hits a contract
|
|
20
|
+
* with no `propsSpec` — the accept-path silently drops the props and
|
|
21
|
+
* the UI renders empty; the override-path hard-throws.
|
|
22
|
+
*
|
|
23
|
+
* This scorer mirrors that exact render-handler gate deterministically
|
|
24
|
+
* (no LLM, no iframe), so the harness catches the "valid-but-broken"
|
|
25
|
+
* class the shape scorer is blind to. The gate it mirrors lives at
|
|
26
|
+
* `mcp-server-handlers/src/renders/render.ts:1218-1269` (props-vs-
|
|
27
|
+
* propsSpec) and the consume action-declaration requirement at
|
|
28
|
+
* `consume.ts` / `submit-action.ts` (every gesture intent MUST be a
|
|
29
|
+
* declared `actionSpec` key).
|
|
30
|
+
*/
|
|
31
|
+
import type { DataContract } from '@ggui-ai/protocol';
|
|
32
|
+
/**
|
|
33
|
+
* What the agent intends to do with the agreed contract on the NEXT
|
|
34
|
+
* turn — the round-trip the produced contract must support. Authored
|
|
35
|
+
* per repair-corpus entry from the agent's original draft + intent.
|
|
36
|
+
*/
|
|
37
|
+
export interface RoundTripExpectation {
|
|
38
|
+
/**
|
|
39
|
+
* The props the agent intends to pass on `ggui_render` once the
|
|
40
|
+
* contract is agreed — its render-time SEED data. Every key here MUST
|
|
41
|
+
* land in `contract.propsSpec.properties[key]` (the ONLY agent→client
|
|
42
|
+
* seed channel — `contextSpec` has no runtime seed path), and the
|
|
43
|
+
* value MUST satisfy that prop's schema. Mirrors the render handler's
|
|
44
|
+
* props-vs-propsSpec gate. Omit / leave empty for contracts with no
|
|
45
|
+
* agent-seeded data (pure forms, broadcasts, counters).
|
|
46
|
+
*/
|
|
47
|
+
readonly renderProps?: Record<string, unknown>;
|
|
48
|
+
/**
|
|
49
|
+
* Gesture intents the agent expects to consume after render (e.g.
|
|
50
|
+
* `toggleTodo`, `submit`). Each MUST be a declared `actionSpec[*]`
|
|
51
|
+
* key, else the gesture is structurally unconsumable — the iframe has
|
|
52
|
+
* no declared intent to dispatch and `ggui_consume` never wakes.
|
|
53
|
+
*/
|
|
54
|
+
readonly consumableActions?: readonly string[];
|
|
55
|
+
}
|
|
56
|
+
export type RoundTripFailureKind =
|
|
57
|
+
/** Repair bailed to an empty `{}` contract — nothing round-trips. */
|
|
58
|
+
'contract-empty'
|
|
59
|
+
/**
|
|
60
|
+
* The agent would seed props but the produced contract declares NO
|
|
61
|
+
* `propsSpec` — the accept-path drops them / the override-path throws.
|
|
62
|
+
* The canonical reshape regression (propsSpec → contextSpec).
|
|
63
|
+
*/
|
|
64
|
+
| 'props-no-home'
|
|
65
|
+
/** A seed-prop key has no matching `propsSpec.properties` entry. */
|
|
66
|
+
| 'props-key-unhomed'
|
|
67
|
+
/** Seed props fail `validatePropsData` against the propsSpec (the
|
|
68
|
+
* real wire validator — wrong type, missing required, etc.). */
|
|
69
|
+
| 'props-rejected'
|
|
70
|
+
/** A consumable gesture intent is absent from `actionSpec`. */
|
|
71
|
+
| 'action-undeclared';
|
|
72
|
+
export interface RoundTripFailure {
|
|
73
|
+
readonly kind: RoundTripFailureKind;
|
|
74
|
+
readonly hint: string;
|
|
75
|
+
}
|
|
76
|
+
export interface RoundTripScore {
|
|
77
|
+
readonly pass: boolean;
|
|
78
|
+
readonly failures: readonly RoundTripFailure[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Score whether a produced contract supports the agent's intended
|
|
82
|
+
* round-trip. Pure / deterministic — mirrors the render + consume wire
|
|
83
|
+
* gates so a "valid-but-round-trip-broken" contract fails here even
|
|
84
|
+
* though `lintContract` and {@link scoreSynthesizedContract} pass it.
|
|
85
|
+
*/
|
|
86
|
+
export declare function scoreContractRoundTrip(contract: DataContract, rt: RoundTripExpectation): RoundTripScore;
|
|
87
|
+
//# sourceMappingURL=round-trip-score.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"round-trip-score.d.ts","sourceRoot":"","sources":["../../src/synth-bench/round-trip-score.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD;AAED,MAAM,MAAM,oBAAoB;AAC9B,qEAAqE;AACnE,gBAAgB;AAClB;;;;GAIG;GACD,eAAe;AACjB,oEAAoE;GAClE,mBAAmB;AACrB;iEACiE;GAC/D,gBAAgB;AAClB,+DAA+D;GAC7D,mBAAmB,CAAC;AAExB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAmBD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,YAAY,EACtB,EAAE,EAAE,oBAAoB,GACvB,cAAc,CAwDhB"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip quality scorer.
|
|
3
|
+
*
|
|
4
|
+
* The shape scorer ({@link scoreSynthesizedContract}) answers "did the
|
|
5
|
+
* negotiator emit the right SPECS?" — a validity floor. This scorer
|
|
6
|
+
* answers the harder question the contract-quality frontier cares
|
|
7
|
+
* about: "is the produced contract actually USABLE end-to-end?" — i.e.
|
|
8
|
+
* after handshake agrees on it, can the agent `ggui_render` its seed
|
|
9
|
+
* data, and `ggui_consume` its gestures, without the wire rejecting or
|
|
10
|
+
* silently dropping anything?
|
|
11
|
+
*
|
|
12
|
+
* Why a structural scorer can't see this: a contract that reshapes a
|
|
13
|
+
* seedable collection from `propsSpec` to `contextSpec` is STRUCTURALLY
|
|
14
|
+
* VALID (passes `lintContract`) and PASSES the shape scorer (it has a
|
|
15
|
+
* contextSpec). Yet it is round-trip-BROKEN: `propsSpec` is the only
|
|
16
|
+
* agent→client seed channel (and the only `ggui_update` target);
|
|
17
|
+
* `contextSpec` is client→agent observed state with NO runtime seed
|
|
18
|
+
* path (`contextSpec.default` is `useState` scaffold, not wire data).
|
|
19
|
+
* So the agent's `ggui_render({props:{todos:[…]}})` hits a contract
|
|
20
|
+
* with no `propsSpec` — the accept-path silently drops the props and
|
|
21
|
+
* the UI renders empty; the override-path hard-throws.
|
|
22
|
+
*
|
|
23
|
+
* This scorer mirrors that exact render-handler gate deterministically
|
|
24
|
+
* (no LLM, no iframe), so the harness catches the "valid-but-broken"
|
|
25
|
+
* class the shape scorer is blind to. The gate it mirrors lives at
|
|
26
|
+
* `mcp-server-handlers/src/renders/render.ts:1218-1269` (props-vs-
|
|
27
|
+
* propsSpec) and the consume action-declaration requirement at
|
|
28
|
+
* `consume.ts` / `submit-action.ts` (every gesture intent MUST be a
|
|
29
|
+
* declared `actionSpec` key).
|
|
30
|
+
*/
|
|
31
|
+
import { validatePropsData } from '@ggui-ai/protocol';
|
|
32
|
+
/**
|
|
33
|
+
* True when a contract declares none of the six spec surfaces — the
|
|
34
|
+
* `EMPTY_CONTRACT` (`{}`) that `ensureConformingContract` returns when a
|
|
35
|
+
* draft is unrepairable. Such a contract is structurally valid but
|
|
36
|
+
* carries no wire at all.
|
|
37
|
+
*/
|
|
38
|
+
function isEmptyContract(contract) {
|
|
39
|
+
return (contract.propsSpec === undefined &&
|
|
40
|
+
contract.actionSpec === undefined &&
|
|
41
|
+
contract.streamSpec === undefined &&
|
|
42
|
+
contract.contextSpec === undefined &&
|
|
43
|
+
contract.agentCapabilities === undefined &&
|
|
44
|
+
contract.clientCapabilities === undefined);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Score whether a produced contract supports the agent's intended
|
|
48
|
+
* round-trip. Pure / deterministic — mirrors the render + consume wire
|
|
49
|
+
* gates so a "valid-but-round-trip-broken" contract fails here even
|
|
50
|
+
* though `lintContract` and {@link scoreSynthesizedContract} pass it.
|
|
51
|
+
*/
|
|
52
|
+
export function scoreContractRoundTrip(contract, rt) {
|
|
53
|
+
const failures = [];
|
|
54
|
+
const renderProps = rt.renderProps ?? {};
|
|
55
|
+
const seedKeys = Object.keys(renderProps);
|
|
56
|
+
const consumable = rt.consumableActions ?? [];
|
|
57
|
+
// An empty contract round-trips nothing it was asked to carry.
|
|
58
|
+
if (isEmptyContract(contract) && (seedKeys.length > 0 || consumable.length > 0)) {
|
|
59
|
+
failures.push({
|
|
60
|
+
kind: 'contract-empty',
|
|
61
|
+
hint: `repair produced an empty contract ({}); the agent's seed data {${seedKeys.join(', ')}} and gestures [${consumable.join(', ')}] have no wire — the draft was unrepairable.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Props acceptance — mirrors render.ts:1218-1269 (the props-vs-propsSpec gate).
|
|
65
|
+
if (seedKeys.length > 0) {
|
|
66
|
+
const propsSpec = contract.propsSpec;
|
|
67
|
+
if (propsSpec === undefined) {
|
|
68
|
+
failures.push({
|
|
69
|
+
kind: 'props-no-home',
|
|
70
|
+
hint: `agent would seed props {${seedKeys.join(', ')}} on ggui_render, but the produced contract declares NO propsSpec. The accept-path silently drops them (render.ts:1233-1253) and the override-path throws (render.ts:1254-1268); the UI renders empty. A seedable collection was likely reshaped to contextSpec, which has no agent→client seed channel.`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const properties = propsSpec.properties ?? {};
|
|
75
|
+
const unhomed = seedKeys.filter((k) => properties[k] === undefined);
|
|
76
|
+
if (unhomed.length > 0) {
|
|
77
|
+
failures.push({
|
|
78
|
+
kind: 'props-key-unhomed',
|
|
79
|
+
hint: `seed-prop keys {${unhomed.join(', ')}} have no propsSpec.properties entry — ggui_render would reject them as undeclared props.`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// The real wire validator the render handler runs (Branch A).
|
|
83
|
+
const result = validatePropsData(renderProps, propsSpec);
|
|
84
|
+
if (!result.valid) {
|
|
85
|
+
failures.push({
|
|
86
|
+
kind: 'props-rejected',
|
|
87
|
+
hint: `validatePropsData rejected the agent's seed props against the produced propsSpec: ${result.violations
|
|
88
|
+
.map((v) => `${v.field}: ${v.message}`)
|
|
89
|
+
.join('; ')}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Consumable gestures — every intent the agent dispatches MUST be a
|
|
95
|
+
// declared actionSpec key, else the gesture is structurally undefined.
|
|
96
|
+
for (const action of consumable) {
|
|
97
|
+
if (contract.actionSpec?.[action] === undefined) {
|
|
98
|
+
failures.push({
|
|
99
|
+
kind: 'action-undeclared',
|
|
100
|
+
hint: `agent expects to consume gesture '${action}', but the produced contract's actionSpec has no '${action}' entry — the iframe has no declared intent to dispatch and ggui_consume never wakes.`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { pass: failures.length === 0, failures };
|
|
105
|
+
}
|