Ir para o conteúdo

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.tools pega 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/mcp pega 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: ou digest: 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.yaml faz de cada operação de um documento OpenAPI uma ferramenta: GET e HEAD leem, e qualquer outro método escreve, a menos que a operação diga o contrário em x-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.