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,testereview.- 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.

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:
inventoryidentifica o escopo e produz evidências.testaguardainventory, executa uma prova e pode entrar em retry.reviewaguardateste trata os resultados anteriores como alegações verificáveis.- 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.

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.

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