Arquitectura del núcleo de BeeverForge
Ejecución y durabilidad
El motor es Temporal. Cada job es un workflow, cada efecto secundario es una actividad y cada espera, desde una consulta a la CI hasta una aprobación de producción, es un temporizador o una condición durable. Por eso, un reinicio del worker no pierde el job: sus esperas continúan donde estaban, y un job cuyo worker se pierde durante la pasada del agente queda en espera para que una persona lo reanude u omita, porque esa pasada no se reintenta.
Workflows, actividades y la cola de tareas#
Un único proceso worker registra todos los workflows y todas las actividades en una sola cola de tareas, beeverforge-jobs. Los workflows principales son:
JobWorkflow- Uno por ticket. El dimensionamiento, la ejecución en la caja, la vigilancia hasta el merge, la promoción y cada espera ocurren dentro de él.
PollWorkflow- Una exploración del tablero por ciclo, que inicia jobs en los cupos libres.
DeployWatchWorkflow- Un hijo independiente que observa el despliegue después de un merge.
PreviewWorkflow- Una vista previa en ejecución de un cambio sin merge, cuando hay un entorno de vista previa configurado.
ConversationWorkflow- Una conversación con la fábrica, admitida y avanzada mediante señales.
Otros workflows cubren la vigilancia del tech-lead, la actualización del conocimiento, el rol de producto y los barridos que se listan abajo. El trabajo pesado, como ejecutar el job en su caja, es la actividad run_job.
Programaciones#
El trabajo recurrente es una programación de Temporal, que el worker concilia al arrancar.
| Programación | Intervalo | Propósito |
|---|---|---|
beeverforge-poller | 3 minutos | Explorar los tableros; un ciclo que se solaparía se omite. |
beeverforge-techlead-watch-* | 1 hora | La ronda del tech-lead sobre el trabajo en espera. |
beeverforge-card-questions-* | 1 hora | Leer las respuestas a las preguntas hechas en las tarjetas. |
beeverforge-okf-refresh-* | 6 horas | Actualizar el mapa de conocimiento respecto a la rama base. |
beeverforge-product-sweep-* | 1 semana | El barrido del rol de producto en busca de trabajo que no puede terminar. |
beeverforge-preview-reaper | 10 minutos | Terminar las vistas previas cuyo pull request ya recibió el merge. |
El motor conserva el historial de los workflows durante 30 días de forma predeterminada (BEEVERFORGE_ENGINE_RETENTION_DAYS).
Señales y temporizadores durables#
La respuesta de una persona llega a un job en curso como una señal: approve_prod, not_yet, human_merge_gate, act_on_impediment y advise_decision. Las esperas son workflow.sleep y workflow.wait_condition con un tiempo límite, de modo que una espera de minutos o días no ocupa ningún worker y sobrevive a un reinicio. Por ejemplo, la consulta a la CI (2 minutos, luego 15), la consulta del despliegue (1 minuto), la aprobación de producción (3 días), la vigilancia del merge (14 días) y un impedimento en espera (3 días).
Efectos secundarios idempotentes#
Una actividad puede volver a ejecutarse tras un fallo, así que cada escritura externa está hecha para poder repetirse sin riesgo o no se reintenta:
- Los pull requests son buscar y luego actualizar: se reutiliza el pull request abierto de la rama.
- Las etiquetas de versión son buscar o crear.
- Las transiciones y los comentarios de las tarjetas llevan el id del evento de la actividad que los causó; la puerta de la tarjeta devuelve la transición ya registrada para ese id en lugar de escribirla de nuevo.
- Los merges se vuelven a leer en la plataforma Git después de la llamada de merge, en lugar de confiar en el resultado de la llamada.
- El propio job solo se reintenta cuando su tipo de caja declara que un reintento se vuelve a conectar al job en curso. Las cajas
containeryworktreeno lo declaran: ejecutan su actividad de job una sola vez, porque un reintento sería una segunda pasada del agente que repetiría cada comentario. La descripciónfargatesí lo declara; su ejecutor lo proporciona un complemento.
Heartbeats#
Las actividades largas emiten heartbeats. Las actividades de job se ejecutan con un tiempo límite de heartbeat de 120 segundos y laten aproximadamente cada 30 segundos; el trabajo que continúa después del último latido está acotado para caber en esa ventana. Así, un worker que muere a mitad de una actividad se detecta dentro del tiempo límite y no al final de la duración completa de la actividad.
Seguridad en la reejecución#
Temporal reproduce el historial de un workflow contra el código actual, así que un cambio en la lógica del workflow no debe cambiar los comandos que un workflow en curso ya registró.
- Cada cambio de ese tipo está protegido por
workflow.patched()con un id que nombra el comportamiento, por ejemplosigned-gatesoa-self-merge-is-read-back. Los historiales antiguos toman el camino antiguo; las ejecuciones nuevas toman el nuevo. - Las pruebas de Replayer ejecutan un workflow contra un servidor Temporal efímero, obtienen el historial registrado y lo reejecutan con
Replayercontra el código actual. Algunas ejecutan primero una copia del workflow anterior al parche. Los historiales se registran dentro de las pruebas; ninguno está guardado como archivo en el repositorio. - El código que debe quedar fuera del cuerpo del workflow se mantiene en actividades. La comprobación de la certificación de la caja al recoger trabajo está en la actividad de exploración del tablero precisamente por eso.
Un cliente del motor por proceso#
Un proceso mantiene un cliente del motor. runtime/temporal/connection.py conserva un único cliente identificado por la configuración de la conexión; los primeros llamadores concurrentes comparten un único intento de conexión, y la reconexión espera como máximo 60 segundos. Pruebas de guarda comprueban que las primeras llamadas concurrentes, y las aprobaciones repetidas, abren un solo cliente.
Estructuras acotadas#
El worker y el panel son procesos de larga vida, así que las cachés en memoria están acotadas por construcción.
util/bounded.pyproporcionaBoundedDict: seguro entre hilos, con un tamaño máximo obligatorio, que expulsa primero la entrada más antigua y cuenta lo que expulsó. Deliberadamente no es una subclase dedict, para que nada pueda hacerlo crecer saltándose su límite.util/single_flight.pypermite que los lectores que llegan a la vez compartan una lectura. Cada uno queda protegido de la cancelación de otro llamador, hay como máximo una lectura en curso, y un fallo se comparte con quienes esperan en ese momento y no se guarda.- Una prueba estructural,
tests/test_no_unbounded_growth.py, exige que cada lugar que reserva o memoiza datos en un proceso de larga vida declare su límite.
Diario, métricas y observabilidad#
- El diario del job registra eventos tipados (
state,agent_action,validation,review,pr,note,warning,error). El destino predeterminado es un archivo,<issue>-events.jsonlen el directorio de registros del proyecto;BEEVERFORGE_EVENT_SINKselecciona otro. - Las métricas registran el costo y el esfuerzo por invocación del agente y por job. Los destinos incluidos son
null,sqliteymemory. Sin configuración el destino esnully no se guarda nada, salvo queBEEVERFORGE_METRICS_TABLEnombre una tabla, lo que seleccionadynamodb, un tipo que proporciona un complemento. El panel las sirve enGET /api/metrics. - El estado de salud tiene sus propias rutas, y la interfaz web de Temporal muestra el historial de cada workflow.
- El núcleo no tiene exportador de Prometheus ni de OpenTelemetry.
API del panel#
El panel es una aplicación FastAPI. Las lecturas tipadas se sirven bajo /api/v1/, y sus rutas sin versión siguen existiendo, marcadas como obsoletas. La descripción OpenAPI está versionada en docs/reference/openapi.json; una prueba comprueba que el archivo versionado es igual al que genera la aplicación e indica el comando que lo regenera, python -m beeverforge.api.openapi.
Fuentes#
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