@ggui-ai/ui-gen 0.20.0 → 0.22.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/base.d.ts +7 -0
- package/dist/adapters/base.js +1 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +17 -3
- package/dist/adapters/generation-dispatch.js +1078 -3464
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +60 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +4 -4
- package/dist/advanced/index.js +1346 -3557
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +10 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +2 -2
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-CxPvudbs.d.ts → create-ui-generator-BEkzEtNz.d.ts} +2 -2
- package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DJNpQ7DI.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/index.d.ts +38 -6
- package/dist/evaluation/index.js +670 -3374
- 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 +10 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +5 -3
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +2 -2
- package/dist/harness/index.js +587 -30
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +7 -1
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.js +1 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +15 -2
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +11 -2
- package/dist/harness/runtime.js +3 -3
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +2 -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 +23 -7
- package/dist/index.js +1346 -3557
- package/dist/index.js.map +1 -1
- package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
- package/dist/patch.d.ts +1 -1
- package/dist/providers/index.js +5 -4
- package/dist/providers/index.js.map +1 -1
- package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
- package/dist/{types-public-DKR_h70H.d.ts → types-public-mM_-1Yk3.d.ts} +59 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +16 -12
- package/src/adapters/base.ts +8 -0
- package/src/adapters/claude/claude-code-login.ts +128 -0
- package/src/adapters/claude/sdk.ts +9 -1
- package/src/adapters/generation-dispatch.ts +19 -2
- package/src/adapters/openai/tokens.ts +18 -0
- package/src/adapters/provider-router.ts +28 -0
- package/src/boilerplate/generate.ts +19 -4
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +7 -3
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +7 -3
- package/src/contract-context.ts +1 -1
- package/src/create-ui-generator.ts +49 -10
- package/src/evaluation/design-identity.ts +16 -5
- package/src/evaluation/index.ts +1 -0
- package/src/evaluation/llm-evaluator.ts +40 -13
- package/src/evaluation/types-public.ts +58 -0
- package/src/evaluation/visual-evaluator.ts +240 -30
- package/src/generator-build.ts +158 -0
- package/src/harness/check/runtime-render/adapter.ts +9 -0
- package/src/harness/check/runtime-render/render-check.ts +26 -5
- package/src/harness/claude-code-login-query.ts +529 -0
- package/src/harness/coding/assemble-result.ts +18 -2
- package/src/harness/coding/generate-task-runner.ts +75 -36
- package/src/harness/coding/run-coding-turn.ts +9 -0
- package/src/harness/coding/run-eval-round.ts +338 -38
- package/src/harness/enforced-coding.ts +20 -0
- package/src/harness/llm-router.ts +390 -55
- package/src/harness/llm-trace-sink.ts +7 -1
- package/src/harness/result-types.ts +14 -0
- package/src/harness/runtime.ts +6 -0
- package/src/index.ts +2 -1
- package/src/providers/openai.ts +31 -19
- package/src/validation/primitives.ts +1 -1
|
@@ -22,17 +22,25 @@ import { listContractGadgets } from "@ggui-ai/protocol";
|
|
|
22
22
|
import type { Classification } from "../../classifier/index.js";
|
|
23
23
|
import type { AgentWorkspace } from "../../coding-agent/workspace.js";
|
|
24
24
|
import type { CostTracker } from "../../evaluation/cost-tracker.js";
|
|
25
|
-
import type { EvalIssue, EvalResult, RuntimeProbeMeta, VisualEvalSummary } from "../../evaluation/types-public.js";
|
|
25
|
+
import type { ContractFeedbackRecord, EvalIssue, EvalResult, RuntimeProbeMeta, VisualEvalSummary, VisualCoverage } from "../../evaluation/types-public.js";
|
|
26
26
|
import { notApplicableCoverage } from "../../evaluation/types-public.js";
|
|
27
|
-
import { mapProviderForEvaluator } from "../enforced-coding.js";
|
|
27
|
+
import { mapProviderForEvaluator, visionJudgeProvider } from "../enforced-coding.js";
|
|
28
28
|
import { runCheck } from "../index.js";
|
|
29
29
|
import { isRecoverableRenderCrash } from "../check/runtime-render/index.js";
|
|
30
30
|
import type { Harness } from "../types-public.js";
|
|
31
31
|
import type { AgentSpec, SingleComponentParams } from "../runtime.js";
|
|
32
|
+
import type {
|
|
33
|
+
CanvasFitReading,
|
|
34
|
+
VisualEvalContext,
|
|
35
|
+
VisualEvalOutcome,
|
|
36
|
+
VisualFitConfig,
|
|
37
|
+
VisualFitOutcome,
|
|
38
|
+
} from "../../evaluation/visual-evaluator.js";
|
|
32
39
|
|
|
33
40
|
type PreWarmedEvalContext =
|
|
34
41
|
import("../../evaluation/llm-evaluator.js").PreWarmedEvalContext;
|
|
35
42
|
type LlmEvalMod = typeof import("../../evaluation/llm-evaluator.js");
|
|
43
|
+
type LLMEvalTokenUsage = import("../../evaluation/llm-evaluator.js").LLMEvalTokenUsage;
|
|
36
44
|
type VisualEvalMod = typeof import("../../evaluation/visual-evaluator.js");
|
|
37
45
|
|
|
38
46
|
/**
|
|
@@ -93,6 +101,20 @@ export interface EvalRoundInput {
|
|
|
93
101
|
*/
|
|
94
102
|
export type EvalRoundControl = "break" | "feedback";
|
|
95
103
|
|
|
104
|
+
/**
|
|
105
|
+
* One call's (or one round's) token accounting. `input` is the NON-cached
|
|
106
|
+
* input; the cache counts are present only when the provider reported them —
|
|
107
|
+
* absent means unreported, never zero. The runner folds every coding turn and
|
|
108
|
+
* every eval round through the same rule (`absorbTokens`), so the published
|
|
109
|
+
* `tokens.total` holds one convention across both (ggui#1281, #1186).
|
|
110
|
+
*/
|
|
111
|
+
export interface TokenUsage {
|
|
112
|
+
readonly input: number;
|
|
113
|
+
readonly output: number;
|
|
114
|
+
readonly cacheRead?: number;
|
|
115
|
+
readonly cacheCreation?: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
export interface EvalRoundResult {
|
|
97
119
|
readonly control: EvalRoundControl;
|
|
98
120
|
/** True only on the low-risk bypass + clean-pass paths. */
|
|
@@ -108,7 +130,7 @@ export interface EvalRoundResult {
|
|
|
108
130
|
readonly prevFailFingerprints: Set<string>;
|
|
109
131
|
readonly preWarmedContext: PreWarmedEvalContext | null | undefined;
|
|
110
132
|
/** Tokens added during this round — caller adds to its own totals. */
|
|
111
|
-
readonly evalTokens:
|
|
133
|
+
readonly evalTokens: TokenUsage;
|
|
112
134
|
/** Wall-clock of the parallel LLM + visual eval block (zero on the
|
|
113
135
|
* low-risk bypass path, which exits before `Promise.all`). Caller
|
|
114
136
|
* accumulates into its `cumulativeEvalLlmMs`. */
|
|
@@ -117,11 +139,35 @@ export interface EvalRoundResult {
|
|
|
117
139
|
readonly lastResultText: string;
|
|
118
140
|
readonly isEvalFeedback: boolean;
|
|
119
141
|
readonly lastDiffFailed: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* ggui#1261 — set ONLY on the round that granted a contract-feedback round:
|
|
144
|
+
* what bought it and the source it was fed back on. The caller keeps it past
|
|
145
|
+
* this round (the next round rebuilds `evalResult`) and stamps it on the
|
|
146
|
+
* result the generation ends with.
|
|
147
|
+
*/
|
|
148
|
+
readonly contractFeedback?: ContractFeedbackRecord;
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
/** Cap feedback to N issues per eval-fix turn so the LLM stays surgical. */
|
|
123
152
|
const MAX_FEEDBACK_ISSUES = 3;
|
|
124
153
|
|
|
154
|
+
/**
|
|
155
|
+
* ggui#1195 — the visual leg's deterministic fit verdict: the inline card's
|
|
156
|
+
* content is taller than the box it was judged at (`canvas-overflow`,
|
|
157
|
+
* critical ⇒ fail). Recognised by the marker the evaluator emits, never by
|
|
158
|
+
* the description's wording.
|
|
159
|
+
*/
|
|
160
|
+
function isFitFail(issue: EvalIssue): boolean {
|
|
161
|
+
return issue.result === "fail" && issue.category === "visual" && issue.subcategory === "canvas-overflow";
|
|
162
|
+
}
|
|
163
|
+
/** The same marker as {@link isFitFail}, on the fingerprint `fingerprintFail` builds (`category|subcategory|…`). */
|
|
164
|
+
const FIT_FINGERPRINT_PREFIX = "visual|canvas-overflow|";
|
|
165
|
+
|
|
166
|
+
/** `[fit]`-tagged so the LLM can see the violation is the box, not the judge's taste. */
|
|
167
|
+
function formatFitFeedback(issue: EvalIssue): string {
|
|
168
|
+
return `[fit] ${issue.category}/${issue.subcategory ?? ""}: ${issue.description}\n Fix: ${issue.fix ?? ""}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
125
171
|
/**
|
|
126
172
|
* `criteriaCoverage` reason stamped on the same-image low-risk bypass
|
|
127
173
|
* exits: tier-1/2 evaluation is never invoked there BY DESIGN, so every
|
|
@@ -131,6 +177,65 @@ const MAX_FEEDBACK_ISSUES = 3;
|
|
|
131
177
|
/** Why an eval result carries no criteria: the round itself threw (ggui#1110). */
|
|
132
178
|
const EVAL_ROUND_THREW_REASON = "eval round threw";
|
|
133
179
|
|
|
180
|
+
/**
|
|
181
|
+
* The in-loop visual leg, made total (ggui#1248). The judge runs on a VISION
|
|
182
|
+
* provider: the agent's provider narrowed by `visionJudgeProvider`, never
|
|
183
|
+
* asserted into the vision union — a lane whose generation provider has no
|
|
184
|
+
* vision path (OpenAI, OpenRouter) gets a leg recorded as SKIPPED with the
|
|
185
|
+
* reason, and the judge is not called. And whatever the leg does, it resolves:
|
|
186
|
+
* a rejection becomes the same skipped coverage with the error, so the round
|
|
187
|
+
* never takes its thrown path on account of the visual leg and the runtime
|
|
188
|
+
* probe still runs — a bar that requires the probe decides on the probe, not
|
|
189
|
+
* on a judge that could not be reached.
|
|
190
|
+
*
|
|
191
|
+
* The leg's FIT half does not need the judge (ggui#1195): the fit verdict is a
|
|
192
|
+
* frame at the declared box and a scroll height, so a no-vision lane with
|
|
193
|
+
* canvases runs `measureFit` alone. Its canvas-overflow fail rides the round's
|
|
194
|
+
* issues like the judge path's would — so the `[fit]` round the cap grants
|
|
195
|
+
* can fire on that lane — and the coverage still reads `skipped` for the judge,
|
|
196
|
+
* with what the fit half measured (or why it could not) appended. Without
|
|
197
|
+
* that half, a card taller than its declared chat box on such a lane was
|
|
198
|
+
* never told so in the loop, so the composition fix was never asked for.
|
|
199
|
+
*/
|
|
200
|
+
function runVisualLeg(
|
|
201
|
+
visualEvalAgent: AgentSpec,
|
|
202
|
+
run: (provider: "claude" | "google") => Promise<VisualEvalOutcome>,
|
|
203
|
+
measureFit: (() => Promise<VisualFitOutcome>) | undefined,
|
|
204
|
+
): Promise<VisualEvalOutcome> {
|
|
205
|
+
const provider = visionJudgeProvider(visualEvalAgent.provider);
|
|
206
|
+
if (provider === undefined) {
|
|
207
|
+
const noJudge = `no vision judge for provider '${visualEvalAgent.provider}' (model ${visualEvalAgent.model}) — the in-loop visual judge runs on a vision provider (anthropic, google)`;
|
|
208
|
+
if (measureFit === undefined) {
|
|
209
|
+
return Promise.resolve({ issues: [], coverage: { status: "skipped", reason: noJudge } });
|
|
210
|
+
}
|
|
211
|
+
const notMeasured = (why: string): VisualEvalOutcome => ({
|
|
212
|
+
issues: [],
|
|
213
|
+
coverage: { status: "skipped", reason: `${noJudge}; fit not measured: ${why}` },
|
|
214
|
+
});
|
|
215
|
+
return measureFit().then(
|
|
216
|
+
(fit): VisualEvalOutcome =>
|
|
217
|
+
fit.status === "measured"
|
|
218
|
+
? {
|
|
219
|
+
issues: fit.issues,
|
|
220
|
+
coverage: { status: "skipped", reason: `${noJudge}; fit measured without the judge: ${describeFitReadings(fit.readings)}` },
|
|
221
|
+
}
|
|
222
|
+
: notMeasured(fit.reason),
|
|
223
|
+
(e: unknown): VisualEvalOutcome => notMeasured(e instanceof Error ? e.message : String(e)),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return run(provider).catch((e: unknown): VisualEvalOutcome => ({
|
|
227
|
+
issues: [],
|
|
228
|
+
coverage: { status: "skipped", reason: `visual leg failed: ${e instanceof Error ? e.message : String(e)}` },
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The fit half's receipt on the coverage: `xs-chat-card 384×516 content 535px (overflow)`, one per canvas. */
|
|
233
|
+
function describeFitReadings(readings: readonly CanvasFitReading[]): string {
|
|
234
|
+
return readings
|
|
235
|
+
.map((r) => `${r.canvas} ${r.viewport.width}×${r.viewport.height} content ${r.contentHeight ?? "?"}px${r.overflow ? " (overflow)" : ""}`)
|
|
236
|
+
.join(", ");
|
|
237
|
+
}
|
|
238
|
+
|
|
134
239
|
const LOW_RISK_BYPASS_REASON =
|
|
135
240
|
"same-image low-risk bypass: axis checks clean, tier-1/2 evaluation not invoked";
|
|
136
241
|
|
|
@@ -285,6 +390,42 @@ function formatRuntimeProbeFeedback(issue: EvalIssue): string {
|
|
|
285
390
|
);
|
|
286
391
|
}
|
|
287
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Exit-probe checks whose FAIL buys ONE feedback round although it is not a
|
|
395
|
+
* crash: a contract defect the probe diagnoses deterministically, with a fix
|
|
396
|
+
* text the model can act on in a turn. Without the round the loop recorded the
|
|
397
|
+
* finding and shipped it — its own evaluator named the defect and the model
|
|
398
|
+
* never heard. Named, and grown only with evidence: `prop-sensitivity` (a
|
|
399
|
+
* declared prop ignored, a literal baked in its place — "You" for
|
|
400
|
+
* `currentUser`) is the class measured failing across models (ggui#1261).
|
|
401
|
+
*/
|
|
402
|
+
const FEEDBACK_PROBE_CHECKS: readonly string[] = ["prop-sensitivity"];
|
|
403
|
+
|
|
404
|
+
/** Rounds past the cap the contract-feedback round may use: one, as the fit round (ggui#1195). */
|
|
405
|
+
const CONTRACT_FEEDBACK_BONUS = 1;
|
|
406
|
+
|
|
407
|
+
/** `runtime:<check>` or `runtime:<check>:<subject>` for a named feedback check, FAIL only. */
|
|
408
|
+
function isFeedbackProbeFail(issue: EvalIssue): boolean {
|
|
409
|
+
const sub = issue.subcategory;
|
|
410
|
+
if (issue.result !== "fail" || typeof sub !== "string") return false;
|
|
411
|
+
return FEEDBACK_PROBE_CHECKS.some((check) => sub === `runtime:${check}` || sub.startsWith(`runtime:${check}:`));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The exit probe's feedback-worthy FAILs this generation has not been told
|
|
416
|
+
* about yet. A finding whose fingerprint already rode a feedback round and
|
|
417
|
+
* recurred is left out — one attempt per finding, then it ships as recorded.
|
|
418
|
+
*/
|
|
419
|
+
/** The round's record: the findings that bought it, and the source they were found on. */
|
|
420
|
+
function contractFeedbackRecord(fails: readonly EvalIssue[], sourceBefore: string): ContractFeedbackRecord {
|
|
421
|
+
return { firedOn: fails.flatMap((i) => (i.subcategory !== undefined ? [i.subcategory] : [])), sourceBefore };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function contractFeedbackFails(exitProbe: ProbeAtExitResult, alreadyFedBack: ReadonlySet<string>): EvalIssue[] {
|
|
425
|
+
if (!exitProbe.fired) return [];
|
|
426
|
+
return exitProbe.probeIssues.filter((i) => isFeedbackProbeFail(i) && !alreadyFedBack.has(fingerprintFail(i)));
|
|
427
|
+
}
|
|
428
|
+
|
|
288
429
|
/**
|
|
289
430
|
* Run one eval round. Mutates `ctx.costTracker` (records LLM eval tokens)
|
|
290
431
|
* and `ctx.workspace` is read-only from this function's POV. Returns
|
|
@@ -321,7 +462,7 @@ export async function runEvalRound(
|
|
|
321
462
|
const currentSource = workspace.read() ?? "";
|
|
322
463
|
|
|
323
464
|
let evalResult: EvalResult | undefined;
|
|
324
|
-
let evalTokens = { input: 0, output: 0 };
|
|
465
|
+
let evalTokens: TokenUsage = { input: 0, output: 0 };
|
|
325
466
|
let evalLlmMs = 0;
|
|
326
467
|
|
|
327
468
|
try {
|
|
@@ -445,9 +586,33 @@ export async function runEvalRound(
|
|
|
445
586
|
}
|
|
446
587
|
|
|
447
588
|
// ── Tier 1+2: LLM evaluation + Visual eval in parallel ──
|
|
448
|
-
let llmResult: (EvalResult &
|
|
589
|
+
let llmResult: (EvalResult & LLMEvalTokenUsage) | null = null;
|
|
449
590
|
let visualIssues: EvalIssue[] | null = null;
|
|
450
591
|
|
|
592
|
+
// The visual leg's page and frame — ONE value each, read by the judge and
|
|
593
|
+
// by its fit half alike, so both frame the same component the same way.
|
|
594
|
+
const visualContext: VisualEvalContext = {
|
|
595
|
+
compiledCode,
|
|
596
|
+
originalPrompt: userPrompt,
|
|
597
|
+
profile: harness.profile,
|
|
598
|
+
// The caller's theme, when named — the round paints what the app paints.
|
|
599
|
+
...(visualEvaluation?.cssTokens !== undefined ? { cssTokens: visualEvaluation.cssTokens } : {}),
|
|
600
|
+
};
|
|
601
|
+
const visualFrame: VisualFitConfig = {
|
|
602
|
+
// The in-loop visual round renders the same sample the runtime probe
|
|
603
|
+
// does (the caller's fixture) unless the config names its own.
|
|
604
|
+
sampleProps: visualEvaluation?.sampleProps ?? fixtureProps,
|
|
605
|
+
// Per-canvas judging (arm-neutral) — absent = single shot.
|
|
606
|
+
...(visualEvaluation?.canvases !== undefined ? { canvases: visualEvaluation.canvases } : {}),
|
|
607
|
+
// ggui#1195 — the declared box per canvas (same-target with the composer).
|
|
608
|
+
...(visualEvaluation?.canvasViewports !== undefined ? { canvasViewports: visualEvaluation.canvasViewports } : {}),
|
|
609
|
+
};
|
|
610
|
+
// The fit verdict is per canvas: a single-shot leg has no box to hold content against.
|
|
611
|
+
const measureFit =
|
|
612
|
+
visualMod && visualFrame.canvases !== undefined && visualFrame.canvases.length > 0
|
|
613
|
+
? () => visualMod.runVisualFit(visualContext, visualFrame)
|
|
614
|
+
: undefined;
|
|
615
|
+
|
|
451
616
|
const evalLlmStart = Date.now();
|
|
452
617
|
const [llm, visual] = await Promise.all([
|
|
453
618
|
llmEvalMod
|
|
@@ -483,35 +648,25 @@ export async function runEvalRound(
|
|
|
483
648
|
)
|
|
484
649
|
: null,
|
|
485
650
|
visualMod
|
|
486
|
-
?
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
: {}),
|
|
506
|
-
// #484/#489 — same threading as the LLM-eval leg above:
|
|
507
|
-
// the visual eval's multimodal call runs inside the same
|
|
508
|
-
// concurrent generation, so it needs the same routing
|
|
509
|
-
// override (never fall back to `process.env`) and the
|
|
510
|
-
// same provider-429-retry observer (retries reach the
|
|
511
|
-
// host application's structured log).
|
|
512
|
-
routeOverride: visualEvalAgent.routeOverride,
|
|
513
|
-
onRetry: visualEvalAgent.onRetry,
|
|
514
|
-
},
|
|
651
|
+
? runVisualLeg(
|
|
652
|
+
visualEvalAgent,
|
|
653
|
+
(provider) =>
|
|
654
|
+
visualMod.runVisualEval(visualContext, {
|
|
655
|
+
provider,
|
|
656
|
+
model: visualEvalAgent.model,
|
|
657
|
+
passThreshold: visualThreshold,
|
|
658
|
+
viewport: visualEvaluation?.viewport,
|
|
659
|
+
...visualFrame,
|
|
660
|
+
// #484/#489 — same threading as the LLM-eval leg above:
|
|
661
|
+
// the visual eval's multimodal call runs inside the same
|
|
662
|
+
// concurrent generation, so it needs the same routing
|
|
663
|
+
// override (never fall back to `process.env`) and the
|
|
664
|
+
// same provider-429-retry observer (retries reach the
|
|
665
|
+
// host application's structured log).
|
|
666
|
+
routeOverride: visualEvalAgent.routeOverride,
|
|
667
|
+
onRetry: visualEvalAgent.onRetry,
|
|
668
|
+
}),
|
|
669
|
+
measureFit,
|
|
515
670
|
)
|
|
516
671
|
: null,
|
|
517
672
|
]);
|
|
@@ -519,6 +674,12 @@ export async function runEvalRound(
|
|
|
519
674
|
llmResult = llm;
|
|
520
675
|
visualIssues = visual?.issues ?? null;
|
|
521
676
|
const visualSummary: VisualEvalSummary | undefined = visual?.summary;
|
|
677
|
+
// ggui#1221 — the judge leg's coverage is ALWAYS stamped: ran / skipped (with the
|
|
678
|
+
// reason) / not-applicable when no visual leg was configured for this round.
|
|
679
|
+
const visualCoverage: VisualCoverage = visual?.coverage ?? { status: 'not-applicable', reason: 'visual leg not configured' };
|
|
680
|
+
if (visualCoverage.status === 'skipped') {
|
|
681
|
+
console.log(`[eval] visual-judge: SKIPPED (${visualCoverage.reason ?? 'no reason recorded'})`);
|
|
682
|
+
}
|
|
522
683
|
|
|
523
684
|
// Track costs from LLM eval calls
|
|
524
685
|
if (llmResult) {
|
|
@@ -526,6 +687,8 @@ export async function runEvalRound(
|
|
|
526
687
|
evalTokens = {
|
|
527
688
|
input: llmResult.inputTokens,
|
|
528
689
|
output: llmResult.outputTokens,
|
|
690
|
+
...(llmResult.cacheReadTokens !== undefined ? { cacheRead: llmResult.cacheReadTokens } : {}),
|
|
691
|
+
...(llmResult.cacheCreationTokens !== undefined ? { cacheCreation: llmResult.cacheCreationTokens } : {}),
|
|
529
692
|
};
|
|
530
693
|
}
|
|
531
694
|
|
|
@@ -549,6 +712,7 @@ export async function runEvalRound(
|
|
|
549
712
|
// requested and the leg ran; every later re-stamp spreads this
|
|
550
713
|
// base so it survives to the assembled result.
|
|
551
714
|
...(visualSummary !== undefined ? { visual: visualSummary } : {}),
|
|
715
|
+
visualCoverage,
|
|
552
716
|
};
|
|
553
717
|
|
|
554
718
|
// ── Log merged results, with mode-check subcategory breakdown ──
|
|
@@ -644,6 +808,40 @@ export async function runEvalRound(
|
|
|
644
808
|
lastDiffFailed: false,
|
|
645
809
|
};
|
|
646
810
|
}
|
|
811
|
+
// ggui#1261 — a contract defect the probe diagnosed (a named check,
|
|
812
|
+
// FAIL) that this generation has not been told about: ONE feedback
|
|
813
|
+
// round with the probe's own diagnosis and fix. The next round re-runs
|
|
814
|
+
// the probe at its exit, so the result this generation ends with
|
|
815
|
+
// carries the post-fix stamp, never this one.
|
|
816
|
+
const contractFails = contractFeedbackFails(exitProbe, prevFailFingerprints);
|
|
817
|
+
if (contractFails.length > 0 && evalRoundsUsed < maxEvalRounds + CONTRACT_FEEDBACK_BONUS) {
|
|
818
|
+
const lines = contractFails.slice(0, MAX_FEEDBACK_ISSUES).map(formatRuntimeProbeFeedback);
|
|
819
|
+
evalResult = {
|
|
820
|
+
...evalResult,
|
|
821
|
+
issues: [...allIssues, ...exitProbe.probeIssues],
|
|
822
|
+
pass: allPass,
|
|
823
|
+
runtimeProbe: exitProbe.meta,
|
|
824
|
+
};
|
|
825
|
+
console.log(
|
|
826
|
+
`[simple] eval round ${evalRoundsUsed}: tier-1/2 clean BUT contract probe fail — granting +1 turn ` +
|
|
827
|
+
`(ggui#1261; ${contractFails.map((i) => i.subcategory).join(", ")})`,
|
|
828
|
+
);
|
|
829
|
+
return {
|
|
830
|
+
control: "feedback",
|
|
831
|
+
evalDone: false,
|
|
832
|
+
evalResult,
|
|
833
|
+
evalRoundsUsed,
|
|
834
|
+
prevModeSubcats: updatedPrevModeSubcats,
|
|
835
|
+
prevFailFingerprints: new Set([...currFailFingerprints, ...contractFails.map(fingerprintFail)]),
|
|
836
|
+
preWarmedContext,
|
|
837
|
+
evalTokens,
|
|
838
|
+
evalLlmMs,
|
|
839
|
+
lastResultText: lines.join("\n\n"),
|
|
840
|
+
isEvalFeedback: true,
|
|
841
|
+
lastDiffFailed: false,
|
|
842
|
+
contractFeedback: contractFeedbackRecord(contractFails, currentSource),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
647
845
|
// Probe didn't trip a recoverable fail — fold its issues (if any)
|
|
648
846
|
// into the merged result and ALWAYS stamp its execution meta, so a
|
|
649
847
|
// did-not-run probe is visible downstream even on this clean exit.
|
|
@@ -683,13 +881,19 @@ export async function runEvalRound(
|
|
|
683
881
|
// to localize them; the +2 max-rounds extension is the right escape
|
|
684
882
|
// hatch, not stuck-loop exit. Keep stuck-loop active for non-runtime
|
|
685
883
|
// fail-sets (the original tier-1 nitpick case it was built for).
|
|
686
|
-
|
|
884
|
+
// ggui#1195 follow-up: a FIT fail (`visual|canvas-overflow|…`) is exempt
|
|
885
|
+
// the same way — its escape hatch is the one `[fit]` round at the cap, and
|
|
886
|
+
// a card whose measured height did not move between rounds would otherwise
|
|
887
|
+
// exit here as "stuck" before that round is ever reached (the adversarial
|
|
888
|
+
// review ran the commit's own test and found round 2's break WAS the stuck
|
|
889
|
+
// exit). Bounded: the cap block grants the fit round exactly once.
|
|
890
|
+
const onlyExtensionExemptFails =
|
|
687
891
|
currFailFingerprints.size > 0 &&
|
|
688
|
-
[...currFailFingerprints].every((fp) => fp.includes("|runtime:"));
|
|
892
|
+
[...currFailFingerprints].every((fp) => fp.includes("|runtime:") || fp.startsWith(FIT_FINGERPRINT_PREFIX));
|
|
689
893
|
if (
|
|
690
894
|
evalRoundsUsed > 1 &&
|
|
691
895
|
currFailFingerprints.size > 0 &&
|
|
692
|
-
!
|
|
896
|
+
!onlyExtensionExemptFails &&
|
|
693
897
|
[...currFailFingerprints].every((fp) => prevFailFingerprints.has(fp))
|
|
694
898
|
) {
|
|
695
899
|
console.log(
|
|
@@ -701,7 +905,8 @@ export async function runEvalRound(
|
|
|
701
905
|
// the runtimeProbe stamp lands (Exp 48 found this exit surfacing
|
|
702
906
|
// as "SKIP — no runtimeProbe stamp" in the bench: honest, but a
|
|
703
907
|
// probe here is cheap and the shipped code deserves the signal).
|
|
704
|
-
//
|
|
908
|
+
// The RECURRING fails get no more feedback — stuck means the model
|
|
909
|
+
// saw them and stopped converging on them.
|
|
705
910
|
const stuckExitProbe = await runProbeAtExit({
|
|
706
911
|
harness,
|
|
707
912
|
sourceCode: currentSource,
|
|
@@ -714,6 +919,36 @@ export async function runEvalRound(
|
|
|
714
919
|
issues: [...evalResult.issues, ...stuckExitProbe.probeIssues],
|
|
715
920
|
runtimeProbe: stuckExitProbe.meta,
|
|
716
921
|
};
|
|
922
|
+
// ggui#1261 — a contract defect the exit probe diagnosed here was never
|
|
923
|
+
// DELIVERED: the stuck set is non-runtime by construction (runtime-only
|
|
924
|
+
// sets are exempt above), and the stuck premise is "the model saw these
|
|
925
|
+
// and did not resolve them". So it gets the same one round as at the
|
|
926
|
+
// clean-PASS and cap exits — the probe's diagnosis only, never the
|
|
927
|
+
// recurring fail; one attempt per finding; bounded at the cap + 1 (Exp
|
|
928
|
+
// 011, candidate r5: the one FAIL that shipped without a round).
|
|
929
|
+
const stuckContractFails = contractFeedbackFails(stuckExitProbe, prevFailFingerprints);
|
|
930
|
+
if (stuckContractFails.length > 0 && evalRoundsUsed < maxEvalRounds + CONTRACT_FEEDBACK_BONUS) {
|
|
931
|
+
const lines = stuckContractFails.slice(0, MAX_FEEDBACK_ISSUES).map(formatRuntimeProbeFeedback);
|
|
932
|
+
console.log(
|
|
933
|
+
`[simple] eval round ${evalRoundsUsed}: stuck BUT contract probe fail never delivered — granting +1 turn ` +
|
|
934
|
+
`(ggui#1261; ${stuckContractFails.map((i) => i.subcategory).join(", ")})`,
|
|
935
|
+
);
|
|
936
|
+
return {
|
|
937
|
+
control: "feedback",
|
|
938
|
+
evalDone: false,
|
|
939
|
+
evalResult,
|
|
940
|
+
evalRoundsUsed,
|
|
941
|
+
prevModeSubcats: updatedPrevModeSubcats,
|
|
942
|
+
prevFailFingerprints: new Set([...currFailFingerprints, ...stuckContractFails.map(fingerprintFail)]),
|
|
943
|
+
preWarmedContext,
|
|
944
|
+
evalTokens,
|
|
945
|
+
evalLlmMs,
|
|
946
|
+
lastResultText: lines.join("\n\n"),
|
|
947
|
+
isEvalFeedback: true,
|
|
948
|
+
lastDiffFailed: false,
|
|
949
|
+
contractFeedback: contractFeedbackRecord(stuckContractFails, currentSource),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
717
952
|
return {
|
|
718
953
|
control: "break",
|
|
719
954
|
evalDone: false,
|
|
@@ -747,6 +982,11 @@ export async function runEvalRound(
|
|
|
747
982
|
// stuck-loop detector won't fire (some fails change), but the cap
|
|
748
983
|
// hits with the runtime fail unresolved.
|
|
749
984
|
const RUNTIME_EXTENSION_BONUS = 2;
|
|
985
|
+
// ggui#1195 — a fit fail (the hello does not fit its declared first
|
|
986
|
+
// screen — the founder's ruling is FAIL, not warn) active at the cap is
|
|
987
|
+
// worth ONE more round: the fix is a composition change the LLM can make
|
|
988
|
+
// in a turn, and without the round the card ships cut off.
|
|
989
|
+
const FIT_EXTENSION_BONUS = 1;
|
|
750
990
|
if (evalRoundsUsed >= maxEvalRounds) {
|
|
751
991
|
// 2026-04-27 (in-loop probe trigger): probe is now off the per-turn
|
|
752
992
|
// hot path; `blocking` from runCheck won't contain `runtime:*` fails
|
|
@@ -764,6 +1004,66 @@ export async function runEvalRound(
|
|
|
764
1004
|
const allowRuntimeExtension =
|
|
765
1005
|
hasRuntimeFail &&
|
|
766
1006
|
evalRoundsUsed < maxEvalRounds + RUNTIME_EXTENSION_BONUS;
|
|
1007
|
+
const fitFails = evalResult.issues.filter(isFitFail);
|
|
1008
|
+
const allowFitExtension =
|
|
1009
|
+
!allowRuntimeExtension &&
|
|
1010
|
+
fitFails.length > 0 &&
|
|
1011
|
+
evalRoundsUsed < maxEvalRounds + FIT_EXTENSION_BONUS;
|
|
1012
|
+
if (allowFitExtension) {
|
|
1013
|
+
const fitLines = fitFails.slice(0, MAX_FEEDBACK_ISSUES).map(formatFitFeedback);
|
|
1014
|
+
evalResult = {
|
|
1015
|
+
...evalResult,
|
|
1016
|
+
issues: [...evalResult.issues, ...exitProbe.probeIssues],
|
|
1017
|
+
runtimeProbe: exitProbe.meta,
|
|
1018
|
+
};
|
|
1019
|
+
console.log(
|
|
1020
|
+
`[simple] eval round ${evalRoundsUsed}: fit fail active at cap — granting +1 retry (ggui#1195; ${fitFails.length} canvas-overflow)`,
|
|
1021
|
+
);
|
|
1022
|
+
return {
|
|
1023
|
+
control: "feedback",
|
|
1024
|
+
evalDone: false,
|
|
1025
|
+
evalResult,
|
|
1026
|
+
evalRoundsUsed,
|
|
1027
|
+
prevModeSubcats: updatedPrevModeSubcats,
|
|
1028
|
+
prevFailFingerprints: new Set([...currFailFingerprints, ...fitFails.map(fingerprintFail)]),
|
|
1029
|
+
preWarmedContext,
|
|
1030
|
+
evalTokens,
|
|
1031
|
+
evalLlmMs,
|
|
1032
|
+
lastResultText: fitLines.join("\n\n"),
|
|
1033
|
+
isEvalFeedback: true,
|
|
1034
|
+
lastDiffFailed: false,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
// ggui#1261 — a named contract FAIL from the exit probe active at the
|
|
1038
|
+
// cap: the same one round as at the clean exit, bounded at the cap + 1.
|
|
1039
|
+
const capContractFails = allowRuntimeExtension ? [] : contractFeedbackFails(exitProbe, prevFailFingerprints);
|
|
1040
|
+
if (capContractFails.length > 0 && evalRoundsUsed < maxEvalRounds + CONTRACT_FEEDBACK_BONUS) {
|
|
1041
|
+
const contractLines = capContractFails.slice(0, MAX_FEEDBACK_ISSUES).map(formatRuntimeProbeFeedback);
|
|
1042
|
+
evalResult = {
|
|
1043
|
+
...evalResult,
|
|
1044
|
+
issues: [...evalResult.issues, ...exitProbe.probeIssues],
|
|
1045
|
+
runtimeProbe: exitProbe.meta,
|
|
1046
|
+
};
|
|
1047
|
+
console.log(
|
|
1048
|
+
`[simple] eval round ${evalRoundsUsed}: contract probe fail active at cap — granting +1 retry ` +
|
|
1049
|
+
`(ggui#1261; ${capContractFails.map((i) => i.subcategory).join(", ")})`,
|
|
1050
|
+
);
|
|
1051
|
+
return {
|
|
1052
|
+
control: "feedback",
|
|
1053
|
+
evalDone: false,
|
|
1054
|
+
evalResult,
|
|
1055
|
+
evalRoundsUsed,
|
|
1056
|
+
prevModeSubcats: updatedPrevModeSubcats,
|
|
1057
|
+
prevFailFingerprints: new Set([...currFailFingerprints, ...capContractFails.map(fingerprintFail)]),
|
|
1058
|
+
preWarmedContext,
|
|
1059
|
+
evalTokens,
|
|
1060
|
+
evalLlmMs,
|
|
1061
|
+
lastResultText: contractLines.join("\n\n"),
|
|
1062
|
+
isEvalFeedback: true,
|
|
1063
|
+
lastDiffFailed: false,
|
|
1064
|
+
contractFeedback: contractFeedbackRecord(capContractFails, currentSource),
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
767
1067
|
if (!allowRuntimeExtension) {
|
|
768
1068
|
// Fold probe issues (if any) into evalResult for telemetry and
|
|
769
1069
|
// ALWAYS stamp the probe's execution meta, even though we're not
|
|
@@ -18,3 +18,23 @@
|
|
|
18
18
|
export function mapProviderForEvaluator(provider: 'anthropic' | 'openai' | 'google' | 'openrouter'): 'claude' | 'openai' | 'google' | 'openrouter' {
|
|
19
19
|
return provider === 'anthropic' ? 'claude' : provider;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The visual judge's provider for an agent's provider (ggui#1248) — only the
|
|
24
|
+
* providers whose SDK path takes an image (`createVisionAgent`: Anthropic,
|
|
25
|
+
* Google). `undefined` for a provider with no vision path, so a caller must
|
|
26
|
+
* decide what the visual leg does there instead of asserting it away: the
|
|
27
|
+
* in-loop judge inherits the generation's agent by default, and an OpenAI or
|
|
28
|
+
* OpenRouter lane reaching `createVisionAgent` got `undefined` back and threw.
|
|
29
|
+
*/
|
|
30
|
+
export function visionJudgeProvider(provider: 'anthropic' | 'openai' | 'google' | 'openrouter'): 'claude' | 'google' | undefined {
|
|
31
|
+
switch (provider) {
|
|
32
|
+
case 'anthropic':
|
|
33
|
+
return 'claude';
|
|
34
|
+
case 'google':
|
|
35
|
+
return 'google';
|
|
36
|
+
case 'openai':
|
|
37
|
+
case 'openrouter':
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|