O primeiro agente que você entrega é sempre um script. O segundo agente é uma cópia do primeiro com ferramentas diferentes. O terceiro agente é onde as coisas dão errado: alguém faz fork de um dos scripts anteriores, as dependências divergem, o logging fica inconsistente, e um trimestre depois você tem cinco agentes em quatro repos sem nenhum código compartilhado.
Se você sabe que vai construir mais de um agente, monte o monorepo antes de construir o segundo. Não antes do primeiro. Existe um custo real em estrutura prematura, e não vou fingir o contrário. Mas a janela entre o agente um e o agente dois é quando você paga menos e economiza mais.
A Premissa da Stack
Este post assume Angular no frontend, Python para o runtime do agente, Docker para dev local. Essa é a stack padrão que uso nos meus próprios projetos. Se você é Node/TypeScript de ponta a ponta, a maior parte disso se traduz diretamente. Se você é Go ou Rust no backend, a estrutura é a mesma, as escolhas de tooling mudam.
Por que Python para o agente em si. Os SDKs de IA (Anthropic, OpenAI) são mais maduros em Python, o ecossistema de ferramentas em torno de vector stores e pipelines de embedding é Python-first, e um loop ReAct fica mais legível em uma linguagem onde uma ferramenta é só uma função e um dict é só um dict. Mantenho o frontend em Angular porque é onde construo todo produto, e o dashboard para observar execuções de agentes é uma app Angular de verdade.
O Layout
my-agents/
├── apps/
│ ├── dashboard/ # Angular app, agent run viewer
│ └── api/ # Python FastAPI, dashboard backend
├── agents/
│ ├── sonar_bridge/ # First agent
│ │ ├── agent.py
│ │ ├── tools.py
│ │ ├── playbook.py
│ │ └── tests/
│ ├── docs_drift/ # Second agent
│ └── bug_triage/ # Third agent
├── libs/
│ ├── base_agent/ # The shared ReAct loop
│ ├── memory/ # pgvector client, Postgres schema, Redis client
│ ├── tools/ # Reusable tools: github, slack, sonar, jira
│ └── telemetry/ # Logging, cost tracking, evaluation
├── infra/
│ ├── docker-compose.yml # postgres, redis, sonarqube for local dev
│ ├── migrations/ # Alembic for Postgres schema
│ └── k8s/ # deployment manifests if you need them
├── knowledge/ # Markdown knowledge base (Obsidian-compatible)
│ ├── conventions/
│ ├── runbooks/
│ └── decisions/
├── pyproject.toml # Workspace root, points to libs and agents
├── package.json # For the dashboard
└── docker-compose.ymlTrês zonas. apps/ é para código voltado ao usuário: o dashboard e sua API. agents/ é para agentes individuais, cada um um pacote pequeno. libs/ é código compartilhado do qual todo agente depende. Nada mais pertence a esse nível.
O Agente Base
libs/base_agent/ é a peça mais importante de toda a estrutura. Contém o loop ReAct, os padrões de acesso à memória, o rastreamento de custo e a telemetria. Todo agente em agents/ importa dele e define apenas o que é específico do seu domínio: o playbook (system prompt), a lista de ferramentas, as regras de escalonamento.
# libs/base_agent/agent.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Callable
from libs.memory import MemoryStack
from libs.telemetry import Telemetry
@dataclass
class AgentConfig:
name: str
version: str
playbook: str
tools: list[Callable]
confidence_threshold: float = 0.7
max_iterations: int = 20
scratchpad_window: int = 6
class BaseAgent(ABC):
"""Owns the loop. The three hooks below are what each agent supplies."""
def __init__(self, config: AgentConfig, memory: MemoryStack, telemetry: Telemetry):
self.config = config
self.memory = memory
self.telemetry = telemetry
@abstractmethod
def _plan(self, trigger: dict, context: str | None,
scratchpad: list) -> tuple[str, dict | None]:
"""One model call. Returns (thought, action), or action=None when done."""
@abstractmethod
def _execute(self, action: dict) -> dict:
"""Run one tool from config.tools and return the observation."""
@abstractmethod
def _decide(self, scratchpad: list) -> dict:
"""Turn the finished scratchpad into the run outcome."""
def run(self, trigger: dict) -> dict:
run_id = self.telemetry.start_run(self.config, trigger)
similar = self.memory.vector_search(trigger, top_k=5)
context = self.memory.summarize(similar) if similar else None
scratchpad = []
for iteration in range(self.config.max_iterations):
thought, action = self._plan(trigger, context, scratchpad)
if action is None:
break
observation = self._execute(action)
scratchpad.append((thought, action, observation))
# The scratchpad is what blows the context budget on a long
# run. Compress the older entries instead of carrying 20
# raw observations into the next plan call.
if len(scratchpad) > self.config.scratchpad_window:
scratchpad = self.memory.compress(
scratchpad, keep_last=self.config.scratchpad_window
)
outcome = self._decide(scratchpad)
self.telemetry.end_run(run_id, outcome)
self.memory.write(run_id, outcome)
return outcomeO BaseAgent é dono do loop e de mais nada. _plan, _execute e _decide são abstratos de propósito: _plan é a chamada ao modelo, _execute é o dispatch de ferramentas, _decide transforma o scratchpad finalizado em um resultado. A implementação concreta desses três mora uma única vez em libs/base_agent/, então um agente novo é um objeto de configuração e um playbook, cerca de 30 linhas. Repare no passo de compressão dentro do loop. Vinte iterações de thought, action e observation crus são o jeito de você acabar com uma conta de tokens que não planejou.
# agents/sonar_bridge/agent.py
from libs.base_agent import ReActAgent, AgentConfig
from libs.memory import MemoryStack
from libs.telemetry import Telemetry
from libs.tools import sonar, github
from .playbook import PLAYBOOK
def build() -> ReActAgent:
return ReActAgent(
config=AgentConfig(
name="sonar_bridge",
version="0.1.0",
playbook=PLAYBOOK,
tools=[
sonar.get_issues_for_pr,
github.read_file_range,
github.comment_on_pr,
],
confidence_threshold=0.85,
),
memory=MemoryStack.default(),
telemetry=Telemetry.default(),
)O Dashboard Realmente Importa
O Angular está fazendo trabalho de verdade aqui. Você precisa de um lugar para ver toda execução de agente, filtrar por resultado, comparar decisões entre versões e reproduzir falhas. Isso não é um nice-to-have, é como você debuga um agente que decidiu escalar três vezes seguidas na noite passada.
A API Python em apps/api/ é um wrapper FastAPI fino que lê do mesmo Postgres em que os agentes escrevem. Sem magia, sem duplicação. O dashboard é um cliente burro dos mesmos dados que seus agentes já produzem.
Dev Local em um Único Comando
# docker-compose.yml at the root
services:
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_PASSWORD: local
POSTGRES_DB: agents
ports: ["5432:5432"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
sonarqube:
image: sonarqube:community
ports: ["9000:9000"]
api:
build: ./apps/api
depends_on: [postgres, redis]
environment:
DATABASE_URL: postgres://postgres:local@postgres/agents
REDIS_URL: redis://redis:6379
ports: ["8000:8000"]
dashboard:
build: ./apps/dashboard
depends_on: [api]
ports: ["4200:4200"]docker-compose up dá a qualquer pessoa que clone o repo um ambiente completo de trabalho: Postgres com pgvector, Redis, SonarQube, a API e o dashboard Angular. Sem mexer em instalações locais de Postgres, sem 'funciona na minha máquina' no primeiro dia.
O Diretório de Conhecimento
knowledge/ é uma pasta de arquivos markdown. Não é uma feature. Não é um plugin. Só markdown. O time edita direto, ou aponta um vault do Obsidian para ela. Um indexador em libs/memory/ sincroniza o conteúdo para o pgvector a cada mudança, e todo agente que precisa de um playbook ou de uma convenção lê da mesma fonte.
Separo a base de conhecimento do código deliberadamente. Conhecimento de negócio muda independentemente do código do agente. Você quer que não-engenheiros editem knowledge/ sem tocar em agents/. O Git ainda versiona. Reviews ainda acontecem. Mas a fronteira fica clara.
O Que Isso Te Dá
- Novo agente = nova pasta em agents/, 30 linhas de Python. Sem debate de infraestrutura.
- Todo agente produz o mesmo formato de telemetria. O dashboard não precisa de código por agente.
- Memória, ferramentas e lógica de avaliação são compartilhadas. Um bug fix em libs/ corrige todos os agentes de uma vez.
- Dev local é um único comando. Do clone à stack rodando, sem documento de setup para seguir.
- Deploy em produção é o mesmo padrão de imagem de container para todo agente. Um pipeline, muitos agentes.
Onde Eu Traço o Limite
Nem tudo pertence ao monorepo. Se um agente tem um raio de impacto diferente, digamos que ele pode escrever em um sistema de produção enquanto os outros só comentam em pull requests, ele ganha seu próprio repo e sua própria cadência de release. Se um agente é experimental e eu não tenho certeza se sobrevive ao mês, eu prototipo fora e movo para dentro quando provar seu valor.
O monorepo é para os agentes de produção. Os estáveis. Aqueles dos quais seu time depende. Todo o resto pode começar como script e ser promovido quando conquistar seu lugar.
Um agente base compartilhado e uma camada de telemetria consistente valem mais do que qualquer agente individual. É esse código que torna o quinto agente fácil de entregar.
Referências
- Tooling de monorepo Nx (se você quiser formalizar): https://nx.dev
- pgvector: https://github.com/pgvector/pgvector
- FastAPI: https://fastapi.tiangolo.com
- SDK Python da Anthropic: https://github.com/anthropics/anthropic-sdk-python