LangChain
A create_agent support agent whose email is approved, edited or rejected - after it asks a human what to offer.
A support agent built with create_agent and HumanInTheLoopMiddleware. There is no graph code: the middleware pauses the run before any gated tool, and the Vigilator bridge speaks its payload natively. One run raises two interrupts - a question (respond) and an email (approve / edit / reject). Source: Vigilator/examples/langchain.
What you'll see
- Inbox - first an interrupt whose action request is the question with a single Respond box; then an interrupt carrying the drafted email with Approve, Edit (a typed form, from the tool's schema) and Reject.
- Live View - a
support-agentsession per stretch of work: the lookup, then the email draft after your answer, then the send.
Run it
Install
git clone https://github.com/Vigilator/examples
cd examples/langchain
uv syncConfigure
cp .env.example .envFill in VIGILATOR_API_KEY and ANTHROPIC_API_KEY. MODEL accepts any init_chat_model string.
Run with polling
uv run main.pyThe agent looks the customer up and asks what compensation it may offer. Answer in the inbox; it drafts an email and asks again. Edit the subject line and approve; the script resumes with your version and prints the agent's reply.
Or resume by webhook
uv run fastapi dev webhook.py
svix listen http://localhost:8000/webhooks/vigilatorRegister the relay's URL under Integrations → Webhooks for interrupt.answered, put the signing secret in .env, and POST /runs. Each decision is delivered and resumes the run - twice for the default prompt.
How it works
The whole configuration is the interrupt_on map: which tools pause, and what a reviewer may do about each.
INTERRUPT_ON = {
"lookup_customer": False, # runs unreviewed
"ask_human": {"allowed_decisions": ["respond"]}, # a question
"send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
}
agent = create_agent(
model=MODEL, tools=TOOLS, system_prompt=SYSTEM_PROMPT,
middleware=[HumanInTheLoopMiddleware(interrupt_on=INTERRUPT_ON)],
checkpointer=checkpointer,
)Before a listed tool runs, the middleware raises an interrupt with action_requests and review_configs. The bridge maps each request onto a Vigilator action request; a request whose only decision is respond becomes a plain question, named after the question text, which is how the inbox presents it:
name = tool_name
if allowed == ["respond"] and isinstance(args.get("question"), str):
name = args["question"] # a question to a human is an action request named after the questionThe reviewer's decision comes back as Command(resume={"decisions": [...]}), one entry per action request in order - exactly what the middleware expects. approve runs the tool, edit runs it with the new arguments, reject returns the reason to the model, and respond returns the human's text as the tool's result without running it. The webhook receiver does the same from the event:
if isinstance(event, InterruptAnsweredEvent):
thread_id = thread_id_from(event.data.external_id)
if thread_id and pending.get(thread_id) == event.data.id:
del pending[thread_id] # a redelivery is now a no-op
background.add_task(drive, thread_id, to_langgraph_resume(decisions_from_event(event)))Adapting it
- Add your tools and list the risky ones in
INTERRUPT_ON.Trueallows every decision;{"allowed_decisions": [...]}narrows them. - Any middleware stack works - the bridge only reads the interrupt payload.
- Deep Agents use the same middleware - see DeepAgents.