@ggui-ai/negotiator 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +49 -0
  3. package/dist/contract-hash.d.ts +54 -0
  4. package/dist/contract-hash.d.ts.map +1 -0
  5. package/dist/contract-hash.js +96 -0
  6. package/dist/contract-validators.d.ts +171 -0
  7. package/dist/contract-validators.d.ts.map +1 -0
  8. package/dist/contract-validators.js +478 -0
  9. package/dist/decision-input.d.ts +48 -0
  10. package/dist/decision-input.d.ts.map +1 -0
  11. package/dist/decision-input.js +14 -0
  12. package/dist/decision.d.ts +54 -0
  13. package/dist/decision.d.ts.map +1 -0
  14. package/dist/decision.js +500 -0
  15. package/dist/index.d.ts +36 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +25 -0
  18. package/dist/intent.d.ts +22 -0
  19. package/dist/intent.d.ts.map +1 -0
  20. package/dist/intent.js +28 -0
  21. package/dist/llm-caller.d.ts +70 -0
  22. package/dist/llm-caller.d.ts.map +1 -0
  23. package/dist/llm-caller.js +38 -0
  24. package/dist/llm-rerank.d.ts +101 -0
  25. package/dist/llm-rerank.d.ts.map +1 -0
  26. package/dist/llm-rerank.js +178 -0
  27. package/dist/negotiate.d.ts +141 -0
  28. package/dist/negotiate.d.ts.map +1 -0
  29. package/dist/negotiate.js +161 -0
  30. package/dist/normalize-schema.d.ts +22 -0
  31. package/dist/normalize-schema.d.ts.map +1 -0
  32. package/dist/normalize-schema.js +191 -0
  33. package/dist/pure.d.ts +30 -0
  34. package/dist/pure.d.ts.map +1 -0
  35. package/dist/pure.js +43 -0
  36. package/dist/rag-search.d.ts +73 -0
  37. package/dist/rag-search.d.ts.map +1 -0
  38. package/dist/rag-search.js +192 -0
  39. package/dist/rerank-eval/pairs.d.ts +28 -0
  40. package/dist/rerank-eval/pairs.d.ts.map +1 -0
  41. package/dist/rerank-eval/pairs.js +531 -0
  42. package/dist/rerank-eval/run-probe-cli.d.ts +3 -0
  43. package/dist/rerank-eval/run-probe-cli.d.ts.map +1 -0
  44. package/dist/rerank-eval/run-probe-cli.js +146 -0
  45. package/dist/rerank-eval/run-probe.d.ts +68 -0
  46. package/dist/rerank-eval/run-probe.d.ts.map +1 -0
  47. package/dist/rerank-eval/run-probe.js +113 -0
  48. package/dist/session.d.ts +42 -0
  49. package/dist/session.d.ts.map +1 -0
  50. package/dist/session.js +21 -0
  51. package/dist/suggestion.d.ts +38 -0
  52. package/dist/suggestion.d.ts.map +1 -0
  53. package/dist/suggestion.js +47 -0
  54. package/dist/synth-bench/corpus.d.ts +106 -0
  55. package/dist/synth-bench/corpus.d.ts.map +1 -0
  56. package/dist/synth-bench/corpus.js +994 -0
  57. package/dist/synth-bench/run-bench-cli.d.ts +3 -0
  58. package/dist/synth-bench/run-bench-cli.d.ts.map +1 -0
  59. package/dist/synth-bench/run-bench-cli.js +181 -0
  60. package/dist/synth-bench/run-bench.d.ts +101 -0
  61. package/dist/synth-bench/run-bench.d.ts.map +1 -0
  62. package/dist/synth-bench/run-bench.js +374 -0
  63. package/dist/synthesize-contract.d.ts +131 -0
  64. package/dist/synthesize-contract.d.ts.map +1 -0
  65. package/dist/synthesize-contract.js +948 -0
  66. package/dist/types.d.ts +30 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +13 -0
  69. package/package.json +74 -0
  70. package/src/contract-hash.ts +102 -0
  71. package/src/contract-validators.ts +604 -0
  72. package/src/decision-input.ts +49 -0
  73. package/src/decision.ts +581 -0
  74. package/src/index.ts +63 -0
  75. package/src/intent.ts +37 -0
  76. package/src/llm-caller.ts +82 -0
  77. package/src/llm-rerank.ts +280 -0
  78. package/src/negotiate.ts +312 -0
  79. package/src/normalize-schema.ts +193 -0
  80. package/src/pure.ts +46 -0
  81. package/src/rag-search.ts +274 -0
  82. package/src/rerank-eval/pairs.ts +624 -0
  83. package/src/rerank-eval/run-probe-cli.ts +197 -0
  84. package/src/rerank-eval/run-probe.ts +198 -0
  85. package/src/session.ts +41 -0
  86. package/src/suggestion.ts +73 -0
  87. package/src/synth-bench/corpus.ts +1126 -0
  88. package/src/synth-bench/run-bench-cli.ts +237 -0
  89. package/src/synth-bench/run-bench.ts +525 -0
  90. package/src/synthesize-contract.ts +1161 -0
  91. package/src/types.ts +31 -0
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Decision Engine — one LLM call, one UI decision.
3
+ *
4
+ * Replaces the V2 brainstorm/option-picker pattern with a single
5
+ * opinionated decision: `create` / `update` / `compose` / `replace`.
6
+ * The LLM sees the agent's data, the current session stack, any
7
+ * `blueprintCandidates` from `ragSearch`, and returns a
8
+ * {@link NegotiatorDecision} with a full {@link DataContract} payload.
9
+ *
10
+ * Contract shape — the returned `contract` always includes an
11
+ * `intent` (semantic identity — same intent = cached component).
12
+ * Other fields are populated opportunistically; `agentCapabilities` is
13
+ * always populated deterministically from `input.agentTools` after
14
+ * the LLM returns (see {@link mergeAgentCapabilities}). The
15
+ * `clientCapabilities.gadgets` catalog is similarly enriched from
16
+ * the per-app gadget list (see {@link mergeGadgets}).
17
+ *
18
+ * Structured output — prefers `llmCaller.callStructured?` with a
19
+ * forced-tool-use schema (guaranteed JSON). Falls back to text + regex
20
+ * JSON extraction if the caller doesn't support structured output, or
21
+ * if the structured call throws. On total parse failure, emits a
22
+ * fallback decision with `action: 'create'` built from the agent's
23
+ * data shape.
24
+ *
25
+ * ## Public surface + semver weight
26
+ *
27
+ * Exported:
28
+ * - `DECISION_SYSTEM_PROMPT` — the system prompt constant. Its
29
+ * content is part of the behavioral contract: changing the prompt
30
+ * changes what the model emits and therefore changes the cache
31
+ * identity of downstream generated blueprints. Treat content
32
+ * changes like `CRITERIA` ordering: CHANGELOG-worthy.
33
+ * - `buildDecisionUserMessage(input)` — pure string builder. Stable
34
+ * output for stable input.
35
+ * - `makeDecision(input, llmCaller)` — runtime orchestrator.
36
+ *
37
+ * Intentionally **not** exported:
38
+ * - `DECISION_TOOL` — the OpenAI-style tool schema handed to
39
+ * `callStructured`. Pinning its shape in the public API would
40
+ * freeze the tool-schema format consumers never need to see.
41
+ * - `mergeAgentCapabilities`, `mergeGadgets`, `buildFallbackDecision`,
42
+ * `inferType` — engine-internal helpers.
43
+ */
44
+ import { gadgetExportName, gadgetIdentityKey } from '@ggui-ai/protocol';
45
+ import { composeAvailableGadgetsSection } from './synthesize-contract.js';
46
+ export const DECISION_SYSTEM_PROMPT = `You are a UI strategist for ggui, a generative UI platform. Given the agent's data, current session state, and blueprint candidates, decide the best way to show this information.
47
+
48
+ Respond with a JSON object:
49
+ {
50
+ "action": "create" | "update" | "compose" | "replace",
51
+ "reasoning": "1-2 sentences explaining why",
52
+ "blueprintId": "matched blueprint ID or null",
53
+ "targetStackItemId": "existing page to update/compose/replace, or null",
54
+ "contract": {
55
+ "intent": "Concise purpose — e.g. 'Display current weather conditions for a quick daily check'",
56
+ "propsSpec": {
57
+ "properties": {
58
+ "fieldName": {
59
+ "description": "what this field is",
60
+ "schema": { "type": "string" },
61
+ "required": true,
62
+ "example": "sample value"
63
+ }
64
+ }
65
+ }
66
+ },
67
+ "adaptations": {
68
+ "fontSize": "compact" | "default" | "large",
69
+ "density": "dense" | "default" | "spacious",
70
+ "complexity": "simplified" | "default" | "detailed"
71
+ }
72
+ }
73
+
74
+ INTENT RULES (most important):
75
+ - The "intent" field captures WHY this UI exists in one sentence.
76
+ - Include: the user's goal (why), what data is shown (what), and how they interact (how).
77
+ - Be abstract enough to match reusable patterns — "Display current weather conditions" not "Display Tokyo weather at 3pm".
78
+ - Same intent = same component can be reused with different data.
79
+ - Examples:
80
+ - "Display current weather conditions for a quick daily check"
81
+ - "Collect user feedback via a multi-field survey form"
82
+ - "Show real-time stock prices with live updates"
83
+ - "Compare two products side by side for purchase decision"
84
+
85
+ DECISION RULES:
86
+ - "create": No existing UI or blueprint matches this intent. Show something new.
87
+ - "update": An existing UI on the stack has the same intent. Update its props.
88
+ - "compose": An existing UI could incorporate this data as a section.
89
+ - "replace": Two or more related UIs would be better as a single unified view.
90
+
91
+ REUSE BIAS (critical for performance):
92
+ - Reusing a blueprint = INSTANT render (cached code, <1 second).
93
+ - Creating new = 20+ seconds of generation. The user waits.
94
+ - Default to reuse. Only "create" when NO candidate can reasonably serve the request.
95
+ - A candidate that shows the SAME KIND of data (e.g., weather, stock prices, user profiles) is a match — even if the specific data differs (Tokyo vs Seoul, AAPL vs GOOG).
96
+ - Ask yourself: "Can this candidate display the agent's data with different prop values?" If yes → reuse it.
97
+ - When reusing a blueprint, copy its contract EXACTLY as-is (including its intent). Do NOT rephrase the intent.
98
+
99
+ CONTRACT RULES:
100
+ - Always include an "intent" field — it's required.
101
+ - If reusing a blueprint: use that blueprint's contract verbatim. Do not modify intent or propsSpec.
102
+ - If no blueprint match: infer propsSpec.properties from the agent's data shape. Each key becomes a prop.
103
+ - Use the data values as examples.
104
+
105
+ ACTION SPEC — declare interactive affordances WHENEVER you see them in the data:
106
+ - The discrimination is local-state vs persistent-state, NOT "did the agent declare agentTools".
107
+ - LOCAL STATE (counter value, theme toggle, slider position, picker selection, search-as-you-type, form draft fields): contextSpec only. NO actionSpec. The slot mirror IS the wire — the agent observes context via its next ggui_consume.
108
+ - PERSISTENT STATE (items with identity / IDs + mutable fields, draft submissions awaiting save, deletions of agent-owned rows): actionSpec required. Each gesture is a discrete event the agent must witness.
109
+ - Inference signals for persistent state:
110
+ • Items with \`id\`/\`itemId\`/\`uuid\` fields + boolean toggle fields like \`done\`/\`completed\`/\`checked\`/\`pinned\`/\`enabled\` → toggle action (e.g. \`toggleTodo\`, \`togglePin\`).
111
+ • Lists where the data shape implies the agent maintains identity → add/delete actions.
112
+ • Form data with mutable fields + a submit gesture → submit action.
113
+ • A click that would cause a server-side side-effect (publish, archive, send, delete-from-database) → action.
114
+ - nextStep is OPTIONAL on actionSpec entries:
115
+ • Bind \`nextStep: "tool_name"\` ONLY when input.agentTools contains a matching tool (exact or close name match — \`todo_toggle\` matches the \`toggleTodo\` action).
116
+ • If no agentTools match, OMIT nextStep. Events drain via ggui_consume on the agent's next turn — the agent's reasoning loop sees the event and decides what to do.
117
+ - Examples:
118
+ • agentTools=["todo_toggle","todo_delete"] + todo data → actionSpec: { "toggleTodo": { label: "Toggle", schema: {type:"object",properties:{id:{type:"string"}},required:["id"]}, nextStep: "todo_toggle" }, "deleteTodo": {...nextStep: "todo_delete"} }
119
+ • agentTools=[] + todo data → SAME actionSpec entries WITHOUT nextStep. The agent's next turn reads the event and reacts.
120
+ • agentTools=[] + counter prompt → contextSpec.count only. NO actionSpec.
121
+
122
+ AGENT CAPABILITIES (catalog):
123
+ - You do NOT need to emit contract.agentCapabilities — it is populated deterministically from input.agentTools after you return.
124
+ - Focus on actionSpec entries + their optional nextStep bindings; the catalog auto-populates.
125
+
126
+ ANTI-PATTERNS — DO NOT EMIT (cross-ref linter rejects at push):
127
+ - "props" / "props.properties" as a CONTRACT field (retired contract-side spelling — the contract field is propsSpec; the wire field on push/update is still "props" but carries VALUES, not the spec)
128
+ - "wiredTools" / "agentTools" / "clientTools" catalog names (retired; use agentCapabilities.tools / clientCapabilities.gadgets)
129
+ - clientCapabilities.capabilities (retired inner key; use clientCapabilities.gadgets)
130
+ - ActionEntry.tool / ActionEntry.dispatch.kind (retired discriminated union; use the flat nextStep field)
131
+ - mode: 'host-routed' / mode: 'agent-routed' (retired; all actions are agent-routed)
132
+ - broadcast: { ... } as a top-level field (retired; use streamSpec[X].source instead)`;
133
+ export function buildDecisionUserMessage(input) {
134
+ const parts = [];
135
+ // Agent's data
136
+ if (input.agentData) {
137
+ parts.push(`Agent's data:\n${JSON.stringify(input.agentData, null, 2)}`);
138
+ }
139
+ if (input.agentPrompt) {
140
+ parts.push(`Agent's hint: "${input.agentPrompt}"`);
141
+ }
142
+ if (input.agentContext) {
143
+ const ctx = typeof input.agentContext === 'string'
144
+ ? input.agentContext
145
+ : JSON.stringify(input.agentContext);
146
+ parts.push(`Agent's context: ${ctx}`);
147
+ }
148
+ // Session state
149
+ const { sessionState } = input;
150
+ if (sessionState.stack.length > 0) {
151
+ const stackSummary = sessionState.stack
152
+ .map((item) => ` [${item.id}] ${item.prompt ?? 'no prompt'}`)
153
+ .join('\n');
154
+ parts.push(`Current UI stack (${sessionState.stack.length} items):\n${stackSummary}`);
155
+ }
156
+ else {
157
+ parts.push('Current UI stack: empty');
158
+ }
159
+ if (sessionState.conversationHistory.length > 0) {
160
+ const recent = sessionState.conversationHistory.slice(-5);
161
+ parts.push(`Recent conversation:\n${recent.map((t) => ` ${t.role}: ${t.content}`).join('\n')}`);
162
+ }
163
+ // Agent tools — MCP tools the agent invokes; component never calls these.
164
+ // When tools ARE listed, prefer binding matching actionSpec entries to them
165
+ // via `nextStep`. When tools are absent BUT the data implies interactivity,
166
+ // still declare actionSpec — events drain via ggui_consume on the next turn.
167
+ if (input.agentTools?.length) {
168
+ parts.push(`Agent-side MCP tools (bind matching actionSpec entries' nextStep to these names; the agent invokes the tool on its next turn):\n` +
169
+ input.agentTools.map((t) => ` • ${t}`).join('\n'));
170
+ }
171
+ else {
172
+ parts.push(`Agent-side MCP tools: none declared. If the data implies interactivity (items with id + mutable fields, draft submissions, deletions of agent-owned rows), STILL declare actionSpec entries — just omit nextStep. The agent's next turn drains events via ggui_consume and reacts.`);
173
+ }
174
+ // Client-side gadget catalog — browser-capability hooks AND
175
+ // operator-registered 3rd-party plugins (Leaflet, Mapbox, Stripe, …)
176
+ // the runtime can serve for this app. Declare bindings under
177
+ // `clientCapabilities.gadgets` ONLY when the produced UI actually
178
+ // imports the hook.
179
+ //
180
+ // Routes through {@link composeAvailableGadgetsSection} so the
181
+ // decision LLM sees the same teaching text the synth-only path
182
+ // does — `description` (what), `usage` (when), with bounded
183
+ // per-entry + total budget. Both the decision path and the
184
+ // synth-only path share this one composer, so they agree on what
185
+ // the LLM is told about each gadget.
186
+ if (input.gadgets?.length) {
187
+ // `composeAvailableGadgetsSection` is component-aware — it
188
+ // flattens the package-keyed `GadgetDescriptor[]` catalog itself
189
+ // and renders hook AND component exports with their render idiom
190
+ // (call vs JSX) + package name. No pre-filter / pre-flatten here.
191
+ const gadgetsSection = composeAvailableGadgetsSection(input.gadgets);
192
+ if (gadgetsSection !== undefined) {
193
+ // Suffix the binding rule the previous flat-loop carried — the
194
+ // composer's section header doesn't say "declare under
195
+ // clientCapabilities.gadgets"; preserve that authoring nudge
196
+ // for the LLM. The permission column is intentionally dropped
197
+ // from the LLM's view: the operator's `App.gadgets`
198
+ // catalog still owns permission policy at registration time and
199
+ // push-time enrichment merges it back in.
200
+ parts.push(`${gadgetsSection}\n\nDeclare each chosen gadget under clientCapabilities.gadgets[<packageName>][<exportName>] = {} — the package name keys the outer map, the export name (use-prefixed hook or PascalCase component) keys the inner map. Declare ONLY when the produced UI imports the export.`);
201
+ }
202
+ }
203
+ // Blueprint candidates (with contract + intents when available)
204
+ if (input.blueprintCandidates.length > 0) {
205
+ const candidates = input.blueprintCandidates
206
+ .map((c) => {
207
+ let line = ` [${c.blueprintId}] ${c.description} (${c.verdict}, ${Math.round(c.similarity * 100)}% match)`;
208
+ line += ` — REUSE = instant render, CREATE NEW = 20s wait`;
209
+ if (c.contract) {
210
+ line += `\n contract: ${JSON.stringify(c.contract)}`;
211
+ }
212
+ return line;
213
+ })
214
+ .join('\n');
215
+ parts.push(`Blueprint candidates (reuse any of these for instant render):\n${candidates}`);
216
+ }
217
+ else {
218
+ parts.push('Blueprint candidates: none (no cached components — "create" will generate new)');
219
+ }
220
+ return parts.join('\n\n');
221
+ }
222
+ /**
223
+ * Tool schema for structured decision output.
224
+ *
225
+ * Matches @ggui-ai/protocol's NegotiatorDecision + DataContract types.
226
+ * Uses forced tool_choice to guarantee valid JSON output from the LLM.
227
+ *
228
+ * Module-internal — intentionally NOT exported. See the public-surface
229
+ * section of the top docstring.
230
+ */
231
+ const DECISION_TOOL = {
232
+ name: 'ui_decision',
233
+ description: 'Output the UI decision with a full data contract.',
234
+ input_schema: {
235
+ type: 'object',
236
+ properties: {
237
+ action: {
238
+ type: 'string',
239
+ enum: ['create', 'update', 'compose', 'replace'],
240
+ description: 'What to do: create (new UI), update (swap data), compose (add to stack), replace (swap UI type)',
241
+ },
242
+ reasoning: { type: 'string', description: 'Brief explanation' },
243
+ blueprintId: {
244
+ type: 'string',
245
+ description: 'Blueprint ID from candidates to reuse. Omit to generate new.',
246
+ },
247
+ contract: {
248
+ type: 'object',
249
+ description: 'Data contract — defines the UI shape and the four wire surfaces (propsSpec / actionSpec / contextSpec / streamSpec)',
250
+ properties: {
251
+ intent: {
252
+ type: 'string',
253
+ description: 'Concise reusable purpose — e.g. "Display weather conditions". Same intent = cached component.',
254
+ },
255
+ propsSpec: {
256
+ type: 'object',
257
+ description: 'Props contract — initial render data',
258
+ properties: {
259
+ description: { type: 'string', description: 'What this data represents' },
260
+ properties: {
261
+ type: 'object',
262
+ description: 'Per-prop definitions keyed by name. Each: { description, schema: { type }, required, example }',
263
+ additionalProperties: {
264
+ type: 'object',
265
+ properties: {
266
+ description: { type: 'string' },
267
+ schema: { type: 'object', properties: { type: { type: 'string' } } },
268
+ required: { type: 'boolean' },
269
+ example: {},
270
+ },
271
+ },
272
+ },
273
+ },
274
+ },
275
+ actionSpec: {
276
+ type: 'object',
277
+ description: 'Action contract — flat map keyed by actionId. Each value describes one user-interaction event the UI can emit. Every action is agent-routed. Optional "nextStep" names the MCP tool the AGENT should invoke on its next turn after the event fires.',
278
+ additionalProperties: {
279
+ type: 'object',
280
+ properties: {
281
+ description: { type: 'string' },
282
+ label: { type: 'string', description: 'Button label' },
283
+ schema: { type: 'object' },
284
+ icon: { type: 'string', description: 'Icon hint (emoji or name)' },
285
+ nextStep: {
286
+ type: 'string',
287
+ description: 'MCP tool name the agent SHOULD call after the user fires this action (must appear in agentCapabilities.tools).',
288
+ },
289
+ },
290
+ },
291
+ },
292
+ streamSpec: {
293
+ type: 'object',
294
+ description: 'Stream contract — flat map keyed by channel name. Declares typed live channels the component consumes. Each value is a StreamChannelEntry describing schema + optional source.tool (the agent-tool name whose deliveries feed this channel).',
295
+ additionalProperties: { type: 'object' },
296
+ },
297
+ },
298
+ required: ['intent'],
299
+ },
300
+ targetStackItemId: {
301
+ type: 'string',
302
+ description: 'Page to target (update/replace actions)',
303
+ },
304
+ adaptations: {
305
+ type: 'object',
306
+ description: 'UI adaptations based on device/context',
307
+ properties: {
308
+ fontSize: { type: 'string', enum: ['compact', 'default', 'large'] },
309
+ density: { type: 'string', enum: ['dense', 'default', 'spacious'] },
310
+ complexity: { type: 'string', enum: ['simplified', 'default', 'detailed'] },
311
+ },
312
+ },
313
+ },
314
+ required: ['action', 'reasoning', 'contract'],
315
+ },
316
+ };
317
+ /** Make the UI decision via one LLM call with all context. */
318
+ export async function makeDecision(input, llmCaller) {
319
+ const userMessage = buildDecisionUserMessage(input);
320
+ // Prefer structured output (tool use) — guaranteed valid JSON
321
+ if (llmCaller.callStructured) {
322
+ try {
323
+ const parsed = await llmCaller.callStructured(DECISION_SYSTEM_PROMPT, userMessage, DECISION_TOOL, 2048);
324
+ return {
325
+ decision: {
326
+ action: parsed.action ?? 'create',
327
+ reasoning: parsed.reasoning ?? 'Structured output',
328
+ blueprintId: parsed.blueprintId ?? undefined,
329
+ contract: mergeGadgets(mergeAgentCapabilities({ ...parsed.contract }, input.agentTools), input.gadgets),
330
+ targetStackItemId: parsed.targetStackItemId ?? undefined,
331
+ adaptations: parsed.adaptations ?? undefined,
332
+ },
333
+ alternatives: [],
334
+ };
335
+ }
336
+ catch (err) {
337
+ console.warn('[decision] Structured output failed, falling back to text:', err.message);
338
+ }
339
+ }
340
+ // Fallback: regex JSON extraction from raw text
341
+ const raw = await llmCaller.call(DECISION_SYSTEM_PROMPT, userMessage, 2048);
342
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
343
+ if (!jsonMatch) {
344
+ return { decision: buildFallbackDecision(input), alternatives: [] };
345
+ }
346
+ try {
347
+ const parsed = JSON.parse(jsonMatch[0]);
348
+ return {
349
+ decision: {
350
+ action: parsed.action ?? 'create',
351
+ reasoning: parsed.reasoning ?? 'No reasoning provided',
352
+ blueprintId: parsed.blueprintId ?? undefined,
353
+ contract: mergeGadgets(mergeAgentCapabilities({ ...parsed.contract }, input.agentTools), input.gadgets),
354
+ targetStackItemId: parsed.targetStackItemId ?? undefined,
355
+ adaptations: parsed.adaptations ?? undefined,
356
+ },
357
+ alternatives: [],
358
+ };
359
+ }
360
+ catch {
361
+ return { decision: buildFallbackDecision(input), alternatives: [] };
362
+ }
363
+ }
364
+ /**
365
+ * Deterministically populate `contract.agentCapabilities.tools` from
366
+ * `input.agentTools`.
367
+ *
368
+ * The LLM decides which agent tools become user-triggered actions
369
+ * (`actionSpec[*].nextStep`), but the contract-level catalog — the
370
+ * canonical list of tool names the agent MAY invoke — is a superset
371
+ * of the input and not something the LLM needs to restate. Populating
372
+ * it here closes the gap where the contract under-declared its
373
+ * agent-tool surface.
374
+ *
375
+ * Merge behavior:
376
+ * - If the LLM returned a `contract.agentCapabilities`, union its entries with input names.
377
+ * - Input-only names get a minimal `AgentToolEntry` (description only).
378
+ * - LLM-declared entries are preserved verbatim (may carry richer schema metadata).
379
+ */
380
+ function mergeAgentCapabilities(contract, inputAgentTools) {
381
+ if (!inputAgentTools?.length && !contract.agentCapabilities)
382
+ return contract;
383
+ const existing = contract.agentCapabilities?.tools ?? {};
384
+ const merged = { ...existing };
385
+ for (const name of inputAgentTools ?? []) {
386
+ if (!merged[name]) {
387
+ merged[name] = { description: `Agent-provided MCP tool: ${name}` };
388
+ }
389
+ }
390
+ const spec = {
391
+ tools: merged,
392
+ };
393
+ return { ...contract, agentCapabilities: spec };
394
+ }
395
+ /**
396
+ * Deterministically enrich `contract.clientCapabilities.gadgets` from
397
+ * the per-app gadget catalog (`app.gadgets`).
398
+ *
399
+ * Per-app `gadgets` declares which browser-capability gadgets the
400
+ * runtime can serve for the app. The LLM authors a per-contract subset
401
+ * (only the gadgets the produced UI actually uses) on the
402
+ * package-keyed wire map (`Record<package, Record<exportName,
403
+ * GadgetExportUse>>`), but partial entries (no `description` / `usage`
404
+ * override) are common — this merge fills in the canonical teaching
405
+ * text from the app catalog.
406
+ *
407
+ * Merge behavior:
408
+ * - Each wire `(package, exportName)` use that resolves to an
409
+ * app-catalog export inherits the registered export's
410
+ * `description` / `usage` (when the wire use didn't override them).
411
+ * - Wire uses that do NOT resolve to any app-catalog export are
412
+ * preserved verbatim (the app may carry a third-party gadget the
413
+ * catalog passed in here doesn't include).
414
+ * - The contract's set of declared `(package, export)` uses stays
415
+ * exactly what the LLM authored — we don't append every app-catalog
416
+ * export, since the contract only declares what the produced UI
417
+ * references.
418
+ * - LLM-authored fields win on conflict (operator may have authored a
419
+ * richer description/usage for the use at this specific call site).
420
+ *
421
+ * Identity keying routes through `gadgetIdentityKey` so the merge
422
+ * agrees byte-for-byte with the push-time gates on what "the same
423
+ * gadget export" means.
424
+ */
425
+ function mergeGadgets(contract, appGadgets) {
426
+ if (!appGadgets?.length || !contract.clientCapabilities?.gadgets) {
427
+ return contract;
428
+ }
429
+ // Index every registered export by its `(name, package)` identity
430
+ // tuple. A descriptor-side `GadgetExport` carries no package
431
+ // identity, so combine `gadgetExportName(exp)` with the descriptor's
432
+ // own `package` to form the key the wire side keys by too.
433
+ const byExport = new Map();
434
+ for (const pkg of appGadgets) {
435
+ for (const exp of pkg.exports) {
436
+ byExport.set(gadgetIdentityKey({
437
+ name: gadgetExportName(exp),
438
+ package: pkg.package,
439
+ }), exp);
440
+ }
441
+ }
442
+ const enriched = {};
443
+ for (const [pkgName, packageUse] of Object.entries(contract.clientCapabilities.gadgets)) {
444
+ const enrichedPackage = {};
445
+ for (const [exportName, use] of Object.entries(packageUse)) {
446
+ const canonical = byExport.get(gadgetIdentityKey({ name: exportName, package: pkgName }));
447
+ if (canonical) {
448
+ enrichedPackage[exportName] = {
449
+ // Registered export's teaching text as the base...
450
+ ...(canonical.description !== undefined
451
+ ? { description: canonical.description }
452
+ : {}),
453
+ ...(canonical.usage !== undefined ? { usage: canonical.usage } : {}),
454
+ // ...then the wire use — LLM-authored fields win on conflict.
455
+ ...use,
456
+ };
457
+ }
458
+ else {
459
+ enrichedPackage[exportName] = use;
460
+ }
461
+ }
462
+ enriched[pkgName] = enrichedPackage;
463
+ }
464
+ return {
465
+ ...contract,
466
+ clientCapabilities: { gadgets: enriched },
467
+ };
468
+ }
469
+ function buildFallbackDecision(input) {
470
+ const props = input.agentData
471
+ ? Object.fromEntries(Object.entries(input.agentData).map(([key, value]) => [
472
+ key,
473
+ {
474
+ description: key,
475
+ schema: { type: inferType(value) },
476
+ required: true,
477
+ example: value,
478
+ },
479
+ ]))
480
+ : {};
481
+ const contract = {
482
+ propsSpec: { properties: props },
483
+ };
484
+ return {
485
+ action: 'create',
486
+ reasoning: 'Fallback: creating new UI (decision LLM failed to parse)',
487
+ contract: mergeGadgets(mergeAgentCapabilities(contract, input.agentTools), input.gadgets),
488
+ };
489
+ }
490
+ function inferType(value) {
491
+ if (typeof value === 'number')
492
+ return 'number';
493
+ if (typeof value === 'boolean')
494
+ return 'boolean';
495
+ if (Array.isArray(value))
496
+ return 'array';
497
+ if (typeof value === 'object' && value !== null)
498
+ return 'object';
499
+ return 'string';
500
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @ggui-ai/negotiator — open-source UI decision engine for ggui.
3
+ *
4
+ * Decides which UI to render (create/update/compose/replace) given agent
5
+ * signal (data/prompt/context/agentTools) and current session state.
6
+ *
7
+ * Composes the storage seams defined in `@ggui-ai/mcp-server-core`
8
+ * (`EmbeddingProvider`, `VectorStore`, `Negotiator`). The decision
9
+ * semantics are open here; concrete cloud-vendor bindings (e.g. a
10
+ * managed embedding service or vector store) live behind those seams
11
+ * so this package stays deployment-agnostic.
12
+ *
13
+ * This barrel stays narrow — it exports only the minimum surface
14
+ * consumers need. Each additive export carries semver weight.
15
+ */
16
+ export { hashContract, buildVariant } from './contract-hash.js';
17
+ export { computeIntentId, shouldSuppressSuggestion } from './intent.js';
18
+ export { detectDataPatterns, buildSuggestion } from './suggestion.js';
19
+ export type { NegotiatorSuggestion } from './suggestion.js';
20
+ export { inferInteractionMode, inferJsonSchemaType } from './pure.js';
21
+ export { ragSearch } from './rag-search.js';
22
+ export type { RagSearchDeps, RagSearchInput, RagSearchResult, } from './rag-search.js';
23
+ export type { NegotiatorOption } from './types.js';
24
+ export type { LLMCaller, LLMCallerConfig, ToolSchema } from './llm-caller.js';
25
+ export type { SessionState, SessionStackEntry } from './session.js';
26
+ export type { NegotiatorDecisionInput } from './decision-input.js';
27
+ export { DECISION_SYSTEM_PROMPT, buildDecisionUserMessage, makeDecision, } from './decision.js';
28
+ export { negotiate } from './negotiate.js';
29
+ export type { NegotiateDeps, NegotiateInput, NegotiateConfig, NegotiateResult, } from './negotiate.js';
30
+ export { rerankCandidates } from './llm-rerank.js';
31
+ export type { RerankCandidate, RerankDecision, RerankQuery, } from './llm-rerank.js';
32
+ export { synthesizeContract } from './synthesize-contract.js';
33
+ export type { SynthesizeContractResult } from './synthesize-contract.js';
34
+ export { validateContractStructure, validateContractNovelty, formatValidationFindings, } from './contract-validators.js';
35
+ export type { ContractValidationFinding, ContractValidationFindingKind, ContractValidationResult, ContractValidationNoveltyDeps, ContractValidationNoveltyOptions, } from './contract-validators.js';
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACtE,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EACV,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACpE,YAAY,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EACV,aAAa,EACb,cAAc,EACd,eAAe,EACf,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,GACZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,yBAAyB,EACzB,6BAA6B,EAC7B,wBAAwB,EACxB,6BAA6B,EAC7B,gCAAgC,GACjC,MAAM,0BAA0B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @ggui-ai/negotiator — open-source UI decision engine for ggui.
3
+ *
4
+ * Decides which UI to render (create/update/compose/replace) given agent
5
+ * signal (data/prompt/context/agentTools) and current session state.
6
+ *
7
+ * Composes the storage seams defined in `@ggui-ai/mcp-server-core`
8
+ * (`EmbeddingProvider`, `VectorStore`, `Negotiator`). The decision
9
+ * semantics are open here; concrete cloud-vendor bindings (e.g. a
10
+ * managed embedding service or vector store) live behind those seams
11
+ * so this package stays deployment-agnostic.
12
+ *
13
+ * This barrel stays narrow — it exports only the minimum surface
14
+ * consumers need. Each additive export carries semver weight.
15
+ */
16
+ export { hashContract, buildVariant } from './contract-hash.js';
17
+ export { computeIntentId, shouldSuppressSuggestion } from './intent.js';
18
+ export { detectDataPatterns, buildSuggestion } from './suggestion.js';
19
+ export { inferInteractionMode, inferJsonSchemaType } from './pure.js';
20
+ export { ragSearch } from './rag-search.js';
21
+ export { DECISION_SYSTEM_PROMPT, buildDecisionUserMessage, makeDecision, } from './decision.js';
22
+ export { negotiate } from './negotiate.js';
23
+ export { rerankCandidates } from './llm-rerank.js';
24
+ export { synthesizeContract } from './synthesize-contract.js';
25
+ export { validateContractStructure, validateContractNovelty, formatValidationFindings, } from './contract-validators.js';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Deterministic identifier for a negotiation intent.
3
+ *
4
+ * Used by the suggestion engine to deduplicate auto-suggested UIs
5
+ * within a session — two prompts that would produce the same intent
6
+ * collapse to a single suggestion. SHA-256 truncated to 16 hex chars
7
+ * (64 bits) gives collision-resistant ids without being wastefully
8
+ * large in logs.
9
+ *
10
+ * @param sessionId Scope. Intent ids are session-local.
11
+ * @param data Data shape (keys only — values ignored). Undefined → 'no-data'.
12
+ * @param action Optional action verb. Defaults to 'create'.
13
+ */
14
+ export declare function computeIntentId(sessionId: string, data: Record<string, unknown> | undefined, action?: string): string;
15
+ /**
16
+ * True if the given intent id is already being handled in this session.
17
+ *
18
+ * The suggestion engine uses this to avoid re-suggesting the same UI
19
+ * while the agent is already preparing one.
20
+ */
21
+ export declare function shouldSuppressSuggestion(intentId: string, activeIntentIds: Set<string>): boolean;
22
+ //# sourceMappingURL=intent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent.d.ts","sourceRoot":"","sources":["../src/intent.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAIR;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAC3B,OAAO,CAET"}
package/dist/intent.js ADDED
@@ -0,0 +1,28 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Deterministic identifier for a negotiation intent.
4
+ *
5
+ * Used by the suggestion engine to deduplicate auto-suggested UIs
6
+ * within a session — two prompts that would produce the same intent
7
+ * collapse to a single suggestion. SHA-256 truncated to 16 hex chars
8
+ * (64 bits) gives collision-resistant ids without being wastefully
9
+ * large in logs.
10
+ *
11
+ * @param sessionId Scope. Intent ids are session-local.
12
+ * @param data Data shape (keys only — values ignored). Undefined → 'no-data'.
13
+ * @param action Optional action verb. Defaults to 'create'.
14
+ */
15
+ export function computeIntentId(sessionId, data, action) {
16
+ const dataShape = data ? Object.keys(data).sort().join(',') : 'no-data';
17
+ const raw = `${sessionId}:${dataShape}:${action ?? 'create'}`;
18
+ return createHash('sha256').update(raw).digest('hex').slice(0, 16);
19
+ }
20
+ /**
21
+ * True if the given intent id is already being handled in this session.
22
+ *
23
+ * The suggestion engine uses this to avoid re-suggesting the same UI
24
+ * while the agent is already preparing one.
25
+ */
26
+ export function shouldSuppressSuggestion(intentId, activeIntentIds) {
27
+ return activeIntentIds.has(intentId);
28
+ }