In my approach, callbacks are first-class. The agent defines server-side functions and passes them to the UI:<p><pre><code> const onRefresh = async () => {
data.loading = true;
data.messages = await loadMessages();
data.loading = false;
};
mount({
data,
callbacks: { onRefresh },
ui: ({ data, callbacks }) => (
<Button onClick={callbacks.onRefresh}>Refresh</Button>
)
});
</code></pre>
When the user clicks the button, it invokes the server-side function. The callback fetches fresh data, updates state via reactive proxies, and the UI reflects it — all without triggering a new LLM turn.<p>So the UI is generated dynamically by the LLM, but the interactions are real server-side code, not just display. Forms work the same way — "await form.result" pauses execution until the user submits.<p>The article has a full walkthrough of the four data flow patterns (forms, live updates, streaming data, callbacks) with demos.