Ir para o conteúdo

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=...) em stalwart_agents.langchain transforma num catálogo no seu processo (capítulo 27);
  • TypeScript: as funções exportadas de um projeto TypeScript, como o pacote @stalwart-agents/catalogue as 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 (ou read_only em _meta); uma ferramenta marcada destructiveHint é uma escrita, diga o que mais disser; uma ferramenta sem marca é uma escrita -- archive_order acima. Uma ferramenta sem marca custa uma confirmação, nunca uma chamada errada;
  • OpenAPI: GET, HEAD e OPTIONS são leituras, e todo outro método uma escrita, a não ser que a operação diga x-stalwart-read: true -- uma busca enviada como POST, como search_orders acima;
  • TypeScript: cada função diz @read ou @write no seu comentário de documentação. Uma que não diz nenhum dos dois não é adivinhada de jeito nenhum: a importação relata tool_kind_undeclared para 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.