Compositor de agentes casero y minimalista
Gradualmente se está extendiendo entre los desarrolladores el concepto de orquestar agentes. Claude tiene previsto, o ya puedes usar (dependiendo de cuando estés leyendo mi artículo), la posibilidad de lanzar varios agentes autónomos que trabajen de forma paralela y coordinada. Esto es estupendo, ¿a quién no le gusta trabajar más rápido? Sin embargo, esta funcionalidad, te la puedes construir tú mismo. Por supuesto, si no te importa consumir muchos tokens de golpe.
Los elementos que uso son sencillos:
- Un archivo
TODO.orgo similar: para definir las tareas, su estado y dependencias. - Un script de ejecución: para lanzar a los agentes con el mismo prompt y limitar el número de agentes en paralelo.
- Un repositorio Git local o remoto: para que los agentes puedan sincronizarse entre el código.
Si estuviéramos en el contexto del framework Scrum:
- Project owner: es el
TODO.org, ya que define las tareas, su descripción, su estado y sus dependencias. - Developers: son los agentes autónomos o el script de ejecución.
- Scrum Master: es el repositorio Git, ya que se ocupa de que todos los agentes respeten las reglas y tiempos.
TODO.org: el centro de coordinación
Lo potente de usar Org Mode es que podemos tener tareas en texto plano con propiedades y estados. Además es legible tanto para humanos como para máquinas.
* TODO Tarea 1
:PROPERTIES:
:ID: tarea-1
:END:
Descripción de la tarea 1.
* TODO Tarea 2
:PROPERTIES:
:ID: tarea-2
:BLOCKER: tarea-1
:END:
Descripción de la tarea 2, que depende de la tarea 1.
Cada tarea irá una debajo de la anterior, con un título, una ID y una descripción detallada. En caso que una tarea dependa de otra, añadiremos una propiedad :BLOCKER: con la ID de la tarea bloqueante. El estado de cada tarea se indicará con el prefijo TODO, IN-PROGRESS o DONE. De esta forma, los agentes podrán leer el archivo, identificar qué tareas están disponibles para ejecutar (aquellas en estado TODO sin bloqueos pendientes) y marcar su progreso.
Aunque claro, puedes usar cualquier formato o plataforma (Jira, Trello, etc). El único requisito es que los agentes puedan interactuar con él de forma programática, para leer las tareas, marcar su estado.
Script de ejecución y prompt de los agentes
De nada nos vale definir tareas si no tenemos agentes que las ejecuten.
Creamos un script run_agents.sh con el siguiente contenido:
#!/bin/bash
MAX_JOBS=16
AGENT_NUM=0
while true; do
while (( $(jobs -rp | wc -l) >= MAX_JOBS )); do
sleep 1
done
git pull --rebase origin main 2>/dev/null
if ! grep -q "^\* TODO" TODO.org; then
wait
break
fi
COMMIT=$(git rev-parse --short=6 HEAD)
AGENT_NUM=$((AGENT_NUM + 1))
LOGFILE="agent_logs/agent_${COMMIT}_${AGENT_NUM}.log"
claude --dangerously-skip-permissions -p "Tu número de agente es: $AGENT_NUM. $(cat AGENT_PROMPT.md)" &> "$LOGFILE" &
done
Como puedes comprobar hay una variable MAX_JOBS que limita el número de agentes que se ejecutan en paralelo. Antes de lanzar cada uno, el script hace un git pull para leer el estado actualizado de TODO.org: si ya no quedan tareas TODO, espera a que terminen los agentes en curso con wait y sale. Cada agente ejecutará el mismo prompt, que se encuentra en AGENT_PROMPT.md, y guardará su salida en un archivo de log con el hash del commit actual y su número de agente ($AGENT_NUM).
He usado claude --dangerously-skip-permissions -p para lanzar a los agentes. El flag --dangerously-skip-permissions es necesario para que cada agente pueda operar de forma autónoma sin detenerse a pedir confirmación en cada acción. Puedes usar el modelo o cliente que prefieras, pero asegúrate de que los agentes puedan ejecutar comandos sin intervención humana.
El modo -p es single-shot: cuando Claude termina de generar su respuesta, el proceso muere solo. Aun así, es importante decirle al agente explícitamente que ejecute exit 0 via bash cuando no queden tareas TODO. Esto evita que el agente se quede en un bucle de espera indefinida cuando todas las tareas están en IN-PROGRESS o DONE.
El AGENT_PROMPT.md es un documento, o Skill, que define el comportamiento de los agentes, las reglas de colaboración, cómo elegir tareas, cómo bloquear tareas, etc. Un ejemplo podría ser el siguiente:
# Prompt de Sincronización para Agentes Autónomos
## Contexto
Eres un agente autónomo que trabaja en colaboración con otros agentes sobre un mismo repositorio Git. La coordinación se realiza a través de un archivo `TODO.org` compartido que actúa como fuente única de verdad para el estado de las tareas.
## Entorno de trabajo
Cada agente trabaja en su propia instancia Docker aislada. Al inicio, levanta tu contenedor a partir del `compose.yaml` del proyecto:
```bash
AGENT_PORT=$((8080 + TU_NÚMERO_DE_AGENTE)) docker compose -p agent-TU_NÚMERO_DE_AGENTE up -d
```
Nunca compartes instancia con otro agente. Cada uno tiene su propio entorno de ejecución independiente. El repositorio Git remoto es el único punto de sincronización entre agentes: toda la coordinación se hace exclusivamente vía `git pull` / `git push`.
## Formato del archivo TODO.org
El archivo `TODO.org` contiene tareas con el siguiente formato:
```org
* TODO Nombre de la tarea
:PROPERTIES:
:ID: identificador-de-la-tarea
:END:
Descripción detallada de lo que hay que hacer.
* TODO Otra tarea con dependencia
:PROPERTIES:
:BLOCKER: identificador-de-la-tarea-que-bloquea
:END:
Esta tarea solo puede comenzarse cuando la tarea bloqueante esté en DONE.
```
### Estados posibles de una tarea
| Estado | Significado |
|---------------|--------------------------------------------------|
| `TODO` | Pendiente, disponible para ser tomada por un agente |
| `IN-PROGRESS` | Un agente la está ejecutando actualmente |
| `DONE` | Completada y commiteada |
### Propiedad BLOCKER
Si una tarea tiene la propiedad `:BLOCKER:`, su valor es el `:ID:` de otra tarea. **No puedes iniciar una tarea bloqueada hasta que la tarea referenciada esté en estado `DONE`.**
## Protocolo de trabajo
Sigue este ciclo estrictamente para cada tarea:
### 1. Sincronizar antes de elegir tarea
```bash
git pull --rebase origin main
```
Lee el archivo `TODO.org` actualizado.
### 2. Elegir una tarea disponible
Una tarea es elegible si y solo si:
- Su estado es `TODO` (no `IN-PROGRESS` ni `DONE`).
- No tiene propiedad `:BLOCKER:`, **o** la tarea referenciada en `:BLOCKER:` ya está en estado `DONE`.
Si no hay ninguna tarea en estado `TODO` (todas están en `IN-PROGRESS` o `DONE`), ejecuta `exit 0` para terminar el proceso.
Si hay tareas `TODO` pero todas tienen bloqueadores pendientes, espera un momento y vuelve al paso 1.
### 3. Marcar la tarea como IN-PROGRESS y hacer commit
Cambia el estado de la tarea en `TODO.org`:
```diff
- * TODO Nombre de la tarea
+ * IN-PROGRESS Nombre de la tarea
```
Haz commit y push inmediatamente:
```bash
git add TODO.org
git commit -m "start: Nombre de la tarea"
git push origin main
```
> **Si el push falla por conflicto**, significa que otro agente modificó `TODO.org` al mismo tiempo. Haz `git pull --rebase`, revisa el estado del archivo, y si la tarea que querías ya fue tomada, vuelve al paso 1.
### 4. Ejecutar la tarea
Realiza el trabajo descrito en la tarea. Trabaja únicamente en lo que la tarea indica, sin modificar archivos fuera de su alcance.
### 5. Marcar como DONE, commit y push
Una vez completada la tarea:
```diff
- * IN-PROGRESS Nombre de la tarea
+ * DONE Nombre de la tarea
```
Haz commit con todos los archivos modificados por la tarea junto con el `TODO.org` actualizado:
```bash
git add -A
git commit -m "done: Nombre de la tarea"
git push origin main
```
> Si el push falla, haz `git pull --rebase`, resuelve conflictos si los hay, y reintenta el push.
### 6. Volver al paso 1
Sincroniza y busca la siguiente tarea disponible.
Si no hay tareas disponibles, ejecuta `exit 0` para terminar el proceso:
```bash
exit 0
```
Dado que los agentes se lanzan con `claude -p`, el proceso termina en cuanto Claude deja de emitir respuesta. Pero hacer `exit 0` explícito vía bash garantiza que el proceso muere incluso si el agente estuviera en un bucle de espera.
## Reglas críticas
- **NUNCA** inicies una tarea que esté en `IN-PROGRESS` — otro agente la está ejecutando.
- **NUNCA** inicies una tarea cuyo `BLOCKER` no esté en `DONE`.
- **SIEMPRE** haz `git pull --rebase` antes de leer `TODO.org` para elegir tarea.
- **SIEMPRE** haz push inmediatamente después de cambiar un estado en `TODO.org`.
- **SIEMPRE** resuelve conflictos de merge conservando el estado más avanzado de cada tarea (p.ej., si tú tienes `IN-PROGRESS` y el remoto tiene `DONE`, conserva `DONE`).
- Trabaja **una sola tarea a la vez**. No tomes múltiples tareas simultáneamente.
- Los mensajes de commit deben seguir el formato: `start: Nombre` / `done: Nombre`.
## Resolución de conflictos en TODO.org
Si al hacer rebase encuentras conflictos en `TODO.org`, aplica esta lógica:
| Tu estado | Estado remoto | Estado resultante |
|---------------|---------------|-------------------|
| `TODO` | `IN-PROGRESS` | `IN-PROGRESS` |
| `TODO` | `DONE` | `DONE` |
| `IN-PROGRESS` | `DONE` | `DONE` |
| `IN-PROGRESS` | `IN-PROGRESS` | **Conflicto real** — conserva el remoto y vuelve al paso 1 |
El principio es: **el estado más avanzado siempre gana**.
Git para sincronización
Como decía al inicio, necesitamos un mecanismo de sincronización de código entre los agentes. Para esto, el repositorio Git es perfecto. Y si trabaja en local, ¡mucho mejor!
Inicializa el repositorio, añade los archivos y haz el primer commit:
git init .
git add .
git commit -m "initial setup"
El commit inicial es imprescindible: sin él no existe la rama main y los agentes no podrán hacer git pull ni git push.
El script usa git pull --rebase origin main, así que necesita un remote llamado origin. La forma más sencilla en local es crear un repositorio bare y apuntarlo como origin:
git init --bare ../mars-agents-remote.git
git remote add origin ../mars-agents-remote.git
git push -u origin main
Si prefieres usar un repositorio remoto real (GitHub, GitLab, etc.), simplemente añade su URL como origin y haz el push inicial.
Las IAs son muy buenas resolviendo conflictos. Si dos agentes intentan tomar la misma tarea, uno de ellos ganará el push y el otro tendrá que hacer pull, resolver el conflicto y volver a intentar. Un mecanismo de sincronización natural y robusto, sin necesidad de locks, semáforos o sistemas de colas.
Y con esto ya estaría todo listo.
Ejecutar y resultados
Ya solo queda ejecutar el script:
bash run_agents.sh
Cada agente irá leyendo el TODO.org, eligiendo tareas, ejecutándolas y actualizando su estado. Si quieres puedes tenerlo abierto, con algún editor que te permita ver los cambios en tiempo real, para ver el progreso.
Ejemplo real
Vamos a coordinar la creación de una landing page para un viaje a Marte.
En tu proyecto tendrás la siguiente estructura:
tu-proyecto/
├── AGENT_PROMPT.md
├── TODO.org
├── run_agents.sh
└── src/
La carpeta src/ es donde los agentes irán creando los archivos de la landing page.
Definamos las tareas en TODO.org:
* TODO Define la estructura de la landing page
:PROPERTIES:
:ID: define-structure
:END:
Crea el archivo `src/index.html` con la siguiente estructura base para una landing page de reserva de plazas en un viaje a Marte:
- Un `<header>` con el título "Reserva tu plaza a Marte" y un subtítulo. Incluye un botón de llamada a la acción (CTA) con el texto "Reservar plaza".
- Un `<section id="stats-section">` vacío donde irá la gráfica de estadísticas de reservas.
- Un `<section id="mission-section">` vacío donde irá la descripción de la misión.
- Un `<section id="cta-section">` vacío donde irá el formulario de reserva.
- Un `<footer>` con el texto "Mars One Reservations · 2045".
El HTML debe ser semántico y válido. No añadas estilos ni scripts aún.
* TODO Añade Bulma CSS
:PROPERTIES:
:ID: add-bulma-css
:BLOCKER: define-structure
:END:
Lee la documentación completa de Bulma CSS en https://bulma.io/documentation/.
Añade Bulma CSS a `src/index.html` mediante CDN. Aplica clases de Bulma para:
- El `<header>` use el componente `hero` en modo `is-fullheight` con fondo oscuro (negro o azul muy oscuro) para evocar el espacio. El botón CTA use la clase `button is-danger is-large`.
- Las secciones usen `container` y `section`.
- El `<footer>` use el componente `footer` de Bulma.
La página debe verse profesional y moderna en escritorio y móvil.
* TODO Añade una gráfica con Chart.js
:PROPERTIES:
:ID: add-chart
:BLOCKER: define-structure
:END:
Añade Chart.js a `src/index.html` mediante CDN. Dentro de `<section id="stats-section">` crea un `<canvas>` e inicializa una gráfica de barras con datos de reservas por ventana de lanzamiento:
- Etiquetas: 2041, 2042, 2043, 2044, 2045, 2046.
- Dataset "Plazas disponibles": 500, 500, 500, 500, 500, 500.
- Dataset "Reservas confirmadas": 12, 87, 203, 341, 489, 71.
La gráfica debe tener título "Reservas por ventana de lanzamiento", leyenda y colores diferenciados. Usa tonos rojos y naranjas para evocar el planeta.
* TODO Escribe los textos
:PROPERTIES:
:ID: write-copy
:BLOCKER: define-structure
:END:
Redacta todos los textos de la landing page y escríbelos directamente en `src/index.html`:
- Subtítulo del header: una frase impactante y breve sobre ser el primer humano en pisar Marte.
- Sección `#mission-section`: un título "La misión", tres párrafos sobre el proyecto (duración del viaje, condiciones, qué incluye la plaza), y una lista de tres beneficios destacados con iconos de emoji.
- Sección `#cta-section`: un título "¿Estás listo?", un párrafo de urgencia indicando que las plazas son limitadas, y un formulario con campos nombre, email y ventana de lanzamiento preferida (select con los años 2041-2046) y botón de envío.
El tono debe ser épico, aspiracional y con un punto de humor.
* TODO Escribe el contenido para la sección de estadísticas
:PROPERTIES:
:ID: write-content
:BLOCKER: define-structure write-copy add-chart
:END:
Redacta el contenido textual para `<section id="stats-section">` en `src/index.html`, encima del canvas de la gráfica. Debe incluir:
- Un título "El ritmo de la historia".
- Dos párrafos que contextualicen los datos: qué significan las ventanas de lanzamiento, por qué las reservas crecen cada año y qué implica que la ventana 2045 esté casi completa.
- Una estadística destacada en grande: "489 valientes ya tienen su plaza para 2045".
El tono debe combinar rigor informativo con emoción.
* TODO Revisa y pule la landing page
:PROPERTIES:
:ID: final-polish
:BLOCKER: add-bulma-css add-chart write-copy write-content
:END:
Revisa `src/index.html` en su conjunto y aplica los siguientes ajustes:
- Comprueba que la narrativa fluye: header impactante, estadísticas que generan urgencia, descripción de la misión y formulario de cierre.
- Asegúrate de que la gráfica encaja visualmente dentro del layout de Bulma.
- Verifica que la página es responsive en pantallas pequeñas.
- Comprueba que el formulario de reserva tiene el atributo `action="#"` y no genera errores en consola.
- Corrige cualquier inconsistencia visual o de maquetación.
En resumen:
- Definimos la estructura HTML.
- Añadimos estilos con Bulma CSS.
- Creamos una gráfica con Chart.js.
- Escribimos los textos globales de la página.
- Escribimos el contenido específico para la sección de estadísticas.
- Revisamos y pulimos el resultado final.
Algunas tareas no se pueden ejecutar hasta que otras estén completas.
Lo ejecutamos con bash run_agents.sh y esperamos.
Conclusión
El esfuerzo de redactar las tareas de forma clara, con el orden del trabajo bien definido, no te lo quita nadie. Asumes el rol de project owner definiendo el backlog, las tareas y las prioridades. Y ello tampoco te garantiza un resultado perfecto. Sin embargo te obliga a pensar, a estructurar y seleccionar un stack tecnológico con cierto criterio. Algo que nunca deberías automatizar.
Si quieres profundizar más al respecto, te recomiendo leer el artículo Building C compiler.
- TODO.org: el centro de coordinación
- Script de ejecución y prompt de los agentes
- Git para sincronización
- Ejecutar y resultados
- Ejemplo real
- Conclusión
Este trabajo está bajo una licencia Attribution-NonCommercial-NoDerivatives 4.0 International.
Apóyame en Ko-fi
Comentarios
Todavía no hay ningún comentario.