Add a chat to your web app
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.
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
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
| Problem | Do this |
|---|---|
| CORS error | Your origin isn’t allowed yet (step 1). |
401 from connect() | getToken replayed an old token; mint a new one each call. |
409 opening a session | The member limit is reached, often by anonymous IDs. |
Tasks sit in queued | Task concurrency is full (Limits); throttle the broker. |
A useChat-style adapter | Not provided: render the AG-UI events yourself. |