aiml
SearchMy learning
Menu

Lesson 17 of 20Module 5

Use Agent Platform models, sessions and Memory Bank

Connect a local ADK runner to managed sessions and Memory Bank, separate model and storage locations, and inspect one cross-session recall run.

Prerequisites

ADK sessions and memory, Google Cloud ADC, and an approved project with Agent Platform access.

By the end, you can

  • Configure managed sessions and Memory Bank separately from the model endpoint.
  • Inspect asynchronous ingestion and one cross-session recall result.
  • Refuse destructive recording resets and check ownership before cleanup.
Runtime
Python 3.12+, gcloud and jq; Google Cloud access for the recorded integration
Dependencies
google-adk[gcp] 2.12.0; google-cloud-aiplatform 2.6.0

Lab code, tests and fixtures (ZIP, 21 KB)Quiz check your understanding

Progress is saved in this browser.

The docs assistant can keep running on your machine while its session history and extracted memories live in Google Cloud. Here we use VertexAiSessionService and VertexAiMemoryBankService with a local Runner. The recording shows a developer’s CLI version recalled in a new session; it also shows that ingestion returning does not mean a memory is searchable.

Names in this lab. Agent Platform is the Google Cloud product family; Agent Runtime is its managed agent runtime. The Python management API uses agent_engines, the resource name uses reasoningEngines, and ADK selects that session backend with an agentengine:// URI. These are product, API and resource identifiers at different layers. This recording keeps application execution local and uses managed session and memory services.

When you need this. Your docs assistant runs in more than one process, and a developer should not have to repeat which CLI version they use in every conversation. Session history belongs to one conversation. Memory Bank extracts selected facts for later conversations; it is a separate service and a separate decision about retained user data.

Recorded example. The archived “Use Agent Platform models, sessions and Memory Bank” demonstration retains the earlier shipctl scenario; the current lab uses docuctl and does not claim a new model result.

One recorded recall in a new sessionRecorded output (excerpt) · textFull output
· session 3635675556772577280
> How do I undo the last production release?
call   load_memory {"query": "shipctl version"}
call   read_page {"page": "rollback"}
result load_memory {"result": {"memories": [{"content": {"parts": [{"text": "I run shipctl 1.x."}], "role": "user"}, "custom_metadata": {}, "author": "user", "timestamp": "2026-10-22T01:31:50.920516+00:00"}]}}
result read_page ["page", "text"]
answer Since you are using shipctl 1.x, run:

```bash
shipctl release revert <release>
```

Specify the release identifier you want to revert to. Note that database migrations are not automatically rolled back, so any migrations run in the last release must be reversed with a new migration.
data flowreturnartifact hand-offfailureRunnerrecord.pyread_page · load_memoryGeminiglobal endpointmodel requestmodel replyVertexAiSessionServiceregional historyeventsVertexAiMemoryBankServiceregional memoriessubmit completed eventssearch resultsdata flowreturnartifact hand-offfailureRunnerrecord.py and agent toolsGeminiglobalVertexAiSessionServicerequestreplyeventsVertexAiMemoryBankServiceregional extracted memoriessubmit eventssearch resultsCLI version recalledone recorded extractionlater search
The local record.py process runs the agent. Model calls use the global endpoint; session history and extracted memories use regional services. Submitting a completed session and searching its extracted memories are separate operations. The recorded immediate search was empty before a later search returned the CLI version.

Lab setup. Download the complete lab ZIP, extract it and open a terminal in adk-agent-platform/. Keep its code, tests and fixtures together. Create the pinned environment for local checks:

Create the lab environmentTerminal · bash
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-lock.txt

The saved cloud results can be read without provisioning services. The cloud steps require the project access listed in the prerequisites.

1. Give the model and stored conversations their own locations

The downloadable agent keeps the docuctl docs and tools from this course. Its Gemini client explicitly uses DOCS_MODEL_LOCATION=global; the session and memory services use the region configured in GOOGLE_CLOUD_LOCATION. In the recorded attempt, requesting this model in the storage region failed with 404, while the global model endpoint worked. That observation does not establish availability in every other region.

docs_helper/agent.pySource · pythonDownload
"""The docs assistant on Gemini Enterprise Agent Platform: Gemini through Vertex, recall through Memory Bank."""
import os
from pathlib import Path

from google.adk import Agent
from google.adk.models.google_llm import Gemini
from google.adk.tools import load_memory
from google.genai import types

DOCS = Path(__file__).resolve().parent / "docs"
PAGES = sorted(p.stem for p in DOCS.glob("*.md"))
# The recorded run uses global for this model. Sessions and Memory Bank stay regional
# (GOOGLE_CLOUD_LOCATION), so the model client gets its own location.
MODEL_LOCATION = os.environ.get("DOCS_MODEL_LOCATION", "global")


def read_page(page: str) -> dict:
    """Read one page of the docuctl documentation: config, deploy, rollback or tokens."""
    if page not in PAGES:
        return {"error": f"No page {page!r}. Pages: {', '.join(PAGES)}."}
    return {"page": page, "text": (DOCS / f"{page}.md").read_text(encoding="utf-8")}


INSTRUCTION = """You answer questions from developers about docuctl, the team's deploy CLI.
Read the relevant page with read_page before you answer. Answers depend on the developer's
docuctl major version (1.x or 2.x). If the developer has not said it in this conversation,
call load_memory to look for it in earlier conversations before you ask. Keep the answer
under 80 words."""

root_agent = Agent(
    name="docs_helper",
    model=Gemini(model="gemini-3.8-flash", client_kwargs={"location": MODEL_LOCATION}),
    instruction=INSTRUCTION,
    tools=[read_page, load_memory],
    generate_content_config=types.GenerateContentConfig(
        temperature=0.2, thinking_config=types.ThinkingConfig(thinking_level="low")),
)

Use Application Default Credentials for the approved project and install the pinned dependencies. GOOGLE_GENAI_USE_ENTERPRISE=true selects the cloud backend. This exercise uses paid services; the saved output can be inspected offline without provisioning them. The lab README separates cloud commands from offline tests.

2. Create a lesson instance with a useful memory topic

The managed instance supplies the resource ID used by the local runner’s session and Memory Bank clients. Creating it does not deploy this runner. create_memory_bank.py creates that instance with a lesson ownership label, a memory lifetime and a custom tool_versions topic:

create_memory_bank.pySource · pythonDownload
"""Create one Agent Runtime instance with no code: it holds Sessions and Memory Bank for the docs assistant."""
import os
import sys

import vertexai

LABEL = "use-agent-platform-models-sessions-and-memory-bank"


def memory_config() -> dict:
    return {"display_name": "docs-helper-memory", "labels": {"aiml-lesson": LABEL},
            "context_spec": {"memory_bank_config": {
                "ttl_config": {"default_ttl": "86400s"},
                "customization_configs": [{"memory_topics": [
                    {"managed_memory_topic": {"managed_topic_enum": "USER_PREFERENCES"}},
                    {"custom_memory_topic": {"label": "tool_versions",
                        "description": "Which version of a CLI, SDK or service the developer runs, for example 'runs docuctl 1.x'."}},
                ]}],
            }}}


def main() -> None:
    client = vertexai.Client(project=os.environ["GOOGLE_CLOUD_PROJECT"],
                             location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"))
    engine = client.agent_engines.create(config=memory_config())
    print("created", engine.api_resource.name)
    print("agent_engine_id", engine.api_resource.name.split("/")[-1], file=sys.stderr)


if __name__ == "__main__":
    main()

The custom topic describes information the extractor should retain. In an earlier recorded attempt with managed topics only, the CLI version was still absent after the polling window. With the custom topic, this run extracted the text shown below. This is one extraction result, not a recall benchmark or a promise about future runs.

Extracted memory from this runRecorded output · text
I run shipctl 1.x.

3. Run two conversations and ingest the first

record.py passes the instance ID as the application name, supplies the managed services to Runner, and creates a new session for each conversation. The instance ID is required here; an arbitrary ADK application name produced the saved ValueError.

record.pySource · pythonDownload
"""Run the docs assistant on Agent Platform Sessions and Memory Bank, and record what happens.

Usage: AGENT_ENGINE_ID=<id> python record.py
Needs ADC (gcloud auth application-default login), GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION.
"""
import asyncio
import json
import os
import re
import time
from pathlib import Path

from google.adk import Agent, Runner
from google.adk.memory import VertexAiMemoryBankService
from google.adk.models.google_llm import Gemini
from google.adk.sessions import InMemorySessionService, VertexAiSessionService
from google.genai import types

from docs_helper.agent import root_agent

HERE = Path(__file__).resolve().parent
PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"]
LOCATION = os.environ["GOOGLE_CLOUD_LOCATION"]
ENGINE = os.environ["AGENT_ENGINE_ID"]
APP = ENGINE  # with Agent Platform Sessions, the app name is the Agent Runtime instance
USER = os.environ.get("DOCS_USER", "dana")
out, record = [], {"model": "gemini-3.8-flash", "location": LOCATION, "failures": {}}


def redact(text: str) -> str:
    return re.sub(r"projects/\d+/", "projects/PROJECT_NUMBER/", str(text))


def log(line: str = "") -> None:
    out.append(redact(line))
    print(redact(line))


def summary(events) -> list[str]:
    lines = []
    for event in events:
        lines += [f"call   {c.name} {json.dumps(c.args)}" for c in event.get_function_calls()]
        for r in event.get_function_responses():
            body = r.response if r.name == "load_memory" else sorted(r.response)
            text = json.dumps(body, default=lambda o: o.model_dump(mode="json", exclude_none=True))
            lines.append(f"result {r.name} {redact(text)[:300]}")
        if event.is_final_response() and event.content:
            lines.append("answer " + "".join(p.text or "" for p in event.content.parts if not p.thought).strip())
    return lines


async def turn(runner: Runner, session_id: str, text: str) -> list:
    message = types.Content(role="user", parts=[types.Part(text=text)])
    return [e async for e in runner.run_async(user_id=USER, session_id=session_id, new_message=message)]


async def failures() -> None:
    # 1. Record the failure from using the session region for the model request.
    regional = Agent(name="docs_helper", model=Gemini(model="gemini-3.8-flash", client_kwargs={"location": LOCATION}),
                     instruction="Answer in one word.")
    runner = Runner(app_name="docs", agent=regional, session_service=InMemorySessionService())
    session = await runner.session_service.create_session(app_name="docs", user_id=USER)
    try:
        await turn(runner, session.id, "Say ok.")
    except Exception as error:  # noqa: BLE001 - record whatever the API raised
        record["failures"]["model_region"] = f"{type(error).__name__}: {redact(error)[:400]}"
    # 2. A Sessions service with no instance id and an ADK-style app name.
    try:
        await VertexAiSessionService(project=PROJECT, location=LOCATION).create_session(app_name="docs_helper", user_id=USER)
    except ValueError as error:
        record["failures"]["app_name"] = f"ValueError: {error}"
    for name, text in record["failures"].items():
        log(f"# failure {name}\n{text}\n")


async def require_empty() -> None:
    """Refuse an already-used instance instead of deleting its memories or sessions."""
    import vertexai
    client = vertexai.Client(project=PROJECT, location=LOCATION)
    name = f"projects/{PROJECT}/locations/{LOCATION}/reasoningEngines/{ENGINE}"
    if any(client.agent_engines.memories.list(name=name)):
        raise RuntimeError("Use a fresh lesson instance; existing memories will not be deleted.")
    sessions = VertexAiSessionService(PROJECT, LOCATION, ENGINE)
    if (await sessions.list_sessions(app_name=APP, user_id=USER)).sessions:
        raise RuntimeError("Use a fresh lesson instance; existing sessions will not be deleted.")


async def main() -> None:
    await require_empty()
    await failures()
    sessions = VertexAiSessionService(PROJECT, LOCATION, ENGINE)
    memory = VertexAiMemoryBankService(PROJECT, LOCATION, ENGINE)
    runner = Runner(app_name=APP, agent=root_agent, session_service=sessions, memory_service=memory)

    # Conversation 1: Dana says which version she runs.
    s1 = await sessions.create_session(app_name=APP, user_id=USER)
    log(f"# conversation 1 · session {s1.id}")
    log("> I run docuctl 1.x. How do I undo release r42 in production?")
    for line in summary(await turn(runner, s1.id, "I run docuctl 1.x. How do I undo release r42 in production?")):
        log(line)
    stored = await sessions.get_session(app_name=APP, user_id=USER, session_id=s1.id)
    record["session_1"] = {"id": s1.id, "stored_events": len(stored.events),
                           "authors": [e.author for e in stored.events]}

    # IngestEvents returns at once; Memory Bank extracts memories in the background.
    # force_flush asks it to process this conversation now instead of on its own trigger.
    started = time.monotonic()
    await memory.add_events_to_memory(app_name=APP, user_id=USER, events=stored.events, session_id=s1.id,
                                      custom_metadata={"force_flush": True})
    record["ingest_return_seconds"] = round(time.monotonic() - started, 2)
    early = await memory.search_memory(app_name=APP, user_id=USER, query="docuctl version")
    record["memories_right_after_ingest"] = len(early.memories)
    log(f"\n# ingest returned in {record['ingest_return_seconds']} s; search right after: {len(early.memories)} memories")
    found = early
    while not found.memories and time.monotonic() - started < 180:
        await asyncio.sleep(3)
        found = await memory.search_memory(app_name=APP, user_id=USER, query="docuctl version")
    record["searchable_after_seconds"] = round(time.monotonic() - started)
    record["memories"] = ["".join(p.text or "" for p in m.content.parts) for m in found.memories]
    log(f"# searchable after about {record['searchable_after_seconds']} s:")
    for text in record["memories"]:
        log(f"  - {text}")

    # Conversation 2: a new session; the version is not in it.
    s2 = await sessions.create_session(app_name=APP, user_id=USER)
    log(f"\n# conversation 2 · session {s2.id}")
    log("> How do I undo the last production release?")
    lines = summary(await turn(runner, s2.id, "How do I undo the last production release?"))
    for line in lines:
        log(line)
    record["session_2"] = {"id": s2.id, "lines": lines}
    listed = await sessions.list_sessions(app_name=APP, user_id=USER)
    record["sessions_listed"] = len(listed.sessions)
    log(f"\n# list_sessions for {USER}: {len(listed.sessions)} sessions")
    await runner.close()

    (HERE / "run-output.txt").write_text("\n".join(out) + "\n", encoding="utf-8")
    (HERE / "recorded-runs.json").write_text(redact(json.dumps(record, indent=2)) + "\n", encoding="utf-8")


if __name__ == "__main__":
    asyncio.run(main())

After the first conversation, add_events_to_memory submits its stored events with force_flush. It returned in 0.03 seconds; the immediate search returned 0 memories. Polling found a result after about 4 seconds in this run. Treat submission and retrieval as different states, and choose a bounded wait for your application.

The second conversation has no version in its message. The agent called load_memory, read the rollback page and used the recalled version. Its answer still asks for a release identifier; the recording demonstrates recall, rather than verifying that an operator could execute the answer without further information.

Existing data before a recording. record.py refuses an instance that already contains memories or this user’s sessions rather than deleting existing data before a run. Use a fresh lesson instance; the offline tests verify that refusal happens before any delete call reaches the cloud client.

Recorded regional-model and app-name failuresRecorded output · jsonDownload
{
  "model_region": "ClientError: 404 NOT_FOUND. {'error': {'code': 404, 'message': 'Publisher model `projects/vertex-ai-experminent/locations/us-central1/publishers/google/models/gemini-3.8-flash` was not found or your project does not have access to it. Ensure you are using a valid model name and that the model is available in the specified region. For more information, see: https://docs.cloud.google.com/gemini-enterprise-agent-",
  "app_name": "ValueError: App name docs_helper is not valid. It should either be the full ReasoningEngine resource name, or the reasoning engine id."
}

The empty-memory attempt is retained in failure-default-topics.txt. Distinguish an empty search from an ingestion failure: inspect the configured topics and the input conversation before changing the agent’s prompt.

Run the local checks and clean up the lesson instance

Offline configuration and data-preservation testsRecorded output · textDownload
test_cleanup_accepts_its_own_label (test_platform.ManagedServices.test_cleanup_accepts_its_own_label) ... ok
test_cleanup_refuses_an_unlabelled_instance (test_platform.ManagedServices.test_cleanup_refuses_an_unlabelled_instance) ... ok
test_config_includes_the_cli_version_topic (test_platform.ManagedServices.test_config_includes_the_cli_version_topic) ... ok
test_existing_memories_are_not_deleted_for_a_recording (test_platform.ManagedServices.test_existing_memories_are_not_deleted_for_a_recording) ... ok
test_existing_sessions_are_not_deleted_for_a_recording (test_platform.ManagedServices.test_existing_sessions_are_not_deleted_for_a_recording) ... ok
test_import_does_not_create_a_cloud_instance (test_platform.ManagedServices.test_import_does_not_create_a_cloud_instance) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.003s

OK

The tests replace cloud clients; they check configuration and guards, not Memory Bank’s extraction quality. The original instance and its sessions/memories were deleted after recording. If you provision your own, run the guarded delete_memory_bank.py with your instance ID and check completion in your project. A label check prevents an accidental mismatch; it is not an authorization system.

Cleanup of an unowned instance. delete_memory_bank.py checks the instance’s aiml-lesson label and refuses a mismatch before deleting it. Check the resource before teardown.

What’s next

Scaffold and deploy an agent with the Agents CLI adds a generated application project and deployment configuration.

Check your understanding

add_events_to_memory returns successfully, but the immediate search is empty. Which state has been established?

Choose one answer, then check your reasoning.

Read the explanation

Submission and searchable extraction are separate states. The recorded lab polls with a bounded wait; return does not guarantee recall.

References

Official documentation and installed ADK APIs checked October 22, 2026.

Written by Arjun Prabhulal.

Module 5

Deploy agents on Google Cloud