Overview and architecture
This page explains what @lynx-js/genui/openui is, how OpenUI Lang maps to a
trusted ReactLynx component tree, and what happens between a streamed Agent
response and the rendered UI.
What this package is
@lynx-js/genui/openui is the ReactLynx client runtime for OpenUI Lang
v0.5. It combines the framework-agnostic parser and evaluator from
@openuidev/lang-core with a ReactLynx renderer and a built-in mobile component
library.
The package provides:
<OpenUiRenderer>for raw or pre-parsed OpenUI input;createOpenUiLibrary()and 26 trusted default component implementations;- incremental parsing for model streams;
- reactive
$variables, expression evaluation, and form state; Query(),Mutation(), and multi-stepAction([...])execution;- structured parser, runtime, render, and tool errors;
- prompt builders that describe the matching component contract to an Agent.
It does not host an Agent, call an LLM, define your network transport, or provide backend tools. Your application owns those pieces.
Quick start
Install the package and optional default theme in a ReactLynx app:
The simplest valid response is:
OpenUI uses positional arguments. The parser maps them to named props in the order of each component's Zod schema. Forward references are allowed, so the root can refer to statements that appear later in the response.
Stream model output
Pass the accumulated model text as response and the generation state as
isStreaming. Reuse existing React or conversation state if your application
already owns both; no separate parser is needed.
responseis the full text received so far, not the latest delta. Append deltas; replace the text when your transport sends a cumulative snapshot.- Keep
isStreaming={true}throughout generation. Partial UI renders, while Query execution, mutation registration, and built-in interactions wait. - On successful completion, apply the authoritative final text and set
isStreaming={false}together. Its default isfalse; static or persisted complete responses can omit it.
Connect your transport
Keep text and generation state in one state object so they update together. The handlers below illustrate connecting a transport that emits deltas or full snapshots; use the handler matching your transport's text format:
If completion provides no final text, keep the accumulated response and update
only the flag: setOutput(current => ({ ...current, isStreaming: false })).
Even when final text equals the last partial, the flag must change to enable
queries, completion diagnostics, and interactions. Custom interactive components
should honor useIsStreaming() too.
How incremental parsing works
The renderer retains a streaming parser while library stays the same. Although
response supplies the full accumulated text, the parser detects appended text,
caches completed statements, and reparses the unfinished tail to preview partial
output. Forward references become renderable when their targets arrive. Changing
or shortening previously received text resets the parser's cache; keep Library
identity stable with useMemo or a module constant.
This is statement-level incremental parsing: preprocessing and result rebuilding
still run on each text update, and one long unfinished statement can be reparsed
repeatedly. It does not provide a component-patch protocol. Changing only
isStreaming with unchanged text does not rerun parsing.
Completion, cancellation, and new sessions
For SSE, finish only after the successful done event, using its authoritative
text. A connection closing by itself does not confirm success.
On failure or cancellation, clear incomplete text with
{ response: null, isStreaming: false }; do not enable queries or actions for an
unfinished result. State updates do not cancel network requests: your transport
owns cancellation and error reporting. Before starting another generation, stop
the old request and reject late events, then reset the text. Clearing response
does not reset runtime form state; use a new renderer key when a fresh session
should reset that state.
The mental model
In ordinary React, your source code chooses a component and passes props:
In OpenUI, the Agent writes the same intent as declarative assignments:
The renderer never evaluates generated JavaScript. It parses the text against
the JSON Schema produced by your Library, turns valid component calls into an
element tree, and looks up each element's trusted ReactLynx implementation by
name. A component that is not in the Library cannot be rendered.
The Library is therefore the contract on both sides:
Keep the prompt Library and renderer Library aligned whenever you add or override a component. A name or prop-order mismatch can produce text that the client cannot validate.
OpenUI Lang v0.5 in one minute
An OpenUI program contains one assignment per line. There are three statement families:
Expressions can contain primitives, arrays, objects, references, member access,
operators, ternaries, and built-ins such as @Count(...). The renderer needs a
statement named root; by default its component must be Stack because that is
the default Library root.
State values are reactive. When $status changes, expressions that read it are
evaluated again and Queries whose arguments depend on it are refreshed.
For the complete language grammar and built-ins, read the OpenUI Lang v0.5 specification.
The end-to-end picture
OpenUI is a loop between an Agent that writes declarative UI and a client that parses, evaluates, and renders it.
- Your transport sends the user request to an Agent service.
- The service calls a model with an OpenUI system prompt built from the same component contract the client supports.
- The transport accumulates text deltas as
responseand passes it withisStreamingto<OpenUiRenderer>. - The streaming parser validates completed statements and resolves references.
- The runtime initializes state and evaluates expressions. After generation
completes, it runs Queries through your
toolProvider. - The renderer walks the evaluated root and mounts trusted ReactLynx components from the Library.
- User interactions execute action steps. Host-facing actions leave through
onAction; state and tool steps stay inside the runtime.
Inside the client
response creates the complete v0.5 runtime, with isStreaming controlling
when queries and interactions become available:
Important runtime behavior:
- Incremental parsing. The parser caches completed statements while the accumulated response grows. Forward references become renderable when their targets arrive.
- Stable Library. Create the Library once with
useMemoor outside the component. Changing its identity creates a new parser and can reset parsing work. - Streaming guard. Pass
isStreaming={true}while generation is active. Query execution and mutation registration wait for stable output, and built-in interactions are disabled. - Reactive state.
$variablesand form values live in one external Store.onStateUpdatecan persist its snapshots;$-prefixed values ininitialStatehydrate reactive declarations. - Queries and mutations. Query defaults and prefetched
initialQueryResultsparticipate in the first synchronous render after their response finishes streaming. A configured tool provider revalidates Queries after commit and whenever referenced state changes. Mutations are registered but run only when an action calls@Run(mutationRef). - Sequential actions.
@Run,@Set,@Reset,@ToAssistant, and@OpenUrlexecute in order. A failed mutation stops the remaining steps. - Soft component failure. An element whose component name is not registered
renders nothing.
onErrorreceives other parse, evaluation, render, and tool failures after streaming settles.
Who owns what
<OpenUiRenderer> props
Pass the full accumulated response and set isStreaming={true} during
generation. isStreaming defaults to false for complete responses.
<OpenUiRenderer result={parseResult} library={library}> remains available for
legacy/static callers. It can render a pre-parsed element tree and forward
simple actions, but it does not create the v0.5 QueryManager or fully execute
@Run, @Set, and @Reset. Do not use it for a new v0.5 integration.
Prefetched Query results
Use initialQueryResults when Query data is already available before the
ReactLynx first screen. Each key is the Query assignment name (statement ID),
so Queries that call the same tool with different arguments remain distinct:
The prefetched value, including null, false, or 0, takes precedence over
the Query default during the first synchronous render. If toolProvider is
present, the runtime starts its normal Query execution after commit and
replaces the fallback when that request settles. Omit the provider when the
prefetched result is an immutable snapshot that should not be revalidated.
Tool providers
For local functions, pass a name-to-function map:
You may also pass an MCP-compatible object with
callTool({ name, arguments }). The runtime extracts structured MCP tool
results before expression evaluation. Missing tools and tool failures are
reported through onError.
Exports
Component styles, the core renderer stylesheet, and the Material Icons font are
implementation details imported by the relevant modules. Do not import private
files under styles/catalog or dist/core.

