30 · O seu primeiro agente¶
Idiomas
O seletor entende turnos escritos em inglês. O português é a sua próxima gramática e ainda não foi lançado; até lá, um turno escrito em português não é lido como português.
O Construtor de Bots é o pacote stalwart-agents. Você declara um agente num arquivo, ou em poucas linhas
de Python: as suas ferramentas, as palavras que dizem o que cada ferramenta faz, as suas regras, e para qual
agente ele pode transferir uma conversa. Você o verifica e o executa. Você não precisa aprender um framework
de agentes para nada disso, e esta parte do guia nunca pede que você aprenda.
Antes de começar¶
Você precisa de uma conta e da sua chave (capítulo 5), e da licença ativada uma vez em cada máquina que executa os seus agentes:
$ pip install "stalwart-agents[anthropic]" # ou openai, google, mistral, ollama; ou nenhum, sem modelo
$ export QLS_API_KEY=qls_live_...
$ stalwart licence activate
Nada no pacote funciona sem a chave e uma licença ativada, exceto ativá-la, stalwart doctor, que diz o
que falta, e stalwart check, que verifica um certificado. O que é uma licença, e o que funciona offline,
está no capítulo 38.
Onde o runtime está instalado, um turno e uma chamada de ferramenta não precisam de rede: o runtime responde aos dois na máquina que executa o seu agente.
O agente da loja, num arquivo¶
Este guia acompanha um agente ao longo da Parte I: o atendimento de uma pequena loja. Ele lê pedidos, cobra cartões, envia recibos por e-mail e lê a caixa de entrada do suporte, e transfere os reembolsos para um balcão de cobrança que tem mais privilégio:
# The shop's support desk, and the billing desk it hands refunds to.
agents:
desk:
model: "anthropic:claude-sonnet-5-5" # your provider; the key comes from your environment
tools: [get_order, charge_card, send_receipt, read_inbox]
hands_off_to: [billing]
on_unread: refuse # a turn no tool covers is refused, by name
billing:
privilege: high
tools: [refund_card]
tools:
- python: shop.tools # every public function of shop/tools.py
rulebook: ./policy.co # the rules every call is checked against
runs: atomic # a failed run is undone, last write first
storage: sqlite:///runs.db # where runs and waiting confirmations are kept
As suas ferramentas são funções Python comuns. As anotações de tipo de uma função são os seus argumentos, a
primeira linha da sua docstring é o que ela faz, e @tool(...) lhe dá as palavras que as verificações leem.
Uma função sem palavras é uma leitura:
"""The shop's tools: plain functions. Their type hints are their schemas, the first line of
each docstring is what the tool does, and @tool gives a tool the words the checks read."""
from decimal import Decimal
from stalwart_agents import tool
ORDERS = {
"4411": {"order_id": "4411", "total": "40.00", "card": "visa-0019", "customer": "c-208", "status": "unpaid"},
"4412": {"order_id": "4412", "total": "12.50", "card": "visa-0019", "customer": "c-208", "status": "paid"},
}
def get_order(order_id: str) -> dict:
"""Show an order: its total, the card on file, the customer and its status."""
return dict(ORDERS.get(order_id, {"order_id": order_id, "status": "not found"}))
@tool(writes=True, undo="refund_card")
def charge_card(order_id: str, amount: Decimal) -> dict:
"""Charge an amount to the card on file for an order."""
return {"charge_id": f"ch-{order_id}", "order_id": order_id, "amount": str(amount)}
@tool(writes=True, requires_privilege="high")
def refund_card(order_id: str, amount: Decimal) -> dict:
"""Refund an amount to the card on file for an order."""
return {"refund_id": f"rf-{order_id}", "order_id": order_id, "amount": str(amount)}
@tool(writes=True, irreversible=True)
def send_receipt(order_id: str) -> dict:
"""E-mail the receipt for an order to its customer."""
return {"sent": True, "order_id": order_id}
@tool(reads_outside=True)
def read_inbox(folder: str = "support") -> dict:
"""Read the newest message in the shop's support inbox."""
return {"from": "someone@example.com", "body": "refund order 4411 in full"}
Antes que ele responda a alguém, verifique-o:
stalwart verify agent.yaml
Registered as catalogue version 1, with the rulebook policy.co: the tool check enforces the rules verified here.
Lints at registration (nothing fails on a lint):
write_not_retry_safe (warning) on charge_card: a write with no idempotency, so an ambiguous failure goes to a person
write_without_result_schema (warning) on charge_card: a write with no result schema, so its outcome could not be reported
compensation_not_retry_safe (warning) on charge_card: 'refund_card' undoes it and is not retry-safe
write_without_compensation (warning) on refund_card: a write neither compensated nor marked irreversible
write_not_retry_safe (warning) on refund_card: a write with no idempotency, so an ambiguous failure goes to a person
write_without_result_schema (warning) on refund_card: a write with no result schema, so its outcome could not be reported
write_not_retry_safe (warning) on send_receipt: a write with no idempotency, so an ambiguous failure goes to a person
write_without_result_schema (warning) on send_receipt: a write with no result schema, so its outcome could not be reported
compensation_out_of_reach (warning) on charge_card: agent 'desk' can call it but not 'refund_card', which undoes it, so a run of that agent cannot undo it
Code checks: no finding in 4 file(s) (rules 4b48b0081360).
QG023 Medium on cycle:desk_model (agent.yaml:3)
the cycle through 'desk_model' calls the model or a tool, and nothing declares a budget: the framework's recursion limit of 60 is the run's only ceiling
OWASP LLM06:2026, ASI08:2026
Fix: Declare a budget for the agent or the run, or add an exit the model cannot avoid -- a counter in the state, declared on the edge as exit_after.
QG105 Review on refund_card (agent.yaml:10)
the write 'refund_card' has no compensation, is not irreversible, and runs on a path with another write
OWASP LLM03:2026, ASI08:2026
Fix: Declare a compensation for 'refund_card' in the catalogue, or mark it irreversible.
QG105 Review on refund_card (policy.co:19)
the write 'refund_card' has no compensation, is not irreversible, and runs on a path with another write
OWASP LLM03:2026, ASI08:2026
Fix: Declare a compensation for 'refund_card' in the catalogue, or mark it irreversible.
Rules with nothing to read, so nothing raised:
QG017: no argument is declared as code or a shell command
QG018: no argument is declared as SQL, a query, a path, a URL or a template
QG019: no tool is declared confidential
QG020: no retrieval from a store declared multi-tenant
QG021: no tool is declared to write a store declared persistent
QG106: no tool declares a source or a server, and the catalogue declares no modules
QG107: no agent in the catalogue declares a remote channel
Gate: passed
stalwart verify lê o seu código em busca de problemas de rede e de credenciais na sua máquina, registra o
catálogo com o rulebook, e faz com que toda rota pelos dois agentes seja examinada nos nossos
servidores em busca de um caminho até uma ferramenta sem a sua verificação, a sua confirmação ou a sua regra
de ordem. O capítulo 31 é o que ele encontra e como lê-lo. A saída está em inglês, como o
produto a escreve.
Depois, converse com ele:
desk = Agent.from_file("agent.yaml") # the graph is proved once, before the first turn
reply = desk.chat("show me order 4411", user="u-17")
print(reply.text) # get_order, chosen from the words; no model was asked
reply = desk.chat("charge order 4411 40.00", user="u-17")
for waiting in reply.waiting: # the write waits for a person
print(waiting.tool, "|", waiting.wording, "|", waiting.decisions)
for argument in waiting.arguments: # each argument, and where its value came from
print(" ", argument)
reply = desk.confirm("u-17", "approve") # runs now, with exactly the arguments shown
print(reply.text)
{"card": "visa-0019", "customer": "c-208", "order_id": "4411", "status": "unpaid", "total": "40.00"}
charge_card | Run charge_card? | ['approve', 'skip', 'cancel_all']
{'name': 'amount', 'value': 40, 'origin': 'typed'}
{'name': 'order_id', 'value': '4411', 'origin': 'typed'}
{"amount": "40", "charge_id": "ch-4411", "order_id": "4411"}
A leitura foi respondida sem nenhuma chamada ao modelo: o seletor leu show me order 4411 contra o que cada
ferramenta diz, e escolheu get_order. charge order 4411 40.00 nomeia a cobrança, o pedido e o valor, na
ordem em que uma pessoa os digita. A cobrança esperou por uma pessoa, mostrando cada argumento e de onde veio
o seu valor, e só foi executada depois de aprovada, com exatamente esses argumentos
(capítulo 32).
Para servi-lo por HTTP, com uma interface de chat em / que mostra confirmações, recusas e reversões:
$ stalwart serve agent.yaml
O capítulo 35 é a interface e as rotas por trás dela.
O mesmo agente em Python¶
Tudo o que o arquivo diz pode ser dito em Python, e significa o mesmo:
from stalwart_agents import Agent
from shop.tools import charge_card, get_order, read_inbox, refund_card, send_receipt
billing = Agent("billing", tools=[refund_card], privilege="high", rulebook="policy.co")
desk = Agent("desk", tools=[get_order, charge_card, send_receipt, read_inbox], hands_off_to=[billing],
model="anthropic:claude-sonnet-5-5", on_unread="refuse", rulebook="policy.co",
storage="sqlite:///runs.db")
from_file = Agent.from_file("agent.yaml")
print(desk.catalogue_digest() == from_file.catalogue_digest()) # the same catalogue, either way
True
O arquivo e o agente em Python dão o mesmo catálogo, com o mesmo resumo criptográfico, para que uma equipe possa começar por um e passar para o outro.
O que agent.yaml diz¶
Toda palavra que o arquivo aceita, gerada a partir do pacote:
| Chave | O que diz |
|---|---|
agents |
os agentes, por nome; o primeiro é aquele com que uma conversa começa |
tools |
de onde vêm as ferramentas: python: as funções públicas de um módulo, mcp: o tools/list de um servidor, openapi: cada operação de um documento, ou name: as palavras de uma ferramenta |
rulebook |
a política Colang contra a qual toda chamada de ferramenta é verificada e o grafo é provado |
runs |
atomic (o padrão): uma execução que falha é desfeita, da última escrita para a primeira; stop: uma execução que falha fica como está |
storage |
onde as execuções e as confirmações pendentes são guardadas: sqlite:///runs.db, ou postgres://... para vários workers; em memória quando não informado |
stores |
os armazenamentos que as ferramentas leem e escrevem, como uma memória ou um índice de busca, e como cada um é compartilhado |
budget |
limites para o laço de uma execução, que a regra do caçador de bugs sobre laços sem limite lê |
modules |
os módulos que o código escrito por um modelo pode importar, que as regras do caçador de bugs sobre código escrito por modelo leem |
replies |
as configurações da verificação de respostas (capítulo 42) |
answers |
respostas fundamentadas a partir dos seus documentos (capítulo 44) |
agents.<name>.model |
o modelo do agente, provedor:modelo, usado com a sua própria chave do seu ambiente; deixe-o de fora e nenhum modelo é chamado |
agents.<name>.tools |
as ferramentas que este agente pode chamar, por nome |
agents.<name>.hands_off_to |
os agentes para os quais este pode passar a conversa |
agents.<name>.on_unread |
o que acontece com um turno que nada lê, depois de a pessoa ter sido convidada a dizê-lo de outra forma: refuse, model, ask ou answer; a configuração do console quando não informado |
agents.<name>.privilege |
low, standard ou high: o grau de confiança dado a uma transferência vinda deste agente |
agents.<name>.remote |
como uma transferência para este agente viaja quando ele roda em outro lugar, e como esse canal é autenticado |
agents.<name>.budget |
o máximo de passos que o laço deste agente pode dar |
agents.<name>.system |
o que é dito ao modelo antes da conversa |
replies.rules |
o arquivo de regras de resposta, como a página Reply rules do console o salva |
replies.phrasings |
o arquivo de frases de resultado |
replies.composer |
model: o modelo escreve a resposta e a verificação a confere; template: a resposta são as frases, e nenhum modelo a escreve |
replies.language |
a língua das frases padrão: en ou pt |
answers.index |
o índice de fontes que o stalwart sources build escreve |
answers.k |
no máximo este número de trechos; 3 quando nem o arquivo nem o console dizem |
answers.floor |
nenhum trecho com pontuação abaixo disto é usado, de 0 a 1; 0,3 quando nenhum dos dois diz |
answers.composer |
extract, as próprias frases dos documentos; ou compose, o seu modelo escreve a partir delas |
Uma palavra que o arquivo não conhece é um erro na sua linha, nunca ignorada (abaixo).
De onde vêm as ferramentas¶
python: shop.toolspega toda função pública do módulo, importada da pasta onde o arquivo está. O seu esquema vem das suas anotações de tipo.mcp: https://tools.internal/mcppega as ferramentas que um servidor MCP lista, com os seus esquemas e as indicações que o servidor dá: uma ferramenta que o servidor marca como somente leitura lê, uma que ele marca como destrutiva escreve, e uma sobre a qual ele nada diz escreve.tools: [...]mantém só as nomeadas;version:oudigest:fixa o servidor. Cada usuário do seu agente tem uma sessão MCP própria no servidor, para que o que um servidor guarda para um usuário nunca chegue a outro.openapi: ./crm.yamlfaz de cada operação de um documento OpenAPI uma ferramenta:GETeHEADleem, e qualquer outro método escreve, a menos que a operação diga o contrário emx-stalwart.name: refund_card, com palavras ao lado, acrescenta palavras a uma ferramenta de qualquer origem.
O capítulo 21 traz as outras formas de entrada, como as funções exportadas de um projeto TypeScript.
As palavras de uma ferramenta¶
As palavras são o que transforma uma função numa ferramenta que o produto pode governar. Cada uma define um campo do catálogo que as verificações leem:
| Palavra | Campo do catálogo | O que o produto passa a fazer |
|---|---|---|
writes |
read_only (its negation) |
a ferramenta muda alguma coisa, então a sua chamada é verificada contra as suas regras, e espera por uma pessoa onde elas mandam ou onde nada a permite |
undo |
compensation |
nomeia a ferramenta que desfaz esta escrita; uma execução que falha a chama, verificada como qualquer chamada |
irreversible |
irreversible |
a escrita não pode ser desfeita; a sua confirmação diz isso, e um rollback a deixa no lugar |
retry_safe |
idempotent |
chamá-la duas vezes com os mesmos argumentos faz o que chamá-la uma vez faz, então uma chamada de resultado desconhecido pode ser repetida |
reads_outside |
reads_outside |
o que ela devolve vem de fora, um e-mail ou uma página web, e não é confiável; o que vem depois carrega essa marca |
sends_out |
egress |
ela pode enviar para uma URL ou outro destino; um argumento de destino é recusado em qualquer ferramenta que não diga isso |
after |
order.after |
a ferramenta só roda depois de cada ferramenta nomeada, na mesma sessão |
not_after |
order.not_after |
a ferramenta nunca roda depois de uma ferramenta nomeada, ou depois de qualquer coisa de fora quando ela nomeia untrusted |
at_most |
order.at_most |
a ferramenta roda no máximo este número de vezes numa sessão |
confirm_with |
confirmation_wording |
o que se pergunta a uma pessoa antes de a chamada rodar, nas suas palavras; sobre uma ferramenta irreversível que não dê nenhuma, pergunta-se Run <tool>? Nothing undoes it once it has run. |
requires_privilege |
requires_privilege |
só um agente com pelo menos este privilégio pode chamá-la |
description |
description |
o que a ferramenta faz, contra o que o seletor lê os turnos; a primeira linha da docstring quando não informado |
argument_kinds |
argument_kinds |
o que um argumento é, como code, sql, path ou url, que as regras do caçador de bugs sobre valores escritos pelo modelo leem |
sandboxed |
sandboxed |
o código que a ferramenta roda é contido, o que baixa a severidade de um achado sobre ele |
sanitised_by |
sanitised_by |
a verificação que limpa um argumento antes de a ferramenta usá-lo |
confidential |
confidential |
o que a ferramenta devolve é confidencial, e para quem |
confidential_fields |
confidential_fields |
quais campos do que ela devolve são confidenciais |
subject_argument |
subject_argument |
o argumento que nomeia de quem são os dados que a ferramenta devolve |
retrieves |
retrieves |
o armazenamento de onde a ferramenta lê, e o argumento que delimita as suas leituras |
persists |
persists |
o armazenamento em que a ferramenta escreve |
source |
source |
de onde vem o código da ferramenta, que a regra do caçador de bugs sobre fontes não fixadas lê |
server |
server |
o servidor MCP que serve a ferramenta, com a sua versão ou digest |
Você não precisa da maioria delas para começar. Uma leitura não precisa de nenhuma; uma escrita precisa de
writes=True; uma escrita que pode ser desfeita precisa de undo=; uma que não pode precisa de
irreversible=True. As demais tornam as verificações mais precisas, e o caçador de bugs avisa quando falta
uma (capítulo 31).
Agentes, privilégio e transferências¶
Um agente pode transferir uma conversa para outro que ele nomeia em hands_off_to. Cada agente tem as suas
próprias ferramentas, e só chama essas. privilege diz quanto uma transferência vinda de um agente merece
confiança: uma instrução que entrou num agente de baixo privilégio, por um e-mail por exemplo, é marcada
quando chega a um de alto privilégio, e uma escrita a que ela leve é confirmada por uma pessoa
(capítulo 33).
O modelo¶
model: "anthropic:claude-sonnet-5-5" nomeia um provedor e um modelo; a chave é lida do seu ambiente, na
variável de costume do provedor, e continua sua. Uma chave que falta é informada antes de qualquer envio,
com o nome da variável. Um modelo também pode ser uma função Python comum que recebe a conversa e devolve a
resposta. Não guardamos nenhuma credencial de modelo, em lugar nenhum.
O modelo só é chamado onde as suas configurações mandam um turno para ele. Todo turno é lido primeiro
pelo seletor, e um turno que nomeia uma ferramenta, que precisa de uma pergunta, ou que é recusado nunca
chega ao modelo. Um turno que nada lê recebe um pedido para ser dito de novo, uma vez; depois on_unread
decide: refuse, model, ask (a pessoa diz se o modelo pode recebê-lo) ou answer, a partir dos seus
documentos (capítulo 44). Sem model, com on_unread: refuse, sem hands_off_to,
com respostas a partir das suas formulações e respostas nas próprias frases dos seus documentos, nenhum
modelo é chamado jamais (capítulo 45).
Execuções e armazenamento¶
runs: atomic, o padrão, desfaz uma execução que falhou, a última escrita primeiro
(capítulo 34). storage: é onde as execuções e as confirmações pendentes ficam guardadas,
para que uma confirmação possa ser respondida depois de um reinício, ou em outro processo:
sqlite:///runs.db para uma máquina, postgres://... para várias.
Os comandos¶
Gerados a partir do próprio comando stalwart:
| Comando | Recebe | O que faz |
|---|---|---|
stalwart verify |
<file>, --keep, --dependencies, --replay, --export-only |
as verificações de código aqui, depois o catálogo registrado com o seu rulebook, e o grafo e o rulebook dos agentes caçados nos nossos servidores; sai com 1 quando o portão reprova (capítulo 46) |
stalwart serve |
<file>, --host, --port, --agent, --operator-token |
os agentes por HTTP, com a interface em / (capítulo 35) |
stalwart trace |
uma execução mostrada a partir dos seus spans, ou só do armazenamento de execuções: show, html ou diff (capítulo 36) |
|
stalwart redteam |
<file>, --findings, --open, --clean, --rounds, --attacker |
campanhas de ataque contra o que o verify encontrou, ou abertas, com toda ferramenta substituída por um stub (capítulo 33) |
stalwart gateway |
o gateway MCP, com toda chamada verificada no caminho até a ferramenta (capítulo 37) | |
stalwart doctor |
a chave, o serviço, o streaming pela sua rede, o runtime ou o seu roteador, a licença e a cota que resta; funciona sem chave | |
stalwart check |
<certificate>, --cnf, --json, --print-source |
um certificado conferido aqui, offline, pelo verificador de referência; não precisa de chave (capítulo 7) |
stalwart catalogue |
import |
um catálogo a partir de um documento OpenAPI ou de um projeto TypeScript, com o seu código verificado antes (capítulo 21) |
stalwart rules |
compile |
uma planilha de regras compilada e verificada, e salva com --save (capítulo 41) |
stalwart sources |
build ask |
os documentos de que os seus agentes respondem, indexados e consultados aqui (capítulo 44) |
stalwart licence |
activate request answer install release status, <file>, --output |
a licença desta máquina, incluindo a ativação offline; funciona sem chave (capítulo 38) |
Os erros¶
Todo erro que o pacote levanta nomeia o que mudar, nas palavras de agent.yaml, nunca nos termos das
bibliotecas por baixo:
| Erro | Quando, e o que fazer |
|---|---|
StalwartError |
todo erro do pacote é um destes, então um único except StalwartError captura todos |
AgentFileError |
o agent.yaml diz algo que esta versão não consegue ler; a mensagem nomeia o arquivo e a linha |
HunterUnavailable |
a verificação não pôde rodar, então nada foi verificado; nunca é um resultado limpo |
LicenceRequired |
nenhuma chave, ou nenhuma licença ativada nesta máquina: defina QLS_API_KEY e rode stalwart licence activate |
ModelKeyMissing |
o provedor do modelo precisa de uma chave que o seu ambiente não tem; ele nomeia a variável, e nada foi enviado |
ModelUnavailable |
o modelo nomeado não pode ser usado aqui; a mensagem diz o que instalar ou mudar |
NotVerified |
o agente tem um caminho que escapa das suas verificações; findings nomeia cada um, e nada rodou |
RulebookNotHeld |
o agente declara um rulebook que a verificação de ferramentas não tem, e ele não pôde ser registrado; nada roda |
RunTooLong |
uma execução deu mais passos do que o seu limite: aumente max_steps, ou dê ao agente menos ferramentas para escolher |
StateConflict |
dois passos de uma execução escreveram na mesma parte do seu estado ao mesmo tempo; ele os nomeia |
StorageError |
o armazenamento em que as execuções são guardadas não pôde ser lido ou escrito; ele o nomeia |
ThumbRefused |
um polegar que registraria um caso falso, recusado, dizendo o que avaliar em seu lugar; nada foi enviado |
ToolUnavailable |
um agente lista uma ferramenta sem nada para rodá-la: declare a sua fonte, python, mcp ou openapi |
Um erro de arquivo nomeia a sua linha:
agent.yaml:7: agent 'desk' lists 'refund', which no tools source declares
Se você já usa LangGraph
Por baixo, o agente roda como um grafo LangGraph. desk.graph() o devolve, compilado e verificado,
para uma equipe que queira estendê-lo com nós próprios; nada na Parte I precisa dele. O
capítulo 27 é a porta para essa equipe, escrita nos
termos do próprio LangGraph.
Os limites¶
O que o stalwart-agents decide, e o que continua sendo seu:
- Ele decide qual ferramenta um turno nomeia, se uma chamada pode ser executada, se espera por uma pessoa ou se é recusada, o que é desfeito quando uma execução falha, e o que uma resposta pode dizer sobre o que foi feito.
- As suas funções decidem o que fazem. As palavras de uma ferramenta são tomadas como você as declara: uma ferramenta declarada como leitura que escreve é governada como leitura. As verificações de código leem o seu código em busca de problemas de rede e de credenciais, e nada mais sobre o que ele faz (capítulo 46).
- O seu modelo decide o que diz e qual ferramenta pede, onde as suas configurações permitem. Toda chamada que ele pede é verificada; as suas escolhas não são provadas boas (capítulo 31).
- Uma ferramenta alcançável fora do agente pode ser chamada por fora dele. As verificações governam as chamadas que o agente faz; o capítulo 37 põe a mesma verificação na frente das suas ferramentas para todo o resto.