TypeScript

Open interrupts, stream live sessions and receive human decisions from your TypeScript agents - by polling or webhooks.

When an AI agent reaches an action that needs sign-off, it opens an interrupt - a request for a human decision. A teammate handles it in the inbox, and your code continues with the outcome. In between, a session streams the agent's conversation to Live View so operators can watch it as it happens. @vigilator/sdk is the official TypeScript SDK for both:

Open interrupts

A typed client that creates interrupts from your agent and fetches their outcome, with retries and typed errors built in.

Stream live sessions

Start a session when a run begins, append messages as the conversation grows, and end it when the run completes.

Receive events

A webhook handler that verifies signatures and turns interrupt and session deliveries into typed events - no polling loop required.

Fully typed

Every request, response and event is typed from the live API spec, so your editor knows every field - and nothing is shipped at runtime but the SDK itself.

Requirements

Node.js 20 or newer, Bun, Deno, or an edge runtime. The SDK has no runtime dependencies: it uses the platform fetch and WebCrypto, and ships ESM and CommonJS builds with type declarations.

Installation

bun add @vigilator/sdk

Quick Start

Create a client

Grab an API key for your organisation from the dashboard and pass it to Client. It is sent as the x-api-key header on every request, and scopes everything the SDK does to that organisation.

import { Client } from "@vigilator/sdk";

const client = new Client({ apiKey: "your-api-key" });

Open an interrupt

When your agent needs a human, create an interrupt. The action requests are its decision surface - the concrete things a reviewer will approve, edit, reject or respond to - so every interrupt needs at least one:

agent.ts
const interrupt = await client.createInterrupt({
  title: "Send onboarding email",
  description: "The agent wants to email a new customer.",
  externalId: "run_42", // your own correlation id, e.g. the agent run
  actionRequests: [
    {
      name: "send_email",
      args: { to: "customer@example.com" },
      allowedDecisions: ["approve", "edit", "reject"],
    },
  ],
});

A plain question to a human is just an action request whose name is the question, with allowedDecisions: ["respond"].

Learn the outcome

Poll getInterrupt with the id you got back - answered flips true once every action request is decided:

const result = await client.getInterrupt(interrupt.id);
if (result.answered) {
  for (const request of result.actionRequests) {
    console.log(request.name, request.decision, request.decidedByName);
  }
}

Or skip polling entirely and let Vigilator call you - see handling webhooks below.

Prefer to start from a working agent? The examples wire this SDK into LangGraph.js and Mastra, with polling and webhook drivers.

Acting on decisions

Each decided action request tells you what the reviewer chose and everything you need to act on it:

DecisionMeaningWhat to read
approveRun the action exactly as proposed.args as you sent them
editRun the action with the reviewer's changes.editedArgs replaces your args
rejectDo not run the action.responseText may hold the reason
respondThe reviewer answered your question.responseText holds the answer

Every decision also carries decidedByName and decidedAt, so your agent can log who made the call and when. The Decision object exported by the SDK holds the four values, so request.decision === Decision.edit and request.decision === "edit" are the same check.

Client behaviour

The client retries requests that fail with 429/502/503/504 or a connection error - 5 attempts with exponential backoff by default, honouring Retry-After, tunable via the constructor. Each attempt is subject to the timeout (5 seconds by default). There is nothing to close when you are done: the client holds no connection pool of its own.

Input that breaks the API contract - an empty session name, an empty message batch, more than 200 messages - throws a ValidationError before any request is sent.

Live View sessions

Interrupts capture the moments an agent stops to ask; a session covers everything in between. Register one when a run starts, stream the conversation as it progresses, and end it when the run completes - operators watch the transcript grow in Live View and can step in when something looks off. Sessions are standalone: they are not linked to interrupts, so you can monitor agents that never raise one.

Start a session

Give the session the agent's name (that is how it appears in Live View) and, optionally, the opening context - typically the user prompt that started the run. Use externalId for your own correlation id, such as the run or thread id in your agent framework.

agent.ts
const session = await client.startSession("billing-agent", {
  externalId: "thread_42",
  messages: [{ type: "human", content: "Refund order #42" }],
});

Append messages as the conversation grows

Call appendSessionMessages whenever the agent produces or receives messages - each call adds up to 200 messages in conversation order. Every call also marks the session as active, so an agent that keeps appending is never mistaken for a crashed one.

await client.appendSessionMessages(session.id, [
  {
    type: "ai",
    content: "Looking up order #42.",
    name: "billing-agent",
    toolCalls: [{ id: "call_1", name: "lookup_order", args: { id: "42" } }],
  },
  {
    type: "tool",
    content: '{"id": "42", "total": 129.0, "refundable": true}',
    name: "lookup_order",
    toolCallId: "call_1",
    toolStatus: "success",
  },
  { type: "ai", content: "Refund issued.", name: "billing-agent" },
]);

Message is the same type you attach to interrupts - see the message format below. Tool results are optional: send them and the dashboard folds each one behind the chip of the call it answers; leave them out and the transcript shows the calls alone.

End the session

When the run completes, end the session. Ending an already-ended session is a no-op that returns the current state, so it is safe to retry - and safe to call even if a watcher disconnected the session first.

await client.endSession(session.id);

A session that goes quiet for longer than your organisation's session timeout is ended automatically, so a crashed agent never lingers in the queue. If your run may pause for long stretches - waiting on an interrupt, say - end the session before the pause and start a new one afterwards, or make sure the timeout in Integrations → Live View covers it.

Message format

Message mirrors LangChain's message types, with the field names in camelCase so a LangChain BaseMessage maps onto it one-to-one.

FieldApplies toMeaning
typeallhuman, ai or tool.
contentallWhat was said - or, for a tool result, the raw output the tool returned.
nameallThe speaker's label for human and ai turns; the tool's name for a tool result.
toolCallsaiThe calls the agent made, in LangChain's shape: { id, name, args } each. The id is what a later tool result points back at.
invalidToolCallsaiCalls the model produced that could not be parsed - shown with a red marker.
toolCallIdtoolThe id of the call this result answers. Required on tool results.
toolStatustoolsuccess (the default when omitted) or error.
additionalKwargs, responseMetadatahuman, aiProvider extras - reasoning content, model name, token usage. Shown in collapsible sections, never in the bubble.

Send tool results as tool messages, not as ai messages with the JSON in content. The dashboard treats a tool message as evidence rather than speech: Live View folds it behind the chip of the call it answers with a tick or cross, and the inbox shows it monospaced under a Tool result badge. A tool message without a toolCallId, or a human / ai message carrying one, is rejected with a ValidationError before it is sent.

Session state

Each call returns the session as the API sees it:

FieldMeaning
idPass it to appendSessionMessages and endSession.
status"active" or "ended" (also available as SessionStatus.active / SessionStatus.ended).
startedAt, endedAtWhen the session opened and closed, as ISO 8601 strings - endedAt is null while active.
lastActivityAtBumped on every append; Live View shows a session as quiet when this goes stale.
messages, messageCountThe most recent messages (capped at 200) and the true total.
assignee, escalatedThe member watching the session, and whether it was raised to the escalated queue.

Monitored time counts towards your organisation's agent hours, recorded when each session ends. Once a free organisation's monthly allowance is spent, startSession throws UsageLimitError until it resets.

Handling webhooks

Vigilator signs webhook deliveries (Svix / Standard Webhooks). WebhookHandler verifies the signature, parses the payload into a typed event, and dispatches it to registered callbacks. Create an endpoint in the dashboard under your organisation's integrations, copy its signing secret, and register callbacks for the events you care about:

lib/vigilator.ts
import { WebhookHandler } from "@vigilator/sdk";

export const webhooks = new WebhookHandler({ secret: "whsec_..." }); // the endpoint's signing secret

webhooks.on("interrupt.answered", async (event) => {
  for (const request of event.data.actionRequests) {
    // act on approve / edit / reject / respond
  }
});

The callback's event parameter is typed from the event name you subscribe to, so event.data.actionRequests autocompletes. The handler is framework-agnostic - pass the raw request body and the request headers from any web framework:

app/webhooks/vigilator/route.ts
import { WebhookVerificationError } from "@vigilator/sdk";
import { webhooks } from "@/lib/vigilator";

export async function POST(request: Request) {
  try {
    await webhooks.handle(await request.text(), request.headers);
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return new Response(null, { status: 401 });
    }
    throw error;
  }
  return new Response(null, { status: 204 });
}

Verify the raw body

Signature verification runs over the request body exactly as it arrived. Re-serializing parsed JSON breaks the signature - always pass the raw text or bytes, before any body parser touches them.

Events

EventTypeSent when
interrupt.createdInterruptCreatedEventAn agent opened a new interrupt.
interrupt.answeredInterruptAnsweredEventEvery action request on an interrupt was decided.
interrupt.escalatedInterruptEscalatedEventAn interrupt was raised to the escalated queue.
session.startedSessionStartedEventAn agent registered a live session.
session.endedSessionEndedEventA live session ended - by the agent, a watcher, the session timeout, or the organisation's usage limit.
session.actionSessionActionEventA watcher fired a custom action against a live session.
anything newerUnknownEventThe event type postdates the installed SDK version.

Every event shares the same envelope: type, timestamp (the moment the lifecycle edge happened, not the delivery time) and data, with the payload keys in camelCase exactly as documented. Event types newer than the installed SDK parse into UnknownEvent instead of throwing, so handlers keep working across SDK versions - and on accepts any event name, so you can subscribe to a newer type before upgrading.

The session events are how Live View talks back to your agent. SessionEndedEvent carries a reason ("agent", "timeout", "manual" or "limit" - also available on SessionEndReason), so you can tell a run that finished on its own from one a watcher disconnected, one that timed out, or one closed because the organisation's monitored hours ran out. SessionActionEvent carries the action name a watcher pressed - a custom action you defined - and triggeredBy, the member who pressed it; what the action means is up to your agent:

import { SessionEndReason } from "@vigilator/sdk";

webhooks.on("session.action", (event) => {
  if (event.data.action === "pause") {
    pauseRun(event.data.externalId); // your correlation id from startSession
  }
});

webhooks.on("session.ended", (event) => {
  if (event.data.reason === SessionEndReason.manual) {
    stopRun(event.data.externalId); // a watcher pulled the plug
  }
});

Return a 2xx quickly and offload slow work - failed deliveries are retried on a backoff schedule. Callbacks are awaited one after another, and an error thrown by one propagates out of handle. For local testing and delivery tooling, see the webhooks guide.

API Reference

Client

new Client(options: ClientOptions)

Prop

Type

Every method returns a promise of the API's response and throws on failure.

createInterrupt(params)

Creates an interrupt and returns it as an Interrupt.

paramsAn InterruptCreateParams: title, description, at least one ActionRequest in actionRequests, and optionally classificationId, externalId (your own correlation id, e.g. an agent run id) and messages (the conversation leading up to the interrupt).
ReturnsThe created interrupt, including its id - keep it to poll for the outcome.
ThrowsValidationError if the params break the API contract; UsageLimitError, PlanRequiredError, AddonRequiredError, WorkspaceLimitError on quota errors; APIError otherwise; VigilatorConnectionError if the API could not be reached.

getInterrupt(interruptId)

Fetches a single interrupt by id as an Interrupt, including any decisions taken on its action requests.

interruptIdId of the interrupt to fetch.
ReturnsThe interrupt as returned by the API.
ThrowsNotFoundError if the interrupt does not exist; APIError on other error responses; VigilatorConnectionError if the API could not be reached.

startSession(name, options?)

Starts a live session and returns it as a Session.

nameThe agent's name as shown in Live View, e.g. "billing-agent". 1-100 characters.
options.externalIdYour own correlation id for the run, e.g. the run or thread id in your agent framework. Echoed back in every session webhook event.
options.messagesThe opening context as an array of Message, e.g. the user prompt that started the run. At most 200.
ReturnsThe created session, including its id - keep it for the append and end calls.
ThrowsValidationError if the arguments break the API contract; UsageLimitError when the organisation's agent hours are spent, PlanRequiredError / AddonRequiredError / WorkspaceLimitError on other quota errors; APIError otherwise; VigilatorConnectionError if the API could not be reached.

appendSessionMessages(sessionId, messages)

Appends messages to a running session and returns it as a Session.

sessionIdId of the session, as returned by startSession.
messagesThe messages to append, in conversation order, as an array of Message. 1-200 per call.
ReturnsThe session, including its most recent messages and the total messageCount.
ThrowsValidationError if the batch breaks the API contract; NotFoundError if the session does not exist; APIError with code === "CONFLICT" (409) if the session has already ended; VigilatorConnectionError if the API could not be reached.

endSession(sessionId)

Ends a session and returns it as a Session. Ending an already-ended session is a no-op that returns the current state, so the call is safe to retry.

sessionIdId of the session, as returned by startSession.
ReturnsThe ended session.
ThrowsNotFoundError if the session does not exist; APIError on other error responses; VigilatorConnectionError if the API could not be reached.

WebhookHandler

new WebhookHandler(options: WebhookHandlerOptions)

Prop

Type

All three verification methods are async and take the raw body (a string, Uint8Array or ArrayBuffer) and the request headers (a Headers instance or a plain object, as Node, Express and Fastify hand them to you; names are matched case-insensitively).

on(eventType, callback)

Registers a callback for one event type and returns a function that removes it again. Callbacks receive the typed event and may be async. Unknown types are allowed, so callbacks can target event types newer than the installed SDK.

handle(body, headers)

Verifies a delivery, parses it, invokes the callbacks registered for its type (awaited one after another, in registration order), and resolves with the typed event.

constructEvent(body, headers)

Verifies a delivery and resolves with the typed event without dispatching - use this if you prefer routing events yourself.

verify(body, headers)

Verifies the signature only, without parsing the body.

All three reject with WebhookVerificationError on missing or malformed Svix headers, a timestamp outside the tolerance window, a signature mismatch, or a verified body that is not valid JSON.

Errors

All SDK errors extend VigilatorError, so a single instanceof check covers the whole SDK:

ErrorMeaning
VigilatorConnectionErrorThe API could not be reached (network failure, timeout). The underlying error is on cause.
ValidationErrorThe input breaks the API contract, so no request was sent. Carries the list of issues.
APIErrorThe API responded with an error status. Carries status, code, message and data.
UsageLimitError402 USAGE_LIMIT_REACHED: the organisation's usage quota is exhausted.
PlanRequiredError402 PLAN_REQUIRED: the feature requires a paid plan.
AddonRequiredError402 ADDON_REQUIRED: the feature requires an add-on.
WorkspaceLimitError403 WORKSPACE_LIMIT_REACHED: the workspace limit has been reached.
NotFoundError404 NOT_FOUND: the requested resource does not exist.
WebhookVerificationErrorA webhook delivery could not be verified.

UsageLimitError through NotFoundError extend APIError, so error instanceof APIError catches them all.

On this page