1. Por que o agente precisa de uma memória separada
Esta parte se baseia nas aulas anteriores do módulo 12 sobre agentes: lá já discutimos a arquitetura básica, o run‑cycle e as ferramentas; aqui o foco é memória e estado.
Fazendo uma analogia com um aplicativo web comum, a LLM aqui — é uma CPU muito inteligente que pode executar “programas de texto” complexos. E o estado do agente — é uma combinação de RAM e SSD: dados de sessão de curta duração e armazenamento de longo prazo.
No chat clássico do ChatGPT sem o seu código, a “memória” — é apenas a lista de mensagens system/user/assistant/tool, que o modelo vê na requisição atual. Para um agente, isso é insuficiente, porque:
- ele precisa lembrar o progresso de processos complexos: qual etapa do workflow já foi concluída, quais opções de presentes já foram filtradas, o que o usuário confirmou;
- ele precisa conhecer fatos de longo prazo sobre o usuário: preferências, endereço de entrega, histórico de pedidos;
- ele precisa conseguir sobreviver a falhas: se o servidor cair no meio da seleção do presente, o usuário não deve ter que inserir tudo de novo.
Se você tentar guardar tudo isso apenas no contexto do prompt, rapidamente atinge o limite da janela de contexto e paga tokens repetidamente pelos mesmos fatos. Ao mesmo tempo, há risco de segurança: dados demais e desnecessários são enviados para o modelo com frequência. Por isso, em sistemas com agentes sempre existe um estado explícito — objeto(s) que vivem fora do histórico de mensagens e são gerenciados por você.
2. Camadas do estado do agente: contexto, session, persistent
Vamos começar dividindo por camadas. Um agente normalmente tem no mínimo três níveis de “memória”:
- Histórico de mensagens (dialogue context).
- Estado de sessão (session state).
- Estado de longa duração (persistent state).
É importante não misturar esses conceitos.
Histórico de mensagens: “memória suja”
O histórico de mensagens — é o que a LLM vê a cada passo: instruções system, solicitações do usuário, respostas do agente, resultados de ferramentas.
A vantagem é que você não precisa gerenciar isso manualmente — o Agents SDK e a própria plataforma fazem isso por meio da entidade Session/Conversation.
A desvantagem — é uma memória “suja”: há muitas palavras desnecessárias, repetições e dados acidentais do usuário. Esses dados custam caro em tokens e são pouco estruturados. Você não quer que uma lista de 200 presentes já filtrados seja lida pelo modelo toda vez como texto simples.
Session state: memória de trabalho de curto prazo
O estado da Session — é um objeto estruturado que vive dentro de uma sessão/conversa do agente. Uma boa analogia para desenvolvedores frontend é useState ou a store do Redux, que vive enquanto a aba está aberta.
Ali vivem coisas como:
- a etapa atual do processo (por exemplo, "collecting_profile" ou "filtering_candidates");
- cache temporário de resultados de ferramentas;
- parâmetros da sessão: localidade, canal escolhido, flags como “o usuário aceitou os termos”.
Esse estado pode ser armazenado próximo ao agente — no Redis, em um KV in‑memory ou via um SessionService embutido do SDK específico. O principal é não tentar enfiar tudo isso no system‑prompt.
Persistent state: dados de longa duração
O estado Persistent vive por muito tempo: entre sessões, checkouts, dispositivos. É o perfil do usuário, seus pedidos, listas de desejos salvas, configurações.
Ideia principal: o agente não “lembra” dados persistentes magicamente; ele os “lê” por meio de ferramentas — por exemplo, get_user_profile, get_past_orders. Nada de variáveis globais ocultas dentro do agente; sempre chamada explícita.
Tabela comparativa
| Camada | Onde vive | Ciclo de vida | Exemplos de dados |
|---|---|---|---|
| Messages | Session / SDK / OpenAI | Um run / diálogo | mensagens system/user/tool |
| Session state | KV / SessionService / Redis | Enquanto a sessão estiver ativa | etapa do workflow, caches temporários |
| Persistent | BD (Postgres/NoSQL/ACP backend) | Entre sessões e diálogos | perfil, pedidos, listas salvas |
3. Session state: o que é e como armazená-lo
Imagine que o agente GiftGenius conduz um processo de várias etapas:
- Coleta o perfil do presenteado.
- Gera a lista de candidatos.
- Filtra por orçamento, entrega, região.
- Prepara a seleção final.
Durante o processo, ele conversa constantemente com o usuário e chama ferramentas. Tudo o que diz respeito ao “progresso de uma sessão específica de seleção de presentes” faz sentido manter no session‑state.
Exemplo de estrutura do estado de sessão do GiftGenius
Vamos descrever um tipo de estado de sessão em TypeScript:
// Estado dentro de uma única "seleção de presente"
export type GiftSessionState = {
step:
| "collecting_profile"
| "generating_candidates"
| "filtering"
| "finalizing";
// rascunho do perfil do presenteado
profileDraft?: {
recipientType?: string;
ageRange?: string;
interests?: string[];
dislikes?: string[];
};
// ids dos produtos candidatos obtidos do backend
candidateIds?: string[];
// presente escolhido pelo usuário
selectedGiftId?: string;
// flags técnicos
locale?: string;
};
Aqui, conscientemente, não colocamos objetos de produto inteiros — apenas seus IDs. Os dados completos devem viver no banco de dados; quando necessário, o agente chama a ferramenta get_gift_details(gift_id).
Session em Agents SDK (conceitualmente)
Em muitos SDKs para agentes há uma abstração de sessão que, por si, cuida do armazenamento do histórico de mensagens e permite que você armazene adicionalmente um estado estruturado. Em pseudocódigo, isso pode parecer assim:
import { createRunner, OpenAIConversationsSession } from "@openai/agents";
// tipo GiftSessionState do exemplo acima
const session = new OpenAIConversationsSession<GiftSessionState>({
sessionId: "chatgpt-thread-id-or-random",
});
const runner = createRunner({ agent });
const result = await runner.run({
session,
input: "Quero um presente para um colega de até US$ 50",
});
O SDK, por baixo dos panos:
- vai buscar o histórico de mensagens para essa sessão;
- vai adicionar a nova mensagem do usuário;
- vai repassar para o modelo e o ferramental;
- vai salvar de volta o estado atualizado (incluindo session.state).
Você trabalha com session.state como com um objeto comum.
Atualizando o session‑state a partir de ferramentas
Padrão típico: uma ferramenta que calcula algo também atualiza o estado de sessão. Por exemplo, uma ferramenta que coleta o perfil do presenteado a partir das respostas do usuário:
export async function updateProfileDraft(
session: GiftSessionState,
answers: { questionId: string; value: string }
): Promise<GiftSessionState> {
const next: GiftSessionState = { ...session };
if (!next.profileDraft) {
next.profileDraft = {};
}
if (answers.questionId === "interests") {
next.profileDraft.interests = answers.value.split(",").map((s) => s.trim());
}
// ...outros campos
next.step = "generating_candidates";
return next;
}
Aqui passamos para a ferramenta não a Session inteira do SDK, mas apenas seu state (tipo GiftSessionState). No código real, faz sentido nomear esse argumento, por exemplo, currentState, para não confundi-lo com o objeto Session.
O agente chama essa ferramenta, recebe um novo objeto de estado e o salva de volta em session.state.
4. Persistent state: memória de longo prazo do agente
Lembre que o GiftGenius não funciona apenas em um chat. O usuário pode voltar depois de uma semana, de outro dispositivo, e dizer: “Escolha um presente para o mesmo amigo da última vez, mas o orçamento aumentou”.
Essa informação deve viver não no session‑state, mas em um armazenamento persistent: no banco de dados, no backend de comércio/ACP (a camada de comércio, que terá um módulo separado) etc.
Exemplo de modelo persistent
Vamos descrever o modelo do perfil do presenteado no banco (simplificado, como um tipo TypeScript):
// O que é armazenado no BD
export type RecipientProfile = {
id: string;
userId: string;
label: string; // "colega de marketing"
recipientType: string;
ageRange?: string;
interests: string[];
dislikes: string[];
lastUsedAt: string; // data ISO
};
E o repositório (por enquanto um Map simples — na prática você teria uma camada ORM/SQL):
const profiles = new Map<string, RecipientProfile>();
export const RecipientRepo = {
async findByUser(userId: string): Promise<RecipientProfile[]> {
return [...profiles.values()].filter((p) => p.userId === userId);
},
async save(profile: RecipientProfile): Promise<void> {
profiles.set(profile.id, profile);
},
};
O agente acessa o persistent por meio de ferramentas
É importante que o agente não acesse o banco diretamente, mas trabalhe via tools. Assim ele permanece uma entidade “limpa”: em um lugar — a LLM e a lógica de planejamento, em outro — a implementação das integrações.
Por exemplo, a ferramenta get_recipient_profiles:
export async function getRecipientProfilesTool(input: {
userId: string;
}): Promise<{ profiles: RecipientProfile[] }> {
const profiles = await RecipientRepo.findByUser(input.userId);
return {
profiles,
};
}
Na descrição da ferramenta, o agente lê: “use este tool para obter os perfis de presenteados salvos para o usuário atual”. Ele decide sozinho quando exatamente chamá‑lo.
Resumindo: o session‑state — trata do progresso de uma conversa específica e de caches temporários que podem ser perdidos sem dor. Os dados persistent — são o que deve sobreviver a sessões e dispositivos: perfis, pedidos, listas de desejos. O agente sempre os lê por meio de ferramentas, e não “lembra magicamente”.
5. Como session e persistent trabalham juntos no run‑cycle
Agora vamos juntar tudo em um esquema geral. A cada passo do run‑cycle do agente temos uma sequência curta:
- Buscamos o session‑state pelo sessionId.
- Se necessário, carregamos dados persistent relevantes do banco com ferramentas.
- Formamos o contexto para o modelo (messages + estado estruturado).
- O modelo decide: responde com texto ou chama ferramentas.
- As ferramentas atualizam o session‑state ou os dados persistent (via banco).
- Salvamos o novo estado da sessão e, se necessário, criamos um checkpoint (sobre isso a seguir).
- Entregamos a resposta ao usuário.
Esquema em mermaid:
flowchart TD
A[Receber input do usuário] --> B["Carregar a Session (state + messages)"]
B --> C{Precisamos de dados persistentes?}
C -- Sim --> D[Chamar tools: get_user_profile, get_recipient_profiles]
C -- Não --> E[Formar o contexto para a LLM]
D --> E
E --> F["Chamar o modelo (LLM)"]
F --> G{O modelo quer chamar um tool?}
G -- Sim --> H[Executar o tool, atualizar session/persistent]
G -- Não --> I[Preparar a resposta final]
H --> J[Criar um checkpoint e salvar a Session]
I --> J
J --> K[Responder ao usuário]
Esse ciclo torna o comportamento do agente reprodutível: a cada passo sabemos explicitamente qual era o estado antes da chamada ao modelo e o que mudou depois.
6. Checkpoints: instantâneos do estado do agente
Checkpoints — são “instantâneos de estado” salvos do agente em uma etapa importante do processo. Isso não é apenas o “session‑state atual”, e sim um fato registrado em um armazenamento externo: na etapa N tínhamos tal estado, tais resultados de ferramentas e tal entrada do usuário.
Para que servem:
- recuperação após erros e quedas;
- possibilidade de o usuário “continuar depois”;
- depuração: reprodutibilidade de um run problemático;
- auditoria: o que exatamente o agente fez antes de, por exemplo, criar um pedido.
O que geralmente entra em um checkpoint
Um checkpoint típico contém:
- identificadores: runId, userId, workflowId, stepId;
- o estado de sessão naquele momento;
- identificadores-chave de entidades persistent (por exemplo, id do rascunho do pedido);
- metadados: hora de criação, versão do agente.
É importante não arrastar para lá todo o texto do diálogo. Abaixo, na seção sobre higiene da memória, voltaremos ao que exatamente vale a pena salvar e ao que não vale.
É melhor armazenar um link para a Session ou um breve resumo das etapas.
7. Projetando checkpoints para o GiftGenius
Vamos pegar nosso processo de seleção de presentes e decidir onde queremos checkpoints. Por exemplo:
- depois de coletar o perfil do presenteado;
- depois de gerar e filtrar inicialmente os candidatos;
- antes de oferecer ao usuário a escolha final.
Tipos para checkpoint e estado do workflow
Vamos descrever o estado do workflow (muito parecido com GiftSessionState, mas já é um “instantâneo” para checkpoints):
export type GiftWorkflowStep =
| "profile_collected"
| "candidates_generated"
| "filtered"
| "final_choice_made";
export type GiftCheckpoint = {
id: string;
runId: string;
userId: string;
step: GiftWorkflowStep;
// parte do estado de sessão,
// de que precisamos para a restauração
sessionState: GiftSessionState;
// quais ids de candidatos foram gerados
candidateIds: string[];
createdAt: string; // ISO
agentVersion: string;
};
Armazenamento de checkpoints (simplificado)
Vamos fazer, como antes, um Map simples em vez de um banco de dados real:
const checkpoints = new Map<string, GiftCheckpoint>();
export const GiftCheckpointRepo = {
async save(cp: GiftCheckpoint) {
checkpoints.set(cp.id, cp);
},
async findByRun(runId: string): Promise<GiftCheckpoint[]> {
return [...checkpoints.values()].filter((c) => c.runId === runId);
},
async findLastByUser(userId: string): Promise<GiftCheckpoint | undefined> {
return [...checkpoints.values()]
.filter((c) => c.userId === userId)
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
},
};
Criando um checkpoint a partir do código do agente
Vamos imaginar um helper que chamamos após uma etapa importante:
import { randomUUID } from "crypto";
export async function createCheckpoint(params: {
runId: string;
userId: string;
step: GiftWorkflowStep;
sessionState: GiftSessionState;
candidateIds: string[];
}) {
const checkpoint: GiftCheckpoint = {
id: randomUUID(),
runId: params.runId,
userId: params.userId,
step: params.step,
sessionState: params.sessionState,
candidateIds: params.candidateIds,
createdAt: new Date().toISOString(),
agentVersion: "v1.3.0",
};
await GiftCheckpointRepo.save(checkpoint);
}
O agente, no momento certo, pode chamar:
await createCheckpoint({
runId,
userId,
step: "filtered",
sessionState,
candidateIds,
});
Na restauração, fazemos:
- Encontramos o último checkpoint por runId ou userId.
- Restauramos o session.state a partir de checkpoint.sessionState.
- Se necessário, buscamos do banco os dados mais recentes pelos candidateIds.
8. Onde armazenar session, persistent e checkpoints tecnicamente
No nível de infraestrutura, você normalmente tem três classes de armazenamentos:
- In‑memory — para dev/demos, rápido, mas temporário.
- Redis (ou outro KV‑store) — para o estado de sessão.
- Banco relacional/NoSQL — para dados persistent e checkpoints.
In‑memory store para desenvolvimento local
Para o modo de desenvolvimento local, um in‑memory store simples é suficiente. Por exemplo, um mini‑armazenamento com TTL para sessões:
type StoredSession<T> = {
state: T;
expiresAt: number;
};
const sessions = new Map<string, StoredSession<GiftSessionState>>();
export function saveSession(sessionId: string, state: GiftSessionState) {
sessions.set(sessionId, {
state,
expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutos
});
}
export function loadSession(sessionId: string): GiftSessionState | undefined {
const stored = sessions.get(sessionId);
if (!stored) return undefined;
if (stored.expiresAt < Date.now()) {
sessions.delete(sessionId);
return undefined;
}
return stored.state;
}
Isso funciona muito bem para desenvolvimento local, mas em produção, com escalabilidade horizontal (várias instâncias), já não vai funcionar.
Redis para o session‑state
No ambiente de produção, é conveniente armazenar o estado de sessão no Redis:
- escrita/leitura rápidas;
- TTL “de fábrica”;
- disponível para todas as instâncias do serviço.
Exemplo em pseudo (simplificado):
// Wrapper em torno do cliente Redis
export async function saveSessionToRedis(
sessionId: string,
state: GiftSessionState
) {
const json = JSON.stringify(state);
await redis.set(`session:${sessionId}`, json, "EX", 60 * 30); // 30 minutos
}
export async function loadSessionFromRedis(
sessionId: string
): Promise<GiftSessionState | undefined> {
const json = await redis.get(`session:${sessionId}`);
return json ? (JSON.parse(json) as GiftSessionState) : undefined;
}
Postgres/outro banco para persistent e checkpoints
O estado persistent e os checkpoints — já são entidades “sérias”, para as quais importam transações, migrações, índices e outras alegrias. Eles são colocados em Postgres, MySQL, Firestore etc.
Padrão de arquitetura simples aqui:
- session no Redis com TTL;
- persistent e checkpoints no banco sem TTL (ou com política de retenção dependente do negócio).
9. Higiene da memória: tamanhos, privacidade, separação de responsabilidades
A memória do agente — não é apenas “colocar um objeto em algum lugar e seguir adiante”. Há algumas regras importantes que economizam dinheiro e preservam sua tranquilidade.
Não colocar tudo em messages
O histórico de mensagens — é um recurso caro:
- seu comprimento impacta fortemente o custo de uma chamada ao modelo;
- em geral, há muito “ruído”.
Portanto:
- procure extrair fatos do histórico para um estado estruturado o quanto antes;
- use sumarização para as partes antigas do histórico;
- se guardar o histórico textual em checkpoints, faça isso separadamente do que é enviado ao modelo.
Privacidade e PII
Especialmente para cenários de comércio, é importante não armazenar dados sensíveis em locais onde não deveriam estar. A documentação de arquitetura de memória enfatiza que dados PII não devem ficar em messages ou em checkpoints sem higienização.
Regras práticas:
- não coloque e‑mail/telefone/endereço diretamente no session‑state, a menos que seja necessário para o funcionamento do agente;
- em logs e checkpoints, prefira escrever identificadores (userId, recipientProfileId) em vez de strings brutas;
- se precisar carregar PII por várias etapas — use campos protegidos separados no armazenamento persistent e, no state, passe apenas a chave.
Separação dos dados de negócio e do log do diálogo
Um bom padrão — considerar o state como “memória limpa” e o messages como “memória suja”.
Ou seja:
- entidades de negócio (perfis, pedidos, carrinhos) vivem o tempo todo no banco;
- state/checkpoints contêm o mínimo necessário para restaurar o processo;
- logs/histórico do chat são armazenados separadamente (por exemplo, em um armazenamento vetorial) e usados para análise, mas não são adicionados a cada requisição ao modelo.
10. Mini prática: o que você salvaria?
Para fixar a diferença entre as camadas de memória, vamos nos afastar da teoria por um segundo e pensar em um caso concreto. Não é obrigatório escrever código — basta esboçar no papel ou mentalmente as estruturas.
Imagine que seu agente GiftGenius teve o seguinte diálogo com o usuário:
- Usuário: “Preciso de um presente para um colega desenvolvedor, orçamento até US$ 50; ele gosta de jogos de tabuleiro e cafeína”.
- Agente: faz algumas perguntas de esclarecimento.
- Usuário: “Ele odeia canecas e já está cheio de cadernos”.
- Agente: gera uma lista de 10 ideias, o usuário escolhe uma, mas diz: “Depois eu volto para finalizar tudo”.
Pense:
- O que você colocaria no session‑state (que pode expirar em 30 minutos)?
- O que iria para o armazenamento persistent, para que o usuário possa voltar em uma semana?
- Como seria o checkpoint após a escolha da ideia, mas antes de finalizar o pedido?
Tente esboçar os tipos TypeScript correspondentes e as funções saveSessionState, savePersistentState, createGiftIdeaCheckpoint por analogia com os exemplos desta aula. Se quiser, você pode rascunhar esses tipos e funções diretamente no editor — será um bom mini checkpoint antes da próxima aula.
11. Erros comuns ao trabalhar com a memória do agente
Erro nº 1: tentar armazenar tudo apenas no histórico de mensagens.
O desenvolvedor se anima: “Ora, o modelo já vê todo o diálogo, por que inventar outro state?”. Como resultado, após algumas dezenas de mensagens, a janela de contexto fica cheia de lixo, os tokens custam como um MacBook novo, e o comportamento do agente fica instável — ele simplesmente não vê fatos antigos importantes. Esse problema deve ser resolvido com a separação explícita do session‑state e do armazenamento persistent, e não aumentando limites.
Erro nº 2: misturar session e persistent em um único objeto.
Às vezes é tentador criar uma única entidade “grande” AgentState, colocar tudo lá dentro e salvar “como está” no banco. A fronteira entre dados temporários de uma conversa específica e dados de longo prazo do usuário fica borrada. Começam histórias do tipo “após o deploy, todas as sessões foram restauradas misteriosamente com dados do ano passado” ou “a sessão de um usuário pegou acidentalmente o perfil persistent de outra pessoa”. Separe os níveis conscientemente.
Erro nº 3: guardar demais nos checkpoints.
Erro comum — escrever no checkpoint todo o JSON de respostas das ferramentas, todo o histórico do diálogo, dados brutos de integrações etc. Depois de algumas semanas de operação, o banco de checkpoints incha demais, os backups demoram uma hora e as consultas ficam lentas. No checkpoint devem viver apenas os fatos realmente necessários para continuar o processo, mais o mínimo de metadados.
Erro nº 4: esquecer o TTL e a limpeza do session‑state.
Se os estados de sessão não tiverem prazo de validade, qualquer experimento aleatório do usuário no Dev Mode permanece no Redis para sempre. Após alguns meses, você olha o monitoramento e vê uma pilha de sessões “esquecidas”, consumindo memória. O nível de session precisa ser projetado com TTL explícito, e o nível persistent — com uma política de retenção bem pensada.
Erro nº 5: guardar PII em state e checkpoints sem necessidade.
É especialmente perigoso quando se joga e‑mail, endereço, número de cartão no session‑state sem pensar, e então esse objeto é serializado em logs, vai para a análise e para os checkpoints. Isso cria riscos sérios do ponto de vista regulatório e de segurança. É melhor armazenar identificadores seguros e, se necessário, resolvê‑los para dados reais por meio de ferramentas separadas e protegidas.
Erro nº 6: ausência de estratégia de restauração a partir de checkpoints.
Algumas equipes registram checkpoints com empenho, mas não pensam em como exatamente o agente deve se restaurar a partir deles. No fim, quando “algo deu errado”, os desenvolvedores olham para a tabela com JSONs bonitos, mas não têm código que saiba reconstruir o run a partir deles. Checkpointing sem um cenário de restauração — é apenas um log caro, não uma ferramenta de confiabilidade.
Erro nº 7: acoplamento rígido do agente a uma implementação específica de armazenamento.
Se o código do agente acessa diretamente Redis/Postgres, fica mais difícil portar, testar e evoluir. Ao mudar a arquitetura (por exemplo, surgindo recursos MCP ou um serviço de state separado), será preciso refatorar a lógica do agente. É muito melhor quando o agente enxerga apenas abstrações como Session e um conjunto de tools, e as ferramentas é que sabem onde exatamente os dados estão.
GO TO FULL VERSION