@ggui-ai/ui-gen 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/generation-dispatch.d.ts +9 -2
- package/dist/adapters/generation-dispatch.js +693 -89
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +28 -11
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +5 -3
- package/dist/advanced/index.js +714 -101
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +267 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +14 -2
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +11 -1
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-B9kV4U9h.d.ts → create-ui-generator-CF0EL0Li.d.ts} +13 -1
- package/dist/{dispatch-Deo9qzrG.d.ts → dispatch-D75M-OfZ.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +101 -0
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/axis-checks/registry.js +101 -0
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +46 -3
- package/dist/evaluation/index.js +358 -17
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -2
- package/dist/harness/check/runtime-render/index.js +268 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.d.ts +2 -0
- package/dist/harness/check/runtime-render/render-check-worker.js +1332 -0
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -0
- package/dist/harness/index.d.ts +2 -2
- package/dist/harness/index.js +263 -18
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +1 -1
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.js +17 -3
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +1 -1
- package/dist/harness/runtime.d.ts +9 -2
- package/dist/harness/runtime.js +20 -6
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +14 -2
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +708 -95
- package/dist/index.js.map +1 -1
- package/dist/{llm-router-1TyI0_TV.d.ts → llm-router-BEVtbAb9.d.ts} +61 -4
- package/dist/patch.d.ts +1 -1
- package/dist/provider-adapter.d.ts +16 -6
- package/dist/provider-adapter.js.map +1 -1
- package/dist/providers/index.d.ts +19 -6
- package/dist/providers/index.js.map +1 -1
- package/dist/{render-check-CNZjHD5g.d.ts → render-check-BZoIE8-u.d.ts} +21 -1
- package/dist/{types-public-C36k1q6s.d.ts → types-public-DIpjC-OA.d.ts} +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +9 -0
- package/src/adapters/openrouter/client.ts +2 -2
- package/src/adapters/openrouter/types.ts +10 -0
- package/src/boilerplate/generate.ts +10 -2
- package/src/boilerplate/json-schema-ts.ts +22 -2
- package/src/boilerplate/system-prompt.ts +1 -0
- package/src/compile.ts +1 -1
- package/src/contract-context.ts +10 -2
- package/src/create-ui-generator.ts +14 -0
- package/src/design-system-docs.ts +6 -4
- package/src/evaluation/axis-checks/checks/state-ui-affordance.ts +162 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/index.ts +11 -0
- package/src/evaluation/llm-evaluator.ts +14 -1
- package/src/evaluation/visual-evaluator.ts +44 -65
- package/src/harness/check/runtime-render/adapter.ts +18 -0
- package/src/harness/check/runtime-render/index.ts +8 -0
- package/src/harness/check/runtime-render/prepare-mockup.ts +14 -0
- package/src/harness/check/runtime-render/production-act-shim.ts +158 -0
- package/src/harness/check/runtime-render/render-check-host.ts +201 -0
- package/src/harness/check/runtime-render/render-check-worker.ts +80 -0
- package/src/harness/check/runtime-render/render-check.ts +112 -1
- package/src/harness/coding/run-eval-round.ts +61 -5
- package/src/harness/index.ts +1 -1
- package/src/harness/llm-router.ts +397 -18
- package/src/harness/llm-trace-sink.ts +2 -1
- package/src/harness/runtime.ts +7 -0
- package/src/index.ts +5 -5
- package/src/provider-adapter.ts +16 -6
- package/src/providers/bedrock.ts +5 -4
- package/src/providers/index.ts +16 -4
- package/src/tools/render-check.ts +19 -7
package/dist/index.js
CHANGED
|
@@ -3,21 +3,21 @@ import * as esbuild2 from 'esbuild';
|
|
|
3
3
|
import { build } from 'esbuild';
|
|
4
4
|
import Anthropic from '@anthropic-ai/sdk';
|
|
5
5
|
import { classifyUi, MODEL_REGISTRY, listContractGadgets, HOOK_NAME_RE, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
|
|
6
|
-
import fs, { mkdirSync, writeFileSync, unlinkSync,
|
|
6
|
+
import fs, { mkdirSync, writeFileSync, unlinkSync, existsSync, readFileSync, rm, createReadStream, createWriteStream } from 'fs';
|
|
7
7
|
import { contentHash } from '@ggui-ai/protocol/content-hash';
|
|
8
8
|
import ts7 from 'typescript';
|
|
9
9
|
import path, { dirname, resolve, join, basename } from 'path';
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
11
|
import { Linter } from 'eslint';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
13
|
+
import { runSandboxed } from '@ggui-ai/sandbox';
|
|
12
14
|
import { createHash } from 'crypto';
|
|
13
15
|
import 'fs/promises';
|
|
14
16
|
import { tmpdir } from 'os';
|
|
15
17
|
import { Readable } from 'stream';
|
|
16
18
|
import { pipeline } from 'stream/promises';
|
|
17
19
|
import { createBrotliDecompress, createUnzip } from 'zlib';
|
|
18
|
-
import { fileURLToPath, pathToFileURL } from 'url';
|
|
19
20
|
import { parseGeneratorSlug, formatGeneratorSlug, isValidGeneratorSlug } from '@ggui-ai/mcp-server-core';
|
|
20
|
-
import { runSandboxed } from '@ggui-ai/sandbox';
|
|
21
21
|
import git from 'isomorphic-git';
|
|
22
22
|
import { Volume, createFsFromVolume } from 'memfs';
|
|
23
23
|
import { createPatch, applyPatch } from 'diff';
|
|
@@ -1154,14 +1154,16 @@ var OpenRouterError;
|
|
|
1154
1154
|
var init_types = __esm({
|
|
1155
1155
|
"src/adapters/openrouter/types.ts"() {
|
|
1156
1156
|
OpenRouterError = class extends Error {
|
|
1157
|
-
constructor(message, status, code) {
|
|
1157
|
+
constructor(message, status, code, headers) {
|
|
1158
1158
|
super(message);
|
|
1159
1159
|
this.status = status;
|
|
1160
1160
|
this.code = code;
|
|
1161
|
+
this.headers = headers;
|
|
1161
1162
|
this.name = "OpenRouterError";
|
|
1162
1163
|
}
|
|
1163
1164
|
status;
|
|
1164
1165
|
code;
|
|
1166
|
+
headers;
|
|
1165
1167
|
get isTransient() {
|
|
1166
1168
|
return this.status === 408 || this.status === 429 || this.status === 502 || this.status === 503;
|
|
1167
1169
|
}
|
|
@@ -1206,7 +1208,7 @@ var init_client2 = __esm({
|
|
|
1206
1208
|
if (!response.ok) {
|
|
1207
1209
|
const errorBody = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
1208
1210
|
const message = errorBody.error?.message ?? response.statusText;
|
|
1209
|
-
throw new OpenRouterError(message, response.status);
|
|
1211
|
+
throw new OpenRouterError(message, response.status, void 0, response.headers);
|
|
1210
1212
|
}
|
|
1211
1213
|
return response.json();
|
|
1212
1214
|
}
|
|
@@ -1224,7 +1226,7 @@ var init_client2 = __esm({
|
|
|
1224
1226
|
if (!response.ok) {
|
|
1225
1227
|
const errorBody = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
1226
1228
|
const message = errorBody.error?.message ?? response.statusText;
|
|
1227
|
-
throw new OpenRouterError(message, response.status);
|
|
1229
|
+
throw new OpenRouterError(message, response.status, void 0, response.headers);
|
|
1228
1230
|
}
|
|
1229
1231
|
if (!response.body) {
|
|
1230
1232
|
throw new OpenRouterError("No response body for streaming request", 500);
|
|
@@ -2077,6 +2079,15 @@ function jsonSchemaTypeToTs(schema) {
|
|
|
2077
2079
|
if (schema.enum?.length) {
|
|
2078
2080
|
return schema.enum.map((v) => typeof v === "string" ? `'${v}'` : String(v)).join(" | ");
|
|
2079
2081
|
}
|
|
2082
|
+
if (Array.isArray(schema.type)) {
|
|
2083
|
+
const { nullable: _nullable, ...rest } = schema;
|
|
2084
|
+
const members = [...new Set(schema.type)].map(
|
|
2085
|
+
(member) => jsonSchemaTypeToTs({ ...rest, type: member })
|
|
2086
|
+
);
|
|
2087
|
+
const unique = [...new Set(members)];
|
|
2088
|
+
if (schema.nullable && !unique.includes("null")) unique.push("null");
|
|
2089
|
+
return unique.join(" | ");
|
|
2090
|
+
}
|
|
2080
2091
|
let result;
|
|
2081
2092
|
switch (schema.type) {
|
|
2082
2093
|
case "string":
|
|
@@ -2094,7 +2105,8 @@ function jsonSchemaTypeToTs(schema) {
|
|
|
2094
2105
|
case "array": {
|
|
2095
2106
|
if (schema.items) {
|
|
2096
2107
|
const itemType = jsonSchemaTypeToTs(schema.items);
|
|
2097
|
-
|
|
2108
|
+
const itemsDeclareObject = Array.isArray(schema.items.type) ? schema.items.type.includes("object") : schema.items.type === "object";
|
|
2109
|
+
result = itemsDeclareObject && schema.items.properties ? `Array<${itemType}>` : itemType.includes("|") ? `(${itemType})[]` : `${itemType}[]`;
|
|
2098
2110
|
} else {
|
|
2099
2111
|
result = "unknown[]";
|
|
2100
2112
|
}
|
|
@@ -4112,6 +4124,29 @@ function errorSummary(e) {
|
|
|
4112
4124
|
}
|
|
4113
4125
|
return `${e.constructor.name}${status}${code}: ${e.message.slice(0, 120)}${body}`;
|
|
4114
4126
|
}
|
|
4127
|
+
function isRateLimited(e) {
|
|
4128
|
+
if (!(e instanceof Error) || !("status" in e)) return false;
|
|
4129
|
+
return e.status === 429;
|
|
4130
|
+
}
|
|
4131
|
+
function extractStatus(e) {
|
|
4132
|
+
if (!(e instanceof Error) || !("status" in e)) return void 0;
|
|
4133
|
+
const status = e.status;
|
|
4134
|
+
return typeof status === "number" ? status : void 0;
|
|
4135
|
+
}
|
|
4136
|
+
function extractRetryAfterSec(e) {
|
|
4137
|
+
if (!(e instanceof Error) || !("headers" in e)) return void 0;
|
|
4138
|
+
const headers = e.headers;
|
|
4139
|
+
if (headers === null || typeof headers !== "object" || !("get" in headers) || typeof headers.get !== "function") {
|
|
4140
|
+
return void 0;
|
|
4141
|
+
}
|
|
4142
|
+
const raw = headers.get("retry-after");
|
|
4143
|
+
if (raw === null) return void 0;
|
|
4144
|
+
const parsed = Number(raw);
|
|
4145
|
+
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : void 0;
|
|
4146
|
+
}
|
|
4147
|
+
function sleep(ms) {
|
|
4148
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4149
|
+
}
|
|
4115
4150
|
function interactionText(interaction) {
|
|
4116
4151
|
let text = "";
|
|
4117
4152
|
for (const step of interaction.steps ?? []) {
|
|
@@ -4136,24 +4171,38 @@ function toUserInputStep(text) {
|
|
|
4136
4171
|
function createAgent(providerOrConfig) {
|
|
4137
4172
|
const provider = typeof providerOrConfig === "string" ? providerOrConfig : providerOrConfig.provider;
|
|
4138
4173
|
const routeOverride = typeof providerOrConfig === "string" ? void 0 : providerOrConfig.routeOverride;
|
|
4174
|
+
const onRetry = typeof providerOrConfig === "string" ? void 0 : providerOrConfig.onRetry;
|
|
4139
4175
|
switch (provider) {
|
|
4140
4176
|
case "anthropic":
|
|
4141
|
-
return new AnthropicAgent(routeOverride);
|
|
4177
|
+
return new AnthropicAgent(routeOverride, onRetry);
|
|
4142
4178
|
case "openai":
|
|
4143
|
-
return new OpenAIAgent(routeOverride);
|
|
4179
|
+
return new OpenAIAgent(routeOverride, onRetry);
|
|
4144
4180
|
case "google":
|
|
4145
|
-
return new GoogleAgent(routeOverride);
|
|
4181
|
+
return new GoogleAgent(routeOverride, onRetry);
|
|
4146
4182
|
case "openrouter":
|
|
4147
|
-
return new OpenRouterAgent(routeOverride);
|
|
4183
|
+
return new OpenRouterAgent(routeOverride, onRetry);
|
|
4148
4184
|
}
|
|
4149
4185
|
}
|
|
4150
|
-
|
|
4186
|
+
function createVisionAgent(config) {
|
|
4187
|
+
switch (config.provider) {
|
|
4188
|
+
case "anthropic":
|
|
4189
|
+
return new AnthropicAgent(config.routeOverride, config.onRetry);
|
|
4190
|
+
case "google":
|
|
4191
|
+
return new GoogleAgent(config.routeOverride, config.onRetry);
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
var MAX_RETRY_ATTEMPTS, RETRY_AFTER_CAP_SEC, DEFAULT_BACKOFF_BASE_MS, DEFAULT_BACKOFF_MAX_MS, MAX_TOTAL_RETRY_DELAY_MS, LLMAgent, AnthropicAgent, OpenAIAgent, GoogleAgent, OpenRouterAgent;
|
|
4151
4195
|
var init_llm_router = __esm({
|
|
4152
4196
|
"src/harness/llm-router.ts"() {
|
|
4153
4197
|
init_client();
|
|
4154
4198
|
init_provider_router();
|
|
4155
4199
|
init_tool_bridge();
|
|
4156
4200
|
init_llm_trace_sink();
|
|
4201
|
+
MAX_RETRY_ATTEMPTS = 2;
|
|
4202
|
+
RETRY_AFTER_CAP_SEC = 15;
|
|
4203
|
+
DEFAULT_BACKOFF_BASE_MS = 1e3;
|
|
4204
|
+
DEFAULT_BACKOFF_MAX_MS = 8e3;
|
|
4205
|
+
MAX_TOTAL_RETRY_DELAY_MS = 2e4;
|
|
4157
4206
|
LLMAgent = class {
|
|
4158
4207
|
client = null;
|
|
4159
4208
|
// ── Session state for server-side chaining ──────────────
|
|
@@ -4168,8 +4217,11 @@ var init_llm_router = __esm({
|
|
|
4168
4217
|
* back to `process.env` exactly as before.
|
|
4169
4218
|
*/
|
|
4170
4219
|
routeOverride;
|
|
4171
|
-
|
|
4220
|
+
/** See `AgentConfig.onRetry` (#489). Absent is a no-op. */
|
|
4221
|
+
onRetry;
|
|
4222
|
+
constructor(routeOverride, onRetry) {
|
|
4172
4223
|
this.routeOverride = routeOverride;
|
|
4224
|
+
this.onRetry = onRetry;
|
|
4173
4225
|
}
|
|
4174
4226
|
async getClient() {
|
|
4175
4227
|
if (!this.client) {
|
|
@@ -4204,17 +4256,83 @@ var init_llm_router = __esm({
|
|
|
4204
4256
|
async sendToolResult(_results) {
|
|
4205
4257
|
}
|
|
4206
4258
|
/**
|
|
4207
|
-
* Execute an API call
|
|
4208
|
-
*
|
|
4259
|
+
* Execute an API call, retrying once or twice on HTTP 429
|
|
4260
|
+
* (rate-limited) before giving up (#489).
|
|
4261
|
+
*
|
|
4262
|
+
* Policy: on a 429, honor the provider's `Retry-After` header when
|
|
4263
|
+
* present (capped at `RETRY_AFTER_CAP_SEC` — a provider asking for a
|
|
4264
|
+
* longer wait is treated as "don't retry", not "wait longer": making
|
|
4265
|
+
* the caller sit through 15+ more seconds on a single failed call is
|
|
4266
|
+
* a bad experience regardless of what's driving the request);
|
|
4267
|
+
* otherwise fall back to exponential backoff with jitter, capped at
|
|
4268
|
+
* `DEFAULT_BACKOFF_MAX_MS` per attempt. Stops after
|
|
4269
|
+
* `MAX_RETRY_ATTEMPTS` retries OR once the cumulative delay would
|
|
4270
|
+
* exceed `MAX_TOTAL_RETRY_DELAY_MS`, whichever comes first. Any
|
|
4271
|
+
* non-429 error, or a 429 with no attempts left, is logged and
|
|
4272
|
+
* re-thrown immediately — unchanged from the pre-#489 behavior.
|
|
4273
|
+
*
|
|
4274
|
+
* **Budget scope (final-review correction):** `MAX_TOTAL_RETRY_DELAY_MS`
|
|
4275
|
+
* bounds a single `apiCall()` invocation, not a whole generation. A
|
|
4276
|
+
* generation issues many sequential `apiCall()` calls (coding turns,
|
|
4277
|
+
* tool-result round-trips, eval rounds); under sustained throttling
|
|
4278
|
+
* each one can independently add up to `MAX_TOTAL_RETRY_DELAY_MS`, so
|
|
4279
|
+
* a generation with k rate-limited calls can add up to roughly `k *
|
|
4280
|
+
* MAX_TOTAL_RETRY_DELAY_MS` to how long it holds its caller's
|
|
4281
|
+
* resources (e.g. the caller's concurrency slot) — not a flat 20s
|
|
4282
|
+
* ceiling. No shared per-generation budget is threaded here by
|
|
4283
|
+
* design (accepted ruling, not an oversight): the caller's own
|
|
4284
|
+
* queue/admission backstop is what bounds how long other
|
|
4285
|
+
* queued work waits, independent of any one generation's retry
|
|
4286
|
+
* total.
|
|
4209
4287
|
*/
|
|
4210
4288
|
async apiCall(fn) {
|
|
4211
4289
|
const start = Date.now();
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4290
|
+
let totalDelayMs = 0;
|
|
4291
|
+
for (let attempt = 0; ; attempt++) {
|
|
4292
|
+
try {
|
|
4293
|
+
return await fn();
|
|
4294
|
+
} catch (e) {
|
|
4295
|
+
const ms = Date.now() - start;
|
|
4296
|
+
if (attempt >= MAX_RETRY_ATTEMPTS || !isRateLimited(e)) {
|
|
4297
|
+
console.error(`[${this.provider}] API error after ${ms}ms: ${errorSummary(e)}`);
|
|
4298
|
+
throw e;
|
|
4299
|
+
}
|
|
4300
|
+
const retryAfterSec = extractRetryAfterSec(e);
|
|
4301
|
+
if (retryAfterSec !== void 0 && retryAfterSec > RETRY_AFTER_CAP_SEC) {
|
|
4302
|
+
console.error(
|
|
4303
|
+
`[${this.provider}] API error after ${ms}ms (retry-after ${retryAfterSec}s exceeds ${RETRY_AFTER_CAP_SEC}s cap, not retrying): ${errorSummary(e)}`
|
|
4304
|
+
);
|
|
4305
|
+
throw e;
|
|
4306
|
+
}
|
|
4307
|
+
const delayMs = retryAfterSec !== void 0 ? retryAfterSec * 1e3 : Math.min(DEFAULT_BACKOFF_BASE_MS * 2 ** attempt, DEFAULT_BACKOFF_MAX_MS) + Math.floor(Math.random() * 250);
|
|
4308
|
+
if (totalDelayMs + delayMs > MAX_TOTAL_RETRY_DELAY_MS) {
|
|
4309
|
+
console.error(
|
|
4310
|
+
`[${this.provider}] API error after ${ms}ms (retry budget exhausted, not retrying): ${errorSummary(e)}`
|
|
4311
|
+
);
|
|
4312
|
+
throw e;
|
|
4313
|
+
}
|
|
4314
|
+
totalDelayMs += delayMs;
|
|
4315
|
+
const info = {
|
|
4316
|
+
provider: this.provider,
|
|
4317
|
+
attempt: attempt + 1,
|
|
4318
|
+
maxAttempts: MAX_RETRY_ATTEMPTS,
|
|
4319
|
+
status: extractStatus(e),
|
|
4320
|
+
retryAfterSec,
|
|
4321
|
+
delayMs,
|
|
4322
|
+
message: errorSummary(e)
|
|
4323
|
+
};
|
|
4324
|
+
console.warn(
|
|
4325
|
+
`[${this.provider}] rate-limited (attempt ${info.attempt}/${info.maxAttempts}), retrying in ${delayMs}ms`
|
|
4326
|
+
);
|
|
4327
|
+
try {
|
|
4328
|
+
this.onRetry?.(info);
|
|
4329
|
+
} catch (observerError) {
|
|
4330
|
+
console.warn(
|
|
4331
|
+
`[${this.provider}] onRetry observer threw (ignored, retry proceeds): ${errorSummary(observerError)}`
|
|
4332
|
+
);
|
|
4333
|
+
}
|
|
4334
|
+
await sleep(delayMs);
|
|
4335
|
+
}
|
|
4218
4336
|
}
|
|
4219
4337
|
}
|
|
4220
4338
|
};
|
|
@@ -4307,6 +4425,73 @@ var init_llm_router = __esm({
|
|
|
4307
4425
|
throw e;
|
|
4308
4426
|
}
|
|
4309
4427
|
}
|
|
4428
|
+
async callVision(model, systemPrompt, userPrompt, image, maxTokens) {
|
|
4429
|
+
const client = await this.getClient();
|
|
4430
|
+
const traceId = newLlmTraceId();
|
|
4431
|
+
const startedAt = Date.now();
|
|
4432
|
+
const resolvedModel = this.resolveModel(model);
|
|
4433
|
+
const imageBlock = {
|
|
4434
|
+
type: "image",
|
|
4435
|
+
source: {
|
|
4436
|
+
type: "base64",
|
|
4437
|
+
media_type: image.mediaType,
|
|
4438
|
+
data: image.base64
|
|
4439
|
+
}
|
|
4440
|
+
};
|
|
4441
|
+
try {
|
|
4442
|
+
const response = await this.apiCall(
|
|
4443
|
+
() => client.messages.create({
|
|
4444
|
+
model: resolvedModel,
|
|
4445
|
+
max_tokens: maxTokens ?? 1500,
|
|
4446
|
+
system: systemPrompt,
|
|
4447
|
+
messages: [
|
|
4448
|
+
{
|
|
4449
|
+
role: "user",
|
|
4450
|
+
content: [imageBlock, { type: "text", text: userPrompt }]
|
|
4451
|
+
}
|
|
4452
|
+
]
|
|
4453
|
+
})
|
|
4454
|
+
);
|
|
4455
|
+
const text = response.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
4456
|
+
const endedAt = Date.now();
|
|
4457
|
+
emitLlmTraceEvent({
|
|
4458
|
+
id: traceId,
|
|
4459
|
+
at: startedAt,
|
|
4460
|
+
endedAt,
|
|
4461
|
+
durationMs: endedAt - startedAt,
|
|
4462
|
+
provider: "anthropic",
|
|
4463
|
+
model: resolvedModel,
|
|
4464
|
+
kind: "callVision",
|
|
4465
|
+
systemPrompt,
|
|
4466
|
+
userPrompt,
|
|
4467
|
+
result: {
|
|
4468
|
+
inputTokens: response.usage.input_tokens,
|
|
4469
|
+
outputTokens: response.usage.output_tokens,
|
|
4470
|
+
text
|
|
4471
|
+
}
|
|
4472
|
+
});
|
|
4473
|
+
return {
|
|
4474
|
+
text,
|
|
4475
|
+
inputTokens: response.usage.input_tokens,
|
|
4476
|
+
outputTokens: response.usage.output_tokens
|
|
4477
|
+
};
|
|
4478
|
+
} catch (e) {
|
|
4479
|
+
const endedAt = Date.now();
|
|
4480
|
+
emitLlmTraceEvent({
|
|
4481
|
+
id: traceId,
|
|
4482
|
+
at: startedAt,
|
|
4483
|
+
endedAt,
|
|
4484
|
+
durationMs: endedAt - startedAt,
|
|
4485
|
+
provider: "anthropic",
|
|
4486
|
+
model: resolvedModel,
|
|
4487
|
+
kind: "callVision",
|
|
4488
|
+
systemPrompt,
|
|
4489
|
+
userPrompt,
|
|
4490
|
+
error: { message: e instanceof Error ? e.message : String(e) }
|
|
4491
|
+
});
|
|
4492
|
+
throw e;
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4310
4495
|
async callTools(model, systemPrompt, userPrompt, tools, toolChoice = "required", _scopedTools) {
|
|
4311
4496
|
const client = await this.getClient();
|
|
4312
4497
|
const traceId = newLlmTraceId();
|
|
@@ -4681,6 +4866,72 @@ var init_llm_router = __esm({
|
|
|
4681
4866
|
outputTokens: interaction.usage?.total_output_tokens ?? 0
|
|
4682
4867
|
};
|
|
4683
4868
|
}
|
|
4869
|
+
async callVision(model, systemPrompt, userPrompt, image, maxTokens) {
|
|
4870
|
+
const client = await this.getClient();
|
|
4871
|
+
const traceId = newLlmTraceId();
|
|
4872
|
+
const startedAt = Date.now();
|
|
4873
|
+
const resolvedModel = this.resolveModel(model);
|
|
4874
|
+
try {
|
|
4875
|
+
const response = await this.apiCall(
|
|
4876
|
+
() => client.models.generateContent({
|
|
4877
|
+
model: resolvedModel,
|
|
4878
|
+
contents: [
|
|
4879
|
+
{
|
|
4880
|
+
role: "user",
|
|
4881
|
+
parts: [
|
|
4882
|
+
{ inlineData: { mimeType: image.mediaType, data: image.base64 } },
|
|
4883
|
+
{ text: userPrompt }
|
|
4884
|
+
]
|
|
4885
|
+
}
|
|
4886
|
+
],
|
|
4887
|
+
config: {
|
|
4888
|
+
systemInstruction: systemPrompt,
|
|
4889
|
+
...maxTokens ? { maxOutputTokens: maxTokens } : {}
|
|
4890
|
+
}
|
|
4891
|
+
})
|
|
4892
|
+
);
|
|
4893
|
+
const text = response.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("") || "";
|
|
4894
|
+
const usage = response.usageMetadata;
|
|
4895
|
+
const result = {
|
|
4896
|
+
text,
|
|
4897
|
+
inputTokens: usage?.promptTokenCount ?? 0,
|
|
4898
|
+
outputTokens: usage?.candidatesTokenCount ?? 0
|
|
4899
|
+
};
|
|
4900
|
+
const endedAt = Date.now();
|
|
4901
|
+
emitLlmTraceEvent({
|
|
4902
|
+
id: traceId,
|
|
4903
|
+
at: startedAt,
|
|
4904
|
+
endedAt,
|
|
4905
|
+
durationMs: endedAt - startedAt,
|
|
4906
|
+
provider: "google",
|
|
4907
|
+
model: resolvedModel,
|
|
4908
|
+
kind: "callVision",
|
|
4909
|
+
systemPrompt,
|
|
4910
|
+
userPrompt,
|
|
4911
|
+
result: {
|
|
4912
|
+
inputTokens: result.inputTokens,
|
|
4913
|
+
outputTokens: result.outputTokens,
|
|
4914
|
+
text
|
|
4915
|
+
}
|
|
4916
|
+
});
|
|
4917
|
+
return result;
|
|
4918
|
+
} catch (e) {
|
|
4919
|
+
const endedAt = Date.now();
|
|
4920
|
+
emitLlmTraceEvent({
|
|
4921
|
+
id: traceId,
|
|
4922
|
+
at: startedAt,
|
|
4923
|
+
endedAt,
|
|
4924
|
+
durationMs: endedAt - startedAt,
|
|
4925
|
+
provider: "google",
|
|
4926
|
+
model: resolvedModel,
|
|
4927
|
+
kind: "callVision",
|
|
4928
|
+
systemPrompt,
|
|
4929
|
+
userPrompt,
|
|
4930
|
+
error: { message: e instanceof Error ? e.message : String(e) }
|
|
4931
|
+
});
|
|
4932
|
+
throw e;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4684
4935
|
async callTools(model, systemPrompt, userPrompt, tools, _toolChoice = "required", scopedTools) {
|
|
4685
4936
|
const client = await this.getClient();
|
|
4686
4937
|
const interactionTools = tools.map((t) => ({
|
|
@@ -5191,6 +5442,140 @@ var init_policy2 = __esm({
|
|
|
5191
5442
|
}
|
|
5192
5443
|
});
|
|
5193
5444
|
|
|
5445
|
+
// src/harness/check/runtime-render/render-check-host.ts
|
|
5446
|
+
var render_check_host_exports = {};
|
|
5447
|
+
__export(render_check_host_exports, {
|
|
5448
|
+
mapSandboxResultToCheckResult: () => mapSandboxResultToCheckResult,
|
|
5449
|
+
runRenderCheckViaWorker: () => runRenderCheckViaWorker
|
|
5450
|
+
});
|
|
5451
|
+
function resolveWorkerSpawn2() {
|
|
5452
|
+
const jsCandidates = [
|
|
5453
|
+
// Sibling — when import.meta.url is already .../harness/check/runtime-render/.
|
|
5454
|
+
new URL(`./${WORKER_BASENAME}.js`, import.meta.url),
|
|
5455
|
+
// From dist/index.js.
|
|
5456
|
+
new URL(`./harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
|
|
5457
|
+
// From dist/harness/index.js.
|
|
5458
|
+
new URL(`./check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
|
|
5459
|
+
// From dist/check/index.js, dist/advanced/index.js, dist/<flat>.js.
|
|
5460
|
+
new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url)
|
|
5461
|
+
];
|
|
5462
|
+
for (const jsUrl of jsCandidates) {
|
|
5463
|
+
const jsPath = fileURLToPath(jsUrl);
|
|
5464
|
+
if (existsSync(jsPath)) {
|
|
5465
|
+
return { command: process.execPath, args: [jsPath] };
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
const tsCandidates = [
|
|
5469
|
+
new URL(`./${WORKER_BASENAME}.ts`, import.meta.url),
|
|
5470
|
+
new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.ts`, import.meta.url)
|
|
5471
|
+
];
|
|
5472
|
+
for (const tsUrl of tsCandidates) {
|
|
5473
|
+
const tsPath = fileURLToPath(tsUrl);
|
|
5474
|
+
if (existsSync(tsPath)) {
|
|
5475
|
+
const require_ = createRequire(import.meta.url);
|
|
5476
|
+
const tsxLoader = pathToFileURL(require_.resolve("tsx")).href;
|
|
5477
|
+
return { command: process.execPath, args: ["--import", tsxLoader, tsPath] };
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
const tried = [...jsCandidates, ...tsCandidates].map((u) => fileURLToPath(u)).join(", ");
|
|
5481
|
+
throw new Error(
|
|
5482
|
+
`render-check: isolated worker not found at any of: ${tried}. Did \`pnpm build\` run?`
|
|
5483
|
+
);
|
|
5484
|
+
}
|
|
5485
|
+
function failed(reason, t0) {
|
|
5486
|
+
return {
|
|
5487
|
+
ok: false,
|
|
5488
|
+
issues: [{ check: "render-no-throw", outcome: "failed", reason }],
|
|
5489
|
+
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5492
|
+
function unverified(reason, t0) {
|
|
5493
|
+
return {
|
|
5494
|
+
ok: false,
|
|
5495
|
+
issues: [{ check: "render-no-throw", outcome: "unverified", reason }],
|
|
5496
|
+
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
function mapSandboxResultToCheckResult(result, t0) {
|
|
5500
|
+
if (result.outcome === "timeout") {
|
|
5501
|
+
return failed(
|
|
5502
|
+
`render check timed out after ${CHECK_TIMEOUT_MS}ms in the isolated worker (likely infinite loop or runaway effect in the component).`,
|
|
5503
|
+
t0
|
|
5504
|
+
);
|
|
5505
|
+
}
|
|
5506
|
+
if (result.outcome === "overflow-stdout" || result.outcome === "overflow-stderr") {
|
|
5507
|
+
return failed(
|
|
5508
|
+
`render check worker produced excessive output (${result.outcome}) \u2014 the component is likely in a pathological logging or error loop.`,
|
|
5509
|
+
t0
|
|
5510
|
+
);
|
|
5511
|
+
}
|
|
5512
|
+
if (result.outcome === "spawn-error") {
|
|
5513
|
+
return unverified(
|
|
5514
|
+
`render check worker failed to start \u2014 ${result.errorMessage}`,
|
|
5515
|
+
t0
|
|
5516
|
+
);
|
|
5517
|
+
}
|
|
5518
|
+
if (result.outcome === "canceled") {
|
|
5519
|
+
return unverified("render check worker was canceled before completing.", t0);
|
|
5520
|
+
}
|
|
5521
|
+
if (result.outcome !== "exit") {
|
|
5522
|
+
return unverified(
|
|
5523
|
+
`render check worker ended with unexpected sandbox outcome '${result.outcome}'.`,
|
|
5524
|
+
t0
|
|
5525
|
+
);
|
|
5526
|
+
}
|
|
5527
|
+
if (result.exitCode !== 0) {
|
|
5528
|
+
const tail = result.stderr.trim() || result.stdout.trim();
|
|
5529
|
+
return unverified(
|
|
5530
|
+
`render check worker exited ${result.exitCode}${tail ? ` \u2014 ${tail.slice(0, 600)}` : ""}`,
|
|
5531
|
+
t0
|
|
5532
|
+
);
|
|
5533
|
+
}
|
|
5534
|
+
const stdout = result.stdout.trim();
|
|
5535
|
+
if (stdout.length === 0) {
|
|
5536
|
+
return unverified("render check worker exited without producing a verdict.", t0);
|
|
5537
|
+
}
|
|
5538
|
+
try {
|
|
5539
|
+
return JSON.parse(stdout);
|
|
5540
|
+
} catch (err) {
|
|
5541
|
+
return unverified(
|
|
5542
|
+
`render check worker verdict was not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
5543
|
+
t0
|
|
5544
|
+
);
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
async function runRenderCheckViaWorker(input) {
|
|
5548
|
+
const t0 = Date.now();
|
|
5549
|
+
let spawn;
|
|
5550
|
+
try {
|
|
5551
|
+
spawn = resolveWorkerSpawn2();
|
|
5552
|
+
} catch (err) {
|
|
5553
|
+
return unverified(err instanceof Error ? err.message : String(err), t0);
|
|
5554
|
+
}
|
|
5555
|
+
const result = await runSandboxed({
|
|
5556
|
+
command: spawn.command,
|
|
5557
|
+
args: spawn.args,
|
|
5558
|
+
timeoutMs: CHECK_TIMEOUT_MS,
|
|
5559
|
+
maxStdoutBytes: CHECK_STDOUT_CAP,
|
|
5560
|
+
nodeHeapMb: CHECK_NODE_HEAP_MB,
|
|
5561
|
+
stdin: JSON.stringify(input),
|
|
5562
|
+
// NODE_ENV steers React's production vs development build — keep
|
|
5563
|
+
// parity with the caller; everything else stays on the sandbox's
|
|
5564
|
+
// default allowlist.
|
|
5565
|
+
env: { NODE_ENV: process.env.NODE_ENV ?? "production" }
|
|
5566
|
+
});
|
|
5567
|
+
return mapSandboxResultToCheckResult(result, t0);
|
|
5568
|
+
}
|
|
5569
|
+
var CHECK_TIMEOUT_MS, CHECK_STDOUT_CAP, CHECK_NODE_HEAP_MB, WORKER_BASENAME;
|
|
5570
|
+
var init_render_check_host = __esm({
|
|
5571
|
+
"src/harness/check/runtime-render/render-check-host.ts"() {
|
|
5572
|
+
CHECK_TIMEOUT_MS = 3e4;
|
|
5573
|
+
CHECK_STDOUT_CAP = 2 * 1024 * 1024;
|
|
5574
|
+
CHECK_NODE_HEAP_MB = 512;
|
|
5575
|
+
WORKER_BASENAME = "render-check-worker";
|
|
5576
|
+
}
|
|
5577
|
+
});
|
|
5578
|
+
|
|
5194
5579
|
// src/harness/hashline.ts
|
|
5195
5580
|
var hashline_exports = {};
|
|
5196
5581
|
__export(hashline_exports, {
|
|
@@ -6025,7 +6410,11 @@ async function runLLMEvaluation(context, config, preWarmedContext) {
|
|
|
6025
6410
|
designSystemSummary: context.designContext
|
|
6026
6411
|
});
|
|
6027
6412
|
const numberedSource = context.sourceCode.split("\n").map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
|
|
6028
|
-
const agent = createAgent({
|
|
6413
|
+
const agent = createAgent({
|
|
6414
|
+
provider: routerProvider,
|
|
6415
|
+
routeOverride: config.routeOverride,
|
|
6416
|
+
onRetry: config.onRetry
|
|
6417
|
+
});
|
|
6029
6418
|
const evalStart = Date.now();
|
|
6030
6419
|
const totalCriteriaCount = TIER_1_CRITERIA.length + TIER_2_CRITERIA.length;
|
|
6031
6420
|
const results = await Promise.all([
|
|
@@ -9393,6 +9782,7 @@ var visual_evaluator_exports = {};
|
|
|
9393
9782
|
__export(visual_evaluator_exports, {
|
|
9394
9783
|
buildRenderHTML: () => buildRenderHTML,
|
|
9395
9784
|
bundleForRendering: () => bundleForRendering,
|
|
9785
|
+
callMultimodalLLM: () => callMultimodalLLM,
|
|
9396
9786
|
runVisualEval: () => runVisualEval,
|
|
9397
9787
|
runVisualEvaluation: () => runVisualEvaluation
|
|
9398
9788
|
});
|
|
@@ -9517,63 +9907,23 @@ async function captureScreenshot(html, viewport = { width: 1280, height: 800 })
|
|
|
9517
9907
|
return null;
|
|
9518
9908
|
}
|
|
9519
9909
|
}
|
|
9520
|
-
async function callMultimodalLLM(
|
|
9910
|
+
async function callMultimodalLLM(config, model, prompt, screenshot, originalPrompt) {
|
|
9521
9911
|
const userPrompt = `## Original Request
|
|
9522
9912
|
${originalPrompt}
|
|
9523
9913
|
|
|
9524
9914
|
Evaluate the screenshot of the generated component.`;
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
baseURL: "https://api.anthropic.com"
|
|
9530
|
-
});
|
|
9531
|
-
const response2 = await client2.messages.create({
|
|
9532
|
-
model: model.startsWith("anthropic/") ? model.slice(10) : model,
|
|
9533
|
-
max_tokens: 1500,
|
|
9534
|
-
system: prompt,
|
|
9535
|
-
messages: [{
|
|
9536
|
-
role: "user",
|
|
9537
|
-
content: [
|
|
9538
|
-
{
|
|
9539
|
-
type: "image",
|
|
9540
|
-
source: {
|
|
9541
|
-
type: "base64",
|
|
9542
|
-
media_type: "image/png",
|
|
9543
|
-
data: screenshot.toString("base64")
|
|
9544
|
-
}
|
|
9545
|
-
},
|
|
9546
|
-
{ type: "text", text: userPrompt }
|
|
9547
|
-
]
|
|
9548
|
-
}]
|
|
9549
|
-
});
|
|
9550
|
-
return {
|
|
9551
|
-
text: response2.content.filter((b) => b.type === "text").map((b) => b.text).join(""),
|
|
9552
|
-
inputTokens: response2.usage.input_tokens,
|
|
9553
|
-
outputTokens: response2.usage.output_tokens
|
|
9554
|
-
};
|
|
9555
|
-
}
|
|
9556
|
-
const { GoogleGenAI } = await import('@google/genai');
|
|
9557
|
-
const client = new GoogleGenAI({
|
|
9558
|
-
apiKey: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
|
|
9915
|
+
const agent = createVisionAgent({
|
|
9916
|
+
provider: config.provider === "claude" ? "anthropic" : config.provider,
|
|
9917
|
+
routeOverride: config.routeOverride,
|
|
9918
|
+
onRetry: config.onRetry
|
|
9559
9919
|
});
|
|
9560
|
-
|
|
9561
|
-
model
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
]
|
|
9568
|
-
}],
|
|
9569
|
-
config: { systemInstruction: prompt }
|
|
9570
|
-
});
|
|
9571
|
-
const usage = response.usageMetadata;
|
|
9572
|
-
return {
|
|
9573
|
-
text: response.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("") || "",
|
|
9574
|
-
inputTokens: usage?.promptTokenCount ?? 0,
|
|
9575
|
-
outputTokens: usage?.candidatesTokenCount ?? 0
|
|
9576
|
-
};
|
|
9920
|
+
return agent.callVision(
|
|
9921
|
+
model,
|
|
9922
|
+
prompt,
|
|
9923
|
+
userPrompt,
|
|
9924
|
+
{ mediaType: "image/png", base64: screenshot.toString("base64") },
|
|
9925
|
+
1500
|
|
9926
|
+
);
|
|
9577
9927
|
}
|
|
9578
9928
|
async function runVisualEvaluation(context, config) {
|
|
9579
9929
|
const startTime = Date.now();
|
|
@@ -9591,7 +9941,7 @@ async function runVisualEvaluation(context, config) {
|
|
|
9591
9941
|
console.log(`[visual-eval] screenshot: ${screenshot.length}B (${Date.now() - startTime}ms)`);
|
|
9592
9942
|
const model = config.model ?? getDefaultVisualModel(config.provider);
|
|
9593
9943
|
const response = await callMultimodalLLM(
|
|
9594
|
-
config
|
|
9944
|
+
config,
|
|
9595
9945
|
model,
|
|
9596
9946
|
VISUAL_EVAL_PROMPT,
|
|
9597
9947
|
screenshot,
|
|
@@ -9665,6 +10015,7 @@ async function runVisualEval(context, config) {
|
|
|
9665
10015
|
var VISUAL_EVAL_PROMPT;
|
|
9666
10016
|
var init_visual_evaluator = __esm({
|
|
9667
10017
|
"src/evaluation/visual-evaluator.ts"() {
|
|
10018
|
+
init_llm_router();
|
|
9668
10019
|
VISUAL_EVAL_PROMPT = `You are a visual quality evaluator for ggui \u2014 a platform where AI agents generate React UI components on demand.
|
|
9669
10020
|
|
|
9670
10021
|
## Context
|
|
@@ -10127,10 +10478,12 @@ These tokens adapt automatically to any theme (light, dark, branded). **ALWAYS u
|
|
|
10127
10478
|
- Borders/dividers \u2192 \`var(--ggui-color-outline)\` or \`var(--ggui-color-outlineVariant)\`
|
|
10128
10479
|
|
|
10129
10480
|
### State Colors
|
|
10130
|
-
|
|
10131
|
-
- var(--ggui-color-
|
|
10132
|
-
- var(--ggui-color-
|
|
10133
|
-
- var(--ggui-color-
|
|
10481
|
+
Semantic families ship as SCALES; the \`500\` stop is the role color (per-mode brightness), \`50\` the tint, \`800\` the on-tint text:
|
|
10482
|
+
- var(--ggui-color-success-500) \u2014 success states (tint bg: success-50, text on tint: success-800)
|
|
10483
|
+
- var(--ggui-color-warning-500) \u2014 warning states (warning-50 / warning-800)
|
|
10484
|
+
- var(--ggui-color-error-500) \u2014 error states, destructive actions (error-50 / error-800)
|
|
10485
|
+
- var(--ggui-color-info-500) \u2014 informational (info-50 / info-800)
|
|
10486
|
+
Prefer the primitives' \`tone="success|warning|error|info"\` prop over hand-written vars \u2014 it resolves to the same stops.
|
|
10134
10487
|
|
|
10135
10488
|
### IMPORTANT: Color Rules
|
|
10136
10489
|
- **NEVER** use hardcoded hex colors. ONLY use var(--ggui-color-*) tokens.
|
|
@@ -10430,16 +10783,19 @@ function generateSampleProps(spec) {
|
|
|
10430
10783
|
return props;
|
|
10431
10784
|
}
|
|
10432
10785
|
function synthesizeFromSchema(schema) {
|
|
10433
|
-
|
|
10786
|
+
const declared = Array.isArray(schema.type) ? schema.type.find((t) => t !== "null") : schema.type;
|
|
10787
|
+
switch (declared) {
|
|
10434
10788
|
case "string":
|
|
10435
10789
|
return "sample";
|
|
10436
10790
|
case "number":
|
|
10791
|
+
case "integer":
|
|
10437
10792
|
return 0;
|
|
10438
10793
|
case "boolean":
|
|
10439
10794
|
return false;
|
|
10795
|
+
case "null":
|
|
10796
|
+
return null;
|
|
10440
10797
|
case "array": {
|
|
10441
|
-
|
|
10442
|
-
if (itemSchema) return [synthesizeFromSchema(itemSchema) ?? null];
|
|
10798
|
+
if (schema.items) return [synthesizeFromSchema(schema.items) ?? null];
|
|
10443
10799
|
return [];
|
|
10444
10800
|
}
|
|
10445
10801
|
case "object": {
|
|
@@ -12362,13 +12718,14 @@ function generateBoilerplate(_userPrompt, contract, shellType, screen, composedS
|
|
|
12362
12718
|
const propsFields = [];
|
|
12363
12719
|
const propsProperties = contract?.propsSpec?.properties ?? {};
|
|
12364
12720
|
for (const [key, entry] of Object.entries(propsProperties)) {
|
|
12365
|
-
const required = entry.required
|
|
12721
|
+
const required = entry.required === true;
|
|
12366
12722
|
const nullable = entry.schema.nullable === true;
|
|
12367
12723
|
const tsType = jsonSchemaTypeToTs(entry.schema);
|
|
12368
12724
|
const fullType = nullable ? `${tsType} | null` : tsType;
|
|
12369
12725
|
const parts = [];
|
|
12370
12726
|
if (entry.description) parts.push(entry.description);
|
|
12371
12727
|
if (entry.default !== void 0) parts.push(`(default: ${JSON.stringify(entry.default)})`);
|
|
12728
|
+
if (!required) parts.push("(optional \u2014 the agent may omit it; guard or fall back)");
|
|
12372
12729
|
const desc = parts.length > 0 ? ` // ${parts.join(" ")}` : "";
|
|
12373
12730
|
propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
|
|
12374
12731
|
}
|
|
@@ -12927,6 +13284,7 @@ A \`DataContract\` declares everything a GguiSession exchanges with the outside
|
|
|
12927
13284
|
|
|
12928
13285
|
Props arrive at mount and may be partial on first render; later prop changes reach the SAME mounted component via \`ggui_amend\` (in-place repaint) on the live channel. Stream channels start empty and fill over time. Context slots start at their declared default (often \`null\`). **Never assume a field exists before you read it.**
|
|
12929
13286
|
|
|
13287
|
+
- **Optional contract props**: a \`Props\` field marked \`?\` in the boilerplate may be omitted by the agent ENTIRELY \u2014 the wire gate only enforces \`required: true\` entries, and the harness verifies your component with those props stripped. Guard them (\`?? default\`, \`?.\`, or an empty-state branch) from the FIRST render.
|
|
12930
13288
|
- **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
|
|
12931
13289
|
- **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
|
|
12932
13290
|
- **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
|
|
@@ -14339,6 +14697,106 @@ var STATE_MERGE_CHECKS = [
|
|
|
14339
14697
|
}
|
|
14340
14698
|
];
|
|
14341
14699
|
|
|
14700
|
+
// src/evaluation/axis-checks/checks/state-ui-affordance.ts
|
|
14701
|
+
var STATE_ARIA_OR_ROLE = /aria-(checked|pressed|selected|expanded|current)\s*=|role\s*=/;
|
|
14702
|
+
var STATE_PRIMITIVE_IN_SUBTREE = /<(Checkbox|Toggle|Tabs|Accordion)[\s/>]/;
|
|
14703
|
+
var SEMANTIC_PRIMITIVES = /* @__PURE__ */ new Set(["Button", "Checkbox", "Toggle", "Link", "MenuItem"]);
|
|
14704
|
+
function collectSetters(src) {
|
|
14705
|
+
const setters = /* @__PURE__ */ new Set();
|
|
14706
|
+
for (const m of src.matchAll(/const\s*\[\s*\w+\s*,\s*(set\w+)\s*\]\s*=\s*useState/g)) {
|
|
14707
|
+
setters.add(m[1]);
|
|
14708
|
+
}
|
|
14709
|
+
return setters;
|
|
14710
|
+
}
|
|
14711
|
+
function collectHandlers(src, setters) {
|
|
14712
|
+
const handlers = /* @__PURE__ */ new Set();
|
|
14713
|
+
for (const m of src.matchAll(/const\s+(\w+)\s*=\s*(?:\([^)]*\)|\w+)\s*=>\s*\{?/g)) {
|
|
14714
|
+
const body = src.slice(m.index + m[0].length, m.index + m[0].length + 400);
|
|
14715
|
+
for (const s of setters) {
|
|
14716
|
+
if (body.includes(s)) {
|
|
14717
|
+
handlers.add(m[1]);
|
|
14718
|
+
break;
|
|
14719
|
+
}
|
|
14720
|
+
}
|
|
14721
|
+
}
|
|
14722
|
+
return handlers;
|
|
14723
|
+
}
|
|
14724
|
+
function attrBlock(src, start) {
|
|
14725
|
+
let depth = 0;
|
|
14726
|
+
for (let i = start; i < src.length; i++) {
|
|
14727
|
+
const c = src[i];
|
|
14728
|
+
if (c === "{") depth++;
|
|
14729
|
+
else if (c === "}") depth--;
|
|
14730
|
+
else if (c === ">" && depth === 0) return { block: src.slice(start, i), end: i };
|
|
14731
|
+
}
|
|
14732
|
+
return { block: src.slice(start, start + 900), end: start + 900 };
|
|
14733
|
+
}
|
|
14734
|
+
function onClickValue(block) {
|
|
14735
|
+
const m = block.match(/onClick\s*=\s*\{/);
|
|
14736
|
+
if (!m || m.index === void 0) return null;
|
|
14737
|
+
const open = m.index + m[0].length - 1;
|
|
14738
|
+
let depth = 0;
|
|
14739
|
+
for (let i = open; i < block.length; i++) {
|
|
14740
|
+
if (block[i] === "{") depth++;
|
|
14741
|
+
else if (block[i] === "}") {
|
|
14742
|
+
depth--;
|
|
14743
|
+
if (depth === 0) return block.slice(open + 1, i);
|
|
14744
|
+
}
|
|
14745
|
+
}
|
|
14746
|
+
return block.slice(open + 1);
|
|
14747
|
+
}
|
|
14748
|
+
function subtree(src, pos, tag) {
|
|
14749
|
+
const close = src.indexOf(`</${tag}>`, pos);
|
|
14750
|
+
return src.slice(pos, close === -1 ? pos + 700 : close);
|
|
14751
|
+
}
|
|
14752
|
+
function runStateAriaPresent(input) {
|
|
14753
|
+
if (input.compiledCode === null) return [];
|
|
14754
|
+
const src = input.sourceCode;
|
|
14755
|
+
const setters = collectSetters(src);
|
|
14756
|
+
const handlers = collectHandlers(src, setters);
|
|
14757
|
+
const issues = [];
|
|
14758
|
+
const flaggedLines = /* @__PURE__ */ new Set();
|
|
14759
|
+
const localComponents = /* @__PURE__ */ new Set();
|
|
14760
|
+
for (const m of src.matchAll(/(?:function\s+([A-Z]\w*)\s*\(|const\s+([A-Z]\w*)\s*=)/g)) {
|
|
14761
|
+
localComponents.add(m[1] ?? m[2]);
|
|
14762
|
+
}
|
|
14763
|
+
for (const m of src.matchAll(/<([A-Za-z][A-Za-z0-9]*)\b/g)) {
|
|
14764
|
+
const tag = m[1];
|
|
14765
|
+
if (m.index === void 0) continue;
|
|
14766
|
+
if (localComponents.has(tag)) continue;
|
|
14767
|
+
const { block, end } = attrBlock(src, m.index + m[0].length);
|
|
14768
|
+
const clickValue = onClickValue(block);
|
|
14769
|
+
if (clickValue === null) continue;
|
|
14770
|
+
const mutatesAffordance = [...setters].some((s) => clickValue.includes(s)) || [...handlers].some((h) => new RegExp(`\\b${h}\\b`).test(clickValue)) || // Extracted-subcomponent shape: the toggle handler arrives as an
|
|
14771
|
+
// `on[A-Z]…` callback prop and is invoked from onClick — inside a
|
|
14772
|
+
// ui-affordance component that IS the affordance mutation.
|
|
14773
|
+
/\bon[A-Z]\w*\b/.test(clickValue);
|
|
14774
|
+
if (!mutatesAffordance) continue;
|
|
14775
|
+
if (SEMANTIC_PRIMITIVES.has(tag)) continue;
|
|
14776
|
+
if (STATE_ARIA_OR_ROLE.test(block)) continue;
|
|
14777
|
+
if (STATE_PRIMITIVE_IN_SUBTREE.test(subtree(src, end, tag))) continue;
|
|
14778
|
+
const line = src.slice(0, m.index).split("\n").length;
|
|
14779
|
+
if (flaggedLines.has(line)) continue;
|
|
14780
|
+
flaggedLines.add(line);
|
|
14781
|
+
issues.push(
|
|
14782
|
+
mkIssue(
|
|
14783
|
+
"state.ui_affordance.state_aria_present",
|
|
14784
|
+
`Line ${line}: <${tag}> toggles the UI affordance from its onClick but exposes no state ARIA \u2014 its checked/selected/expanded state is invisible to assistive technology (styling is the only carrier).`,
|
|
14785
|
+
`Add the matching state attribute to the SAME element that carries the onClick: \`role="checkbox"\` + \`aria-checked={\u2026}\` for a toggle row, \`aria-pressed\` for a toggle button, \`aria-selected\` for a selection, \`aria-expanded\` for an open/close affordance \u2014 or replace the hand-rolled control with the state-carrying primitive (Checkbox / Toggle / Tabs / Accordion). \`as={Clickable}\` adds button + keyboard semantics but NOT state.`
|
|
14786
|
+
)
|
|
14787
|
+
);
|
|
14788
|
+
}
|
|
14789
|
+
return issues;
|
|
14790
|
+
}
|
|
14791
|
+
var STATE_UI_AFFORDANCE_CHECKS = [
|
|
14792
|
+
{
|
|
14793
|
+
id: "state.ui_affordance.state_aria_present",
|
|
14794
|
+
axis: "state",
|
|
14795
|
+
values: ["ui-affordance"],
|
|
14796
|
+
run: runStateAriaPresent
|
|
14797
|
+
}
|
|
14798
|
+
];
|
|
14799
|
+
|
|
14342
14800
|
// src/evaluation/axis-checks/checks/realtime.ts
|
|
14343
14801
|
var REALTIME_ACTIVE = ["merge", "append", "status", "presence", "mixed"];
|
|
14344
14802
|
function runStreamHandlerPerEvent(input) {
|
|
@@ -15028,6 +15486,7 @@ var EXTRA_CHECKS = [
|
|
|
15028
15486
|
var REGISTRY = [
|
|
15029
15487
|
...UNIVERSAL_CHECKS,
|
|
15030
15488
|
...STATE_MERGE_CHECKS,
|
|
15489
|
+
...STATE_UI_AFFORDANCE_CHECKS,
|
|
15031
15490
|
...REALTIME_CHECKS,
|
|
15032
15491
|
...WRITES_CHECKS,
|
|
15033
15492
|
...STATE_PAYLOAD_CHECKS,
|
|
@@ -15198,8 +15657,8 @@ ${cjsResult.code}
|
|
|
15198
15657
|
// src/harness/check/runtime-render/load-component.ts
|
|
15199
15658
|
async function loadComponent(input) {
|
|
15200
15659
|
const { sourceCode, moduleResolutions = {} } = input;
|
|
15201
|
-
const { createRequire:
|
|
15202
|
-
const require_ =
|
|
15660
|
+
const { createRequire: createRequire5 } = await import('module');
|
|
15661
|
+
const require_ = createRequire5(import.meta.url);
|
|
15203
15662
|
const sandboxRequire = (id) => {
|
|
15204
15663
|
if (id in moduleResolutions) return moduleResolutions[id];
|
|
15205
15664
|
if (id === "react/jsx-runtime" || id === "react/jsx-dev-runtime") {
|
|
@@ -15455,6 +15914,47 @@ function wrapDetection(kind, jsx, native, custom) {
|
|
|
15455
15914
|
function dedupe(arr) {
|
|
15456
15915
|
return Array.from(new Set(arr));
|
|
15457
15916
|
}
|
|
15917
|
+
var installed = false;
|
|
15918
|
+
function installProductionActShim() {
|
|
15919
|
+
if (installed) return;
|
|
15920
|
+
installed = true;
|
|
15921
|
+
const require_ = createRequire(import.meta.url);
|
|
15922
|
+
let reactCjs;
|
|
15923
|
+
try {
|
|
15924
|
+
reactCjs = require_("react");
|
|
15925
|
+
} catch {
|
|
15926
|
+
return;
|
|
15927
|
+
}
|
|
15928
|
+
if (typeof reactCjs.act === "function") {
|
|
15929
|
+
return;
|
|
15930
|
+
}
|
|
15931
|
+
try {
|
|
15932
|
+
const testUtils = require_("react-dom/test-utils");
|
|
15933
|
+
const reactDom = require_("react-dom");
|
|
15934
|
+
testUtils.act = createFlushSyncAct(reactDom.flushSync);
|
|
15935
|
+
} catch {
|
|
15936
|
+
}
|
|
15937
|
+
}
|
|
15938
|
+
function createFlushSyncAct(flushSync) {
|
|
15939
|
+
return (callback) => {
|
|
15940
|
+
let callbackResult;
|
|
15941
|
+
flushSync(() => {
|
|
15942
|
+
callbackResult = callback();
|
|
15943
|
+
});
|
|
15944
|
+
if (isThenable(callbackResult)) {
|
|
15945
|
+
const pending = callbackResult;
|
|
15946
|
+
return new Promise((resolve3, reject) => {
|
|
15947
|
+
pending.then(() => resolve3(), reject);
|
|
15948
|
+
});
|
|
15949
|
+
}
|
|
15950
|
+
return new Promise((resolve3) => {
|
|
15951
|
+
queueMicrotask(resolve3);
|
|
15952
|
+
});
|
|
15953
|
+
};
|
|
15954
|
+
}
|
|
15955
|
+
function isThenable(value) {
|
|
15956
|
+
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
15957
|
+
}
|
|
15458
15958
|
|
|
15459
15959
|
// src/internal/open-record.ts
|
|
15460
15960
|
function openRecord(o) {
|
|
@@ -15517,6 +16017,14 @@ function parentLink(node) {
|
|
|
15517
16017
|
|
|
15518
16018
|
// src/harness/check/runtime-render/render-check.ts
|
|
15519
16019
|
async function runRenderCheck(input) {
|
|
16020
|
+
const g = hostGlobals();
|
|
16021
|
+
if ("window" in g && "document" in g) {
|
|
16022
|
+
return runRenderCheckInProcess(input);
|
|
16023
|
+
}
|
|
16024
|
+
const { runRenderCheckViaWorker: runRenderCheckViaWorker2 } = await Promise.resolve().then(() => (init_render_check_host(), render_check_host_exports));
|
|
16025
|
+
return runRenderCheckViaWorker2(input);
|
|
16026
|
+
}
|
|
16027
|
+
async function runRenderCheckInProcess(input) {
|
|
15520
16028
|
const t0 = Date.now();
|
|
15521
16029
|
const issues = [];
|
|
15522
16030
|
const teardown = await setupHappyDom();
|
|
@@ -15572,6 +16080,7 @@ async function runRenderCheck(input) {
|
|
|
15572
16080
|
const probe = createProbe();
|
|
15573
16081
|
const wireConfig = createProbeWireConfig(probe);
|
|
15574
16082
|
uninstallSpy = probe.installPostMessageSpy();
|
|
16083
|
+
installProductionActShim();
|
|
15575
16084
|
const { render, cleanup } = await import('@testing-library/react');
|
|
15576
16085
|
const userEventModule = await import('@testing-library/user-event');
|
|
15577
16086
|
const userEvent = userEventModule.default ?? userEventModule;
|
|
@@ -15727,6 +16236,50 @@ async function runRenderCheck(input) {
|
|
|
15727
16236
|
if (issue) issues.push(issue);
|
|
15728
16237
|
}
|
|
15729
16238
|
}
|
|
16239
|
+
const declaredProps = input.contract?.propsSpec?.properties;
|
|
16240
|
+
if (declaredProps) {
|
|
16241
|
+
const optionalFilled = Object.entries(declaredProps).filter(
|
|
16242
|
+
([name, entry]) => entry.required !== true && input.mockupProps[name] !== void 0
|
|
16243
|
+
).map(([name]) => name);
|
|
16244
|
+
if (optionalFilled.length > 0) {
|
|
16245
|
+
const requiredOnlyProps = { ...input.mockupProps };
|
|
16246
|
+
for (const name of optionalFilled) delete requiredOnlyProps[name];
|
|
16247
|
+
const omitBoundaryRef = {};
|
|
16248
|
+
class OmitErrorBoundary extends React.Component {
|
|
16249
|
+
state = { caught: false };
|
|
16250
|
+
static getDerivedStateFromError() {
|
|
16251
|
+
return { caught: true };
|
|
16252
|
+
}
|
|
16253
|
+
componentDidCatch(error) {
|
|
16254
|
+
omitBoundaryRef.error ??= error;
|
|
16255
|
+
}
|
|
16256
|
+
render() {
|
|
16257
|
+
if (this.state.caught) return null;
|
|
16258
|
+
return this.props.children;
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
try {
|
|
16262
|
+
render(
|
|
16263
|
+
React.createElement(GguiWireProvider, {
|
|
16264
|
+
config: wireConfig,
|
|
16265
|
+
children: React.createElement(OmitErrorBoundary, {
|
|
16266
|
+
children: React.createElement(Component, requiredOnlyProps)
|
|
16267
|
+
})
|
|
16268
|
+
})
|
|
16269
|
+
);
|
|
16270
|
+
} catch (e) {
|
|
16271
|
+
omitBoundaryRef.error ??= e instanceof Error ? e : new Error(String(e));
|
|
16272
|
+
}
|
|
16273
|
+
if (omitBoundaryRef.error) {
|
|
16274
|
+
issues.push({
|
|
16275
|
+
check: "optional-props-omitted",
|
|
16276
|
+
outcome: "failed",
|
|
16277
|
+
subject: optionalFilled.join(", "),
|
|
16278
|
+
reason: `Render crashed when the optional prop(s) ${optionalFilled.map((n) => `'${n}'`).join(", ")} were omitted: ${omitBoundaryRef.error.message}. The contract marks them optional (no \`required: true\`), so a legitimate render may leave them out.`
|
|
16279
|
+
});
|
|
16280
|
+
}
|
|
16281
|
+
}
|
|
16282
|
+
}
|
|
15730
16283
|
} finally {
|
|
15731
16284
|
cleanup();
|
|
15732
16285
|
}
|
|
@@ -16298,6 +16851,15 @@ function synthesizeFromSchema2(schema, hint, warnings, depth) {
|
|
|
16298
16851
|
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
16299
16852
|
return synthesizeFromSchema2(schema.anyOf[0], hint, warnings, depth + 1);
|
|
16300
16853
|
}
|
|
16854
|
+
if (Array.isArray(schema.type)) {
|
|
16855
|
+
const primary = schema.type.find((t) => t !== "null") ?? "null";
|
|
16856
|
+
return synthesizeFromSchema2(
|
|
16857
|
+
{ ...schema, type: primary },
|
|
16858
|
+
hint,
|
|
16859
|
+
warnings,
|
|
16860
|
+
depth + 1
|
|
16861
|
+
);
|
|
16862
|
+
}
|
|
16301
16863
|
switch (schema.type) {
|
|
16302
16864
|
case "string": {
|
|
16303
16865
|
switch (schema.format) {
|
|
@@ -16468,6 +17030,16 @@ function toEvalIssue(issue) {
|
|
|
16468
17030
|
description: `Component crashed at runtime: ${issue.reason}`,
|
|
16469
17031
|
fix: classifyRenderCrashFix(issue.reason)
|
|
16470
17032
|
};
|
|
17033
|
+
case "optional-props-omitted":
|
|
17034
|
+
return {
|
|
17035
|
+
tier: 0,
|
|
17036
|
+
result,
|
|
17037
|
+
category: "crash",
|
|
17038
|
+
subcategory,
|
|
17039
|
+
severity: "critical",
|
|
17040
|
+
description: `Component crashed when optional props were omitted: ${issue.reason}`,
|
|
17041
|
+
fix: `Guard every optional prop before member access (props.${subject.split(",")[0]?.trim() ?? "x"}?.map(...), a default via destructuring, or an early empty-state return). Optional props (no \`required: true\` in the contract) may be absent at runtime even though the harness fills them.`
|
|
17042
|
+
};
|
|
16471
17043
|
case "action-wiring": {
|
|
16472
17044
|
const fix = issue.outcome === "unverified" ? `Source shows the action callback flowing into a non-native or custom-component prop. If wiring is intentional (e.g., Dropdown.onChange, drag-drop), this warn is informational \u2014 manual/browser verification is required to confirm. Otherwise wire to a native onClick={() => ${subject}(payload)} on <button> or design-system <Button>.` : `Wire ${subject}() to a native event prop. Common fix: <Button onClick={() => ${subject}(payload)}>Label</Button>. Source-AST analysis didn't find this wiring in your JSX.`;
|
|
16473
17045
|
return {
|
|
@@ -16514,6 +17086,7 @@ async function warmupRuntimeRenderProbe() {
|
|
|
16514
17086
|
missing += 1;
|
|
16515
17087
|
}
|
|
16516
17088
|
};
|
|
17089
|
+
installProductionActShim();
|
|
16517
17090
|
await Promise.all([
|
|
16518
17091
|
tryLoad("happy-dom"),
|
|
16519
17092
|
tryLoad("@testing-library/react"),
|
|
@@ -17590,6 +18163,14 @@ function mapProviderForEvaluator(provider) {
|
|
|
17590
18163
|
|
|
17591
18164
|
// src/harness/coding/run-eval-round.ts
|
|
17592
18165
|
var MAX_FEEDBACK_ISSUES = 3;
|
|
18166
|
+
function selectActionableFeedback(issues) {
|
|
18167
|
+
const pool = issues.filter((i) => i.result === "fail" || i.result === "warn");
|
|
18168
|
+
const reserved = pool.find((i) => i.tier === 0 && i.result === "fail");
|
|
18169
|
+
return [
|
|
18170
|
+
...reserved ? [reserved] : [],
|
|
18171
|
+
...pool.filter((i) => i !== reserved).sort((a, b) => (a.result === "fail" ? 0 : 1) - (b.result === "fail" ? 0 : 1))
|
|
18172
|
+
].slice(0, MAX_FEEDBACK_ISSUES);
|
|
18173
|
+
}
|
|
17593
18174
|
function fingerprintFail(issue) {
|
|
17594
18175
|
const cat = issue.category ?? "";
|
|
17595
18176
|
const sub = issue.subcategory ?? "";
|
|
@@ -17764,7 +18345,14 @@ async function runEvalRound(ctx, input) {
|
|
|
17764
18345
|
// back to `process.env` either (the eval calls run
|
|
17765
18346
|
// inside the same concurrent generation, so the race is
|
|
17766
18347
|
// real here too, not just on the coding turn).
|
|
17767
|
-
routeOverride: evaluationAgent.routeOverride
|
|
18348
|
+
routeOverride: evaluationAgent.routeOverride,
|
|
18349
|
+
// #489 — thread the same provider-429-retry observer.
|
|
18350
|
+
// Without this, a rate-limited retry inside the
|
|
18351
|
+
// evaluation agent's apiCall() happens (the base-class
|
|
18352
|
+
// retry loop is unconditional) but never reaches the
|
|
18353
|
+
// caller, silently missing the host application's
|
|
18354
|
+
// structured log.
|
|
18355
|
+
onRetry: evaluationAgent.onRetry
|
|
17768
18356
|
},
|
|
17769
18357
|
preWarmedContext
|
|
17770
18358
|
) : null,
|
|
@@ -17775,7 +18363,15 @@ async function runEvalRound(ctx, input) {
|
|
|
17775
18363
|
model: visualEvalAgent.model,
|
|
17776
18364
|
passThreshold: visualThreshold,
|
|
17777
18365
|
sampleProps: visualEvaluation?.sampleProps,
|
|
17778
|
-
viewport: visualEvaluation?.viewport
|
|
18366
|
+
viewport: visualEvaluation?.viewport,
|
|
18367
|
+
// #484/#489 — same threading as the LLM-eval leg above:
|
|
18368
|
+
// the visual eval's multimodal call runs inside the same
|
|
18369
|
+
// concurrent generation, so it needs the same routing
|
|
18370
|
+
// override (never fall back to `process.env`) and the
|
|
18371
|
+
// same provider-429-retry observer (retries reach the
|
|
18372
|
+
// host application's structured log).
|
|
18373
|
+
routeOverride: visualEvalAgent.routeOverride,
|
|
18374
|
+
onRetry: visualEvalAgent.onRetry
|
|
17779
18375
|
}
|
|
17780
18376
|
) : null
|
|
17781
18377
|
]);
|
|
@@ -17994,7 +18590,7 @@ async function runEvalRound(ctx, input) {
|
|
|
17994
18590
|
lastDiffFailed: false
|
|
17995
18591
|
};
|
|
17996
18592
|
}
|
|
17997
|
-
const allActionable = evalResult.issues
|
|
18593
|
+
const allActionable = selectActionableFeedback(evalResult.issues);
|
|
17998
18594
|
const issueLines = allActionable.map((issue) => {
|
|
17999
18595
|
const tag = issue.result === "fail" ? "FAIL" : "WARN";
|
|
18000
18596
|
return `[${tag}] ${issue.category}${issue.subcategory ? "/" + issue.subcategory : ""}: ${issue.description}
|
|
@@ -18009,7 +18605,16 @@ async function runEvalRound(ctx, input) {
|
|
|
18009
18605
|
evalResult,
|
|
18010
18606
|
evalRoundsUsed,
|
|
18011
18607
|
prevModeSubcats: updatedPrevModeSubcats,
|
|
18012
|
-
|
|
18608
|
+
// DELIVERED fails only (Exp 54 P1 addendum, proposal 2): the
|
|
18609
|
+
// stuck detector's premise is "the LLM saw these and did not
|
|
18610
|
+
// resolve them" — recording fails that the MAX_FEEDBACK_ISSUES
|
|
18611
|
+
// cap cut from the feedback turns truncation into a premature
|
|
18612
|
+
// stuck-exit (the model never saw the issue it is blamed for
|
|
18613
|
+
// not fixing). An undelivered fail recurring next round now
|
|
18614
|
+
// reads as NEW, gets delivered, and earns its fix round.
|
|
18615
|
+
prevFailFingerprints: new Set(
|
|
18616
|
+
allActionable.filter((i) => i.result === "fail").map(fingerprintFail)
|
|
18617
|
+
),
|
|
18013
18618
|
preWarmedContext,
|
|
18014
18619
|
evalTokens,
|
|
18015
18620
|
evalLlmMs,
|
|
@@ -18504,12 +19109,14 @@ async function dispatchGeneration(params) {
|
|
|
18504
19109
|
const codingAgent = {
|
|
18505
19110
|
provider: routerProvider,
|
|
18506
19111
|
model: resolvedModel,
|
|
18507
|
-
routeOverride: params.routeOverride
|
|
19112
|
+
routeOverride: params.routeOverride,
|
|
19113
|
+
onRetry: params.onRetry
|
|
18508
19114
|
};
|
|
18509
19115
|
const evaluationAgent = params.models?.evaluation ? {
|
|
18510
19116
|
provider: routerProvider,
|
|
18511
19117
|
model: params.models.evaluation,
|
|
18512
|
-
routeOverride: params.routeOverride
|
|
19118
|
+
routeOverride: params.routeOverride,
|
|
19119
|
+
onRetry: params.onRetry
|
|
18513
19120
|
} : void 0;
|
|
18514
19121
|
const effectiveContract = params.contract;
|
|
18515
19122
|
const sessionParams = {
|
|
@@ -18870,11 +19477,15 @@ ${JSON.stringify(clientCapabilities, null, 2)}
|
|
|
18870
19477
|
const uiRequirements = [];
|
|
18871
19478
|
if (contract.propsSpec?.properties) {
|
|
18872
19479
|
for (const [name, entry] of Object.entries(contract.propsSpec.properties)) {
|
|
18873
|
-
const required = entry.required;
|
|
18874
|
-
if (required
|
|
19480
|
+
const required = entry.required === true;
|
|
19481
|
+
if (required) {
|
|
18875
19482
|
uiRequirements.push(
|
|
18876
19483
|
`- **Required prop \`${name}\`** must appear somewhere in rendered DOM (display the value, or use it to drive a label/aria/key).`
|
|
18877
19484
|
);
|
|
19485
|
+
} else {
|
|
19486
|
+
uiRequirements.push(
|
|
19487
|
+
`- **Optional prop \`${name}\`** may be ABSENT at runtime \u2014 the component must render without it (guard the access or apply its default) and must display it when it IS provided.`
|
|
19488
|
+
);
|
|
18878
19489
|
}
|
|
18879
19490
|
}
|
|
18880
19491
|
}
|
|
@@ -18958,7 +19569,8 @@ function createUiGenerator(options = {}) {
|
|
|
18958
19569
|
maxEvalRounds,
|
|
18959
19570
|
qualityConfig,
|
|
18960
19571
|
gadgetCatalog,
|
|
18961
|
-
disableEnvMutation = false
|
|
19572
|
+
disableEnvMutation = false,
|
|
19573
|
+
onRetry
|
|
18962
19574
|
} = options;
|
|
18963
19575
|
const identity = resolveIdentity(options);
|
|
18964
19576
|
return {
|
|
@@ -19036,7 +19648,8 @@ function createUiGenerator(options = {}) {
|
|
|
19036
19648
|
// `dispatchGeneration`.
|
|
19037
19649
|
...input.gadgetTypes !== void 0 ? { gadgetTypes: input.gadgetTypes } : {},
|
|
19038
19650
|
enableRuntimeRender,
|
|
19039
|
-
...routeOverride !== void 0 ? { routeOverride } : {}
|
|
19651
|
+
...routeOverride !== void 0 ? { routeOverride } : {},
|
|
19652
|
+
...onRetry !== void 0 ? { onRetry } : {}
|
|
19040
19653
|
});
|
|
19041
19654
|
const metadata = {
|
|
19042
19655
|
provider: input.llm.provider,
|