Skip to Content
Platform
ReferenceJavaScript SDK

JavaScript SDK

Run tasks against a deployed runtime from a backend or a browser, and read the record of what the agent did.

The JavaScript SDK calls a deployed runtime from your app. The full integration is Add to your app.

Install

npm install @introspection-sdk/introspection-node

Node.js 22 or later. Add @introspection-sdk/introspection-browser for a web app. The client reads INTROSPECTION_TOKEN; see Authentication for other credentials.

EntrypointUse it for
@introspection-sdk/introspection-nodeRunners, tasks, files, conversations, events, metrics, experiments, annotations, tags (backend).
@introspection-sdk/introspection-node/otelIntrospectionLogs: track, feedback, identify (backend).
@introspection-sdk/introspection-browser/apiRun and stream tasks and upload files from a browser over a short-lived session.
@introspection-sdk/introspection-browserIntrospectionClient: 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() optionMeaning
identityuser_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_nameThe agent for every task on this runner. It wins over tasks.start()’s agent_name.
ttl_secondsSession lifetime (server default one hour).
caller, scopeObservability 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() optionEffect
prompt, titleFirst message and display title.
agent_nameRecipe agent to run; ignored when the runner was minted with one.
runtime_idAn exact version, for callers without a runner; a runner ignores it.
metadataClient metadata. model: "<provider>/<model-id>" and thinking_level (off to max) override the root agent’s model for this task.
conversation_metadataString metadata on the conversation.
files, repositoriesAttachments and repositories to clone.
idle_timeout_secondsWarm time between turns (limit).
tags, fork_share_idGrouping 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() optionDefaultMeaning
timeoutMs300000Recovery window, checked only before reconnecting and renewed by new content. When it lapses only your stream throws; the run keeps going.
maxReconnects5Reconnects after connection errors. A 429 (run not ready) waits on Retry-After without spending one.
backoffMs500Base backoff between reconnects.
emitReconnectEventsfalseEmit a CUSTOM introspection.reconnect event per recovery.
signalnoneAbortSignal to stop reading (does not cancel the run).
ErrorMeaningRetry?
RunFailedErrorThe run failed or was cancelled (text() also throws on RUN_ERROR).No.
StreamIncompleteError410, or the stream ended while the task was idle, completed, or awaiting the user.No: read the conversation.
RateLimitErrorStill not ready when the recovery window lapsed.Later.
Other 4xx (not 409/429)Thrown immediately.No: fix the request.
NetworkErrorTransport 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. conversations also has get, retrieve, and exportJson, exportTrajectory, exportArrow, exportStream.
  • events.list needs one event_name; payloads are typed for every family except introspection.track (UnknownEvent). Families: Events & patterns.
  • metrics.query needs absolute timestamps; meta.truncated says whether the row limit was hit. Views and fields: Metrics.
  • List methods return a paginator: await it for one page, for await for 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 create call sets labels, a comment, or reviewers, never two at once. Labels and reviewers are complete sets: read, merge, then write.
  • create needs a signed-in business member’s token; an API key can only list. Labeling in practice: Label production steps.
  • client.tags has list(kind), create, get, update, and delete for the annotation and access catalogues. Only annotation tags take a color.

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

VariablePurpose
INTROSPECTION_TOKENDefault token.
INTROSPECTION_BASE_API_URLControl Plane URL; inside a sandbox, the Data Plane API. (The CLI reads INTROSPECTION_API_BASE_URL.)
INTROSPECTION_BASE_OTEL_URLOTLP endpoint for track/feedback; set inside a sandbox.
INTROSPECTION_SERVICE_NAMEService name on product events.
INTROSPECTION_DEV_TARGETRoute tasks to an introspection dev session.
INTROSPECTION_LOG_LEVELSDK log verbosity.
INTROSPECTION_STRICT_TRACING1 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

Last updated on