diff --git a/pendencias.md b/pendencias.md index 5a1c517..7b9473f 100644 --- a/pendencias.md +++ b/pendencias.md @@ -1282,3 +1282,62 @@ ADR 0019, sem decisão em aberto que precisasse deste arquivo. 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. + +## Squad como executor — Onda 0 (honestidade do auditor) + +- **[contexto] Um parecer de engenharia externo (baseado num run real na VPS + — `forge squad "crie uma calculadora científica... gere um .html"` não + produziu arquivo nenhum) diagnosticou a causa raiz: o squad não tem + executor nem canal de artefato — `CoreService::RunTool` + (`core_server.rs:99`) devolve `Unimplemented` de propósito, o developer + (`developer.py`) faz uma única chamada de LLM e nunca toca disco, e o + auditor julga sem ver o resultado real. Conferi as citações (arquivo:linha) + contra o repo — quase todas exatas. Decisão de arquitetura endossada: + `RunTool` real no Rust (o contrato já existe no proto, zero breaking) + + loop ReAct no developer — não um "shuttle" do `final_output` pelo stream + de eventos. Onda 1 (o executor) e Onda 2 (o loop) ficam para depois; esta + entrega é só a Onda 0, que não depende delas. +- **[correção ao parecer] O auditor não é cego à saída do developer do jeito + que o parecer generalizou.** `validate_results` (chamado incondicionalmente + ao fim de `execute_complex_task`, `orchestrator.py:155`) **já recebia** + `execution_results` de verdade, incluindo o `final_output` real — o + veredito que decide `overall_success` não era cego ao conteúdo. A cegueira + genuína era só a chamada per-step `audit()` (quando o plano inclui uma + ação `"validate"` explícita, mapeada pro auditor via + `_select_agent_for_step`): `step_task` (`orchestrator.py:248-252`) era + montado só com `description`/`action`/`step` do plano, sem nenhum + resultado anterior. Isso mudou o escopo do fix de "rotear resultados pro + auditor" (já acontecia) para dois pontos mais específicos: + 1. `auditor.py`: `_AUDIT_SYSTEM_PROMPT` e `_VALIDATE_RESULTS_SYSTEM_PROMPT` + ganharam proibição explícita de alegar que um arquivo foi + criado/salvo/persistido — nenhum caminho tem evidência de filesystem + hoje (sem `RunTool`, nada escreve disco), então nenhum dos dois pode + honestamente afirmar isso, mesmo vendo o `final_output` real. + 2. `orchestrator.py::_execute_plan_steps`: `step_task` ganhou + `prior_results` (os resultados reais acumulados até aquele passo, não + um resumo sintético) — fecha a cegueira específica do `audit()` + per-step. `auditor.py::audit()` repassa isso ao gateway como + `prior_agent_results` quando presente (não fabrica a chave quando + ausente — proposta inicial em `_get_squad_proposals` continua sem + ela). +- **[decisão] `run_tool` "morto" não é herança de `CoreBackend`.** O trait + (`core_server.rs:27-34`) só declara `generate`/`request_permission` — o + `run_tool` que devolve `Unimplemented` (linha 99) está hardcoded direto no + `impl CoreService for CoreServer`, idêntico pra + qualquer `B`. Não muda o plano da Onda 1 (que já propõe corretamente + adicionar o método ao trait), só corrige a descrição do mecanismo — não + há o que "herdar" hoje, porque não existe hook nenhum pra um backend + fornecer isso. +- **[nota] Testes novos são todos deterministicos via `ScriptedGatewayClient`/ + `RoutingGatewayClient`** (nenhuma chamada de rede/LLM real) — provam a + proibição textual do prompt, a presença/ausência condicional de + `prior_agent_results` no payload, e o fluxo ponta a ponta (plano de 2 + passos "implement"→"validate" via `UnifiedOrchestrator` real) mostrando + que só a chamada per-step do auditor carrega `prior_agent_results`, com o + `final_output` real do developer dentro. +- **[gap remanescente, fora desta entrega] Onda 1 (RunTool real) e Onda 2 + (loop ReAct do developer)** ficam para PRs futuras — são a peça que faz + um arquivo aparecer de verdade no disco. Definição de pronto proposta + pelo parecer (e que endosso): `forge squad "crie X.html..."` produzindo + `X.html` real no workspace, registrado no ledger, com o auditor julgando + sobre um artefato que existe — não sobre uma alegação de texto. diff --git a/python/packages/forge-squad/src/forge_squad/agents/auditor.py b/python/packages/forge-squad/src/forge_squad/agents/auditor.py index 9a50305..e382d46 100644 --- a/python/packages/forge-squad/src/forge_squad/agents/auditor.py +++ b/python/packages/forge-squad/src/forge_squad/agents/auditor.py @@ -41,7 +41,7 @@ "notes": "string — sua avaliação dos achados e do contexto para ESTA tarefa", "additional_checks": ["lista de strings — verificações adicionais recomendadas, se houver"] } -Não aprove automaticamente só porque não há achados críticos na lista — considere a criticidade e o contexto da tarefa. Não reprove automaticamente por qualquer achado menor — pondere severidade.""" +Não aprove automaticamente só porque não há achados críticos na lista — considere a criticidade e o contexto da tarefa. Não reprove automaticamente por qualquer achado menor — pondere severidade. Você NUNCA tem acesso ao sistema de arquivos nem executou nenhuma ferramenta — NUNCA afirme que um arquivo foi criado, salvo ou persistido em disco; avalie somente o texto e os achados reportados.""" _VALIDATE_RESULTS_SYSTEM_PROMPT = """Você é um auditor revisando os resultados de uma equipe de agentes especialistas antes de aprovar a conclusão de uma tarefa. Responda SOMENTE com um objeto JSON: @@ -51,7 +51,7 @@ "issues": ["lista de strings — problemas encontrados nos resultados, se houver"], "agent_scores": {"nome_do_agente": 0.0} } -Avalie cada resultado pelo conteúdo real reportado (sucesso, confiança declarada, presença de erros) — não aprove por padrão. Quando houver evidência determinística de verificação (`verification_evidence` — typecheck/test/lint/SAST reais), pese o veredito e os achados dela — um veredito "fail" ou achados de severidade alta pesam contra a aprovação, mas a decisão final ainda é sua, considerando o contexto da tarefa.""" +Avalie cada resultado pelo conteúdo real reportado (sucesso, confiança declarada, presença de erros) — não aprove por padrão. Quando houver evidência determinística de verificação (`verification_evidence` — typecheck/test/lint/SAST reais), pese o veredito e os achados dela — um veredito "fail" ou achados de severidade alta pesam contra a aprovação, mas a decisão final ainda é sua, considerando o contexto da tarefa. Os agentes reportam apenas o texto que AFIRMAM ter produzido — nenhuma ferramenta grava em disco nesta etapa. NUNCA afirme que um artefato existe, foi salvo ou foi persistido no filesystem; isso não está nos resultados que você recebe.""" _JSON_BLOCK = re.compile(r"\{.*\}", re.DOTALL) @@ -99,21 +99,24 @@ async def audit(self, task: dict[str, Any]) -> dict[str, Any]: issues = self.check_security(task.get("code", "")) if task.get("code") else [] warnings = self.check_quality(task.get("metrics", {})) if task.get("metrics") else [] + payload: dict[str, Any] = { + "task_description": task.get("description", ""), + "security_issues": issues, + "quality_warnings": warnings, + } + # Quando este audit() é a ação "validate" de um passo do plano (não + # a proposta inicial), `_execute_plan_steps` (orchestrator.py) anexa + # os resultados reais dos passos anteriores — sem isto, a auditoria + # per-step era cega até ao texto que o developer disse ter + # produzido, não só ao filesystem. + if task.get("prior_results"): + payload["prior_agent_results"] = task["prior_results"] + request = LlmRequest( model=self.model, messages=[ {"role": "system", "content": _AUDIT_SYSTEM_PROMPT}, - { - "role": "user", - "content": json.dumps( - { - "task_description": task.get("description", ""), - "security_issues": issues, - "quality_warnings": warnings, - }, - ensure_ascii=False, - ), - }, + {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, ], requester=self.agent_type, ) diff --git a/python/packages/forge-squad/src/forge_squad/orchestrator.py b/python/packages/forge-squad/src/forge_squad/orchestrator.py index e06b4c0..81a1c7f 100644 --- a/python/packages/forge-squad/src/forge_squad/orchestrator.py +++ b/python/packages/forge-squad/src/forge_squad/orchestrator.py @@ -249,6 +249,11 @@ async def _execute_plan_steps(self, plan: dict[str, Any], task: dict[str, Any]) "description": step.get("description", ""), "action": step.get("action", ""), "step": step.get("step"), + # Resultados reais dos passos já concluídos (não fabricados, + # não resumo sintético) — sem isto, uma ação "validate" + # mapeada pro auditor (`_select_agent_for_step`) não tinha + # NENHUM contexto do que os passos anteriores produziram. + "prior_results": list(results), } result = await self.agents[agent_name].execute(step_task) quality = await self.evaluator.evaluate_agent_performance(agent_name, result) diff --git a/python/packages/forge-squad/tests/test_auditor.py b/python/packages/forge-squad/tests/test_auditor.py index 6c27c5a..d2d91bc 100644 --- a/python/packages/forge-squad/tests/test_auditor.py +++ b/python/packages/forge-squad/tests/test_auditor.py @@ -1,7 +1,11 @@ import asyncio import json -from forge_squad.agents.auditor import AuditorAgent +from forge_squad.agents.auditor import ( + _AUDIT_SYSTEM_PROMPT, + _VALIDATE_RESULTS_SYSTEM_PROMPT, + AuditorAgent, +) from forge_squad.gateway import LlmResponse, ScriptedGatewayClient @@ -144,6 +148,46 @@ def test_execute_sem_gateway_levanta_erro_claro(): assert "attach_gateway" in str(exc) +def test_prompts_de_auditoria_proibem_alegar_persistencia_de_arquivo(): + # Nenhum caminho do auditor tem evidência de filesystem hoje (RunTool + # ainda não existe, ver core_server.rs) — os dois prompts têm que + # proibir a alegação explicitamente, não só "não mencionar" por acaso. + assert "NUNCA afirme" in _AUDIT_SYSTEM_PROMPT + assert "NUNCA afirme" in _VALIDATE_RESULTS_SYSTEM_PROMPT + + +def test_audit_repassa_resultados_anteriores_reais_como_evidencia_ao_gateway(): + # Quando audit() é a ação "validate" de um passo do plano (não a + # proposta inicial), orchestrator.py anexa os resultados reais dos + # passos anteriores em `prior_results` — antes desta correção, o + # auditor per-step não recebia nada além de description/action/step. + payload = {"passed": True, "confidence": 0.8, "notes": "ok", "additional_checks": []} + gateway = ScriptedGatewayClient([LlmResponse(text=json.dumps(payload))]) + agent = AuditorAgent() + agent.attach_gateway(gateway) + + prior = [{"agent": "developer", "final_output": "calculadora", "success": True}] + asyncio.run(agent.audit({"description": "validar arquivo gerado", "prior_results": prior})) + + sent_content = gateway.requests[0].messages[1]["content"] + assert "prior_agent_results" in sent_content + assert "calculadora" in sent_content + + +def test_audit_sem_prior_results_nao_inclui_a_chave_no_payload(): + # A proposta inicial (`_get_squad_proposals`) chama audit() sem + # `prior_results` — o payload não deve ganhar uma chave vazia/fabricada. + payload = {"passed": True, "confidence": 0.8, "notes": "ok", "additional_checks": []} + gateway = ScriptedGatewayClient([LlmResponse(text=json.dumps(payload))]) + agent = AuditorAgent() + agent.attach_gateway(gateway) + + asyncio.run(agent.audit({"description": "revisar plano"})) + + sent_content = gateway.requests[0].messages[1]["content"] + assert "prior_agent_results" not in sent_content + + def test_resposta_sem_json_cai_no_fallback_honesto_nao_aprovado(): agent = AuditorAgent() agent.attach_gateway(ScriptedGatewayClient([LlmResponse(text="não consigo avaliar isso.")])) diff --git a/python/packages/forge-squad/tests/test_orchestrator.py b/python/packages/forge-squad/tests/test_orchestrator.py index e367e4f..d692823 100644 --- a/python/packages/forge-squad/tests/test_orchestrator.py +++ b/python/packages/forge-squad/tests/test_orchestrator.py @@ -170,6 +170,74 @@ def test_verification_evidence_presente_flui_para_validate_results(tmp_path): assert "verification_evidence" in auditor_calls[1].messages[1]["content"] +def test_step_task_de_validate_recebe_resultados_reais_dos_passos_anteriores(tmp_path): + # Antes desta correção, o step_task de uma ação "validate" só carregava + # description/action/step do plano — o auditor per-step era cego até ao + # texto que o developer disse ter produzido, não só ao filesystem + # (RunTool ainda não existe). Plano de 2 passos sequenciais (o 1º tem + # `dependencies` não-vazio só pra escapar do caminho paralelo de + # `_can_parallelize` e provar a passagem via step_task simples). + gateway = RoutingGatewayClient( + { + "planner": LlmResponse( + text=json.dumps( + { + "steps": [ + { + "step": 1, + "action": "implement", + "description": "criar arquivo", + "estimated_time": 5, + "dependencies": ["seed"], + "can_fail": True, + }, + { + "step": 2, + "action": "validate", + "description": "validar arquivo", + "estimated_time": 5, + "dependencies": [1], + "can_fail": True, + }, + ], + "estimated_duration": 10, + "confidence": 0.8, + } + ) + ), + "architect": LlmResponse( + text=json.dumps({"problem_analysis": "x", "recommendation": "y", "architecture": "z", "components": [], "confidence": 0.9}) + ), + "developer": LlmResponse( + text=json.dumps({"final_output": "conteudo-real-do-developer", "status": "completed", "confidence": 0.9}) + ), + "auditor": LlmResponse( + text=json.dumps({"passed": True, "approved": True, "confidence": 0.9, "notes": "ok", "issues": [], "agent_scores": {}, "additional_checks": []}) + ), + "designer": LlmResponse(text=json.dumps({"pattern": "x", "components": [], "confidence": 0.8})), + "ops": LlmResponse(text=json.dumps({"strategy": "x", "stages": [], "confidence": 0.8})), + } + ) + # Conteúdos de proposta heterogêneos (arch/dev/aud não concordam em + # forma) podem disparar HITL por consenso fraco — irrelevante pro que + # este teste prova, então aprova automaticamente pra chegar no step. + orch = UnifiedOrchestrator( + gateway, + permission_client=ScriptedPermissionClient([PermissionDecision(approved=True)]), + memory=AgentMemorySystem(storage_dir=tmp_path), + ) + asyncio.run(orch.execute_complex_task({"description": "criar e validar arquivo"})) + + # Das chamadas do auditor (proposta + step "validate" + validate_results + # final), só a do step carrega prior_agent_results — e o final_output + # real do developer está lá dentro, não só metadados do plano. + audit_step_calls = [ + c for c in gateway.calls if c.requester == "auditor" and "prior_agent_results" in c.messages[1]["content"] + ] + assert len(audit_step_calls) == 1 + assert "conteudo-real-do-developer" in audit_step_calls[0].messages[1]["content"] + + def test_propostas_sao_envolvidas_em_proposal_e_consenso_computa(tmp_path): # Se o wrapping Proposal(...) falhasse, reach_consensus levantaria; aqui # provamos que o consenso computou um decision_maker real.