From d4f9d576c64a7bcb633fb40b310d72bfdc0a928f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 04:13:36 +0000 Subject: [PATCH] fix: modelo do squad web/dashboard hardcoded em claude-sonnet-5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O pool Python de longa duração do agente web (squad_agent.rs, capacidade 1, construído uma vez na subida do dashboard) sempre instanciava os 5 agentes com "claude-sonnet-5" fixo, sem nenhum caminho de request/frontend pra sobrescrever — selecionar outro modelo na tela Modelo não tinha efeito algum no squad, só na Sessão (que já era wireada desde a Onda 13). Sem ADR por trás (diferente do max_autonomy_level/ADR 0021, que um diagnóstico externo citou por engano pra justificar este hardcode). - squad_agent.rs: helper squad_model() lê FORGE_SQUAD_MODEL com fallback pro default antigo; usado em default_squad_pool (o que de fato determina o modelo dos agentes Python), no RunOpts do handler (tier de rate-limit) e no rótulo de modelo do ledger, que antes sempre "mentia" claude-sonnet-5. - infra/docker: documenta a env var e corrige a distinção real entre `forge squad --model` (CLI, já funcionava) e o squad do dashboard (não funcionava); docker-compose.yml ganha environment: no serviço dashboard (não passava nenhuma API key antes). - pendencias.md: registra o achado, a correção e o gap remanescente (per-tarefa real exigiria model no proto SquadTask). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016kDm3viu7mJm6n5zphKwAF --- crates/forge-cli/src/squad_agent.rs | 42 ++++++++++++++--- infra/docker/README.md | 20 ++++++++ infra/docker/docker-compose.yml | 12 +++++ pendencias.md | 73 +++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 6 deletions(-) diff --git a/crates/forge-cli/src/squad_agent.rs b/crates/forge-cli/src/squad_agent.rs index 2fa9314..c3e77a4 100644 --- a/crates/forge-cli/src/squad_agent.rs +++ b/crates/forge-cli/src/squad_agent.rs @@ -320,10 +320,11 @@ where // Abre a sessão de ledger ANTES de mover `description` para o // `SquadTask` abaixo — mesma sessão/ledger que o resto da plataforma - // usa (`.forge/forge.db`), "model" aqui é só rótulo informativo (a - // sessão de squad não escolhe modelo por si — cada agente chama - // `Generate` com o modelo do próprio request). - let mut ledger_session = crate::session::Session::open(&root, &description, "claude-sonnet-5") + // usa (`.forge/forge.db`). "model" aqui é rótulo informativo do que o + // pool está configurado pra usar (`squad_model()`), não uma escolha + // por-tarefa — a sessão de squad não escolhe modelo por si, e cada + // agente Python chama `Generate` com o modelo herdado do pool. + let mut ledger_session = crate::session::Session::open(&root, &description, &squad_model()) .map_err(|e| e.to_string())?; let lease = pool @@ -430,7 +431,7 @@ async fn run_squad_handler( )); } else { let opts = crate::RunOpts { - model: "claude-sonnet-5".into(), + model: squad_model(), agent: "build".into(), yes: false, no_cache: false, @@ -523,6 +524,20 @@ pub fn router(hub: SquadHub, pool: Arc) -> Router { .with_state(SquadAgentState { hub, pool }) } +/// Modelo do squad — configurável via `FORGE_SQUAD_MODEL` porque o pool é +/// construído **uma vez só**, na subida do `forge dashboard` (capacidade 1, +/// reusado sequencialmente entre tarefas — ver comentário de módulo), e vira +/// o default de todos os 5 agentes Python (`UnifiedOrchestrator`, que passa +/// este `model` pra cada `ArchitectAgent`/`DeveloperAgent`/etc.). Não existe +/// hoje um caminho por-tarefa (`RunSquadBody`/`SquadTask` não carregam +/// `model`) — fazer isso de verdade exigiria o campo no proto, escopo maior +/// que esta correção. Sem a env var, mantém o default antigo +/// (`claude-sonnet-5`) — comportamento inalterado pra quem não configurar +/// nada. +fn squad_model() -> String { + std::env::var("FORGE_SQUAD_MODEL").unwrap_or_else(|_| "claude-sonnet-5".into()) +} + /// Constrói o pool do squad para o agente web — capacidade 1 (ver /// comentário de módulo). Workspace Python ausente não impede a /// construção (lazy: só falha, com erro claro, no primeiro `acquire()` @@ -536,7 +551,7 @@ pub fn default_squad_pool(root: &std::path::Path) -> Arc { py_dir, socket_dir, core_sock, - "claude-sonnet-5".into(), + squad_model(), 1, Duration::from_secs(30), )) @@ -577,6 +592,21 @@ mod tests { .collect() } + /// Prova as duas pontas do bug real (achado em produção via VPS): sem a + /// env var, o comportamento antigo (`claude-sonnet-5`) continua intacto; + /// com ela, o squad passa a pedir o modelo configurado — antes desta + /// correção não havia NENHUM jeito de mudar isso sem recompilar. + #[test] + fn squad_model_le_forge_squad_model_com_fallback_pro_claude_sonnet_5() { + std::env::remove_var("FORGE_SQUAD_MODEL"); + assert_eq!(squad_model(), "claude-sonnet-5"); + + std::env::set_var("FORGE_SQUAD_MODEL", "deepseek-chat"); + assert_eq!(squad_model(), "deepseek-chat"); + + std::env::remove_var("FORGE_SQUAD_MODEL"); + } + #[test] fn resolve_hitl_sem_pendente_devolve_err() { let hub = SquadHub::new(Duration::from_millis(100)); diff --git a/infra/docker/README.md b/infra/docker/README.md index 44ab68e..7e67e97 100644 --- a/infra/docker/README.md +++ b/infra/docker/README.md @@ -59,6 +59,26 @@ forge chat --model deepseek-chat forge squad --model deepseek-chat "tarefa multi-agente" ``` +**Só pelo CLI.** `forge squad --model` (acima) spawna um processo Python novo +a cada chamada e passa `--model` de verdade (`squad.rs::run_squad` → +`SquadSupervisor::spawn(..., &opts.model)`) — funciona como os outros dois. + +**O botão "Squad" do dashboard (navegador) é diferente.** Ali os 5 agentes +Python rodam num pool de longa duração com capacidade 1, criado **uma vez** +quando o `forge dashboard` sobe (não a cada tarefa) — não existe hoje um +caminho por-tarefa pra escolher modelo (nem a tela "Modelo" do frontend, nem +nenhum campo do request chegam até o squad; ver `pendencias.md`). Configure +**antes** de subir o dashboard, via env var: + +```sh +docker run --rm --network host -e DEEPSEEK_API_KEY=sk-... -e FORGE_SQUAD_MODEL=deepseek-chat \ + -v "$PWD":/work forge:test forge dashboard --port 7878 +``` + +Sem `FORGE_SQUAD_MODEL`, o squad do dashboard continua mandando +`claude-sonnet-5` pro provider configurado — que a DeepSeek rejeita (400), +mesmo com a key certa. + Modelos disponíveis na DeepSeek hoje: `deepseek-chat` (V3, uso geral) e `deepseek-reasoner` (R1, raciocínio — mais lento). Confirme na doc oficial da DeepSeek o nome exato vigente e a janela de contexto real do modelo escolhido; diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index 88563ee..3621ffa 100644 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -54,6 +54,18 @@ services: dockerfile: infra/docker/Dockerfile image: forge:test network_mode: host + environment: + # Mesma regra do serviço `forge` acima (ADR 0001, prioridade + # Anthropic → DeepSeek → OpenAI). Sem isto, `run --rm dashboard` sobe + # sem NENHUM provider — squad/sessão pelo navegador falham com + # "nenhum provider configurado" antes mesmo de chegar no modelo. + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + # Modelo dos 5 agentes do squad ao vivo (pool de longa duração, + # construído uma vez nesta subida — não há campo por-tarefa ainda; + # ver README "Usando a API da DeepSeek" e pendencias.md). + FORGE_SQUAD_MODEL: ${FORGE_SQUAD_MODEL:-} volumes: - ../../:/work:rw command: ["forge", "dashboard", "--port", "7878"] diff --git a/pendencias.md b/pendencias.md index 61d9e96..5a1c517 100644 --- a/pendencias.md +++ b/pendencias.md @@ -1209,3 +1209,76 @@ ADR 0019, sem decisão em aberto que precisasse deste arquivo. seguidos) — reconfirmado verde rodando isolado logo em seguida (1.5s até a primeira asserção). Contenção de recurso do ambiente, não regressão: nada nesta onda toca squad/sidecar Python. + +## Pós-Fase 7 — modelo do squad hardcoded (achado em produção, VPS real) + +- **[achado — bug real, sem ADR por trás] O squad do dashboard (navegador) + sempre mandava `"claude-sonnet-5"` pros 5 agentes Python, mesmo com + `DEEPSEEK_API_KEY` configurada e o usuário selecionando `deepseek-chat` na + tela Modelo.** Reportado como "selecionei deepseek na UI, cliquei, e não + mudou nada" — confirmado batendo no código, não no relato: `RunSquadBody` + (`squad_agent.rs`) só tem `task: String`, o frontend não tinha como mandar + modelo pro squad; `default_squad_pool` (que constrói o pool Python de + longa duração, capacidade 1, criado uma vez na subida do `forge + dashboard`) tinha `"claude-sonnet-5"` literal, que vira o default de + `ArchitectAgent`/`DeveloperAgent`/etc. (`orchestrator.py`) pra sempre, + até o processo reiniciar. + - **Importante: isso é diferente do `max_autonomy_level` (ADR 0021).** Uma + investigação externa citou o comentário "Descope explícito da Onda 13 + (ADR 0021)" pra justificar o hardcode do **modelo** — mas esse + comentário (`squad_agent.rs`, perto da construção do `SquadTask`) é + sobre `max_autonomy_level`, um campo totalmente diferente. Conferi a + ADR 0021 inteira: fala só de autonomia, nunca de modelo. O hardcode do + modelo não era uma decisão documentada — era lacuna real, sem dono. + - **O `forge squad --model` via CLI (terminal) nunca teve esse bug** — + `squad.rs::run_squad` sempre passou `opts.model` de verdade pro + `SquadSupervisor::spawn` (processo Python novo a cada chamada, não um + pool persistente). O bug é específico do agente web/dashboard + (`squad_agent.rs`), que reusa um pool entre tarefas. + - **Mecanismo completo do "400 modelo desconhecido"** (pra não repetir o + diagnóstico externo que citou a ADR errada e propôs um workaround que + ele mesmo reconheceu não funcionar — setar `ANTHROPIC_API_KEY` com uma + key falsa "pra forçar" a DeepSeek): `Gateway::from_env()` + (`forge-llm/src/gateway.rs:44-81`) escolhe o provider **só por qual env + var existe e não é vazia**, em ordem fixa Anthropic → DeepSeek → + OpenAI — o nome do modelo nunca decide o provider, só vira o texto + dentro do corpo da requisição pro provider que "ganhou" a prioridade + (`call_provider`/`build_request_body`). Consequência: mesmo com + `ANTHROPIC_API_KEY` falsa, o Gateway tentaria Anthropic primeiro (401), + cairia pra DeepSeek (fallback real, o `for` tenta todos), mas o texto do + modelo continuaria `"claude-sonnet-5"` — a DeepSeek rejeitaria do mesmo + jeito. **E isso também é um risco pra Sessão/chat** (que já manda + `body.model` corretamente, Onda 13): se `ANTHROPIC_API_KEY` estiver + definida no ambiente (mesmo com valor velho/inválido), ela sempre é + tentada primeiro, e uma seleção de `deepseek-chat` na UI mandaria esse + texto pra Anthropic antes de cair pro DeepSeek. Vale conferir no + ambiente da VPS se `ANTHROPIC_API_KEY` está mesmo ausente. +- **[decisão] Fix: `FORGE_SQUAD_MODEL` (env var), não trocar o literal por + outro literal.** `default_squad_pool` agora lê `FORGE_SQUAD_MODEL` com + fallback pra `"claude-sonnet-5"` (comportamento antigo intacto pra quem + não configurar nada) via um helper `squad_model()` compartilhado — usado + também no `RunOpts.model` de `run_squad_handler` (que antes só afetava o + tier de rate-limit, não o texto do modelo — corrigido por consistência, + não porque fosse a causa do bug) e no rótulo de `model` da sessão do + ledger (antes hardcoded, então o ledger sempre "mentia" `claude-sonnet-5` + mesmo quando outro modelo era usado — agora reflete o que o pool está + configurado pra usar de verdade). +- **[dúvida/gap remanescente] Isto NÃO faz a tela Modelo controlar o squad + por tarefa** — `FORGE_SQUAD_MODEL` é uma env var fixa no deploy (lida uma + vez, na subida do dashboard), não um campo por-request. Fazer a seleção + da tela Modelo valer pro squad de verdade exige adicionar `model` ao + proto `SquadTask` (`schemas/proto/squad.proto`) e cada agente Python ler + por tarefa em vez de herdar do pool na construção — mesma categoria de + mudança arquitetural do `max_autonomy_level`/ADR 0021 (campo novo no + proto + Python consumindo por chamada), não coube nesta correção pontual. + Deixo registrado como trabalho futuro, não como algo que o fix atual já + resolve. +- **[decisão] `docker-compose.yml`: serviço `dashboard` ganhou `environment:`** + (`ANTHROPIC_API_KEY`/`DEEPSEEK_API_KEY`/`OPENAI_API_KEY`/`FORGE_SQUAD_MODEL`, + mesmo padrão do serviço `forge`) — achado ao revisar o arquivo pra + documentar a env var nova: o serviço `dashboard` não passava NENHUMA key + pro container via `docker compose run --rm dashboard` (só funcionava se o + usuário lembrasse de repetir `-e` na linha de comando). Sem isso, o + dashboard subiria sem provider nenhum configurado — bug adjacente, não o + reportado, mas bloqueava o mesmo fluxo (squad/sessão pelo navegador) então + corrigi junto em vez de deixar half-fixed.