Onda 1–3: RunTool ativado — squad como executor real - #48
Merged
Conversation
CoreBackend ganha run_tool (core_server.rs); o handler gRPC vira um passthrough fino em vez de Unimplemented — RunTool já existia no proto, zero mudança breaking. core_run_tool (squad.rs) recalcula o escopo real via Tool::scope (o ToolCall.scope da rede é só informativo, nunca decide permissão), avalia via PermissionEngine (perfil BUILD), roda a ferramenta em spawn_blocking e registra squad.tool_run no ledger. Os três CoreBackend de produção (GatewayCoreBackend, WebSquadCoreBackend, ScriptedSquadCoreBackend) ganham ToolRegistry/PermissionEngine e delegam a core_run_tool, cada um reusando sua própria ponte HITL já existente. Testes de fronteira (core_server_inprocess.rs, sem Python): um ToolCall via UDS puro cria um arquivo real no disco; uma negação do motor de permissões não executa nada. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6H8cTKxEw8uPHW8L38apx
…verdade
tool_client.py (novo) + GrpcToolClient (grpc_clients.py): ToolClient
Protocol sobre CoreService.RunTool — zero codegen novo, o stub Python já
existia gerado e nunca usado.
developer.py: _implement_with_tools é o loop ReAct real — o modelo
alterna entre tool_call (executado via tool_client, sob o motor de
permissões do Rust) e final_answer, até um dos dois ou até estourar
_MAX_REACT_STEPS/_REACT_TIMEOUT_SECONDS (honesto: "incomplete", nunca
fabrica sucesso). Sinal de ativação: bool(task.get("action")) — separa
trabalho real do plano de proposta/avaliação sem tocar o caminho de
chamada única existente.
Achado real ao revisar o plano antes de implementar: _can_parallelize
manda um passo "implement" sem dependencies (o caso comum) para
_extract_parallel_tasks, que chamava o developer SEM "action" — o sinal
de ativação nunca disparava nesse caminho, reproduzindo o bug original
mesmo com RunTool/ReAct prontos. Fix: _extract_parallel_tasks propaga
action/prior_results (mesma forma do step_task sequencial). Regressão
coberta por teste que falha sem o fix (verificado manualmente revertendo
e rodando antes de reaplicar).
core_generate (squad.rs) ganha o papel "assistant" (Role::Assistant) —
sem isto, o histórico multi-turno do loop ReAct colapsava tudo em
Role::User, e a API da Anthropic exige alternância estrita.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6H8cTKxEw8uPHW8L38apx
…ADR 0023 auditor.py: _claims_completion_without_write_evidence é um gate duro (mesma filosofia de forge_review/gates.py::evaluate) — reprova "completed" sem tool_calls de escrita bem-sucedida ANTES do gateway ser chamado. Só se aplica a resultados que passaram pelo loop ReAct (carregam a chave tool_calls, mesmo vazia); um resultado do caminho antigo de chamada única nunca teve infraestrutura de ferramenta disponível, então não é gateado — achado real ao rodar a suíte (sem essa distinção, o gate quebrava um teste legítimo da Onda 0 que usa o caminho antigo de propósito). orchestrator.py: o veredito final vira observável via um novo StepResult (step_id: "final_validation") — sem isto, "o auditor julga sobre o arquivo real" não seria verificável fora do dict de retorno que server.py descarta. Teste de fechamento (squad_e2e.rs, processo Python real, sem key): forge squad "crie scientific-calculator.html..." produz o arquivo de verdade no workspace, registra squad.tool_run no ledger, e o auditor aprova sobre evidência real — o backend roteirizado falha alto e claro (assert dentro do generate()) se o payload que chega ao Rust não carregar tool_calls, provando que o auditor não julga no vácuo. ADR 0023 documenta a decisão completa (Ondas 1-3) e os dois achados reais de implementação (caminho paralelo do orquestrador, papel "assistant" em core_generate). pendencias.md fecha o gap remanescente registrado na Onda 0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6H8cTKxEw8uPHW8L38apx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumo
Ativa
RunToolnoCoreService(Rust), implementa o loop ReAct real noDeveloperAgent(Python) e adiciona um gate duro no auditor para validar evidência real de execução de ferramenta. Fecha as Ondas 1–3 da "tool execution architecture" (ADR 0023):forge squad "crie X.html..."agora produzX.htmlde verdade no workspace, registrado no ledger, com o auditor julgando sobre artefato que existe — não sobre alegação de texto.Mudanças principais
Onda 1 — Executor real no Rust
crates/forge-cli/src/squad.rs: novo helpercore_run_toolcompartilhado pelos trêsCoreBackendde produção (GatewayCoreBackend,WebSquadCoreBackend,ScriptedSquadCoreBackend). Recalcula escopo real viaTool::scope(oToolCall.scopeda rede é só informativo — nunca decide permissão), avalia viaPermissionEngine(perfilBUILDreutilizado), executa emspawn_blockinge registra no ledger via novosession::append_entry(variante sem override).crates/forge-cli/src/session.rs: refatoraappend_override_entryemappend_entry_implgenérico; novoappend_entrypúblico para logging fora do ciclo de vida deSession.crates/forge-sidecar/src/core_server.rs: implementaCoreBackend::run_tool(antesUnimplemented); handler gRPC é passthrough fino.crates/forge-sidecar/src/service.rs: testes ganham stub derun_tool.crates/forge-cli/src/squad_agent.rs:WebSquadCoreBackendganharoot/tools/tool_permissionse chamacore_run_tool.exit_code:0sucesso;1erro de execução/args inválidos/ferramenta desconhecida (vale tentar de novo);-1negado (não adianta repetir) — sinal estrutural para o loop ReAct.Onda 2 — Loop ReAct real no
DeveloperAgent(Python)python/packages/forge-squad/src/forge_squad/agents/developer.py: novo_implement_with_toolsé o loop ReAct real. O modelo alterna entretool_call(executado de verdade viaToolClient) efinal_answer, iterando até um dos dois ou até estourar_MAX_REACT_STEPS/_REACT_TIMEOUT_SECONDS. Sinal de ativação:bool(task.get("action")) and tool_client is not None— separa trabalho real do plano de proposta/avaliação.python/packages/forge-squad/src/forge_squad/tool_client.py(novo): contratoToolClientdesacoplado do transporte gRPC (mesmo padrão deGatewayClient/PermissionClient).ScriptedToolClientpara testes.python/packages/forge-squad/src/forge_squad/grpc_clients.py: novoGrpcToolClientsobreCoreService.RunTool(stub Python já existia, nunca usado).python/packages/forge-squad/src/forge_squad/orchestrator.py:UnifiedOrchestrator.__init__ganhatool_clientopcional; só o developer o recebe (attach_tool_client).python/packages/forge-squad/src/forge_squad/server.py: injeta `Ghttps://claude.ai/code/session_01K6H8cTKxEw8uPHW8L38apx