@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.
Files changed (54) hide show
  1. package/dist/adapters/base.js +1 -0
  2. package/dist/adapters/base.js.map +1 -1
  3. package/dist/adapters/generation-dispatch.js +60 -51
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js +13 -2
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/advanced/index.js +61 -51
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +1 -1
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.js +13 -24
  12. package/dist/boilerplate.js.map +1 -1
  13. package/dist/check/index.d.ts +2 -2
  14. package/dist/check/index.js.map +1 -1
  15. package/dist/classifier/index.d.ts +14 -17
  16. package/dist/classifier/index.js +3 -7
  17. package/dist/classifier/index.js.map +1 -1
  18. package/dist/evaluation/index.js +12 -4
  19. package/dist/evaluation/index.js.map +1 -1
  20. package/dist/harness/check/runtime-render/index.js.map +1 -1
  21. package/dist/harness/index.js +25 -28
  22. package/dist/harness/index.js.map +1 -1
  23. package/dist/harness/runtime.js +14 -25
  24. package/dist/harness/runtime.js.map +1 -1
  25. package/dist/harness/types-public.js +13 -24
  26. package/dist/harness/types-public.js.map +1 -1
  27. package/dist/index.js +62 -51
  28. package/dist/index.js.map +1 -1
  29. package/dist/provider-adapter-contract.js +12 -3
  30. package/dist/provider-adapter-contract.js.map +1 -1
  31. package/dist/providers/index.d.ts +2 -2
  32. package/dist/providers/index.js.map +1 -1
  33. package/dist/validation/index.d.ts +1 -1
  34. package/dist/validation/index.js +5 -0
  35. package/dist/validation/index.js.map +1 -1
  36. package/package.json +12 -12
  37. package/src/blueprint-validator.ts +1 -1
  38. package/src/boilerplate/generate.ts +20 -31
  39. package/src/boilerplate/system-prompt.ts +2 -2
  40. package/src/check/extract-wire-calls.ts +2 -2
  41. package/src/classifier/axes.ts +2 -2
  42. package/src/classifier/classifier.ts +2 -2
  43. package/src/classifier/infer-layout.ts +1 -1
  44. package/src/classifier/infer-render.ts +1 -1
  45. package/src/classifier/infer-tooling.ts +1 -1
  46. package/src/classifier/infer-trigger.ts +3 -12
  47. package/src/classifier/inspect.ts +7 -7
  48. package/src/coding-agent/self-check.ts +1 -1
  49. package/src/evaluation/cost-tracker.ts +39 -11
  50. package/src/harness/check/runtime-render/render-check.ts +4 -4
  51. package/src/harness/llm-router.ts +2 -2
  52. package/src/providers/bedrock.ts +2 -2
  53. package/src/tools/get-wire.ts +1 -1
  54. 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.5.0-rc.2",
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.5.0-rc.2",
192
- "@ggui-ai/protocol": "0.5.0-rc.2",
193
- "@ggui-ai/project-config": "0.5.0-rc.2"
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.5.0-rc.2",
233
- "@ggui-ai/gadgets": "0.5.0-rc.2",
234
- "@ggui-ai/sandbox": "0.5.0-rc.2",
235
- "@ggui-ai/ui-visual-tester": "0.5.0-rc.2",
236
- "@ggui-ai/wire": "0.5.0-rc.2"
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.5.0-rc.2",
252
- "@ggui-ai/project-config": "0.5.0-rc.2",
253
- "@ggui-ai/protocol": "0.5.0-rc.2"
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((contract.propsSpec as { shape?: Record<string, unknown> }).shape ?? {});
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: { properties: { fieldName: { schema, required, description } } }
187
+ // Contract shape (PropsSpec is ALWAYS the wrapper — never flat):
188
+ // { properties: { fieldName: { schema, required, description } } }
189
189
  const propsFields: string[] = [];
190
- const propsData = contract?.propsSpec as JsonObject | undefined;
191
- // Unwrap: contract.propsSpec may be { properties: { ... } } or flat { field: spec }
192
- const propsProperties = (propsData?.properties as JsonObject) ?? propsData ?? {};
193
- for (const [key, value] of Object.entries(propsProperties)) {
194
- if (typeof value === "object" && value !== null) {
195
- const spec = value as JsonObject;
196
- const schema = spec.schema as JsonObject | undefined;
197
- const required = spec.required !== false;
198
- const nullable = schema?.nullable === true;
199
- const tsType = schema
200
- ? // JsonSchema extends JsonObject, so this is a plain structural
201
- // downcast (no `unknown` erasure) of already-shaped contract data.
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?, tool? } } }
218
- // ActionEntry.dispatch: when `kind === 'tool'`, this action routes to an
219
- // MCP tool registered on this server (synchronous dispatch). Wire useAction
220
- // normally — the platform handles tool dispatch. The tool name is
221
- // informational (use it for button labels, icons, copy).
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.history, stream.latest, etc.
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 | undefined, history: T[]}\`. The default \`history\` is \`[]\` so it's safe to map; \`latest\` is undefined until the first frame arrives — guard before reading \`.foo\`.
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
- * `agentTools` and `clientCapabilities` are NOT enumerated:
216
- * - `agentTools` is a catalog the AGENT invokes (no component hook).
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
@@ -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.libraries
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.libraries present (browser-capability hooks the component imports)
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?: string;
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, blueprint);
42
+ const writeTrigger = inferWriteTrigger(s, prompt);
43
43
  const tooling = inferTooling(s);
44
44
 
45
45
  const vector: AxisVector = {
@@ -4,7 +4,7 @@ import type { AxisSource, LayoutShape } from "./axes";
4
4
  import type { ContractSignals } from "./inspect";
5
5
 
6
6
  interface BlueprintHint {
7
- mechanic?: string;
7
+ mechanic?: import("@ggui-ai/protocol").ScreenMechanic;
8
8
  layoutHint?: string;
9
9
  }
10
10
 
@@ -4,7 +4,7 @@ import type { AxisSource, RenderShape } from "./axes";
4
4
  import type { ContractSignals } from "./inspect";
5
5
 
6
6
  interface BlueprintHint {
7
- mechanic?: string;
7
+ mechanic?: import("@ggui-ai/protocol").ScreenMechanic;
8
8
  layoutHint?: string;
9
9
  }
10
10
 
@@ -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.libraries`.
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
- tool?: string;
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.libraries` entries
303
- * (always with empty `requestKeys`) — libraries are pure declaration.
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.libraries`. The
323
- * `requestKeys` field is always empty — libraries are
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>; tool?: string };
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
- tool: a?.tool,
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
- // `agentTools` (catalog declaration, NOT a hook) and `clientCapabilities`
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
- const PRICE_PER_1K_TOKENS: Record<string, { input: number; output: number }> = {
8
- 'claude-sonnet-4-6': { input: 0.003, output: 0.015 },
9
- 'claude-haiku-4-5-20251001': { input: 0.0008, output: 0.004 },
10
- 'gemini-3-flash-preview': { input: 0.0001, output: 0.0004 },
11
- // Gemini 3.5 Flash-Lite — $0.30/$2.50 per MTok (ai.google.dev/gemini-api/docs/pricing).
12
- 'gemini-3.5-flash-lite': { input: 0.0003, output: 0.0025 },
13
- 'gpt-5.4-mini': { input: 0.0003, output: 0.0012 },
14
- // $1.00 in / $6.00 out per MTok (OpenAI list, 2026-07) → per-1K.
15
- 'gpt-5.6-luna': { input: 0.001, output: 0.006 },
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 = PRICE_PER_1K_TOKENS[model] ?? { input: 0.003, output: 0.015 };
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 `agentTools` is a
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 `agentTools` is a catalog the AGENT invokes (no component
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
  }
@@ -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-3-5-sonnet-20241022-v2:0`
60
- * - Bedrock cross-region inference profiles: `us.anthropic.claude-3-5-sonnet-20241022-v2:0`
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
@@ -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 — agentTools 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 `agentTools` 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";
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";
@@ -60,6 +60,8 @@ export const VALID_PRIMITIVES = [
60
60
  'EmptyState',
61
61
  'Stat',
62
62
  'Stepper',
63
+ 'Markdown',
64
+ 'MarkdownInline',
63
65
  'Header',
64
66
  'Sidebar',
65
67
  'CardGrid',