@ggui-ai/ui-gen 0.5.0-rc.2 → 0.6.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.js +1 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.js +60 -51
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +13 -2
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.js +61 -51
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +1 -1
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +13 -24
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +2 -2
- package/dist/check/index.js.map +1 -1
- package/dist/classifier/index.d.ts +14 -17
- package/dist/classifier/index.js +3 -7
- package/dist/classifier/index.js.map +1 -1
- package/dist/evaluation/index.js +12 -4
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/index.js +25 -28
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/runtime.js +14 -25
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.js +13 -24
- package/dist/harness/types-public.js.map +1 -1
- package/dist/index.js +62 -51
- package/dist/index.js.map +1 -1
- package/dist/provider-adapter-contract.js +12 -3
- package/dist/provider-adapter-contract.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +5 -0
- package/dist/validation/index.js.map +1 -1
- package/package.json +12 -12
- package/src/blueprint-validator.ts +1 -1
- package/src/boilerplate/generate.ts +20 -31
- package/src/boilerplate/system-prompt.ts +2 -2
- package/src/check/extract-wire-calls.ts +2 -2
- package/src/classifier/axes.ts +2 -2
- package/src/classifier/classifier.ts +2 -2
- package/src/classifier/infer-layout.ts +1 -1
- package/src/classifier/infer-render.ts +1 -1
- package/src/classifier/infer-tooling.ts +1 -1
- package/src/classifier/infer-trigger.ts +3 -12
- package/src/classifier/inspect.ts +7 -7
- package/src/coding-agent/self-check.ts +1 -1
- package/src/evaluation/cost-tracker.ts +39 -11
- package/src/harness/check/runtime-render/render-check.ts +4 -4
- package/src/harness/llm-router.ts +2 -2
- package/src/providers/bedrock.ts +2 -2
- package/src/tools/get-wire.ts +1 -1
- package/src/validation/primitives.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "UI generation harness for ggui — the LLM system prompt, multi-turn coding loop, and evaluation engine that turn a DataContract into a React component. Implements the UiGenerator contract from @ggui-ai/mcp-server-core; consumed by @ggui-ai/mcp-server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -188,9 +188,9 @@
|
|
|
188
188
|
"puppeteer-core": "^24.40.0",
|
|
189
189
|
"react": "^18.0.0 || ^19.0.0",
|
|
190
190
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
191
|
-
"@ggui-ai/mcp-server-core": "0.
|
|
192
|
-
"@ggui-ai/
|
|
193
|
-
"@ggui-ai/
|
|
191
|
+
"@ggui-ai/mcp-server-core": "0.6.0",
|
|
192
|
+
"@ggui-ai/project-config": "0.6.0",
|
|
193
|
+
"@ggui-ai/protocol": "0.6.0"
|
|
194
194
|
},
|
|
195
195
|
"peerDependenciesMeta": {
|
|
196
196
|
"@google/adk": {
|
|
@@ -229,11 +229,11 @@
|
|
|
229
229
|
"openai": "^6.29.0",
|
|
230
230
|
"typescript": "^5.0.0",
|
|
231
231
|
"zod": "^4.3.6",
|
|
232
|
-
"@ggui-ai/design": "0.
|
|
233
|
-
"@ggui-ai/gadgets": "0.
|
|
234
|
-
"@ggui-ai/sandbox": "0.
|
|
235
|
-
"@ggui-ai/
|
|
236
|
-
"@ggui-ai/
|
|
232
|
+
"@ggui-ai/design": "0.6.0",
|
|
233
|
+
"@ggui-ai/gadgets": "0.6.0",
|
|
234
|
+
"@ggui-ai/sandbox": "0.6.0",
|
|
235
|
+
"@ggui-ai/wire": "0.6.0",
|
|
236
|
+
"@ggui-ai/ui-visual-tester": "0.6.0"
|
|
237
237
|
},
|
|
238
238
|
"devDependencies": {
|
|
239
239
|
"@google/adk": "^0.5.0",
|
|
@@ -248,9 +248,9 @@
|
|
|
248
248
|
"react-dom": "^19.0.0",
|
|
249
249
|
"tsup": "^8.5.0",
|
|
250
250
|
"vitest": "^3.2.6",
|
|
251
|
-
"@ggui-ai/mcp-server-core": "0.
|
|
252
|
-
"@ggui-ai/
|
|
253
|
-
"@ggui-ai/
|
|
251
|
+
"@ggui-ai/mcp-server-core": "0.6.0",
|
|
252
|
+
"@ggui-ai/protocol": "0.6.0",
|
|
253
|
+
"@ggui-ai/project-config": "0.6.0"
|
|
254
254
|
},
|
|
255
255
|
"engines": {
|
|
256
256
|
"node": ">=20.0.0"
|
|
@@ -187,7 +187,7 @@ function selfCheckTier(source: string, contract?: DataContract): SelfCheckResult
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
if (contract?.propsSpec) {
|
|
190
|
-
const propNames = Object.keys(
|
|
190
|
+
const propNames = Object.keys(contract.propsSpec.properties ?? {});
|
|
191
191
|
for (const propName of propNames) {
|
|
192
192
|
if (!source.includes(propName)) {
|
|
193
193
|
warnings.push({
|
|
@@ -30,7 +30,6 @@ import type {
|
|
|
30
30
|
ContextSpec,
|
|
31
31
|
DataContract,
|
|
32
32
|
JsonObject,
|
|
33
|
-
JsonSchema,
|
|
34
33
|
JsonValue,
|
|
35
34
|
StreamSpec,
|
|
36
35
|
} from "@ggui-ai/protocol";
|
|
@@ -185,40 +184,30 @@ export function generateBoilerplate(
|
|
|
185
184
|
appGadgets?: readonly GadgetDescriptor[],
|
|
186
185
|
): string {
|
|
187
186
|
// ── Props interface from data contract (data only — NO action callbacks) ──
|
|
188
|
-
// Contract shape
|
|
187
|
+
// Contract shape (PropsSpec is ALWAYS the wrapper — never flat):
|
|
188
|
+
// { properties: { fieldName: { schema, required, description } } }
|
|
189
189
|
const propsFields: string[] = [];
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
jsonSchemaTypeToTs(schema as JsonSchema)
|
|
203
|
-
: "unknown";
|
|
204
|
-
const fullType = nullable ? `${tsType} | null` : tsType;
|
|
205
|
-
// Build comment: description + default value hint
|
|
206
|
-
const parts: string[] = [];
|
|
207
|
-
if (spec.description) parts.push(String(spec.description));
|
|
208
|
-
if (spec.default !== undefined) parts.push(`(default: ${JSON.stringify(spec.default)})`);
|
|
209
|
-
const desc = parts.length > 0 ? ` // ${parts.join(' ')}` : "";
|
|
210
|
-
propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
|
|
211
|
-
} else {
|
|
212
|
-
propsFields.push(` ${key}: ${typeof value === "string" ? value : "unknown"};`);
|
|
213
|
-
}
|
|
190
|
+
const propsProperties = contract?.propsSpec?.properties ?? {};
|
|
191
|
+
for (const [key, entry] of Object.entries(propsProperties)) {
|
|
192
|
+
const required = entry.required !== false;
|
|
193
|
+
const nullable = entry.schema.nullable === true;
|
|
194
|
+
const tsType = jsonSchemaTypeToTs(entry.schema);
|
|
195
|
+
const fullType = nullable ? `${tsType} | null` : tsType;
|
|
196
|
+
// Build comment: description + default value hint
|
|
197
|
+
const parts: string[] = [];
|
|
198
|
+
if (entry.description) parts.push(entry.description);
|
|
199
|
+
if (entry.default !== undefined) parts.push(`(default: ${JSON.stringify(entry.default)})`);
|
|
200
|
+
const desc = parts.length > 0 ? ` // ${parts.join(' ')}` : "";
|
|
201
|
+
propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
|
|
214
202
|
}
|
|
215
203
|
|
|
216
204
|
// ── Actions (useAction hooks — fire-and-forget to agent) ──
|
|
217
|
-
// Contract shape: { actionSpec: { actionName: { label, description, schema?, example?,
|
|
218
|
-
// ActionEntry.
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
// informational (use it for
|
|
205
|
+
// Contract shape: { actionSpec: { actionName: { label, description, schema?, example?, nextStep? } } }
|
|
206
|
+
// ActionEntry.nextStep: optional hint naming the MCP tool the AGENT
|
|
207
|
+
// should invoke on its next turn after this action fires. Actions have
|
|
208
|
+
// exactly one routing target — the agent; there is NO synchronous
|
|
209
|
+
// server-side dispatch. The name is informational here (use it for
|
|
210
|
+
// button labels, icons, copy).
|
|
222
211
|
const actionTypeAliases: string[] = [];
|
|
223
212
|
const actionHookCalls: string[] = [];
|
|
224
213
|
const actionReturnFields: string[] = [];
|
|
@@ -349,10 +349,10 @@ A \`DataContract\` declares everything a GguiSession exchanges with the outside
|
|
|
349
349
|
|
|
350
350
|
Props arrive via \`ggui_update\` and may be partial on first render. Stream channels start empty and fill over time. Context slots start at their declared default (often \`null\`). **Never assume a field exists before you read it.**
|
|
351
351
|
|
|
352
|
-
- **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.
|
|
352
|
+
- **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
|
|
353
353
|
- **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
|
|
354
354
|
- **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
|
|
355
|
-
- **Stream latest**: \`useStream\` returns \`{latest: T |
|
|
355
|
+
- **Stream latest**: \`useStream\` returns \`{latest: T | null, all: T[], isComplete: boolean}\`. The default \`all\` is \`[]\` so it's safe to map; \`latest\` is null until the first frame arrives — guard before reading \`.foo\`.
|
|
356
356
|
- **Stream reconciliation**: when a stream event carries an \`action\` discriminant (e.g. \`create | move | edit | delete\`), the channel is a CRUD feed — your handler MUST branch on EVERY value: append on \`create\`, drop on \`delete\`, replace-by-id on \`move\` / \`edit\`. Merging only the "edit" case silently loses created and deleted items. Reconcile into the SAME state that seeds from \`props\` (e.g. \`useState(() => props.tasks ?? [])\`) so the seed data and the live feed render as one list — and handle an event whose id is not yet present (a \`create\` for an unknown item) by inserting it, not ignoring it.
|
|
357
357
|
- **Loading state**: while data is still absent, render \`<Skeleton>\` placeholders — never a blank screen. \`<Skeleton variant="text" />\` for a text line, \`variant="circle"\` for an avatar slot, default \`rect\` for a block.
|
|
358
358
|
- **Empty state**: when a list or results array is empty, render \`<EmptyState title="…" description="…" />\` — a region that renders nothing when empty looks broken to the user.
|
|
@@ -212,8 +212,8 @@ export function extractWireImports(code: string): ReadonlySet<string> {
|
|
|
212
212
|
* slots. Contract-key typing matches the `@ggui-ai/protocol` canonical
|
|
213
213
|
* flat-map shape.
|
|
214
214
|
*
|
|
215
|
-
* `
|
|
216
|
-
* - `
|
|
215
|
+
* `agentCapabilities.tools` and `clientCapabilities` are NOT enumerated:
|
|
216
|
+
* - `agentCapabilities.tools` is a catalog the AGENT invokes (no component hook).
|
|
217
217
|
* Cross-references surface via `actionSpec[*].nextStep` and
|
|
218
218
|
* `streamSpec[*].source.tool`, already covered by `action` / `stream`.
|
|
219
219
|
* - `clientCapabilities` are declarations of browser-capability hooks
|
package/src/classifier/axes.ts
CHANGED
|
@@ -89,9 +89,9 @@ export type LayoutShape =
|
|
|
89
89
|
// =============================================================================
|
|
90
90
|
|
|
91
91
|
export type ToolingShape =
|
|
92
|
-
| "none" // contract has no agentCapabilities.tools and no clientCapabilities.
|
|
92
|
+
| "none" // contract has no agentCapabilities.tools and no clientCapabilities.gadgets
|
|
93
93
|
| "wired" // agentCapabilities.tools present (catalog referenced via actionSpec.nextStep / streamSpec.source.tool)
|
|
94
|
-
| "client" // clientCapabilities.
|
|
94
|
+
| "client" // clientCapabilities.gadgets present (browser-capability hooks the component imports)
|
|
95
95
|
| "both"; // contract exposes both catalogs
|
|
96
96
|
|
|
97
97
|
// =============================================================================
|
|
@@ -23,7 +23,7 @@ export interface ClassifyInput {
|
|
|
23
23
|
contract: ClassifierInput;
|
|
24
24
|
prompt?: string;
|
|
25
25
|
blueprint?: {
|
|
26
|
-
mechanic?:
|
|
26
|
+
mechanic?: import("@ggui-ai/protocol").ScreenMechanic;
|
|
27
27
|
layoutHint?: string;
|
|
28
28
|
};
|
|
29
29
|
}
|
|
@@ -39,7 +39,7 @@ export function classifyAxes(input: ClassifyInput): Classification {
|
|
|
39
39
|
const fetch = inferFetch(s);
|
|
40
40
|
const render = inferRender(s, prompt, blueprint);
|
|
41
41
|
const layout = inferLayout(s, prompt, blueprint);
|
|
42
|
-
const writeTrigger = inferWriteTrigger(s, prompt
|
|
42
|
+
const writeTrigger = inferWriteTrigger(s, prompt);
|
|
43
43
|
const tooling = inferTooling(s);
|
|
44
44
|
|
|
45
45
|
const vector: AxisVector = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// packages/ui-gen/src/classifier/infer-tooling.ts
|
|
2
2
|
//
|
|
3
3
|
// tooling axis — derived purely from contract presence of
|
|
4
|
-
// `agentCapabilities.tools` / `clientCapabilities.
|
|
4
|
+
// `agentCapabilities.tools` / `clientCapabilities.gadgets`.
|
|
5
5
|
// Describes the *direction* of tool usage present on the contract:
|
|
6
6
|
// wired = UI references an agent tool, client = UI declares a
|
|
7
7
|
// browser-capability gadget hook.
|
|
@@ -3,19 +3,16 @@
|
|
|
3
3
|
import type { AxisSource, WriteTrigger } from "./axes";
|
|
4
4
|
import type { ContractSignals } from "./inspect";
|
|
5
5
|
|
|
6
|
-
interface BlueprintHint {
|
|
7
|
-
mechanic?: string;
|
|
8
|
-
layoutHint?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
const DRAG_RX = /\b(drag|drop|dragging|draggable)\b/i;
|
|
12
7
|
const SWIPE_RX = /\b(swipe|swipes|gesture\s*stack)\b/i;
|
|
13
8
|
const KEYBOARD_RX = /\b(keyboard\s*shortcut|hotkey|keystroke|kbd)\b/i;
|
|
14
9
|
|
|
10
|
+
// The blueprint hint no longer participates: the mechanics that once
|
|
11
|
+
// carried a trigger signal (drag/swipe) were deleted from ScreenMechanic
|
|
12
|
+
// in draft-2026-06-12; static/live/form say nothing about triggers.
|
|
15
13
|
export function inferWriteTrigger(
|
|
16
14
|
s: ContractSignals,
|
|
17
15
|
prompt: string,
|
|
18
|
-
blueprint: BlueprintHint | undefined,
|
|
19
16
|
): { value: WriteTrigger; source: AxisSource } {
|
|
20
17
|
// If there are no actions at all, trigger is irrelevant — default to 'click'
|
|
21
18
|
// but mark source as 'default' to convey that the value isn't load-bearing.
|
|
@@ -23,12 +20,6 @@ export function inferWriteTrigger(
|
|
|
23
20
|
return { value: "click", source: "default" };
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
// Blueprint mechanic — explicit authored hint
|
|
27
|
-
if (blueprint?.mechanic === "drag")
|
|
28
|
-
return { value: "drag", source: "blueprint" };
|
|
29
|
-
if (blueprint?.mechanic === "swipe")
|
|
30
|
-
return { value: "swipe", source: "blueprint" };
|
|
31
|
-
|
|
32
23
|
// Prompt signals
|
|
33
24
|
if (DRAG_RX.test(prompt)) return { value: "drag", source: "prompt" };
|
|
34
25
|
if (SWIPE_RX.test(prompt)) return { value: "swipe", source: "prompt" };
|
|
@@ -171,7 +171,7 @@ function hasGeoCoordsRecursive(node: unknown, depth = 0): boolean {
|
|
|
171
171
|
|
|
172
172
|
export interface ActionEntryInfo {
|
|
173
173
|
name: string;
|
|
174
|
-
|
|
174
|
+
nextStep?: string;
|
|
175
175
|
example?: Record<string, unknown>;
|
|
176
176
|
/** Top-level keys in example whose value is scalar (string/number/boolean). */
|
|
177
177
|
scalarKeys: string[];
|
|
@@ -299,8 +299,8 @@ export function inferStreamKindFromSchema(
|
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
301
|
* Per-tool projection of the contract's `agentCapabilities.tools`
|
|
302
|
-
* catalog. Also reused for `clientCapabilities.
|
|
303
|
-
* (always with empty `requestKeys`) —
|
|
302
|
+
* catalog. Also reused for `clientCapabilities.gadgets` entries
|
|
303
|
+
* (always with empty `requestKeys`) — gadgets are pure declaration.
|
|
304
304
|
*/
|
|
305
305
|
export interface AgentToolInfo {
|
|
306
306
|
name: string;
|
|
@@ -319,8 +319,8 @@ export interface ContractSignals {
|
|
|
319
319
|
* fetch-axis classification. */
|
|
320
320
|
agentTools: AgentToolInfo[];
|
|
321
321
|
/**
|
|
322
|
-
* Binding names declared in `clientCapabilities.
|
|
323
|
-
* `requestKeys` field is always empty —
|
|
322
|
+
* Binding names declared in `clientCapabilities.gadgets`. The
|
|
323
|
+
* `requestKeys` field is always empty — gadgets are
|
|
324
324
|
* declaration-only.
|
|
325
325
|
*/
|
|
326
326
|
clientCapabilities: AgentToolInfo[];
|
|
@@ -405,7 +405,7 @@ export function inspect(contract: ClassifierInput): ContractSignals {
|
|
|
405
405
|
(contract.actionSpec as Record<string, unknown> | undefined) ?? {};
|
|
406
406
|
const actions: ActionEntryInfo[] = [];
|
|
407
407
|
for (const [name, action] of Object.entries(actionsMap)) {
|
|
408
|
-
const a = action as { example?: Record<string, unknown>;
|
|
408
|
+
const a = action as { example?: Record<string, unknown>; nextStep?: string };
|
|
409
409
|
const scalarKeys = scalarKeyCount(a?.example);
|
|
410
410
|
const allKeys = allTopLevelKeys(a?.example);
|
|
411
411
|
const referencedEntities = referencedEntitiesForPayload(
|
|
@@ -415,7 +415,7 @@ export function inspect(contract: ClassifierInput): ContractSignals {
|
|
|
415
415
|
);
|
|
416
416
|
actions.push({
|
|
417
417
|
name,
|
|
418
|
-
|
|
418
|
+
nextStep: a?.nextStep,
|
|
419
419
|
example: a?.example,
|
|
420
420
|
scalarKeys,
|
|
421
421
|
allKeys,
|
|
@@ -72,7 +72,7 @@ export async function runCodingAgentSelfCheck(
|
|
|
72
72
|
// will not fire the wire at runtime — but `pnpm typecheck` won't flag the
|
|
73
73
|
// absence (a missing call is a missing side effect, not a type error).
|
|
74
74
|
// This rule closes that gap at the deterministic self_check tier.
|
|
75
|
-
// `
|
|
75
|
+
// `agentCapabilities.tools` (catalog declaration, NOT a hook) and `clientCapabilities`
|
|
76
76
|
// (vendor-package hooks, not @ggui-ai/wire) are intentionally excluded.
|
|
77
77
|
if (contract) {
|
|
78
78
|
try {
|
|
@@ -4,16 +4,44 @@
|
|
|
4
4
|
// Records LLM token usage across all calls in a generation pipeline
|
|
5
5
|
// and enforces an optional budget ceiling.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
import { MODEL_REGISTRY, type ModelId } from '@ggui-ai/protocol';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a bare wire model id (what the generation pipeline reports —
|
|
11
|
+
* `gpt-5.6-luna`, `claude-haiku-4-5-20251001`) to a LiteLLM-keyed
|
|
12
|
+
* {@link MODEL_REGISTRY} entry (`openai/gpt-5.6-luna`,
|
|
13
|
+
* `anthropic/claude-haiku-4-5`).
|
|
14
|
+
*
|
|
15
|
+
* This table used to hand-maintain its own per-1K prices, and drifted:
|
|
16
|
+
* on 2026-07-31 four of its six rows were wrong (Haiku 4.5 20% low,
|
|
17
|
+
* gemini-3-flash-preview 5-7x low, gpt-5.4-mini ~3x low, Luna 5x high
|
|
18
|
+
* after OpenAI's cut). Deriving from the registry — which the pod's
|
|
19
|
+
* pricing test now pins against the vendored LiteLLM snapshot — means
|
|
20
|
+
* one place to update and one place that can be wrong.
|
|
21
|
+
*
|
|
22
|
+
* Resolution order mirrors the benchmark harness's
|
|
23
|
+
* `resolveJudgeCostModelId`: exact key, unique `/<model>` suffix, then
|
|
24
|
+
* the same suffix match with a trailing `-YYYYMMDD` date pin stripped.
|
|
25
|
+
*/
|
|
26
|
+
function resolveRegistryId(model: string): ModelId | null {
|
|
27
|
+
const keys = Object.keys(MODEL_REGISTRY) as ModelId[];
|
|
28
|
+
if ((keys as string[]).includes(model)) return model as ModelId;
|
|
29
|
+
const bySuffix = keys.find((id) => id.endsWith(`/${model}`));
|
|
30
|
+
if (bySuffix) return bySuffix;
|
|
31
|
+
const undated = model.replace(/-\d{8}$/, '');
|
|
32
|
+
return keys.find((id) => id.endsWith(`/${undated}`)) ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Fallback when a model has no registry entry — Sonnet-class rates. */
|
|
36
|
+
const FALLBACK_PER_1K = { input: 0.003, output: 0.015 };
|
|
37
|
+
|
|
38
|
+
/** Per-1K input/output USD for a bare wire model id. */
|
|
39
|
+
export function pricePer1k(model: string): { input: number; output: number } {
|
|
40
|
+
const id = resolveRegistryId(model);
|
|
41
|
+
if (id === null) return FALLBACK_PER_1K;
|
|
42
|
+
const { costs } = MODEL_REGISTRY[id];
|
|
43
|
+
return { input: costs.inputPer1M / 1000, output: costs.outputPer1M / 1000 };
|
|
44
|
+
}
|
|
17
45
|
|
|
18
46
|
export class CostTracker {
|
|
19
47
|
private totalCost = 0;
|
|
@@ -21,7 +49,7 @@ export class CostTracker {
|
|
|
21
49
|
constructor(private maxBudget: number | null) {}
|
|
22
50
|
|
|
23
51
|
record(model: string, inputTokens: number, outputTokens: number): number {
|
|
24
|
-
const prices =
|
|
52
|
+
const prices = pricePer1k(model);
|
|
25
53
|
const cost = (inputTokens / 1000) * prices.input + (outputTokens / 1000) * prices.output;
|
|
26
54
|
this.totalCost += cost;
|
|
27
55
|
return cost;
|
|
@@ -475,8 +475,8 @@ export async function runRenderCheck(
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
// ── Check 3: Agent-tool wiring — RETIRED ──────────────────────────
|
|
478
|
-
// `useWiredTool` is retired. The contract's `
|
|
479
|
-
// catalog of tools the AGENT invokes — there is no component hook
|
|
478
|
+
// `useWiredTool` is retired. The contract's `agentCapabilities.tools`
|
|
479
|
+
// is a catalog of tools the AGENT invokes — there is no component hook
|
|
480
480
|
// surface. Cross-refs from actionSpec[*].nextStep /
|
|
481
481
|
// streamSpec[*].source.tool ride through checks 2 (action wiring)
|
|
482
482
|
// and 6 (stream re-render); the catalog itself has no
|
|
@@ -965,8 +965,8 @@ async function checkActionWiring(input: CheckActionInput): Promise<RenderCheckIs
|
|
|
965
965
|
}
|
|
966
966
|
|
|
967
967
|
// `checkWiredToolWiring` is retired — `useWiredTool` was removed; the
|
|
968
|
-
// contract's `
|
|
969
|
-
// hook to verify). Cross-refs from `actionSpec[*].nextStep` and
|
|
968
|
+
// contract's `agentCapabilities.tools` is a catalog the AGENT invokes
|
|
969
|
+
// (no component hook to verify). Cross-refs from `actionSpec[*].nextStep` and
|
|
970
970
|
// `streamSpec[*].source.tool` ride through the action / stream check
|
|
971
971
|
// arms in `runRenderCheck` instead.
|
|
972
972
|
|
|
@@ -876,7 +876,7 @@ export class OpenAIAgent extends LLMAgent {
|
|
|
876
876
|
*/
|
|
877
877
|
function interactionText(interaction: Interactions.Interaction): string {
|
|
878
878
|
let text = '';
|
|
879
|
-
for (const step of interaction.steps) {
|
|
879
|
+
for (const step of interaction.steps ?? []) {
|
|
880
880
|
if (step.type === 'model_output') {
|
|
881
881
|
for (const content of step.content ?? []) {
|
|
882
882
|
if (content.type === 'text') {
|
|
@@ -892,7 +892,7 @@ function interactionText(interaction: Interactions.Interaction): string {
|
|
|
892
892
|
function functionCallSteps(
|
|
893
893
|
interaction: Interactions.Interaction,
|
|
894
894
|
): Interactions.FunctionCallStep[] {
|
|
895
|
-
return interaction.steps.filter(
|
|
895
|
+
return (interaction.steps ?? []).filter(
|
|
896
896
|
(s): s is Interactions.FunctionCallStep => s.type === 'function_call',
|
|
897
897
|
);
|
|
898
898
|
}
|
package/src/providers/bedrock.ts
CHANGED
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
* model id namespaces:
|
|
57
57
|
*
|
|
58
58
|
* - Direct API: `claude-haiku-4-5`, `claude-opus-4-7`, etc.
|
|
59
|
-
* - Bedrock foundation models: `anthropic.claude-
|
|
60
|
-
* - Bedrock cross-region inference profiles: `us.anthropic.claude-
|
|
59
|
+
* - Bedrock foundation models: `anthropic.claude-sonnet-4-6`
|
|
60
|
+
* - Bedrock cross-region inference profiles: `us.anthropic.claude-haiku-4-5-20251001-v1:0`
|
|
61
61
|
*
|
|
62
62
|
* The adapter passes whatever `request.model` contains straight to
|
|
63
63
|
* Bedrock — translation lives in the caller (model picker / deployment
|
package/src/tools/get-wire.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* AUTO-GENERATED from @ggui-ai/wire JSDoc.
|
|
3
3
|
* Do not edit manually. Run: pnpm --filter @ggui-ai/wire generate:docs
|
|
4
4
|
*/
|
|
5
|
-
export const WIRE_DOCUMENTATION = "# ggui Wire Hooks Reference\n\n> Wire hooks connect generated UI components to agent communication.\n> They are pre-imported in the boilerplate — use them directly.\n> All hooks must be called inside a GguiWireProvider (handled automatically by the renderer).\n\nImport: `import { useAction, useStream } from '@ggui-ai/wire'`\n\n## Communication Hooks\n\n\n\nThese are the wire primitives for component-agent communication. `useWiredTool` retired 2026-05-11 —
|
|
5
|
+
export const WIRE_DOCUMENTATION = "# ggui Wire Hooks Reference\n\n> Wire hooks connect generated UI components to agent communication.\n> They are pre-imported in the boilerplate — use them directly.\n> All hooks must be called inside a GguiWireProvider (handled automatically by the renderer).\n\nImport: `import { useAction, useStream } from '@ggui-ai/wire'`\n\n## Communication Hooks\n\n\n\nThese are the wire primitives for component-agent communication. `useWiredTool` retired 2026-05-11 — agentCapabilities.tools is a catalog the AGENT invokes, not a component hook surface; user gestures use `useAction(name)` and the optional `nextStep` field on the action entry names the tool the agent SHOULD invoke next.\n\n### useAction\n\nFire an action to the agent. Fire-and-forget — no response, no pending state.\n\nEvery action is an event: it lands on the GguiSession's consume buffer and\nthe agent receives it on its next turn (via `ggui_consume`). The action\nentry's optional `nextStep` names the tool the agent SHOULD invoke in\nresponse — advisory only; the agent owns the call decision. Component code\nis identical either way: call `useAction(name)(payload)` and treat\n`nextStep` as informational (use it to inform button labels, icons, copy).\n\nRUNTIME DEDUP (backstop, not a feature). Same-`(name, payload)` calls within\none event-loop task are coalesced — the first wins, subsequent duplicates\nare suppressed. This is the structural defense against LLM-generated\nnested-interactive components where a Checkbox `onChange` and an outer\n`Card as={Clickable}` `onClick` both wire to the same `useAction` binding;\nthe inner gesture bubbles to the outer handler, so without dedup one user\nclick would fire the action twice (a toggle would run back-to-back and the\nuser's change would disappear). **Do not rely on this as a feature** — wire\neach `useAction` callback to exactly ONE interactive surface; the dedup\nexists only because the LLM's source code can be wrong in subtle ways and\nthe runtime is the only place that can see the actual event-bubble path.\nSee `dispatch-dedup.ts` for the full failure-mode rationale.\n\nNEVER SILENT. When the dedup fires, a `console.warn` is emitted in BOTH dev\nand prod with the full diagnostic. The suppression is always visible in\nbrowser DevTools; operators investigating a \"the second click does nothing\"\nreport see the warning immediately. A structured observer also exists as\n`WireConfig.onDispatchSuppressed`, but the first-party renderers never set\nit — it is reachable only from a hand-built `WireConfig` (see its\ndocstring), so the console signal is what operators get in practice.\n\n**Signature:** `useAction<T = unknown>(actionName: string): (data: T) => void`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| actionName | `string` | Action name from the action contract |\n\n**Returns:** `(data: T) => void`\n\n**Example:**\n```tsx\nconst submitForm = useAction<{name: string; email: string}>('formSubmit');\n\n// In JSX:\n<Button onClick={() => submitForm({ name, email })}>Submit</Button>\n```\n\n\n### useStream\n\nSubscribe to deliveries on a named stream channel.\n\nHonors the channel's per-delivery `mode` ('append' vs 'replace')\nand the optional `complete` terminal marker. Channels declared\n`mode: 'replace'` on the spec typically emit every delivery with\n`mode: 'replace'` — this hook folds them into a single-latest\nvalue without accumulating history.\n\n**Signature:** `useStream<T = unknown>(channelName: string): StreamResult<T>`\n\n**Parameters:**\n\n| Param | Type | Description |\n|-------|------|-------------|\n| channelName | `string` | Channel name from the GguiSession's streamSpec |\n\n**Returns:** `StreamResult<T>`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| latest | `T \\| null` | Most recent payload delivered on this channel, or null if none received yet. |\n| all | `T[]` | All payloads accumulated on this channel. - `mode: 'append'` deliveries are pushed to the tail (continuous stream). - `mode: 'replace'` deliveries collapse `all` to a single-element array containing the latest payload — matching the channel's \"full replacement\" semantics. |\n| isComplete | `boolean` | Truthy after the channel has delivered an envelope with `complete: true`. Subscribers flip into a \"channel closed\" rendering state based on this signal; further deliveries on a completed channel are still accumulated, since the underlying wire doesn't enforce quiescence. |\n\n**Example:**\n```tsx\nconst progress = useStream<{ percent: number; message: string }>('progress');\n\n// In JSX:\n{progress.latest && (\n <Progress value={progress.latest.percent} />\n)}\n<Text>{progress.all.length} updates received</Text>\n```\n\n\n## Context Hooks\n\n\n\nRead-only access to render, app, and auth context.\n\n### useAuth\n\nRead-only auth context. Token excluded — auth is added server-side.\n\n**Signature:** `useAuth(): AuthInfo`\n\n**Returns:** `AuthInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| userId | `string` | userId |\n| isAuthenticated | `boolean` | isAuthenticated |\n\n**Example:**\n```tsx\nconst auth = useAuth();\n\n// In JSX:\n{auth.isAuthenticated\n ? <Text>Welcome, user {auth.userId}</Text>\n : <Text>Please sign in</Text>\n}\n```\n\n\n### useApp\n\nRead-only app metadata.\n\n**Signature:** `useApp(): AppInfo`\n\n**Returns:** `AppInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| appId | `string` | appId |\n| appName | `string` | appName |\n| appDescription | `string` | appDescription |\n| appIcon | `string` | appIcon |\n\n**Example:**\n```tsx\nconst app = useApp();\n\n// In JSX:\n<Heading>{app.appName}</Heading>\n{app.appDescription && <Text>{app.appDescription}</Text>}\n```\n\n\n### useRender\n\nRead-only render context with connection status.\n\n**Signature:** `useRender(): GguiSessionInfo`\n\n**Returns:** `GguiSessionInfo`\n\n| Property | Type | Description |\n|----------|------|-------------|\n| sessionId | `string` | sessionId |\n| isConnected | `boolean` | isConnected |\n\n**Example:**\n```tsx\nconst render = useRender();\n\n// In JSX:\n<Badge variant={render.isConnected ? 'success' : 'error'}>\n {render.isConnected ? 'Connected' : 'Disconnected'}\n</Badge>\n```\n\n\n## Provider\n\n\n\nThe provider is set up automatically by the renderer. Generated components do not need to wrap themselves in a provider.\n\n### GguiWireProvider\n\nReact context provider that injects WireConfig for all wire hooks.\n\n**Props:**\n\n| Prop | Type | Description |\n|------|------|-------------|\n| config | `WireConfig` | config |\n| children | `ReactNode` | children |\n\n**Example:**\n```tsx\n<GguiWireProvider config={wireConfig}>\n <YourComponent />\n</GguiWireProvider>\n```\n\n\n## Internal: WireConfig\n\n\n\nThis is the configuration object passed to GguiWireProvider. Generated components do not interact with this directly — it is provided by the renderer.\n\n### WireConfig\n\nConfiguration injected by the provider — the renderer inside the\niframe.\n\nEvery method is typed against the contract generic `T` so typed\ncallers get compile-time enforcement:\n - `dispatch(name, data)` — `name` MUST be a declared actionSpec\n key; `data` MUST satisfy that action's schema.\n - `subscribe(channel, handler)` — same discipline for streamSpec.\n\nUntyped callers (`T = DataContract` default) degrade to the broad\nshape via the conditional `WireDispatchData` / `WireStreamPayload`\naliases — no call-site break.\n\nThe contract's `agentCapabilities.tools` catalog declares tools the AGENT\ninvokes (not the component); user gestures fire via\n`dispatch(name, data)` and the optional `nextStep` field on the\naction entry names the tool the agent SHOULD invoke next.\n\nThe renderer mounts exactly ONE GguiSession per iframe — `render.sessionId`\nis the stable identity the WireProvider was constructed with. No\nper-item scoping factory; with a one-GguiSession-per-mount lifecycle,\n\"scope\" collapses to identity.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| app | `{ readonly appId: string; readonly appName: string; readonly appDescription?: string; readonly appIcon?: string; }` | app |\n| render | `{ readonly sessionId: string; readonly isConnected: boolean; }` | render |\n| auth | `{ readonly userId?: string; readonly isAuthenticated: boolean; }` | auth |\n| dispatch | `<N extends string>( actionName: N, data: WireDispatchData<T, N>, ) => void` | Fire an action to the agent (fire-and-forget over WS). Typed callers get compile-time checked `name` + `data`; untyped callers (`T = DataContract`) keep the broad shape. |\n| subscribe | `<N extends string>( channelName: N, handler: (delivery: StreamDelivery<WireStreamPayload<T, N>>) => void, ) => () => void` | Subscribe to deliveries on a named stream channel. |\n| onDispatchSuppressed | `(info: DispatchSuppressedInfo) => void` | Optional structured observability for `useAction`'s task-scoped duplicate-dispatch suppression. Fires alongside the always-on `console.warn` whenever the runtime coalesces a same-(name, payload) re-dispatch within one event-loop task — the nested- interactive double-fire backstop. Reachability constraint: the first-party renderers do NOT set this field — `buildRootWireConfig` (`@ggui-ai/iframe-runtime`) and `<GguiRender>`'s internal config both omit it. It fires only when a host hand-builds a complete `WireConfig` and mounts `GguiWireProvider` itself. On every first-party render path the `console.warn` is the sole suppression signal. |\n";
|