Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions crates/forge-cli/src/squad_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -523,6 +524,20 @@ pub fn router(hub: SquadHub, pool: Arc<SquadPool>) -> 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()`
Expand All @@ -536,7 +551,7 @@ pub fn default_squad_pool(root: &std::path::Path) -> Arc<SquadPool> {
py_dir,
socket_dir,
core_sock,
"claude-sonnet-5".into(),
squad_model(),
1,
Duration::from_secs(30),
))
Expand Down Expand Up @@ -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));
Expand Down
20 changes: 20 additions & 0 deletions infra/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions infra/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
73 changes: 73 additions & 0 deletions pendencias.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading