Skip to content

TypeScript SDK

This page covers how to install, configure, and use the official @web-agent/sdk Node / browser package: open a session, start a run, and stream events.

bash
npm install @web-agent/sdk
# or pnpm add @web-agent/sdk / yarn add @web-agent/sdk / bun add @web-agent/sdk

Works in Node 20+ and modern browsers.

Don't ship a server-grade wa_ key to the browser. Keys grant project-wide access; ship them only to server-side code or to environments where you trust the runtime.

One entry point: Client

User-facing API products live on the same Client:

typescript
import { Client } from "@web-agent/sdk";

Create a client with new Client({ apiKey, projectId }); both apiKey and projectId are required — the examples below read them from environment variables.

ResourceProductUse case
client.sessions / messages / eventsDoAnything (open-ended)Free-form input; the agent picks the path.
client.deepResearchDeepResearch (research → report)Standalone API.
client.webSearchWebSearch (query → results)Synchronous by default (wait: true).
client.trackTrack (monitor → snapshot)Long-lived monitors with webhook delivery.

DoAnything — open-ended runs

typescript
import { Client } from "@web-agent/sdk";

const client = new Client({
  apiKey: process.env.WEBAGENT_API_KEY!,
  projectId: process.env.WEBAGENT_PROJECT_ID!,
});

const session = await client.sessions.create({
  instructions: "Search Hacker News for the top 5 stories today.",
});
const run = session.runs[0]!;   // session-create implicitly queues the first run

for await (const event of client.events.stream(session.id, run.id)) {
  console.log(event.type, event.data);
  if (event.type === "run.completed") break;
}

Wire fields stay snake_case to match the API exactly; method names are camelCase.

Resume + heartbeats

typescript
for await (const event of client.events.stream(session.id, run.id, {
  lastEventId: "142",
  includeHeartbeats: false,
})) {
  if (event.type === "run.completed") break;
}

Backed by fetch with manual SSE parsing — works in Node 20+, Bun, Cloudflare Workers, and modern browsers.

Follow-up run vs. inflight message

typescript
// 1. Push into the current run's chat queue
await client.messages.send(session.id, run.id, {
  content: "Also include the comment count for each.",
});

// 2. Start a NEW run in the SAME session
const followup = await client.sessions.createRun(session.id, {
  instructions: "Click into the first post and summarise it.",
});

Answer an input request

typescript
await client.messages.intervene(session.id, run.id, {
  kind: "answer_input_request",
  input_request_id: "ir_01HXX",
  response: { solved: true },
});

The kind discriminator lets the same endpoint handle take-control / release-control too — see Take Control.

Cancel / stop / list

typescript
await client.sessions.cancelRun(session.id, run.id, { reason: "user_cancelled" });
await client.sessions.stop(session.id, { force: false });
const list = await client.sessions.list({ status: "running", limit: 20 });
list.items.forEach((s) => console.log(s.id, s.status));

DeepResearch — research → report

DeepResearch is a standalone API — the path carries no project segment (/v1/deep_research); the project tenant resolves from the Bearer token.

typescript
const run = await client.deepResearch.run({
  topic: "Open-source vector DB landscape 2026",
  depth: "deep",                  // light / standard / deep
  requireOutlineApproval: true,   // outline HITL gate (default on)
});

// Subscribe to events (DR uses the DoAnything SSE channel) + respond to the gate
for await (const event of client.events.stream(
  run.session_id as string, run.run_id as string,
)) {
  if (event.type === "run.input_request") {
    await client.deepResearch.intervene(run.run_id as string, {
      requestId: (event.data as { request_id: string }).request_id,
      response: "approve",  // or { action: "approve_with_edits", edits: [...] }
    });
  }
  if (event.type === "run.completed") break;
}

// Pull the three-piece artifact set (final.md / citations.json / confidence.json)
const artifacts = await client.deepResearch.listArtifacts(run.run_id as string);
const final = await client.deepResearch.getArtifact(
  run.run_id as string, artifacts[0]!.id as string,
);

WebSearch — query → results

WebSearch is project-scoped. run() defaults to wait: true: the server blocks for up to 30s and returns the full result once the search completes; past 30s it returns 202 — call get(runId) to poll.

typescript
// Synchronous (default)
const result = await client.webSearch.run({
  queries: ["best TypeScript ORM 2026"],
  engines: ["tavily"],
  summarize: true,
});

// Async
const pending = await client.webSearch.runAsync({
  queries: ["best TypeScript ORM 2026"],
});
const detail = await client.webSearch.get(pending.run_id as string);

// Refine (re-run within the same run)
await client.webSearch.refine(pending.run_id as string, {
  text: "add site:reddit.com and re-run",
});

Track — long-lived monitors

Track creates a long-running monitor. It checks the target pages repeatedly on a schedule (cron / interval / event), saving a snapshot each time; when a change meets the trigger condition, it notifies you through the channel you configured (such as a webhook).

typescript
const mon = await client.track.create({
  intent:
    "Notify me when the iPhone 17 Pro listing on apple.com goes below $999",
  schedule: { kind: "interval", interval_seconds: 3600 },
  notifyChannel: { kind: "callback_url", url: "https://hooks.example.com/track" },
});

// Lifecycle controls — pause / resume / refine via patch:
await client.track.pause(mon.id as string, { reason: "manual review" });
await client.track.resume(mon.id as string);
await client.track.refine(mon.id as string, {
  triggerDsl: { op: "lt", field: "price", value: 999 },
});

// Manually run one check (bypasses the schedule):
const outcome = await client.track.runNow(mon.id as string);

// Snapshot history (newest first):
const snaps = await client.track.listSnapshots(mon.id as string);
const snap = await client.track.getSnapshot(
  mon.id as string,
  snaps.items[0]!.id as string,
);

// Webhook delivery history + re-send a failed delivery:
const deliveries = await client.track.listDeliveries(mon.id as string, {
  includePayload: true,
});
await client.track.retryDelivery(
  mon.id as string,
  deliveries.items[0]!.id as number,
);

// Cancel terminates the monitor (terminal state):
await client.track.cancel(mon.id as string); // equivalent: client.track.delete(...)

Alignment HITL (optional)

If the supervisor needs you to disambiguate intent, the monitor moves to pending_clarification and emits an alignment.input_request event. Answer with intervene():

typescript
await client.track.intervene(mon.id as string, {
  requestId: "req_align_1",
  response: "SKU A",
});

You can also push free-text guidance into the alignment queue via client.track.message(monId, { content: "…" }).

Errors

typescript
import {
  ApiError,
  InsufficientCreditsError,
  RateLimitedError,
  UnauthorizedError,
} from "@web-agent/sdk";

try {
  await client.sessions.create({ instructions: "…" });
} catch (err) {
  if (err instanceof InsufficientCreditsError) {
    console.log("top up:", err.detail, err.extra);
  } else if (err instanceof ApiError) {
    console.log(err.code, err.statusCode, err.detail);
  } else {
    throw err;
  }
}

Every error class subclasses ApiError and exposes code / statusCode / detail / extra matching the API error envelope.

Types

DoAnything request / response types are top-level exports:

typescript
import type {
  Session, Run, Event, EventType,
  CreateSessionRequest, CreateRunRequest, InterveneRequest,
  RunStatus, SessionStatus, TerminalReason,
} from "@web-agent/sdk";

DR / DS / WS responses come back as Record<string, unknown> (the OpenAPI envelope verbatim) — index by key (run.run_id / run.status). Each resource also exports its own option types (DRRunOptions / DSRunOptions / WSRunOptions).

Next steps