@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.
- package/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/admin-blueprints-transport.d.ts +114 -0
- package/dist/admin-blueprints-transport.d.ts.map +1 -0
- package/dist/admin-blueprints-transport.js +118 -0
- package/dist/admin-oauth-providers-transport.d.ts +40 -0
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
- package/dist/admin-oauth-providers-transport.js +263 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +75 -0
- package/dist/build-mcp.d.ts +128 -0
- package/dist/build-mcp.d.ts.map +1 -0
- package/dist/build-mcp.js +113 -0
- package/dist/code-store-fs.d.ts +19 -0
- package/dist/code-store-fs.d.ts.map +1 -0
- package/dist/code-store-fs.js +98 -0
- package/dist/console-auth.d.ts +139 -0
- package/dist/console-auth.d.ts.map +1 -0
- package/dist/console-auth.js +102 -0
- package/dist/console-cache.d.ts +78 -0
- package/dist/console-cache.d.ts.map +1 -0
- package/dist/console-cache.js +105 -0
- package/dist/console-headers.d.ts +124 -0
- package/dist/console-headers.d.ts.map +1 -0
- package/dist/console-headers.js +49 -0
- package/dist/console-llm-trace.d.ts +66 -0
- package/dist/console-llm-trace.d.ts.map +1 -0
- package/dist/console-llm-trace.js +105 -0
- package/dist/console-payloads.d.ts +67 -0
- package/dist/console-payloads.d.ts.map +1 -0
- package/dist/console-payloads.js +105 -0
- package/dist/console-theme-routes.d.ts +111 -0
- package/dist/console-theme-routes.d.ts.map +1 -0
- package/dist/console-theme-routes.js +202 -0
- package/dist/console-timeline.d.ts +45 -0
- package/dist/console-timeline.d.ts.map +1 -0
- package/dist/console-timeline.js +169 -0
- package/dist/console-validator.d.ts +67 -0
- package/dist/console-validator.d.ts.map +1 -0
- package/dist/console-validator.js +105 -0
- package/dist/console-welcome.d.ts +7 -0
- package/dist/console-welcome.d.ts.map +1 -0
- package/dist/console-welcome.js +221 -0
- package/dist/csrf-middleware.d.ts +55 -0
- package/dist/csrf-middleware.d.ts.map +1 -0
- package/dist/csrf-middleware.js +138 -0
- package/dist/email-login.d.ts +174 -0
- package/dist/email-login.d.ts.map +1 -0
- package/dist/email-login.js +254 -0
- package/dist/email-resend.d.ts +29 -0
- package/dist/email-resend.d.ts.map +1 -0
- package/dist/email-resend.js +71 -0
- package/dist/email-sender-from-env.d.ts +34 -0
- package/dist/email-sender-from-env.d.ts.map +1 -0
- package/dist/email-sender-from-env.js +112 -0
- package/dist/email-smtp.d.ts +42 -0
- package/dist/email-smtp.d.ts.map +1 -0
- package/dist/email-smtp.js +81 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/instructions-presets.d.ts +112 -0
- package/dist/instructions-presets.d.ts.map +1 -0
- package/dist/instructions-presets.js +195 -0
- package/dist/llm-backed-negotiator.d.ts +178 -0
- package/dist/llm-backed-negotiator.d.ts.map +1 -0
- package/dist/llm-backed-negotiator.js +579 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +41 -0
- package/dist/mcp-apps-inbound.d.ts +86 -0
- package/dist/mcp-apps-inbound.d.ts.map +1 -0
- package/dist/mcp-apps-inbound.js +278 -0
- package/dist/mcp-apps-outbound.d.ts +448 -0
- package/dist/mcp-apps-outbound.d.ts.map +1 -0
- package/dist/mcp-apps-outbound.js +1163 -0
- package/dist/mcp-mounts.d.ts +239 -0
- package/dist/mcp-mounts.d.ts.map +1 -0
- package/dist/mcp-mounts.js +222 -0
- package/dist/oauth-login-types.d.ts +160 -0
- package/dist/oauth-login-types.d.ts.map +1 -0
- package/dist/oauth-login-types.js +9 -0
- package/dist/oauth-login.d.ts +77 -0
- package/dist/oauth-login.d.ts.map +1 -0
- package/dist/oauth-login.js +455 -0
- package/dist/oauth-providers/github.d.ts +17 -0
- package/dist/oauth-providers/github.d.ts.map +1 -0
- package/dist/oauth-providers/github.js +89 -0
- package/dist/oauth-providers/google.d.ts +18 -0
- package/dist/oauth-providers/google.d.ts.map +1 -0
- package/dist/oauth-providers/google.js +59 -0
- package/dist/oauth-providers-store.d.ts +32 -0
- package/dist/oauth-providers-store.d.ts.map +1 -0
- package/dist/oauth-providers-store.js +291 -0
- package/dist/oauth.d.ts +347 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +686 -0
- package/dist/pairing-transport.d.ts +99 -0
- package/dist/pairing-transport.d.ts.map +1 -0
- package/dist/pairing-transport.js +223 -0
- package/dist/rate-limit-middleware.d.ts +36 -0
- package/dist/rate-limit-middleware.d.ts.map +1 -0
- package/dist/rate-limit-middleware.js +57 -0
- package/dist/render-gate.d.ts +87 -0
- package/dist/render-gate.d.ts.map +1 -0
- package/dist/render-gate.js +77 -0
- package/dist/render-rate-limit.d.ts +59 -0
- package/dist/render-rate-limit.d.ts.map +1 -0
- package/dist/render-rate-limit.js +73 -0
- package/dist/render-signing.d.ts +98 -0
- package/dist/render-signing.d.ts.map +1 -0
- package/dist/render-signing.js +113 -0
- package/dist/request-context.d.ts +113 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +154 -0
- package/dist/reserved-validators.d.ts +22 -0
- package/dist/reserved-validators.d.ts.map +1 -0
- package/dist/reserved-validators.js +101 -0
- package/dist/schema-compat.d.ts +167 -0
- package/dist/schema-compat.d.ts.map +1 -0
- package/dist/schema-compat.js +187 -0
- package/dist/security-headers-middleware.d.ts +38 -0
- package/dist/security-headers-middleware.d.ts.map +1 -0
- package/dist/security-headers-middleware.js +30 -0
- package/dist/server.d.ts +2060 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +6338 -0
- package/dist/session-channel.d.ts +651 -0
- package/dist/session-channel.d.ts.map +1 -0
- package/dist/session-channel.js +1756 -0
- package/dist/storage.d.ts +89 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +171 -0
- package/dist/thread-transport.d.ts +118 -0
- package/dist/thread-transport.d.ts.map +1 -0
- package/dist/thread-transport.js +478 -0
- package/dist/user-session-auth.d.ts +167 -0
- package/dist/user-session-auth.d.ts.map +1 -0
- package/dist/user-session-auth.js +148 -0
- 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
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -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
|
+
}
|