Skip to Content
Platform
How-toAdd a chat to your web app

Add a chat to your web app

A browser chat that streams from your deployed agent. Your server’s token broker route mints a short-lived session; credentials never reach the browser.

You need: a deployed runtime (Deploy) and a service account (Credentials) or a federated application.

1. Ask Introspection to allow your origin

Email support@introspection.dev every origin the browser calls from, such as https://app.example.com and http://localhost:3000.

2. Add the broker route

Take the user ID from your session, never the request body.

app/api/introspection/session/route.ts
import { IntrospectionClient } from "@introspection-sdk/introspection-node"; export async function POST(req: Request) { const user = await requireSignedInUser(req); // your auth and rate limit // Service-account tokens last 60 minutes and never refresh: cache below that. const cp = await IntrospectionClient.fromServiceAccount({ clientId: process.env.INTROSPECTION_SERVICE_ACCOUNT_CLIENT_ID!, clientSecret: process.env.INTROSPECTION_SERVICE_ACCOUNT_CLIENT_SECRET!, project: process.env.INTROSPECTION_PROJECT!, }); const runtime = await cp.runtimes.resolve("support-agent"); const session = await cp.runtimes.openRunner(runtime.id, { identity: { user_id: user.id } }); return Response.json( { token: session.session_token, runtimeId: runtime.id, dpUrl: session.deployment.endpoint }, { headers: { "Cache-Control": "no-store" } }, ); }

3. Connect and start a task in the browser

app/chat/agent.ts
import { EventType, IntrospectionApiClient } from "@introspection-sdk/introspection-browser/api"; const session = () => fetch("/api/introspection/session", { method: "POST" }).then((r) => r.json()); const first = await session(); const client = new IntrospectionApiClient({ dpUrl: first.dpUrl, getToken: async () => (await session()).token, // always mint fresh }); await client.connect(); const handle = await client.tasks.start({ runtime_id: first.runtimeId, prompt: text }); saveTaskId(handle.run.task_id); // before streaming: it is also the conversation ID for await (const event of handle.stream()) { if (event.type === EventType.TEXT_MESSAGE_CONTENT) appendDelta(event.delta ?? ""); }

Send the next message with client.tasks.runs.create(taskId, { kind: "prompt", prompt: { text } }). For a Stop button, call handle.cancel(): leaving the loop doesn’t stop the agent.

4. Attach files

const f = await client.files.upload({ file: input.files[0] }); await client.tasks.runs.create(taskId, { kind: "prompt", prompt: { text }, files: [{ id: f.id }] });

5. Resume after a reload

The stream replays the run from its start, so clear that turn first.

const task = await client.tasks.get(taskId); const { active_run_id, latest_run_id } = (task.metadata ?? {}) as Record<string, string>; for await (const event of client.tasks.runs.stream(taskId, active_run_id ?? latest_run_id)) render(event);

Verify

introspection tasks get <task-id> --query status -o table introspection conversations get <task-id>

Expect idle and the exchange in the transcript.

Or ask your agent: “Add a chat panel for support-agent with a token broker.”

Your own identity provider, anonymous visitors, and feedback

Your own identity provider. With federated sign-in, the broker exchanges the user’s ID token instead. An end-user token can’t look up runtimes, so take runtimeId from config.

import { tokenExchange } from "@introspection-sdk/introspection-node"; const { access_token, dp_url } = await tokenExchange({ subjectToken: idToken, clientId: process.env.INTROSPECTION_FEDERATED_CLIENT_ID!, project: process.env.INTROSPECTION_PROJECT!, }); return Response.json({ token: access_token, runtimeId, dpUrl: dp_url });

Visitors who aren’t signed in. Pass a random, secret anonymous_id per visitor, never conversation_id alone (visitors would share one owner). Each counts toward the member limit; history doesn’t carry over to a later user_id.

Thumbs feedback. Send it from your server, tied to the answer it rates (record feedback).

Limits

ProblemDo this
CORS errorYour origin isn’t allowed yet (step 1).
401 from connect()getToken replayed an old token; mint a new one each call.
409 opening a sessionThe member limit is reached, often by anonymous IDs.
Tasks sit in queuedTask concurrency is full (Limits); throttle the broker.
A useChat-style adapterNot provided: render the AG-UI events yourself.
Last updated on