Skip to content

Web Framework Integration

Reactive Agents includes first-class support for streaming agent output into React, Vue, and Svelte applications. The pattern is consistent across frameworks:

  1. Server — A route handler calls AgentStream.toSSE() and returns a standard Response
  2. Client — A hook/composable/store consumes the SSE stream and exposes reactive state

The server-side is identical regardless of which client framework you use. AgentStream.toSSE() returns a standard Web API Response, making it compatible with any framework that accepts one.

app/api/agent/route.ts
import { ReactiveAgents, AgentStream } from "reactive-agents";
export async function POST(req: Request) {
const { prompt } = await req.json();
const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withReasoning()
.withTools()
.build();
return AgentStream.toSSE(agent.runStream(prompt));
}
src/routes/api/agent/+server.ts
import { ReactiveAgents, AgentStream } from "reactive-agents";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request }) => {
const { prompt } = await request.json();
const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withTools()
.build();
return AgentStream.toSSE(agent.runStream(prompt));
};
server/api/agent.post.ts
import { ReactiveAgents, AgentStream } from "reactive-agents";
export default defineEventHandler(async (event) => {
const { prompt } = await readBody(event);
const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withTools()
.build();
// Return the Web API Response directly — h3 handles it
return AgentStream.toSSE(agent.runStream(prompt));
});
// Bun.serve
Bun.serve({
port: 3000,
async fetch(req) {
if (req.method === "POST" && new URL(req.url).pathname === "/agent") {
const { prompt } = await req.json();
const agent = await ReactiveAgents.create().withProvider("anthropic").withTools().build();
return AgentStream.toSSE(agent.runStream(prompt));
}
return new Response("Not found", { status: 404 });
},
});

Install the package:

Terminal window
bun add @reactive-agents/react

useAgentStream — Token-by-token streaming

Section titled “useAgentStream — Token-by-token streaming”
import { useAgentStream } from "@reactive-agents/react";
function Chat() {
const { text, status, error, run, cancel } = useAgentStream("/api/agent");
return (
<div>
<button
onClick={() => run("Research the latest AI agent frameworks")}
disabled={status === "streaming"}
>
{status === "streaming" ? "Thinking..." : "Ask"}
</button>
{status === "streaming" && (
<button onClick={cancel}>Stop</button>
)}
<p style={{ whiteSpace: "pre-wrap" }}>{text}</p>
{status === "error" && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}

useAgentStream return values:

PropertyTypeDescription
textstringAccumulated output (grows as tokens arrive)
status"idle" | "streaming" | "completed" | "error"Current execution state
outputstring | nullFull output when status === "completed"
eventsAgentStreamEvent[]All raw events received since last run()
errorstring | nullError message when status === "error"
run(prompt: string, body?) => voidStart a stream; cancels any active stream
cancel() => voidCancel the active stream
import { useAgent } from "@reactive-agents/react";
function Summary({ text }: { text: string }) {
const { output, loading, error, run } = useAgent("/api/agent");
return (
<div>
<button onClick={() => run(`Summarize: ${text}`)} disabled={loading}>
{loading ? "Summarizing..." : "Summarize"}
</button>
{output && <p>{output}</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}
const { text, run } = useAgentStream("/api/agent", {
headers: {
Authorization: `Bearer ${token}`,
"X-Session-Id": sessionId,
},
});
import { useAgentStream } from "@reactive-agents/react";
function AgentWithProgress() {
const { text, events, status, run } = useAgentStream("/api/agent");
const progress = events.findLast((e) => e._tag === "IterationProgress") as
| { iteration: number; maxIterations: number }
| undefined;
return (
<div>
<button onClick={() => run("Research TypeScript 5.x features")}>Run</button>
{progress && (
<progress value={progress.iteration} max={progress.maxIterations} />
)}
<pre>{text}</pre>
</div>
);
}

Install the package:

Terminal window
bun add @reactive-agents/vue
<script setup lang="ts">
import { useAgentStream } from "@reactive-agents/vue";
const { text, status, error, run, cancel } = useAgentStream("/api/agent");
</script>
<template>
<div>
<button
@click="run('Research the latest AI agent frameworks')"
:disabled="status === 'streaming'"
>
{{ status === 'streaming' ? 'Thinking...' : 'Ask' }}
</button>
<button v-if="status === 'streaming'" @click="cancel">Stop</button>
<p style="white-space: pre-wrap">{{ text }}</p>
<p v-if="status === 'error'" style="color: red">{{ error }}</p>
</div>
</template>

All return values are Vue readonly refs — use them directly in templates or watch them:

const { text, status, output } = useAgentStream("/api/agent");
watch(status, (s) => {
if (s === "completed") console.log("Done:", output.value);
});
<script setup lang="ts">
import { useAgent } from "@reactive-agents/vue";
const { output, loading, error, run } = useAgent("/api/agent");
</script>
<template>
<button @click="run('Summarize this article')" :disabled="loading">
{{ loading ? "Working..." : "Summarize" }}
</button>
<p v-if="output">{{ output }}</p>
</template>

Install the package:

Terminal window
bun add @reactive-agents/svelte

Returns a Svelte writable store — subscribe with $ prefix in templates:

<script lang="ts">
import { createAgentStream } from "@reactive-agents/svelte";
const agent = createAgentStream("/api/agent");
</script>
<button
on:click={() => agent.run("Research the latest AI agent frameworks")}
disabled={$agent.status === "streaming"}
>
{$agent.status === "streaming" ? "Thinking..." : "Ask"}
</button>
{#if $agent.status === "streaming"}
<button on:click={agent.cancel}>Stop</button>
{/if}
<p style="white-space: pre-wrap">{$agent.text}</p>
{#if $agent.status === "error"}
<p style="color: red">{$agent.error}</p>
{/if}

Store state shape:

interface AgentStreamState {
text: string; // Accumulated output
status: "idle" | "streaming" | "completed" | "error";
output: string | null;
error: string | null;
events: AgentStreamEvent[];
}
<script lang="ts">
import { createAgent } from "@reactive-agents/svelte";
const agent = createAgent("/api/agent");
</script>
<button
on:click={() => agent.run("Summarize this article")}
disabled={$agent.loading}
>
{$agent.loading ? "Working..." : "Summarize"}
</button>
{#if $agent.output}
<p>{$agent.output}</p>
{/if}

All hooks/stores accept an optional body object merged into the request body:

// React
run("Summarize this", { sessionId: "abc", temperature: 0.3 });
// Vue
run("Summarize this", { sessionId: "abc" });
// Svelte
agent.run("Summarize this", { sessionId: "abc" });

Update your server endpoint to read these:

app/api/agent/route.ts
export async function POST(req: Request) {
const { prompt, sessionId, temperature } = await req.json();
const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withModel({ model: "claude-sonnet-4-20250514", temperature: temperature ?? 0.7 })
.build();
return AgentStream.toSSE(agent.runStream(prompt));
}

All three packages export AgentStreamEvent for typed event handling:

import type { AgentStreamEvent } from "@reactive-agents/react"; // or vue / svelte
function handleEvent(event: AgentStreamEvent) {
if (event._tag === "TextDelta") console.log(event.text);
if (event._tag === "IterationProgress") console.log(event.iteration, event.maxIterations);
if (event._tag === "StreamCompleted") console.log(event.output, event.metadata);
if (event._tag === "StreamError") console.error(event.cause);
}