@ggui-ai/ui-gen 0.21.0 → 0.23.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 +253 -76
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +36 -11
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +3 -3
- package/dist/advanced/index.js +447 -76
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +37 -7
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +1 -0
- 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-D0kbWwln.d.ts} +1 -1
- package/dist/{dispatch-DGvhmARp.d.ts → dispatch-wxaIMwZt.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +19 -4
- 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 +19 -4
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +3 -3
- package/dist/evaluation/index.js +58 -26
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +4 -4
- package/dist/harness/check/runtime-render/index.js +26 -6
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +37 -11
- 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 +2 -1
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +9 -5
- package/dist/harness/types-public.js +1 -0
- 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 +436 -75
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +1 -1
- package/dist/{render-check-C0CqVceF.d.ts → render-check-Blwj8gB6.d.ts} +18 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/{types-public-CzYqZclY.d.ts → types-public-Ow-tNtFE.d.ts} +41 -3
- 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 +12 -12
- package/src/adapters/claude/claude-code-login.ts +62 -7
- package/src/adapters/claude/sdk.ts +6 -5
- package/src/advanced/generator.ts +16 -1
- package/src/blueprint-validator.ts +21 -6
- package/src/boilerplate/hard-sections.ts +1 -0
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +7 -3
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +7 -3
- package/src/coding-agent/tools.ts +6 -1
- package/src/contract-context.ts +1 -1
- package/src/create-ui-generator.ts +30 -0
- package/src/evaluation/axis-checks/checks/state-payload.ts +29 -4
- package/src/evaluation/index.ts +1 -0
- package/src/evaluation/llm-evaluator.ts +40 -13
- package/src/evaluation/types-public.ts +42 -2
- package/src/evaluation/visual-evaluator.ts +9 -6
- package/src/generator-build.ts +158 -0
- package/src/harness/check/runtime-render/adapter.ts +19 -0
- package/src/harness/check/runtime-render/render-check-host.ts +31 -9
- package/src/harness/check/runtime-render/render-check.ts +19 -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 +162 -5
- package/src/harness/llm-router.ts +3 -3
- package/src/harness/result-types.ts +14 -0
- package/src/harness/types-public.ts +9 -5
- package/src/index.ts +2 -1
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/validation/primitives.ts +1 -1
|
@@ -19,7 +19,7 @@ import { judgeDesignIdentity, type JudgeDesignIdentity } from './design-identity
|
|
|
19
19
|
import { resolve, dirname } from 'path';
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
|
-
import { existsSync, writeFileSync,
|
|
22
|
+
import { existsSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
|
|
23
23
|
import { createHash } from 'node:crypto';
|
|
24
24
|
import { tmpdir } from 'os';
|
|
25
25
|
import { createVisionAgent, type AgentConfig } from '../harness/llm-router';
|
|
@@ -346,9 +346,12 @@ async function bundleForRendering(
|
|
|
346
346
|
sampleProps: Record<string, unknown>,
|
|
347
347
|
designSrc: string = resolve(resolveDesignPackageDir(), 'src'),
|
|
348
348
|
): Promise<string> {
|
|
349
|
-
// Write component + entry to temp files for esbuild
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
// Write component + entry to temp files for esbuild — in a directory of THIS
|
|
350
|
+
// call's own. `mkdtempSync` is atomic and unique: a timestamp-named directory
|
|
351
|
+
// was shared by two bundles started in the same millisecond, so the first
|
|
352
|
+
// build read the second's component (a judge scoring the wrong generation)
|
|
353
|
+
// and the first's cleanup deleted the files the second was building.
|
|
354
|
+
const tmpDir = mkdtempSync(resolve(tmpdir(), 'ggui-visual-eval-'));
|
|
352
355
|
const componentFile = resolve(tmpDir, 'component.tsx');
|
|
353
356
|
const entryFile = resolve(tmpDir, 'entry.tsx');
|
|
354
357
|
|
|
@@ -439,8 +442,8 @@ try {
|
|
|
439
442
|
|
|
440
443
|
return result.outputFiles[0]?.text ?? '';
|
|
441
444
|
} finally {
|
|
442
|
-
|
|
443
|
-
|
|
445
|
+
// The whole per-call directory; `force` makes an already-absent path a no-op.
|
|
446
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
444
447
|
}
|
|
445
448
|
}
|
|
446
449
|
|
|
@@ -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
|
+
}
|
|
@@ -54,11 +54,30 @@ export const DEFAULT_RUNTIME_RENDER_CHECK: RuntimeRenderCheck = {
|
|
|
54
54
|
return { status: "infra-skipped", issues: [], reason: message };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const hostLoad = result.stats.hostLoad;
|
|
58
|
+
const load = hostLoad !== undefined ? { hostLoad } : {};
|
|
59
|
+
|
|
60
|
+
// ggui#1299: a check that ran out of wall-clock time is not a verdict on
|
|
61
|
+
// the component. It becomes the `timed-out` status (did-not-run, never a
|
|
62
|
+
// pass, never a crash), never an eval issue — so the coding agent is never
|
|
63
|
+
// told to fix a crash that did not happen.
|
|
64
|
+
if (result.incomplete !== undefined) {
|
|
65
|
+
const { elapsedMs, boundMs } = result.incomplete;
|
|
66
|
+
const loadNote =
|
|
67
|
+
hostLoad !== undefined
|
|
68
|
+
? `; host load ${hostLoad.start.toFixed(1)} → ${hostLoad.end.toFixed(1)} on ${hostLoad.cores} CPUs`
|
|
69
|
+
: "";
|
|
70
|
+
const reason = `render check did not finish within ${boundMs} ms (stopped at ${elapsedMs} ms)${loadNote}`;
|
|
71
|
+
console.warn(`[runtime-render] probe timed out — ${reason}`);
|
|
72
|
+
return { status: "timed-out", issues: [], reason, elapsedMs, ...load };
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
return {
|
|
58
76
|
status: "ran",
|
|
59
77
|
issues: result.issues
|
|
60
78
|
.map(toEvalIssue)
|
|
61
79
|
.filter((x): x is EvalIssue => x !== null),
|
|
80
|
+
...load,
|
|
62
81
|
};
|
|
63
82
|
},
|
|
64
83
|
};
|
|
@@ -11,13 +11,24 @@
|
|
|
11
11
|
* `unverified`, and nothing throws):
|
|
12
12
|
*
|
|
13
13
|
* - worker exit 0 + parseable verdict → the verdict, verbatim.
|
|
14
|
-
* - timeout → `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* - timeout → `incomplete` (ggui#1299): the check
|
|
15
|
+
* did not finish, which is NOT a verdict on the component. The bound
|
|
16
|
+
* is wall-clock, and on a contended host an ordinary component
|
|
17
|
+
* crosses it; mapping it to `failed` reported crashes that never
|
|
18
|
+
* happened. It carries its elapsed ms, and the host load (below)
|
|
19
|
+
* says how busy the host was. React's own runaway guards ("Too many
|
|
20
|
+
* re-renders", "Maximum update depth exceeded") throw, so they still
|
|
21
|
+
* reach `render-no-throw` as `failed`; what now reads `incomplete`
|
|
22
|
+
* is a check that ran out of wall-clock time, whatever the cause.
|
|
23
|
+
* - overflow → `failed` (a pathological logging or
|
|
24
|
+
* error loop; output volume does not depend on host load).
|
|
25
|
+
* - spawn-error / non-zero exit / unparseable stdout
|
|
18
26
|
* → `unverified` (the harness could not
|
|
19
27
|
* run the check; never blame the component for our plumbing).
|
|
20
28
|
*
|
|
29
|
+
* Every isolated check records the host's 1-minute load average at its
|
|
30
|
+
* start and end, and the CPUs available, on `stats.hostLoad`.
|
|
31
|
+
*
|
|
21
32
|
* Spawn resolution copies the proven `src/tools/render-check.ts`
|
|
22
33
|
* pattern: prefer the built `dist/.../render-check-worker.js`; in
|
|
23
34
|
* dev/test (vitest imports `src/`), fall back to `node --import tsx`
|
|
@@ -29,9 +40,11 @@
|
|
|
29
40
|
*/
|
|
30
41
|
import { existsSync } from 'node:fs';
|
|
31
42
|
import { createRequire } from 'node:module';
|
|
43
|
+
import { availableParallelism, loadavg } from 'node:os';
|
|
32
44
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
45
|
import { runSandboxed } from '@ggui-ai/sandbox';
|
|
34
46
|
import type { SandboxResult } from '@ggui-ai/sandbox';
|
|
47
|
+
import type { ProbeHostLoad } from '../../../evaluation/types-public.js';
|
|
35
48
|
import type { RenderCheckResult, RunRenderCheckInput } from './render-check.js';
|
|
36
49
|
|
|
37
50
|
/**
|
|
@@ -121,10 +134,12 @@ export function mapSandboxResultToCheckResult(
|
|
|
121
134
|
t0: number,
|
|
122
135
|
): RenderCheckResult {
|
|
123
136
|
if (result.outcome === 'timeout') {
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
issues: [],
|
|
140
|
+
incomplete: { kind: 'timeout', elapsedMs: result.durationMs, boundMs: CHECK_TIMEOUT_MS },
|
|
141
|
+
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 },
|
|
142
|
+
};
|
|
128
143
|
}
|
|
129
144
|
if (result.outcome === 'overflow-stdout' || result.outcome === 'overflow-stderr') {
|
|
130
145
|
return failed(
|
|
@@ -184,6 +199,7 @@ export async function runRenderCheckViaWorker(
|
|
|
184
199
|
return unverified(err instanceof Error ? err.message : String(err), t0);
|
|
185
200
|
}
|
|
186
201
|
|
|
202
|
+
const loadAtStart = loadavg()[0] ?? 0;
|
|
187
203
|
const result = await runSandboxed({
|
|
188
204
|
command: spawn.command,
|
|
189
205
|
args: spawn.args,
|
|
@@ -197,5 +213,11 @@ export async function runRenderCheckViaWorker(
|
|
|
197
213
|
env: { NODE_ENV: process.env.NODE_ENV ?? 'production' },
|
|
198
214
|
});
|
|
199
215
|
|
|
200
|
-
|
|
216
|
+
const hostLoad: ProbeHostLoad = {
|
|
217
|
+
start: loadAtStart,
|
|
218
|
+
end: loadavg()[0] ?? 0,
|
|
219
|
+
cores: availableParallelism(),
|
|
220
|
+
};
|
|
221
|
+
const mapped = mapSandboxResultToCheckResult(result, t0);
|
|
222
|
+
return { ...mapped, stats: { ...mapped.stats, hostLoad } };
|
|
201
223
|
}
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type ParentLink,
|
|
46
46
|
} from "./host-boundary.js";
|
|
47
47
|
import { hostGlobals, openRecord } from "../../../internal/open-record.js";
|
|
48
|
+
import type { ProbeHostLoad } from "../../../evaluation/types-public.js";
|
|
48
49
|
import { primeInputs } from "@ggui-ai/ui-visual-tester/prime-inputs";
|
|
49
50
|
|
|
50
51
|
/**
|
|
@@ -118,13 +119,31 @@ export type InputPrimingDiagnostic =
|
|
|
118
119
|
| { readonly primed: number }
|
|
119
120
|
| { readonly error: string };
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The check started but did not finish (ggui#1299). The only kind is a
|
|
124
|
+
* wall-clock timeout in the isolated worker. It is not a verdict on the
|
|
125
|
+
* component: `issues` is empty and `ok` is false because nothing was
|
|
126
|
+
* verified, never because something failed.
|
|
127
|
+
*/
|
|
128
|
+
export interface RenderCheckIncomplete {
|
|
129
|
+
readonly kind: "timeout";
|
|
130
|
+
/** How long the worker ran before it was stopped. */
|
|
131
|
+
readonly elapsedMs: number;
|
|
132
|
+
/** The wall-clock bound it crossed. */
|
|
133
|
+
readonly boundMs: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
export interface RenderCheckResult {
|
|
122
137
|
readonly ok: boolean;
|
|
123
138
|
readonly issues: readonly RenderCheckIssue[];
|
|
139
|
+
/** Present only when the check did not finish; see {@link RenderCheckIncomplete}. */
|
|
140
|
+
readonly incomplete?: RenderCheckIncomplete;
|
|
124
141
|
readonly stats: {
|
|
125
142
|
readonly actionsChecked: number;
|
|
126
143
|
readonly streamsChecked: number;
|
|
127
144
|
readonly renderMs: number;
|
|
145
|
+
/** Host load around an isolated check (absent in-process). */
|
|
146
|
+
readonly hostLoad?: ProbeHostLoad;
|
|
128
147
|
};
|
|
129
148
|
}
|
|
130
149
|
|
|
@@ -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
|
|