21 · Catálogos¶
O roteador lê um turno contra as suas ferramentas, e a verificação de ferramenta decide uma chamada pelo que
cada ferramenta declara: os dois leem o seu catálogo. Este capítulo é como as ferramentas entram num
catálogo -- a partir das formas em que você já as escreve --, como cada uma é marcada como leitura ou
escrita, os lints que o stalwart verify relata e como uma ferramenta é tirada de serviço. O catálogo é um
arquivo seu: nada dele fica guardado nos servidores da Stalwart, e todo plano e toda verificação o levam.
Idiomas
O seletor lê turnos escritos em inglês e em português do Brasil (pt-BR). O idioma é escolhido, nunca adivinhado a partir do turno: na página Settings do console (Language the selector reads), em language numa requisição de plano, ou no settings.language do seu arquivo de vocabulário; sem nenhum deles, inglês. Um turno em português pode ser digitado com os acentos ou sem eles. O português europeu não é uma gramática que o seletor tenha, e um turno em qualquer outra língua não é lido.
Fazendo o seu catálogo¶
O stalwart catalogue import escreve o catálogo na sua máquina, em --out (ou o imprime). Ele lê as suas
ferramentas em quatro formas:
- MCP: as ferramentas de um servidor MCP, listadas a partir desta máquina com
--mcp <endereço ou comando>; - OpenAPI: um documento OpenAPI 3.0 ou 3.1, uma ferramenta por operação;
- LangChain: os seus objetos de ferramenta LangChain, que o
tool_catalogue(tools, agent=...)emstalwart_agents.langchaintransforma num catálogo no seu processo (capítulo 27); - TypeScript: as funções exportadas de um projeto TypeScript, como o pacote
@stalwart-agents/catalogueas lê das declarações do seu compilador (abaixo).
O catálogo de um agente da Parte I é feito a partir do agent.yaml toda vez que ele começa, e pelo stalwart
verify (capítulo 31). As ferramentas da loja, como um servidor MCP as lista e como um
documento OpenAPI:
# A resposta de um servidor MCP a `tools/list`, como ele a envia.
MCP = {"tools": [
{"name": "get_order", "description": "Mostra um pedido: o total, o cartão cadastrado e a situação.",
"inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
"annotations": {"readOnlyHint": True}},
{"name": "charge_card", "description": "Cobra um valor no cartão cadastrado de um pedido.",
"inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}, "amount": {"type": "number"}},
"required": ["order_id", "amount"]},
"annotations": {"readOnlyHint": False},
"_meta": {"compensation": {"tool": "refund_card",
"arguments": {"order_id": "$args.order_id", "amount": "$args.amount"}}}},
{"name": "refund_card", "description": "Estorna um valor para o cartão cadastrado de um pedido.",
"inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}, "amount": {"type": "number"}},
"required": ["order_id", "amount"]},
"annotations": {"destructiveHint": True}},
{"name": "archive_order", "description": "Move um pedido para o arquivo.",
"inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}},
]}
# A mesma loja como um documento OpenAPI 3.1: uma ferramenta por operação.
OPENAPI = {"openapi": "3.1.0", "info": {"title": "Loja", "version": "1"}, "paths": {
"/orders/{order_id}": {"get": {
"operationId": "get_order", "summary": "Mostra um pedido",
"parameters": [{"name": "order_id", "in": "path", "required": True, "schema": {"type": "string"}}]}},
"/orders/{order_id}/charges": {"post": {
"operationId": "charge_card", "summary": "Cobra um valor no cartão cadastrado",
"parameters": [{"name": "order_id", "in": "path", "required": True, "schema": {"type": "string"}}],
"requestBody": {"content": {"application/json": {"schema": {
"type": "object", "properties": {"amount": {"type": "number"}}, "required": ["amount"]}}}},
"x-stalwart-idempotency-key": "Idempotency-Key"}},
"/orders/search": {"post": {
"operationId": "search_orders", "summary": "Busca os pedidos", "x-stalwart-read": True,
"requestBody": {"content": {"application/json": {"schema": {
"type": "object", "properties": {"customer": {"type": "string"}}}}}}}},
}}
stalwart catalogue import --mcp http://localhost:8931/mcp --out catalogue.json
stalwart catalogue import shop-openapi.json --out catalogue.json
O nome de uma ferramenta é lido como palavras: get_order, getOrder e get-order são todos get order,
a primeira palavra a sua ação e o resto a coisa sobre a qual ela age. Para uma operação OpenAPI sem
operationId, a ação vem do seu resumo e a coisa do seu caminho.
Leituras e escritas¶
Toda escrita é confirmada por uma pessoa antes de rodar (capítulo 24), então importa se uma ferramenta escreve, e isso nunca é adivinhado na direção que custa caro a você:
- MCP: uma leitura onde ela diz
readOnlyHint: true(ouread_onlyem_meta); uma ferramenta marcadadestructiveHinté uma escrita, diga o que mais disser; uma ferramenta sem marca é uma escrita --archive_orderacima. Uma ferramenta sem marca custa uma confirmação, nunca uma chamada errada; - OpenAPI:
GET,HEADeOPTIONSsão leituras, e todo outro método uma escrita, a não ser que a operação digax-stalwart-read: true-- uma busca enviada comoPOST, comosearch_ordersacima; - TypeScript: cada função diz
@readou@writeno seu comentário de documentação. Uma que não diz nenhum dos dois não é adivinhada de jeito nenhum: a importação relatatool_kind_undeclaredpara ela, um erro (abaixo).
O que a forma de uma ferramenta não consegue dizer, a ferramenta declara ao lado -- em annotations ou
_meta do MCP, no objeto x-stalwart de uma operação OpenAPI ou nos metadados LangChain, com os mesmos
nomes: o desfazer que a reverte (compensation, como charge_card declara acima), se ela é irreversible,
a sua chave de idempotency, se ela envia dados para fora (egress) ou lê texto de fora (reads_outside),
o privilégio de que precisa, a frase que a sua confirmação mostra e as declarações que o caçador de bugs lê
(capítulo 31). As palavras de ferramenta da Parte I são essas mesmas declarações
(capítulo 30).
O relatório de lints¶
O stalwart verify faz o lint do catálogo na sua máquina e imprime o relatório ao lado dos seus achados. Os
avisos da loja:
| Lint | O que o registro diz (como impresso) |
|---|---|
write_without_compensation |
a write neither compensated nor marked irreversible |
write_not_retry_safe |
a write with no idempotency, so an ambiguous failure goes to a person |
write_without_result_schema |
a write with no result schema, so its outcome could not be reported |
compensation_not_retry_safe |
'<undo>' undoes it and is not retry-safe |
Um aviso é um conselho. Um erro aponta uma ferramenta que o leitor não consegue rotear com segurança: duas ferramentas que ele não consegue distinguir, nomeando as duas -- um turno feito para uma poderia chegar à outra --, ou um nome que ele lê como gramática. Corrija na definição da ferramenta e importe de novo.
Versões¶
A versão em vigor é o arquivo: guarde-o no seu repositório ao lado do agent.yaml, e o histórico dele é o
do catálogo. Um plano e uma verificação leem o catálogo que recebem, então um mais antigo é restaurado
pegando o arquivo de volta do histórico.
Ferramentas retiradas¶
Retirar uma ferramenta faz os seus agentes pararem de chamá-la: liste-a em withdrawn: no agent.yaml,
ou no gateway.yaml para os agentes atrás de um gateway (capítulo 48). Uma
ferramenta retirada é recusada antes de qualquer outra coisa e nunca é listada para o modelo, até o arquivo
deixar de citá-la. A lista é lida onde o agente ou o gateway roda: nada é buscado para mantê-la.
TypeScript¶
npm install --save-dev @stalwart-agents/catalogue
npx tsc --declaration --emitDeclarationOnly --outDir dist
stalwart catalogue import ./server --declarations ./server/dist --out catalogue.json
O @stalwart-agents/catalogue lê as declarações que o seu próprio compilador escreve -- cada função
exportada uma ferramenta, os seus parâmetros os argumentos, o seu tipo de retorno o resultado -- e não roda
nada. O stalwart catalogue import então roda as verificações de código sobre os fontes do projeto, na sua
máquina: o código nunca sai da sua máquina, e só as contagens são enviadas à Stalwart. Um achado, ou uma
função que não diz nem @read nem @write, faz a importação falhar: o comando sai com 1. O stalwart
catalogue import openapi.yaml lê um documento OpenAPI do mesmo jeito, sem código a verificar.
Os limites¶
Os limites do capítulo 20 valem aqui, e mais estes:
- Um catálogo diz o que as suas ferramentas declaram. Uma ferramenta que escreve e se declara leitura é lida como leitura: marcar leituras é uma afirmação sua, e a verificação só confirma o que lhe dizem que escreve.
- Um lint lê as declarações, não as ferramentas: uma compensação declarada é uma que a verificação vai chamar, desfaça ela alguma coisa ou não.