1. Por qué un agente necesita una memoria aparte
Esta parte se basa en lecciones previas del módulo 12 sobre agentes: allí ya discutimos la arquitectura básica, el ciclo de ejecución y las herramientas; aquí el foco está en la memoria y el estado.
Si hacemos una analogía con una aplicación web convencional, la LLM aquí es como una CPU muy inteligente que puede ejecutar «programas de texto» complejos. Y el estado del agente es una combinación de RAM y SSD: datos de sesión de corta duración y almacenamiento a largo plazo.
En un chat clásico de ChatGPT sin tu código, la «memoria» es simplemente la lista de mensajes system/user/assistant/tool, que el modelo ve en la solicitud actual. Para un agente esto no es suficiente porque:
- necesita recordar el progreso de procesos complejos: qué paso del flujo de trabajo ya se superó, qué opciones de regalos ya se filtraron, qué confirmó el usuario;
- necesita conocer hechos a largo plazo sobre el usuario: preferencias, dirección de entrega, historial de pedidos;
- debe ser capaz de tolerar fallos: si el servidor cae en mitad de la selección de un regalo, el usuario no debería tener que introducirlo todo de nuevo.
Si intentas guardar todo esto solo en el contexto del prompt, pronto chocarás con el límite de la ventana de contexto y pagarás tokens por los mismos hechos una y otra vez. Al mismo tiempo te arriesgas en seguridad: demasiados datos innecesarios se mandan regularmente al modelo. Por eso en sistemas con agentes siempre hay un estado explícito: objeto(s) que viven fuera del historial de mensajes y que gestionas tú.
2. Capas del estado del agente: contexto, session, persistent
Empecemos por descomponer en capas. Un agente suele tener al menos tres niveles de «memoria»:
- Historial de mensajes (dialogue context).
- Estado de sesión (session state).
- Estado persistente (persistent state).
Es importante no mezclar estos conceptos en un mismo saco.
Historial de mensajes: «memoria sucia»
El historial de mensajes es lo que ve la LLM en cada paso: instrucciones system, solicitudes del usuario, respuestas del agente y resultados de herramientas.
La ventaja es que no necesitas gestionarlo a mano: el Agents SDK y la propia plataforma se encargan a través de la entidad Session/Conversation.
La desventaja es que es «memoria sucia»: hay muchas palabras de relleno, repeticiones y datos accidentales del usuario. Esos datos son caros en tokens y están poco estructurados. No quieres que la lista de regalos ya filtrados, con 200 elementos, se lea a la modelo cada vez como texto plano.
Session state: memoria de trabajo a corto plazo
El estado de Session es un objeto estructurado que vive dentro de una sesión/conversación del agente. Una buena analogía para un desarrollador frontend es useState o un store de Redux que vive mientras la pestaña está abierta.
Allí viven cosas como:
- el paso actual del proceso (por ejemplo, "collecting_profile" o "filtering_candidates");
- caché temporal de resultados de herramientas;
- parámetros de la sesión: localización, canal elegido, banderas como «el usuario ha aceptado las condiciones».
Este estado puede almacenarse cerca del agente — en Redis, en un almacén KV en memoria o a través del SessionService incorporado del SDK concreto. Lo importante es no intentar meter todo eso en el system‑prompt.
Persistent state: datos a largo plazo
El estado Persistent vive mucho tiempo: entre sesiones, compras y dispositivos. Es el perfil del usuario, sus pedidos, listas de deseos guardadas, configuraciones.
Idea clave: el agente no «recuerda» datos persistentes mágicamente, los «lee» a través de herramientas — por ejemplo, get_user_profile, get_past_orders. Nada de variables globales ocultas dentro del agente; siempre llamadas explícitas.
Tabla comparativa
| Capa | Dónde vive | Ciclo de vida | Ejemplos de datos |
|---|---|---|---|
| Messages | Session / SDK / OpenAI | Una ejecución/run / diálogo | mensajes system/user/tool |
| Session state | KV / SessionService / Redis | Mientras la sesión esté viva | paso del flujo de trabajo, cachés temporales |
| Persistent | BD (Postgres/NoSQL/backend ACP) | Entre sesiones y diálogos | perfil, pedidos, listas guardadas |
3. Session state: qué es y cómo almacenarlo
Imagina que el agente GiftGenius realiza un proceso de varios pasos:
- Recopila el perfil del destinatario del regalo.
- Genera una lista de candidatos.
- Los filtra por presupuesto, entrega, región.
- Prepara la selección final.
Durante el proceso habla continuamente con el usuario y llama a herramientas. Todo lo que pertenece al «progreso de la sesión concreta de selección de regalo» es lógico mantenerlo en el session‑state.
Ejemplo de estructura del estado de sesión de GiftGenius
Describamos el tipo de estado de sesión en TypeScript:
// Estado dentro de una "selección de regalo"
export type GiftSessionState = {
step:
| "collecting_profile"
| "generating_candidates"
| "filtering"
| "finalizing";
// borrador del perfil del destinatario
profileDraft?: {
recipientType?: string;
ageRange?: string;
interests?: string[];
dislikes?: string[];
};
// id de productos candidatos recibidos del backend
candidateIds?: string[];
// regalo seleccionado por el usuario
selectedGiftId?: string;
// marcadores técnicos
locale?: string;
};
Aquí conscientemente no guardamos los objetos completos de los productos — solo sus ID. Que los datos completos vivan en la BD; cuando haga falta, el agente llama a la herramienta get_gift_details(gift_id).
Session en Agents SDK (conceptualmente)
En muchos SDK para agentes existe la abstracción de sesión, que se encarga por sí misma de almacenar el historial de mensajes y te permite almacenar adicionalmente un estado estructurado. En pseudocódigo podría verse así:
import { createRunner, OpenAIConversationsSession } from "@openai/agents";
// tipo GiftSessionState del ejemplo anterior
const session = new OpenAIConversationsSession<GiftSessionState>({
sessionId: "chatgpt-thread-id-or-random",
});
const runner = createRunner({ agent });
const result = await runner.run({
session,
input: "Quiero un regalo para un colega de hasta 50$",
});
El SDK bajo el capó:
- recuperará el historial de mensajes para esta sesión;
- añadirá el nuevo mensaje del usuario;
- lo pasará al modelo y al conjunto de herramientas;
- guardará de vuelta el estado actualizado (incluido session.state).
Tú trabajas con session.state como con un objeto normal.
Actualización del session‑state desde herramientas
Patrón típico: una herramienta que calcula algo, a la vez actualiza el estado de sesión. Por ejemplo, una herramienta que recopila el perfil del destinatario a partir de las respuestas del usuario:
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());
}
// ...otros campos
next.step = "generating_candidates";
return next;
}
Aquí no pasamos a la herramienta toda la Session del SDK, sino solo su state (tipo GiftSessionState). En código real tiene sentido llamar a ese argumento, por ejemplo, currentState, para no confundirlo con el objeto Session.
El agente llama a esta herramienta, obtiene un nuevo objeto de estado y lo guarda de vuelta en session.state.
4. Persistent state: memoria a largo plazo del agente
Recordemos ahora que GiftGenius no funciona solo en un chat. El usuario puede volver dentro de una semana, desde otro dispositivo, y decir: «Elige un regalo para el mismo amigo que la vez anterior, pero el presupuesto ha aumentado».
Esa información no debe vivir en el session‑state, sino en el almacenamiento persistente: en la base de datos, en el backend de comercio/ACP (la capa de commerce, que verá un módulo aparte), etc.
Ejemplo de modelo persistente
Describamos el modelo de perfil del destinatario en la BD (simplificado, como tipo de TypeScript):
// Lo que se guarda en la BD
export type RecipientProfile = {
id: string;
userId: string;
label: string; // "compañero de marketing"
recipientType: string;
ageRange?: string;
interests: string[];
dislikes: string[];
lastUsedAt: string; // fecha ISO
};
Y el repositorio (por ahora un Map simple — en la realidad harías una capa 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);
},
};
El agente accede a lo persistente a través de herramientas
Es importante que el agente no acceda directamente a la BD, sino que trabaje vía tools. Así se mantiene como una entidad «limpia»: en un sitio — la LLM y la lógica de planificación; en otro — la implementación de integraciones.
Por ejemplo, la herramienta get_recipient_profiles:
export async function getRecipientProfilesTool(input: {
userId: string;
}): Promise<{ profiles: RecipientProfile[] }> {
const profiles = await RecipientRepo.findByUser(input.userId);
return {
profiles,
};
}
En la descripción de la herramienta, el agente lee: «usa esta tool para obtener los perfiles de destinatarios guardados para el usuario actual». Él decide por sí mismo cuándo invocarla.
En resumen: el session‑state trata sobre el progreso de la conversación concreta y los cachés temporales que se pueden perder sin dolor. Los datos persistentes son lo que debe sobrevivir a sesiones y dispositivos: perfiles, pedidos, listas de deseos. El agente siempre los lee a través de herramientas, no los «recuerda mágicamente».
5. Cómo session y persistent trabajan juntos en el ciclo de ejecución
Ahora juntemos todo en un esquema general. En cada paso del ciclo de ejecución del agente tenemos una secuencia corta:
- Cargamos el session‑state por sessionId.
- Si hace falta, traemos datos persistentes relevantes desde la BD mediante herramientas.
- Formamos el contexto para el modelo (mensajes + estado estructurado).
- El modelo decide: responde con texto o llama a herramientas.
- Las herramientas actualizan o bien el session‑state o bien los datos persistentes (a través de la BD).
- Guardamos el nuevo estado de sesión y, si hace falta, creamos un checkpoint (más adelante hablamos de ello).
- Devolvemos la respuesta al usuario.
Esquema en mermaid:
flowchart TD
A[Obtener input del usuario] --> B["Cargar Session (state + messages)"]
B --> C{¿Se necesitan datos persistentes?}
C -- Sí --> D[Invocar tools: get_user_profile, get_recipient_profiles]
C -- No --> E[Formar el contexto para la LLM]
D --> E
E --> F["Invocar el modelo (LLM)"]
F --> G{¿El modelo quiere invocar una herramienta?}
G -- Sí --> H[Ejecutar tool, actualizar session/persistent]
G -- No --> I[Preparar la respuesta final]
H --> J[Crear checkpoint y guardar Session]
I --> J
J --> K[Respuesta al usuario]
Este ciclo hace que el comportamiento del agente sea reproducible: en cada paso sabemos explícitamente qué estado había antes de llamar al modelo y qué cambió después.
6. Checkpoints: instantáneas del estado del agente
Los checkpoints son «instantáneas del estado» del agente guardadas en un paso importante del proceso. No es simplemente «el session‑state actual», sino el hecho registrado en un almacenamiento externo: en el paso N teníamos tal estado, tales resultados de herramientas y tal entrada del usuario.
Para qué sirven:
- recuperación tras errores y caídas;
- posibilidad de «continuar más tarde» por parte del usuario;
- depuración: reproducibilidad de una ejecución problemática;
- auditoría: qué hizo exactamente el agente antes de, por ejemplo, crear un pedido.
Qué suele incluir un checkpoint
Un checkpoint típico contiene:
- identificadores: runId, userId, workflowId, stepId;
- el estado de sesión en ese momento;
- identificadores clave de entidades persistentes (por ejemplo, id del borrador de pedido);
- metadatos: hora de creación, versión del agente.
Es importante no arrastrar ahí todo el texto del diálogo. Más abajo, en la sección sobre higiene de la memoria, volveremos a qué conviene guardar y qué no.
Mejor guardar un enlace a la Session o un breve resumen de pasos.
7. Diseñamos checkpoints para GiftGenius
Tomemos nuestro proceso de selección de regalo y decidamos dónde queremos checkpoints. Por ejemplo:
- después de recopilar el perfil del destinatario;
- después de generar y filtrar inicialmente los candidatos;
- antes de proponer al usuario la elección final.
Tipos para checkpoint y estado del workflow
Describamos el estado del flujo de trabajo (muy parecido a GiftSessionState, pero esto ya es un «molde» 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 del estado de sesión
// que necesitamos para la recuperación
sessionState: GiftSessionState;
// qué id de candidatos se generaron
candidateIds: string[];
createdAt: string; // ISO
agentVersion: string;
};
Almacenamiento de checkpoints (simplificado)
Hagamos, como antes, un Map sencillo en lugar de una BD 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];
},
};
Creación de un checkpoint desde el código del agente
Imaginemos un helper que llamamos tras un paso 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);
}
El agente, en el momento oportuno, puede llamar a:
await createCheckpoint({
runId,
userId,
step: "filtered",
sessionState,
candidateIds,
});
Al recuperar, hacemos:
- Encontrar el último checkpoint por runId o userId.
- Restaurar session.state desde checkpoint.sessionState.
- Si hace falta, traer desde la BD datos frescos por candidateIds.
8. Dónde guardar técnicamente session, persistent y checkpoints
A nivel de infraestructura, normalmente tienes tres clases de almacenamiento:
- In‑memory — para dev/demos, memoria rápida pero temporal.
- Redis (u otro KV‑store) — para el estado de sesión.
- BD relacional/NoSQL — para datos persistentes y checkpoints.
Almacén in‑memory para desarrollo local
Para modo local de desarrollo basta con un simple almacén en memoria. Por ejemplo, un mini‑almacén con TTL para sesiones:
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;
}
Esto va perfecto para dev local, pero en producción con escalado horizontal (varias instancias) ya no funcionará.
Redis para el session‑state
En producción, el estado de sesión es cómodo guardarlo en Redis:
- lectura/escritura rápida;
- TTL «de fábrica»;
- accesible para todas las instancias del servicio.
Ejemplo en pseudo (simplificado):
// Envoltura alrededor del cliente de 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/u otra BD para persistent y checkpoints
El estado persistente y los checkpoints son ya entidades «serias», para las que importan transacciones, migraciones, índices y demás. Se suelen alojar en Postgres, MySQL, Firestore, etc.
El patrón arquitectónico aquí es sencillo:
- session en Redis con TTL;
- persistent y checkpoints en la BD sin TTL (o con una política de retención según negocio).
9. Higiene de la memoria: tamaños, privacidad, separación de responsabilidades
La memoria del agente no es simplemente «poner un objeto en algún sitio y listo». Hay varias reglas importantes que ahorran dinero y te mantienen la tranquilidad.
No meter todo en messages
El historial de mensajes es un recurso caro:
- su longitud influye mucho en el coste de la solicitud al modelo;
- normalmente hay mucho «ruido».
Por tanto:
- intenta extraer del historial los hechos hacia un estado estructurado lo antes posible;
- usa resúmenes (summarization) para las partes antiguas del historial;
- si guardas en los checkpoints el historial textual, hazlo separado de lo que se envía al modelo.
Privacidad y PII
Especialmente para escenarios de comercio, es importante no almacenar datos sensibles en lugares donde no deben acabar. Los documentos sobre arquitectura de memoria recalcan que no se deben conservar datos PII en messages o checkpoints sin limpieza.
Reglas prácticas:
- no pongas email/teléfono/dirección directamente en el session‑state si no es necesario para el trabajo del agente;
- en logs y checkpoints intenta escribir identificadores (userId, recipientProfileId) en lugar de strings en bruto;
- si necesitas transportar PII a lo largo de varios pasos, usa campos protegidos aparte en el almacenamiento persistente y en el state pasa solo la clave.
Separación de datos de negocio y log del diálogo
Un buen patrón es considerar el state como «memoria limpia», y los messages como «memoria sucia».
Es decir:
- las entidades de negocio (perfiles, pedidos, carritos) viven siempre en la BD;
- el state/los checkpoints contienen el mínimo necesario para recuperar el proceso;
- los logs/el historial del chat se almacenan aparte (por ejemplo, en un almacén vectorial) y se usan para analítica, pero no se mezclan en cada solicitud al modelo.
10. Mini‑práctica: ¿qué guardarías?
Para afianzar la diferencia entre capas de memoria, apartémonos un segundo de la teoría y pensemos en un caso concreto. No es necesario escribir código: basta con esbozar estructuras en papel o mentalmente.
Imagina que tu agente GiftGenius mantuvo el siguiente diálogo con el usuario:
- Usuario: «Necesito un regalo para un colega desarrollador, presupuesto hasta 50$, le gustan los juegos de mesa y la cafeína».
- Agente: hace un par de preguntas de aclaración.
- Usuario: «Odia las tazas y ya está lleno de cuadernos».
- Agente: genera una lista de 10 ideas, el usuario elige una, pero dice: «Volveré luego para terminar».
Piensa:
- ¿Qué pondrías en el session‑state (que puede expirar en 30 minutos)?
- ¿Qué iría al almacenamiento persistente para que el usuario pueda volver dentro de una semana?
- ¿Cómo se vería el checkpoint después de elegir la idea pero antes de formalizar el pedido?
Intenta esbozar los tipos de TypeScript y las funciones saveSessionState, savePersistentState, createGiftIdeaCheckpoint por analogía con los ejemplos de esta lección. Si quieres, puedes escribir esos tipos y funciones directamente en un editor siguiendo los ejemplos anteriores: será un buen mini‑checkpoint antes de la próxima lección.
11. Errores típicos al trabajar con la memoria del agente
Error n.º 1: intentar almacenar todo solo en el historial de mensajes.
El desarrollador se alegra: «Pues el modelo ya ve todo el diálogo, ¿para qué inventar otro state?». Como resultado, tras unas decenas de mensajes, la ventana de contexto se llena de basura, los tokens cuestan como un MacBook nuevo y el comportamiento del agente se vuelve inestable: simplemente no ve hechos antiguos importantes. Este problema hay que resolverlo con la separación explícita de session‑state y almacenamiento persistente, no aumentando límites.
Error n.º 2: mezclar session y persistent en un solo objeto.
A veces es tentador crear una «gran» entidad AgentState, meterle de todo y guardarla «tal cual» en la base de datos. Entonces se difumina la frontera entre datos temporales de una conversación concreta y datos a largo plazo del usuario. Empiezan historias del tipo «tras el despliegue, todas las sesiones se restauraron misteriosamente con datos del año pasado» o «la sesión de un usuario cogió por error el perfil persistente de otro». Separa los niveles conscientemente.
Error n.º 3: guardar demasiado en los checkpoints.
Un fallo habitual es escribir en el checkpoint todo el JSON de la respuesta de herramientas, todo el historial del diálogo, datos en bruto de integraciones y demás. Tras un par de semanas en producción, la base de checkpoints se infla indecentemente, las copias de seguridad tardan una hora y las consultas a la BD se ralentizan. En el checkpoint deben vivir solo los hechos realmente necesarios para continuar el proceso, más un mínimo de metadatos.
Error n.º 4: olvidar el TTL y la limpieza del session‑state.
Si los estados de sesión no tienen fecha de caducidad, cualquier experimento casual de un usuario en Dev Mode se queda en Redis para siempre. A los pocos meses, miras el monitoreo y ves una montaña de sesiones «olvidadas» consumiendo memoria. El nivel de session hay que diseñarlo con TTL explícito, y el nivel persistente con una política de retención bien pensada.
Error n.º 5: guardar PII en el state y en los checkpoints sin necesidad.
Especialmente peligroso cuando en el session‑state se mete sin pensar email, dirección, número de tarjeta, y luego ese objeto se serializa en logs, se va a analítica y a los checkpoints. Esto crea riesgos serios desde el punto de vista regulatorio y de seguridad. Mejor guarda identificadores seguros y, si hace falta, resuélvelos a datos reales mediante herramientas protegidas aparte.
Error n.º 6: falta de estrategia de recuperación desde checkpoints.
Algunos equipos registran checkpoints honestamente, pero no piensan cómo debe recuperarse el agente desde ellos. Al final, cuando «algo va mal», los desarrolladores miran una tabla con JSON bonitos, pero no tienen código que sepa reconstruir la ejecución a partir de ellos. Hacer checkpointing sin un guion de recuperación es solo un log caro, no una herramienta de fiabilidad.
Error n.º 7: acoplar rígidamente el agente a una implementación concreta de almacenamiento.
Si el código del agente va directamente a Redis/Postgres, es más difícil migrarlo, probarlo y evolucionarlo. Al cambiar la arquitectura (por ejemplo, si aparecen recursos MCP o un servicio de estado aparte), habrá que rehacer la lógica del agente. Es mucho mejor cuando el agente solo ve las abstracciones Session y un conjunto de tools, y son las herramientas las que saben dónde están exactamente los datos.
GO TO FULL VERSION