JavaScript SDK
The JavaScript SDK calls a deployed runtime from your app. The full integration is Add to your app.
Install
npm install @introspection-sdk/introspection-nodeNode.js 22 or later. Add @introspection-sdk/introspection-browser for a web
app. The client reads INTROSPECTION_TOKEN; see
Authentication for other credentials.
| Entrypoint | Use it for |
|---|---|
@introspection-sdk/introspection-node | Runners, tasks, files, conversations, events, metrics, experiments, annotations, tags (backend). |
@introspection-sdk/introspection-node/otel | IntrospectionLogs: track, feedback, identify (backend). |
@introspection-sdk/introspection-browser/api | Run and stream tasks and upload files from a browser over a short-lived session. |
@introspection-sdk/introspection-browser | IntrospectionClient: track, feedback, identify (browser). |
Run a task
client.runtimes(slug).run() opens a runner,
one session against a deployed runtime. It is the recommended server-side API;
runtimes.resolve() and openRunner() are only for a
browser token broker.
import { EventType, IntrospectionClient } from "@introspection-sdk/introspection-node";
const client = new IntrospectionClient();
const runner = await client.runtimes("support-agent").run({
identity: { user_id: "user_123" },
});
const handle = await runner.tasks.start({ prompt: "Summarize my open tickets." });
for await (const event of handle.stream()) {
if (event.type === EventType.TEXT_MESSAGE_CONTENT) process.stdout.write(event.delta ?? "");
}
// Follow-up turn on the same task; kind "steer" redirects a running turn.
const next = await runner.tasks.runs.create(handle.run.task_id, {
kind: "prompt",
prompt: { text: "Now draft the reply." },
});
console.log(await next.text());run() option | Meaning |
|---|---|
identity | user_id, anonymous_id, or conversation_id, plus tags and string metadata for the end user’s member. Who then owns the task: Run a task for an end user. |
agent_name | The agent for every task on this runner. It wins over tasks.start()’s agent_name. |
ttl_seconds | Session lifetime (server default one hour). |
caller, scope | Observability payload echoed on runner.context; a narrower scope. |
The runner has clients including tasks, files, conversations, events,
metrics, and shares. runner.context shows what the run resolved to
(runtime, recipe, experiment arm). Finish with await runner.close() and
await client.shutdown(); a closed runner throws RunnerExpiredError.
Start options
tasks.start() / tasks.create() option | Effect |
|---|---|
prompt, title | First message and display title. |
agent_name | Recipe agent to run; ignored when the runner was minted with one. |
runtime_id | An exact version, for callers without a runner; a runner ignores it. |
metadata | Client metadata. model: "<provider>/<model-id>" and thinking_level (off to max) override the root agent’s model for this task. |
conversation_metadata | String metadata on the conversation. |
files, repositories | Attachments and repositories to clone. |
idle_timeout_seconds | Warm time between turns (limit). |
tags, fork_share_id | Grouping tags; start from a shared conversation. |
There is no structured-output option: see Structured output.
Continue and control a task
Reattach after a reload
const task = await runner.tasks.get(taskId);
const runId = task.metadata.active_run_id ?? task.metadata.latest_run_id;
for await (const event of runner.tasks.runs.stream(taskId, runId)) render(event);The SDK has no since cursor: a reattach replays the run from its start (or
one MESSAGES_SNAPSHOT), so render idempotently. Never start a replacement
task because a stream closed.
Stream options and errors
stream() option | Default | Meaning |
|---|---|---|
timeoutMs | 300000 | Recovery window, checked only before reconnecting and renewed by new content. When it lapses only your stream throws; the run keeps going. |
maxReconnects | 5 | Reconnects after connection errors. A 429 (run not ready) waits on Retry-After without spending one. |
backoffMs | 500 | Base backoff between reconnects. |
emitReconnectEvents | false | Emit a CUSTOM introspection.reconnect event per recovery. |
signal | none | AbortSignal to stop reading (does not cancel the run). |
| Error | Meaning | Retry? |
|---|---|---|
RunFailedError | The run failed or was cancelled (text() also throws on RUN_ERROR). | No. |
StreamIncompleteError | 410, or the stream ended while the task was idle, completed, or awaiting the user. | No: read the conversation. |
RateLimitError | Still not ready when the recovery window lapsed. | Later. |
Other 4xx (not 409/429) | Thrown immediately. | No: fix the request. |
NetworkError | Transport failure after maxReconnects. | Yes. |
Ordinary HTTP calls retry twice (429 on any method, 502/503/504 on
GET, 500 ms base backoff); uploads are never retried. There is no
per-request timeout.
Answer an interrupt
When the agent asks the user, the task is awaiting_user and the run ends with
an interrupt outcome. Resume with one entry per interrupt:
const resumed = await runner.tasks.runs.resume(taskId, {
resume: [{ interruptId, status: "resolved", payload: { approved: true } }],
});Payloads and the full flow: Ask the user.
Cancel
Stopping iteration does not stop the agent. handle.cancel() aborts the turn
and keeps the sandbox warm; { mode: "drain", drain_within_seconds } lets it
finish, then tears down. See Task operations.
Files
const spec = await runner.files.upload({ file: new Uint8Array(buffer), name: "spec.md" });
await runner.tasks.start({ prompt: "Summarize spec.md.", files: [{ id: spec.id }] });
const bytes = await runner.files.download(spec.id);The agent gets a read-only path under /workspace/files/, not the contents.
files also has createText, list, get, update, delete,
downloadStream, and versions. list() filters by name, file_type,
storage_path, task_id, member_id, tag, and metadata; for category,
name_contains, or conversation_id (such as one task’s outputs), use
introspection files list --filter.
Shares and tags: Sharing.
Conversations, events, and metrics
An agent task’s conversation id is its task id.
for await (const c of runner.conversations.list({ sort: "cost", direction: "desc" })) {
console.log(c.id, c.cost.usd);
}
for await (const item of runner.conversations.items.list(taskId)) console.log(item.span_id);
for await (const e of runner.events.list({ event_name: "introspection.feedback", conversation_id: taskId })) {
console.log(e.payload.name);
}
const result = await runner.metrics.query({
view: "conversations",
metrics: [{ measure: "total_cost_usd", aggregation: "sum" }],
from_timestamp: "2026-07-01T00:00:00Z",
to_timestamp: "2026-07-08T00:00:00Z",
});- Items are newest first and carry only the turn’s new messages;
conversations.items.get(id, itemId)returns the full input history.conversationsalso hasget,retrieve, andexportJson,exportTrajectory,exportArrow,exportStream. events.listneeds oneevent_name; payloads are typed for every family exceptintrospection.track(UnknownEvent). Families: Events & patterns.metrics.queryneeds absolute timestamps;meta.truncatedsays whether the row limit was hit. Views and fields: Metrics.- List methods return a paginator:
awaitit for one page,for awaitfor all.
Feedback
import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
const logs = new IntrospectionLogs({ serviceName: "support-app" });
logs.feedback("thumbs_down", { conversationId: taskId, comments: "Missed the refund policy", value: 0 });
logs.track("case_closed", { source: "web" });
await logs.shutdown();feedback takes conversationId, previousResponseId (read it from the
task’s metadata.last_response.response_id), comments, and extra keys stored
as properties.
Name and sentiment rules: Send feedback.
The withConversation/withUserId/withAgent/withBaggage helpers need
registerOTelGlobals() from /otel called once at startup. In a browser, use
IntrospectionClient from @introspection-sdk/introspection-browser with the
token your broker mints, never an API key.
Annotations and tags
await client.tags.create({ kind: "annotation", name: "wrong-refund", color: "#f97316" });
await client.annotations.create(
{ trace_id: traceId, span_id: spanId },
{ labels: ["wrong-refund"] }, // or { comment: "..." } or { reviewerEmails: ["dana@acme.example"] }
{ event_id: eventId }, // optional UUIDv7; a retry with the same id never double-posts
);
for await (const a of client.annotations.list({ label: "wrong-refund" })) console.log(a.trace_id, a.labels);- One
createcall sets labels, a comment, or reviewers, never two at once. Labels and reviewers are complete sets: read, merge, then write. createneeds a signed-in business member’s token; an API key can onlylist. Labeling in practice: Label production steps.client.tagshaslist(kind),create,get,update, anddeletefor theannotationandaccesscatalogues. Only annotation tags take acolor.
Browser
IntrospectionApiClient from /api takes dpUrl and a getToken that returns a
newly minted token each time it is called (on connect and after a 401): Web chat.
Experiments
client.experiments has list, get, create(document),
update(id, document), delete(id), startById, endById, and
cancelById; documents match
introspection experiments create.
client.experiments(id) returns a handle with run(), start(), end(), and
cancel(); runner.context.arm_label shows the arm.
Advanced
Warm an interactive task
Create the task without a prompt when the user starts typing (this starts the
sandbox), then send the first prompt run on it. Archive drafts the user
abandons.
const { task } = await runner.tasks.create({ title: "New conversation" });
await runner.tasks.runs.create(task.id, { kind: "prompt", prompt: { text: userMessage } });Environment variables
| Variable | Purpose |
|---|---|
INTROSPECTION_TOKEN | Default token. |
INTROSPECTION_BASE_API_URL | Control Plane URL; inside a sandbox, the Data Plane API. (The CLI reads INTROSPECTION_API_BASE_URL.) |
INTROSPECTION_BASE_OTEL_URL | OTLP endpoint for track/feedback; set inside a sandbox. |
INTROSPECTION_SERVICE_NAME | Service name on product events. |
INTROSPECTION_DEV_TARGET | Route tasks to an introspection dev session. |
INTROSPECTION_LOG_LEVEL | SDK log verbosity. |
INTROSPECTION_STRICT_TRACING | 1 throws instead of warning on an OTel registration conflict. |
Errors
Every HTTP failure throws IntrospectionAPIError (status, code,
requestId, body, retryAfter) or a subclass: AuthenticationError,
InsufficientScopeError (adds missingCapability), RunnerExpiredError,
NotFoundError, ConflictError, ValidationError, RateLimitError,
SandboxUnavailableError, NetworkError (status 0). Streams add
RunFailedError and StreamIncompleteError (which to retry).
The browser /api entrypoint re-exports eight of them; import the rest from
@introspection-sdk/types.
Next
- Add to your app: the full integration.
- Web chat: broker, browser client, feedback.
- Task operations: statuses and cancellation.