
O portão de aprovação do seu agente é provavelmente apenas um rótulo de aviso
Cada framework de agente permite que um modelo chame uma ferramenta. A maioria imprimirá algo antes de uma perigosa. Muito poucos realmente param.
Essa lacuna é fácil de enviar sem perceber, porque os dois estados parecem idênticos em uma captura de tela. Um cartão que diz "isso irá postar no Slack, aprovar?" e um cartão que diz a mesma coisa e então posta de qualquer maneira renderizam os mesmos pixels. A diferença só aparece no executor, e o executor é a parte que ninguém captura.
Passei os últimos três dias construindo a execução de gráficos no Vodou: um formato de receita legível, um plano que você vê antes de qualquer coisa ser executada, chamadas de ferramentas reais em paralelo e um portão de aprovação. Trinta commits, cinquenta e nove arquivos. Dois desses commits existem porque meu portão era uma etiqueta de aviso por dois dias e meu executor paralelo nunca foi paralelo.
O que foi enviado
Uma receita é a forma legível de um plano. Ela se compila em actions.json, que é o que os motores realmente executam.
together sources:
calendar: google-calendar.list-events {"calendarId":"primary"}
mail: gmail.messages_list {"labelIds":["UNREAD"]}
slack: slack.slack_search_messages {"query":"mentions:me"}
then:
need: 2 de 3
briefing: escrever um briefing matinal a partir de {calendar, mail, slack}
ask me:
- postar em #daily?
O compilador vive em exatamente um lugar. Chat, o CLI, o assistente de configuração e o proponente de habilidades todos o chamam, e nenhum deles pode formar uma opinião privada sobre o que need: 2 de 3 significa. Essa foi uma escolha deliberada, e a alternativa, um parser TypeScript para a UI e um em Rust para o runtime, é como você acaba com um cartão de plano que discorda da execução.
Compilar produz um cartão, e nada é executado até que um botão seja pressionado:
Veja como eu executaria isso
juntos — eles não precisam um do outro, então eles rodam ao mesmo tempo
calendário google-calendar·list-events
correio gmail·messages_list
ping slack·slack_post_message ⚠
juntar — precisa de 2 de 3
⚠ slack·slack_post_message muda algo fora do Vodou, então isso
irá parar e perguntar antes de executá-lo. Diga "sem perguntar" para optar por não perguntar.
[Executar uma vez] [Salvar + agendar] [Editar receita]
A resolução server·tool está em cada linha. Uma resolução de ferramenta errada agora é visível enquanto ainda é livre para corrigir, em vez de ser descoberta por uma mensagem do Slack que não deveria ter sido enviada.
Uma frase para uma execução
A luta
O ventilador nunca foi um ventilador. Cada rascunho do meu plano descreveu o executor de ferramenta paralelo existente como "já o diamante": a peça sobre a qual eu poderia construir. Ele envolveu chamadas de ferramentas em futures::join_all e despachou um conjunto delas de uma vez. Parecia correto.
O cliente de ferramenta por baixo não era assíncrono. Era uma gravação de stdio bloqueante seguida por uma leitura bloqueante. join_all verifica cada ramo em uma tarefa, então o primeiro ramo bloqueou o loop de verificação e os outros esperaram sua vez. O ventilador funcionou estritamente um de cada vez e se reportou como paralelo.
Três ferramentas somente leitura em três servidores MCP diferentes:
placa 412ms · recordação 268ms · proc 83ms
total 770ms soma 763ms máximo 412ms
770 é a soma, não o máximo. Se o ventilador fosse real, o relógio de parede estaria perto de 412. Esse código estava no caminho de múltiplas intenções desde que foi enviado, cobrando silenciosamente a soma de cada ramo. Depois de entregar cada ramo a uma verdadeira thread para que a chamada bloqueante possua algo que é permitido bloquear:
Relógio de parede vs soma dos ramos
A segunda execução teve ramos mais pesados. Esse é o ponto. Antes, o relógio de parede rastreava a soma. Depois, rastreou o ramo mais lento. Os números absolutos importam menos do que a relação entre as duas barras em cada par, e essa relação é o único teste honesto de se o seu ventilador é um ventilador.
A lição que eu realmente paguei: async fn na assinatura não é a coisa que a torna concorrente. Uma chamada bloqueante dentro de um futuro é uma chamada bloqueante. Se você nunca mediu o relógio de parede em relação às durações somadas dos ramos individuais, você não sabe qual deles você tem.
O portão era decoração. O cartão do plano imprimia o ⚠ e a frase sobre parar para perguntar. Eu enviei. Era, por dois dias, uma legenda.
A razão pela qual isso importava mais do que um bug cosmético: o motor de parâmetros do Vodou preenche automaticamente booleanos declarados como true. Portanto, um ventilador contendo slack_post_message não falharia por falta de argumentos. Ele postaria, com argumentos que ninguém digitou, sob um aviso que prometia que não o faria. Um aviso que o executor não honra é pior do que nenhum aviso, porque compra a confiança do usuário e depois a gasta.
A correção foi estrutural, não cosmética. Qualquer ferramenta que envia, posta, deleta ou paga agora recebe um passo de aprovação anexado às ações compiladas. O portão é um nó no gráfico. O driver que percorre o gráfico para. nele porque ele está lá, não porque um renderizador se lembrou de ter cuidado.
Essa mudança arrastou uma segunda atrás dela. Uma execução estacionada deve sobreviver à pergunta. Se a vida de uma execução terminasse quando a vez terminasse,
Empresas brasileiras que utilizam agentes de IA devem garantir que seus sistemas de aprovação sejam robustos para evitar erros que possam comprometer a confiança do usuário. A implementação de um controle de aprovação pode melhorar a segurança e a eficácia das interações automatizadas.



