Rike Pool

Generative UI and agentic loops in Gemini 3

Generative UI and agentic loops in Gemini 3

Gemini 3 introduces two primitives that alter the developer interaction model: Generative UI (streaming interactive components) and Native Agents (a permission-gated execution loop). These are not just chat interface upgrades. They represent a phase shift where the model moves from generating static tokens to generating application state and executing logic within the browser DOM.

In this post, we look at how these features work under the hood, run a few sanity checks on their limitations, and discuss why this accelerates the softening of the stack.

The mechanics of Generative UI

The standard LLM interaction model is Text -> Text or Text -> Code. The user creates the execution environment. Gemini 3 changes this to Text -> Hydrated Component.

When we ask the model to “Visualize this sorting algorithm”, it creates a structured definition that the client interface hydrates into a live widget. We can think of this as Server-Side Rendering (SSR) where the “Server” is the LLM.

  1. Token stream: The model emits tokens representing a component schema.
  2. Client hydration: The chat interface intercepts these tokens, maps them to a widget type, and mounts a sandbox.
  3. State injection: The data generated by the model is injected as props.

This mechanism mirrors how we handle advanced MDX rendering. In MDX, we map markdown nodes to React components explicitly. Here, the model generates the component tree on the fly based on tool definitions.

The rendering pipeline

The model outputs a schema validated by a library like Zod. We use the Vercel AI SDK to handle the stream. The implementation looks like this:

import { streamUI } from 'ai/rsc';
import { google } from '@ai-sdk/google';
import { z } from 'zod';
import { InteractiveChart } from './components/charts';

// Map model intent to React components
const result = await streamUI({
  model: google('gemini-3-flash'),
  messages: history,
  tools: {
    visualizeData: {
      description: 'Render a chart for data visualization',
      parameters: z.object({
        data: z.array(z.number()),
        labels: z.array(z.string()),
        type: z.enum(['line', 'bar'])
      }),
      // The component "hydrates" immediately on the client
      generate: ({ data, labels, type }) => (
        <InteractiveChart data={data} labels={labels} type={type} />
      )
    }
  }
});

The client parses this stream. If visualizeData is called, the <InteractiveChart /> component renders. If no tool matches, it falls back to text generation.

The agentic state machine

The second capability is the Gemini Agent. While frameworks like LangChain offered “agents” via prompt engineering, Gemini 3 integrates the Plan-Execute-Verify loop into the native inference path.

The Agent implementation follows a strict state machine. It does not hallucinate actions blindly. It requests a tool, waits for the kernel to execute it, and reads the output before proceeding.

We handle this loop manually to enforce permissions. This mirrors the typed boundaries we see in the Model Context Protocol (MCP). The model treats the external world as an API that must be queried safely.

import google.generativeai as genai
from google.protobuf import struct_pb2

# Disable automatic execution to inject the permission gate
chat = model.start_chat(enable_automatic_function_calling=False)
response = chat.send_message("Clean up the temp logs")

# The manual event loop
while response.parts[0].function_call:
    call = response.parts[0].function_call
    
    # 1. The Permission Gate (Critical)
    if call.name == "delete_file":
        print(f"⚠️ Model wants to delete: {call.args['path']}")
        if input("Allow? (y/n): ") != "y":
            break # Stop execution
            
    # 2. Execution
    # execute_tool is our wrapper around the filesystem API
    result = execute_tool(call.name, **call.args)
    
    # 3. Context Update
    # We feed the tool output back to the model to continue the loop
    response = chat.send_message(
        genai.protos.Part(
            function_response=genai.protos.FunctionResponse(
                name=call.name,
                response={"result": result}
            )
        )
    )

Sanity check

We need to verify if this is robust or if it breaks under edge cases. We tested the data visualization engine with a deliberately malformed request to see how the hydration layer handles errors.

The test: Provide a JSON dataset with mixed types (strings and integers in a number field) and ask for a graph.

The code:

// The Zod schema acts as the firewall
const ChartSchema = z.object({
  data: z.array(z.number()) // Strict number validation
});

const corruptedData = [10, "Error", 30]; // Malformed input

try {
  ChartSchema.parse({ data: corruptedData });
} catch (e) {
  console.log("Sanity check passed: Schema rejected invalid types.");
}

The result:

  1. The SDK intercepted the malformed output from the model.
  2. The Zod validation failed before the React component attempted to render.
  3. The interface displayed a structured error message instead of crashing the view.

This confirms that the “Generative UI” is not just a pass-through pipe. There is an intermediate logic layer that validates the data structure before passing it to the view library. This reduces the loop of experimentation required to get a working chart.

FAQ

Can I export the code generated by Generative UI?

Yes. The interface provides a toggle to view the underlying source (typically React or vanilla HTML/JS). You can lift this code directly into your repository.

Does the Agent work with local files?

Yes, but with a bridge. You must run a local binary that exposes your file system as a tool to the model. The model cannot arbitrarily read your disk without this handshake.

Is Generative UI secure?

Yes. The components render in an isolated context. They cannot access localStorage, cookies, or make unauthorized network requests unless you explicitly inject those capabilities via the tools definition.