
Memoria en agentes de IA: sesión, persistencia y context window con Strands
Construimos un copiloto de código que analiza tu repo, pero es amnésico: cada turno arranca en blanco y cada ejecución olvida la anterior. Aquí le damos memoria con Strands y Amazon Bedrock, y descubrimos que recordar tiene una factura en tokens.
Series: Strands (3 articles)
- 3Memoria en agentes de IA: sesión, persistencia y context window con Strands This article
Le explicas tu repo a un agente de IA y te da un análisis útil: ubica el entrypoint, te dice qué mejorar, todo bien. Pero le haces una segunda pregunta y ya no recuerda nada de lo anterior, y cuando cierras la terminal y vuelves mañana, empiezas de cero otra vez.
Ese es el problema que resolvemos hoy. Vamos a usar como ejemplo un copiloto de código, un agente que analiza repositorios, que construimos en el artículo anterior de esta serie. Hacía su trabajo bien dentro de una sola llamada, pero sufría de amnesia: cada turno era el primero, cada ejecución era la primera. No necesitas haber leído esa parte para seguir esta; lo que importa es que es un agente que habla contigo y hoy le vamos a dar memoria.
La memoria es lo que convierte una serie de llamadas sueltas en una conversación. Sin ella, no tienes un copiloto, tienes una función que da respuestas y las olvida.
Pero hay un detalle que casi nadie menciona: recordar todo tampoco es la respuesta. El context window es finito, y cada token que arrastras del pasado lo re-envías al modelo en cada turno, así que lo pagas una y otra vez. Recordar no es gratis.
Este es el segundo artículo de la serie #PausaYConstruye, y el reto de hoy no es guardar mensajes. Es decidir qué recordar, dónde, por cuánto tiempo y a qué costo. Building in public, con el pie en alto, sobre el código de la semana pasada.
Vamos por partes.
Dos memorias, dos problemas
Cuando decimos "memoria" en un agente, casi siempre juntamos dos cosas que son distintas. Separarlas es el primer paso.
Memoria de corto plazo (sesión). Es el historial del hilo actual. Vive mientras dura la conversación y su límite es el context window del modelo. Es lo que hace que, dentro de una charla, no tengas que repetir lo que ya dijiste. "Ya te expliqué que el entrypoint es
cli.py" es corto plazo.Memoria de largo plazo (persistente). Es lo que sobrevive al cierre del proceso. Vive en disco o en una base de datos, y su límite es el almacenamiento y la relevancia. "La última vez que analizaste este repo, el health score fue 72" es largo plazo. Cierras la terminal, y mañana sigue ahí.
Son dos problemas con dos límites diferentes: el corto plazo choca con el context window, el largo plazo choca con el almacenamiento. Un buen sistema de memoria resuelve los dos, pero no los confunde.
Y hay una tercera cosa que no es memoria: el conocimiento indexado del repo. Que el agente "sepa" qué hay en cada archivo del proyecto no es memoria conversacional, es RAG, y eso llega en la Semana 3. Marco la frontera desde ya porque es fácil meterlo todo en el mismo saco. Hoy solo hablamos de recordar la conversación.


El session manager: la pieza central
Si tuviera que señalar una sola pieza nueva del Episodio 2, es esta: el session manager. Es el componente que mantiene el historial de la conversación y se lo entrega al agente en cada turno.
¿Dónde encaja en Strands? Cuando construyes un
Agent, le puedes pasar los mensajes previos de la conversación. El agente arranca su turno sembrado con ese historial, así que "recuerda" lo que se dijo antes. El session manager es quien decide qué historial entregar y cómo persistirlo.La decisión de diseño aquí es de responsabilidad única. Toda la lógica de memoria vive en un módulo nuevo,
memory.py, separado del wiring del agente. El módulo hace tres cosas y solo tres: persistir, recuperar y podar el historial. El agente no sabe cómo se guarda nada; solo recibe una lista de mensajes ya lista para usar.La llave de todo esto es el
session_id. Una sesión es un hilo de conversación recuperable. Si reusas el mismo session_id entre ejecuciones, retomas la conversación donde la dejaste. Si usas uno distinto, empiezas un hilo nuevo. Esa sola idea es lo que separa "amnesia" de "memoria persistente".Nota: Strands ya separa estas dos responsabilidades de fábrica, y vale la pena tenerlo claro. En el SDK, un session manager (FileSessionManager,S3SessionManager) se encarga de la persistencia, y un conversation manager (SlidingWindowConversationManager,SummarizingConversationManager) se encarga de la poda. Aquí las junto a propósito en un soloSessionManagercasero para ver las dos piezas trabajar juntas en el mismo flujo. Más adelante te muestro dónde encaja cada componente nativo.
El flujo de un turno completo con memoria se ve así:

Son cinco pasos que orquesta el
SessionManager: cargar el historial, podarlo para que quepa en el presupuesto de tokens, correr el agente, agregar el nuevo intercambio, y guardar.Persistencia: empieza en JSON, pero detrás de una interfaz
Aquí está la decisión de arquitectura más importante del episodio, y es fácil no verla porque el código se ve simple.
El backend de almacenamiento va detrás de una interfaz. No guardamos en JSON "porque sí": guardamos en JSON a través de un contrato pequeño y estable, para poder cambiar a SQLite, DynamoDB o Redis mañana sin tocar el agente.
Ese contrato es un
Protocol de Python con solo dos métodos:1
2
3
4
5
6
7
8
9
10
11
class MemoryStore(Protocol):
"""Storage backend for conversation history, keyed by session_id."""
def load(self, sessionId: str) -> list[Message]:
"""Return the stored history for sessionId (empty list if none)."""
...
def save(self, sessionId: str, messages: list[Message]) -> None:
"""Persist messages as the full history for sessionId."""
...Eso es todo.
load y save. Es el único punto donde el resto de la app toca el almacenamiento. Cualquier backend nuevo implementa estos dos métodos y nada más cambia.Nota: unProtocolen Python es una forma de definir una interfaz sin herencia. Una clase "cumple" el protocolo con solo tener los métodos correctos, sin declarar que lo implementa. El@runtime_checkablepermite verificarlo conisinstanceen los tests.
Nota: el tipoMessageque ves en las firmas viene del SDK (from strands.types.content import Message). Por debajo es undictcon la forma del Converse API de Bedrock:{"role": ..., "content": [...]}. Por eso más adelante armamos los mensajes como diccionarios a mano.
El único backend que envía este episodio es un archivo JSON:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class JsonFileStore:
"""JSON file-backed MemoryStore.
Persists each session's history under
<baseDir>/session_<session_id>/messages.json. Simple, inspectable,
zero infrastructure. The base directory is configurable, never hardcoded.
"""
def __init__(self, baseDir: str | Path) -> None:
self._baseDir = Path(baseDir).expanduser()
def load(self, sessionId: str) -> list[Message]:
# ...reads and deserializes the session's messages.json
...
def save(self, sessionId: str, messages: list[Message]) -> None:
# ...serializes and writes messages.json atomically
...¿Por qué JSON primero? Cero infraestructura, se inspecciona a mano (abres el archivo y ves la conversación), y es perfecto para el live y para los tests. No necesitas levantar una base de datos para ver si tu memoria funciona.
¿Y por qué la interfaz importa desde el día 1? Porque acoplarte a un backend es deuda técnica cara. El día de mañana el copiloto puede necesitar SQLite (transaccional, local), o DynamoDB y Redis (concurrentes, con expiración automática, para varias instancias). Con esta interfaz, ese cambio es un swap de una clase, no una reescritura del agente. La interfaz cuesta poco hoy y ahorra mucho después.
Aquí conviene ser honesta: Strands ya trae persistencia de fábrica. El SDK incluye un
FileSessionManager que guarda el historial en disco y un S3SessionManager para almacenamiento en la nube. No los estoy ignorando. La MemoryStore propia es una decisión de arquitectura, no un desconocimiento del SDK: quiero que el punto de escalamiento sea mío y quede explícito en el código, sin depender de qué backends venga a soportar el session manager nativo. Es el mismo principio del Episodio 1 al centralizar el modelo en model.py: la pieza que va a cambiar vive detrás de un límite que yo controlo. Cuando el copiloto no necesite ese control fino, el FileSessionManager de Strands es la opción más corta y la recomendada.Y hay un tercer camino que vale la pena que conozcas, porque es la salida "nativa" para backends que Strands no trae listos (SQLite, Supabase, Redis). El SDK expone un
SessionRepository, la interfaz base sobre la que están construidos FileSessionManager y S3SessionManager. Si implementas esa interfaz para tu backend, en vez de una propia, tu almacenamiento se enchufa directo al Agent con session_manager=... y aprovecha lo que el framework ya resuelve: la serialización de mensajes, el ciclo de vida de la sesión, y las funciones nuevas que Strands agregue después. Entonces, ¿cuándo cada uno?| Camino | Cuándo conviene | El precio |
|---|---|---|
MemoryStore propia (load / save) | Aprender el mecanismo, control total, cero acoplamiento al SDK | Manejas el flujo de sesión a mano; no heredas las funciones nuevas del framework |
Implementar SessionRepository de Strands | Producto de largo plazo sobre Strands con un backend custom (Supabase, SQLite) | Atas tu backend al contrato del SDK, que es más grande y puede cambiar |
En esta serie voy por la interfaz propia a propósito: es transparente y demuestra el punto de escalamiento sin arrastrar el peso del
SessionRepository completo. Si tu copiloto va a ser un servicio real con usuarios, ahí sí evaluaría el SessionRepository para no quedar fuera del ciclo del framework. Ninguno es incorrecto: uno controla tu límite, el otro se apoya en el del framework.Un par de detalles del
JsonFileStore que no dejé al azar. La escritura es atómica: escribe a un archivo temporal y luego hace rename, así un crash a mitad de escritura no deja un historial a medio guardar. Y el session_id se valida contra .. y separadores de ruta, porque un id de sesión que viene de afuera no debería poder escribir fuera de su carpeta.¿Y cómo llega este
MemoryStore hasta el agente? No lo toca directamente. El SessionManager que vimos arriba es quien lo usa: llama a load al inicio del turno para sembrar al agente con el historial, y a save al final para persistir el intercambio. El agente nunca ve el backend; solo recibe la lista de mensajes ya lista. El wiring completo lo armamos en la sección de integración, más abajo.
Poda del context window: el corazón del episodio
Llegamos a la parte que separa un buffer de mensajes de un sistema de memoria de verdad.
El problema es simple de ver: el historial crece sin límite, el context window no. Y como cada token del historial se re-envía al modelo en cada turno, el costo y la latencia crecen con la conversación. Una charla de 30 turnos arrastra 30 turnos de historial en el turno 31.
Podar el context window no es opcional en producción, así que la pregunta no es si lo haces, sino cómo. Hay dos estrategias, y elegir entre ellas es un trade-off directo.
Truncado (ventana deslizante). Conservas los mensajes más recientes que caben en el presupuesto de tokens y descartas los viejos. Es barato, no cuesta ninguna llamada extra al modelo, pero pierde el contexto antiguo de golpe.
Resumen. Condensas los mensajes viejos con el modelo, guardas ese resumen como un solo mensaje, y conservas la cola reciente. Preserva el contexto, pero cuesta una llamada extra al modelo y puede perder detalle.
La decisión clave es exponer esto como parámetro configurable, no como un valor fijo en el código. La estrategia y el umbral de tokens se inyectan desde la configuración:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def prune(
messages: list[Message],
*,
strategy: PruneStrategy = "truncate",
maxTokens: int = 4000,
summarizer: Summarizer | None = None,
preserveRecentMessages: int = 2,
) -> list[Message]:
"""Prune conversation history so it fits within a token budget."""
tokensBefore = countTokens(messages)
if tokensBefore <= maxTokens or len(messages) <= 1:
# Already within budget: no pruning, no cost.
return list(messages)
if strategy == "truncate":
pruned = _pruneTruncate(messages, maxTokens=maxTokens)
else: # "summarize"
if summarizer is None:
raise ValueError("The 'summarize' strategy requires a summarizer callable.")
pruned = _pruneSummarize(
messages, maxTokens=maxTokens,
summarizer=summarizer, preserveRecentMessages=preserveRecentMessages,
)
tokensAfter = countTokens(pruned)
logger.info(
"memory.prune strategy=%s tokens_before=%d tokens_after=%d saved=%d",
strategy, tokensBefore, tokensAfter, tokensBefore - tokensAfter,
)
return prunedFíjate en tres cosas. Primero: si el historial ya cabe en el presupuesto, no se poda nada y no se gasta nada. La poda solo actúa cuando hace falta. Segundo: el
summarizer se inyecta en vez de importar el modelo directamente. Eso mantiene prune testeable sin Bedrock (le pasas un resumidor falso en los tests) y honra la regla de "configurable, no hardcodeado". Tercero: cada evento de poda se loguea con los tokens antes y después. Eso no es decoración, es lo que te deja ver el costo en la demo.El resumen tiene un cuidado extra: siempre preserva los mensajes más recientes intactos. No queremos que el último intercambio se resuma justo cuando el usuario lo necesita:
1
2
3
4
5
6
7
def _pruneSummarize(messages, *, maxTokens, summarizer, preserveRecentMessages):
"""Summarize the oldest messages, keep the recent tail intact."""
olderMessages = messages[:-preserveCount]
recentMessages = messages[-preserveCount:]
summaryText = summarizer(olderMessages).strip()
summary = _summaryMessage(summaryText)
return [summary, *recentMessages]Los mensajes viejos se colapsan en un solo resumen que va al frente; la cola reciente sobrevive tal cual.
¿Cuál eliges? El criterio es concreto. Trunca cuando el pasado lejano ya no importa, por ejemplo un soporte transaccional donde cada consulta es independiente. Resume cuando el hilo tiene continuidad, como una investigación larga sobre el mismo repo donde lo que dijiste hace 20 turnos todavía cuenta.

Nota: Un detalle del token counting que vale aclarar: no llamamos a un tokenizador real. Usamos la heurística de ~4 caracteres por token, que es barata y sin dependencias. No sirve para facturar con precisión, pero sirve perfecto para decidir cuándo podar y para loguear el costo relativo. Que es justo para lo que lo usamos.
Eso sí, sé honesta con una limitación: en código la heurística se desajusta. Los tokens del código suelen ser más cortos que los del texto normal, así que ~4 chars/token puede subestimar y hacer que podes tarde. Como este copiloto analiza repos, tenlo en el radar: si necesitas precisión, cambia la heurística por
tiktoken o por el conteo de tokens nativo de Bedrock. Curiosamente el propio Strands usa una idea parecida por default (una heurística basada en caracteres) y permite activar conteo exacto por proveedor, aunque ese detalle interno puede cambiar entre versiones.Y aquí cierro el círculo de lo que prometí en la sección del session manager. Estas dos estrategias no las inventé: son exactamente los dos conversation managers que Strands trae de fábrica. El truncado es el
SlidingWindowConversationManager (de hecho es el default del SDK), y el resumen es el SummarizingConversationManager, que expone un summary_ratio y un preserve_recent_messages, el mismo parámetro que usamos aquí. Strands hasta ofrece un context_manager="auto" en el Agent, que activa un SummarizingConversationManager con compresión proactiva más offloading, todo en una sola línea. Entonces, ¿por qué las escribimos a mano? Porque el objetivo aquí no es tapar el mecanismo, es verlo funcionar. Cuando entiendes qué hace prune por dentro, usar el componente nativo deja de ser magia y pasa a ser una decisión informada.El costo de recordar: pongámosle números
Hasta aquí hablé del costo en abstracto. Bajémoslo a números, porque es donde la memoria deja de ser un concepto y se vuelve una línea en tu factura.
La ecuación honesta de lo que pagas por turno es:
1
tokens_por_turno ≈ system_prompt + historial (crece) + turno_actualEl
system_prompt es fijo. El turno actual es más o menos constante. Pero el historial crece, y como se re-envía completo cada vez, sin poda el costo por turno sube de forma lineal con la conversación.Veamos qué pasa en una conversación de 20 turnos, comparando las tres opciones:
| Estrategia | Tokens del historial en el turno 20 | Forma de la curva | Costo extra de la estrategia |
|---|---|---|---|
| Sin poda | Todo el historial acumulado (crece y crece) | Lineal ascendente | Ninguno, pero pagas todo cada turno |
| Truncado | Fijo en el umbral (los últimos N mensajes) | Plana desde que llenas el umbral | Ninguno (no llama al modelo) |
| Resumen | Umbral + un resumen compacto del pasado | Plana, pero conserva contexto viejo | Una llamada al modelo por cada evento de resumen |
Nota: los valores exactos dependen de tu modelo, tu umbral y tus mensajes. Corre la demo con tu propio repo para ver tus números; la tabla ilustra la forma de cada curva, que es lo que importa para decidir.
Lo interesante es la forma. El truncado y el resumen aplanan la curva: dejas de pagar más por turno solo porque la conversación creció. La diferencia entre ellos, que ya vimos, es qué pagas por aplanarla.

¿Y cuándo vale la pena la llamada extra del resumen? Cuando el costo de perder contexto viejo es mayor que el de esa llamada. En una investigación larga donde el agente necesita recordar decisiones de hace 15 turnos, resumir sale barato comparado con que el agente se contradiga o repita trabajo.
Y esto se conecta con el Episodio 1. ¿Recuerdas el estado compartido, esa decisión de no pasar objetos gigantes entre tools? Esa disciplina ya te ahorraba tokens dentro de una corrida. La poda hace lo mismo entre turnos. Argumentos de tool grandes más historial grande se suman; controlar ambos es higiene de costo, no un lujo.
Integración: memoria dentro del copiloto
Con las piezas listas, integrarlas es poco código. La configuración se lee del entorno, así nada queda hardcodeado:
1
2
3
4
5
6
7
8
class MemoryConfig:
"""Resolved memory configuration for one CLI run."""
backend: str
directory: str
strategy: PruneStrategy
maxTokens: int
preserveRecentMessages: intBackend, directorio, estrategia, umbral y cuántos mensajes recientes proteger. Todo viene de variables de entorno (
MEMORY_BACKEND, MEMORY_STRATEGY, MEMORY_MAX_TOKENS…) con defaults razonables para que el copiloto funcione sin configurar nada.El session manager se arma desde esa config, y aquí hay un detalle que vale la pena notar: el resumidor (que es una llamada a Bedrock) solo se conecta cuando la estrategia es
summarize. Un run con truncate nunca toca el modelo para podar:1
2
3
4
5
6
7
8
9
10
11
def buildSessionManager(config: MemoryConfig):
"""Construct a configured SessionManager from a MemoryConfig."""
store = buildStore(config.backend, baseDir=config.directory)
summarizer = _bedrockSummarizer if config.strategy == "summarize" else None
return SessionManager(
store,
strategy=config.strategy,
maxTokens=config.maxTokens,
summarizer=summarizer,
preserveRecentMessages=config.preserveRecentMessages,
)Y el turno completo, en el CLI, sigue exactamente el flujo que dibujamos: cargar el historial por
session_id, sembrar el agente con él, correr, y persistir al final.1
2
3
4
5
6
7
8
9
10
# Long-term memory: recover prior conversation for this session_id.
priorMessages = sessionManager.loadHistory(sessionId)
agent = buildAgent(priorMessages=priorMessages)
agentResult = agent(userPromptText)
# End of turn: append the new exchange and persist.
userMessage = {"role": "user", "content": [{"text": userPromptText}]}
assistantMessage = {"role": "assistant", "content": [{"text": str(agentResult)}]}
sessionManager.appendExchange(sessionId, userMessage, assistantMessage)Nota:buildAgentes nuestro propio factory del Episodio 1, no una API de Strands. Por dentro le pasa el historial alAgentnativo con su parámetromessages=, que es el que precarga la conversación. UsamospriorMessagescomo nombre en el wrapper para que el flujo se lea claro; elAgentde Strands no acepta ese nombre directamente.
Hay una decisión sutil en el
SessionManager que vale la pena resaltar: la poda se aplica en la lectura, no en la escritura. El historial se persiste completo; lo que se poda es la vista que recibe el modelo. Así la memoria de largo plazo queda intacta en disco (puedes resumir de nuevo, o cambiar de estrategia después) mientras el modelo solo ve lo que cabe en su presupuesto. Guardas todo, envías lo justo.Y mantenemos el alcance: la memoria guarda la conversación, no vuelve a analizar el repo en cada turno. De eso ya se encargan las tools y el estado compartido del Episodio 1, que ahora también persiste entre corridas junto al historial.
Evaluación: probar que de verdad recuerda
La memoria introduce un modo de fallo que antes no existía: que el agente contradiga o ignore algo que ya se dijo. Un copiloto amnésico no puede contradecirse porque no recuerda; uno con memoria sí. Así que hay que evaluarlo.
La buena noticia es que reusamos la infraestructura de evaluación del Episodio 1, y los casos de memoria son deterministas si el backend es local. No dependen de Bedrock.
El caso central es multi-turno: el turno 1 fija un hecho, y un turno posterior lo interroga sin repetirlo. Si el agente recuerda, el hecho sigue ahí después de cruzar el límite del proceso:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test_multiTurn_recallAcrossRuns(tmp_path):
# Turn 1 (run 1): a fact is stated and persisted.
manager1 = SessionManager(JsonFileStore(tmp_path), strategy="truncate", maxTokens=10_000)
manager1.appendExchange(
"proj",
_msg("user", "The entrypoint is cli.py."),
_msg("assistant", "Understood, the entrypoint is cli.py."),
)
# Turn 2 (a fresh manager, simulating a new run): the fact must be
# recoverable without the user repeating it.
manager2 = SessionManager(JsonFileStore(tmp_path), strategy="truncate", maxTokens=10_000)
history = manager2.loadHistory("proj")
allText = " ".join(
block["text"] for message in history
for block in message["content"] if "text" in block
)
assert "cli.py" in allTextTambién probamos la poda, y aquí es donde sirve tener un resumidor que se puede inyectar. Con un resumidor falso y determinista verificamos dos cosas: que la información reciente y crítica sobrevive, y que el resumen conserva los hechos clave:
1
2
3
4
5
6
7
8
9
10
11
12
def test_prune_summarize_preservesKeyFactsAndRecent():
def fakeSummarizer(oldMessages):
# A faithful summary preserves the key facts.
return "Key facts: entrypoint is cli.py; database is postgres."
pruned = prune(messages, strategy="summarize", maxTokens=120,
summarizer=fakeSummarizer, preserveRecentMessages=2)
# The summary preserves the critical fact...
assert "cli.py" in pruned[0]["content"][0]["text"]
# ...and the most recent messages survive intact.
assert pruned[-2:] == recentTodo esto corre sin llamar a Bedrock, sin flakiness (esas pruebas que a veces pasan y a veces fallan sin que cambie el código) y en cada commit. Esa disciplina es la semilla de la Semana 7, donde la evaluación crece a escala, pero arranca aquí, con casos multi-turno que un simple
assert puede verificar.La demo: el copiloto recordando entre sesiones
La prueba real es correrlo en dos procesos distintos con el mismo
session_id.Sesión A. Analizas un repo y estableces contexto. Cierras el proceso.
1
code-copilot https://github.com/octocat/Hello-World --session-id demo-ep2Sesión B. Proceso nuevo, misma
session_id. El copiloto retoma sin que repitas nada:1
code-copilot https://github.com/octocat/Hello-World --session-id demo-ep2Al arrancar, el CLI te dice qué recuperó:
1
2
3
Code Copilot — agent mode (Amazon Bedrock Nova Pro)
Session: demo-ep2 | backend=json | strategy=truncate | max_tokens=4000
Recovered 4 message(s) from prior session.Y para ver la poda en acción, fuerzas el crecimiento del historial hasta pasar el umbral. El log te muestra el evento con los tokens antes y después:
1
INFO code_copilot.memory: memory.prune strategy=truncate tokens_before=5200 tokens_after=3980 saved=1220Ahí ves, en números, el efecto de la poda sobre lo que el siguiente turno le va a costar al modelo. Cambia
MEMORY_STRATEGY=summarize y corre lo mismo: la curva de tokens cambia de forma, a cambio de esa llamada extra.Lo que quiero que te lleves
Este episodio no trata de guardar mensajes en un archivo. Trata de tratar la memoria como una decisión de arquitectura, con sus trade-offs a la vista:
- Corto plazo (sesión) y largo plazo (persistente) son dos problemas distintos, cada uno con su límite: el context window contra el almacenamiento.
- El session manager entrega el historial al agente cada turno; sepáralo del wiring en su propio módulo.
- Persiste detrás de una interfaz (
MemoryStore): JSON hoy, DB mañana, sin tocar el agente. - Podar el context window no es opcional en producción: truncado (barato, pierde pasado) contra resumen (preserva, cuesta una llamada).
- Recordar no es gratis: sin poda el costo por turno crece linealmente; truncar y resumir aplanan la curva, cada uno con su precio.
- La memoria abre un modo de fallo nuevo (contradecirse), así que evalúala con casos multi-turno deterministas.
- Poda en la lectura, no en la escritura: guarda el historial completo, envía al modelo solo lo que cabe.
- Strands ya trae estas piezas (
FileSessionManager,SlidingWindowConversationManager,SummarizingConversationManager); las construimos a mano para entender el mecanismo, no por reemplazarlas. En producción, arranca con las nativas y saca tu propia interfaz solo cuando necesites ese control.
Cada decisión aquí es el gancho de una capa que viene después. Y por eso el copiloto ya no es un demo que responde y olvida, sino un sistema que recuerda con criterio.
Qué sigue
En la Semana 3 el copiloto deja de recordar solo la conversación y aprende a entender todo el repo: llega RAG, y con él la distinción entre memoria conversacional (lo de hoy) y conocimiento del repositorio (lo que viene). También lo vamos a exponer como servicio.
Mientras tanto, el ejercicio, sobre el copiloto con memoria:
- Implementa un segundo backend de
MemoryStore(SQLite) y actívalo por configuración sin tocarcli.py. Si la interfaz está bien diseñada, no deberías necesitar cambiar nada más. - Corre una conversación de ~15 turnos con
MEMORY_STRATEGY=truncatey otra consummarize. Mide los tokens acumulados y compáralos. ¿Cuánto aplanó cada una la curva? - Escribe un caso de eval multi-turno donde el turno 1 fija un hecho y un turno posterior, ya pasada la poda, lo interroga. ¿Sobrevivió el hecho? ¿Con qué estrategia y por qué?
- Repo: clona
Ep.2 - Un copiloto que recuerda/en github.com/hsaenzG/Copiloto-de-Codigo y corre dos sesiones con la mismasession_id. - Video del live: youtube.com/live/ZVgf1NWOCE4
- Docs de Strands: persistencia de sesión con session managers y conversation managers (truncado y resumen) .
- Anterior: Semana 1 — Un copiloto de código de verdad (orquestación multi-tool y evaluación).
¿Te resultó útil? Compártelo con tu equipo o cuéntame qué estrategia de poda usarías en tu caso. Y si ya estás construyendo agentes con memoria, me encantaría saber cómo estás resolviendo la persistencia y el costo de arrastrar contexto.
Series: Strands (3 articles)
- 3Memoria en agentes de IA: sesión, persistencia y context window con Strands This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article