Deterministic memories

Copy as Markdown

In the previous post we gave the agent a memory by calling add_session_to_memory() in an after-agent callback. That works, but it leans on an embedding model to decide what’s worth keeping from each session — which makes it a little non-deterministic. Say “remember that …” and it almost certainly will; let it summarize a trading run on its own, and the specific trades may or may not survive.

For a trading agent, the trades are exactly the thing we want to remember, every single time. So rather than hope the model picks them out, let’s write that memory ourselves.

There are two pieces to saving a memory deterministically.

First, capture what happened — in a structured way, at the moment it happens. The trade is a tool call, so place_trade_order is the natural place to record it: we stash each confirmed fill in the tool context’s session state as the order goes through.

        # Record the confirmed fill in session state so save_to_memory can
        # persist a clean, structured trade record at the end of the cycle.
        if tool_context is not None:
            trades = tool_context.state.get("trades_this_session", [])
            trades.append({
                "symbol": symbol,
                "side": side,
                "amount_usd": amount_usd,
                "qty": qty,
                "reason": reason,
            })
            tool_context.state["trades_this_session"] = trades

Second, when the session’s memory save fires, do the embedding-based review and write our own memory for the trades. The full transcript still goes to memory for fuzzy cross-session recall — news context, the agent’s reasoning — but on top of that we build a clean, structured record from the fills we captured and save it directly with add_memory(). There’s no LLM in that second path, so the trade record lands every time.

async def save_to_memory(callback_context: CallbackContext):
    # Full transcript: general cross-session recall (news context, reasoning).
    await callback_context.add_events_to_memory(
        events=callback_context.session.events,
        custom_metadata={"force_flush": True},
    )

    # Structured trade record: a clean, guaranteed memory of exactly what was
    # traded and why, built from the orders captured in place_trade_order.
    trades = callback_context.state.get("trades_this_session", [])
    if not trades:
        return

    lines = []
    for t in trades:
        size = f"${t['amount_usd']}" if t.get("amount_usd") else f"{t['qty']} shares"
        rationale = f" — {t['reason']}" if t.get("reason") else ""
        lines.append(f"{t['side'].upper()} {t['symbol']} {size}{rationale}")
    text = "Trades executed this session:\n" + "\n".join(lines)
    print(text, flush=True)

    await callback_context.add_memory(
        memories=[MemoryEntry(
            content=types.Content(role="model", parts=[types.Part(text=text)]),
            author="TradingAgent",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )],
        custom_metadata={"type": "trade", "force_flush": True},
    )

Two things are worth calling out in there. We’ve swapped last post’s add_session_to_memory() for add_events_to_memory(), which lets us pass the events explicitly and force a flush — but under the hood it’s still the same embedding-based extraction, just handling the fuzzy half. And place_trade_order gains a reason argument, which the agent’s instructions now tell it to always pass: the ticker, its sentiment score, and a one-line news summary. That rationale rides along into the structured memory, so a future session can recall not just what we traded, but why.

The full diff to add this is as follows.

agent.py.diff
$ diff -u ../02_Memories/trading_agent/agent.py trading_agent/agent.py 
--- ../02_Memories/trading_agent/agent.py       2026-05-18 04:50:36.609821500 +0000
+++ trading_agent/agent.py      2026-05-30 22:03:31.981041603 +0000
@@ -1,7 +1,7 @@
 import os
 import sys
 import time
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from typing import Dict, Any, Optional
 from dotenv import load_dotenv
 
@@ -13,8 +13,11 @@
 
 from google.adk.agents import Agent
 from google.adk.agents.callback_context import CallbackContext
+from google.adk.memory.memory_entry import MemoryEntry
 from google.adk.tools.load_memory_tool import LoadMemoryTool
 from google.adk.tools.preload_memory_tool import PreloadMemoryTool
+from google.adk.tools.tool_context import ToolContext
+from google.genai import types
 from vertexai import agent_engines
 
 # Load environment variables
@@ -106,14 +109,23 @@
     quote = stock_client.get_stock_latest_quote(request_params)
     return float(quote[symbol].ask_price)
 
-def place_trade_order(symbol: str, side: str, amount_usd: Optional[float] = None, qty: Optional[float] = None) -> str:
-    """Places a market order (fractional shares supported for buys)."""
+def place_trade_order(symbol: str, side: str, amount_usd: Optional[float] = None, qty: Optional[float] = None, reason: str = "", tool_context: ToolContext = None) -> str:
+    """Places a market order (fractional shares supported for buys).
+
+    Args:
+        symbol: The ticker to trade.
+        side: 'buy' or 'sell'.
+        amount_usd: Dollar amount for a notional buy.
+        qty: Share quantity (required for sells).
+        reason: Short rationale for the trade (ticker, sentiment score, news summary).
+                Saved to memory so future sessions can recall why this trade was made.
+    """
     side = side.lower()
     if side not in ['buy', 'sell']:
         return "Error: Side must be 'buy' or 'sell'."
 
     order_side = OrderSide.BUY if side == 'buy' else OrderSide.SELL
-    
+
     try:
         if amount_usd is not None and side == 'buy':
             order_request = MarketOrderRequest(
@@ -134,16 +146,54 @@
 
         time.sleep(1)
         order = trading_client.submit_order(order_data=order_request)
+
+        # Record the confirmed fill in session state so save_to_memory can
+        # persist a clean, structured trade record at the end of the cycle.
+        if tool_context is not None:
+            trades = tool_context.state.get("trades_this_session", [])
+            trades.append({
+                "symbol": symbol,
+                "side": side,
+                "amount_usd": amount_usd,
+                "qty": qty,
+                "reason": reason,
+            })
+            tool_context.state["trades_this_session"] = trades
+
         return f"Success: {side.upper()} order placed for {symbol}. Order ID: {order.id}"
     except Exception as e:
         return f"Failed to place {side} order for {symbol}: {str(e)}"
 
 async def save_to_memory(callback_context: CallbackContext):
+    # Full transcript: general cross-session recall (news context, reasoning).
     await callback_context.add_events_to_memory(
         events=callback_context.session.events,
         custom_metadata={"force_flush": True},
     )
 
+    # Structured trade record: a clean, guaranteed memory of exactly what was
+    # traded and why, built from the orders captured in place_trade_order.
+    trades = callback_context.state.get("trades_this_session", [])
+    if not trades:
+        return
+
+    lines = []
+    for t in trades:
+        size = f"${t['amount_usd']}" if t.get("amount_usd") else f"{t['qty']} shares"
+        rationale = f" — {t['reason']}" if t.get("reason") else ""
+        lines.append(f"{t['side'].upper()} {t['symbol']} {size}{rationale}")
+    text = "Trades executed this session:\n" + "\n".join(lines)
+    print(text, flush=True)
+
+    await callback_context.add_memory(
+        memories=[MemoryEntry(
+            content=types.Content(role="model", parts=[types.Part(text=text)]),
+            author="TradingAgent",
+            timestamp=datetime.now(timezone.utc).isoformat(),
+        )],
+        custom_metadata={"type": "trade", "force_flush": True},
+    )
+
 # Define the Agent
 root_agent = Agent(
     name="TradingAgent",
@@ -172,6 +222,11 @@
        - Rank by positivity.
        - Create market BUY trades for EXACTLY $1000 of EACH of **up to 5** stocks with highest scores.
        - Stop if available cash is less than $1000.
+
+    IMPORTANT: Every time you call `place_trade_order` (buy or sell), pass the `reason`
+    argument with a concise rationale: the ticker, its sentiment score, and a one-line
+    news summary. This is saved to memory so future sessions can recall why each trade
+    was made.
     
     6. FINAL REPORT:
        Produce a comprehensive summary:

Now every trading run leaves behind two kinds of memory: the fuzzy transcript for the model to draw on, and a precise, structured record of exactly what we traded and why. Start a fresh session, ask “what did you recently trade?”, and it’ll answer from that record reliably — not just when the embedding model happened to hold on to it.

The tradeoff is that you’re now writing the memories that matter by hand. It’s a little more code, but for the things you can’t afford to lose — trades, decisions, anything you’ll want to audit later — deterministic beats best-effort.