Docs

Examples

Fullstack Streaming

A minimal server and React flow for streaming completions and agents into UI messages.

Fullstack streaming connects a browser UI to server-owned model and agent execution. The browser keeps UIMessage[] state. The React hooks send converted core Message[] requests, and the server returns JSONL runtime events.

Use this shape when you want to test direct completion streaming and agent streaming from the same frontend before adding auth, persistence, tools, or trace storage.

Scenario

A local React app needs two panels:

  • completion: one direct model stream through createCompletionStream(...)
  • agent: one reusable agent stream through agent.prompt(messages).stream()

Both panels should append into message state and show the same UI message structure.

Flow

Step Owner
keep UIMessage[] state React hook
convert and send { messages, stream: true } @anvia/react
run model or agent server route
return JSONL events @anvia/server
reduce events back into UIMessage[] @anvia/react

Server

This example uses Hono because it keeps the routes small. The same handlers work in any server framework that can return a Response.

import { AgentBuilder, createCompletionStream } from "@anvia/core";
import type { UIStreamRequest } from "@anvia/core/ui";
import { OpenAIClient } from "@anvia/openai";
import { createEventStream } from "@anvia/server";
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import "dotenv/config";

const instructions = "Answer clearly and concisely.";

const client = new OpenAIClient({
  apiKey: process.env.OPENAI_API_KEY,
});

const model = client.completionModel("gpt-5");
const agent = new AgentBuilder("assistant", model)
  .instructions(instructions)
  .maxTokens(500)
  .build();

const app = new Hono()
  .use(cors())
  .get("/", (c) => c.json({ ok: true }))
  .post("/completion", async (c) => {
    const body = (await c.req.json()) as UIStreamRequest;

    return createEventStream(
      createCompletionStream(model, {
        instructions,
        messages: body.messages,
        maxTokens: 500,
      }),
      { format: "jsonl" },
    );
  })
  .post("/agent", async (c) => {
    const body = (await c.req.json()) as UIStreamRequest;

    return createEventStream(agent.prompt(body.messages).stream(), {
      format: "jsonl",
    });
  });

serve({
  fetch: app.fetch,
  port: Number(process.env.PORT ?? 8000),
});

The request body is the default React wire shape:

type UIStreamRequest = {
  messages: Message[];
  stream: true;
  metadata?: JsonValue;
};

React Client

Use useCompletion for the direct completion route and useChat for the agent route. Both hooks expose messages, status, stop, and reset behavior around the same UI message state.

import { useChat, useCompletion } from "@anvia/react";
import { useState } from "react";

const apiUrl = "http://localhost:8000";

export function App() {
  const completion = useCompletion({
    endpoint: `${apiUrl}/completion`,
    format: "jsonl",
  });
  const agent = useChat({
    endpoint: `${apiUrl}/agent`,
    format: "jsonl",
  });
  const [agentInput, setAgentInput] = useState("");

  return (
    <main>
      <section>
        <h2>Completion</h2>
        <form
          onSubmit={(event) => {
            event.preventDefault();
            void completion.complete();
          }}
        >
          <textarea
            value={completion.input}
            onChange={(event) => completion.setInput(event.target.value)}
          />
          <button disabled={completion.status === "streaming"}>Send</button>
          <button type="button" onClick={completion.stop}>
            Stop
          </button>
          <button type="button" onClick={() => completion.reset()}>
            Reset
          </button>
        </form>
        <p>Status: {completion.status}</p>
        <pre>{JSON.stringify(completion.messages, null, 2)}</pre>
      </section>

      <section>
        <h2>Agent</h2>
        <form
          onSubmit={(event) => {
            event.preventDefault();
            void agent.sendMessage(agentInput);
            setAgentInput("");
          }}
        >
          <textarea
            value={agentInput}
            onChange={(event) => setAgentInput(event.target.value)}
          />
          <button disabled={agent.status === "streaming"}>Send</button>
          <button type="button" onClick={agent.stop}>
            Stop
          </button>
          <button type="button" onClick={() => agent.reset()}>
            Reset
          </button>
        </form>
        <p>Status: {agent.status}</p>
        <pre>{JSON.stringify(agent.messages, null, 2)}</pre>
      </section>
    </main>
  );
}

What To Look For

useCompletion.complete() appends each prompt into the current message state. The completion server receives the full converted core message history on each request.

useChat.sendMessage() does the same for the agent route. Raw agent stream events are reduced into assistant message parts. Text appears as type: "text", reasoning appears as type: "reasoning", and tool calls appear as type: "tool".

An agent tool call becomes part of the assistant message:

{
  id: "tool_tool_1",
  type: "tool",
  toolName: "lookup_order",
  toolCallId: "tool_1",
  callId: "call_1",
  state: "output-available",
  input: { orderId: "A-100" },
  output: { status: "shipped" }
}

The agent final event adds run metadata to the assistant message:

{
  role: "assistant",
  parts: [{ type: "text", text: "Order A-100 has shipped." }],
  metadata: { runId: "run_123" }
}

Next Patterns