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/sdkQuick 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:
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:
| Decision | Meaning | What to read |
|---|---|---|
approve | Run the action exactly as proposed. | args as you sent them |
edit | Run the action with the reviewer's changes. | editedArgs replaces your args |
reject | Do not run the action. | responseText may hold the reason |
respond | The 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.
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.
| Field | Applies to | Meaning |
|---|---|---|
type | all | human, ai or tool. |
content | all | What was said - or, for a tool result, the raw output the tool returned. |
name | all | The speaker's label for human and ai turns; the tool's name for a tool result. |
toolCalls | ai | The calls the agent made, in LangChain's shape: { id, name, args } each. The id is what a later tool result points back at. |
invalidToolCalls | ai | Calls the model produced that could not be parsed - shown with a red marker. |
toolCallId | tool | The id of the call this result answers. Required on tool results. |
toolStatus | tool | success (the default when omitted) or error. |
additionalKwargs, responseMetadata | human, ai | Provider 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:
| Field | Meaning |
|---|---|
id | Pass it to appendSessionMessages and endSession. |
status | "active" or "ended" (also available as SessionStatus.active / SessionStatus.ended). |
startedAt, endedAt | When the session opened and closed, as ISO 8601 strings - endedAt is null while active. |
lastActivityAt | Bumped on every append; Live View shows a session as quiet when this goes stale. |
messages, messageCount | The most recent messages (capped at 200) and the true total. |
assignee, escalated | The 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:
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:
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
| Event | Type | Sent when |
|---|---|---|
interrupt.created | InterruptCreatedEvent | An agent opened a new interrupt. |
interrupt.answered | InterruptAnsweredEvent | Every action request on an interrupt was decided. |
interrupt.escalated | InterruptEscalatedEvent | An interrupt was raised to the escalated queue. |
session.started | SessionStartedEvent | An agent registered a live session. |
session.ended | SessionEndedEvent | A live session ended - by the agent, a watcher, the session timeout, or the organisation's usage limit. |
session.action | SessionActionEvent | A watcher fired a custom action against a live session. |
| anything newer | UnknownEvent | The 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.
params | An 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). |
| Returns | The created interrupt, including its id - keep it to poll for the outcome. |
| Throws | ValidationError 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.
interruptId | Id of the interrupt to fetch. |
| Returns | The interrupt as returned by the API. |
| Throws | NotFoundError 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.
name | The agent's name as shown in Live View, e.g. "billing-agent". 1-100 characters. |
options.externalId | Your 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.messages | The opening context as an array of Message, e.g. the user prompt that started the run. At most 200. |
| Returns | The created session, including its id - keep it for the append and end calls. |
| Throws | ValidationError 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.
sessionId | Id of the session, as returned by startSession. |
messages | The messages to append, in conversation order, as an array of Message. 1-200 per call. |
| Returns | The session, including its most recent messages and the total messageCount. |
| Throws | ValidationError 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.
sessionId | Id of the session, as returned by startSession. |
| Returns | The ended session. |
| Throws | NotFoundError 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:
| Error | Meaning |
|---|---|
VigilatorConnectionError | The API could not be reached (network failure, timeout). The underlying error is on cause. |
ValidationError | The input breaks the API contract, so no request was sent. Carries the list of issues. |
APIError | The API responded with an error status. Carries status, code, message and data. |
UsageLimitError | 402 USAGE_LIMIT_REACHED: the organisation's usage quota is exhausted. |
PlanRequiredError | 402 PLAN_REQUIRED: the feature requires a paid plan. |
AddonRequiredError | 402 ADDON_REQUIRED: the feature requires an add-on. |
WorkspaceLimitError | 403 WORKSPACE_LIMIT_REACHED: the workspace limit has been reached. |
NotFoundError | 404 NOT_FOUND: the requested resource does not exist. |
WebhookVerificationError | A webhook delivery could not be verified. |
UsageLimitError through NotFoundError extend APIError, so error instanceof APIError catches them all.