A v1 foi um agente em TypeScript com prompt acoplado a stages fixas.
A v3 é um engine genérico que carrega Agent Blueprints do banco, orquestra 27 nodes em 11 layers (com paralelismo real) e executa Agent Operating Procedures versionados — sem recompilar.
Tudo observável, tudo testável, tudo reversível.
Cada node é uma função pura, tipada, testável isoladamente. Nodes dentro da mesma layer rodam em paralelo via LangGraph fan-out. Judges isolados depois de cada decisão cara — não no meio dela.
ChatState (immutable) — reads only what it declared in its reads[] contractPartial<ChatState> — writes only what it declared in its writes[] contract. Engine fails loud on undeclared writes.NodeContext injected — ai, logger, clock, vectorStore, retry, metrics. Zero third-party imports inside the node.buildMockChatState() + mock providers. Each node has a .test.ts next to it. No integration required.
Hoje: cada agent version é uma pasta em libs/ai-engine/src/agents/, com agent.ts, stages.ts, prompts/ — recompile para alterar qualquer coisa.
v3: um Agent Blueprint no banco, declarativo, versionado, fork-safe, com templates parseados em runtime.
// Cada AgentVersion carrega-se do DB; engine é 100% genérico. type AgentBlueprint = { id: BlueprintId; // abp_... — immutable, fork para editar agentVersionCode: string; // "magrass-sdr@v2.3.1" organizationId?: OrganizationId; // null → template global persona: PersonaAOP; // nome, tom, gênero, linguagem stages: StageAOP[]; // discriminated por kind variables: VariableSchemaAOP; // zod-compiled schema tools: ToolBindingAOP[]; // aliases + whenToCall + howToCall validators: ValidatorAOP[]; // llm-judges + code-checks memory: MemoryPolicyAOP; // scope, retention, compaction templates: TemplateRefAOP[]; // liquid templates versionados rollout: RolloutAOP; // canary %, auto-rollback thresholds }; // engine.ts — sem conhecimento de agente específico const blueprint = await blueprintRepo.loadByCode('magrass-sdr@v2.3.1'); const result = await engine.run({ blueprint, conversation, inbound });
required[] preenchidas. Retries com clarification.Uma conversa tem três dimensões ortogonais de estado: quem controla, qual o resultado, quem classificou o resultado. Isso elimina a confusão AWAITING_PAYMENT vs. TRANSFERRED vs. ACTIVE — essas viram stages, não status.
| dimensão | valores | semântica |
|---|---|---|
| currently_controlled_by | SYSTEM HUMAN NOBODY | Quem está respondendo agora. HUMAN trava a IA automaticamente. |
| outcome | PENDING WIN LOSS HANDOFF EXPIRED | Resultado terminal. PENDING = conversa viva. Terminais emitem métrica. |
| outcome_classified_by | SYSTEM HUMAN | Quem definiu. Humano sempre pode reclassificar retroativamente. |
SYSTEM · PENDING · SYSTEMHUMAN · PENDING · SYSTEM — IA silenciada até release.SYSTEM · WIN · SYSTEM — stage terminal disparou.HUMAN · WIN · HUMAN — revenue atribuído ao humano.SYSTEM · LOSS · HUMAN — gera training signal.NOBODY · EXPIRED · SYSTEM — engine background-job fechou.
Todo pedaço de texto que vai para o LLM — system prompt, few-shot exemplo, mensagem de fallback — vive como Template no banco, com ID, versão e parse tree.
Sem override parcial. Quer editar? Fork. Cada blueprint isolado, zero herança cruzada.
{# tpl_response_gen @v4 — owned by abp_magrass_sdr_v2 #} {# fork from: tpl_response_gen@v3 (global) #} <role> Você é {{ persona.name }}, {{ persona.role }} da {{ org.name }}. </role> <context> {% for variable in required_variables %} {{ variable.name }}: {{ variables[variable.name] | default: "[ausente]" }} {% endfor %} </context> <examples> {% render 'few-shot' for stage.few_shot_examples %} </examples> <output>JSON: { response, reasoning, confidence }</output>
Blueprint chama scheduling.search. Engine resolve para calendly.find_slots OU google.freebusy conforme org.
Trocar provider sem recompilar, sem editar prompt. Plus: AOP declara whenToCall + howToCall + afterCall — LLM sabe exatamente o que fazer.
tools: - alias: scheduling.search # semantic, not provider-specific required: true frequency: AT_LEAST_ONCE whenToCall: | Quando ainda não foram apresentados horários OU cliente pediu novas opções OU horários anteriores foram rejeitados. howToCall: arguments: targetDate: source: llm instruction: | Se cliente disse "amanhã" → runtime.tomorrowDate. Se "semana que vem" → próxima segunda. Default → next business day. clientPhone: source: state.lead.phoneNumber # engine fills, LLM can't hallucinate afterCall: onSuccess: instruction: "Sugira o slot mais próximo proativamente." sideEffects: - setVariable: { lastSearchAt: "${runtime.now}" } onFailure: instruction: "Diga que teve instabilidade e vai retornar em minutos." retry: { maxAttempts: 2 }
schedulingSearch quebra 4 prompts hardcoded, 2 testes e o judge — e você só descobre em produção.
reference_index.where(ref: 'scheduling.search') → lista todos os blueprints, stages, templates, validators que dependem. CI bloqueia merge se houver referência órfã.
A v1 joga tudo no ChatState e reza. v3 separa memória em quatro escopos ortogonais com políticas de retenção e compactação independentes — cada um com RLS no Postgres e TTL distinto.
| scope | contém | retenção | compactação | isolamento |
|---|---|---|---|---|
| conversation | mensagens, variáveis, stage atual | ativa + 90d | sliding window + summary | por conversationId |
| lead | preferências, histórico, sentimentos | lifetime | fato semântico destilado | por leadId |
| organization | objeções conhecidas, clusters | lifetime | embedding-clustered | por organizationId (RLS) |
| agent | aprendizados cross-org (templates) | lifetime | curadoria manual | por agentVersionCode |
HITL (Human-in-the-Loop): pausa síncrona em stage crítico, com snapshot + resume. FITL (Feedback-in-the-Loop): humano corrige resposta da IA retroativamente, e o delta vira sinal de treino — meta-AI agrega para proposta de ajuste de template.
Tracing via Dependency Injection — igual ao MetricsService e ClockService já fazem. Engine não conhece Langfuse; só conhece TracerProvider. Trocar vendor = um módulo em externals/, zero mudança em core.
export abstract class TracerProvider { abstract startSpan(params: StartSpanParams): Span; abstract addEvent(span: Span, event: TraceEvent): void; abstract setAttribute(span: Span, key: string, value: unknown): void; abstract endSpan(span: Span, status: SpanStatus): void; } // externals/observability/langfuse/langfuse-tracer.service.ts // ← ONLY file allowed to import 'langfuse' SDK // swap vendor → create externals/observability/arize/ and re-register
Zero hype. Cada peça escolhida por um motivo específico, com alternativa testada e descartada.
AiProvider.Sem migração gradual. v1 e v3 coexistem em runtime; orgs migram por feature flag. Quando 100% migrado, v1 é deletado inteiro.
libs/ai-engine/src/agents/magrass-sdr/) deletado. CI bloqueia qualquer PR que adicione código de agente específico fora de blueprint. Linha final.
Cada item aqui foi debatido, testado, ou vivido em produção. Registro para não voltarmos a cair.
cost ?? 0, agentVersion ?? 'unknown' escondem bugs por dias. → throw, sempre.import langfuse dentro de node bloqueia swap. → sempre via Provider abstract + externals/.Um engine, N agents, zero código por cliente. Tudo declarativo, tudo versionado, tudo reversível. Humano tem autoridade. IA tem velocidade. Engine tem disciplina.