Un worker reserva la tarea inventory y desaparece. Otro worker no puede ejecutar a la vez esa misma unidad, pero tampoco debería esperar para siempre. Si la única copia del estado vive en la conversación del supervisor, el flujo se ha detenido con el primer proceso.
En este tutorial montarás una orquestación multiagente en TypeScript capaz de sobrevivir a ese fallo. PostgreSQL guarda tareas, dependencias, intentos, leases y eventos. Dos workers compiten por el trabajo sin duplicarlo, un fallo transitorio provoca un reintento y la revisión solo empieza cuando terminan el inventario y la prueba.
Resultado del tutorial
- Un flujo ejecutable con
inventory,testyreview.- Estado duradero fuera del contexto de los agentes.
- Reserva atómica de tareas mediante leases.
- Recuperación de un worker interrumpido y reintentos limitados.
- Una interfaz para sustituir el agente simulado por Codex o Claude Code.

¿Cuándo merece la pena usar una arquitectura multiagente?
Usa varios agentes cuando el trabajo tenga fronteras independientes, requiera herramientas distintas o quieras separar permisos. La documentación "AI agent orchestration patterns", mantenida por Microsoft en 2026, recomienda emplear la menor complejidad capaz de resolver el caso. Un agente con herramientas sigue siendo la opción más fácil de depurar.
El fan-out empieza a compensar cuando una tarea admite investigación paralela o una separación real del riesgo. Un worker puede cartografiar el repositorio sin escribir. Otro puede ejecutar pruebas en un worktree aislado. Un tercero revisa los artefactos sin recibir permiso para modificar el parche.
No multipliques los agentes cuando todos dependan del mismo archivo, deban seguir una secuencia corta o carguen el mismo contexto. La documentación "Subagents", de OpenAI, recomienda el paralelismo sobre todo para exploración, pruebas, triaje y síntesis. También advierte de que la escritura paralela aumenta los conflictos y el coste de coordinación.
| Situación | Elección inicial |
|---|---|
| Una tarea corta con herramientas conocidas | Un agente |
| Lectura paralela con síntesis central | Subagentes |
| Módulos independientes que necesitan comunicarse | Equipo de agentes |
| Flujo largo con reanudación y dependencias | Orquestador duradero |
Este tutorial amplía el fan-out de subagentes para migraciones de código. Allí, el objetivo es dividir un cambio. Aquí nos ocupamos del mecanismo que reserva, reanuda y verifica cada unidad.
Arquitectura del orquestador
El supervisor permanecerá fuera de los agentes. Decide qué tareas están listas, recupera los leases vencidos y cierra el flujo. Los workers reciben una tarea tipada, llaman a un AgentAdapter y devuelven un artefacto breve. PostgreSQL es la fuente de verdad para el estado y las dependencias.
La documentación de Microsoft aconseja persistir el progreso y los resultados intermedios en un almacenamiento duradero cuando la orquestación atraviesa varias interacciones o ejecuciones largas. El mismo documento trata el fallo de un nodo, la pérdida de mensajes y el error en cascada como problemas clásicos de los sistemas distribuidos. Un prompt no sustituye a un timeout, un reintento ni una transacción.
El flujo seguirá este orden:
inventoryidentifica el alcance y produce pruebas.testespera ainventory, ejecuta una comprobación y puede volver a intentarlo.reviewespera atesty trata los resultados anteriores como afirmaciones verificables.- El supervisor termina cuando todas las tareas alcanzan un estado terminal.
Las salidas no serán transcripciones completas. Contendrán summary y evidence. Cuando un flujo debe atravesar más sesiones y subagentes sin reenviar registros en bruto, utilizo RemoteCode para ampliar el alcance de Codex y Claude Code con menos repetición de contexto como herramienta del propio autor. La referencia encaja aquí porque el ahorro de tokens depende del contrato de handoff, no de esconder el estado dentro del prompt.
Requisitos previos
Necesitas Docker con Compose, Git y Node.js 24 LTS. La página oficial "Node.js Releases" incluye la rama 24 como LTS en julio de 2026. El ejemplo usa PostgreSQL 18, TypeScript 7.0.2, pg 8.22.0 y tsx 4.23.1.
Comprueba el entorno:
node --version
npm --version
docker version
docker compose version
El código funciona en Linux, macOS y Windows con Docker Desktop. El puerto 54339 debe estar libre. Si ya está ocupado, cambia el lado izquierdo del mapeo en compose.yaml y ajusta DATABASE_URL.
Prepara el proyecto TypeScript y PostgreSQL
Crea una carpeta vacía e instala las dependencias fijadas. Las versiones siguientes estaban publicadas en npm el 25/07/2026. Fijarlas permite reproducir el tutorial, mientras que package-lock.json registra el árbol resuelto.
mkdir coding-agent-orchestrator
cd coding-agent-orchestrator
npm init -y
npm install pg@8.22.0
npm install --save-dev typescript@7.0.2 tsx@4.23.1 \
@types/node@24.13.3 @types/pg@8.20.0
mkdir src
Sustituye package.json por:
{
"name": "coding-agent-orchestrator",
"private": true,
"type": "module",
"scripts": {
"demo": "tsx src/index.ts",
"check": "tsc --noEmit"
},
"dependencies": {
"pg": "8.22.0"
},
"devDependencies": {
"@types/node": "24.13.3",
"@types/pg": "8.20.0",
"tsx": "4.23.1",
"typescript": "7.0.2"
}
}
Crea tsconfig.json:
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
Crea compose.yaml:
services:
postgres:
image: postgres:18-alpine
environment:
POSTGRES_USER: agents
POSTGRES_PASSWORD: agents
POSTGRES_DB: agents
ports:
- "54339:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U agents -d agents"]
interval: 2s
timeout: 2s
retries: 15
Inicia la base de datos y espera al healthcheck:
docker compose up -d --wait
El comando debe terminar con el contenedor en estado saludable. Si la versión de Compose de tu equipo no acepta --wait, ejecuta docker compose up -d y comprueba docker compose ps antes de continuar.
Persiste tareas, dependencias y eventos
El estado mínimo debe indicar quién reservó la tarea, hasta cuándo es válido el lease, cuántos intentos se han realizado y qué prueba se obtuvo. La tabla task_dependencies impide que la revisión empiece antes de la prueba. task_events conserva el rastro de transiciones sin convertir la transcripción en una base de datos.
Crea src/index.ts con los imports, tipos, conexión y bootstrap:
import { Pool, type PoolClient } from "pg";
type TaskStatus = "ready" | "running" | "succeeded" | "failed";
type TaskKind = "inventory" | "test" | "review";
type Task = {
id: string;
kind: TaskKind;
input: Record<string, unknown>;
status: TaskStatus;
attempts: number;
max_attempts: number;
lease_owner: string | null;
};
type AgentOutput = { summary: string; evidence: string[] };
type AgentAdapter = (task: Task) => Promise<AgentOutput>;
const pool = new Pool({
connectionString:
process.env.DATABASE_URL ??
"postgres://agents:agents@127.0.0.1:54339/agents",
});
const sleep = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function inTransaction<T>(
operation: (client: PoolClient) => Promise<T>,
): Promise<T> {
const client = await pool.connect();
try {
await client.query("BEGIN");
const result = await operation(client);
await client.query("COMMIT");
return result;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
async function bootstrap() {
await pool.query(`
CREATE TABLE IF NOT EXISTS tasks (
id text PRIMARY KEY,
kind text NOT NULL CHECK (kind IN ('inventory', 'test', 'review')),
input jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL CHECK (
status IN ('ready', 'running', 'succeeded', 'failed')
),
attempts integer NOT NULL DEFAULT 0,
max_attempts integer NOT NULL DEFAULT 3,
available_at timestamptz NOT NULL DEFAULT now(),
lease_owner text,
lease_until timestamptz,
output jsonb,
last_error text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS task_dependencies (
task_id text NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
depends_on text NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
PRIMARY KEY (task_id, depends_on)
);
CREATE TABLE IF NOT EXISTS task_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
task_id text NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
event text NOT NULL,
worker text,
detail jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
`);
}
async function seedWorkflow() {
await pool.query(
"TRUNCATE task_events, task_dependencies, tasks RESTART IDENTITY CASCADE",
);
await pool.query(`
INSERT INTO tasks (id, kind, input, status)
VALUES
('inventory', 'inventory', '{"scope":"src"}', 'ready'),
('test', 'test', '{"command":"npm test"}', 'ready'),
('review', 'review', '{"policy":"require-evidence"}', 'ready');
INSERT INTO task_dependencies (task_id, depends_on)
VALUES ('test', 'inventory'), ('review', 'test');
`);
}
async function appendEvent(
client: PoolClient | Pool,
taskId: string,
event: string,
worker: string | null,
detail: Record<string, unknown> = {},
) {
await client.query(
`INSERT INTO task_events (task_id, event, worker, detail)
VALUES ($1, $2, $3, $4)`,
[taskId, event, worker, JSON.stringify(detail)],
);
}
seedWorkflow solo borra las tablas de la base de datos de demostración. No copies ese TRUNCATE en un servicio compartido. En producción, cada ejecución debe recibir un workflow_id y la creación de tareas ha de ser idempotente.
Reserva trabajo con una transacción y un lease
Dos workers pueden consultar la cola al mismo tiempo. La reserva debe producirse en la misma transacción que selecciona la tarea. La documentación "SELECT" de PostgreSQL 18 explica que SKIP LOCKED evita esperar por filas ya reservadas y resulta adecuado para tablas con varios consumidores, aunque no ofrece una vista general coherente de los datos.
Añade la función siguiente al mismo archivo:
async function claimTask(
worker: string,
leaseMilliseconds = 2_000,
): Promise<Task | null> {
return inTransaction(async (client) => {
const result = await client.query<Task>(
`
SELECT t.id, t.kind, t.input, t.status, t.attempts,
t.max_attempts, t.lease_owner
FROM tasks t
WHERE t.status = 'ready'
AND t.available_at <= now()
AND NOT EXISTS (
SELECT 1
FROM task_dependencies d
JOIN tasks prerequisite ON prerequisite.id = d.depends_on
WHERE d.task_id = t.id
AND prerequisite.status <> 'succeeded'
)
ORDER BY t.created_at, t.id
FOR UPDATE SKIP LOCKED
LIMIT 1
`,
);
const task = result.rows[0];
if (!task) return null;
const claimed = await client.query<Task>(
`
UPDATE tasks
SET status = 'running',
attempts = attempts + 1,
lease_owner = $2,
lease_until = now() + ($3 * interval '1 millisecond'),
updated_at = now()
WHERE id = $1
RETURNING id, kind, input, status, attempts,
max_attempts, lease_owner
`,
[task.id, worker, leaseMilliseconds],
);
await appendEvent(client, task.id, "claimed", worker, {
leaseMilliseconds,
});
return claimed.rows[0] ?? null;
});
}
El NOT EXISTS libera una tarea solo cuando todas sus dependencias están en succeeded. ORDER BY mantiene un orden predecible entre las candidatas. La propia documentación de PostgreSQL advierte de que LIMIT sin ordenación puede devolver subconjuntos distintos entre ejecuciones.
El lease no es un bloqueo prolongado de la base de datos. La transacción termina en cuanto queda registrada la reserva. A partir de ahí, lease_until representa el derecho temporal del worker sobre esa unidad. Si el proceso muere, otro worker podrá recuperarla.
Recupera un worker interrumpido sin duplicar la tarea
Un lease vencido vuelve a ready. El evento debe guardar el propietario anterior antes de limpiar las columnas. La transición ha de ser atómica, porque varios supervisores pueden buscar vencimientos al mismo tiempo.

Añade:
async function recoverExpiredLeases() {
return inTransaction(async (client) => {
const expired = await client.query<{
id: string;
old_owner: string | null;
}>(
`
WITH expired AS (
SELECT id, lease_owner AS old_owner
FROM tasks
WHERE status = 'running' AND lease_until < now()
FOR UPDATE
)
UPDATE tasks AS task
SET status = 'ready',
lease_owner = NULL,
lease_until = NULL,
last_error = 'lease expired before completion',
updated_at = now()
FROM expired
WHERE task.id = expired.id
RETURNING task.id, expired.old_owner
`,
);
for (const task of expired.rows) {
await appendEvent(
client,
task.id,
"lease-expired",
task.old_owner,
);
}
return expired.rowCount ?? 0;
});
}
Una tarea recuperada puede volver a ejecutarse, por lo que la operación del agente debe ser idempotente o suceder en un entorno desechable. Para trabajar con código, usa un worktree aislado, una rama propia o un parche como artefacto. No permitas que dos workers publiquen en la misma rama ni apliquen la misma migración.
El ejemplo utiliza un lease corto para que el fallo resulte visible en pocos segundos. En producción, renueva los leases mediante un heartbeat y calcula el plazo según la duración real de las tareas. Un plazo arbitrariamente largo solo convierte un fallo rápido en una espera larga.
Separa el éxito, el reintento y el fallo terminal
El worker solo completa una tarea si todavía posee el lease. Una respuesta tardía no puede sobrescribir el resultado de quien recuperó el trabajo. Los fallos transitorios vuelven a ready con un retraso. En el último intento, el estado pasa a failed.
Añade:
async function completeTask(
task: Task,
worker: string,
output: AgentOutput,
) {
const result = await pool.query(
`
UPDATE tasks
SET status = 'succeeded',
output = $3,
lease_owner = NULL,
lease_until = NULL,
updated_at = now()
WHERE id = $1 AND status = 'running' AND lease_owner = $2
`,
[task.id, worker, JSON.stringify(output)],
);
if (result.rowCount !== 1) throw new Error(`lease lost for ${task.id}`);
await appendEvent(pool, task.id, "succeeded", worker, output);
}
async function failTask(task: Task, worker: string, error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const terminal = task.attempts >= task.max_attempts;
const result = await pool.query(
`
UPDATE tasks
SET status = $3,
available_at = CASE
WHEN $3 = 'ready'
THEN now() + ($4 * interval '1 millisecond')
ELSE available_at
END,
lease_owner = NULL,
lease_until = NULL,
last_error = $5,
updated_at = now()
WHERE id = $1 AND status = 'running' AND lease_owner = $2
`,
[
task.id,
worker,
terminal ? "failed" : "ready",
250 * task.attempts,
message,
],
);
if (result.rowCount !== 1) throw new Error(`lease lost for ${task.id}`);
await appendEvent(
pool,
task.id,
terminal ? "failed" : "retry",
worker,
{ message },
);
}
El retraso aumenta con attempts, pero el ejemplo no añade jitter para que la salida sea predecible. En un servicio con muchos workers, incluye jitter y fija un límite máximo. Clasifica también los errores: un prompt no válido o un permiso denegado no mejoran con un reintento; una indisponibilidad de red sí puede hacerlo.
Coloca Codex o Claude Code detrás del mismo adaptador
El orquestador no debe saber qué modelo atiende una tarea. AgentAdapter recibe un contrato y devuelve pruebas. El adaptador simulado permite comprobar la cola, los fallos y las dependencias sin efectuar una llamada a una API. Después puedes sustituirlo por Codex, Claude Code o un servicio propio.
Añade el adaptador determinista:
const mockAgent: AgentAdapter = async (task) => {
await sleep(120);
if (task.kind === "test" && task.attempts === 1) {
throw new Error("transient test runner failure");
}
const evidence: Record<TaskKind, string[]> = {
inventory: ["src/index.ts", "package.json"],
test: ["npm test: passed"],
review: ["dependencies satisfied", "evidence attached"],
};
return {
summary: `${task.kind} completed`,
evidence: evidence[task.kind],
};
};
Para un adaptador real, construye el prompt a partir de task.input, ejecuta el CLI en su propio worktree y transforma el JSONL en un AgentOutput. La documentación sobre observabilidad de agentes de código en CI explica por qué la transcripción completa debe permanecer en un artefacto restringido, mientras que la cola solo recibe el resumen y las pruebas.
Mantén los permisos separados por tipo. inventory puede ser de solo lectura. test ejecuta una lista conocida de comandos. review lee el diff y los resultados, pero no publica ni edita. Si todos los adaptadores reciben acceso total, la separación en agentes se queda en una mera división de prompts.
Ejecuta los workers y verifica el flujo completo
El bucle intenta recuperar leases, reserva una tarea lista y llama al adaptador. Cuando no encuentra trabajo, comprueba si aún quedan tareas en ready o running. Dos instancias pueden ejecutar este bucle al mismo tiempo porque la reserva utiliza FOR UPDATE SKIP LOCKED.
Añade el resto del archivo:
async function workerLoop(worker: string, adapter: AgentAdapter) {
for (;;) {
await recoverExpiredLeases();
const task = await claimTask(worker);
if (!task) {
const unfinished = await pool.query<{ count: string }>(
`SELECT count(*) FROM tasks
WHERE status IN ('ready', 'running')`,
);
if (Number(unfinished.rows[0]?.count ?? 0) === 0) return;
await sleep(100);
continue;
}
try {
const output = await adapter(task);
await completeTask(task, worker, output);
} catch (error) {
await failTask(task, worker, error);
}
}
}
async function printReport() {
const tasks = await pool.query(
`SELECT id, status, attempts, output, last_error
FROM tasks ORDER BY created_at, id`,
);
console.table(tasks.rows);
const events = await pool.query(
`SELECT task_id, event, worker
FROM task_events ORDER BY id`,
);
console.table(events.rows);
}
async function main() {
await bootstrap();
await seedWorkflow();
const abandoned = await claimTask("worker-interrompido", 500);
console.log(`Lease abandonado: ${abandoned?.id}`);
await sleep(650);
await Promise.all([
workerLoop("worker-a", mockAgent),
workerLoop("worker-b", mockAgent),
]);
await printReport();
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(() => pool.end());
Ejecuta el type-check y la demostración:
npm run check
npm run demo
La tabla final debe mostrar inventory, test y review en succeeded. inventory tendrá dos intentos porque el primer worker abandonó el lease. test también tendrá dos, ya que el adaptador simula un fallo transitorio. review tendrá un intento y solo aparecerá después de que test termine correctamente.

En la tabla de eventos, comprueba este orden:
inventory claimed worker-interrompido
inventory lease-expired worker-interrompido
inventory claimed worker-a
inventory succeeded worker-a
test claimed worker-a
test retry worker-a
test claimed worker-b
test succeeded worker-b
review claimed worker-b
review succeeded worker-b
Esta prueba valida la infraestructura, no la inteligencia del modelo. Sustituir mockAgent por un CLI añade otro conjunto de evals: calidad del parche, validez de las pruebas, uso de herramientas y cumplimiento de la especificación. La regresión para agentes en CI debe seguir siendo un gate independiente.
Errores habituales y diagnóstico
Los fallos de orquestación deben mostrarse como estado, no como silencio. Microsoft recomienda exponer los errores para que el supervisor pueda responder y validar la salida antes de entregársela al siguiente agente. Un mensaje convincente no debe liberar dependencias sin cumplir el contrato.
| Síntoma | Causa probable | Corrección |
|---|---|---|
address already in use |
El puerto 54339 ya está ocupado. |
Cambia el puerto en compose.yaml y DATABASE_URL. |
ECONNREFUSED |
PostgreSQL todavía no está saludable. | Ejecuta docker compose ps y revisa los registros. |
| La misma tarea se ejecuta en dos workers | La selección y el UPDATE no están en la misma transacción. |
Conserva FOR UPDATE SKIP LOCKED dentro de inTransaction. |
Una tarea se queda en running |
No existe recuperación ni heartbeat. | Ejecuta recoverExpiredLeases y monitoriza lease_until. |
review empieza demasiado pronto |
La consulta ha ignorado task_dependencies. |
Mantén el NOT EXISTS sobre los requisitos previos incompletos. |
| Los reintentos no terminan | max_attempts no participa en la transición. |
Convierte el fallo en terminal al alcanzar el límite. |
| Gana un resultado tardío | La finalización no comprueba al propietario del lease. | Actualiza solo si lease_owner todavía coincide con el worker. |
Si una dependencia llega a failed, el ejemplo deja sin ejecutar las tareas siguientes. En producción, crea el estado blocked, registra la causa y termina el workflow sin seguir consultando la cola. Define también una política para la cancelación iniciada por una persona.
Durante la validación de este tutorial, el primer arranque falló porque el puerto 54329 ya estaba ocupado. Cambié el ejemplo a 54339, repetí el type-check y solo entonces ejecuté el flujo. Es un detalle corriente, pero sirve como comprobación: un fallo de infraestructura debe aparecer con una causa concreta, no convertirse en «ningún trabajo disponible».
¿Qué falta antes de llevarlo a producción?
El ejemplo demuestra la reserva, las dependencias, la recuperación y los reintentos, pero no es un servicio completo. El primer paso consiste en separar el supervisor y los workers en procesos distintos. El segundo es renovar los leases durante las tareas largas. Después llegan la cancelación, la dead-letter queue, la idempotencia, la autenticación, las métricas y la retención de artefactos.
La documentación "Orchestrate teams of Claude Code sessions" describe un lead, teammates, una lista compartida y un mailbox. También marca los agent teams como experimentales y señala limitaciones de reanudación y coordinación. Usa esta función cuando encaje con el flujo interactivo, pero no confundas el estado local de la herramienta con el contrato duradero de tu producto.
Para escribir en paralelo, aísla el sistema de archivos. Cada tarea recibe su propio worktree, rama o sandbox. El supervisor acepta un parche o commit, no una edición concurrente en el checkout compartido. El verificador aplica el artefacto en un entorno limpio y ejecuta pruebas deterministas antes de permitir el merge.
También faltan el control de acceso y la reducción de contexto. Cada worker debe recibir solo su tarea, los artefactos de las dependencias y las herramientas necesarias. El presupuesto de contexto para agentes de código ayuda a decidir qué persistir, resumir o descartar entre handoffs.
Preguntas frecuentes
¿Necesito varios agentes para cualquier tarea grande?
No. Microsoft recomienda la menor complejidad que resuelva el caso, y OpenAI propone los subagentes sobre todo para trabajo independiente con mucha lectura. Si las partes escriben en el mismo archivo o siguen una secuencia corta, un agente con checkpoints suele ser más predecible.
¿Por qué usar PostgreSQL en lugar de la memoria del supervisor?
Un estado externo permite reanudar tareas después de una interrupción y coordinar más de un proceso. Microsoft aconseja utilizar almacenamiento duradero para el progreso y los resultados de orquestaciones largas. PostgreSQL también ofrece transacciones y SKIP LOCKED, apropiado para consumidores concurrentes de una tabla usada como cola.
¿Un lease garantiza la ejecución exactamente una vez?
No. El lease reduce la ejecución concurrente, pero un worker puede completar el efecto externo y morir antes de guardar el éxito. Trata la ejecución como «al menos una vez». Usa claves idempotentes, worktrees desechables y la validación del artefacto antes de publicar cualquier cambio.
¿Puedo mezclar Codex y Claude Code en el mismo workflow?
Sí, siempre que ambos implementen el mismo AgentAdapter. El contrato debe normalizar la entrada, los permisos, el timeout, la salida y las pruebas. No introduzcas transcripciones distintas en la misma tabla. Guarda los datos en bruto en un artefacto restringido y persiste solo lo necesario para tomar la siguiente decisión.
¿Cuándo conviene usar un equipo de agentes en lugar de subagentes?
Usa subagentes cuando el supervisor solo necesite el resultado de tareas delimitadas. La documentación de Claude Code reserva los agent teams para trabajadores que deben compartir hallazgos y coordinarse entre sí. Esa comunicación consume más contexto y añade estados que también tendrás que observar y recuperar.
La siguiente decisión arquitectónica
Ejecuta primero el ejemplo con mockAgent y provoca fallos. Mata un worker, ocupa el puerto, fuerza un timeout y haz que un adaptador devuelva pruebas no válidas. Cuando el orquestador exponga cada caso sin duplicar trabajo ni liberar dependencias antes de tiempo, sustituye solo un adaptador por un agente real.
Después, conecta el resultado a evals de pull requests para agentes de código. El supervisor decide cuándo puede ejecutarse una tarea. El eval decide si el resultado merece avanzar. Separar estas responsabilidades mantiene el flujo recuperable incluso cuando el modelo se equivoca de forma convincente.
Referencias consultadas
- Microsoft Azure Architecture Center, "AI agent orchestration patterns", consultado el 25/07/2026, https://learn.microsoft.com/en-us/azure/architecture/ai-ml/guide/ai-agent-design-patterns
- Microsoft Learn, "Orchestrator and subagent multi-agent patterns", consultado el 25/07/2026, https://learn.microsoft.com/en-us/agents/architecture/multi-agent-orchestrator-sub-agent
- OpenAI Developers, "Subagents", consultado el 25/07/2026, https://developers.openai.com/codex/subagents
- Claude Code Docs, "Orchestrate teams of Claude Code sessions", consultado el 25/07/2026, https://code.claude.com/docs/en/agent-teams
- Claude Code Docs, "Create custom subagents", consultado el 25/07/2026, https://code.claude.com/docs/en/sub-agents
- PostgreSQL, "SELECT", consultado el 25/07/2026, https://www.postgresql.org/docs/current/sql-select.html
- Node.js, "Node.js Releases", consultado el 25/07/2026, https://nodejs.org/en/about/previous-releases
- npm, "pg", consultado el 25/07/2026, https://www.npmjs.com/package/pg
- npm, "TypeScript", consultado el 25/07/2026, https://www.npmjs.com/package/typescript
- npm, "tsx", consultado el 25/07/2026, https://www.npmjs.com/package/tsx
- npm, "@types/node", consultado el 25/07/2026, https://www.npmjs.com/package/@types/node
- npm, "@types/pg", consultado el 25/07/2026, https://www.npmjs.com/package/@types/pg