Arquitetura do núcleo do BeeverForge
Execução e durabilidade
O motor é o Temporal. Cada job é um workflow, cada efeito colateral é uma atividade e cada espera, de uma consulta à CI a uma aprovação de produção, é um temporizador ou uma condição durável. Por isso, reiniciar um worker não perde o job: suas esperas continuam de onde estavam, e um job cujo worker se perde durante a passada do agente fica em espera para que uma pessoa o retome ou pule, porque essa passada não é repetida.
Workflows, atividades e a fila de tarefas#
Um único processo worker registra todos os workflows e todas as atividades em uma só fila de tarefas, beeverforge-jobs. Os principais workflows são:
JobWorkflow- Um por ticket. O dimensionamento, a execução na caixa, o acompanhamento até o merge, a promoção e cada espera acontecem dentro dele.
PollWorkflow- Uma varredura do quadro por ciclo, que inicia jobs nas vagas livres.
DeployWatchWorkflow- Um filho independente que observa a implantação depois de um merge.
PreviewWorkflow- Uma prévia em execução de uma mudança sem merge, quando há um ambiente de prévia configurado.
ConversationWorkflow- Uma conversa com a fábrica, admitida e conduzida por sinais.
Outros workflows cobrem a vigilância do tech-lead, a atualização do conhecimento, o papel de produto e as varreduras listadas abaixo. O trabalho pesado, como executar o job na sua caixa, é a atividade run_job.
Agendamentos#
O trabalho recorrente é um agendamento do Temporal, que o worker reconcilia ao iniciar.
| Agendamento | Intervalo | Finalidade |
|---|---|---|
beeverforge-poller | 3 minutos | Varrer os quadros; um ciclo que se sobreporia é ignorado. |
beeverforge-techlead-watch-* | 1 hora | A ronda do tech-lead sobre o trabalho em espera. |
beeverforge-card-questions-* | 1 hora | Ler as respostas às perguntas feitas nos cartões. |
beeverforge-okf-refresh-* | 6 horas | Atualizar o mapa de conhecimento em relação ao branch base. |
beeverforge-product-sweep-* | 1 semana | A varredura do papel de produto em busca de trabalho que não pode terminar. |
beeverforge-preview-reaper | 10 minutos | Encerrar as prévias cujo pull request já recebeu merge. |
O motor guarda o histórico dos workflows por 30 dias por padrão (BEEVERFORGE_ENGINE_RETENTION_DAYS).
Sinais e temporizadores duráveis#
A resposta de uma pessoa chega a um job em andamento como um sinal: approve_prod, not_yet, human_merge_gate, act_on_impediment e advise_decision. As esperas são workflow.sleep e workflow.wait_condition com tempo limite, de modo que uma espera de minutos ou dias não ocupa nenhum worker e sobrevive a uma reinicialização. Exemplos são a consulta à CI (2 minutos, depois 15), a consulta da implantação (1 minuto), a aprovação de produção (3 dias), o acompanhamento do merge (14 dias) e um impedimento em espera (3 dias).
Efeitos colaterais idempotentes#
Uma atividade pode ser executada de novo após uma falha, então cada escrita externa é feita para ser repetida com segurança ou não é repetida:
- Os pull requests são buscar e depois atualizar: o pull request aberto do branch é reutilizado.
- As tags de versão são buscar ou criar.
- As transições e os comentários dos cartões levam o id do evento da atividade que os causou; a porta do cartão devolve a transição já registrada para esse id em vez de escrevê-la de novo.
- Os merges são lidos de novo na plataforma Git depois da chamada de merge, em vez de confiar no resultado da chamada.
- O próprio job só é repetido quando seu tipo de caixa declara que uma nova tentativa se reconecta ao job em andamento. As caixas
containereworktreenão declaram isso: executam sua atividade de job uma única vez, porque uma nova tentativa seria uma segunda passada do agente que repetiria cada comentário. A descriçãofargatedeclara isso; seu executor é fornecido por um complemento.
Heartbeats#
As atividades longas emitem heartbeats. As atividades de job rodam com tempo limite de heartbeat de 120 segundos e batem a cada 30 segundos aproximadamente; o trabalho que continua depois da última batida é limitado para caber nessa janela. Assim, um worker que morre no meio de uma atividade é detectado dentro do tempo limite, e não ao fim da duração completa da atividade.
Segurança na reexecução#
O Temporal reexecuta o histórico de um workflow contra o código atual, então uma mudança na lógica do workflow não pode alterar os comandos que um workflow em andamento já registrou.
- Cada mudança desse tipo é protegida por
workflow.patched()com um id que nomeia o comportamento, por exemplosigned-gatesoua-self-merge-is-read-back. Os históricos antigos seguem o caminho antigo; as novas execuções seguem o novo. - Os testes de Replayer executam um workflow contra um servidor Temporal efêmero, obtêm o histórico registrado e o reexecutam com
Replayercontra o código atual. Alguns executam antes uma cópia do workflow de antes do patch. Os históricos são registrados dentro dos testes; nenhum é versionado como arquivo. - O código que precisa ficar fora do corpo do workflow é mantido em atividades. A verificação da prova da caixa no recolhimento fica na atividade de varredura do quadro exatamente por isso.
Um cliente do motor por processo#
Um processo mantém um cliente do motor. runtime/temporal/connection.py guarda um único cliente identificado pelas configurações da conexão; os primeiros chamadores concorrentes compartilham uma única tentativa de conexão, e a reconexão espera no máximo 60 segundos. Testes de guarda verificam que as primeiras chamadas concorrentes, e as aprovações repetidas, abrem um único cliente.
Estruturas limitadas#
O worker e o painel são processos de longa duração, então os caches em memória são limitados por construção.
util/bounded.pyforneceBoundedDict: seguro entre threads, com tamanho máximo obrigatório, que remove primeiro a entrada mais antiga e conta o que removeu. De propósito, não é uma subclasse dedict, para que nada possa fazê-lo crescer contornando seu limite.util/single_flight.pypermite que leitores que chegam juntos compartilhem uma leitura. Cada um fica protegido do cancelamento de outro chamador, há no máximo uma leitura em andamento, e uma falha é compartilhada com quem está esperando naquele momento e não é guardada.- Um teste estrutural,
tests/test_no_unbounded_growth.py, exige que cada ponto que aloca dados ou faz memoização em um processo de longa duração declare seu limite.
Diário, métricas e observabilidade#
- O diário do job registra eventos tipados (
state,agent_action,validation,review,pr,note,warning,error). O destino padrão é um arquivo,<issue>-events.jsonlno diretório de logs do projeto;BEEVERFORGE_EVENT_SINKseleciona outro. - As métricas registram custo e esforço por invocação do agente e por job. Os destinos incluídos são
null,sqliteememory. Sem configuração, o destino énulle nada é guardado, a menos queBEEVERFORGE_METRICS_TABLEnomeie uma tabela, o que selecionadynamodb, um tipo fornecido por um complemento. O painel as serve emGET /api/metrics. - A saúde tem rotas próprias, e a interface web do Temporal mostra o histórico de cada workflow.
- O núcleo não tem exportador de Prometheus nem de OpenTelemetry.
API do painel#
O painel é uma aplicação FastAPI. As leituras tipadas são servidas em /api/v1/, e seus caminhos sem versão continuam existindo, marcados como obsoletos. A descrição OpenAPI está versionada em docs/reference/openapi.json; um teste verifica que o arquivo versionado é igual ao que a aplicação gera e mostra o comando que o regenera, python -m beeverforge.api.openapi.
Fontes#
beeverforge/runtime/temporal/__init__.pybeeverforge/runtime/temporal/worker.pybeeverforge/runtime/temporal/workflow.pybeeverforge/runtime/temporal/activities.pybeeverforge/runtime/temporal/schedule.pybeeverforge/runtime/temporal/connection.pybeeverforge/adapters/sandbox/registry.pybeeverforge/lifecycle/card.pybeeverforge/util/bounded.pybeeverforge/util/single_flight.pybeeverforge/observability/events.pybeeverforge/observability/registry.pybeeverforge/api/openapi.pydocs/reference/openapi.jsontests/test_the_merge_gate_is_heard_on_every_path.pytests/test_the_panel_holds_one_engine_client.pytests/test_no_unbounded_growth.pytests/test_the_api_is_versioned.py- ADR-0009