LangChain and LangGraph
stalwart_agents.langchain puts Stalwart's routing, tool check and sagas inside LangChain
and LangGraph agents. No selector, vocabulary or check is in this module. Every decision it
acts on is made by the runtime the wheel carries, on the same machine under your licence,
or by the enterprise qls-router you run in your own environment, where the client names it
(router_url). Stalwart's hosted API makes none of them. A turn that needs a calculation no
tool answers is run by the compute node in the qls-sandbox container image on your own host,
where a container engine and the image are present; elsewhere it is declined, its reason
starting compute off:.
$ pip install stalwart-agents # the middlewares, the graph parts, Guard
$ pip install "stalwart-agents[otel]" # OpenTelemetry spans
$ pip install "stalwart-agents[postgres]" # resume confirmations from Postgres (or [sqlite])
$ pip install "stalwart-agents[nemo]" # the NeMo Guardrails actions and rails
LangChain: two middlewares, an importer, a callback
from langchain.agents import create_agent
from stalwart_agents.langchain import (StalwartRouterMiddleware, StalwartToolGuardMiddleware,
StalwartFeedbackCallback, register_catalogue)
register_catalogue(tools, agent="desk", client=client) # POST /v1/catalogues
agent = create_agent(model, tools, middleware=[
StalwartRouterMiddleware(client, agent="desk", on_unread="refuse"),
StalwartToolGuardMiddleware(client, agent="desk"),
])
- The router reads each user turn into a plan before the model is called. A turn
that resolves to a tool, an ask or a decline costs no model call. A computation is
written by your own model. A turn nothing reads is asked to be rephrased once; after
that it follows your setting:
refuse,model, orask, which hands it to the model only once the user says yes (adelegateinterrupt:approveordecline). - The tool guard checks every tool call, whoever chose it. A refused call runs nothing and gets a result saying why. A write pauses on LangGraph's interrupt, and runs only once the exact arguments shown are approved. The calls of one message run side by side and take turns on the session's one history, so none is lost.
- The composition: NeMo Guardrails' middleware outermost, then the router, then the model; the tool guard around tool execution. Ours never removes another middleware's hook.
StalwartFeedbackCallbackremembers each run's turn and tool.thumbs(run_id, up=False)posts one feedback record. Where the router read the turn, the record is the router's own reading, from the run's state: its verdict, deciding rule and the agent's visible tools; a turn it handed to the model recordsunreadand the model's tool. A turn of several instructions is thumbed per step (step=); an answer the conversation decided, or the reply after a confirmation, is refused withThumbRefused.
LangGraph: the parts, or the whole graph
from stalwart_agents.langchain.graph import create_stalwart_agent
agent = create_stalwart_agent(model, tools, catalogue=catalogue, agent="desk", client=client,
checkpointer=checkpointer) # verified once per graph digest
| Part | Does |
|---|---|
StalwartState |
our channels beside your messages: the session, the history (append-only), trust labels, the saga, open confirmations, the plan |
stalwart_messages, trim_pairs |
add_messages, and a tool call never left without its result; results in call order; trimming that keeps pairs |
stalwart_route_node |
the turn read into a plan, returned as Command with its destinations declared |
StalwartToolNode |
every call checked (freeze, check, run once, record, push the undo), one step a pass, so a crash mid-call is resumed honestly |
stalwart_edge |
a conditional edge that can take only the destinations it declares, and a rejected_route otherwise |
stalwart_rollback_node, stalwart_rewind_node |
a failed run undone last-in first-out through the check, then the memory rewound, never with an undo pending and never the history |
stalwart_hand_off_node |
a hand-off that carries the handing agent's label |
stalwart_branches, stalwart_join_node |
a message's calls fanned out as Send branches, checked one history at a time and undone in reverse order of completion; a subgraph's undos handed to its parent when it completes, its failure the parent's when it does not |
with_stalwart_channels |
our channels added to any state schema, for a supervisor, a swarm or your own graph that runs a stalwart agent across turns |
create_stalwart_agent, build_stalwart_graph |
the graph generated from your catalogue, verified by the graph mode; compile() verifies whatever nodes you add |
validate_state, validating |
each break of the state named, the history's chain token too with history_verifier; debug mode stops at the node that caused it |
export_graph, replay_witness |
the export the verifier reads, and a finding's witness replayed with every tool stubbed |
Plain Python: Guard
from stalwart_agents.langchain.guard import Guard
guard = Guard(client, agent="desk", catalogue=catalogue, tools=[charge_card, refund_card])
with guard.run(guard.session()) as run:
run.call(charge_card, order_id=o, amount=a) # checked, keyed, its undo pushed
run.call(provision_account, order_id=o) # raises -> the charge refunded
stalwart_agents.langchain.guard loads no LangChain and no LangGraph.
Around the agent
stalwart_agents.langchain.server.ResumeServer: an ASGI app over your checkpointer. It lists the open interrupts (nested and parallel ones each by its own id), resumes one on any worker, refuses a stale resume with409 interrupt_stale, streams a thread's steps (resumable byLast-Event-ID), and lets an operator roll a thread back:ResumeServer(operator_token=...), sent asAuthorization: BeareronPOST /threads/{id}/rollback; with none set, the route answers 403. Two answers to one interrupt are told apart by a claim (SqlClaimsover your database for several workers). Each answer carries the turn'srefusalandrollbackas data.Webhook(url, secret)posts each interrupt with an HMAC signature, a timestamp and a five-minute replay window; a post that fails never fails the turn.- Receipts: a call carries
current_receipt()-- the session's history ending on the check that allowed it -- so an enforcement point in front of your tools (the route of the Kong recipe) admits it on your agent's own check rather than deciding it again. stalwart_agents.langchain.otel:OtelTracerandNodeSpansgive a span for every node, plan, check, call, undo, interrupt and resume, with the decision as a span event. Content is off by default.python -m stalwart_agents.langchain.trace show|html|diffrenders a run from its spans file, or from the checkpointer alone.stalwart_agents.langchain.nemo: thestalwart_routeandstalwart_check_toolactions, and an input rail and execution guard in Colang 1.0 and Colang 2. Both snippets verify in our solver.stalwart_agents.langchain.redteam.RedTeam: a planner, an exploiter and a critic aimed at the hunter's findings, or open, against your graph and your model, every tool a stub. Not realised in budget says nothing about safety.graph_from_exportbuilds the target from an export where your own code is not at hand.