Roteamento automático de agents: a matriz que decide quem trabalha em cada tarefa (2026 edicao)
English summary: In multi-agent orchestration, choosing which agent, on which harness, at which effort tier is a routing decision — not a preference. This guide walks through a real cross-harness worker matrix (Claude, Codex, OpenCode, Agy) shipped in a production PR, explains why manual agent selection breaks down past a handful of tasks, and builds the minimal dispatcher that replaces guesswork with a declarative, versioned, fail-closed policy.
Publicado: julho de 2026
Tempo de leitura: 19 minutos
Segunda-feira, 9h. Você tem quatro tarefas na fila: escrever um changelog, refatorar um módulo de autenticação, investigar uma race condition e desenhar a migração de um serviço para outra região. Você abre o Claude Code, decide "essa parece grande, vou de Opus", copia o prompt, espera. Terça-feira, outra pessoa do time pega uma tarefa parecida, decide "Sonnet resolve", copia outro prompt. Sexta-feira, alguém pergunta por que o changelog mecânico consumiu o mesmo orçamento de tokens que a migração cross-region.
Ninguém errou sozinho. O sistema nunca teve uma política. Teve roteamento por vibe: cada pessoa adivinhando com um critério mental diferente, nenhum versionado, nenhum reproduzível na máquina do colega ao lado.
A tese deste artigo é verificável: orquestração multi-agente só escala com segurança quando a escolha de agente, harness e nível de esforço é uma matriz declarativa, versionada e auditável — não uma decisão ad-hoc repetida a cada tarefa por cada pessoa. Se dois membros do time, diante da mesma tarefa, podem chegar a rotas diferentes sem que nenhuma delas seja "errada" por escrito, você ainda não tem roteamento. Tem opinião distribuída.
Vamos construir essa matriz do zero, depois mostrar uma versão real dela — publicada, testada, rodando em quatro harnesses diferentes.
Dois termos vão aparecer o tempo todo: tier é o nível de agente/harness/custo que executa a tarefa (barato, balanceado, caro); effort é o quanto de raciocínio a própria tarefa exige, independente de quem vai rodá-la. A matriz existe para conectar os dois sem depender de julgamento manual.
Rode em 60 segundos
O exemplo abaixo é um dispatcher completo e determinístico. Ele não chama nenhum agente de verdade: workers são funções simuladas, o que permite testar a política de roteamento sem custo nem credenciais.
Salve como dispatch.mjs e rode com Node.js 20 ou superior:
node dispatch.mjs
const matrix = {
version: "2026-07-16",
tiers: {
budget: { worker: "haiku_worker", costUnits: 1, maxEffort: "medium" },
balanced: { worker: "sonnet_worker", costUnits: 3, maxEffort: "high" },
quality: { worker: "opus_worker", costUnits: 8, maxEffort: "xhigh" },
},
taskRouting: {
docs: "budget",
"mechanical-test": "budget",
lint: "budget",
implementation: "balanced",
refactor: "balanced",
"integration-test": "balanced",
architecture: "quality",
"root-cause": "quality",
"security-audit": "quality",
migration: "quality",
},
escalation: { maxFailuresBeforeEscalate: 2 },
};
const effortRank = { low: 0, medium: 1, high: 2, xhigh: 3 };
class ValidationError extends Error {
constructor(reason) {
super(reason);
this.reason = reason;
}
}
function classify(task) {
const tier = matrix.taskRouting[task.type];
if (!tier) throw new ValidationError(`unknown_task_type:${task.type}`);
return tier;
}
function eligible(tierName, task) {
const tier = matrix.tiers[tierName];
// fail-closed: esforço acima do teto da tier nunca é arredondado pra baixo
return effortRank[task.effort] <= effortRank[tier.maxEffort];
}
// Worker simulado. O `if` abaixo é um gancho artificial só desta demo:
// força 2 falhas em "balanced" pra provar a escalação automática logo adiante.
// Troque a condição pra experimentar outros cenários.
async function runWorker(tierName, task, attempt) {
if (task.id === "demo-escalation" && tierName === "balanced" && attempt <= 2) {
return { ok: false, reason: "validation_failed" };
}
return { ok: true, worker: matrix.tiers[tierName].worker, output: `done:${task.id}` };
}
async function dispatch(task) {
let tierName = classify(task);
if (!eligible(tierName, task)) {
throw new ValidationError(`effort_exceeds_tier:${tierName}`);
}
let failures = 0;
const tierOrder = ["budget", "balanced", "quality"];
let tierIndex = tierOrder.indexOf(tierName);
for (let attempt = 1; attempt <= 6; attempt += 1) {
const result = await runWorker(tierName, task, attempt);
if (result.ok) {
console.log(JSON.stringify({
event: "agent.dispatch.decision",
taskId: task.id,
matrixVersion: matrix.version,
requestedTier: classify(task),
effectiveTier: tierName,
worker: result.worker,
attempt,
escalated: tierIndex > tierOrder.indexOf(classify(task)),
}));
return result;
}
failures += 1;
if (failures >= matrix.escalation.maxFailuresBeforeEscalate) {
failures = 0;
tierIndex = Math.min(tierIndex + 1, tierOrder.length - 1);
tierName = tierOrder[tierIndex];
}
}
throw new Error("no_tier_resolved_task");
}
const result = await dispatch({
id: "demo-escalation",
type: "refactor",
effort: "high",
});
console.log(JSON.stringify(result));
Saída esperada:
{"event":"agent.dispatch.decision","taskId":"demo-escalation","matrixVersion":"2026-07-16","requestedTier":"balanced","effectiveTier":"quality","worker":"opus_worker","attempt":3,"escalated":true}
{"ok":true,"worker":"opus_worker","output":"done:demo-escalation"}
Os costUnits são unidades fictícias, só para ordenar tiers. Em produção o catálogo real carrega custo por provedor e por harness — a matriz que veremos adiante faz exatamente isso.
O que aconteceu
O dispatcher tomou quatro decisões, em ordem:
- classificou a tarefa (
refactor) na tierbalanced, porque é o mapeamento declarado emtaskRouting; - verificou que o esforço pedido (
high) cabe no teto da tier antes de gastar qualquer chamada; - simulou duas falhas de validação em
balanced—maxFailuresBeforeEscalateestá fixado em2na matriz — e escalou paraqualitysem intervenção humana; - registrou tier solicitada, tier efetiva, worker, tentativa e se houve escalação.
Troque effort para "xhigh" mantendo type: "docs". A tier budget tem maxEffort: "medium", então o dispatcher recusa a tarefa com effort_exceeds_tier antes de rodar qualquer worker — documentação não vira desculpa para gastar tier de arquitetura, e uma tarefa de alto esforço classificada errado não é silenciosamente empurrada para o tier mais barato.
Troque type para "deploy-prod". Não existe no taskRouting. O erro é unknown_task_type, não um fallback silencioso para uma tier qualquer. Tarefa sem classificação declarada falha fechada — a alternativa, "assume balanced e segue", é como surge o roteamento por vibe.
A intuição: uma torre de controle, não uma escala de plantão
Uma escala de plantão manual funciona até o segundo turno simultâneo. Alguém esquece de anotar quem está de sobreaviso, duas pessoas pegam o mesmo incidente, uma tarefa crítica fica sem dono porque "achei que fulano ia pegar".
Uma torre de controle de tráfego aéreo não funciona assim. Cada aeronave declara tipo, origem, destino e urgência antes de entrar no espaço aéreo. A torre não decide "essa parece grande, deixa ela esperar" no improviso — segue um protocolo que já sabe, para aquele tipo de aeronave e aquela condição, qual pista, qual prioridade, qual desvio se a pista principal estiver ocupada.
O paralelo com orquestração multi-agente é direto:
- classificação decide que tipo de tarefa chegou (
docs,refactor,architecture...); - matriz decide qual tier — e portanto qual agente, harness e custo — tem permissão e capacidade para essa tarefa;
- execução lida com falha, escalação e evidência de que o trabalho foi validado.
O agente pode ajudar a classificar a intenção da tarefa. Ele não deveria ser a fonte de verdade sobre qual tier ele mesmo merece rodar — isso é decisão de política, fora do agente, do mesmo jeito que um piloto não define sozinho sua própria prioridade de pouso.
Brinquedo e produção
| No exemplo | Em produção |
|---|---|
| dois tiers hardcoded, três workers simulados | catálogo com múltiplos provedores, harnesses e famílias de modelo por tier |
taskRouting fixo no código | fonte de verdade versionada, validada antes de qualquer render ou instalação |
| escalação por contagem simples | escalação com evidência de falha, motivo registrado, notificação ao usuário |
| um harness (Node local) | matriz replicada de forma consistente em múltiplos harnesses (CLIs diferentes, mesma política) |
| worker simulado sempre disponível | sincronização periódica contra o catálogo real do provedor, sem inventar IDs de modelo |
| log no console | instalador idempotente (rodar de novo não duplica nem corrompe nada), validação --validate-only não-mutante, testes automatizados |
O salto não é "adicionar mais tiers". É transformar cada suposição do brinquedo em artefato versionado, testável e instalável em qualquer máquina do time.
Por que automatizar isso — seis motivos, com evidência real
Isso é o núcleo do artigo. Cada motivo abaixo é ilustrado com um caso real: o PR #28 do repositório lemon-ai-hub, que adiciona uma worker matrix cross-harness ao plugin smart-sub-agents — uma matriz tier×effort que reproduz o mesmo grid de workers nomeados em Claude Code, Codex, OpenCode e Agy.
1. Consistência entre pessoas e entre máquinas
Sem matriz declarada, cada pessoa do time desenvolve seu próprio critério de "isso merece o modelo caro". Dois devs, mesma tarefa, rotas diferentes — e nenhuma auditável.
O PR #28 resolve isso com uma única fonte de verdade: references/provider-matrix.json, contendo workerMatrix (a definição dos tiers por harness) e taskRouting (o mapa budget/time por tipo de tarefa), alinhado ao contrato de smart-dispatch. Qualquer máquina que rode o instalador reproduz exatamente a mesma matriz — não uma aproximação dela.
2. Portabilidade cross-harness sem reescrever a política
Times reais não usam um harness só. Alguém prefere Claude Code, outro usa Codex, outro roda OpenCode com modelos gratuitos via Zen. Se a política de roteamento vive na cabeça de cada pessoa, ela não atravessa harness nenhum.
O PR nomeia isso explicitamente como contrato de nomenclatura. Claude é a fonte canônica — os corpos reais dos agentes vivem lá; os outros harnesses geram ou symlinkam a partir dela, em vez de manter cópias independentes:
| Harness | Padrão | Exemplo |
|---|---|---|
| Claude (canônico) | {haiku|sonnet|opus}_worker_{effort} | sonnet_worker_high |
| Codex | {luna|terra|sol}_worker_{effort} | luna_worker_max |
| OpenCode Zen | zen_{family}_worker_{effort} | zen_sol_worker_max |
| OpenCode Go | go_{family}_worker_{effort} | go_luna_worker_high |
| Agy | symlink → Claude + aliases nomeados Codex | opus_worker_high → ~/.claude/agents/... |
Codex ganha TOML nativo; OpenCode recebe markdown adaptado em duas lanes — "lane" aqui é só o caminho de acesso ao modelo: Zen é a lane gratuita, Go é a lane paga por assinatura; Agy faz symlink para os corpos do Claude e expõe aliases com nome Codex. A política é uma só — a representação varia por harness, sem que o comportamento varie junto.
3. Instalação idempotente em vez de configuração manual repetida
Documentar "copie esses arquivos de agente para ~/.claude/agents" funciona uma vez, na máquina de quem escreveu. Na segunda máquina, alguém esquece um arquivo, cria um agente com nome levemente diferente, e a matriz diverge sem ninguém perceber.
O PR adiciona install_worker_matrix.py: instalador idempotente que escreve para ~/.claude, ~/.codex, ~/.config/opencode e ~/.agy, marca cada arquivo gerado com managed-by: smart-sub-agents/worker-matrix — para nunca apagar um agente que não foi gerado por ele — e symlinka os corpos do Claude para o Agy.
4. Fail-closed em vez de fail-silent quando o catálogo está errado
Um catálogo de modelos que aceita qualquer string como "modelo válido" degrada silenciosamente: alguém digita um ID de modelo descontinuado, o roteador tenta usá-lo, o erro só aparece em produção.
validate_catalog.py valida famílias, lanes e efforts do workerMatrix e do taskRouting antes de qualquer instalação ou renderização — e tanto install_worker_matrix.py quanto render_agents.py recusam operar sobre uma matriz malformada. A evidência de verificação do PR é direta:
python3 plugins/smart-sub-agents/scripts/validate_catalog.py
→ OK: 10 providers, 48 models, 6 harnesses
Validação que roda antes da ação, não depois do incidente, é a diferença entre "o sistema recusou" e "o sistema tentou e falhou em produção".
5. Sincronização contra a realidade, nunca invenção de modelo
Catálogos de modelo ficam desatualizados rápido: um provedor descontinua um modelo, lança outro, renomeia um terceiro. Um catálogo estático vira ficção em semanas.
sync_provider_matrix.py reconcilia a matriz contra a saída ao vivo de opencode models, marcando entradas novas como discovered e entradas ausentes como stale — e a regra que o PR declara explicitamente no corpo é: "never invent model IDs". A sincronização só promove o que o provedor confirma; tudo o mais fica pendente de revisão humana antes de virar rota ativa.
6. Roteamento por orçamento e tempo, não por preferência pessoal
"Esse tipo de tarefa merece o modelo caro" é uma frase sem critério reproduzível até virar tabela. O contrato taskRouting, alinhado ao smart-dispatch, mapeia tipo de tarefa para tier de forma que qualquer pessoa — ou qualquer agente decidindo por outro agente — chega à mesma rota:
| Tipo de tarefa | Tier | Exemplos reais no smart-dispatch |
|---|---|---|
| Docs, comentários, changelog, tradução | budget | Haiku / Luna (low-medium) / Flash |
| Teste mecânico, padrão conhecido | budget | resultado só é aceito depois de rodar o teste de verdade — nunca por inspeção visual do diff |
| Implementação, refactor, multi-step agentic | balanced | Sonnet / Terra (medium-high) / Pro |
| Plano, arquitetura, root-cause, auditoria de segurança, migração | quality | Opus/Fable / Sol (high-xhigh) / Pro com thinking máximo |
A regra de escalação automática do smart-dispatch fecha o ciclo: se um tier falha a validação duas vezes, o dispatcher para de insistir no modelo barato, escala para o tier seguinte e notifica explicitamente — "Escalating to [Model] due to persistent validation failures." Ninguém precisa lembrar de escalar manualmente às 23h de sexta.
Quatro falhas reais em orquestração sem matriz declarada
1. Divergência silenciosa entre máquinas do time
Sem instalador versionado, a lista de agentes disponíveis em ~/.claude/agents na sua máquina e na do colega diverge aos poucos — alguém adiciona um agente manualmente, outra pessoa esquece de atualizar.
Como detectar: install_worker_matrix.py --validate-only comparando o estado instalado contra a matriz declarada, em CI ou localmente, sem tocar em nada.
Resposta: instalador idempotente com marcador managed-by, para diferenciar o que é gerado do que é customização legítima.
2. Tarefa cara rodando em tier barato porque a classificação falhou
Uma tarefa de arquitetura entra classificada como docs por engano de quem escreveu o pedido. O tier budget roda, entrega algo raso, ninguém percebe até o resultado já ter sido usado como base para outra decisão.
Como detectar: teto de esforço por tier (maxEffort) que rejeita a combinação antes de rodar, como no exemplo de 60 segundos — effort_exceeds_tier é um erro barato comparado a uma arquitetura mal decidida.
Resposta: classificação explícita no pedido, não inferida silenciosamente; falha fechada quando o tipo de tarefa não está no taskRouting.
3. Modelo descontinuado ainda referenciado na matriz
Um provedor remove um modelo do catálogo. Uma matriz estática continua apontando para ele. A primeira tentativa de uso falha em produção, sem aviso prévio.
Como detectar: sync_provider_matrix.py rodando contra a listagem viva do provedor, marcando o modelo como stale antes que alguém tente usá-lo.
Resposta: sincronização periódica (mensal/semanal, conforme o cadence documentado no PR) com promoção humana de discovered para ativo — nunca automática, nunca inventada.
4. Escalação manual que não acontece porque ninguém está olhando
Um tier barato falha repetidamente numa tarefa crítica, às 22h de sexta. Sem regra de escalação automática, a tarefa fica travada até segunda, quando alguém finalmente nota e troca manualmente para um modelo melhor.
Como detectar: contagem de falhas consecutivas por tier, visível em log estruturado (agent.dispatch.decision no exemplo de 60 segundos).
Resposta: maxFailuresBeforeEscalate como regra declarada, não como lembrete na cabeça de alguém — o smart-dispatch real usa exatamente esse padrão: duas falhas, escalação automática, notificação explícita.
Observabilidade e evidência de correção
Uma matriz sem prova de que continua correta é só um arquivo JSON que alguém confia. O PR #28 trata isso como parte do entregável, não como opcional:
- validação estrutural antes de qualquer instalação:
validate_catalog.py→OK: 10 providers, 48 models, 6 harnesses; - suíte de testes cobrindo validador, renderizador, normalização de alias e falha fechada para modelo desconhecido — 7 testes na primeira versão, evoluindo para 13 depois de uma segunda rodada de revisão que corrigiu quatro achados, entre eles o caso em que
install_worker_matrixcarregava o catálogo sem rodar a mesma validação querender_agentsjá exigia, e a heurística de classificação de tier que confundia o modelominimaxcom um modelo "mini" (budget) por causa de umsubstring matchfrouxo no nome; - versionamento explícito do plugin:
plugin.jsone.claude-plugin/marketplace.jsonsincronizados de1.0.0para1.1.0, seguindo a regra documentada de sincronia entre os dois arquivos — a matriz não é só código, é um artefato versionado como qualquer dependência.
Sem essa evidência, "a matriz está certa" é uma afirmação. Com ela, é um fato reproduzível por qualquer pessoa que rode os mesmos dois comandos.
Donos e rollback
| Artefato | Dono | Evidência exigida | Reversão |
|---|---|---|---|
provider-matrix.json (workerMatrix + taskRouting) | plataforma de IA / mantenedor do plugin | validate_catalog.py verde | reverter para versão anterior do arquivo |
| Instalação nos harnesses locais | cada desenvolvedor, via instalador | install --validate-only verde | reinstalar a versão anterior da matriz |
| Sincronização contra catálogo vivo | mantenedor, cadência mensal/semanal | diff discovered/stale revisado por humano | não promover; manter estado anterior |
| Contrato de nomenclatura entre harnesses | mantenedor do smart-sub-agents | testes de normalização de alias | ajustar mapeamento sem tocar nos corpos canônicos |
O ponto estrutural: a matriz é canônica no Claude e derivada em todo o resto — Codex, OpenCode e Agy consomem a mesma fonte em vez de manter cópias independentes. Reverter significa reverter um arquivo, reinstalar, revalidar. Não significa reescrever configuração espalhada em quatro lugares diferentes.
Exercícios
Use o dispatch.mjs do início como laboratório.
1. Prove o teto de esforço por tier
Adicione um tipo de tarefa novo em taskRouting mapeado para budget, e tente rodá-lo com effort: "xhigh".