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
59 changes: 59 additions & 0 deletions pendencias.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<B: CoreBackend> CoreService for CoreServer<B>`, 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.
29 changes: 16 additions & 13 deletions python/packages/forge-squad/src/forge_squad/agents/auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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,
)
Expand Down
5 changes: 5 additions & 0 deletions python/packages/forge-squad/src/forge_squad/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 45 additions & 1 deletion python/packages/forge-squad/tests/test_auditor.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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": "<html>calculadora</html>", "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.")]))
Expand Down
68 changes: 68 additions & 0 deletions python/packages/forge-squad/tests/test_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading