Python
Open interrupts, stream live sessions and receive human decisions from your Python 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-py-sdk is the official Python 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 a Pydantic model generated from the live API spec, so your editor knows every field.
Requirements
Python 3.10 or newer. The SDK's only dependencies are httpx and pydantic.
Installation
uv add vigilator-py-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.
from vigilator_py_sdk import Client
client = Client(key="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:
from vigilator_py_sdk import ActionRequest, AllowedDecision, InterruptsPostRequest
interrupt = client.create_interrupt(
InterruptsPostRequest(
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=[
ActionRequest(
name="send_email",
args={"to": "customer@example.com"},
allowedDecisions=[AllowedDecision.approve, AllowedDecision.edit, AllowedDecision.reject],
)
],
)
)A plain question to a human is just an action request whose name is the
question, with allowedDecisions=[AllowedDecision.respond].
Learn the outcome
Poll get_interrupt with the id you got back - answered flips true once every action request is decided:
result = client.get_interrupt(interrupt.id)
if result.answered:
for request in result.actionRequests:
print(request.name, request.decision, request.decidedByName)Or skip polling entirely and let Vigilator call you - see handling webhooks below.
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.
Client behaviour
The client retries requests that fail with 429/502/503/504 or a connection error - 5 attempts with exponential backoff by default, tunable via the constructor. Use it as a context manager to release the connection pool when you are done:
with Client(key="your-api-key") as client:
...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 external_id for your own correlation id, such as the run or thread id in your agent framework.
from vigilator_py_sdk import Message, MessageType
session = client.start_session(
"billing-agent",
external_id="thread_42",
messages=[Message(type=MessageType.human, content="Refund order #42")],
)Append messages as the conversation grows
Call append_session_messages 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.
client.append_session_messages(
session.id,
[
Message(type=MessageType.ai, content="Looking up order #42.", name="billing-agent"),
Message(type=MessageType.ai, content="Refund issued.", name="billing-agent"),
],
)Message is the same model you attach to interrupts. type is human or
ai; name labels the speaker, and toolCalls, additionalKwargs and
responseMetadata carry provider extras such as tool invocations.
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.
client.end_session(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.
Session state
Each call returns the session as the API sees it:
| Field | Meaning |
|---|---|
id | Pass it to append_session_messages and end_session. |
status | SessionStatus.active or SessionStatus.ended. |
startedAt, endedAt | When the session opened and closed - endedAt is None 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, start_session raises 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:
from vigilator_py_sdk import InterruptAnsweredEvent, WebhookHandler
webhooks = WebhookHandler(secret="whsec_...") # the endpoint's signing secret
@webhooks.on("interrupt.answered")
def resume_agent(event: InterruptAnsweredEvent) -> None:
for request in event.data.action_requests:
... # act on approve / edit / reject / respondThe handler is framework-agnostic - pass the raw request body and the request headers from any web framework:
from fastapi import FastAPI, HTTPException, Request, Response
from vigilator_py_sdk import WebhookVerificationError
app = FastAPI()
@app.post("/webhooks/vigilator")
async def vigilator_webhook(request: Request) -> Response:
try:
webhooks.handle(await request.body(), request.headers)
except WebhookVerificationError:
raise HTTPException(status_code=401) from None
return Response(status_code=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 bytes.
Events
| Event | Model | 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 manager review queue. |
session.started | SessionStartedEvent | An agent registered a live session. |
session.ended | SessionEndedEvent | A live session ended - by the agent, a watcher, or the session timeout. |
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. Event models use snake_case attributes for the camelCase payload keys - event.data.action_requests, request.decided_by_name, and so on. Event types newer than the installed SDK parse into UnknownEvent instead of raising, so handlers keep working across SDK versions.
The session events are how Live View talks back to your agent. SessionEndedEvent carries a reason (SessionEndReason.agent, timeout or manual), so you can tell a run that finished on its own from one a watcher disconnected or one that timed out. SessionActionEvent carries the action name a watcher pressed - a custom action you defined - and triggered_by, the member who pressed it; what the action means is up to your agent:
from vigilator_py_sdk import SessionActionEvent, SessionEndedEvent, SessionEndReason
@webhooks.on("session.action")
def on_action(event: SessionActionEvent) -> None:
if event.data.action == "pause":
pause_run(event.data.external_id) # your correlation id from start_session
@webhooks.on("session.ended")
def on_ended(event: SessionEndedEvent) -> None:
if event.data.reason is SessionEndReason.manual:
stop_run(event.data.external_id) # a watcher pulled the plugReturn a 2xx quickly and offload slow work - failed deliveries are retried on a backoff schedule. For local testing and delivery tooling, see the webhooks guide.
API Reference
Client
Client(
key: str,
base_url: str = "https://api.vigilator.dev",
retries: int = 5,
backoff_factor: float = 0.5,
timeout: httpx.Timeout | float = 5.0,
)Prop
Type
create_interrupt(interrupt)
Creates an interrupt and returns it as an InterruptsPostResponse.
interrupt | An InterruptsPostRequest: 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. |
| Raises | UsageLimitError, PlanRequiredError, AddonRequiredError, WorkspaceLimitError on quota errors; APIError otherwise; VigilatorConnectionError if the API could not be reached. |
get_interrupt(interrupt_id)
Fetches a single interrupt by id as an InterruptsIdGetResponse, including any decisions taken on its action requests.
interrupt_id | Id of the interrupt to fetch. |
| Returns | The interrupt as returned by the API. |
| Raises | NotFoundError if the interrupt does not exist; APIError on other error responses; VigilatorConnectionError if the API could not be reached. |
start_session(name, *, external_id=None, messages=None)
Starts a live session and returns it as a SessionsPostResponse.
name | The agent's name as shown in Live View, e.g. "billing-agent". 1-100 characters. |
external_id | 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. |
messages | The opening context as a list 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. |
| Raises | 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. |
append_session_messages(session_id, messages)
Appends messages to a running session and returns it as a SessionsIdMessagesPostResponse.
session_id | Id of the session, as returned by start_session. |
messages | The messages to append, in conversation order, as a list of Message. 1-200 per call. |
| Returns | The session, including its most recent messages and the total messageCount. |
| Raises | 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. |
end_session(session_id)
Ends a session and returns it as a SessionsIdEndPostResponse. Ending an already-ended session is a no-op that returns the current state, so the call is safe to retry.
session_id | Id of the session, as returned by start_session. |
| Returns | The ended session. |
| Raises | NotFoundError if the session does not exist; APIError on other error responses; VigilatorConnectionError if the API could not be reached. |
Input that breaks the API contract - an empty name, an empty message batch, more than 200 messages - raises pydantic.ValidationError before any request is sent.
close()
Closes the underlying HTTP connection pool. Called automatically when the client is used as a context manager.
WebhookHandler
WebhookHandler(secret: str, tolerance: float = 300.0)Prop
Type
on(event_type)
Decorator that registers a callback for one event type. Callbacks receive the typed event. 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 (synchronously, in registration order), and returns the typed event.
construct_event(body, headers)
Verifies a delivery and returns 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 verification methods raise 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 inherit from VigilatorError:
| Error | Meaning |
|---|---|
VigilatorConnectionError | The API could not be reached (network failure, timeout). |
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 subclass APIError, so except APIError catches them all.