Loop de agente (chat completions)
Auto-ativação
Seção intitulada “Auto-ativação”Quando uma organização tem pelo menos um servidor MCP outbound habilitado (configurado em MCP → Servers), toda requisição em /v1/chat/completions passa automaticamente pelo loop de agente. A decisão é server-side, baseada na flag mcp_agent_active do organization_profiles:
mcp_agent_active = has_mcp_outbound AND EXISTS(SELECT 1 FROM mcp_outbound_servers s WHERE s.org_id = ... AND s.enabled = true)Desabilite todos os servidores (ou remova has_mcp_outbound do plano) e a próxima requisição volta para o caminho pass-through padrão sem nenhuma mudança de comportamento.
Opt-out por requisição
Seção intitulada “Opt-out por requisição”Um cliente cuja org tem o loop de agente ativo ainda pode rodar uma chat completion pass-through pura mandando o header floopy-mcp-disabled:
floopy-mcp-disabled: trueValores truthy (true / 1 / yes, case-insensitive) pulam o caminho do agente para aquela única requisição. Útil quando o caller sabe que o prompt não precisa de tools e quer evitar o overhead por round do agente.
O que acontece por round
Seção intitulada “O que acontece por round”client → /v1/chat/completions │ ├─ org_ctx.mcp_agent_active = true E sem header de opt-out │ ▼ agent::agent_loop::run │ a cada round: │ 1. budget.check_round (max_rounds, wall-clock) │ 2. chamada LLM via execute_strategies ← span por round │ • cancelável via tokio::select! │ • dispatcha para provider (OpenAI/Anthropic/Gemini/Bedrock) │ 3. sem tool_calls → retorna mensagem final do assistente │ 4. com tool_calls → dispatch_round (paralelo, allowlist-checado) │ 4a. cache hit? reusa o valor │ 4b. miss? resolve secret_ref, chama upstream, cacheia │ 4c. SSRF guard checa o IP resolvido — bloqueio aborta a │ request inteira, não só a tool call (ver Segurança) │ 5. emite audit_log.agent.tool_call por dispatch │ 6. anexa mensagens de tool, loopDescoberta de tools (auto-discovery, não declarada)
Seção intitulada “Descoberta de tools (auto-discovery, não declarada)”A Floopy segue o modelo padrão de mercado para MCP: clientes referenciam servidores, não tools individuais. O loop de agente chama tools/list em cada servidor habilitado em paralelo (cache TTL de 10 minutos), agrega os resultados e expõe a união ao LLM. Tools são deduplicadas por nome — em colisões, vence o primeiro servidor (ordem de configuração).
Validação estrita de input-schema: tools cujo inputSchema esteja ausente, null ou não seja um objeto JSON são removidas do catálogo. A remoção é logada como mcp_tool_invalid_schema; se um servidor ficar sem tools válidas após validação, dispara um warning de nível de servidor mcp_server_no_valid_tools. A spec MCP exige inputSchema, então um servidor caindo aqui em produção está mal configurado.
Budgets
Seção intitulada “Budgets”Três tetos independentes, aplicados dentro do loop:
- Rounds — limitado em
runtime.max_rounds(1..50, default 10), e adicionalmente emMAX_ROUNDS_CAP = 50independente da config da org. - Wall clock — deadline de 120s selado na aquisição. Cada round corre a chamada LLM e o dispatch das tools contra
tokio::time::sleep_until(deadline)viatokio::select!, então um upstream lento não consegue ultrapassar o deadline. - Concorrência — 16 runs simultâneos por org, aplicado por contador atômico em Redis (Lua
GET-and-conditional-INCR). Excedente retorna429comretry_after_secs = 60.
Esgotar rounds ou wall-clock emite uma linha audit_log.agent.budget_exhausted e retorna as mensagens parciais com finish_reason = "length".
Cache de resultados de tools
Seção intitulada “Cache de resultados de tools”Tool calls idênticas (mesma chave (org_id, server_id, tool_name, canonical_args)) dentro de runtime.tool_cache_ttl_seconds reusam o resultado anterior. O valor cacheado é HMAC-assinado com FLOOPY_AGENT_CACHE_PEPPER, então:
- Um writer no Redis não consegue injetar um resultado forjado.
- Um valor de uma
(org, server, tool)não pode ser migrado para outra — a chave do cache faz parte do MAC. - Uma rotação de pepper invalida toda entrada cacheada de uma vez.
tool_cache_ttl_seconds = 0 desabilita o cache por completo.
Modos de stream
Seção intitulada “Modos de stream”runtime.stream_mode é o switch da org entre dois modos:
final_only— quando o cliente pedestream: true, o loop roda até o fim (rounds intermediários bufferizados server-side), e a mensagem final do assistente é fragmentada em múltiplos frames SSEdelta.contentde ~64 bytes para o cliente ver a resposta progressivamente. Limites UTF-8 são preservados. Texto intermediário (ex.: “Vou consultar o clima.”) não é streamado ao vivo nesta versão — fica reservado para um follow-up que conduza cada round através do upstream em modo stream.disabled— mesmo que o cliente peçastream: true, a resposta é um únicochat.completionJSON não-streaming. Útil para integrações que não consomem SSE.
Quando o cliente não pede streaming, ambos os modos retornam JSON.
Compatibilidade entre providers
Seção intitulada “Compatibilidade entre providers”Tools, tool_use / tool_calls e tool_result fazem round-trip por todos os providers da Floopy:
| Provider | Campo de tool | Campo de resultado | Stop reason |
|---|---|---|---|
| OpenAI / OpenAI-compat | tools[].function.parameters | tool_calls[] | tool_calls |
| Anthropic | tools[].input_schema | content[].tool_use | tool_use |
| Gemini | tools[].functionDeclarations | parts[].functionCall | (inferido pelo tipo de part) |
| Bedrock Converse | toolConfig.tools[].toolSpec | output.message.content[].toolUse | tool_use |
A Floopy traduz entre essas formas no fio — clientes sempre falam o shape do OpenAI.
Segurança: guard SSRF outbound
Seção intitulada “Segurança: guard SSRF outbound”Toda chamada HTTP outbound do loop de agente (e do delivery de webhooks) passa por uma policy SSRF cluster-wide carregada uma vez de variáveis de ambiente no startup. O comportamento default mantém os blocks hardcoded pré-PR-2; operadores podem ajustar sem mudança de código:
FLOOPY_OUTBOUND_SSRF_BLOCK_LOOPBACK=trueFLOOPY_OUTBOUND_SSRF_BLOCK_PRIVATE=true # RFC1918FLOOPY_OUTBOUND_SSRF_BLOCK_LINK_LOCAL=true # inclui 169.254.169.254 (cloud metadata)FLOOPY_OUTBOUND_SSRF_BLOCK_MULTICAST=trueFLOOPY_OUTBOUND_SSRF_BLOCK_CGNAT=true # 100.64/10FLOOPY_OUTBOUND_SSRF_BLOCK_ULA_V6=true # fc00::/7FLOOPY_OUTBOUND_SSRF_EXTRA_BLOCKED_CIDRS= # CSV de CIDRs v4FLOOPY_OUTBOUND_SSRF_ALLOWLIST_CIDRS= # CSV; sobrepõe todo blockUm bloqueio durante o loop aborta a request inteira com HTTP 403 (outbound_ssrf_blocked: ssrf_blocked:{reason}:{ip}). Não realimenta o erro ao LLM como tool_result — fazer isso permitiria que um atacante controlando uma URL de servidor MCP enumerasse a rede interna da Floopy uma tool call por vez.
Validação de argumentos da tool
Seção intitulada “Validação de argumentos da tool”Toda tool call produzida pelo LLM é re-validada contra o inputSchema retornado pelo tools/list do upstream antes do Floopy dispatchar. Uma violação de schema vira tool error realimentado ao LLM em vez de crash duro — o loop continua e o modelo tem chance de retry com argumentos corretos. Bloqueios SSRF (acima) são a única classe fatal.
Observabilidade
Seção intitulada “Observabilidade”Cada round escreve no ClickHouse request_response_rmt com:
surface = 'agent'agent_run_id— UUID; igual ao request_id do round 0, reusado entre rounds.round_index— zero-based.tool_call_index—NULLna linha da chamada LLM; carregado separadamente em linhas de audit por dispatch.
Cada tool call escreve uma linha audit_log.agent.tool_call com (agent_run_id, round_index, tool_call_index, server_id, tool, status, latency_ms).
O orquestrador abre um span routing_execute em volta da invocação inteira do agente, com N spans filhos agent_round_{idx} (um por dispatch LLM) carregando atributos round_index e agent_run_id. Junte tudo por agent_run_id para rastreabilidade ponta-a-ponta.
Desabilitando
Seção intitulada “Desabilitando”- Por requisição: header
floopy-mcp-disabled: true. - Por servidor: toggle do Switch no card do Server.
- Por org: remova
has_mcp_outbounddo plano ou desabilite todos os servidores. - Tweak por runtime:
tool_cache_ttl_seconds = 0desabilita só o cache;max_rounds = 1força comportamento single-shot.
O overhead do loop quando inativo é uma checagem booleana em OrgContext.mcp_agent_active mais uma leitura opcional de header.