Skip to content

004 — Memory architecture (agente conversacional)

Status: Aceita (2026-06-03)

Cenário 008 estabeleceu AgentChat como Domain Service stateless: recebe { messages, budget, goal, household, model } e devolve a próxima mensagem. Histórico fica fora do domínio — decisão consciente, mirror de FeasibilityCheck.evaluate({ today }) (sem clock implícito, sem estado escondido).

A consequência prática: hoje conversa some no restart. Não há retrieval do que o casal disse semana passada, não há persistência de preferências (“a gente conta em BRL”), não há compressão de turns antigos. Cenários 012+ (chat persistido com retrieval) e 013 (write tools que persistem state) batem nessa parede.

Constraint operacional do Gabriel: o agente continua stateless. Ele recebe messaging context + dados relevantes injetados por turno. O que precisa entrar é UMA memória persistente — uma camada externa que sobrevive restart, retorna o slice relevante por turno, e é injetada via DI no AgentChat. Não é estado escondido do service; é input enriquecido pelo caller.

Otimização-chave: context window. Gemini 2.5 Flash via OpenRouter (ADR 001) tem janela larga mas custo proporcional. Despejar histórico inteiro a cada turno é caro e ruim — precisa de tiers (conversa recente verbatim, working memory denormalizada, observações comprimidas, semantic recall opcional).

Research externo já foi feito (relatório separado, conhecido pelo time) avaliando Letta, mem0, Zep/Graphiti, LangGraph memory, Cognee, Mastra, @ai-sdk-tools/memory (Midday), e DIY com better-sqlite3 + sqlite-vec. Esta ADR consolida a escolha — sem reabrir benchmark.

Adotar @mastra/memory + @mastra/libsql como memory layer, ponte com Vercel AI SDK via withMastra() de @mastra/ai-sdk (ou injeção direta da classe Memory no AgentChat.ask, caso o wrapper se mostre flaky no spike).

AgentChat.ask recebe identificadores opacos pra memória — não a memória em si:

agentChat.ask({
messages: CoreMessage[],
resourceId: string, // "casa-gabriel" (mapeia ao Household)
threadId: string, // por conversa
}): Promise<{ reply, toolCalls }>

Memória é injetada via construtor (DI) — mirror do pattern FeasibilityCheck.evaluate({ today }): dependências externas chegam como input, não como singleton.

Quatro tiers, ligados on/off conforme cenário

Section titled “Quatro tiers, ligados on/off conforme cenário”
  1. Conversation history — durável, libSQL tables chat_threads + chat_messages. Substitui o “histórico some no restart”. Padrão ligado a partir do cenário 012.
  2. Working memory — JSON estruturado por resourceId (couple/Household). Denormalized read model:
    {
    members: [{ name, role? }],
    currencyPreferred: "BRL",
    monthlyIncomeBRL: 12000,
    activeGoals: [{ name, target, deadline, savedInTarget }],
    // ...
    }
    LLM atualiza via tool-call específico (Mastra default — sem fact-extraction silencioso rodando em background). Trade-off consciente: menos determinístico que rules-based, mas evita lógica de extração custom.
  3. Observational memory — compressão de turns antigos em observações densas quando thread cresce. Não exige vector DB; Mastra faz com summarization in-place.
  4. Semantic recallOPT-IN, OFF por default. Liga só quando miss-rate de retrieval doer. Quando ligar: Gemini Embedding 001 via OpenRouter (mesmo provider que LLM, ADR 001). Custo de embeddings deferido até ter sinal.
  • libSQL single-file ./mel.db (SQLite-compatible) gerenciado pelo @mastra/libsql adapter.
  • Schemas + tables criadas pelo Mastra. Não tem schema-as-TS por drizzle aqui (Mastra gerencia internamente).
  • Dois SQLite drivers no início: better-sqlite3 (sync) pros domain repos via drizzle (ADR 001), libSQL pro Mastra. Coexistem em arquivos .db separados ou no mesmo arquivo (libSQL é SQLite-compat). Reconciliação futura: drizzle suporta libSQL — convergir pra um driver só quando schema estabilizar e custo de migração for baixo.
  • Letta/MemGPT — Python runtime, conflita com Vercel AI SDK (Node/TS).
  • mem0 — best path é managed cloud (conflita com local-first do projeto); Node SDK tem bug P1 #3291 conhecido.
  • Zep/Graphiti — exige graph DB (Neo4j/Postgres/Kuzu). Overkill pra app local-first de casal.
  • LangGraph memory — graph runtime + checkpointer, não memory layer; substituiria Vercel AI SDK (conflita com ADR 001).
  • Cognee — Python-first, graph DB.
  • DIY com sqlite-vec — viável e alinhado com ethos local-first/minimal-deps. Fallback se Mastra surface incomodar. Perde fact extraction + observational prontos.
  • @ai-sdk-tools/memory (Midday) — API mais Vercel-shaped, mas falta semantic recall na doc e é coupled com @ai-sdk-tools/agents. Strong reference se Mastra falhar — re-avaliar.

Positivas:

  • AgentChat continua stateless — contract do cenário 008 só ganha dois IDs (resourceId, threadId), não vaza memória pro domínio.
  • Local-first preservado — libSQL é SQLite-compat, single-file. Nenhum serviço externo obrigatório.
  • 4 tiers de memória disponíveis sem inventar lógica de compressão/extração.
  • Embeddings + custo associado deferidos até precisar (semantic recall OFF default).
  • Apache-2.0, 99% TS, 24.7k stars, último release maio/2026 — bus factor razoável.
  • Mock LLM (ADR 002) continua viável: memory layer é injetada via DI, spec passa stub determinístico.

Negativas / Trade-offs:

  • 4 packages novos (@mastra/core, @mastra/memory, @mastra/libsql, @mastra/ai-sdk) sobre a stack do ADR 001 — surface de dep cresce.
  • Dois SQLite drivers coexistindo (better-sqlite3 + libSQL) até reconciliar. Risco de divergência de schema/conexão; mitigado por arquivos .db separados no início.
  • withMastra() wrapper é novo — spike necessário pra validar ergonomia com Vercel AI SDK. Plano B: injeção direta da classe Memory no AgentChat.
  • Mastra traz Agent, workflows e outros conceitos que não vamos usar — surface conceitual maior que um lib focada em memory only. Aceito: usamos só Memory + libsql, ignoramos o resto.
  • Working memory é LLM-driven (tool-call edita o JSON) — menos determinístico que rules-based. Trade-off aceito pra não construir fact extraction custom.
  • ADR 001 não menciona Mastra. Amendment formal só quando virar dep instalada — esta ADR já documenta a escolha; sobreposição não justifica revisar 001 ainda.
  • DIY com better-sqlite3 + sqlite-vec + drizzle — alinha 100% com ethos minimal-deps e single-driver. Perde fact extraction + observational compression prontos; vira projeto-dentro-do-projeto. Mantido como fallback se Mastra surface incomodar (semestre).
  • @ai-sdk-tools/memory (Midday) — API Vercel-shaped mais natural pro AgentChat. Falta doc de semantic recall e tem coupling com @ai-sdk-tools/agents. Strong reference se Mastra falhar no spike.
  • Letta/mem0/Zep/Cognee/LangGraph — descartados por runtime (Python), modelo (cloud-only), ou infra (graph DB). Todos conflitam com local-first + Node/TS do projeto.
  • Manter AgentChat stateless sem memória persistente — adiar mais um ciclo. Funciona pra 008–011 mas trava 012+ (chat persistido) e 013 (write tools com state) ao mesmo tempo. Decidir agora evita escolha em pânico depois.
  • Postgres + pgvector — overkill pra projeto local-first; sem operação de DB externo justificada por um app de casal.