TLDR;
In this tutorial, you’ll learn how to build a full-stack agent that preserves response streaming after page reloads. We’ll use Talon, a polyglot agent control plane, paired with the Parallel Search MCP. We’ll then walk through embedding a chat UI in your app that connects to the Talon gateway.
Talon is an agent control plane that manages persisting, hosting, and governing AI agents. Here are some quick features:
- No-code agent configuration: You can configure agent parameters in production without needing to update your code.
- Handling agent connectivity: Talon manages delivering a real time endpoint to your frontend.
- Easy to self host: Built in Rust, Talon can host thousands of concurrent sessions in a single VM before you’re required to scale up.
Since Talon manages the agent persistence, runtime, and UI SDK for you, your chat UI will automatically continue streaming if a user reloads the page.
Now let’s get started.
Prerequisites
To understand this tutorial, you’ll need to have an understanding of React, and:
- OpenAI AI – to enable your agent to perform various tasks using the GPT models
- Parallel Search MCP – a search engine built specifically for AI agents, delivering real-time, accurate, and factual results in an instant
Setting up the project
To get started, set up a standard Vite React app:
mkdir demo
cd demo
pnpm init
pnpm add dotenv express http-proxy-middleware \
@connectrpc/connect@1.7.0 @connectrpc/connect-node@1.7.0 @connectrpc/connect-web@1.7.0 \
@impalasys/talon-client @impalasys/talon-server @impalasys/talon-chat \
react react-dom
pnpm add -D vite @vitejs/plugin-react
pnpm up @impalasys/talon-server
Update your package.json so you can run the app:
{
"type": "module",
"scripts": {
"dev": "node src/server.js",
"build": "vite build",
"start": "NODE_ENV=production node src/server.js"
}
}
After that, a .env file with an OpenAI API key, Parallel Search MCP API key, and a randomly generated Talon JWT secret:
OPENAI_API_KEY=
PARALLEL_API_KEY=
TALON_JWT_SECRET=
Setting up your server
Then, setup the backend server in src/server.js:
import "dotenv/config";
import crypto from "node:crypto";
import express from "express";
import { createPromiseClient } from "@connectrpc/connect";
import { createGrpcTransport } from "@connectrpc/connect-node";
import { gateway, gatewayConnect, manifests } from "@impalasys/talon-client";
import { authorizationHeader, mintJwt, start as startTalon } from "@impalasys/talon-server";
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY is required. Put it in .env or export it before running the demo.");
}
const namespace = "default";
const agent = "copilot";
const parallelSearch = "parallel-search";
const jwtSecret = process.env.TALON_JWT_SECRET || crypto.randomUUID();
const providerName = process.env.OPENAI_API_KEY ? "openai" : "mock";
const modelName = process.env.OPENAI_MODEL || "gpt-4.1-mini";
const talon = await startTalon({
jwtSecret,
provider: process.env.OPENAI_API_KEY
? {
name: "openai",
baseUrl: "https://api.openai.com/v1",
model: modelName,
apiKey: process.env.OPENAI_API_KEY,
}
: undefined,
});
const adminToken = mintJwt(jwtSecret, {
subject: "talon-demo-backend",
ttlSeconds: 3600,
});
const transport = createGrpcTransport({
baseUrl: `http://${talon.grpcEndpoint}`,
httpVersion: "2",
interceptors: [
(next) => async (req) => {
req.header.set("authorization", authorizationHeader(adminToken));
return next(req);
},
],
});
const client = createPromiseClient(gatewayConnect.GatewayService, transport);
await bootstrapAgent();
const app = express();
app.get("/api/talon", (_req, res) => {
res.json({
namespace,
agent,
gatewayUrl: talon.uiEndpoint,
rpcGatewayUrl: `http://${talon.grpcEndpoint}`,
});
});
app.get("/api/talon-token", (_req, res) => {
res.json({
token: mintJwt(jwtSecret, {
subject: "talon-demo-browser",
ttlSeconds: 15 * 60,
namespace,
agent,
}),
});
});
if (process.env.NODE_ENV === "production") {
app.use(express.static("dist"));
} else {
const { createServer } = await import("vite");
const vite = await createServer({ server: { middlewareMode: true }, appType: "spa" });
app.use(vite.middlewares);
}
const httpServer = app.listen(3000, () => {
console.log("Chat app: http://127.0.0.1:3000");
console.log(`Talon gateway: ${talon.uiEndpoint}`);
});
async function bootstrapAgent() {
await ignoreExists(() =>
client.createMcpServer(
new gateway.CreateMcpServerRequest({
server: new manifests.McpServer({
apiVersion: "talon.impalasys.com/v1",
kind: "McpServer",
metadata: new manifests.ObjectMeta({ name: parallelSearch }),
spec: new manifests.McpServerSpec({
transport: "http",
target: "https://search-mcp.parallel.ai/mcp",
headers: { authorization: `Bearer ${process.env.PARALLEL_API_KEY}` }
}),
}),
}),
),
);
await ignoreExists(() =>
client.createAgent(
new gateway.CreateAgentRequest({
ns: namespace,
name: agent,
definition: new manifests.AgentDefinition({
source: {
case: "customSpec",
value: new manifests.AgentSpec({
systemPrompt: "You are a helpful chat assistant in a Node.js demo app. Be concise.",
mcpServerRefs: [parallelSearch],
modelPolicy: new manifests.ModelPolicy({
profiles: [
new manifests.ModelProfile({
name: "default",
model: new manifests.Model({
provider: providerName,
name: modelName,
temperature: 0.7,
}),
}),
],
}),
}),
},
}),
}),
),
);
}
async function ignoreExists(fn) {
try {
await fn();
} catch (error) {
if (!String(error).toLowerCase().includes("already")) throw error;
}
}
async function shutdown() {
httpServer.close();
await talon.stop();
process.exit(0);
}
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
This server starts the Talon node and provides endpoints to tell the frontend how to connect and authorize to Talon. It additionally creates an agent named “copilot” that runs on gpt-4.1-mini. Next, let’s create a simple frontend chat panel to talk to the copilot agent.
Frontend UI
First, create a index.html layout file:
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
Then, you can make use the Talon chat UI package to connect to the copilot agent. Save this in src/main.jsx:
import { createRoot } from "react-dom/client";
import React from "react";
import { createPromiseClient } from "@connectrpc/connect";
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { useEffect, useState } from "react";
import { gateway, gatewayConnect } from "@impalasys/talon-client";
import { TalonCopilot } from "@impalasys/talon-chat";
const styles = {
main: { minHeight: "100vh", display: "grid", gridTemplateRows: "auto minmax(0, 1fr)", fontFamily: "system-ui, sans-serif", background: "#f7f7f8" },
header: { padding: "20px 24px", background: "white", borderBottom: "1px solid #ddd" },
title: { margin: 0, fontSize: 22 },
subtitle: { margin: "4px 0 0", color: "#555" },
section: { minHeight: 0, padding: 20 },
chat: { height: "calc(100vh - 110px)" },
};
function sessionStorageKey(namespace, agent) {
return `talon:session:${namespace}:${agent}`;
}
function App() {
const [config, setConfig] = useState(null);
const [token, setToken] = useState(null);
const [sessionId, setSessionId] = useState(null);
useEffect(() => {
async function loadTalon() {
const [configResponse, tokenResponse] = await Promise.all([
fetch("/api/talon"),
fetch("/api/talon-token"),
]);
setConfig(await configResponse.json());
setToken((await tokenResponse.json()).token);
}
loadTalon();
}, []);
useEffect(() => {
if (!config) return;
setSessionId(localStorage.getItem(sessionStorageKey(config.namespace, config.agent)));
}, [config]);
if (!config || !token) return <main style={styles.main}>Starting Talon...</main>;
const transport = createGrpcWebTransport({
baseUrl: config.rpcGatewayUrl,
interceptors: [
(next) => async (req) => {
req.header.set("authorization", `Bearer ${token}`);
return next(req);
},
],
});
const client = createPromiseClient(gatewayConnect.GatewayService, transport);
const gatewayClient = {
createSession: ({ ns, agent }) =>
client.createSession(new gateway.CreateSessionRequest({ ns, agent })),
getSession: ({ ns, agent, sessionId, messageLimit }) =>
client.getSession(new gateway.GetSessionRequest({ ns, agent, sessionId, messageLimit })),
listSessionMessages: ({ ns, agent, sessionId, pageSize, beforeMessageId }) =>
client.listSessionMessages(
new gateway.ListSessionMessagesRequest({
ns,
agent,
sessionId,
pageSize,
beforeMessageId,
}),
),
};
return (
<main style={styles.main}>
<header style={styles.header}>
<h1 style={styles.title}>Talon Chat Demo</h1>
<p style={styles.subtitle}>A React chat app backed by a local Talon node.</p>
</header>
<section style={styles.section}>
<div style={styles.chat}>
<TalonCopilot
namespace={config.namespace}
agent={config.agent}
gatewayUrl={config.gatewayUrl}
authToken={token}
gatewayClient={gatewayClient}
sessionId={sessionId || undefined}
onSessionChange={(nextSessionId) => {
localStorage.setItem(sessionStorageKey(config.namespace, config.agent), nextSessionId);
setSessionId(nextSessionId);
}}
placeholder="Ask the demo assistant..."
/>
</div>
</section>
</main>
);
}
createRoot(document.getElementById("root")).render(<App />);
Run it
export OPENAI_API_KEY=...
export PARALLEL_API_KEY=...
pnpm dev
Then open http://localhost:3000 and chat with the agent.

Top comments (0)