Composer
A catalog is a vocabulary, and vocabularies should fit the product. Pick components from both catalogs and the composer builds the mix: one prompt-pack (the primary catalog carries the surface; the other's components ride the protocol's explicit-catalogId rule, with their fixtures rewritten into mixed examples), one contract per catalog for validation, and the registration for svelte-a2ui. The wire never changes — a composition narrows what the agent is taught and what the renderer accepts, nothing else.
my-console.pack.md — paste into your agent's system prompt
# my-console — prompt-pack (composed from auri ops)
> Composed vocabulary: Stat, Callout.
> System-prompt snippet teaching an agent the `auri ops` vocabulary. Everything below the rule is
> the pack.
---
You can render live UI for the user by emitting A2UI v1.0 messages as JSONL — one complete JSON
object per line, no surrounding markdown or prose. You describe components from a fixed catalog;
you never write markup or code.
Catalog id: `https://chaliceforauri.github.io/auri/catalogs/ops/v2.json`
## The wire in 30 seconds
Three message kinds. A minimal complete stream:
```
{"version":"v1.0","createSurface":{"surfaceId":"s1","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateDataModel":{"surfaceId":"s1","value":{"p95":342}}}
{"version":"v1.0","updateComponents":{"surfaceId":"s1","components":[{"id":"root","component":"Stat","label":"Checkout p95","value":{"path":"/p95"},"unit":"ms"}]}}
```
- Every line has the shape `{"version":"v1.0","<messageKind>":{...}}` — it ends with **two**
closing braces minimum: one for the message, one for the envelope. Balance every line before
the newline; a dropped final `}` is the most common emission mistake.
- `createSurface` comes first; after it, data and components may arrive in any order.
- Components form a flat list addressed by `id`. Nothing paints until a component with the id
`root` exists, and only components reachable from `root` render.
- Any displayable property takes either a literal (`"value": 342`) or a data binding
(`"value": {"path": "/p95"}`) — an RFC 6901 JSON Pointer into the surface's data model.
- **To change what's on screen, change the data, not the components.** Bind values you expect to
update, then send:
```
{"version":"v1.0","updateDataModel":{"surfaceId":"s1","path":"/p95","value":329}}
```
## Rules
1. **Raw values only in data props.** Emit `12400`, never `"12,400"`, `"$12.4K"` or `"98%"` — the
renderer formats numbers, dates and units in the user's locale. Units go in the `unit` prop.
Prose is the opposite: in callout text and summaries write times and numbers for humans
("yesterday at 22:14 UTC"), never raw ISO strings.
2. **`intent` judges, `trend` describes.** They are independent axes: latency rising is
`"trend": "up"` with `"intent": "bad"`; error rate falling is `"trend": "down"` with
`"intent": "good"`.
3. **One intent scale everywhere**: `good` (healthy, succeeding) · `bad` (failing, critical) ·
`warning` (needs attention, degraded) · `info` (informational) · `neutral` (no judgment).
Omit `intent` when you aren't making a claim.
4. **No icons, colors, or sizes.** Intent implies the iconography; the host theme decides the look.
5. **Send data in small slices.** Several short `updateDataModel` messages beat one giant nested
one — each line must be a complete, balanced JSON object, and small messages paint sooner.
After the initial send, always include a `path`: an `updateDataModel` without one **replaces
the entire data model**, blanking every other binding on the surface.
**Everything an action carries lives INSIDE `event`.** `name`, `context` and
`userMessage` are all keys of `event` — never siblings of it:
```
CORRECT {"event":{"name":"saved","context":{"id":"a1"},"userMessage":"Saved the draft"}}
INVALID {"event":{"name":"saved"},"context":{"id":"a1"}}
```
Hoisting any of them out of `event` makes the action invalid and it will be
rejected. This is the single most common shape mistake observed in live
emissions across every auri catalog.
`event` accepts **only** those three keys. To send a result back to the surface
after handling an action, reply with `updateDataModel` on the path you want
written — you authored the surface, so you already know the path.
## Components
### Stat — a KPI tile
One metric: label, current value, and optionally its change, direction and judgment.
| prop | type | required | notes |
| --------- | -------------------------- | -------- | --------------------------------------------------------- |
| `label` | string | yes | short metric name, e.g. `"Error rate"` |
| `value` | number \| string | yes | raw reading; bind with `{"path"}` if it updates |
| `unit` | string | no | `"ms"`, `"%"`, `"req/s"`, or a currency code like `"USD"` |
| `delta` | number | no | signed change vs the comparison period |
| `caption` | string | no | one line of context, e.g. `"vs previous hour"` |
| `trend` | `"up" \| "down" \| "flat"` | no | direction only — never a judgment |
| `intent` | intent scale | no | judgment of the reading; default `"neutral"` |
```
{"id":"latency","component":"Stat","label":"Checkout p95","value":{"path":"/p95"},"unit":"ms","delta":{"path":"/p95Delta"},"trend":"down","intent":"good","caption":"vs previous hour"}
```
### Callout — an alert/note block
The agent telling the user something in prose: a heads-up, a caveat, a status note.
| prop | type | required | notes |
| -------- | ------------ | -------- | -------------------------------------------------------------- |
| `title` | string | no | short heading |
| `text` | string | yes | body; inline markdown allowed: `**bold**`, `` `code` ``, links |
| `intent` | intent scale | no | default `"info"` — a callout's resting state is informational |
```
{"id":"deploy_note","component":"Callout","title":"Deploy window tonight","text":"Payments API deploys **21:00–21:30 UTC**. Expect brief elevated latency.","intent":"info"}
```
## Mixing with the basic catalog
Layout containers (`Row`, `Column`, `Card`, `List`) come from the A2UI basic catalog. When the
surface's `catalogId` is the ops catalog, give basic-catalog components an explicit `catalogId`:
```
{"id":"root","component":"Column","catalogId":"https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json","children":["status","latency"]}
```
## The forms catalog — mixed onto this surface
The components below come from a second catalog. Emit each of them with an explicit
`"catalogId": "https://chaliceforauri.github.io/auri/catalogs/forms/v2.json"` — the surface's default catalog stays
`https://chaliceforauri.github.io/auri/catalogs/ops/v2.json`.
### Rules for forms components
1. **Every field's `value` is a binding — never a literal.** `"value": {"path": "/signup/email"}`
points at where the answer lives in the data model. Seed defaults by writing that path with
`updateDataModel`; the user's input is written back to the same path. Give every field its own
path — two fields sharing one is a bug.
2. **Read answers from the data model.** After submission you can read any field's path, or
hand-pick the answers you need into the submit action's `context` as `{"path": ...}` bindings —
the decision payload, not the world.
3. **Validation is the `checks` array**, evaluated live by the renderer:
`{"condition": {"call": "...", "args": {...}}, "message": "human text"}`. The call and
its args go INSIDE `condition`; `message` sits beside it. Only five calls exist —
`required` · `email` · `regex` (args `{"pattern"}`) · `length` (args `{"min"}`/`{"max"}`) ·
`numeric` (args `{"min"}`/`{"max"}`). The renderer supplies the field's current value; you
never pass it. `message` is what the user reads when it fails — always plain human text.
4. **Raw values on the wire.** Dates are ISO 8601 (`"2026-08-19"`), numbers unformatted — the
renderer localizes. Prose (labels, hints, messages) is the opposite: written for humans.
5. **Send data in small slices.** After the initial send, always include a `path`: an
`updateDataModel` without one **replaces the entire data model**, blanking every binding on
the surface — including the user's half-typed answers.
6. **Send components in small batches too.** `updateComponents` may be sent repeatedly — each
message merges into the flat list by `id`. Emit the root and two or three fields per line, then
the next batch. Never pack a whole form into one line: long lines are where braces get lost,
and shorter lines paint sooner.
**Everything an action carries lives INSIDE `event`.** `name`, `context` and
`userMessage` are all keys of `event` — never siblings of it:
```
CORRECT {"event":{"name":"saved","context":{"id":"a1"},"userMessage":"Saved the draft"}}
INVALID {"event":{"name":"saved"},"context":{"id":"a1"}}
```
Hoisting any of them out of `event` makes the action invalid and it will be
rejected. This is the single most common shape mistake observed in live
emissions across every auri catalog.
`event` accepts **only** those three keys. To send a result back to the surface
after handling an action, reply with `updateDataModel` on the path you want
written — you authored the surface, so you already know the path.
### TextField — one line of text
| prop | type | required | notes |
| ------------- | ------------------------------------------------- | -------- | --------------------------------------------------- |
| `label` | string | yes | visible label and accessible name |
| `value` | `{"path"}` | yes | where the answer lives; always a binding |
| `kind` | `"text" \| "email" \| "url" \| "tel" \| "secret"` | no | input treatment + mobile keyboard; default `"text"` |
| `placeholder` | string | no | example content, never a label substitute |
| `hint` | string | no | one line of help under the field |
| `checks` | check[] | no | see rule 3 |
```
{"id":"email","component":"TextField","label":"Work email","kind":"email","value":{"path":"/contact/email"},"hint":"We only use this for receipts.","checks":[{"condition":{"call":"required"},"message":"Enter your email."},{"condition":{"call":"email"},"message":"That doesn't look like an email address."}]}
```
### SubmitBar — submit (and cancel) with pending state
Submission fires only when every check on the surface passes. Hand-pick the answers you need into
`context`. To report a server-side verdict afterwards, reply with `updateDataModel` on the path you
want written — bind a `Callout` or the field's own error to that path when you build the form.
| prop | type | required | notes |
| -------------- | ---------- | -------- | -------------------------------------------------------------- |
| `submitAction` | action | yes | `{"event":{"name","context",...}}`; name it after what happens |
| `submitLabel` | string | no | e.g. `"Create account"`; default localized "Submit" |
| `cancelAction` | action | no | renders a quiet cancel button |
| `cancelLabel` | string | no | |
| `pending` | `{"path"}` | no | bind and set true while you process; the bar disables |
```
{"id":"submit","component":"SubmitBar","submitLabel":"File report","pending":{"path":"/report/pending"},"submitAction":{"event":{"name":"report_filed","context":{"severity":{"path":"/report/severity"},"details":{"path":"/report/details"}}}}}
```
## Examples
One Stat stream:
```jsonl
{"version":"v1.0","createSurface":{"surfaceId":"checkout_health","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateDataModel":{"surfaceId":"checkout_health","value":{"p95":342,"p95Delta":-38}}}
{"version":"v1.0","updateComponents":{"surfaceId":"checkout_health","components":[{"id":"root","component":"Stat","label":"Checkout p95 latency","value":{"path":"/p95"},"unit":"ms","delta":{"path":"/p95Delta"},"trend":"down","intent":"good","caption":"vs previous hour"}]}}
{"version":"v1.0","updateDataModel":{"surfaceId":"checkout_health","path":"/p95","value":329}}
```
One Callout stream:
```jsonl
{"version":"v1.0","createSurface":{"surfaceId":"deploy_note","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateComponents":{"surfaceId":"deploy_note","components":[{"id":"root","component":"Callout","title":"Deploy window tonight","text":"Payments API deploys **21:00–21:30 UTC**. Expect brief elevated latency on `/charge`.","intent":"info"}]}}
```
One mixed TextField stream (note the explicit catalogId):
```jsonl
{"version":"v1.0","createSurface":{"surfaceId":"f","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateDataModel":{"surfaceId":"f","value":{"email":""}}}
{"version":"v1.0","updateComponents":{"surfaceId":"f","components":[{"id":"root","component":"Column","catalogId":"https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json","children":["email"]},{"id":"email","component":"TextField","label":"Work email","kind":"email","value":{"path":"/email"},"hint":"We only use this for receipts.","checks":[{"condition":{"call":"required"},"message":"Enter your email."},{"condition":{"call":"email"},"message":"That doesn't look like an email address."}],"catalogId":"https://chaliceforauri.github.io/auri/catalogs/forms/v2.json"}]}}
```
One mixed SubmitBar stream (note the explicit catalogId):
```jsonl
{"version":"v1.0","createSurface":{"surfaceId":"f","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateDataModel":{"surfaceId":"f","value":{"email":"","pending":false}}}
{"version":"v1.0","updateComponents":{"surfaceId":"f","components":[{"id":"root","component":"Column","catalogId":"https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json","children":["email","submit"]},{"id":"email","component":"TextField","label":"Work email","kind":"email","value":{"path":"/email"},"checks":[{"condition":{"call":"required"},"message":"Enter your email."}],"catalogId":"https://chaliceforauri.github.io/auri/catalogs/forms/v2.json"},{"id":"submit","component":"SubmitBar","submitLabel":"Subscribe","pending":{"path":"/pending"},"submitAction":{"event":{"name":"subscribed","context":{"email":{"path":"/email"}}}},"catalogId":"https://chaliceforauri.github.io/auri/catalogs/forms/v2.json"}]}}
```
my-console.ops.contract.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://chaliceforauri.github.io/auri/catalogs/ops/v2.json",
"protocolVersion": "v1.0",
"title": "my-console",
"description": "Composed from auri ops: Stat, Callout. Source contract: https://chaliceforauri.github.io/auri/catalogs/ops/v2.json",
"catalogId": "https://chaliceforauri.github.io/auri/catalogs/ops/v2.json",
"instructions": "Rules for emitting this catalog. 1) Emit raw values: 12400, never \"12,400\" or \"$12.4K\" — the renderer formats numbers, dates and units in the user's locale. 2) `intent` judges, `trend` describes: latency rising is trend \"up\" with intent \"bad\"; error rate falling is trend \"down\" with intent \"good\". 3) One shared intent scale: good (healthy) | bad (failing) | warning (needs attention) | info (informational) | neutral (no judgment). Omit intent when you are not making a claim. 4) No icons, colors, or sizes on the wire — intent implies iconography and the theme decides the look. 5) Bind values you expect to change with {\"path\": \"/json/pointer\"} and update them via updateDataModel instead of re-sending components. 6) Layout containers (Row, Column, Card, List) come from the basic catalog: give those components an explicit \"catalogId\": \"https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json\".",
"components": {
"Stat": {
"type": "object",
"description": "A KPI tile: one metric with its label, current value, and optionally its change, direction and judgment.",
"properties": {
"component": {
"const": "Stat"
},
"label": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
"description": "Short name of the metric, e.g. 'Error rate'. Required — it is also the tile's accessible name."
},
"value": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicValue",
"description": "The current reading, raw and unformatted (a number like 12400, or a short string like 'Healthy'). Bind with {\"path\": ...} when it will update."
},
"unit": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
"description": "Unit rendered beside the value: 'ms', '%', 'req/s', or an ISO 4217 currency code like 'USD' (currencies are formatted in the user's locale). Omit for unitless values."
},
"delta": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicNumber",
"description": "Signed change versus the comparison period, as a raw number (e.g. -38). The renderer formats the sign."
},
"caption": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
"description": "One small line of context under the value, e.g. 'vs previous hour'."
},
"trend": {
"oneOf": [
{
"type": "string",
"enum": [
"up",
"down",
"flat"
]
},
{
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DataBinding"
},
{
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCall"
}
],
"description": "Direction of movement: 'up', 'down' or 'flat'. Describes, never judges — pair with intent for the judgment."
},
"intent": {
"oneOf": [
{
"type": "string",
"enum": [
"good",
"bad",
"warning",
"info",
"neutral"
]
},
{
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DataBinding"
},
{
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCall"
}
],
"description": "Judgment of the current reading on the shared scale. Default: 'neutral'.",
"default": "neutral"
}
},
"required": [
"component",
"label",
"value"
]
},
"Callout": {
"type": "object",
"description": "An alert/note block the agent uses to tell the user something in prose: a heads-up, a caveat, a status note.",
"properties": {
"component": {
"const": "Callout"
},
"title": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
"description": "Optional short heading for the callout."
},
"text": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
"description": "The callout body. Required. Supports inline markdown: **bold**, *italics*, `code`, [links](https://...)."
},
"intent": {
"oneOf": [
{
"type": "string",
"enum": [
"good",
"bad",
"warning",
"info",
"neutral"
]
},
{
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DataBinding"
},
{
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCall"
}
],
"description": "The callout's register on the shared scale. Default: 'info' — a callout exists to call attention; informational is its resting state.",
"default": "info"
}
},
"required": [
"component",
"text"
]
}
},
"$defs": {
"anyComponent": {
"oneOf": [
{
"$ref": "#/components/Stat"
},
{
"$ref": "#/components/Callout"
}
],
"discriminator": {
"propertyName": "component"
}
},
"anyFunction": {
"not": {}
}
}
}my-console.forms.contract.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://chaliceforauri.github.io/auri/catalogs/forms/v2.json",
"protocolVersion": "v1.0",
"title": "my-console",
"description": "Composed from auri forms: TextField, SubmitBar. Source contract: https://chaliceforauri.github.io/auri/catalogs/forms/v2.json",
"catalogId": "https://chaliceforauri.github.io/auri/catalogs/forms/v2.json",
"instructions": "Rules for emitting this catalog. 1) Every field's `value` is a data binding {\"path\": \"/json/pointer\"} — that pointer is where the user's answer lives. Never emit a literal value; seed defaults by writing the data model with updateDataModel. 2) Give each field its own path; two fields sharing a path is a bug. 3) Validation is the `checks` array: {\"call\", \"args\", \"message\"} using only the built-ins required | email | regex | length | numeric. The renderer supplies the field's current value; args carry the rest (e.g. {\"min\": 8}). `message` is always human text. 4) Raw values on the wire: dates are ISO 8601 (\"2026-08-19\"), numbers unformatted — the renderer localizes. 5) Read answers from the data model, or hand-pick them into submitAction context as {\"path\": ...} bindings. 6) Labels are required — a field without an accessible name cannot be emitted. 7) Send components in small batches — updateComponents merges by id, so emit two or three components per message rather than a whole form in one line.",
"components": {
"TextField": {
"type": "object",
"properties": {
"component": {
"const": "TextField"
},
"label": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
"description": "Visible field label, e.g. 'Work email'. Required — it is the field's accessible name."
},
"value": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DataBinding",
"description": "Data binding for the answer, always {\"path\": ...}. Seed a default by writing that path with updateDataModel; the user's input is written back to it."
},
"kind": {
"enum": [
"text",
"email",
"url",
"tel",
"secret"
],
"description": "Input treatment and mobile keyboard. 'secret' masks input. Default 'text'."
},
"placeholder": {
"type": "string",
"description": "Example content shown while empty. Never a substitute for label."
},
"hint": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
"description": "One line of help under the field, e.g. 'We only use this for receipts.'"
},
"checks": {
"type": "array",
"items": {
"type": "object",
"description": "A validation check. Spec-shaped — `condition` is a FunctionCall — but narrowed to the renderer's built-ins, and `message` is required because a check with no human text fails silently.",
"properties": {
"condition": {
"type": "object",
"properties": {
"call": {
"enum": [
"required",
"email",
"regex",
"length",
"numeric"
],
"description": "One of the protocol's built-in validation functions. The renderer supplies the field's current value automatically."
},
"args": {
"type": "object",
"description": "Arguments beyond the value: regex takes {\"pattern\"}, length and numeric take {\"min\"} and/or {\"max\"}. required and email take none."
}
},
"required": [
"call"
],
"additionalProperties": false
},
"message": {
"type": "string",
"description": "Human text shown when the check fails. Always required."
}
},
"required": [
"condition",
"message"
],
"additionalProperties": false
},
"description": "Validation rules, evaluated by the renderer as the user types. Submission is blocked while any check fails."
}
},
"required": [
"label",
"value"
]
},
"SubmitBar": {
"type": "object",
"properties": {
"component": {
"const": "SubmitBar"
},
"submitAction": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/Action",
"description": "Fired on submit, only when every check on the surface passes. Hand-pick the answers you need into context as {\"path\": ...} bindings."
},
"submitLabel": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
"description": "Names the act of submitting, e.g. 'Create account'. Default: localized 'Submit'."
},
"cancelAction": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/Action",
"description": "Optional secondary action; renders a quiet cancel button."
},
"cancelLabel": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString"
},
"pending": {
"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean",
"description": "Bind to a path and set true while processing the submission — the bar disables and shows progress. Set false (or write a server error) when done."
}
},
"required": [
"submitAction"
]
}
},
"$defs": {
"anyComponent": {
"oneOf": [
{
"$ref": "#/components/TextField"
},
{
"$ref": "#/components/SubmitBar"
}
],
"discriminator": {
"propertyName": "component"
}
},
"anyFunction": {
"not": {}
}
}
}registration — svelte-a2ui
import { createCatalogRegistry, basicCatalog } from 'svelte-a2ui';
import { opsCatalog } from '@aurilabs/ops';
import { formsCatalog } from '@aurilabs/forms';
const pick = (catalog, names) =>
({ id: catalog.id, components: Object.fromEntries(names.map((n) => [n, catalog.components[n]])) });
export const catalog = createCatalogRegistry([
pick(opsCatalog, ['Stat', 'Callout']),
pick(formsCatalog, ['TextField', 'SubmitBar']),
basicCatalog
]);run the gate against your composition
# save the pack and each contract, then from packages/ops: node scripts/emission-eval.js \ --models openai:gpt-5.6 \ --pack my-console.pack.md \ --contract my-console.ops.contract.json \ --scenarios-file your-scenarios.json node scripts/emission-eval.js \ --models openai:gpt-5.6 \ --pack my-console.pack.md \ --contract my-console.forms.contract.json \ --scenarios-file your-scenarios.json # validation is per-catalog: each run validates its own catalog's components # and skips the other's (they carry an explicit foreign catalogId on the wire). # a model emitting a component you cut still fails its catalog's run.