When the agent owns memory: harness, session, memory
New assumptions: the agent may write memory, and Postgres keeps only what is needed to run the agents. This lesson shows what still has to stay in Postgres, where a Thread lives now, and where Google's Memory Bank fits. About 15 minutes.
Warm-up: from memory, why did Lesson 1 recommend one agent session per job?
Generators never wrote memory, Postgres was the only source of truth, and no platform could reliably keep a session through an overnight Reaction. The first two premises are now withdrawn, and the third turned out to be wrong for ADK (see below). So the rule is open again.
What changed
- Agents may write memory. This reverses "generators never write memory" from ticket 07.
- Thread history may leave your schema. Postgres keeps only the harness: what your own code needs to run the agents.
- Correction to Lessons 1–2. The ADK docs list only an in-memory session service for TypeScript, but the published
@google/adk2.1.0 package includesDatabaseSessionService, which acceptspostgres://URIs, plusVertexAiSessionServiceandVertexAiMemoryBankService.[1] An ADK session can therefore live durably in your Supabase Postgres.
Three layers of state
| Layer | Holds | Who reads and writes it | Lives in |
|---|---|---|---|
| Harness | Anything your code must know before or without asking a model | Your code, deterministically | Postgres (thin) |
| Session | One Thread's transcript: Lessons, Replies, Reactions sent as events, tool calls | The agent runtime | ADK session table in Supabase, a Claude Managed Agents session, or a Gemini interaction chain |
| Long-term memory | What lasts across Threads: what was hard, preferences (Notes), Topic recaps | The agent (and, optionally, your code) | Memory Bank, Claude Managed Agents memory stores, or your own memory tool |
The harness test. Keep a fact in Postgres if your code needs it deterministically: to route a Telegram update, draw the right buttons, deduplicate, schedule, cap spend, or choose which Thread to start. A model's memory is probabilistic and eventually consistent. Button rows and idempotency can't depend on it.
For this bot the harness is roughly: Telegram message_id ↔ Thread/session and message kind (so a reply or a button tap finds its target), the Topic's latest Lesson (only its Simple/Proceed/Difficult taps take effect; a tap on an older Lesson gets a toast), update_id deduplication, jobs and runRefs, the published Backlog copy, which Topic is in progress (and whether it is finished or paused), and the usage and spend log. Everything else is a candidate to move.
A Thread can now be one session
The Thread is the session. A Reaction or Reply is appended as a new event and runs one turn, and nothing runs in between. The overnight gap is harmless because the session is stored, not held in a running process. Threads are never closed (ticket 12, 2026-09-17): older Threads still accept Replies, so a session must stay resumable indefinitely, and there is no "close" moment for writing memory. Write memories incrementally instead: after each turn from that turn's events (the quickstart recommends add_events_to_memory for incremental processing), or when a newer Thread starts. Two Lessons may be generated at once, but they belong to different sessions, and the Topic level stays a harness rule, so concurrent turns don't conflict.
| ADK 2.1 (TS) on Cloud Run | Claude Managed Agents | Gemini Antigravity | |
|---|---|---|---|
| Thread → session | DatabaseSessionService on Supabase Postgres; each event is one runner call | One session per Thread; it sits idle between events (runtime is billed on active time). How long an idle session stays resumable is unverified[2] | A previous_interaction_id chain plus environment_id; environments are kept 7 days after last activity[3], so a Reply to an older Thread may need a fresh interaction seeded from memory |
| Long-term memory | VertexAiMemoryBankService, or your own tool | Memory stores: files the agent reads and writes, versioned and redactable[4] | No memory service documented; you could call Memory Bank over REST |
| Keeps OpenRouter | For generation, yes. Memory Bank extraction runs on Gemini | No | No |
One cost to watch with session-per-Thread: the context grows with each turn, and every turn re-sends it. That is fine for a Thread of a few Lessons. Keep Threads short, and let /next start fresh.
Your question: is Memory Bank relevant?
Yes. It is the managed form of the "agent writes memory" layer. What it does, per Google's docs:[5]
- It extracts and consolidates memories from session events. New facts are merged into existing memories rather than appended, and you can steer extraction with custom topics and few-shot examples.
- It retrieves by similarity within a scope. The ADK quickstart uses
{user_id, app_name}.[6] - It keeps revisions and supports TTL, so you can see how a memory changed, and stale memories expire.
- It accepts direct writes (
CreateMemory) and event ingestion. A classified Note could be written directly instead of being extracted. - ADK integration:
PreloadMemoryToolloads memories every turn.LoadMemoryToolloads them when the model decides to, and a callback'sadd_session_to_memory()writes them. The agent can run on Cloud Run (--memory_service_uri=agentengine://ID). The quickstart shows only Python, but the TS package ships the service class.
How it maps onto your project
| Today (ticket 07) | With Memory Bank |
|---|---|
| Append-only raw events kept forever | Session events, plus memory revisions as the audit trail |
| Topic recap written on finish, pause or retire | Incremental add_events_to_memory after each turn, with a custom "difficulties / level" topic |
| Notes with Topic or all-Topics scope | Direct CreateMemory with a Note topic; scope carried in the memory |
| Fixed context for each call (last 30 Lesson lines…) | Similarity retrieval chosen by the agent |
psql readback for Strategy Sessions | Lost. The toolkit would read memories through the API instead |
What it costs you
- Money: storage at $0.30/GiB-month, and $0.085 per 3M reads or per 1M writes, billed since 2026-09-01. Gemini tokens for extraction and embeddings are billed separately.[7] For one learner that comes to cents.
- Setup: a GCP project with billing, and an Agent Platform (Agent Engine) instance whose id scopes the memories, even if the agent itself runs elsewhere.
- Your rules: extraction runs on Google's model, so OpenRouter-only no longer covers memory. Consolidation is non-deterministic, so "Answer outcomes never move the level" has to stay a harness rule in your code. Google also warns about memory poisoning, where false facts get stored. A learner's Reply is untrusted input.
Practice: which layer?
1. The Telegram message_id of the Topic's latest Lesson, so a Simple/Proceed/Difficult tap is either applied or answered with a toast.
2. The full text of the Thread's three Lessons and the learner's Instruction, needed for the next Follow-up.
3. "Struggles with idempotency; an analogy to insurance claims numbers helped", learned across several Threads.
4. Which Topic is in progress, so the 07:00 run knows what to start.
5. A pending Lesson generation's runRef and attempt count.
Take back to your tickets. (1) This reverses a decision in ticket 07 and is hard to undo once memory lives in a vendor store, so it deserves an ADR: "agents own session and memory; Postgres is the harness". (2) Ticket 10 (data model) shrinks to the harness tables. (3) Ticket 08 needs a new readback path, since Strategy Sessions would read Memory Bank or memory stores, not psql views. (4) The map's OpenRouter-only preference needs an exception for memory extraction if you pick Memory Bank.
Unsure where some piece of state belongs? Ask me, your teacher, with the concrete item, and we'll run the harness test on it together.