@ggui-ai/mcp-server 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 (141) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +48 -0
  3. package/dist/admin-blueprints-transport.d.ts +114 -0
  4. package/dist/admin-blueprints-transport.d.ts.map +1 -0
  5. package/dist/admin-blueprints-transport.js +118 -0
  6. package/dist/admin-oauth-providers-transport.d.ts +40 -0
  7. package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
  8. package/dist/admin-oauth-providers-transport.js +263 -0
  9. package/dist/auth.d.ts +39 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +75 -0
  12. package/dist/build-mcp.d.ts +128 -0
  13. package/dist/build-mcp.d.ts.map +1 -0
  14. package/dist/build-mcp.js +113 -0
  15. package/dist/code-store-fs.d.ts +19 -0
  16. package/dist/code-store-fs.d.ts.map +1 -0
  17. package/dist/code-store-fs.js +98 -0
  18. package/dist/console-auth.d.ts +139 -0
  19. package/dist/console-auth.d.ts.map +1 -0
  20. package/dist/console-auth.js +102 -0
  21. package/dist/console-cache.d.ts +78 -0
  22. package/dist/console-cache.d.ts.map +1 -0
  23. package/dist/console-cache.js +105 -0
  24. package/dist/console-headers.d.ts +124 -0
  25. package/dist/console-headers.d.ts.map +1 -0
  26. package/dist/console-headers.js +49 -0
  27. package/dist/console-llm-trace.d.ts +66 -0
  28. package/dist/console-llm-trace.d.ts.map +1 -0
  29. package/dist/console-llm-trace.js +105 -0
  30. package/dist/console-payloads.d.ts +67 -0
  31. package/dist/console-payloads.d.ts.map +1 -0
  32. package/dist/console-payloads.js +105 -0
  33. package/dist/console-theme-routes.d.ts +111 -0
  34. package/dist/console-theme-routes.d.ts.map +1 -0
  35. package/dist/console-theme-routes.js +202 -0
  36. package/dist/console-timeline.d.ts +45 -0
  37. package/dist/console-timeline.d.ts.map +1 -0
  38. package/dist/console-timeline.js +169 -0
  39. package/dist/console-validator.d.ts +67 -0
  40. package/dist/console-validator.d.ts.map +1 -0
  41. package/dist/console-validator.js +105 -0
  42. package/dist/console-welcome.d.ts +7 -0
  43. package/dist/console-welcome.d.ts.map +1 -0
  44. package/dist/console-welcome.js +221 -0
  45. package/dist/csrf-middleware.d.ts +55 -0
  46. package/dist/csrf-middleware.d.ts.map +1 -0
  47. package/dist/csrf-middleware.js +138 -0
  48. package/dist/email-login.d.ts +174 -0
  49. package/dist/email-login.d.ts.map +1 -0
  50. package/dist/email-login.js +254 -0
  51. package/dist/email-resend.d.ts +29 -0
  52. package/dist/email-resend.d.ts.map +1 -0
  53. package/dist/email-resend.js +71 -0
  54. package/dist/email-sender-from-env.d.ts +34 -0
  55. package/dist/email-sender-from-env.d.ts.map +1 -0
  56. package/dist/email-sender-from-env.js +112 -0
  57. package/dist/email-smtp.d.ts +42 -0
  58. package/dist/email-smtp.d.ts.map +1 -0
  59. package/dist/email-smtp.js +81 -0
  60. package/dist/index.d.ts +102 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +122 -0
  63. package/dist/instructions-presets.d.ts +112 -0
  64. package/dist/instructions-presets.d.ts.map +1 -0
  65. package/dist/instructions-presets.js +195 -0
  66. package/dist/llm-backed-negotiator.d.ts +178 -0
  67. package/dist/llm-backed-negotiator.d.ts.map +1 -0
  68. package/dist/llm-backed-negotiator.js +579 -0
  69. package/dist/logger.d.ts +23 -0
  70. package/dist/logger.d.ts.map +1 -0
  71. package/dist/logger.js +41 -0
  72. package/dist/mcp-apps-inbound.d.ts +86 -0
  73. package/dist/mcp-apps-inbound.d.ts.map +1 -0
  74. package/dist/mcp-apps-inbound.js +278 -0
  75. package/dist/mcp-apps-outbound.d.ts +448 -0
  76. package/dist/mcp-apps-outbound.d.ts.map +1 -0
  77. package/dist/mcp-apps-outbound.js +1163 -0
  78. package/dist/mcp-mounts.d.ts +239 -0
  79. package/dist/mcp-mounts.d.ts.map +1 -0
  80. package/dist/mcp-mounts.js +222 -0
  81. package/dist/oauth-login-types.d.ts +160 -0
  82. package/dist/oauth-login-types.d.ts.map +1 -0
  83. package/dist/oauth-login-types.js +9 -0
  84. package/dist/oauth-login.d.ts +77 -0
  85. package/dist/oauth-login.d.ts.map +1 -0
  86. package/dist/oauth-login.js +455 -0
  87. package/dist/oauth-providers/github.d.ts +17 -0
  88. package/dist/oauth-providers/github.d.ts.map +1 -0
  89. package/dist/oauth-providers/github.js +89 -0
  90. package/dist/oauth-providers/google.d.ts +18 -0
  91. package/dist/oauth-providers/google.d.ts.map +1 -0
  92. package/dist/oauth-providers/google.js +59 -0
  93. package/dist/oauth-providers-store.d.ts +32 -0
  94. package/dist/oauth-providers-store.d.ts.map +1 -0
  95. package/dist/oauth-providers-store.js +291 -0
  96. package/dist/oauth.d.ts +347 -0
  97. package/dist/oauth.d.ts.map +1 -0
  98. package/dist/oauth.js +686 -0
  99. package/dist/pairing-transport.d.ts +99 -0
  100. package/dist/pairing-transport.d.ts.map +1 -0
  101. package/dist/pairing-transport.js +223 -0
  102. package/dist/rate-limit-middleware.d.ts +36 -0
  103. package/dist/rate-limit-middleware.d.ts.map +1 -0
  104. package/dist/rate-limit-middleware.js +57 -0
  105. package/dist/render-gate.d.ts +87 -0
  106. package/dist/render-gate.d.ts.map +1 -0
  107. package/dist/render-gate.js +77 -0
  108. package/dist/render-rate-limit.d.ts +59 -0
  109. package/dist/render-rate-limit.d.ts.map +1 -0
  110. package/dist/render-rate-limit.js +73 -0
  111. package/dist/render-signing.d.ts +98 -0
  112. package/dist/render-signing.d.ts.map +1 -0
  113. package/dist/render-signing.js +113 -0
  114. package/dist/request-context.d.ts +113 -0
  115. package/dist/request-context.d.ts.map +1 -0
  116. package/dist/request-context.js +154 -0
  117. package/dist/reserved-validators.d.ts +22 -0
  118. package/dist/reserved-validators.d.ts.map +1 -0
  119. package/dist/reserved-validators.js +101 -0
  120. package/dist/schema-compat.d.ts +167 -0
  121. package/dist/schema-compat.d.ts.map +1 -0
  122. package/dist/schema-compat.js +187 -0
  123. package/dist/security-headers-middleware.d.ts +38 -0
  124. package/dist/security-headers-middleware.d.ts.map +1 -0
  125. package/dist/security-headers-middleware.js +30 -0
  126. package/dist/server.d.ts +2060 -0
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +6338 -0
  129. package/dist/session-channel.d.ts +651 -0
  130. package/dist/session-channel.d.ts.map +1 -0
  131. package/dist/session-channel.js +1756 -0
  132. package/dist/storage.d.ts +89 -0
  133. package/dist/storage.d.ts.map +1 -0
  134. package/dist/storage.js +171 -0
  135. package/dist/thread-transport.d.ts +118 -0
  136. package/dist/thread-transport.d.ts.map +1 -0
  137. package/dist/thread-transport.js +478 -0
  138. package/dist/user-session-auth.d.ts +167 -0
  139. package/dist/user-session-auth.d.ts.map +1 -0
  140. package/dist/user-session-auth.js +148 -0
  141. package/package.json +76 -0
@@ -0,0 +1,579 @@
1
+ /**
2
+ * LLM-backed `HandshakeNegotiator` for the OSS server.
3
+ *
4
+ * Composes BYOK credentials + a `ProviderAdapter` from
5
+ * `@ggui-ai/ui-gen/providers` into an `LLMCaller`, then runs
6
+ * `@ggui-ai/negotiator`'s `negotiate()` pipeline. Mirrors the cloud
7
+ * `createBedrockNegotiator` pattern (cloud/ggui-protocol-pod/src/
8
+ * tools/handshake.ts), stripped of Bedrock-specific embedding +
9
+ * vector-store wiring — those are absent on OSS by default and
10
+ * `negotiate()` handles missing RAG deps gracefully.
11
+ *
12
+ * ## What this binding does
13
+ *
14
+ * On every `ggui_handshake` call:
15
+ *
16
+ * 1. Resolves BYOK creds via the supplied `resolveLlm(ctx)`. No
17
+ * creds → returns a "create" result with a `no-creds` reason.
18
+ * 2. Selects the matching `ProviderAdapter` for the resolved
19
+ * provider (anthropic / openai / google / openrouter / bedrock).
20
+ * 3. Wraps the adapter into an `LLMCaller` (single-shot text
21
+ * completion).
22
+ * 4. Calls `negotiate(deps, input)` with `embedding: undefined,
23
+ * vectors: undefined` — RAG search is skipped, decision LLM
24
+ * runs against an empty candidate list, returns a sensible
25
+ * create/update decision based on session state + agent
26
+ * prompt.
27
+ * 5. Maps the `NegotiateResult` onto the OSS-shape
28
+ * `HandshakeNegotiatorResult`.
29
+ *
30
+ * ## Failure modes
31
+ *
32
+ * Operational errors (network flap, provider 5xx, rate limit) fail
33
+ * open: returns a "create" result with the error reason. Bugs
34
+ * (TypeError / ReferenceError / RangeError / SyntaxError) re-throw
35
+ * — those are programmer errors that should surface, not be
36
+ * silently swallowed.
37
+ *
38
+ * ## Cost posture
39
+ *
40
+ * One LLM call per handshake (when creds resolve). Operators
41
+ * concerned about cost can either (a) skip handshake and call
42
+ * `ggui_push` directly with `{story}`, or (b) bind a different
43
+ * negotiator (e.g., the cache-backed one for read-only cache
44
+ * lookups) via `createGguiServer({handshake: {negotiator: ...}})`.
45
+ *
46
+ * ## Default binding
47
+ *
48
+ * Bound by default in `createGguiServer` when handshake is enabled
49
+ * AND a `resolveLlm` is wired into `generation`. OSS is cloud-aligned:
50
+ * same `negotiate()` pipeline, just with degraded RAG when local
51
+ * infrastructure isn't bound.
52
+ */
53
+ import { negotiate } from '@ggui-ai/negotiator';
54
+ import { DEFAULT_GENERATOR_SLUG, matchBlueprint, } from '@ggui-ai/mcp-server-handlers/session-mutations';
55
+ import { createHash, randomUUID } from 'node:crypto';
56
+ import { blueprintKey } from '@ggui-ai/protocol/blueprint-key';
57
+ import { selectAdapter } from '@ggui-ai/ui-gen/providers';
58
+ /**
59
+ * Operational error classifier — mirrors the cloud helper. Bugs
60
+ * surface; provider failures + network blips degrade to a "create"
61
+ * stub so the agent isn't blocked by a transient infrastructure
62
+ * issue.
63
+ */
64
+ function isOperationalError(err) {
65
+ if (err instanceof TypeError)
66
+ return false;
67
+ if (err instanceof ReferenceError)
68
+ return false;
69
+ if (err instanceof RangeError)
70
+ return false;
71
+ if (err instanceof SyntaxError)
72
+ return false;
73
+ return true;
74
+ }
75
+ /**
76
+ * Wrap a resolved BYOK credential pair into an `LLMCaller` the
77
+ * negotiator can call. The adapter is chosen via `selectAdapter`
78
+ * (anthropic / openai / google / openrouter / bedrock). `call` runs
79
+ * one `complete()` round-trip on the underlying adapter.
80
+ *
81
+ * `callStructured` is wired for Anthropic only. Anthropic's
82
+ * `/v1/messages` natively supports forced tool use via `tools[] +
83
+ * tool_choice: {type:'tool', name}`, so we hit the API directly here
84
+ * instead of expanding the `ProviderAdapter` interface for one
85
+ * provider. Other providers (OpenAI, Google, OpenRouter, Bedrock)
86
+ * omit `callStructured`; consumers detect absence and fall back to
87
+ * regex-JSON extraction on the text path. When this story shifts —
88
+ * e.g., we want OpenAI tool use too — promote `completeWithTool`
89
+ * onto `ProviderAdapter` as an optional method and wire each
90
+ * adapter; this in-place implementation stays the bridge until then.
91
+ *
92
+ * Used by:
93
+ * - `@ggui-ai/negotiator/llm-rerank` (Tier 2 RAG match judge)
94
+ * - `@ggui-ai/negotiator/synthesize-contract` (cold-path contract
95
+ * synthesizer)
96
+ */
97
+ export function buildLlmCaller(selection, providerKey) {
98
+ const adapter = selectAdapter(selection.provider);
99
+ const isAnthropic = selection.provider === 'anthropic';
100
+ const caller = {
101
+ async call(systemPrompt, userMessage, maxTokens) {
102
+ const result = await adapter.complete({
103
+ apiKey: providerKey.key,
104
+ model: selection.model,
105
+ systemPrompt,
106
+ userPrompt: userMessage,
107
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
108
+ });
109
+ if (!result.ok) {
110
+ throw new Error(`[llm-backed-negotiator] ${selection.provider} ${selection.model} ` +
111
+ `failed: ${result.error.kind} — ${result.error.message}`);
112
+ }
113
+ return result.response.text;
114
+ },
115
+ };
116
+ if (isAnthropic) {
117
+ caller.callStructured = async (systemPrompt, userMessage, tool, maxTokens) => {
118
+ const result = await anthropicCallStructured({
119
+ apiKey: providerKey.key,
120
+ model: selection.model,
121
+ systemPrompt,
122
+ userMessage,
123
+ tool,
124
+ maxTokens,
125
+ });
126
+ return result;
127
+ };
128
+ }
129
+ return caller;
130
+ }
131
+ /** Anthropic-direct tool-use call. Forces a single tool invocation
132
+ * and returns the tool's `input` JSON. Throws on non-2xx, network
133
+ * errors, or response-shape failures so the caller (rerank judge,
134
+ * synthesizer) can collapse to its null-decision fallback. */
135
+ async function anthropicCallStructured(args) {
136
+ const body = {
137
+ model: args.model,
138
+ max_tokens: args.maxTokens ?? 1024,
139
+ // `temperature` was pinned to 0 for deterministic structured
140
+ // output, but Anthropic deprecated the parameter on newer
141
+ // tool-use models (Haiku 4.5+ rejects it with HTTP 400). Dropped.
142
+ // `tool_choice: { type: 'tool', name }` below already binds the
143
+ // output shape to the declared input_schema — the model can't
144
+ // emit a free-form text response when forced-tool is set.
145
+ // Residual stochasticity is in field VALUES (e.g. action names);
146
+ // both consumers (synthesizer + rerank judge) MUST tolerate
147
+ // paraphrase via canonical-key normalisation rather than relying
148
+ // on temperature=0.
149
+ system: args.systemPrompt,
150
+ messages: [{ role: 'user', content: args.userMessage }],
151
+ tools: [
152
+ {
153
+ name: args.tool.name,
154
+ description: args.tool.description,
155
+ input_schema: args.tool.input_schema,
156
+ },
157
+ ],
158
+ // Forced tool use — model MUST emit exactly this tool. Without
159
+ // this the model can drift to a text reply and synth/rerank
160
+ // both lose their structured guarantee.
161
+ tool_choice: { type: 'tool', name: args.tool.name },
162
+ };
163
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
164
+ method: 'POST',
165
+ headers: {
166
+ 'x-api-key': args.apiKey,
167
+ 'anthropic-version': '2023-06-01',
168
+ 'content-type': 'application/json',
169
+ },
170
+ body: JSON.stringify(body),
171
+ });
172
+ if (!response.ok) {
173
+ const text = await response.text().catch(() => '');
174
+ throw new Error(`anthropic tool-use HTTP ${response.status}: ${text.slice(0, 500)}`);
175
+ }
176
+ const json = (await response.json());
177
+ // Find the tool_use block. With `tool_choice: {type:'tool'}` the
178
+ // model is forced to emit exactly one; defensively scan in case the
179
+ // shape ever shifts.
180
+ const toolUse = json.content?.find((block) => block.type === 'tool_use' && block.name === args.tool.name);
181
+ if (!toolUse || toolUse.input === undefined) {
182
+ throw new Error(`anthropic tool-use response missing tool_use block for "${args.tool.name}"`);
183
+ }
184
+ return toolUse.input;
185
+ }
186
+ const DEFAULT_GEN_LATENCY_MS = 30_000;
187
+ function buildCreateFallback(draftContract, reason, _estimatedLatencyMs) {
188
+ // Fallback path stamps an `origin: 'agent'` suggestion against the
189
+ // agent's draft. Gen-pending; no codeHash; provisional blueprintId.
190
+ const contractHash = blueprintKey(draftContract);
191
+ const suggestion = {
192
+ origin: 'agent',
193
+ rationale: reason,
194
+ blueprintMeta: {
195
+ blueprintId: `bp_${randomUUID()}`,
196
+ contractHash,
197
+ generator: 'ui-gen-default-haiku-4-5',
198
+ variance: {},
199
+ },
200
+ };
201
+ return {
202
+ action: 'create',
203
+ reason,
204
+ suggestion,
205
+ effectiveContract: draftContract,
206
+ };
207
+ }
208
+ /**
209
+ * Build an LLM-backed `HandshakeNegotiator` for the OSS server.
210
+ * Wires BYOK creds + an LLM provider adapter into
211
+ * `@ggui-ai/negotiator`'s `negotiate()` pipeline. Operators get the
212
+ * same negotiation shape cloud uses, with RAG gracefully degraded
213
+ * (no embedding / vectors required).
214
+ *
215
+ * @public
216
+ */
217
+ export function createLlmBackedHandshakeNegotiator(deps) {
218
+ const estimatedLatencyMs = deps.estimatedGenerationLatencyMs ?? DEFAULT_GEN_LATENCY_MS;
219
+ return {
220
+ async decide({ intent, blueprintDraft, gadgets, ctx, sessionId }) {
221
+ const draftContract = blueprintDraft.contract;
222
+ // Handshake-time exact-key fast path. Runs BEFORE the BYOK
223
+ // creds resolve + LLM synth — a cache hit needs neither the
224
+ // operator's API key nor a model round-trip. When the agent's
225
+ // draft canonical-key-equals a registered blueprint, return
226
+ // `origin: 'cache'` with the matched blueprint's contract +
227
+ // componentCode hash so the paired push.accept short-circuits
228
+ // straight into commitCachedStackItem.
229
+ //
230
+ // No-match (or any throw) falls through to today's synth path
231
+ // — the negotiator stays useful when the registry is cold,
232
+ // when matchBlueprint hiccups, or when the deployment skipped
233
+ // the cache deps entirely.
234
+ if (deps.cache) {
235
+ try {
236
+ const matchDeps = {
237
+ registry: deps.cache,
238
+ ...(deps.installedBlueprints
239
+ ? { installedBlueprints: deps.installedBlueprints }
240
+ : {}),
241
+ };
242
+ const matchResult = await matchBlueprint(matchDeps, ctx.appId, {
243
+ intent,
244
+ contract: draftContract,
245
+ });
246
+ if (matchResult.strategy === 'exact-key') {
247
+ const matched = matchResult.blueprint;
248
+ const codeHash = createHash('sha256')
249
+ .update(matched.componentCode)
250
+ .digest('hex');
251
+ const suggestion = {
252
+ origin: 'cache',
253
+ rationale: matchResult.reason,
254
+ blueprintMeta: {
255
+ blueprintId: matched.id,
256
+ contractHash: matched.contractKey,
257
+ codeHash,
258
+ generator: DEFAULT_GENERATOR_SLUG,
259
+ variance: {},
260
+ selectedReason: matchResult.reason,
261
+ },
262
+ };
263
+ return {
264
+ action: 'reuse',
265
+ reason: matchResult.reason,
266
+ suggestion,
267
+ effectiveContract: matched.contract,
268
+ };
269
+ }
270
+ // Other strategies (no-match, semantic) fall through to
271
+ // the negotiate() path below. Semantic doesn't fire when
272
+ // a contract is supplied (the fuzzy-match gate blocks it);
273
+ // listing it here is structural completeness, not a
274
+ // reachable branch today.
275
+ }
276
+ catch (err) {
277
+ if (!isOperationalError(err))
278
+ throw err;
279
+ // Registry hiccup — log + fall through to synth so the
280
+ // handshake never crashes on a transient cache backend
281
+ // issue.
282
+ const message = err instanceof Error ? err.message : String(err);
283
+ // eslint-disable-next-line no-console -- operator-visible signal
284
+ console.warn(`[llm-backed-negotiator] matchBlueprint exact-key probe failed; falling through to synth: ${message}`);
285
+ }
286
+ }
287
+ const creds = await deps.resolveLlm(ctx);
288
+ if (!creds) {
289
+ return buildCreateFallback(draftContract, 'no-creds: no BYOK credentials resolved for the configured provider; ggui_push will surface the same error and the handshake stays a no-op create.', estimatedLatencyMs);
290
+ }
291
+ try {
292
+ const llm = buildLlmCaller(creds.selection, creds.providerKey);
293
+ const declaredAgentTools = Object.keys(draftContract.agentCapabilities?.tools ?? {});
294
+ const synthPrompt = blueprintDraft.variance?.seedPrompt ?? intent;
295
+ const result = await negotiate({ llm }, {
296
+ agent: {
297
+ prompt: synthPrompt,
298
+ ...(declaredAgentTools.length > 0
299
+ ? { agentTools: declaredAgentTools }
300
+ : {}),
301
+ ...(gadgets !== undefined ? { gadgets } : {}),
302
+ },
303
+ config: {
304
+ appId: ctx.appId,
305
+ sessionId: sessionId ?? 'handshake',
306
+ includeSharedPool: false,
307
+ },
308
+ });
309
+ const decision = result.decision;
310
+ const contract = decision.contract;
311
+ const contractHash = result.storedContractHash ?? blueprintKey(contract);
312
+ const draftHash = blueprintKey(draftContract);
313
+ const blueprintId = decision.blueprintId;
314
+ // Origin routing:
315
+ // - blueprintId present (cache hit) → origin: 'cache'
316
+ // - contract == draft → origin: 'agent'
317
+ // - contract != draft → origin: 'synth'
318
+ let suggestion;
319
+ if (blueprintId) {
320
+ suggestion = {
321
+ origin: 'cache',
322
+ rationale: decision.reasoning ?? `cache match (${blueprintId})`,
323
+ blueprintMeta: {
324
+ blueprintId,
325
+ contractHash,
326
+ generator: 'ui-gen-default-haiku-4-5',
327
+ variance: {},
328
+ },
329
+ };
330
+ }
331
+ else if (contractHash === draftHash) {
332
+ suggestion = {
333
+ origin: 'agent',
334
+ rationale: decision.reasoning ?? 'novel-but-clean contract',
335
+ blueprintMeta: {
336
+ blueprintId: `bp_${randomUUID()}`,
337
+ contractHash,
338
+ generator: 'ui-gen-default-haiku-4-5',
339
+ variance: {},
340
+ },
341
+ };
342
+ }
343
+ else {
344
+ suggestion = {
345
+ origin: 'synth',
346
+ rationale: decision.reasoning ?? 'synth amended contract',
347
+ blueprintMeta: {
348
+ blueprintId: `bp_${randomUUID()}`,
349
+ contractHash,
350
+ generator: 'ui-gen-default-haiku-4-5',
351
+ variance: {},
352
+ },
353
+ amendments: {
354
+ contractDiff: [
355
+ {
356
+ op: 'replace',
357
+ path: '',
358
+ value: contract,
359
+ },
360
+ ],
361
+ reasoning: decision.reasoning ?? 'negotiator-amended contract',
362
+ },
363
+ };
364
+ }
365
+ return {
366
+ action: blueprintId ? 'reuse' : 'create',
367
+ reason: decision.reasoning ?? 'negotiated',
368
+ suggestion,
369
+ effectiveContract: contract,
370
+ };
371
+ }
372
+ catch (err) {
373
+ if (!isOperationalError(err))
374
+ throw err;
375
+ const message = err instanceof Error ? err.message : String(err);
376
+ const errorClass = err instanceof Error ? err.name : 'unknown';
377
+ return buildCreateFallback(draftContract, `negotiator-degraded: ${errorClass} during decision LLM call — ${message}. Falling back to bare-create; the paired ggui_push will still generate the UI.`, estimatedLatencyMs);
378
+ }
379
+ },
380
+ // LLM-driven variant selection. Reads each candidate's
381
+ // `variance` + `validatorScore` + `isOperatorDefault` + the
382
+ // generator slug, asks the LLM to pick the best fit for the
383
+ // current request's `intent` + `variance`, and returns a
384
+ // calibrated decision. The caller (`selectVariantWithLlm`)
385
+ // thresholds on `confidence`.
386
+ //
387
+ // Errors throw — the caller catches and falls through to the
388
+ // deterministic ladder. The decide() seam uses a more permissive
389
+ // fail-open pattern because there's no fallback higher up; the
390
+ // variant-selector caller owns the fallback path itself.
391
+ async selectVariant({ candidates, context, ctx }) {
392
+ if (candidates.length === 0) {
393
+ throw new Error('selectVariant: empty candidates list — orchestration should short-circuit before calling');
394
+ }
395
+ const creds = await deps.resolveLlm(ctx);
396
+ if (!creds) {
397
+ throw new Error('selectVariant: no BYOK credentials resolved; orchestration falls through to deterministic ladder');
398
+ }
399
+ const llm = buildLlmCaller(creds.selection, creds.providerKey);
400
+ return runVariantSelectionLlm(llm, candidates, context);
401
+ },
402
+ };
403
+ }
404
+ /**
405
+ * The system prompt for the variant-selection LLM call. Calibration
406
+ * is load-bearing — the model is explicitly told to surface low
407
+ * confidence when signals are weak so the deterministic-ladder
408
+ * fallback takes over. The prompt is intentionally short: high
409
+ * token budget on the user message (candidate JSON) is more useful
410
+ * than verbose system framing.
411
+ */
412
+ export const VARIANT_SELECTION_SYSTEM_PROMPT = `You are the variant selector for the ggui UI matcher. You receive a shortlist of pre-built UI blueprint variants and a request context. Pick the variant that best fits the request.
413
+
414
+ Each variant carries:
415
+ - blueprintId: stable identity (you MUST echo back exactly one of these).
416
+ - generator: which generator built it (e.g. "ui-gen-default-haiku-4-5", "ui-gen-advanced-opus-4-7"). Advanced wins on visual polish; default wins on simplicity.
417
+ - validatorScore: optional 0-1 self-assessed quality from the advanced generator's validators. Higher is better, undefined ⇒ unknown.
418
+ - isOperatorDefault: true ⇒ the human operator pinned this as the default. Strong signal.
419
+ - variance.persona: free-form tag ("minimalist", "data-dense", "mobile-first"…).
420
+ - variance.aesthetic: optional free-form tag ("glassy", "flat", "editorial"…).
421
+ - variance.context: small structured signal (theme, accent, …).
422
+ - variance.seedPrompt: the operator's original prose that produced this variant.
423
+
424
+ The request context carries the same fields. Match on:
425
+ 1. variance.persona equality / closeness (strongest non-pin signal).
426
+ 2. variance.aesthetic equality / closeness.
427
+ 3. variance.context overlap (shared keys + values).
428
+ 4. seedPrompt semantic similarity to context.intent.
429
+
430
+ Honor operator pins (isOperatorDefault: true) unless variance.persona / variance.aesthetic on the request clearly contradicts the pinned variant — that's the only case where you should override the pin.
431
+
432
+ Calibrate confidence honestly. Return high (≥ 0.7) only when a clear best match exists. Return low (< 0.6) when signals are weak — the orchestration falls back to a deterministic ladder in that case. The fallback is safe; over-confident picks are NOT.`;
433
+ const VARIANT_SELECTION_TOOL_NAME = 'select_variant';
434
+ const VARIANT_SELECTION_TOOL_DESCRIPTION = 'Pick the variant that best fits the request context. Return the chosen blueprintId, a calibrated 0-1 confidence, and a one-sentence reason citing the matching axes (persona / aesthetic / context / seedPrompt / pin / validator).';
435
+ /**
436
+ * JSON Schema for the structured-output tool call. Forces the model
437
+ * to emit `{blueprintId, confidence, reason}` — no free-text prose.
438
+ * Used on Anthropic via `LLMCaller.callStructured`; the text-fallback
439
+ * path parses the same shape from regex-extracted JSON.
440
+ */
441
+ const VARIANT_SELECTION_TOOL_SCHEMA = {
442
+ type: 'object',
443
+ properties: {
444
+ blueprintId: {
445
+ type: 'string',
446
+ description: 'One of the candidate blueprintIds shown in the request. Echo exactly — must match a candidate.',
447
+ },
448
+ confidence: {
449
+ type: 'number',
450
+ minimum: 0,
451
+ maximum: 1,
452
+ description: 'Calibrated confidence in this pick on [0, 1]. Use < 0.6 when signals are weak; the orchestration falls back to a deterministic ladder.',
453
+ },
454
+ reason: {
455
+ type: 'string',
456
+ description: 'One-sentence rationale citing the matching axes (persona, aesthetic, context, seedPrompt, validator, pin).',
457
+ },
458
+ },
459
+ required: ['blueprintId', 'confidence', 'reason'],
460
+ };
461
+ /**
462
+ * Build the user-message payload for the variant-selection prompt.
463
+ * The candidate list is projected to a compact JSON shape that
464
+ * surfaces the decision-relevant fields only — full contract
465
+ * embedding is too much surface area for a sub-second pick.
466
+ *
467
+ * Exposed for testing — the prompt structure is load-bearing, so
468
+ * snapshot tests against this output anchor regressions.
469
+ */
470
+ export function buildVariantSelectionUserMessage(candidates, context) {
471
+ const projectedCandidates = candidates.map((c) => ({
472
+ blueprintId: c.blueprintId,
473
+ generator: c.generator,
474
+ ...(c.validatorScore !== undefined
475
+ ? { validatorScore: c.validatorScore }
476
+ : {}),
477
+ ...(c.isOperatorDefault === true ? { isOperatorDefault: true } : {}),
478
+ variance: {
479
+ ...(c.variance.persona !== undefined
480
+ ? { persona: c.variance.persona }
481
+ : {}),
482
+ ...(c.variance.context !== undefined
483
+ ? { context: c.variance.context }
484
+ : {}),
485
+ ...(c.variance.seedPrompt !== undefined
486
+ ? { seedPrompt: c.variance.seedPrompt }
487
+ : {}),
488
+ },
489
+ }));
490
+ const requestProjection = {
491
+ contractHash: context.contractHash,
492
+ ...(context.intent !== undefined ? { intent: context.intent } : {}),
493
+ ...(context.variance !== undefined
494
+ ? {
495
+ variance: {
496
+ ...(context.variance.persona !== undefined
497
+ ? { persona: context.variance.persona }
498
+ : {}),
499
+ ...(context.variance.aesthetic !== undefined
500
+ ? { aesthetic: context.variance.aesthetic }
501
+ : {}),
502
+ ...(context.variance.context !== undefined
503
+ ? { context: context.variance.context }
504
+ : {}),
505
+ ...(context.variance.seedPrompt !== undefined
506
+ ? { seedPrompt: context.variance.seedPrompt }
507
+ : {}),
508
+ },
509
+ }
510
+ : {}),
511
+ };
512
+ return [
513
+ 'CANDIDATES:',
514
+ JSON.stringify(projectedCandidates, null, 2),
515
+ '',
516
+ 'REQUEST:',
517
+ JSON.stringify(requestProjection, null, 2),
518
+ '',
519
+ 'Pick the variant that best fits the REQUEST. Echo the blueprintId exactly, surface calibrated confidence, give a one-sentence rationale.',
520
+ ].join('\n');
521
+ }
522
+ /**
523
+ * Run the LLM call + decode. Anthropic path uses `callStructured`
524
+ * (forced tool use ⇒ guaranteed JSON); other providers fall back to
525
+ * text + regex-extracted JSON. The caller (`selectVariantWithLlm`)
526
+ * catches any throw from this function and routes through the
527
+ * deterministic ladder; this function surfaces detail in the thrown
528
+ * error so telemetry can attribute the fallback cause.
529
+ */
530
+ async function runVariantSelectionLlm(llm, candidates, context) {
531
+ const userMessage = buildVariantSelectionUserMessage(candidates, context);
532
+ if (llm.callStructured) {
533
+ const decoded = await llm.callStructured(VARIANT_SELECTION_SYSTEM_PROMPT, userMessage, {
534
+ name: VARIANT_SELECTION_TOOL_NAME,
535
+ description: VARIANT_SELECTION_TOOL_DESCRIPTION,
536
+ input_schema: VARIANT_SELECTION_TOOL_SCHEMA,
537
+ }, 512);
538
+ return parseVariantSelectionResponse(decoded);
539
+ }
540
+ // Text-fallback — parse JSON via regex. Lower reliability;
541
+ // operators on non-Anthropic providers get this path until
542
+ // `callStructured` extends to their adapter.
543
+ const text = await llm.call(VARIANT_SELECTION_SYSTEM_PROMPT, `${userMessage}\n\nRespond as ONE LINE of JSON: {"blueprintId":"…","confidence":0.0-1.0,"reason":"…"}`, 512);
544
+ const match = text.match(/\{[\s\S]*?"blueprintId"[\s\S]*?\}/);
545
+ if (!match) {
546
+ throw new Error(`variant-selection: no JSON object found in text response (length ${text.length})`);
547
+ }
548
+ return parseVariantSelectionResponse(JSON.parse(match[0]));
549
+ }
550
+ /**
551
+ * Parse + validate the LLM-tool-use response shape. Exposed for
552
+ * testing; in production it is only called via `runVariantSelectionLlm`.
553
+ *
554
+ * @throws Error on any shape violation. The caller catches and falls
555
+ * through to the deterministic ladder; the message is surfaced in
556
+ * `VariantSelectionResult.reason` for telemetry.
557
+ */
558
+ export function parseVariantSelectionResponse(raw) {
559
+ if (raw === null || typeof raw !== 'object') {
560
+ throw new Error('variant-selection: response is not an object');
561
+ }
562
+ const obj = raw;
563
+ const blueprintId = obj['blueprintId'];
564
+ const confidence = obj['confidence'];
565
+ const reason = obj['reason'];
566
+ if (typeof blueprintId !== 'string' || blueprintId.length === 0) {
567
+ throw new Error('variant-selection: blueprintId missing or non-string');
568
+ }
569
+ if (typeof confidence !== 'number' ||
570
+ !Number.isFinite(confidence) ||
571
+ confidence < 0 ||
572
+ confidence > 1) {
573
+ throw new Error(`variant-selection: confidence missing or out of range: ${JSON.stringify(confidence)}`);
574
+ }
575
+ if (typeof reason !== 'string') {
576
+ throw new Error('variant-selection: reason missing or non-string');
577
+ }
578
+ return { blueprintId, confidence, reason };
579
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Minimal structured logger for the OSS server.
3
+ *
4
+ * Hosted closed-runtime code typically uses pino for CloudWatch-shaped
5
+ * logs; the OSS server ships with a zero-dep console emitter by default.
6
+ * Operators who want structured log ingestion can pass their own
7
+ * `Logger` object on `createGguiServer({ logger })`.
8
+ */
9
+ export interface Logger {
10
+ info(event: string, fields?: Record<string, unknown>): void;
11
+ warn(event: string, fields?: Record<string, unknown>): void;
12
+ error(event: string, fields?: Record<string, unknown>): void;
13
+ /** Optional; most call sites never touch debug. */
14
+ debug?(event: string, fields?: Record<string, unknown>): void;
15
+ /** Create a child logger with bound fields. */
16
+ child(fields: Record<string, unknown>): Logger;
17
+ }
18
+ /**
19
+ * JSON-line logger. One `console.log` per event, level-tagged. Quiet
20
+ * unless called — no banners, no colors, no timestamps at boot.
21
+ */
22
+ export declare function createConsoleLogger(bound?: Record<string, unknown>): Logger;
23
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,mDAAmD;IACnD,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9D,+CAA+C;IAC/C,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;CAChD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CA8B/E"}
package/dist/logger.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Minimal structured logger for the OSS server.
3
+ *
4
+ * Hosted closed-runtime code typically uses pino for CloudWatch-shaped
5
+ * logs; the OSS server ships with a zero-dep console emitter by default.
6
+ * Operators who want structured log ingestion can pass their own
7
+ * `Logger` object on `createGguiServer({ logger })`.
8
+ */
9
+ /**
10
+ * JSON-line logger. One `console.log` per event, level-tagged. Quiet
11
+ * unless called — no banners, no colors, no timestamps at boot.
12
+ */
13
+ export function createConsoleLogger(bound = {}) {
14
+ const emit = (level, event, fields) => {
15
+ const record = {
16
+ level,
17
+ time: new Date().toISOString(),
18
+ event,
19
+ ...bound,
20
+ ...fields,
21
+ };
22
+ const line = JSON.stringify(record);
23
+ // The whole point of this module is to emit structured log lines
24
+ // to stdout/stderr — console is the correct primitive here. Hosts
25
+ // that want pino / winston / custom sinks pass their own `Logger`
26
+ // via `createGguiServer({ logger })`.
27
+ // eslint-disable-next-line no-console
28
+ if (level === 'error')
29
+ console.error(line);
30
+ // eslint-disable-next-line no-console
31
+ else
32
+ console.log(line);
33
+ };
34
+ return {
35
+ info: (event, fields) => emit('info', event, fields),
36
+ warn: (event, fields) => emit('warn', event, fields),
37
+ error: (event, fields) => emit('error', event, fields),
38
+ debug: (event, fields) => emit('debug', event, fields),
39
+ child: (fields) => createConsoleLogger({ ...bound, ...fields }),
40
+ };
41
+ }