The round-trip trap is the sharpest framing here — paying hundreds of tokens and seconds of latency for 3.50 x 3 is hard to defend once you see it. Making the function a first-class computed() signal rather than a callback is the part I'd steal: it kills the flicker, not just the network call. Do you see teams resisting this because the agent has to be told about the catalog, or is the catalog the easy part?
I'm a Google Developer Expert in Dart and Flutter (one of 12 in North America). I have 45 years of experience with backend, web, mobile, devops, and training.
The catalog is actually the easy part—especially since modern LLMs are already trained extensively on tool calling and JSON schemas. When an agent connects to an A2UI renderer, advertising registered catalog functions and their CommonSchemas is no different than handing a standard function-calling schema to Gemini or Claude. The models pick up the bindings immediately and rarely hallucinate the arguments once they have typed schemas.
Where I see teams hesitate initially isn't the catalog—it is the paradigm shift regarding boundaries:
The "Oracle Fallacy": In early GenUI demos, the tempting illusion is that the LLM should handle everything end-to-end. Treating the model as an all-knowing oracle instead of a semantic layout compiler makes developers reluctant to draw a hard architectural line between creative layout and deterministic computation.
Fear of Async Plumbing: Teams that haven't worked with synchronous signals naturally assume that wiring client-side execution means building complex StreamController pipelines, debouncers, or manual setState() webs across generated widgets. Once they see that the catalog function compiles directly into a native computed() signal—updating the leaf widget in the exact same frame with 0ms latency and zero microtasks—that resistance evaporates into relief.
Telling the agent what functions exist is just schema advertising. The real win is deciding up front that your app's math, device hardware, and business rules belong to the local processor, not the token budget.
The Oracle Fallacy is the right name for it — demos that let the model do everything look magical until the bill and the latency arrive. And the async-plumbing fear is real only until someone sees the computed() signal update in the same frame; that's the demo that converts skeptics. The boundary decision up front is the actual architecture work — everything else is just wiring.
I'm a Google Developer Expert in Dart and Flutter (one of 12 in North America). I have 45 years of experience with backend, web, mobile, devops, and training.
"The boundary decision up front is the actual architecture work — everything else is just wiring."
That summary shows how well the message resonated. When readers adopt your framing ("The Oracle Fallacy") and independently play back the core principle—that the demo converting skeptics is seeing the synchronous computed() signal update in the exact same frame without the async latency tax—that's how architectural ideas take root.
That's the best compliment a framing can get — when someone else starts using it back at you. The demo point is exactly right: seeing the computed() signal update in the same frame is what makes the abstraction click for skeptics. Where does the boundary decision get hardest for you — when the catalog itself is dynamic and the agent has to discover functions at runtime?
I'm a Google Developer Expert in Dart and Flutter (one of 12 in North America). I have 45 years of experience with backend, web, mobile, devops, and training.
Dynamic runtime discovery is actually the straightforward mechanical part—we already solve that cleanly with capability handshakes, role-based scoping, or MCP-style tool discovery (for example, unlocking privileged catalog functions only when a user authenticates or navigates into a specific enterprise surface).
Where the boundary decision gets genuinely difficult is semantic grain size:
1. The "Assembly Language" vs. "God Function" Dilemma
If your catalog functions are too granular (like multiply(), add(), or formatCurrency()), you force the agent to compose arithmetic ASTs in JSON. You end up accidentally reinventing a fragile Lisp interpreter in JSON, and the agent will eventually botch business-rule order (for example, calculating regional sales tax before versus after applying manufacturer rebates).
If your catalog functions are too monolithic (like calculateEntireCheckoutState()), you strip away the agent's generative flexibility. At that point, you aren't doing Generative UI anymore; you're just writing a classic hardcoded wizard with an AI decorator.
Finding that sweet spot—functions that encapsulate a complete, inviolable domain rule (like calculateCartItemCost or validatePostalCode) while still leaving the layout and data-flow topology to the agent—is where real domain modeling happens.
2. Referential Transparency vs. Local Side Effects
The other hard edge is side-effect boundaries. Pure math (price * qty * (1 - discount)) is trivial because it is referentially transparent—it can recalculate fifty times a second with zero consequences. But as soon as a client function touches device hardware (GPS coordinates, biometric auth) or mutates local persistence (reserving local cache slots), you have to strictly quarantine it out of reactive computed() signals and route it strictly through explicit, deliberate user actions (SubmitAction).
If a function has side effects, it cannot live in the reactive rendering graph. That separation is where architecture either holds or falls apart.
I research AI tools, APIs & agents — comparing specs, pricing and real user reports — then publish honest rankings so you don't waste money. Research-first, never sponsored fluff.
The Oracle Fallacy is the right name for it — demos that let the model do everything look magical until the bill and the latency arrive. Making the function a first-class computed() signal rather than a callback is the part I'd steal: it kills the flicker, not just the network call. One thing I'm curious about: when the agent invents a call to a function that isn't in the catalog, what does the surface do — fail loud, or quietly render nothing?
I'm a Google Developer Expert in Dart and Flutter (one of 12 in North America). I have 45 years of experience with backend, web, mobile, devops, and training.
Great question, Paul. It depends on whether the call happens via Inbound RPC or Reactive UI Binding, but in both cases it fails explicitly and loudly rather than silently swallowing the error into a ghost screen.
Here is what happens under the hood:
1. Inbound RPC (CallRendererFunctionMessage)
If the remote agent sends a direct RPC message requesting a function that doesn't exist in the catalog, A2uiSurfaceBloc intercepts it and immediately returns a structured RendererFunctionResponseMessage with a typed error:
{"functionCallId":"call-123","error":"Function not found: computeRocketTrajectory"}
The agent gets immediate feedback that the tool isn't registered, and the client application continues running without a hitch.
When a streamed component binds a dynamic property (like a Text or Card) to an unregistered function:
catalog.invoke() throws a typed A2uiCatalogResolutionError: Function not found: <name>.
Inside the signals graph, the computed() signal captures this as a SignalEffectException.
In debug mode, this flags immediately in your logs and DevTools console, identifying the exact missing function name so you don't spend hours wondering why a field is blank.
In the Flutter widget tree (bloc_signals_genui_flutter), component resolvers are wrapped in defensive error boundaries. The individual offending node renders its fallback error state (or standard debug error tile), while the rest of the surface tree continues to mount and render cleanly.
Paying hundreds of tokens to compute 3.50 times 3 is the part that should bother people, the phone does it in the same frame at 0ms. The 20 percent coupon button is the flip side though, once that discount runs locally with no server check, a tampered client can apply it as often as it likes.
I'm a Google Developer Expert in Dart and Flutter (one of 12 in North America). I have 45 years of experience with backend, web, mobile, devops, and training.
Spot on, and this touches the first law of client-server security: never trust the client for final transactional authority.
Client-side functions exist for responsive user ergonomics and 120 FPS visual feedback, not for final financial settlement.
It is the exact same invariant web developers have lived with for thirty years:
Your client-side JavaScript (or Dart in Flutter) calculates cart totals and renders the discount instantly so the user doesn't wait through a spinner while typing or dragging sliders.
But when the user taps "Place Order", your backend API / payment gateway calculates the authoritative price against the database and applies the verified coupon rules before charging the card. Nobody lets a browser POST {"total_to_charge": 0.00} directly to Stripe!
In bloc_signals_genui, this is why we separated Path A (Interactive Actions) and Path B (Bidirectional RPC):
Local expressions like calculateCost handle instant preview arithmetic in the UI signal graph.
When an authoritative action occurs (like applying a restricted promo code or checking out), SubmitAction emits an A2uiRendererAction or invokes callAgentFunction() to validate against the server before committing state.
The phone gives you 0ms interactive rendering; the server keeps the ledger honest.
We're a place where coders share, stay up-to-date and grow their careers.
The round-trip trap is the sharpest framing here — paying hundreds of tokens and seconds of latency for 3.50 x 3 is hard to defend once you see it. Making the function a first-class computed() signal rather than a callback is the part I'd steal: it kills the flicker, not just the network call. Do you see teams resisting this because the agent has to be told about the catalog, or is the catalog the easy part?
The catalog is actually the easy part—especially since modern LLMs are already trained extensively on tool calling and JSON schemas. When an agent connects to an A2UI renderer, advertising registered catalog functions and their
CommonSchemasis no different than handing a standard function-calling schema to Gemini or Claude. The models pick up the bindings immediately and rarely hallucinate the arguments once they have typed schemas.Where I see teams hesitate initially isn't the catalog—it is the paradigm shift regarding boundaries:
StreamControllerpipelines, debouncers, or manualsetState()webs across generated widgets. Once they see that the catalog function compiles directly into a nativecomputed()signal—updating the leaf widget in the exact same frame with 0ms latency and zero microtasks—that resistance evaporates into relief.Telling the agent what functions exist is just schema advertising. The real win is deciding up front that your app's math, device hardware, and business rules belong to the local processor, not the token budget.
The Oracle Fallacy is the right name for it — demos that let the model do everything look magical until the bill and the latency arrive. And the async-plumbing fear is real only until someone sees the computed() signal update in the same frame; that's the demo that converts skeptics. The boundary decision up front is the actual architecture work — everything else is just wiring.
"The boundary decision up front is the actual architecture work — everything else is just wiring."
That summary shows how well the message resonated. When readers adopt your framing ("The Oracle Fallacy") and independently play back the core principle—that the demo converting skeptics is seeing the synchronous computed() signal update in the exact same frame without the async latency tax—that's how architectural ideas take root.
High-signal discussion from start to finish!
That's the best compliment a framing can get — when someone else starts using it back at you. The demo point is exactly right: seeing the computed() signal update in the same frame is what makes the abstraction click for skeptics. Where does the boundary decision get hardest for you — when the catalog itself is dynamic and the agent has to discover functions at runtime?
Dynamic runtime discovery is actually the straightforward mechanical part—we already solve that cleanly with capability handshakes, role-based scoping, or MCP-style tool discovery (for example, unlocking privileged catalog functions only when a user authenticates or navigates into a specific enterprise surface).
Where the boundary decision gets genuinely difficult is semantic grain size:
1. The "Assembly Language" vs. "God Function" Dilemma
multiply(),add(), orformatCurrency()), you force the agent to compose arithmetic ASTs in JSON. You end up accidentally reinventing a fragile Lisp interpreter in JSON, and the agent will eventually botch business-rule order (for example, calculating regional sales tax before versus after applying manufacturer rebates).calculateEntireCheckoutState()), you strip away the agent's generative flexibility. At that point, you aren't doing Generative UI anymore; you're just writing a classic hardcoded wizard with an AI decorator.Finding that sweet spot—functions that encapsulate a complete, inviolable domain rule (like
calculateCartItemCostorvalidatePostalCode) while still leaving the layout and data-flow topology to the agent—is where real domain modeling happens.2. Referential Transparency vs. Local Side Effects
The other hard edge is side-effect boundaries. Pure math (
price * qty * (1 - discount)) is trivial because it is referentially transparent—it can recalculate fifty times a second with zero consequences. But as soon as a client function touches device hardware (GPS coordinates, biometric auth) or mutates local persistence (reserving local cache slots), you have to strictly quarantine it out of reactivecomputed()signals and route it strictly through explicit, deliberate user actions (SubmitAction).If a function has side effects, it cannot live in the reactive rendering graph. That separation is where architecture either holds or falls apart.
The Oracle Fallacy is the right name for it — demos that let the model do everything look magical until the bill and the latency arrive. Making the function a first-class computed() signal rather than a callback is the part I'd steal: it kills the flicker, not just the network call. One thing I'm curious about: when the agent invents a call to a function that isn't in the catalog, what does the surface do — fail loud, or quietly render nothing?
Great question, Paul. It depends on whether the call happens via Inbound RPC or Reactive UI Binding, but in both cases it fails explicitly and loudly rather than silently swallowing the error into a ghost screen.
Here is what happens under the hood:
1. Inbound RPC (
CallRendererFunctionMessage)If the remote agent sends a direct RPC message requesting a function that doesn't exist in the catalog,
A2uiSurfaceBlocintercepts it and immediately returns a structuredRendererFunctionResponseMessagewith a typed error:The agent gets immediate feedback that the tool isn't registered, and the client application continues running without a hitch.
2. Reactive UI Bindings (
{"call": "unknownFunction"})When a streamed component binds a dynamic property (like a
TextorCard) to an unregistered function:catalog.invoke()throws a typedA2uiCatalogResolutionError: Function not found: <name>.computed()signal captures this as aSignalEffectException.bloc_signals_genui_flutter), component resolvers are wrapped in defensive error boundaries. The individual offending node renders its fallback error state (or standard debug error tile), while the rest of the surface tree continues to mount and render cleanly.No silent black holes or phantom state!
Paying hundreds of tokens to compute 3.50 times 3 is the part that should bother people, the phone does it in the same frame at 0ms. The 20 percent coupon button is the flip side though, once that discount runs locally with no server check, a tampered client can apply it as often as it likes.
Spot on, and this touches the first law of client-server security: never trust the client for final transactional authority.
Client-side functions exist for responsive user ergonomics and 120 FPS visual feedback, not for final financial settlement.
It is the exact same invariant web developers have lived with for thirty years:
{"total_to_charge": 0.00}directly to Stripe!In
bloc_signals_genui, this is why we separated Path A (Interactive Actions) and Path B (Bidirectional RPC):calculateCosthandle instant preview arithmetic in the UI signal graph.SubmitActionemits anA2uiRendererActionor invokescallAgentFunction()to validate against the server before committing state.The phone gives you 0ms interactive rendering; the server keeps the ledger honest.