All tutorials
Track 39·State & Control

Unstructured memory across sessions

Append free-text session notes to a plain text file. Each new instance reads the full diary as context. No schema, no keys, just text.

intermediate8 min
Video coming soon
Browse this tutorial's folder in tutorials-pygithub.com/OpenSymbolicAI/tutorials-py/tree/main/39-unstructured-memory

Not every piece of information fits a key. A debugging session, a meeting summary, a line of thought mid-project: these do not have a natural label. This tutorial stores session notes as free text: one line per session, appended to diary.txt. When the user asks what was discussed, the agent reads the whole file.

When structured memory is not enough#

Track 38 stored facts under named keys. That works well for preferences and settings. It breaks down when the information is narrative: "I narrowed the memory leak to the connection pool" does not have a clean key name. Forcing it into a key-value shape loses context.

A plain text diary sidesteps the schema problem. The agent writes a one-sentence summary of each session and appends it. The next session reads all the lines. The structure is just a list of notes, one per session.

The tradeoff: the file only grows. Track 38 keys overwrite; a diary only accumulates. This section covers the simple version. What to do when it gets long is discussed at the end.

The agent#

Three primitives: read, append, respond.

python
# diary_agent.py
class DiaryAgent(PlanExecute):

    @primitive(read_only=True)
    def read_diary(self) -> str:
        """Read all past session notes from diary.txt.

        Example: read_diary() -> "Debugging a memory leak in Flask app."
        """

    @primitive(read_only=False)
    def append_diary(self, entry: str) -> str:
        """Append one line to diary.txt.

        Keep the entry short: one sentence describing what the user said.
        Example: append_diary("Fixed Flask pool with pool_recycle=1800.") -> "Logged."
        """

    @primitive(read_only=True)
    def summarize(self, text: str) -> str:
        """Use an LLM to summarize diary entries into a short answer.

        Example: summarize("Debugging leak...\\nFixed pool...") -> "You were debugging..."
        """

    @primitive(read_only=True)
    def respond(self, message: str) -> str:
        """Return the message as the final response."""

When the user shares an update, the plan calls append_diary with a one-sentence summary and acknowledges with respond. When the user asks where they left off, the plan calls read_diary, passes the result to summarize (which makes an internal LLM call), and returns the summary.

Run it#

bash
uv run main.py

Three sessions on the same diary.txt:

text
============================================================
Session 1
------------------------------------------------------------
User: I'm debugging a memory leak in my Flask app. Narrowed it to the connection pool.

Plan:
  r = append_diary("Debugging a memory leak in Flask app; narrowed to the connection pool.")
  result = respond("Noted. I've logged that you're debugging a Flask memory leak.")

Agent: Noted. I've logged that you're debugging a Flask memory leak.

diary.txt after this session:
  Debugging a memory leak in Flask app; narrowed to the connection pool.

============================================================
Session 2
------------------------------------------------------------
User: Fixed the pool issue by setting pool_recycle=1800. Now testing the API endpoints.

Plan:
  r = append_diary("Fixed Flask connection pool with pool_recycle=1800; now testing API endpoints.")
  result = respond("Logged. Good progress on the Flask project!")

Agent: Logged. Good progress on the Flask project!

diary.txt after this session:
  Debugging a memory leak in Flask app; narrowed to the connection pool.
  Fixed Flask connection pool with pool_recycle=1800; now testing API endpoints.

============================================================
Session 3
------------------------------------------------------------
User: Where did I leave off on my Flask project?

Plan:
  diary = read_diary()
  summary = summarize(diary)
  result = respond(summary)

Agent: You were debugging a memory leak traced to the connection pool.
You fixed it with pool_recycle=1800 and moved on to testing the API endpoints.

What to notice#

The diary grows one line per session. append_diary always adds; it never removes or rewrites. After ten sessions the file has ten lines. After a hundred it has a hundred. The full file is read on every recall query, so context window pressure increases over time.

summarize makes an internal LLM call. Like select_relevant_keys in Track 38, this primitive calls self._llm.generate() directly. The plan passes raw diary text into summarize; the primitive converts it to a natural language answer. The plan itself only sees the string that comes back.

The agent compresses as it writes. append_diary is called with a one-sentence summary of the user's update, not the full original text. "I'm debugging a memory leak in my Flask app. Narrowed it to the connection pool. Been at it for two hours and tried a few approaches already." becomes one line. Each entry is pre-compressed before it is stored.

Where this breaks down. After many sessions the diary grows too long to fit in context or becomes noisy. The standard fixes are pruning (keep only the last N lines), rolling summarisation (collapse old entries into a paragraph), or retrieval (embed the lines and fetch only the most relevant). Those patterns add complexity. This tutorial shows the foundation they build on.

Structured vs unstructured at a glance. Track 38 keys overwrite: updating a fact stays the same file size. This diary only appends: every session adds a line. Use structured memory when information has a clear label; use the diary when it does not.