Agent memories with ADK

Copy as Markdown

The Trading agent we deployed earlier will automatically have session persistence when deployed on Agent Platform. You can test this by asking it a day later “What trades did you execute” from the same session that you executed the trades from. The full conversation history is preserved, even while the agent is suspended (you’re not paying for it to run continuously).

You can also inspect the saved session data from the console.

https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/memory-bank/setup

But if you start a new session, and ask the agent what it did yesterday, it won’t know.

> what did you trade yesterday?

I am a trading agent designed to execute trades based on the current news cycle. I do not have a memory of past trading days. Would you like me to run today's trading cycle?
async def save_to_memory(callback_context: CallbackContext):
    await callback_context.add_session_to_memory()

# to the agent call:
    tools=[get_financial_news, get_portfolio_status, get_latest_price, place_trade_order, PreloadMemoryTool()],
    after_agent_callback=save_to_memory,
View the full change to agents.py
import os
import time
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from dotenv import load_dotenv

from alpaca.data.historical import NewsClient, StockHistoricalDataClient
from alpaca.data.requests import NewsRequest, StockLatestQuoteRequest
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import MarketOrderRequest
from alpaca.trading.enums import OrderSide, TimeInForce

from google.adk.agents import Agent
from google.adk.agents.callback_context import CallbackContext
from google.adk.tools.preload_memory_tool import PreloadMemoryTool
from vertexai import agent_engines

# Load environment variables
env_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(env_path)

# API Keys
APCA_API_KEY_ID = os.getenv('APCA_API_KEY_ID')
APCA_API_SECRET_KEY = os.getenv('APCA_API_SECRET_KEY')

# Initialize Alpaca Clients
trading_client = TradingClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY, paper=True)
news_client = NewsClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)
stock_client = StockHistoricalDataClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)

# Full S&P 500 Tickers List (Retrieved April 2026)
SP500_TICKERS = [
"A", "AAPL", "ABBV", "ABT", "ACGL", "ACN", "ADBE", "ADI", "ADM", "ADP", "ADSK", "AEE", "AEP", "AES", "AFL", "AIG", "AIZ", "AJG", "AKAM", "ALB", "ALGN", "ALL", "ALLE", "AMAT", "AMCR", "AMD", "AME", "AMGN", "AMP", "AMT", "AMZN", "ANET", "AON", "AOS", "APA", "APD", "APH", "APO", "APP", "APTV", "ARE", "ATO", "AVB", "AVGO", "AVY", "AWK", "AXON", "AXP", "AZO", "BA", "BAC", "BALL", "BAX", "BBY", "BDX", "BEN", "BF.B", "BG", "BIIB", "BK", "BKNG", "BKR", "BLDR", "BLK", "BMY", "BR", "BRO", "BRK.B", "BSX", "BWA", "BX", "BXP", "C", "CAG", "CAH", "CARR", "CAT", "CB", "CBOE", "CBRE", "CCI", "CCL", "CDNS", "CDW", "CEG", "CF", "CFG", "CHD", "CHRW", "CHTR", "CI", "CINF", "CL", "CLX", "CMA", "CMCSA", "CME", "CMG", "CMI", "CMS", "CNC", "CNP", "COF", "COO", "COP", "COR", "COST", "CPAY", "CPB", "CPRT", "CPT", "CRL", "CRM", "CRWD", "CSCO", "CSGP", "CSX", "CTAS", "CTSH", "CTVA", "CVS", "CVX", "CZR", "D", "DAL", "DAY", "DD", "DE", "DECK", "DELL", "DFS", "DG", "DGX", "DHI", "DHR", "DIS", "DLR", "DLTR", "DOC", "DOV", "DOW", "DPZ", "DRI", "DTE", "DUK", "DVA", "DVN", "DXCM", "EA", "EBAY", "ECL", "ED", "EFX", "EG", "EIX", "EL", "ELV", "EMN", "EMR", "ENPH", "EOG", "EPAM", "EQT", "EQR", "ERIE", "ES", "ESS", "ETN", "ETR", "EVRG", "EW", "EXC", "EXPD", "EXPE", "EXR", "F", "FANG", "FAST", "FCX", "FDS", "FDX", "FE", "FFIV", "FI", "FICO", "FIS", "FITB", "FMC", "FOX", "FOXA", "FRT", "FSLR", "FTNT", "FTV", "GD", "GDDY", "GE", "GEHC", "GEN", "GEV", "GILD", "GIS", "GL", "GLW", "GM", "GNRC", "GOOG", "GOOGL", "GPC", "GPN", "GRMN", "GS", "GWW", "HAL", "HAS", "HBAN", "HCA", "HD", "HES", "HIG", "HII", "HLT", "HOLX", "HON", "HPE", "HPQ", "HRL", "HSIC", "HST", "HSY", "HUBB", "HUM", "HWM", "IBM", "ICE", "IDXX", "IEX", "IFF", "ILMN", "INCY", "INTC", "INTU", "INVH", "IP", "IPG", "IQV", "IR", "IRM", "ISRG", "IT", "ITW", "IVZ", "J", "JBHT", "JCI", "JKHY", "JNJ", "JPM", "K", "KDP", "KEY", "KEYS", "KHC", "KIM", "KLAC", "KMB", "KMI", "KMX", "KO", "KR", "KVUE", "L", "LDOS", "LEN", "LH", "LHX", "LII", "LIN", "LKQ", "LLY", "LMT", "LNT", "LOW", "LRCX", "LULU", "LUV", "LVS", "LW", "LYB", "LYV", "MA", "MAA", "MAR", "MAS", "MCD", "MCHP", "MCK", "MCO", "MDLZ", "MDT", "MET", "META", "MGM", "MHK", "MKC", "MLM", "MMC", "MMM", "MNST", "MO", "MOH", "MOS", "MPC", "MPWR", "MRK", "MRNA", "MS", "MSCI", "MSFT", "MSI", "MTB", "MTD", "MU", "NDAQ", "NDSN", "NEE", "NEM", "NFLX", "NI", "NKE", "NOC", "NOW", "NRG", "NSC", "NTAP", "NTRS", "NUE", "NVDA", "NVR", "NWS", "NWSA", "NXPI", "O", "ODFL", "OKE", "OMC", "ON", "ORCL", "ORLY", "OTIS", "OXY", "PANW", "PARA", "PAYC", "PAYX", "PCAR", "PCG", "PEAK", "PEG", "PEP", "PFE", "PFG", "PG", "PGR", "PH", "PHM", "PKG", "PLD", "PLTR", "PM", "PNC", "PNR", "PNW", "PODD", "POOL", "PPG", "PPL", "PRU", "PSA", "PSX", "PTC", "PWR", "PYPL", "QCOM", "QRVO", "RCL", "REG", "REGN", "RF", "RHI", "RJF", "RL", "RMD", "ROK", "ROL", "ROP", "ROST", "RSG", "RTX", "RVTY", "SBAC", "SBUX", "SCHW", "SHW", "SJM", "SLB", "SMCI", "SNA", "SNPS", "SO", "SOLV", "SPG", "SPGI", "SRE", "STE", "STLD", "STT", "STX", "STZ", "SWK", "SWKS", "SYF", "SYK", "SYY", "T", "TAP", "TDG", "TDY", "TECH", "TEL", "TER", "TFC", "TFX", "TGT", "TJX", "TKO", "TMUS", "TMO", "TPR", "TRGP", "TRMB", "TROW", "TRV", "TSCO", "TSLA", "TSN", "TT", "TTWO", "TXN", "TXT", "TYL", "UAL", "UBER", "UDR", "UHS", "ULTA", "UNH", "UNP", "UPS", "URI", "USB", "V", "VFC", "VICI", "VLO", "VLTO", "VMC", "VRSK", "VRSN", "VRTX", "VST", "VTR", "VTRS", "VZ", "WAB", "WAT", "WBA", "WBD", "WDC", "WEC", "WELL", "WFC", "WHR", "WM", "WMB", "WMT", "WRB", "WST", "WTW", "WY", "WYNN", "XEL", "XOM", "XYL", "YUM", "ZBH", "ZBRA", "ZION", "ZTS"
]

def get_financial_news() -> str:
    """
    Fetches the latest financial news for ALL S&P 500 companies from the last 24 hours.
    Splits into two batches to stay within URL length limits.
    """
    start_time = datetime.now() - timedelta(days=1)
    
    # Split tickers into two batches of approx 250 each
    mid = len(SP500_TICKERS) // 2
    batches = [SP500_TICKERS[:mid], SP500_TICKERS[mid:]]
    
    all_formatted_news = []
    
    for batch in batches:
        request_params = NewsRequest(
            symbols=",".join(batch),
            start=start_time,
            limit=50
        )
        time.sleep(1)  # Rate-limit delay
        try:
            news_response = news_client.get_news(request_params)
            for article in news_response.data['news']:
                # Focus on news stories specific to 1-3 symbols
                if 1 <= len(article.symbols) <= 3:
                    symbols_str = ", ".join(article.symbols)
                    all_formatted_news.append(
                        f"Ticker(s): {symbols_str}\n"
                        f"Headline: {article.headline}\n"
                        f"Summary: {article.summary}\n"
                        "---"
                    )
        except Exception as e:
            print(f"Error fetching news for batch: {str(e)}")

    if not all_formatted_news:
        return "No relevant single-stock news found for the S&P 500 in the last 24 hours."
        
    return "\n".join(all_formatted_news)

def get_portfolio_status() -> Dict[str, Any]:
    """Queries Alpaca for account status and open positions."""
    time.sleep(1)
    account = trading_client.get_account()
    time.sleep(1)
    positions = trading_client.get_all_positions()
    
    pos_list = []
    for p in positions:
        pos_list.append({
            "symbol": p.symbol,
            "qty": float(p.qty),
            "market_value": float(p.market_value),
            "current_price": float(p.current_price)
        })
        
    return {
        "available_cash": float(account.cash),
        "positions": pos_list
    }

def get_latest_price(symbol: str) -> float:
    """Retrieves the most recent ask price for a specific ticker."""
    request_params = StockLatestQuoteRequest(symbol_or_symbols=symbol)
    time.sleep(1)
    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)."""
    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(
                symbol=symbol,
                notional=amount_usd,
                side=order_side,
                time_in_force=TimeInForce.DAY
            )
        elif qty is not None:
            order_request = MarketOrderRequest(
                symbol=symbol,
                qty=qty,
                side=order_side,
                time_in_force=TimeInForce.GTC
            )
        else:
            return "Error: Either amount_usd (for buy) or qty (for sell/buy) must be provided."

        time.sleep(1)
        order = trading_client.submit_order(order_data=order_request)
        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):
    await callback_context.add_session_to_memory()

# Define the Agent
root_agent = Agent(
    name="TradingAgent",
    model="gemini-2.5-pro",
    instruction="""
    You are a sophisticated stock trading AI agent using Gemini 2.5 Pro. Your mission is to execute a daily trading cycle based on news sentiment for the entire S&P 500.
    
    FOLLOW THESE STEPS IN ORDER:
    
    1. NEWS LOOKUP: Call `get_financial_news()` to discover the latest news affecting any of the 500 S&P companies.
    
    2. SENTIMENT ANALYSIS: 
       - Analyze the news for positive or negative sentiments using Gemini 2.5 Pro and assign a sentiment rating between -1.0 and 1.0.
       - CRITICAL: Ignore Stock Analyst Rating changes; market news only.
       - CRITICAL: Only consider news stories specific to 1-3 stocks.
       - Format your tracking of each analyzed story as: Primary ticker affected, news summary, score.
    
    3. PORTFOLIO QUERY: Call `get_portfolio_status()`.
    
    4. SELL PHASE:
       - For every stock held: If sentiment score is NEGATIVE (< 0), sell ALL shares.
       - If no negative sentiment, do not sell.
    
    5. BUY PHASE:
       - Identify stocks NOT held that have **high confidence positive sentiments**.
       - 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.
    
    6. FINAL REPORT:
       Produce a comprehensive summary:
       - SUMMARY OF ORDERS: List all orders placed with ticker, action, score, and news summary.
       - MARKET SENTIMENT OVERVIEW: Top 10 Positive and Top 10 Negative news stories (Ticker, Summary, and Score).
    
    You have access to memories from past trading sessions. Use them to inform your decisions — for example, if you previously traded a stock based on similar news, consider the outcome.

    Execute the entire cycle once when prompted.
    """,
    tools=[get_financial_news, get_portfolio_status, get_latest_price, place_trade_order, PreloadMemoryTool()],
    after_agent_callback=save_to_memory,
)

# Wrap in AdkApp for execution
app = agent_engines.AdkApp(agent=root_agent)
Prompt
> look at this agent. how might I want to use sessions and memory from Vertex Agent Engine?
> ok, how to integrate memory bank

How ADK’s add_session_to_memory memories work is that a special LLM embedding model analyzes the session looking for pertinent data to save. If you specifically say something like “remember that …” it will almost certainly remember it. However, it may or may not specifically remember the trading actions.

To add a bit more determinisim, we can additionally specifically request to save