Skip to content
Go to Dashboard

TypeScript SDK

This page covers how to install, configure, and use the official @web-agent/sdk Node / browser package: open a session, run a task, 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";
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 tasks

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 task = session.tasks[0]!;   // session-create implicitly queues the first task

for await (const event of client.events.stream(session.id, task.id)) {
  console.log(event.type, event.data);
  if (event.type === "task.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, task.id, {
  lastEventId: "142",
  includeHeartbeats: false,
})) {
  if (event.type === "task.completed") break;
}

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

Follow-up task vs. inflight message

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

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

Answer an input request

typescript
await client.messages.intervene(session.id, task.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.cancelTask(session.id, task.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

DR is a Standalone API (pidless: /v1/deep_research); the project tenant resolves from the Bearer token.

typescript
const task = 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(
  task.session_id as string, task.task_id as string,
)) {
  if (event.type === "task.input_request") {
    await client.deepResearch.intervene(task.task_id as string, {
      requestId: (event.data as { request_id: string }).request_id,
      response: "approve",  // or { action: "approve_with_edits", edits: [...] }
    });
  }
  if (event.type === "task.completed") break;
}

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

WebSearch — query → results

WS is project-scoped. run() defaults to wait: true: the server blocks for ≤30s and returns the done envelope; on timeout it returns 202 — call get(taskId) 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.task_id as string);

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

Track — long-lived monitors

Track is project-scoped. A monitor is a long-lived background job: cron / interval / event schedule + an extraction goal + a notify channel (webhook). Each tick produces a snapshot; whenever the trigger DSL judges the diff worth notifying, the channel fires.

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", every_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 fire a tick (bypasses 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 outbox + retry:
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, Task, Event, EventType,
  CreateSessionRequest, CreateTaskRequest, InterveneRequest,
  TaskStatus, SessionStatus, TerminalReason,
} from "@web-agent/sdk";

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

Next steps