@ggui-ai/ui-gen 0.21.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/generation-dispatch.d.ts +1 -1
- package/dist/adapters/generation-dispatch.js +198 -59
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +35 -10
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +2 -2
- package/dist/advanced/index.js +381 -58
- package/dist/advanced/index.js.map +1 -1
- 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-D7kOXNjJ.d.ts → create-ui-generator-BEkzEtNz.d.ts} +1 -1
- package/dist/{dispatch-DGvhmARp.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 +3 -3
- package/dist/evaluation/index.js +37 -20
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +34 -9
- package/dist/harness/index.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 +1 -1
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +22 -6
- package/dist/index.js +381 -58
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +1 -1
- package/dist/{types-public-CzYqZclY.d.ts → types-public-mM_-1Yk3.d.ts} +16 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +12 -12
- package/src/adapters/claude/claude-code-login.ts +62 -7
- package/src/adapters/claude/sdk.ts +6 -5
- 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 +30 -0
- package/src/evaluation/index.ts +1 -0
- package/src/evaluation/llm-evaluator.ts +40 -13
- package/src/evaluation/types-public.ts +16 -0
- package/src/evaluation/visual-evaluator.ts +9 -6
- package/src/generator-build.ts +158 -0
- package/src/harness/claude-code-login-query.ts +5 -4
- package/src/harness/coding/assemble-result.ts +8 -1
- 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 +160 -5
- package/src/harness/result-types.ts +14 -0
- package/src/index.ts +2 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generator's BUILD identity (ggui#1280): values that change when the
|
|
3
|
+
* triad's templates change and never with the request, so generations group
|
|
4
|
+
* by the build that produced them.
|
|
5
|
+
*
|
|
6
|
+
* - `promptTemplateSha256` / `boilerplateTemplateSha256` are sha256 over the
|
|
7
|
+
* production system prompt and the boilerplate rendered on the design-mode
|
|
8
|
+
* pin's FIXED fixtures (`PIN_FIXTURES`), in the live env, so an env-gated
|
|
9
|
+
* block (the pitfalls) counts as it really is. The design-mode pin renders
|
|
10
|
+
* the same functions and asserts these values equal its recorded constants
|
|
11
|
+
* under the default env: the runtime key and the pin cannot drift.
|
|
12
|
+
* - These are BUILD keys, not instance fingerprints. A hash of the prompt one
|
|
13
|
+
* generation actually sent embeds the request and differs every time, so it
|
|
14
|
+
* could never group two generations by build.
|
|
15
|
+
* - `version` is the package's own version, read once from the nearest
|
|
16
|
+
* `package.json` named `@ggui-ai/ui-gen` above this module. It is absent
|
|
17
|
+
* when none is found (e.g. a consumer bundled this package into one file),
|
|
18
|
+
* never guessed.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import type { GeneratorBuild } from '@ggui-ai/mcp-server-core';
|
|
25
|
+
import { generateBoilerplate } from './boilerplate/generate.js';
|
|
26
|
+
import type { DesignMode } from './design-mode.js';
|
|
27
|
+
import { buildSystemPrompt } from './harness/runtime.js';
|
|
28
|
+
import { PIN_FIXTURES } from './pin-fixtures.js';
|
|
29
|
+
|
|
30
|
+
/** The separator the rendered fixtures are joined with before hashing. */
|
|
31
|
+
export const TEMPLATE_FIXTURE_BOUNDARY = '\n<<<pin-fixture-boundary>>>\n';
|
|
32
|
+
|
|
33
|
+
/** sha256 hex of a rendered template. */
|
|
34
|
+
export function templateSha256(text: string): string {
|
|
35
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The production system prompt for every pin fixture, joined. The constrained
|
|
40
|
+
* arm uses the positional default-path signature (no `designMode` argument),
|
|
41
|
+
* which is what the pin has always recorded.
|
|
42
|
+
*/
|
|
43
|
+
export function renderPromptTemplates(designMode: DesignMode): string {
|
|
44
|
+
return PIN_FIXTURES.map((f) => {
|
|
45
|
+
switch (designMode) {
|
|
46
|
+
case 'constrained':
|
|
47
|
+
return buildSystemPrompt(f.userRequest, f.shellType, f.screen);
|
|
48
|
+
case 'free':
|
|
49
|
+
return buildSystemPrompt(f.userRequest, f.shellType, f.screen, undefined, undefined, undefined, 'free');
|
|
50
|
+
}
|
|
51
|
+
}).join(TEMPLATE_FIXTURE_BOUNDARY);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The boilerplate for every pin fixture, joined — same signature split as the prompt. */
|
|
55
|
+
export function renderBoilerplateTemplates(designMode: DesignMode): string {
|
|
56
|
+
return PIN_FIXTURES.map((f) => {
|
|
57
|
+
switch (designMode) {
|
|
58
|
+
case 'constrained':
|
|
59
|
+
return generateBoilerplate(f.userRequest, f.contract, f.shellType, f.screen);
|
|
60
|
+
case 'free':
|
|
61
|
+
return generateBoilerplate(f.userRequest, f.contract, f.shellType, f.screen, undefined, undefined, 'free');
|
|
62
|
+
}
|
|
63
|
+
}).join(TEMPLATE_FIXTURE_BOUNDARY);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* ui-gen's precise {@link GeneratorBuild}: the core port's generic shape with
|
|
68
|
+
* ui-gen's own keys — `mode` is the design mode, and `digests` names the two
|
|
69
|
+
* template digests (the keys a blueprint's build stamp copies).
|
|
70
|
+
*/
|
|
71
|
+
export interface UiGenBuild extends GeneratorBuild {
|
|
72
|
+
/** This package's version; absent when its `package.json` cannot be found at runtime. */
|
|
73
|
+
readonly version?: string;
|
|
74
|
+
readonly mode: DesignMode;
|
|
75
|
+
readonly digests: {
|
|
76
|
+
readonly promptTemplateSha256: string;
|
|
77
|
+
readonly boilerplateTemplateSha256: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const PACKAGE_NAME = '@ggui-ai/ui-gen';
|
|
82
|
+
/** How far up from this module the package root can sit (`src/`, `dist/`, a nested entry). */
|
|
83
|
+
const MAX_PACKAGE_DEPTH = 6;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A `package.json` the walk meets may not be ours, and in a consumer's bundle
|
|
87
|
+
* it may not even parse. A candidate that does not parse is skipped (as if
|
|
88
|
+
* absent), so reading this package's version can never fail a generation —
|
|
89
|
+
* the version is then absent, never guessed. Only a JSON `SyntaxError` is
|
|
90
|
+
* handled here; any other error is a real fault and propagates.
|
|
91
|
+
*/
|
|
92
|
+
function parsePackageJson(text: string): unknown {
|
|
93
|
+
try {
|
|
94
|
+
const parsed: unknown = JSON.parse(text);
|
|
95
|
+
return parsed;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err instanceof SyntaxError) return undefined;
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Walk up from `startDir` (this module's own directory by default) to the
|
|
104
|
+
* nearest `package.json` named `@ggui-ai/ui-gen` and return its version;
|
|
105
|
+
* `undefined` when none is found within reach.
|
|
106
|
+
*/
|
|
107
|
+
export function findPackageVersion(startDir: string = dirname(fileURLToPath(import.meta.url))): string | undefined {
|
|
108
|
+
let dir = startDir;
|
|
109
|
+
for (let i = 0; i < MAX_PACKAGE_DEPTH; i++) {
|
|
110
|
+
const candidate = join(dir, 'package.json');
|
|
111
|
+
const pkg = existsSync(candidate) ? parsePackageJson(readFileSync(candidate, 'utf8')) : undefined;
|
|
112
|
+
if (pkg !== undefined) {
|
|
113
|
+
if (
|
|
114
|
+
typeof pkg === 'object' &&
|
|
115
|
+
pkg !== null &&
|
|
116
|
+
'name' in pkg &&
|
|
117
|
+
pkg.name === PACKAGE_NAME &&
|
|
118
|
+
'version' in pkg &&
|
|
119
|
+
typeof pkg.version === 'string'
|
|
120
|
+
) {
|
|
121
|
+
return pkg.version;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const parent = dirname(dir);
|
|
125
|
+
if (parent === dir) break;
|
|
126
|
+
dir = parent;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let versionRead = false;
|
|
132
|
+
let packageVersion: string | undefined;
|
|
133
|
+
const identities = new Map<string, UiGenBuild>();
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The build identity for `designMode`, computed once per process per design
|
|
137
|
+
* mode and pitfalls env gate (the only env the templates read), so a changed
|
|
138
|
+
* gate is never answered from a stale memo.
|
|
139
|
+
*/
|
|
140
|
+
export function generatorBuild(designMode: DesignMode): UiGenBuild {
|
|
141
|
+
const key = `${designMode}|${process.env.GGUI_PITFALLS ?? ''}|${process.env.GGUI_NEW_PITFALLS ?? ''}`;
|
|
142
|
+
const known = identities.get(key);
|
|
143
|
+
if (known !== undefined) return known;
|
|
144
|
+
if (!versionRead) {
|
|
145
|
+
packageVersion = findPackageVersion();
|
|
146
|
+
versionRead = true;
|
|
147
|
+
}
|
|
148
|
+
const identity: UiGenBuild = {
|
|
149
|
+
...(packageVersion !== undefined ? { version: packageVersion } : {}),
|
|
150
|
+
mode: designMode,
|
|
151
|
+
digests: {
|
|
152
|
+
promptTemplateSha256: templateSha256(renderPromptTemplates(designMode)),
|
|
153
|
+
boilerplateTemplateSha256: templateSha256(renderBoilerplateTemplates(designMode)),
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
identities.set(key, identity);
|
|
157
|
+
return identity;
|
|
158
|
+
}
|
|
@@ -43,7 +43,7 @@ import { z } from "zod";
|
|
|
43
43
|
import type { LLMToolDef } from "../llm.js";
|
|
44
44
|
import {
|
|
45
45
|
claudeCodeLoginQueryOptions,
|
|
46
|
-
|
|
46
|
+
loginChildEnv,
|
|
47
47
|
} from "../adapters/claude/claude-code-login.js";
|
|
48
48
|
import { MCP_SERVER_NAME } from "../adapters/claude/tool-bridge.js";
|
|
49
49
|
import { toolArgsToJsonObject } from "../adapters/tool-bridge.js";
|
|
@@ -186,8 +186,9 @@ export async function* imageUserMessage(
|
|
|
186
186
|
* SDK's typed `thinking` option (the probe spelled it `thinkingConfig`,
|
|
187
187
|
* untyped, and saw a thinking block anyway — whether the typed spelling
|
|
188
188
|
* is honoured is Exp 009's to measure; it reports thinking tokens as
|
|
189
|
-
* their own column either way);
|
|
190
|
-
*
|
|
189
|
+
* their own column either way); the binary inherits an allowlisted env
|
|
190
|
+
* (`loginChildEnv`: no provider key, nothing else the parent merely holds,
|
|
191
|
+
* ggui#1278); Layer 1's pins keep it tool-less (the CLI's
|
|
191
192
|
* own tools), config-less (`~/.claude` never loads) and non-bare (the
|
|
192
193
|
* login store IS read). Tools, when offered, come as ggui's in-process
|
|
193
194
|
* MCP server with NO allow-list and the denying intercept.
|
|
@@ -206,7 +207,7 @@ export function buildLoginQueryOptions(params: {
|
|
|
206
207
|
systemPrompt: params.systemPrompt,
|
|
207
208
|
maxTurns: 1,
|
|
208
209
|
thinking: { type: "disabled" },
|
|
209
|
-
env:
|
|
210
|
+
env: loginChildEnv(params.env ?? process.env),
|
|
210
211
|
...claudeCodeLoginQueryOptions(),
|
|
211
212
|
...(params.tools !== undefined
|
|
212
213
|
? {
|
|
@@ -122,7 +122,14 @@ export async function assembleGenerationResult(
|
|
|
122
122
|
generationTimeMs: totalMs,
|
|
123
123
|
turnsUsed: telemetry.turnsUsed,
|
|
124
124
|
passesUsed: 1,
|
|
125
|
-
|
|
125
|
+
// ggui#1261 — the contract-feedback round's record rides the result the
|
|
126
|
+
// generation ends with (whose probe stamp is the post-round verdict).
|
|
127
|
+
evalResult:
|
|
128
|
+
telemetry.evalResult !== undefined && telemetry.contractFeedback !== undefined
|
|
129
|
+
? { ...telemetry.evalResult, contractFeedback: telemetry.contractFeedback }
|
|
130
|
+
: telemetry.evalResult,
|
|
131
|
+
// ggui#404 — present only when the same-exchange guard ended the run.
|
|
132
|
+
...(telemetry.sameExchangeBreak !== undefined ? { sameExchangeBreak: telemetry.sameExchangeBreak } : {}),
|
|
126
133
|
needsBackgroundImprovement,
|
|
127
134
|
selfCheckPassed: telemetry.selfCheckPassed,
|
|
128
135
|
timing,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { Classification } from "../../classifier/index.js";
|
|
14
14
|
import type { JsonValue } from "@ggui-ai/protocol";
|
|
15
|
-
import type { EvalResult } from "../../evaluation/types-public.js";
|
|
15
|
+
import type { ContractFeedbackRecord, EvalResult } from "../../evaluation/types-public.js";
|
|
16
16
|
import type { Harness, Task, TaskContext } from "../types-public.js";
|
|
17
17
|
import type { SingleComponentParams } from "../runtime.js";
|
|
18
18
|
import type { TaskRunner } from "../index.js";
|
|
@@ -20,8 +20,11 @@ import type { CodingSession } from "./init-session.js";
|
|
|
20
20
|
import {
|
|
21
21
|
runCodingTurn,
|
|
22
22
|
type A1Phase,
|
|
23
|
+
type CodingTurnResult,
|
|
23
24
|
} from "./run-coding-turn.js";
|
|
24
25
|
import { runEvalRound } from "./run-eval-round.js";
|
|
26
|
+
import type { TokenUsage } from "./run-eval-round.js";
|
|
27
|
+
import type { SameExchangeBreak } from "../result-types.js";
|
|
25
28
|
|
|
26
29
|
type PreWarmedEvalContext =
|
|
27
30
|
import("../../evaluation/llm-evaluator.js").PreWarmedEvalContext;
|
|
@@ -64,15 +67,27 @@ export interface GenerateTelemetry {
|
|
|
64
67
|
/** Total output tokens billed across coding + eval. */
|
|
65
68
|
totalOut: number;
|
|
66
69
|
/**
|
|
67
|
-
* Prompt-cache tokens accumulated across coding turns
|
|
68
|
-
* provider reports them. Optional: stays
|
|
69
|
-
* that don't expose cache accounting (never
|
|
70
|
-
* the boundary can distinguish "no cache reads"
|
|
70
|
+
* Prompt-cache tokens accumulated across coding turns AND eval rounds
|
|
71
|
+
* (ggui#1281), when the provider reports them. Optional: stays
|
|
72
|
+
* `undefined` for providers that don't expose cache accounting (never
|
|
73
|
+
* defaulted to 0 here, so the boundary can distinguish "no cache reads"
|
|
74
|
+
* from "unreported").
|
|
71
75
|
*/
|
|
72
76
|
cacheReadTokens?: number;
|
|
73
77
|
cacheCreationTokens?: number;
|
|
74
78
|
/** Latest evalResult — undefined when eval was disabled or never ran. */
|
|
75
79
|
evalResult: EvalResult | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* ggui#1261 — the contract-feedback round, once one fired: kept across the
|
|
82
|
+
* rounds after it (each rebuilds `evalResult`) so the assembled result can
|
|
83
|
+
* carry the round's before beside the source the generation ends with.
|
|
84
|
+
*/
|
|
85
|
+
contractFeedback: ContractFeedbackRecord | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* The same-exchange guard's record (ggui#404), set by the turn it ended —
|
|
88
|
+
* undefined unless that guard is what stopped the loop.
|
|
89
|
+
*/
|
|
90
|
+
sameExchangeBreak: SameExchangeBreak | undefined;
|
|
76
91
|
/** Latest compiled code — empty string until the first self-check pass. */
|
|
77
92
|
compiledCode: string;
|
|
78
93
|
/**
|
|
@@ -111,6 +126,8 @@ export function createTelemetry(): GenerateTelemetry {
|
|
|
111
126
|
totalIn: 0,
|
|
112
127
|
totalOut: 0,
|
|
113
128
|
evalResult: undefined,
|
|
129
|
+
contractFeedback: undefined,
|
|
130
|
+
sameExchangeBreak: undefined,
|
|
114
131
|
compiledCode: "",
|
|
115
132
|
pairedSource: "",
|
|
116
133
|
selfCheckPassed: false,
|
|
@@ -120,6 +137,56 @@ export function createTelemetry(): GenerateTelemetry {
|
|
|
120
137
|
};
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Fold one call's (or one eval round's) tokens into the run's totals — the ONE
|
|
142
|
+
* rule for coding turns and eval rounds alike (ggui#1281: the eval path used to
|
|
143
|
+
* add input/output only, so `tokens.total` dropped every eval call's cached
|
|
144
|
+
* prefix). A cache counter is seeded on its first report and stays `undefined`
|
|
145
|
+
* while nothing has reported one: unreported, never zero.
|
|
146
|
+
*/
|
|
147
|
+
export function absorbTokens(telemetry: GenerateTelemetry, usage: TokenUsage): void {
|
|
148
|
+
telemetry.totalIn += usage.input;
|
|
149
|
+
telemetry.totalOut += usage.output;
|
|
150
|
+
if (usage.cacheRead !== undefined) {
|
|
151
|
+
telemetry.cacheReadTokens = (telemetry.cacheReadTokens ?? 0) + usage.cacheRead;
|
|
152
|
+
}
|
|
153
|
+
if (usage.cacheCreation !== undefined) {
|
|
154
|
+
telemetry.cacheCreationTokens = (telemetry.cacheCreationTokens ?? 0) + usage.cacheCreation;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Fold one coding turn's measurements into the run's telemetry — tokens (via
|
|
160
|
+
* {@link absorbTokens}), timings, the phase/outcome counters, and the
|
|
161
|
+
* same-exchange guard's record when that guard ended the turn (ggui#404). One
|
|
162
|
+
* function so every per-turn fact the result is assembled from lands in one
|
|
163
|
+
* place.
|
|
164
|
+
*/
|
|
165
|
+
export function absorbTurn(telemetry: GenerateTelemetry, turn: CodingTurnResult): void {
|
|
166
|
+
absorbTokens(telemetry, {
|
|
167
|
+
input: turn.tokens.input,
|
|
168
|
+
output: turn.tokens.output,
|
|
169
|
+
...(turn.cacheReadTokens !== undefined ? { cacheRead: turn.cacheReadTokens } : {}),
|
|
170
|
+
...(turn.cacheCreationTokens !== undefined ? { cacheCreation: turn.cacheCreationTokens } : {}),
|
|
171
|
+
});
|
|
172
|
+
telemetry.cumulativeLlmMs += turn.llmMs;
|
|
173
|
+
telemetry.cumulativeToolMs += turn.toolMs;
|
|
174
|
+
if (turn.phase) {
|
|
175
|
+
if (turn.phase === "scaffold") telemetry.counters.phases.scaffold++;
|
|
176
|
+
else if (turn.phase === "fill") telemetry.counters.phases.fill++;
|
|
177
|
+
else if (turn.phase === "impl") telemetry.counters.phases.impl++;
|
|
178
|
+
else if (turn.phase === "eval-fix") telemetry.counters.phases.evalFix++;
|
|
179
|
+
else telemetry.counters.phases.patch++;
|
|
180
|
+
}
|
|
181
|
+
if (turn.outcome) {
|
|
182
|
+
if (turn.outcome === "PASS") telemetry.counters.outcomes.pass++;
|
|
183
|
+
else if (turn.outcome === "PATCH_INVALID") telemetry.counters.outcomes.patchInvalid++;
|
|
184
|
+
else if (turn.outcome === "DIFF_FAIL") telemetry.counters.outcomes.diffFail++;
|
|
185
|
+
else telemetry.counters.outcomes.selfCheckFail++;
|
|
186
|
+
}
|
|
187
|
+
if (turn.sameExchangeBreak !== undefined) telemetry.sameExchangeBreak = turn.sameExchangeBreak;
|
|
188
|
+
}
|
|
189
|
+
|
|
123
190
|
export interface CreateGenerateRunnerInput {
|
|
124
191
|
readonly session: CodingSession;
|
|
125
192
|
readonly params: SingleComponentParams;
|
|
@@ -203,38 +270,10 @@ export function createGenerateTaskRunner(input: CreateGenerateRunnerInput): Task
|
|
|
203
270
|
},
|
|
204
271
|
);
|
|
205
272
|
|
|
206
|
-
telemetry
|
|
207
|
-
telemetry.totalOut += turn.tokens.output;
|
|
208
|
-
// Accumulate prompt-cache counters only when the provider reported
|
|
209
|
-
// them this turn. Seeding from `undefined` on first report keeps the
|
|
210
|
-
// "unreported" providers' totals absent rather than 0.
|
|
211
|
-
if (turn.cacheReadTokens !== undefined) {
|
|
212
|
-
telemetry.cacheReadTokens =
|
|
213
|
-
(telemetry.cacheReadTokens ?? 0) + turn.cacheReadTokens;
|
|
214
|
-
}
|
|
215
|
-
if (turn.cacheCreationTokens !== undefined) {
|
|
216
|
-
telemetry.cacheCreationTokens =
|
|
217
|
-
(telemetry.cacheCreationTokens ?? 0) + turn.cacheCreationTokens;
|
|
218
|
-
}
|
|
219
|
-
telemetry.cumulativeLlmMs += turn.llmMs;
|
|
220
|
-
telemetry.cumulativeToolMs += turn.toolMs;
|
|
273
|
+
absorbTurn(telemetry, turn);
|
|
221
274
|
iconNamesCache = turn.iconNamesCache;
|
|
222
275
|
preWarmedContext = turn.preWarmedContext;
|
|
223
276
|
|
|
224
|
-
if (turn.phase) {
|
|
225
|
-
if (turn.phase === "scaffold") telemetry.counters.phases.scaffold++;
|
|
226
|
-
else if (turn.phase === "fill") telemetry.counters.phases.fill++;
|
|
227
|
-
else if (turn.phase === "impl") telemetry.counters.phases.impl++;
|
|
228
|
-
else if (turn.phase === "eval-fix") telemetry.counters.phases.evalFix++;
|
|
229
|
-
else telemetry.counters.phases.patch++;
|
|
230
|
-
}
|
|
231
|
-
if (turn.outcome) {
|
|
232
|
-
if (turn.outcome === "PASS") telemetry.counters.outcomes.pass++;
|
|
233
|
-
else if (turn.outcome === "PATCH_INVALID") telemetry.counters.outcomes.patchInvalid++;
|
|
234
|
-
else if (turn.outcome === "DIFF_FAIL") telemetry.counters.outcomes.diffFail++;
|
|
235
|
-
else telemetry.counters.outcomes.selfCheckFail++;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
277
|
if (turn.control === "break") break;
|
|
239
278
|
if (turn.control === "continue") {
|
|
240
279
|
lastResultText = turn.lastResultText;
|
|
@@ -334,9 +373,9 @@ export function createGenerateTaskRunner(input: CreateGenerateRunnerInput): Task
|
|
|
334
373
|
prevFailFingerprints = round.prevFailFingerprints;
|
|
335
374
|
preWarmedContext = round.preWarmedContext;
|
|
336
375
|
if (round.evalResult) telemetry.evalResult = round.evalResult;
|
|
376
|
+
if (round.contractFeedback) telemetry.contractFeedback = round.contractFeedback;
|
|
337
377
|
evalDone = round.evalDone;
|
|
338
|
-
telemetry
|
|
339
|
-
telemetry.totalOut += round.evalTokens.output;
|
|
378
|
+
absorbTokens(telemetry, round.evalTokens);
|
|
340
379
|
|
|
341
380
|
if (round.control === "break") break;
|
|
342
381
|
// round.control === "feedback" — set next coding turn input.
|
|
@@ -21,6 +21,7 @@ import { executeTool } from "../../coding-agent/tools.js";
|
|
|
21
21
|
import type { CostTracker } from "../../evaluation/cost-tracker.js";
|
|
22
22
|
import type { LLMAgent } from "../llm-router.js";
|
|
23
23
|
import type { Harness } from "../types-public.js";
|
|
24
|
+
import type { SameExchangeBreak } from "../result-types.js";
|
|
24
25
|
import {
|
|
25
26
|
APPLY_CHANGES_HASHLINE_TOOL,
|
|
26
27
|
APPLY_CHANGES_HASHLINE_TOOL_FLAT,
|
|
@@ -244,6 +245,12 @@ export interface CodingTurnResult {
|
|
|
244
245
|
readonly isEvalFeedback: boolean;
|
|
245
246
|
readonly iconNamesCache: string | null;
|
|
246
247
|
readonly preWarmedContext: PreWarmedEvalContext | null | undefined;
|
|
248
|
+
/**
|
|
249
|
+
* Set only on the `break` the same-exchange guard returns (ggui#404) — the
|
|
250
|
+
* one way a caller can tell this guard ended the run, since `break` is a
|
|
251
|
+
* control value many exits share. No key on any other turn.
|
|
252
|
+
*/
|
|
253
|
+
readonly sameExchangeBreak?: SameExchangeBreak;
|
|
247
254
|
}
|
|
248
255
|
|
|
249
256
|
/**
|
|
@@ -637,6 +644,7 @@ ${closingInstruction}`;
|
|
|
637
644
|
isEvalFeedback: false,
|
|
638
645
|
iconNamesCache,
|
|
639
646
|
preWarmedContext,
|
|
647
|
+
sameExchangeBreak: { tool: call.name, repeats: iconRepeats },
|
|
640
648
|
};
|
|
641
649
|
}
|
|
642
650
|
return {
|
|
@@ -964,6 +972,7 @@ ${closingInstruction}`;
|
|
|
964
972
|
isEvalFeedback: false,
|
|
965
973
|
iconNamesCache,
|
|
966
974
|
preWarmedContext,
|
|
975
|
+
sameExchangeBreak: { tool: call.name, repeats: sameExchangeRepeats },
|
|
967
976
|
};
|
|
968
977
|
}
|
|
969
978
|
|
|
@@ -22,7 +22,7 @@ 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, VisualCoverage } 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
27
|
import { mapProviderForEvaluator, visionJudgeProvider } from "../enforced-coding.js";
|
|
28
28
|
import { runCheck } from "../index.js";
|
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
type PreWarmedEvalContext =
|
|
41
41
|
import("../../evaluation/llm-evaluator.js").PreWarmedEvalContext;
|
|
42
42
|
type LlmEvalMod = typeof import("../../evaluation/llm-evaluator.js");
|
|
43
|
+
type LLMEvalTokenUsage = import("../../evaluation/llm-evaluator.js").LLMEvalTokenUsage;
|
|
43
44
|
type VisualEvalMod = typeof import("../../evaluation/visual-evaluator.js");
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -100,6 +101,20 @@ export interface EvalRoundInput {
|
|
|
100
101
|
*/
|
|
101
102
|
export type EvalRoundControl = "break" | "feedback";
|
|
102
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
|
+
|
|
103
118
|
export interface EvalRoundResult {
|
|
104
119
|
readonly control: EvalRoundControl;
|
|
105
120
|
/** True only on the low-risk bypass + clean-pass paths. */
|
|
@@ -115,7 +130,7 @@ export interface EvalRoundResult {
|
|
|
115
130
|
readonly prevFailFingerprints: Set<string>;
|
|
116
131
|
readonly preWarmedContext: PreWarmedEvalContext | null | undefined;
|
|
117
132
|
/** Tokens added during this round — caller adds to its own totals. */
|
|
118
|
-
readonly evalTokens:
|
|
133
|
+
readonly evalTokens: TokenUsage;
|
|
119
134
|
/** Wall-clock of the parallel LLM + visual eval block (zero on the
|
|
120
135
|
* low-risk bypass path, which exits before `Promise.all`). Caller
|
|
121
136
|
* accumulates into its `cumulativeEvalLlmMs`. */
|
|
@@ -124,6 +139,13 @@ export interface EvalRoundResult {
|
|
|
124
139
|
readonly lastResultText: string;
|
|
125
140
|
readonly isEvalFeedback: boolean;
|
|
126
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;
|
|
127
149
|
}
|
|
128
150
|
|
|
129
151
|
/** Cap feedback to N issues per eval-fix turn so the LLM stays surgical. */
|
|
@@ -368,6 +390,42 @@ function formatRuntimeProbeFeedback(issue: EvalIssue): string {
|
|
|
368
390
|
);
|
|
369
391
|
}
|
|
370
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
|
+
|
|
371
429
|
/**
|
|
372
430
|
* Run one eval round. Mutates `ctx.costTracker` (records LLM eval tokens)
|
|
373
431
|
* and `ctx.workspace` is read-only from this function's POV. Returns
|
|
@@ -404,7 +462,7 @@ export async function runEvalRound(
|
|
|
404
462
|
const currentSource = workspace.read() ?? "";
|
|
405
463
|
|
|
406
464
|
let evalResult: EvalResult | undefined;
|
|
407
|
-
let evalTokens = { input: 0, output: 0 };
|
|
465
|
+
let evalTokens: TokenUsage = { input: 0, output: 0 };
|
|
408
466
|
let evalLlmMs = 0;
|
|
409
467
|
|
|
410
468
|
try {
|
|
@@ -528,7 +586,7 @@ export async function runEvalRound(
|
|
|
528
586
|
}
|
|
529
587
|
|
|
530
588
|
// ── Tier 1+2: LLM evaluation + Visual eval in parallel ──
|
|
531
|
-
let llmResult: (EvalResult &
|
|
589
|
+
let llmResult: (EvalResult & LLMEvalTokenUsage) | null = null;
|
|
532
590
|
let visualIssues: EvalIssue[] | null = null;
|
|
533
591
|
|
|
534
592
|
// The visual leg's page and frame — ONE value each, read by the judge and
|
|
@@ -629,6 +687,8 @@ export async function runEvalRound(
|
|
|
629
687
|
evalTokens = {
|
|
630
688
|
input: llmResult.inputTokens,
|
|
631
689
|
output: llmResult.outputTokens,
|
|
690
|
+
...(llmResult.cacheReadTokens !== undefined ? { cacheRead: llmResult.cacheReadTokens } : {}),
|
|
691
|
+
...(llmResult.cacheCreationTokens !== undefined ? { cacheCreation: llmResult.cacheCreationTokens } : {}),
|
|
632
692
|
};
|
|
633
693
|
}
|
|
634
694
|
|
|
@@ -748,6 +808,40 @@ export async function runEvalRound(
|
|
|
748
808
|
lastDiffFailed: false,
|
|
749
809
|
};
|
|
750
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
|
+
}
|
|
751
845
|
// Probe didn't trip a recoverable fail — fold its issues (if any)
|
|
752
846
|
// into the merged result and ALWAYS stamp its execution meta, so a
|
|
753
847
|
// did-not-run probe is visible downstream even on this clean exit.
|
|
@@ -811,7 +905,8 @@ export async function runEvalRound(
|
|
|
811
905
|
// the runtimeProbe stamp lands (Exp 48 found this exit surfacing
|
|
812
906
|
// as "SKIP — no runtimeProbe stamp" in the bench: honest, but a
|
|
813
907
|
// probe here is cheap and the shipped code deserves the signal).
|
|
814
|
-
//
|
|
908
|
+
// The RECURRING fails get no more feedback — stuck means the model
|
|
909
|
+
// saw them and stopped converging on them.
|
|
815
910
|
const stuckExitProbe = await runProbeAtExit({
|
|
816
911
|
harness,
|
|
817
912
|
sourceCode: currentSource,
|
|
@@ -824,6 +919,36 @@ export async function runEvalRound(
|
|
|
824
919
|
issues: [...evalResult.issues, ...stuckExitProbe.probeIssues],
|
|
825
920
|
runtimeProbe: stuckExitProbe.meta,
|
|
826
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
|
+
}
|
|
827
952
|
return {
|
|
828
953
|
control: "break",
|
|
829
954
|
evalDone: false,
|
|
@@ -909,6 +1034,36 @@ export async function runEvalRound(
|
|
|
909
1034
|
lastDiffFailed: false,
|
|
910
1035
|
};
|
|
911
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
|
+
}
|
|
912
1067
|
if (!allowRuntimeExtension) {
|
|
913
1068
|
// Fold probe issues (if any) into evalResult for telemetry and
|
|
914
1069
|
// ALWAYS stamp the probe's execution meta, even though we're not
|