Metadata-Version: 2.5
Name: stalwart-agents
Version: 1.0.51
Summary: Build your own AI agent, powerful but controlled: your tools and your rules, checked on your own machine.
Project-URL: Homepage, https://stalwart.it
Project-URL: Documentation, https://api.stalwart.it/sdk/
Project-URL: API reference, https://api.stalwart.it/
Author-email: Stalwart Technologies Ltd <crm@stalwart.it>
License-Expression: LicenseRef-Stalwart-Proprietary AND MIT
License-File: LICENSE
License-File: src/stalwart_agents/client/_vendor/LICENSE
Keywords: agents,ai,guardrails,langchain,langgraph,mcp,red-team,tool-calling,verification
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Typing :: Typed
Requires-Python: <3.15,>=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: langchain-core<2,>=1
Requires-Dist: langchain<2,>=1
Requires-Dist: langgraph<2,>=1
Requires-Dist: pyyaml>=6
Requires-Dist: uvicorn>=0.30
Provides-Extra: all
Requires-Dist: langchain-anthropic<2,>=1; extra == 'all'
Requires-Dist: langchain-google-genai<5,>=4; extra == 'all'
Requires-Dist: langchain-mistralai<2,>=1; extra == 'all'
Requires-Dist: langchain-ollama<2,>=1; extra == 'all'
Requires-Dist: langchain-openai<2,>=1; extra == 'all'
Provides-Extra: anthropic
Requires-Dist: langchain-anthropic<2,>=1; extra == 'anthropic'
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: jsonschema>=4; extra == 'dev'
Requires-Dist: langgraph-checkpoint-sqlite<4,>=3.1; extra == 'dev'
Requires-Dist: langgraph-supervisor<0.1,>=0.0.31; extra == 'dev'
Requires-Dist: langgraph-swarm<0.2,>=0.1; extra == 'dev'
Requires-Dist: lupa<3,>=2.5; extra == 'dev'
Requires-Dist: markdown2==2.5.5; extra == 'dev'
Requires-Dist: mcp<3,>=2.3; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: opentelemetry-api>=1.25; extra == 'dev'
Requires-Dist: opentelemetry-sdk>=1.25; extra == 'dev'
Requires-Dist: pdoc==16.0.0; extra == 'dev'
Requires-Dist: pygments==2.21.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: twine>=5; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Provides-Extra: google
Requires-Dist: langchain-google-genai<5,>=4; extra == 'google'
Provides-Extra: mistral
Requires-Dist: langchain-mistralai<2,>=1; extra == 'mistral'
Provides-Extra: nemo
Requires-Dist: nemoguardrails<0.25,>=0.24; (python_version < '3.14') and extra == 'nemo'
Provides-Extra: ollama
Requires-Dist: langchain-ollama<2,>=1; extra == 'ollama'
Provides-Extra: openai
Requires-Dist: langchain-openai<2,>=1; extra == 'openai'
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.25; extra == 'otel'
Requires-Dist: opentelemetry-sdk>=1.25; extra == 'otel'
Provides-Extra: postgres
Requires-Dist: langgraph-checkpoint-postgres<4,>=3.1; extra == 'postgres'
Provides-Extra: sqlite
Requires-Dist: langgraph-checkpoint-sqlite<4,>=3.1; extra == 'sqlite'
Description-Content-Type: text/markdown

# stalwart-agents

Build your own AI agent, powerful but controlled: your tools and your rules, checked on your
own machine. Declare it in one file -- its tools, its rules and its users -- then check it and
run it. You don't need to learn an agent framework for any of this.

**Before you start.** You need a Stalwart account and its API key. Sign up at
[stalwart.it](https://stalwart.it/register-interest): a free account opens with its first key,
in the console, in English or Portuguese. Set the key as `QLS_API_KEY`, and run `stalwart licence activate` once on each
machine that runs your agents. What each plan includes is on
[stalwart.it](https://stalwart.it/products). Nothing in the package works without the key and
an activated licence, except activating the licence itself, `stalwart doctor`, which says what
is missing, and `stalwart check`, which checks a certificate offline
([the licence and the runtime](https://api.stalwart.it/sdk/runtime/)).

```
$ pip install stalwart-agents                # everything: the agents, the client, the gateway, the runtime
$ pip install "stalwart-agents[anthropic]"   # with a model provider: anthropic, openai, google, mistral or ollama
$ pip install "stalwart-agents[all]"         # with every provider
```

The documentation is at [api.stalwart.it/sdk](https://api.stalwart.it/sdk/), in English and
[Portuguese](https://api.stalwart.it/pt/sdk/). Start with
[Build an agent](https://api.stalwart.it/sdk/agents/): an agent of your own, from sign-up to
`stalwart serve`.

The local runtime ships for Linux (x86-64 and arm64), macOS on Apple silicon and Windows (x86-64).
It reads every turn and checks every tool call on your own machine, under your licence. Anywhere
else -- an Intel Mac, Alpine, Windows on ARM -- the same command installs the package without
it, and plans and checks come from the enterprise `qls-router` you run in your own environment
(`QLS_ROUTER_URL`). Stalwart's hosted API answers neither: with no runtime and no router, a
plan or a check raises `SelectorUnavailableError`, saying which to set up.

```yaml
# agent.yaml
agents:
  desk:
    model: "anthropic:claude-sonnet-5-5"   # your provider and key, from your environment
    tools: [get_order, charge_card, refund_card, read_inbox]
    hands_off_to: [billing]
  billing:
    privilege: high
    tools: [issue_refund]
tools:
  - python: shop.tools                     # plain functions, schemas from their type hints
  - mcp: https://tools.internal/mcp        # an MCP server's tools/list
    receipts: true                         # behind a gateway that admits a call on the agent's own check
  - openapi: ./crm.yaml                    # each HTTP operation a tool
  - name: refund_card
    writes: true
rulebook: ./policy.co                      # Colang 1.0 or 2.x
runs: atomic                               # a failed run is undone
storage: sqlite:///runs.db                 # where runs are kept; postgres://... for several workers
```

Or in Python, with the same meaning:

```python
from stalwart_agents import Agent, tool

@tool(writes=True, undo="refund_card")
def charge_card(order_id: str, amount: Decimal) -> dict: ...

desk = Agent("desk", tools=[get_order, charge_card, refund_card], rulebook="policy.co",
             model="anthropic:claude-sonnet-5-5")
reply = desk.chat("refund order 4411", user="u-17")
if reply.waiting:                          # a write waits for the person
    reply = desk.confirm("u-17", "approve")
```

Before a turn runs, the agents' graph is proved once (`verify=False` runs it unproved).
A turn nothing reads is asked to be said again once; then `on_unread` decides: `refuse`,
`model`, `ask`, where the person says whether the model may have it
(`desk.confirm(user, "approve")` or `"decline"`), or `answer`, from your documents (below).
A request no tool covers -- a calculation, say -- is what `model` is for. With `refuse`, no
`hands_off_to`, `composer: template` for replies and `composer: extract` for answers, no model
is ever called ([worked examples](https://api.stalwart.it/sdk/examples/)).

**Guarded and proved.** Two checks sit around the model, both decided by native code on your own
host: the router decides whether a model is called at all, and every tool call is checked --
`allow`, `confirm` or `refuse` -- whether the router or the model chose it. Before the first turn
the graph is proved: every path to a write without its check, its confirmation or its order
rule is a finding, and `Agent(...)` raises `NotVerified` with nothing run. The rulebook it is
proved against is the one it runs under: at start-up, and on every `stalwart verify`, it is
registered with the catalogue for the tool check to enforce (`RulebookNotHeld` where it cannot be).

```python
from stalwart_agents import Agent, NotVerified

desk = Agent.from_file("agent.yaml")
try:
    desk.chat("refund order 4411", user="u-17")
except NotVerified as refused:            # nothing ran
    for finding in refused.findings:
        print(finding["rule"], finding.get("action"))   # QG012 issue_refund
```

```console
$ stalwart verify agent.yaml --replay    # in CI: exits 1 when the gate fails
Registered as catalogue version 7, with the rulebook policy.co: the tool check enforces the rules verified here.
QG012 High on issue_refund (agent.yaml:7)
  Replay: reproduced
Gate: failed
```

**In CI.** `stalwart verify` runs the code checks on your machine and the bug hunter on
Stalwart's servers -- nothing besides the package is installed. A runner activates its licence
first and releases it last:

```yaml
    env:
      QLS_API_KEY: ${{ secrets.QLS_API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - run: pip install stalwart-agents
      - run: stalwart licence activate
      - run: stalwart verify agent.yaml --replay --keep findings/ --dependencies uv.lock
      - if: always()
        run: stalwart licence release
```

Exit 0 is a passed gate, 1 a failed one, 2 a file it cannot read or no key, and 3 a hunt that
could not run -- never a clean result
([the command in full](https://api.stalwart.it/sdk/guardrail/#checking-an-agent-in-ci-stalwart-verify)).

Every check, every guarantee and what is proved, with examples:
[Guarded and proved](https://api.stalwart.it/sdk/guardrail/).

**Rules without Colang.** The rules over your tools can be a YAML sheet a person reads
aloud, compiled to the same rulebook a Colang policy becomes:

```yaml
# rules.yaml
rules:
  - action: issue_refund
    requires: [supervisor_approved]
    when: { amount_over: 200 }
  - action: close_account
    requires_confirmation: true
  - action: delete_customer
    never: true
  - action: send_statement
    after: [verify_identity]
```

`stalwart rules compile rules.yaml` compiles it and lints it against your newest catalogue
-- a rule for an action the catalogue lacks, a write the sheet never names -- and `--save`
registers it on the catalogue, where every tool check enforces it. A construct the
compiler cannot state exactly is refused with its line, never approximated.

**Replies true to what was done.** `replies:` holds every reply to the run it follows:

```yaml
replies:
  rules: reply-rules.yaml        # personal data by class, phrases never to say, allowed offers
  phrasings: phrasings.yaml      # how each write's outcome is told: done, failed, refused, ...
  composer: model                # or template: the reply is the phrasings, and no model writes it
```

Each sentence that speaks of a write is held to that write's outcome -- *done* only if it
ran, a *failed* or *refused* one never told as done -- and every amount, date, identifier
or address in it to a value the call was sent or returned. A sentence that misstates is
replaced by the outcome's phrasing, and a write the reply leaves out is added. Personal
data the rules withhold is masked, or the reply held back. It runs in the local runtime,
and only counts leave the machine.

**Answers from your documents.** A question no tool answers can be answered from your own
documents, never by a tool's side effect: documentation describing a refund is never a
refund.

```yaml
answers:
  index: sources.json      # `stalwart sources build docs/*.md --out sources.json`
  composer: extract        # the documents' own sentences; compose: your model writes from them
agents:
  desk:
    on_unread: answer
```

Each sentence cites the passage it draws on, and the support check removes one that
states a number, date, amount, name or negation its passages do not. It checks those
facts, not the meaning around them: a sentence that keeps a passage's facts and draws a
conclusion the passage does not is not caught, which is why `extract` -- the passages'
own sentences -- is the default. `compose` needs a plan that includes composed answers.

**Calculations on your host.** A turn that needs a calculation no tool answers -- *what is
the average price of my orders* -- is a compute step, on every plan: the tools it reads run,
your own model writes one Python function from the data's shape (never its values), the
runtime admits it -- no socket, no file, no import outside a short list -- and the
`qls-sandbox` image runs it in a fresh container with no network. It needs a container
engine: `python -m stalwart_agents.sandbox` fetches the image this release runs, and without
the engine or the image the turn is declined, its reason starting `compute off:`.

**What reaches Stalwart's servers.** Conversations, tool calls, a calculation's data, function
and result, your documents and their index stay on your host. What is sent -- a solve and its inputs, a catalogue's tool descriptions, a
rule sheet, a guardrail generation prompt, and a thumb's feedback record, which carries the
words of the turn it rates -- is scanned first for e-mail addresses, telephone
numbers, payment card numbers, IBANs, UK National Insurance and Brazilian CPF numbers, and
any identifiers you list as your own (`Client(own_identifiers=[...])`). A match sends
nothing: `PersonalDataError` names each match's kind and where it is, never its value.

A person's thumb on a reply is feedback the hosted miner learns your users' words from:

```python
reply = desk.chat("what is in order 4411?", user="u-17")
desk.thumbs(reply, up=False)        # one record: the turn's words, the tool, the router's verdict and rule
```

The record holds the rated turn's words and the router's reading of them -- the tool, its
verdict and deciding rule, the tools the agent could reach -- and nothing else: no reply
text, argument value, tool result or earlier turn. A turn the router could not read and
the model answered records the verdict `unread` and the tool the model called. The same
thumb twice is one record. Some thumbs are refused with `ThumbRefused`, saying what to
thumb instead: a turn of several instructions is thumbed one step at a time
(`step=0`, `step=1`, ...), and the reply after a confirmation is thumbed on the turn
that named the tool. `stalwart serve` takes the same thumb at
`POST /threads/{id}/feedback`, from the session that was shown the reply only.

An MCP server's tools are called in an MCP session per user: the `user` a turn names,
or its thread under `stalwart serve`. What a server keeps for one session never reaches
another user. `desk.close()` ends every session; `stalwart serve` ends them when it stops.

A tool's words are plain: `writes`, `undo`, `irreversible`, `retry_safe`,
`reads_outside`, `sends_out`, `after`, `not_after`, `at_most`, and the words the bug
hunter's OWASP rules read (`argument_kinds`, `sandboxed`, `confidential`, ...).
A model is `provider:model`, resolved with your own key from your environment. A
missing key is named before anything is sent. A model can also be a plain function
that takes the conversation and returns the reply.

| Command | Does |
|---|---|
| [`stalwart catalogue import openapi.yaml`](https://api.stalwart.it/sdk/examples/#one-catalogue-from-the-tools-you-have) | the catalogue from an OpenAPI document, or from a TypeScript project's exported functions tagged `@read` or `@write`, with the code checks run over its sources first; registered with `QLS_API_KEY` |
| [`stalwart rules compile rules.yaml [--save]`](https://api.stalwart.it/sdk/examples/#every-tool-call-checked-against-your-rules) | the rule sheet compiled and linted against the newest catalogue; `--save` registers it |
| [`stalwart sources build\|ask`](https://api.stalwart.it/sdk/examples/#answers-from-your-documents) | the documents your agents answer questions from: `build` makes their index here, from local files, and `ask --index` answers a question from it; the documents never leave your host |
| [`stalwart verify agent.yaml`](https://api.stalwart.it/sdk/guardrail/#checking-an-agent-in-ci-stalwart-verify) | registers the catalogue with its rulebook, and has the service's bug hunter hunt the graph the agents run and the rulebook; exits 1 when the gate fails. `--export-only FILE` writes the export for CI; `--replay` replays each graph finding's witness on the agents, every tool a stub, and fails the gate on one that does not reproduce |
| [`stalwart serve agent.yaml`](https://api.stalwart.it/sdk/agents/#serve-it) | runs the agents over HTTP: turns, confirmations resumed on any worker, the step stream, an operator's rollback (with `--operator-token` or `QLS_OPERATOR_TOKEN`; 403 without), a person's thumb on a reply (`POST /threads/{id}/feedback`), and the interface at `/` |
| [`stalwart trace show\|html\|diff`](https://api.stalwart.it/sdk/langchain/#around-the-agent) | renders a run from its spans, or from the run store alone |
| [`stalwart redteam agent.yaml --findings f.json`](https://api.stalwart.it/sdk/langchain/#around-the-agent) | attacker campaigns at what `verify` found, every tool a stub |
| [`stalwart gateway --config gateway.yaml`](https://api.stalwart.it/sdk/gateway/) | the MCP gateway: every tool call an agent makes, checked on its way to the tool |
| [`stalwart doctor`](https://api.stalwart.it/sdk/runtime/#what-works-without-a-key) | the key, the service and its latency, whether streaming gets through your network, and whether the local runtime or your `qls-router` answers plans and checks, with the licence, the lease and the quota left; with no key, it says how to get one |
| [`stalwart check certificate.json`](https://api.stalwart.it/sdk/client/#checking-a-certificate-yourself) | a certificate checked here, offline, by the carried reference checker; needs no key |
| [`stalwart licence activate\|request\|answer\|install\|release\|status`](https://api.stalwart.it/sdk/runtime/#activating-a-machine) | this machine's licence, offline activation included |

Nothing on `stalwart_agents`' own surface names LangChain or LangGraph: no name,
signature, error or message. A test holds the package to that. `stalwart_agents.langchain`
is there for a team already on LangGraph. `desk.graph()` is the one way to reach the graph that
runs underneath, for a team that wants to extend it.

The same install carries the rest, each documented on its own page:

| Page | What |
|---|---|
| [Build an agent](https://api.stalwart.it/sdk/agents/) | an agent of your own: sign-up, the key, the licence, an `agent.yaml`, a turn, a confirmation and `stalwart serve` |
| [Worked examples](https://api.stalwart.it/sdk/examples/) | each capability shown: the model called only where you allow it, or never; rollbacks; receipts behind Kong; a LangGraph graph validated; a certificate for every rule; every draft verified |
| [The client](https://api.stalwart.it/sdk/client/) | `stalwart_agents.client`: the Stalwart API from Python -- routing, tool checks, guardrail, agent graph and random forest verification, and the bug hunter |
| [The client's quickstart](https://api.stalwart.it/sdk/quickstart/) | the client in five minutes |
| [LangChain and LangGraph](https://api.stalwart.it/sdk/langchain/) | `stalwart_agents.langchain`: the middlewares, the graph parts and `Guard`, for a team already on LangGraph |
| [The gateway](https://api.stalwart.it/sdk/gateway/) | `stalwart gateway`: the MCP gateway, its container and network policy |
| [The Kong recipe](https://api.stalwart.it/sdk/kong/) | the check in front of your tools from Kong AI Gateway, with every file the recipe applies |
| [The licence and the runtime](https://api.stalwart.it/sdk/runtime/) | what a licence is, what works without one, and the runtime the wheel carries |
| [The Python API reference](https://api.stalwart.it/sdk/reference/) | every public name of `stalwart_agents`, `.client`, `.langchain` and `.gateway`, from their docstrings |
| [The HTTP API reference](https://api.stalwart.it/) | the Stalwart Agents API the client calls |
