Um worker reserva a tarefa inventory e desaparece. Outro worker não pode executar a mesma unidade ao mesmo tempo, mas também não deve esperar para sempre. Se a única cópia do estado vive na conversa do supervisor, o fluxo parou junto com o primeiro processo.

Neste tutorial, você vai montar uma orquestração multiagente em TypeScript que sobrevive a essa falha. O PostgreSQL guarda tarefas, dependências, tentativas, leases e eventos. Dois workers disputam trabalho sem duplicá-lo, uma falha transitória entra em retry e a revisão só começa depois que inventário e teste terminam.

Resultado do tutorial

  • Um fluxo executável com inventory, test e review.
  • Estado durável fora do contexto dos agentes.
  • Reserva atômica de tarefas por lease.
  • Recuperação de worker interrompido e retry limitado.
  • Uma interface para trocar o agente simulado por Codex ou Claude Code.

Arquitetura abstrata distribui tarefas entre coding agents e reúne evidências antes da saída final, sem texto visível.

Quando vale usar uma arquitetura multiagente?

Use vários agentes quando o trabalho tem fronteiras independentes, ferramentas distintas ou permissões que você quer separar. A documentação "AI agent orchestration patterns", mantida pela Microsoft em 2026, recomenda a menor complexidade capaz de resolver o caso. Um agente com ferramentas continua sendo o padrão mais fácil de depurar.

O fan-out começa a pagar a conta quando uma tarefa permite investigação paralela ou separação real de risco. Um worker pode mapear o repositório sem escrita. Outro pode executar testes num worktree isolado. Um terceiro revisa os artefatos, sem receber permissão para alterar o patch.

Não multiplique agentes quando todos dependem do mesmo arquivo, precisam seguir uma ordem curta ou carregam o mesmo contexto. A documentação "Subagents", da OpenAI, recomenda paralelismo primeiro para exploração, testes, triagem e síntese. Ela também alerta que escrita paralela aumenta conflito e custo de coordenação.

Situação Escolha inicial
Uma tarefa curta com ferramentas conhecidas Um agente
Leitura paralela com síntese central Subagentes
Módulos independentes que precisam conversar Time de agentes
Fluxo longo com retomada e dependências Orquestrador durável

Este tutorial aprofunda o fan-out de subagentes para migrações de código. Lá, o foco é particionar uma mudança. Aqui, o foco é o mecanismo que reserva, retoma e verifica cada unidade.

Qual arquitetura será implementada?

O supervisor ficará fora dos agentes. Ele decide quais tarefas estão prontas, recupera leases vencidos e encerra o fluxo. Os workers recebem uma tarefa tipada, chamam um AgentAdapter e devolvem um artefato curto. O PostgreSQL é a fonte de verdade para estado e dependências.

A documentação da Microsoft orienta persistir progresso e resultados intermediários num armazenamento durável quando a orquestração atravessa interações ou execuções longas. O mesmo documento trata falha de nó, perda de mensagem e erro em cascata como problemas clássicos de sistemas distribuídos. Prompt não substitui timeout, retry nem transação.

O fluxo terá esta ordem:

  1. inventory identifica o escopo e produz evidências.
  2. test aguarda inventory, executa uma prova e pode entrar em retry.
  3. review aguarda test e trata os resultados anteriores como alegações verificáveis.
  4. O supervisor termina quando todas as tarefas chegam a estado terminal.

As saídas não serão transcripts completos. Elas carregam summary e evidence. Quando um fluxo precisa atravessar mais sessões e subagentes sem reenviar logs brutos, uso o RemoteCode para estender Codex e Claude Code com menos repetição de contexto como ferramenta do próprio autor. A referência cabe aqui porque economia de tokens depende do contrato de handoff, não de esconder estado dentro do prompt.

Pré-requisitos

Você precisa de Docker com Compose, Git e Node.js 24 LTS. A página oficial "Node.js Releases" lista a linha 24 como LTS em julho de 2026. O exemplo usa PostgreSQL 18, TypeScript 7.0.2, pg 8.22.0 e tsx 4.23.1.

Confira o ambiente:

node --version
npm --version
docker version
docker compose version

O código funciona em Linux, macOS e Windows com Docker Desktop. A porta 54339 precisa estar livre. Se ela já estiver ocupada, troque o lado esquerdo do mapeamento em compose.yaml e ajuste DATABASE_URL.

Prepare o projeto TypeScript e o PostgreSQL

Crie uma pasta vazia e instale as dependências fixadas. As versões abaixo estavam publicadas no npm em 25/07/2026. O pin deixa o tutorial reproduzível, enquanto o package-lock.json registra a árvore resolvida.

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

Substitua 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"
  }
}

Crie tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2024",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

Crie 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

Suba o banco e espere o healthcheck:

docker compose up -d --wait

O comando deve terminar com o contêiner saudável. Se o Compose da sua máquina não aceita --wait, rode docker compose up -d e confira docker compose ps antes de continuar.

Persista tarefas, dependências e eventos

O estado mínimo precisa responder quem reservou a tarefa, até quando o lease vale, quantas tentativas ocorreram e que evidência saiu. A tabela task_dependencies impede que revisão comece antes do teste. task_events mantém a trilha de transições sem usar o transcript como banco.

Crie src/index.ts com os imports, tipos, conexão e 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 apaga somente as tabelas do banco de demonstração. Não copie esse TRUNCATE para um serviço compartilhado. Em produção, cada execução deve receber um workflow_id, e a criação de tarefas precisa ser idempotente.

Reserve trabalho com transação e lease

Dois workers podem consultar a fila ao mesmo tempo. A reserva precisa ocorrer na mesma transação que seleciona a tarefa. A documentação "SELECT", do PostgreSQL 18, explica que SKIP LOCKED evita espera sobre linhas já reservadas e é adequado a tabelas com múltiplos consumidores, embora não sirva como visão geral consistente dos dados.

Adicione a função abaixo ao mesmo arquivo:

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;
  });
}

O NOT EXISTS libera uma tarefa somente quando todas as dependências estão em succeeded. ORDER BY mantém uma ordem previsível entre candidatas. A própria documentação do PostgreSQL avisa que LIMIT sem ordenação pode devolver subconjuntos diferentes entre execuções.

O lease não é um lock longo do banco. A transação termina assim que a reserva é gravada. Depois disso, lease_until representa o direito temporário do worker sobre aquela unidade. Se o processo morrer, outro worker poderá recuperá-la.

Recupere worker interrompido sem duplicar a tarefa

Um lease vencido volta para ready. O evento precisa guardar o antigo dono antes de limpar as colunas. Essa transição deve ser atômica, pois vários supervisores podem procurar expirações ao mesmo tempo.

Um lease abstrato sai de um worker interrompido e volta a outro worker por meio do estado durável, sem texto visível.

Adicione:

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;
  });
}

Uma tarefa recuperada pode rodar novamente, então a operação feita pelo agente precisa ser idempotente ou acontecer num ambiente descartável. Para código, use worktree isolado, branch própria ou patch como artefato. Não deixe dois workers publicarem na mesma branch ou aplicarem a mesma migration.

O exemplo usa lease curto para tornar a falha visível em segundos. Em produção, renove leases com heartbeat e escolha o prazo a partir da duração real das tarefas. Um prazo arbitrariamente grande só transforma falha rápida em espera longa.

Separe sucesso, retry e falha terminal

O worker só conclui uma tarefa se ainda possui o lease. Uma resposta atrasada não pode sobrescrever o resultado de quem recuperou o trabalho. Falhas transitórias voltam para ready com atraso. A última tentativa muda o estado para failed.

Adicione:

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 },
  );
}

O atraso cresce com attempts, mas o exemplo não adiciona jitter para manter a saída previsível. Num serviço com muitos workers, inclua jitter e um limite máximo. Também classifique erros: prompt inválido e permissão negada não melhoram com retry; indisponibilidade de rede pode melhorar.

Coloque Codex ou Claude Code atrás do mesmo adapter

O orquestrador não deve saber qual modelo atende a tarefa. AgentAdapter recebe um contrato e devolve evidências. O adaptador simulado permite testar fila, falha e dependência sem uma chamada de API. Depois, você pode substituí-lo por Codex, Claude Code ou um serviço próprio.

Adicione o adaptador determinístico:

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 um adapter real, monte o prompt a partir de task.input, execute o CLI num worktree próprio e transforme JSONL em AgentOutput. A documentação de observabilidade de coding agents no CI mostra por que o transcript completo deve ficar num artefato restrito, enquanto a fila recebe apenas resumo e evidência.

Mantenha permissões por tipo. inventory pode ser somente leitura. test executa uma lista de comandos conhecida. review lê diff e resultados, mas não publica nem edita. Se todos os adapters recebem acesso total, a separação em agentes vira apenas uma divisão de prompts.

Execute workers e verifique o fluxo completo

O loop tenta recuperar leases, reserva uma tarefa pronta e chama o adapter. Quando não encontra trabalho, ele verifica se ainda há tarefas em ready ou running. Duas instâncias podem executar esse loop ao mesmo tempo porque a reserva usa FOR UPDATE SKIP LOCKED.

Adicione o restante do arquivo:

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());

Faça o type-check e rode a demonstração:

npm run check
npm run demo

A tabela final deve mostrar inventory, test e review em succeeded. inventory terá duas tentativas porque o primeiro worker abandonou o lease. test também terá duas, pois o adapter simula uma falha transitória. review terá uma tentativa e só aparecerá depois do sucesso de test.

Resultados abstratos de vários coding agents passam por verificação antes de formar um artefato aceito, sem texto visível.

Na tabela de eventos, confira esta ordem:

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

Esse é o teste do encanamento, não da inteligência do modelo. Trocar mockAgent por um CLI adiciona outro conjunto de evals: qualidade do patch, validade da evidência, uso de ferramentas e aderência à spec. A regressão para agentes no CI deve continuar como gate separado.

Erros comuns e diagnóstico

Falhas de orquestração precisam aparecer como estado, não como silêncio. A documentação da Microsoft recomenda expor erros para que o supervisor possa responder e validar a saída antes de passá-la ao agente seguinte. Uma mensagem convincente não deve liberar dependências sem um contrato aceito.

Sintoma Causa provável Correção
address already in use A porta 54339 já está ocupada. Troque a porta em compose.yaml e DATABASE_URL.
ECONNREFUSED O PostgreSQL ainda não ficou saudável. Rode docker compose ps e confira os logs.
A mesma tarefa roda em dois workers A seleção e o UPDATE não estão na mesma transação. Preserve FOR UPDATE SKIP LOCKED dentro de inTransaction.
Uma tarefa fica em running Não existe recuperação ou heartbeat. Execute recoverExpiredLeases e monitore lease_until.
review começa cedo A consulta ignorou task_dependencies. Mantenha o NOT EXISTS sobre pré-requisitos não concluídos.
Retry não termina max_attempts não participa da transição. Promova a falha a terminal quando o limite for atingido.
Um resultado atrasado vence A conclusão não confere o dono do lease. Atualize apenas quando lease_owner ainda corresponde ao worker.

Se uma dependência chega a failed, o exemplo deixa as tarefas seguintes sem execução. Em produção, crie o estado blocked, registre a causa e finalize o workflow sem ficar consultando a fila. Também defina uma política para cancelamento iniciado por pessoa.

Durante a validação deste tutorial, a primeira subida falhou porque a porta 54329 já estava ocupada. Troquei o exemplo para 54339, repeti o type-check e só então rodei o fluxo. É um detalhe banal, mas serve de teste: falha de infraestrutura precisa aparecer com causa concreta, não virar "nenhum trabalho disponível".

O que falta antes de levar para produção?

O exemplo prova reserva, dependência, recuperação e retry, mas não é um serviço completo. A primeira evolução é separar supervisor e workers em processos diferentes. A segunda é renovar leases durante tarefas longas. Depois entram cancelamento, dead-letter, idempotência, autenticação, métricas e retenção de artefatos.

A documentação "Orchestrate teams of Claude Code sessions" descreve lead, teammates, lista compartilhada e mailbox. Ela também marca agent teams como experimental e aponta limitações de retomada e coordenação. Use o recurso quando ele atende ao fluxo interativo, mas não confunda estado local da ferramenta com o contrato durável do seu produto.

Para escrita paralela, isole o sistema de arquivos. Cada tarefa recebe worktree, branch ou sandbox próprio. O supervisor aceita um patch ou commit, não uma edição concorrente no checkout compartilhado. O verificador aplica o artefato num ambiente limpo e roda provas determinísticas antes de liberar merge.

Também faltam controle de acesso e redução de contexto. Um worker deve receber somente a tarefa, os artefatos das dependências e as ferramentas necessárias. O orçamento de contexto para coding agents ajuda a decidir o que persistir, resumir ou descartar entre handoffs.

Perguntas frequentes

Preciso de vários agentes para qualquer tarefa grande?

Não. A Microsoft recomenda a menor complexidade que resolva o caso, e a OpenAI indica subagentes primeiro para trabalho independente e pesado em leitura. Se as partes escrevem no mesmo arquivo ou seguem uma ordem curta, um agente com checkpoints costuma ser mais previsível.

Por que usar PostgreSQL em vez da memória do supervisor?

Estado externo permite retomar tarefas depois de interrupção e coordenar mais de um processo. A Microsoft recomenda armazenamento durável para progresso e resultados de orquestrações longas. O PostgreSQL ainda oferece transação e SKIP LOCKED, apropriado a consumidores concorrentes de uma tabela usada como fila.

Lease garante execução exatamente uma vez?

Não. Lease reduz execução concorrente, mas um worker pode concluir o efeito externo e morrer antes de gravar sucesso. Trate a execução como pelo menos uma vez. Use chaves idempotentes, worktrees descartáveis e validação do artefato antes de publicar qualquer mudança.

Posso misturar Codex e Claude Code no mesmo workflow?

Sim, desde que ambos implementem o mesmo AgentAdapter. O contrato precisa normalizar entrada, permissão, timeout, saída e evidência. Não force transcripts diferentes para dentro da mesma tabela. Guarde o bruto em artefato restrito e persista somente o necessário para a próxima decisão.

Quando usar time de agentes em vez de subagentes?

Use subagentes quando o supervisor só precisa do resultado de tarefas focadas. A documentação do Claude Code reserva agent teams para trabalhadores que precisam compartilhar achados e coordenar entre si. Essa comunicação custa mais contexto e adiciona estados que você também precisa observar e recuperar.

Próxima decisão arquitetural

Rode primeiro com o mockAgent e provoque falhas. Mate um worker, ocupe a porta, force timeout e faça um adapter devolver evidência inválida. Quando o orquestrador expõe cada caso sem duplicar trabalho nem liberar dependências cedo, troque apenas um adapter por um agente real.

Depois conecte o resultado a evals de PR para coding agents. O supervisor decide quando uma tarefa pode rodar. O eval decide se a saída merece avançar. Separar essas responsabilidades mantém o fluxo recuperável mesmo quando o modelo erra de maneira convincente.

Referências consultadas

  • Microsoft Azure Architecture Center, "AI agent orchestration patterns", consultado em 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 em 25/07/2026, https://learn.microsoft.com/en-us/agents/architecture/multi-agent-orchestrator-sub-agent
  • OpenAI Developers, "Subagents", consultado em 25/07/2026, https://developers.openai.com/codex/subagents
  • Claude Code Docs, "Orchestrate teams of Claude Code sessions", consultado em 25/07/2026, https://code.claude.com/docs/en/agent-teams
  • Claude Code Docs, "Create custom subagents", consultado em 25/07/2026, https://code.claude.com/docs/en/sub-agents
  • PostgreSQL, "SELECT", consultado em 25/07/2026, https://www.postgresql.org/docs/current/sql-select.html
  • Node.js, "Node.js Releases", consultado em 25/07/2026, https://nodejs.org/en/about/previous-releases
  • npm, "pg", consultado em 25/07/2026, https://www.npmjs.com/package/pg
  • npm, "TypeScript", consultado em 25/07/2026, https://www.npmjs.com/package/typescript
  • npm, "tsx", consultado em 25/07/2026, https://www.npmjs.com/package/tsx
  • npm, "@types/node", consultado em 25/07/2026, https://www.npmjs.com/package/@types/node
  • npm, "@types/pg", consultado em 25/07/2026, https://www.npmjs.com/package/@types/pg