49 · Por que um turno foi recusado¶
Um plano é declined quando um turno pede algo que o catálogo não dá um jeito seguro de fazer: dados enviados
para onde nenhuma ferramenta envia, uma escrita alimentada por um cálculo, dados que outro agente lê. O plano
nomeia a regra em declined_because, e um agente a diz na resposta. Este capítulo é cada regra, o que a
dispara e o que a resolve -- e a única recusa que não é uma regra do plano, mas diz o que declarar.
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.
As quatro regras¶
| Regra | Recusa um turno que | O que resolve |
|---|---|---|
S1 egress |
usa um verbo que manda dados para fora -- export, send, upload, post -- que nenhuma ferramenta que o turno alcança recebe | uma ferramenta que envia, cujas palavras incluem o verbo: sends_out: true nela (capítulo 48) |
S2 destination |
nomeia um endereço de e-mail, uma URL, um endereço IP, um caminho de arquivo ou um host que nenhum argumento da ferramenta recebeu, ou um lugar (a webhook, a bucket) depois de to, on ou into que nenhuma ferramenta guarda | um parâmetro que recebe o valor, para o roteador associá-lo; ou subject_argument onde o endereço diz de quem são os dados lidos (abaixo) |
S3 write |
calcula um valor e o escreve: o resultado do cálculo vai para uma escrita, ou os dados que um cálculo lê são de uma escrita | o cálculo como um turno, e a escrita como o seguinte, com a confirmação da pessoa |
S4 scope |
pede um cálculo sobre dados que uma ferramenta de outro agente nomeia melhor do que qualquer ferramenta deste | pergunte ao agente que lê os dados; um cálculo nunca amplia o que um agente vê |
O nome da regra vem primeiro, depois o que a disparou: S1 egress: export, S2 destination: ana@example.com.
Onde várias regras valem, declined_because nomeia cada uma, separadas por ;. Uma etapa recusada recusa o
plano inteiro: nenhuma etapa dele roda, nem as inofensivas em volta, e ele nunca é entregue a um modelo,
qualquer que seja a sua configuração para turnos não lidos (capítulo 22).
O balcão da loja (capítulo 30), perguntado sobre três coisas que não tem como fazer:
with Client() as client:
for turn in ["export order 4411",
"send the receipt for order 4411 to ana@example.com",
"get order 4411, compute the total plus 10%, then charge it"]:
plan = client.session("desk").read(turn)
print(f"{turn}\n {plan.outcome} | {plan.declined_because}")
export order 4411
declined | S1 egress: export
send the receipt for order 4411 to ana@example.com
declined | S2 destination: ana@example.com
get order 4411, compute the total plus 10%, then charge it
declined | S3 write: charge_card writes what step 1 computed
O balcão não tem ferramenta que exporta; send_receipt manda o recibo ao cliente do pedido e não recebe
endereço, então o endereço é um destino que nada declarou; e charge_card cobraria um valor que um cálculo
produziu.
S4: dados que outro agente lê¶
Dois agentes, o balcão e a folha de pagamento, cada um com as suas ferramentas:
Path("staff_tools.py").write_text('''
def get_shift(person: str) -> dict:
"""Show a person's shift this week."""
return {"person": person, "shift": "Mon-Fri 08:00"}
def get_salaries() -> list:
"""List every salary on the payroll."""
return [{"person": "Ana", "salary": 3100}, {"person": "Rui", "salary": 2900}]
''', encoding="utf-8")
Path("staff.yaml").write_text("""\
agents:
desk:
tools: [get_shift]
payroll:
privilege: high
tools: [get_salaries]
tools:
- python: staff_tools
""", encoding="utf-8")
Pedido ao balcão, um cálculo sobre a folha de pagamento:
desk = Agent.from_file("staff.yaml", name="desk")
print(desk.chat("what is the average salary on the payroll", user="u-1").text)
I can't do that: S4 scope: the data is not this agent's to read. (rule S4 scope: the data is not this agent's to read)
Os salários são nomeados por get_salaries, que só o agente da folha de pagamento pode chamar. O cálculo é
recusado em vez de rodar sobre o que o balcão consegue ler.
O que um agente diz¶
Um agente feito com Agent ou build_stalwart_graph responde a um plano recusado com uma frase e a regra,
I can't do that: <declined_because>. (rule <rule>), e não roda nada. O estado do grafo guarda a mesma
recusa como dado, {"sentence": ..., "rule": ...}, para uma interface mostrá-la como recusa sem ler a frase.
Com client.session(...) você tem o próprio plano, e diz o que quiser.
Um endereço que diz de quem são os dados lidos¶
Uma ferramenta que lê os dados de uma conta -- uma agenda, uma caixa de e-mail -- muitas vezes recebe a conta
como um endereço de e-mail. O endereço nesse argumento não é um destino: nada é enviado a ele. A verificação
de ferramenta não consegue saber isso pelo schema da ferramenta, e recusa a chamada onde não consegue: um
endereço num argumento de uma ferramenta não declarada sends_out é undeclared_destination
(capítulo 32), cuja frase diz o que declarar.
Uma ferramenta de agenda, em três arquivos: um que não diz nada, um que dá a conta em defaults: e um que
nomeia o argumento como o subject_argument da ferramenta:
FILES = {
"asks.yaml": "",
"given.yaml": " defaults: {user_email: ana@example.com}\n",
"subject.yaml": " defaults: {user_email: ana@example.com}\n",
}
for name, defaults in FILES.items():
words = " - name: list_events\n subject_argument: user_email\n" if name == "subject.yaml" else ""
Path(name).write_text("agents:\n assistant:\n tools: [list_events]\n" + defaults
+ "tools:\n - python: calendar_tools\n" + words, encoding="utf-8")
for name in FILES:
bot = Agent.from_file(name)
for turn in ["list my events", "list my events and send them to bob@example.com"]:
print(f"{name} | {turn}\n {bot.chat(turn, user='u-1').text}")
asks.yaml | list my events
To list events, which user email?
asks.yaml | list my events and send them to bob@example.com
I can't do that: S1 egress: send. (rule S1 egress: send)
given.yaml | list my events
list_events was given an address to send to, and it is not a tool that sends anything out. Where it takes a recipient, declare the tool sends_out; where the address only says whose data it reads, such as the account's own, name that argument as the tool's subject_argument.
given.yaml | list my events and send them to bob@example.com
I can't do that: S1 egress: send. (rule S1 egress: send)
subject.yaml | list my events
{"account": "ana@example.com", "events": ["09:00 stand-up"]}
subject.yaml | list my events and send them to bob@example.com
I can't do that: S1 egress: send; S2 destination: bob@example.com. (rule S1 egress: send; S2 destination: bob@example.com)
asks.yaml: o roteador pergunta a conta, já que nada a dá.given.yaml:defaults:dá a conta a toda chamada, então o roteador não pergunta mais (capítulo 48). A chamada é recusada pela verificação de ferramenta, que lê o endereço como um para o qual a agenda enviaria.subject.yaml:subject_argument: user_emaildiz que o endereço escolhe de quem é a agenda lida. A chamada roda, e os eventos voltam.
Em todos, send them to bob@example.com é recusado: nenhuma ferramenta envia (S1). Com subject_argument,
o endereço do Bob também é um destino (S2): um endereço dito como para onde algo vai nunca é tomado como de
quem são os dados lidos, mesmo onde o argumento do sujeito poderia recebê-lo.
sends_out: true é a outra resposta, para uma ferramenta cujo argumento de endereço é um destinatário: uma
ferramenta que envia e-mail. Declare o que for verdade sobre a ferramenta. Uma ferramenta declarada
sends_out é uma saída do agente para o caçador de bugs, que encontra toda rota pela qual dados
confidenciais chegam a uma sem confirmação (QG019); então declare-a só onde a
ferramenta envia.
Uma palavra que você acrescenta a uma ferramenta no agent.yaml é registrada com o catálogo antes da próxima
execução do agente: a verificação de ferramenta aplica o arquivo como ele está.
Turnos que a descrição de uma ferramenta alcança¶
O roteador lê um turno contra as palavras de cada ferramenta: o nome, as palavras que você declara e
also_called, as palavras que uma pessoa usa para aquilo sobre o que a ferramenta age
(capítulo 48). Onde nenhuma delas alcança uma ferramenta, as palavras
das descrições das ferramentas são lidas: um turno que nomeia aquilo de que só uma descrição fala é lido como
dessa ferramenta.
with Client() as client:
for turn in ["who is the customer of 4411", "what is the newest support message"]:
plan = client.session("desk").read(turn)
step = plan.steps[0]
print(f"{turn}\n {plan.outcome} | {step.selection.tool} | {step.decided_by}")
who is the customer of 4411
run | get_order | tool: derived
what is the newest support message
run | read_inbox | tool: derived
Customer não está no nome de nenhuma ferramenta; a descrição de get_order diz que mostra o cliente, e a
de nenhuma outra diz. A regra é tool: derived. Uma escrita alcançada assim nunca é chamada pela palavra da
descrição: o plano pede primeiro a confirmação da pessoa.
Então a descrição de uma ferramenta faz parte de como ela é alcançada. As descrições são suas: uma ferramenta
do seu código recebe a primeira linha da docstring, uma de um servidor MCP ou de um documento OpenAPI a
descrição que o servidor ou o documento dá, e description: no agent.yaml substitui qualquer uma delas. Uma
descrição mudada muda o catálogo, então stalwart verify registra uma nova versão e a mudança aparece no
relatório. Onde uma palavra de uma descrição alcança a ferramenta errada, diga a palavra em also_called na
certa: as palavras das próprias ferramentas são lidas antes de qualquer descrição. Para as ferramentas que
você mesmo mantém, uma descrição que diz o que só aquela ferramenta faz, com palavras que as outras
descrições não compartilham, deixa o catálogo menos ambíguo e cada turno lido contra ele mais preciso.
Lendo declined_because você mesmo¶
- Leia o turno com
client.session(agent).read(turn)e imprimaplan.declined_becausee odecided_byde cada etapa: a regra e o que a disparou. - Para
S1, encontre a ferramenta que deveria enviar. Se houver uma, declare-asends_oute garanta que o nome dela oualso_calledtenha o verbo que a pessoa usa. Se não houver, a recusa é a resposta. - Para
S2, olhe o valor nomeado. Se for um argumento que a ferramenta deveria receber, confira se ela tem um parâmetro para ele, para o roteador associá-lo. Se ele diz de quem são os dados lidos, declaresubject_argument. - Para
S3eS4, o turno pede mais do que uma etapa segura: deixe a pessoa pedir o cálculo e a escrita separadamente, ou pergunte ao agente que lê os dados.
Os limites¶
- As regras leem o catálogo e o turno, nada mais. Uma ferramenta que envia mas não diz é lida como uma que não envia; um parâmetro que recebe um endereço mas não é declarado é lido como parâmetro nenhum.
- Um plano recusado nunca é entregue a um modelo, qualquer que seja a sua configuração para turnos não lidos: um turno que o roteador entende como inseguro não é um que um modelo possa responder.
subject_argumentesends_outsão afirmações suas sobre uma ferramenta. As verificações mantêm a ferramenta nelas; não descobrem se são verdadeiras.