@agentable-ui/genui 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -1,66 +1,110 @@
1
1
  # @agentable-ui/genui
2
2
 
3
- Generative UI for Agentable: the agent describes what to render, you keep control of what can be rendered.
3
+ Render model-generated interfaces using components you allow. Use `Renderer` for text streamed by your application, or `AgentSurface` for an in-page agent that calls a `present` tool.
4
+
5
+ ## Install
4
6
 
5
7
  ```bash
6
8
  npm install @agentable-ui/genui @agentable-ui/ui @agentable-ui/react @agentable-ui/core
7
9
  ```
8
10
 
11
+ ## Render a program
12
+
13
+ This complete example renders a static program. Replace `program` with the accumulated text from your model stream and set `isStreaming` while chunks arrive.
14
+
9
15
  ```tsx
10
- import { AgentSurface } from "@agentable-ui/genui";
16
+ import { Renderer } from "@agentable-ui/genui";
11
17
  import { uiCatalog } from "@agentable-ui/ui";
18
+ import "@agentable-ui/ui/styles.css";
19
+
20
+ const program = `root = Card("Order summary", [total])
21
+ total = Stat("Revenue", "$48.2k")`;
12
22
 
13
- <AgentSurface components={uiCatalog} name="present" maxNodes={60} />;
23
+ export default function Summary() {
24
+ return <Renderer components={uiCatalog} response={program} isStreaming={false} />;
25
+ }
14
26
  ```
15
27
 
16
- That declares a `present` tool whose input schema is generated from the catalog. The agent sends a UI spec; the component tree it can produce is exactly the catalog you passed and nothing else.
28
+ `response` is the **full text received so far**, not just the newest chunk. Complete lines render as they arrive; interactions stay disabled until streaming finishes. `onError` receives structured parsing and validation issues. Your app owns model requests, credentials, and streaming transport.
17
29
 
18
- ## Why one tool
30
+ ## Give a browser agent a surface
19
31
 
20
- Exposing a tool per component burns the budget and lets the agent invent markup. A single `present` tool with a generated schema keeps the cost flat and the output constrained.
32
+ ```tsx
33
+ import { AgentableProvider } from "@agentable-ui/react";
34
+ import { AgentSurface } from "@agentable-ui/genui";
35
+ import { uiCatalog } from "@agentable-ui/ui";
36
+ import "@agentable-ui/ui/styles.css";
37
+
38
+ export default function App() {
39
+ return (
40
+ <AgentableProvider>
41
+ <AgentSurface components={uiCatalog} name="present" maxNodes={60} />
42
+ </AgentableProvider>
43
+ );
44
+ }
45
+ ```
21
46
 
22
- ## Validation
47
+ The surface declares one `present` tool with a schema derived from its currently allowed catalog. An agent can supply either a program or a JSON tree:
23
48
 
24
- `validateSpec(spec, catalog, allowed, opts)` runs before anything renders and rejects unknown components, bad prop shapes, specs deeper than `maxDepth` (default 6) and specs larger than `maxNodes` (default 60). Failures come back to the agent as structured issues it can correct, not as a thrown error.
49
+ ```json
50
+ {"program":"root = Card(\"Hello\")"}
51
+ ```
25
52
 
26
- - `catalogSchema(catalog, allowed)` — the generated JSON Schema for the `present` tool.
27
- - `renderSpec(spec, catalog)` — render a validated spec yourself.
53
+ ```json
54
+ {"root":{"component":"Card","props":{"title":"Hello"}}}
55
+ ```
28
56
 
29
- ## Interactive surfaces
57
+ A single tool avoids one tool per visual component; its schema size still depends on the catalog. Use a focused catalog and inspect the result in devtools.
30
58
 
31
- Nodes can do more than display props:
59
+ ## Bind data and handle events
32
60
 
33
- ```jsonc
34
- {
35
- "component": "RecordTable", "id": "orders",
36
- "bind": { "rows": "data.paid" }, // read app data
37
- "on": { "select": [{ "type": "set", "path": "ui.selected", "value": "{{value}}" }] }
38
- }
39
- {
40
- "component": "Button", "when": "ui.selected", "props": { "label": "Refund this order" },
41
- "on": { "press": [{ "type": "tool", "name": "refund_order", "args": { "id": "{{ui.selected.id}}" }, "into": "ui.last" }] }
42
- }
61
+ - `$data.*` in Lang (or `data.*` in JSON bindings) reads application data passed through `data`. Generated UI cannot write to it. Read paths can filter and count arrays: `$data.items[done=true].length`.
62
+ - `$ui.*` is local interface state. Actions can set, merge, push, remove, increment, or toggle it.
63
+ - A catalog entry maps event names to callback props, for example `events: { press: "onPress" }`.
64
+ - `when` conditionally shows a node. `@Say` forwards a message to `onMessage`.
65
+
66
+ For example:
67
+
68
+ ```text
69
+ root = Stack([input, greeting])
70
+ input = TextInput("Name", value: $ui.name, change: @Set($ui.name, $event))
71
+ greeting = Text("Thanks for sharing your name.", when: $ui.name)
43
72
  ```
44
73
 
45
- - `data.*` is what you pass as `<AgentSurface data={...}>`. It is read-only to generated UI and re-renders when it changes.
46
- - `ui.*` belongs to the surface. Actions (`set`, `merge`, `push`, `remove`, `increment`, `toggle`) may only write here.
47
- - `tool` actions call registered tools through `agentable.invokeCapability`, so confirmation, caching and invalidation work exactly as for an agent. Only tools an agent could reach (tier 0 or 1) are callable; narrow them with `tools={[...]}` or turn them off with `tools={false}`.
48
- - `present({ ops })` patches the current tree by node id (`add`, `update`, `remove`, `set`) instead of resending it.
49
- - With `data` set, a read-only `<name>_state` tool returns the shape of `data`, the current `ui` state and the tree.
50
- - Catalog entries declare events as `events: { press: "onPress" }`: event name to callback prop, whose first argument becomes `{{value}}`.
74
+ Tool routing differs by host:
75
+
76
+ | Host | Tool configuration |
77
+ |---|---|
78
+ | `Renderer` | `tools={{ tool_name: callback }}` supplies callbacks. Missing names can fall back to a mounted Agentable registry. Direct callbacks follow your application's policies. |
79
+ | `AgentSurface` | `tools={["tool_name"]}` narrows callable registry tools; `tools={false}` disables tool actions. Registry calls retain confirmation, caching, and invalidation. |
51
80
 
52
- ## Catalog entries
81
+ `AgentSurface` can also patch the current tree with `present({ ops })`, using node ids and `add`, `update`, `remove`, or `set` operations. When `data` is supplied, a read-only `<name>_state` tool reports the data shape, UI state, and current tree; set `stateTool={false}` to omit it.
82
+
83
+ ## Validation and custom catalogs
84
+
85
+ ```tsx
86
+ import { Stat } from "@agentable-ui/ui";
53
87
 
54
- ```ts
55
88
  const catalog = {
56
89
  Stat: {
57
90
  component: Stat,
58
- props: z.object({ label: z.string(), value: z.string(), delta: z.string().optional() }),
59
- description: "A single headline number.",
91
+ description: "A headline number.",
92
+ props: {
93
+ type: "object",
94
+ properties: {
95
+ label: { type: "string" },
96
+ value: { type: "string" },
97
+ },
98
+ required: ["label", "value"],
99
+ },
60
100
  },
61
101
  };
62
102
  ```
63
103
 
64
- `props` accepts a JSON Schema object or any Standard Schema validator. `uiCatalog` from [`@agentable-ui/ui`](../ui) is a ready-made set.
104
+ Catalog schemas accept JSON Schema or supported Standard Schema validators. Unknown components, invalid props, and excessive depth/node counts produce structured issues. `AgentSurface` defaults to depth 6 and 60 nodes. Renderer prunes invalid nodes so valid parts can remain visible; validation options depend on the host.
105
+
106
+ Lower-level exports include `validateSpec`, `catalogSchema`, and `renderSpec`. Validate before calling `renderSpec` directly.
107
+
108
+ See [lang](../lang) for syntax, prompts, and parsing; [ui](../ui) for the built-in catalog.
65
109
 
66
- MIT
110
+ MIT licensed.
package/dist/index.d.ts CHANGED
@@ -10,9 +10,19 @@ interface SurfaceState {
10
10
  data: Record<string, unknown>;
11
11
  ui: Record<string, unknown>;
12
12
  }
13
- /** "a.b[0].c" -> ["a", "b", 0, "c"]. Throws on __proto__, prototype and constructor segments. */
14
- export declare function parsePath(path: string): Array<string | number>;
15
- /** Read a path, following own properties only. Undefined for missing or unsafe paths. */
13
+ /** A read-only list filter in a path: `items[done=true]`, `items[status!="archived"]`. */
14
+ interface PathFilter {
15
+ field: string;
16
+ op: "=" | "!=";
17
+ value: string | number | boolean;
18
+ }
19
+ type PathKey = string | number | PathFilter;
20
+ /**
21
+ * "a.b[0].c" -> ["a", "b", 0, "c"]; "items[done=true]" -> ["items", {field: "done", op: "=", value: true}].
22
+ * Throws on __proto__, prototype and constructor segments, and on malformed selectors.
23
+ */
24
+ export declare function parsePath(path: string): PathKey[];
25
+ /** Read a path, following own properties only. Filters narrow arrays. Undefined for missing or unsafe paths. */
16
26
  export declare function getPath(obj: unknown, path: string): unknown;
17
27
  type StateAction = {
18
28
  type: "set";
@@ -274,4 +284,4 @@ export interface AgentSurfaceProps {
274
284
  */
275
285
  export declare function AgentSurface({ components, name, id, group, tier, priority, maxDepth, maxNodes, placeholder, onPresent, className, clearable, data, tools, stateTool, onActionError, onMessage }: AgentSurfaceProps): import("react").JSX.Element;
276
286
  //#endregion
277
- export type { MessageAction, RendererError, RendererProps, SpecIssue, StateAction, SurfaceOp, SurfaceState, ToolAction, UIAction, UISpec, UiStore, ValidateOptions };
287
+ export type { MessageAction, PathFilter, PathKey, RendererError, RendererProps, SpecIssue, StateAction, SurfaceOp, SurfaceState, ToolAction, UIAction, UISpec, UiStore, ValidateOptions };
package/dist/index.js CHANGED
@@ -10,14 +10,62 @@ const UNSAFE_KEYS = /* @__PURE__ */ new Set([
10
10
  "prototype",
11
11
  "constructor"
12
12
  ]);
13
- /** "a.b[0].c" -> ["a", "b", 0, "c"]. Throws on __proto__, prototype and constructor segments. */
13
+ const isFilter = (k) => typeof k === "object";
14
+ function literal(raw) {
15
+ if (raw.startsWith("\"")) return raw.slice(1, -1);
16
+ if (raw === "true") return true;
17
+ if (raw === "false") return false;
18
+ if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
19
+ return raw;
20
+ }
21
+ /**
22
+ * "a.b[0].c" -> ["a", "b", 0, "c"]; "items[done=true]" -> ["items", {field: "done", op: "=", value: true}].
23
+ * Throws on __proto__, prototype and constructor segments, and on malformed selectors.
24
+ */
14
25
  function parsePath(path) {
15
26
  if (!path) return [];
16
- const keys = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean).map((p) => /^\d+$/.test(p) ? Number(p) : p);
17
- for (const k of keys) if (typeof k === "string" && UNSAFE_KEYS.has(k)) throw new Error(`Path "${path}" uses the reserved key "${k}".`);
27
+ const keys = [];
28
+ let i = 0;
29
+ while (i < path.length) {
30
+ const c = path[i];
31
+ if (c === ".") {
32
+ i++;
33
+ continue;
34
+ }
35
+ const rest = path.slice(i);
36
+ if (c === "[") {
37
+ const index = /^\[(\d+)\]/.exec(rest);
38
+ if (index) {
39
+ keys.push(Number(index[1]));
40
+ i += index[0].length;
41
+ continue;
42
+ }
43
+ const filter = /^\[([A-Za-z_][A-Za-z0-9_]*)(!?=)("[^"\]]*"|[A-Za-z0-9_.-]+)\]/.exec(rest);
44
+ if (!filter) throw new Error(`Path "${path}" has a bad selector; use [0] or [field=value].`);
45
+ keys.push({
46
+ field: filter[1],
47
+ op: filter[2],
48
+ value: literal(filter[3])
49
+ });
50
+ i += filter[0].length;
51
+ continue;
52
+ }
53
+ const seg = /^[^.[\]]+/.exec(rest)[0];
54
+ keys.push(/^\d+$/.test(seg) ? Number(seg) : seg);
55
+ i += seg.length;
56
+ }
57
+ for (const k of keys) {
58
+ const name = isFilter(k) ? k.field : k;
59
+ if (typeof name === "string" && UNSAFE_KEYS.has(name)) throw new Error(`Path "${path}" uses the reserved key "${name}".`);
60
+ }
18
61
  return keys;
19
62
  }
20
- /** Read a path, following own properties only. Undefined for missing or unsafe paths. */
63
+ function matches(item, f) {
64
+ const v = item !== null && typeof item === "object" && Object.hasOwn(item, f.field) ? item[f.field] : void 0;
65
+ const eq = typeof f.value === "boolean" ? Boolean(v) === f.value : v !== void 0 && v !== null && String(v) === String(f.value);
66
+ return f.op === "=" ? eq : !eq;
67
+ }
68
+ /** Read a path, following own properties only. Filters narrow arrays. Undefined for missing or unsafe paths. */
21
69
  function getPath(obj, path) {
22
70
  let keys;
23
71
  try {
@@ -27,6 +75,11 @@ function getPath(obj, path) {
27
75
  }
28
76
  let cur = obj;
29
77
  for (const key of keys) {
78
+ if (isFilter(key)) {
79
+ if (!Array.isArray(cur)) return void 0;
80
+ cur = cur.filter((item) => matches(item, key));
81
+ continue;
82
+ }
30
83
  if (cur == null || typeof cur !== "object" || !Object.hasOwn(cur, key)) return void 0;
31
84
  cur = cur[key];
32
85
  }
@@ -64,6 +117,7 @@ function setPath(obj, keys, value) {
64
117
  function uiKeys(path) {
65
118
  const keys = parsePath(path);
66
119
  if (keys[0] !== "ui" || keys.length < 2) throw new Error(`Actions may only write ui.* paths (got "${path}").`);
120
+ if (keys.some(isFilter)) throw new Error(`Filters like [field=value] are read-only (got "${path}").`);
67
121
  return keys.slice(1);
68
122
  }
69
123
  function reduce(ui, a) {
@@ -554,8 +608,8 @@ function Renderer({ components, library, response, isStreaming = false, data, to
554
608
  isStreaming
555
609
  ]);
556
610
  const fail = useCallback((message, node) => {
557
- setActionError(message);
558
- onActionErrorRef.current?.(message, node);
611
+ if (onActionErrorRef.current) onActionErrorRef.current(message, node);
612
+ else setActionError(message);
559
613
  }, []);
560
614
  const emit = useCallback((node, event, value) => {
561
615
  const actions = node.on?.[event];
@@ -1039,8 +1093,8 @@ function AgentSurface({ components, name = "present", id, group, tier, priority,
1039
1093
  }
1040
1094
  } : null);
1041
1095
  const fail = useCallback((message, node) => {
1042
- setError(message);
1043
- onActionErrorRef.current?.(message, node);
1096
+ if (onActionErrorRef.current) onActionErrorRef.current(message, node);
1097
+ else setError(message);
1044
1098
  }, []);
1045
1099
  const emit = useCallback((node, event, value) => {
1046
1100
  const actions = node.on?.[event];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentable-ui/genui",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Generative UI for Agentable: a streaming Renderer for Agentable Lang programs, AgentSurface with a single validated present tool, catalog schema generation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,9 +34,9 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@agentable-ui/core": "^0.1.1",
38
- "@agentable-ui/react": "^0.1.0",
39
- "@agentable-ui/lang": "^0.1.0"
37
+ "@agentable-ui/react": "^0.1.1",
38
+ "@agentable-ui/lang": "^0.2.1",
39
+ "@agentable-ui/core": "^0.2.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "react": "^18.2.0 || ^19.0.0"
@@ -47,7 +47,7 @@
47
47
  "@types/react": "^19.2.18",
48
48
  "@types/react-dom": "^19.2.7",
49
49
  "@testing-library/react": "^16.3.3",
50
- "@agentable-ui/ui": "^0.1.1"
50
+ "@agentable-ui/ui": "^0.2.1"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsdown",