Integrate into a full-stack app
This guide uses a Node.js application and the refund-exception-agent from the
Quickstart. The user submits an order, watches the agent
review the refund policy, and can reconnect or rate the result later.
Authenticated user → Application server → Agent runtime
↑ │ │
└──── saved task and feedback ──────────┘The application contract
| Application responsibility | Introspection primitive |
|---|---|
| Authenticate the person using the product | Runner identity derived from the trusted server session |
| Select deployable agent behavior | Stable runtime slug resolved within the credential’s environment |
| Preserve long-running work across requests | Durable task ID stored on the application record |
| Reconstruct what the user saw | Conversation and response IDs retained from trusted events |
| Capture whether the result helped | Feedback or product outcome joined to those evidence IDs |
The example assumes the runtime is deployed and the application server has an Introspection service account. Keep domain behavior in the agent recipe. The application authenticates the user, starts or resumes work, renders events, and retains outcomes; it should not rebuild the refund policy as UI-specific prompt logic.
1. Open an identity-bound runner
Create the Introspection client on the server. Derive the runner identity from the authenticated server session, never from an untrusted request body.
import { IntrospectionClient } from "@introspection-sdk/introspection-node";
const client = 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 runner = await client.runtimes("refund-exception-agent").run({
identity: { user_id: authenticatedUser.id },
});The stable runtime slug resolves the version assigned to the credential’s environment. Do not embed a runtime-version UUID in application code.
Expected result: tasks created by this runner are scoped to the authenticated application user and the selected project environment.
2. Start the task and stream the work
Start one task for the user’s refund request and save its ID before streaming. The task is the durable application binding; the HTTP request is not.
const handle = await runner.tasks.start({
prompt: [
"Review order 1842 against the refund policy.",
"The order is final-sale and inside 30 days.",
"Return the decision and draft the next response.",
].join(" "),
});
await orders.saveAgentBinding(order.id, {
introspectionTaskId: handle.run.task_id,
applicationUserId: authenticatedUser.id,
});
for await (const event of handle.stream()) {
await sendEventToBrowser(event);
await retainEvidenceIdsFromEvent(order.id, event);
}Persist conversation and response IDs when they appear in trusted Introspection events. Those IDs identify the exact answer a user later rates or corrects.
Expected result: the browser receives incremental progress and the final decision, while the application record retains the task, conversation, and response identifiers behind that result.
3. Reconnect from durable state
When the browser reconnects, load the application record, verify that it belongs to the authenticated user, and use the saved task ID to recover canonical state. Reattach to an active run when replay is available. If the replay window has a gap, refetch the durable task and conversation rather than inventing missing events.
The exact continuation, cancellation, replay, and hydration calls live in Continue and control a task and Resumable streams.
A product record may point to an Introspection task. It does not become an Introspection project, and its workspace ID does not replace the end-user identity attached to the runner.
Expected result: refreshing the browser does not create duplicate work, and the user sees the same durable conversation produced by the original task.
4. Record the user’s outcome
The conversation shows what the agent did. Improvement also needs to know what the user thought or what happened next. Post the UI action to the authenticated backend, verify ownership, and attach the signal to the saved evidence IDs.
import { IntrospectionLogs } from
"@introspection-sdk/introspection-node/otel";
const outcomes = new IntrospectionLogs({
token: process.env.INTROSPECTION_TOKEN,
serviceName: "refund-operations-app",
});
const binding = await orders.getAgentBinding(order.id);
assert(binding.applicationUserId === authenticatedUser.id);
outcomes.feedback("refund_decision_feedback", {
taskId: binding.introspectionTaskId,
conversationId: binding.introspectionConversationId,
previousResponseId: binding.introspectionResponseId,
outcome: submittedFeedback.outcome,
comments: submittedFeedback.comments,
});
await outcomes.flush();Use a stable, low-cardinality feedback name. Put the user’s correction or reason in the options instead of generating a new metric name for every case.
Expected result: the product outcome can be opened beside the exact agent conversation and response that caused it.
5. Close resources deliberately
Close the runner after the request or server workflow no longer needs it, and shut the shared client down during application termination.
await runner.close();
process.on("SIGTERM", async () => {
await client.shutdown();
});For long-lived servers, centralize the client and lifecycle policy rather than creating an unbounded client per request.
Direct browser alternative
A browser can stream directly from the Introspection Data Plane after your server exchanges its authenticated user session for a short-lived, identity-bound runner session. The browser must never receive an API key, service-account secret, or general Control Plane token.
Use this architecture when a SPA needs native streaming, cancellation, files, and reconnect behavior without proxying every Data Plane call. Follow Browser applications and the browser token broker for the exact contract.
Common failure pattern: binding work to the browser request
If the task ID exists only in memory or in one streaming response, a refresh can duplicate the task and detach later feedback from the answer the user saw. Save the task ID before streaming, derive identity on the server, and treat event IDs as trusted evidence rather than accepting them from the browser.
Done when
- The server opens the runtime for an authenticated application identity.
- The product saves the task ID before it depends on the stream.
- A reconnect hydrates the original task and conversation without duplicate work.
- Credentials remain on the trusted server boundary.
- Feedback joins to the exact task, conversation, and response.
- Domain behavior remains in the recipe rather than leaking into UI prompt code.
Next
Use Learn from production when a correction, product outcome, or recurring behavior shows what the agent should do better.
For API detail, use the JS SDK reference, Authentication, and Tasks and runs.