Verified against Semitexa Ultimate 2026.10.03.1952
Screens an AI Composes
A model may compose a screen, and the server checks it with the same catalog and the same permissions a person composing it gets. The model never writes HTML. It writes a UI tree: the components to use, their props, and where each goes. The server validates the tree and draws it through the same renderers a template uses. A tree that fails is never drawn: the model is told exactly what to fix.
The UI tree
A tree is a flat JSON document. Its nodes refer to each other by id.
{
"version": "semitexa.ui-tree/v1",
"root": "overview",
"nodes": {
"overview": {"type": "platform.card", "props": {"title": "Products"}, "children": ["count", "add"]},
"count": {"type": "platform.stat", "props": {"label": "Products", "value": {"$data": "/count"}}},
"add": {"type": "platform.button", "props": {"text": "Add a product", "href": {"$action": "add"}}}
},
"data": {"count": "3"},
"actions": {"add": {"kind": "create", "screen": "playground.products"}}
}
nodes— each node has atype(a component or primitive name),props, and optionallychildren(ids) and aslot(which of its parent's slots it goes into; the parent's first slot when left out).data— the data model. A prop bound with{"$data": "/json/pointer"}takes the value at that pointer.actions— named server actions. A prop set to{"$action": "name"}takes the action's value, such as the path a button opens. An agent picks an action kind from a closed list; it cannot name a handler.
The shape follows A2UI (flat, id-referenced, a separate data model, named actions). It is not the A2UI wire format: an adapter for that comes when the spec is pinned.
What the server checks
UiTreeValidator reports every fault at once, so a model repairs the whole answer in one turn.
- Shape. Version, root, ids and node fields, children that exist and are used once, no cycles, nothing the root does not reach, at most 500 nodes and 32 levels.
- Components. Each
typeis a component open to agents, and one this visitor may use. - Props. Each prop is declared by the component's contract, has its type and allowed values, and none it requires is missing. A bound prop is checked by the value it binds to.
- Slots. Children go only into slots the component declares.
- Actions. Each
$actionnames one of the tree's actions, and each action passes its kind's own check. For example, a CRUD create needs a screen that exists and the visitor's create permission on it.
Each fault says where and what was expected:
{
"code": "tree.prop_unknown",
"path": "/nodes/count/props/valeu",
"message": "platform.stat has no prop \"valeu\".",
"expected": "label, value, delta, trend, caption",
"got": "valeu",
"hint": "Did you mean \"value\"?"
}
path is a JSON Pointer into the tree. The codes include tree.component_unknown,
tree.component_forbidden, tree.prop_invalid, tree.prop_required, tree.slot_unknown,
tree.action_forbidden and tree.unreachable.
Opening a component to agents
A component is open to agents when its #[AsUiContract] says so:
#[AsUiContract(
summary: 'Revenue for a period.',
props: [new UiProp('period', required: true, values: ['day', 'week', 'month'])],
agent: true, // default: previewSafe
permission: 'reports.view', // who may be shown it
)]
agentdefaults topreviewSafe.permissionis checked through the same authorizer as routes. A visitor without it is never told the component exists: it is left out of the catalog, the prompt and the schema.
Behaviors are never open. A component without a contract cannot be checked, so it is never open either.
Actions
An action kind is a class with #[AsUiTreeAction(kind: '…')] implementing
UiTreeActionKindInterface. It has three methods:
| Method | What it does |
|---|---|
check() |
Returns the action's faults. |
propValue() |
Returns what the prop that uses it receives. |
describe() |
Tells the model how to write it, and lists only what this visitor may use. |
Built in:
| Kind | Shape | Value |
|---|---|---|
navigate |
{"kind": "navigate", "to": "/path"} |
a same-site path |
create |
{"kind": "create", "screen": "<crud screen id>"} |
the screen's create dialog |
edit |
{"kind": "edit", "screen": "…", "record": "<id>"} |
one record's edit dialog |
The action is authorized when the tree is checked, and again when it runs: the screen it opens checks its own permission.
Composing
UiTreeComposer::compose($description) runs the loop:
- It asks the model with the visitor's catalog. The prompt is
platform-ui.ui-tree.compose. - It checks the answer.
- If the answer fails, it sends the faults back as a correction turn
(
platform-ui.ui-tree.repair). - It stops after three rounds or at the first sound tree, and draws only a sound tree.
$result = $composer->compose('A products overview with a way to add one.');
// ['tree' => ?UiTree, 'html' => ?string, 'rounds' => [...], 'failure' => ?string]
It works with any LlmProviderInterface: JSON is taken from the reply, inside code fences or not.
ScriptedProvider (semitexa/llm) answers with fixed replies, for tests and demos.
A component's #[UiOn] handler can call the composer directly, because component handlers
receive their #[InjectAsReadonly] services before they run.
From the command line
bin/semitexa ui:tree:catalog --grant=catalog.read # what this visitor may compose from
bin/semitexa ui:tree:catalog --grant=catalog.read --prompt # the prompt text a model is given
bin/semitexa ui:tree:catalog --schema # adds the JSON Schema of a whole tree
bin/semitexa ui:tree:validate screen.json --grant=catalog.read,catalog.create
bin/semitexa ui:tree:render screen.json --grant=catalog.read
bin/semitexa ui:tree:compose "A products overview" --grant=catalog.read
--grantnames the permissions of the visitor to check for. Repeat it, or separate the permissions with commas.ui:tree:validateprints one JSON envelope (semitexa.platform-ui.ui-tree-check/v1). It exits 1 when the tree is refused.
Over MCP
bin/semitexa ui:mcp --grant=… is an MCP server on stdio, for a coding agent or an MCP host.
{"mcpServers": {"semitexa-ui": {"command": "bin/semitexa", "args": ["ui:mcp", "--grant=catalog.read"]}}}
| Tool | Answers with |
|---|---|
ui_catalog |
the components and action kinds for the visitor |
ui_schema |
the tree's JSON Schema |
ui_validate |
the faults, as above |
ui_render |
an embedded ui://semitexa/tree/<hash> resource with the HTML, also readable through resources/read |
There is no HTTP door for it. In the browser, a page reaches the composer through its components, over KISS and HUG like everything else.
Proof in the Playground
| What | Where | Checked by |
|---|---|---|
| admin: first answer refused with three faults, second drawn | /playground/ai/compose |
compose-screen.spec.ts |
| viewer: "Add a product" refused every round, nothing drawn | /playground/ai/compose (as viewer) |
compose-screen.spec.ts |