Skip to main content

adk-aerospike

Tests

Aerospike-backed storage services for Google Agent Development Kit (ADK).

Status: alpha (0.0.2). All three ADK storage interfaces implemented end-to-end against ADK 2.x.

What's in here

Three implementations of ADK's pluggable storage interfaces, plus URI-scheme registration so the adk CLI can use Aerospike directly:

ADK interface This package Backed by
BaseSessionService AerospikeSessionService Aerospike KV + Map/List CDTs (chunked session records)
BaseArtifactService AerospikeArtifactService Aerospike KV (one record per version)
BaseMemoryService AerospikeMemoryService Aerospike KV (lexical search via per-token posting-list PKs)

Why use this

  • All three ADK storage interfaces in one package — Session, Artifact, and Memory, backed by a single Aerospike cluster.
  • Native in-process client. Talks directly to Aerospike; nothing extra to deploy or operate.
  • Lexical memory search in Aerospike — text is tokenized at write time; each query token does a point read on posting-list keys (app:user:kw:<token>), then hydrates matching memory rows. Same word-overlap semantics as ADK's InMemoryMemoryService.
  • Session list via per-user manifest — app:user:sl holds session ids; list reads metadata bins only (no full event tail over the wire).
  • Single-record server-side atomic appends — Aerospike Map/List CDTs let append_event commit state delta, event append, and timestamp bump in one round trip.
  • Chunked session records handle long event histories without hitting Aerospike's write-block-size limit, while keeping the hot path a single operation.
  • Single round-trip get_session — batch_read fetches the session record, app-state record, and user-state record in one network call.
  • adk web integration — register the aerospike:// URI scheme and the CLI flags work out of the box.
  • No AI/ML dependency. Memory is a storage backend, not an embedding pipeline — no embedder to wire up, no model to host.

Install

pip install adk-aerospike

Requires Python 3.11+ and Aerospike Database 7.x or 8.x (Community or Enterprise).

Development install

From a clone of this repository (Python 3.11+):

python -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
python -c "from adk_aerospike import AerospikeSessionService; print('ok')"

Release process: RELEASING.md.

Quick start

Spin up a local Aerospike container:

docker run --rm -d --name aerospike -p 3000-3003:3000-3003 aerospike/aerospike-server:latest

Then wire it into an ADK agent:

import asyncio
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.genai import types
from adk_aerospike import AerospikeSessionService

async def main() -> None:
    session_service = AerospikeSessionService.from_uri(
        "aerospike://localhost:3000/test"
    )
    agent = LlmAgent(name="greeter", model="gemini-2.5-flash",
                     instruction="Be friendly. <30 words.")
    runner = Runner(agent=agent, app_name="quickstart",
                    session_service=session_service)

    session = await session_service.create_session(
        app_name="quickstart", user_id="user-42"
    )
    async for event in runner.run_async(
        user_id="user-42", session_id=session.id,
        new_message=types.Content(role="user", parts=[types.Part(text="Hi!")]),
    ):
        for part in (event.content.parts if event.content else []):
            if part.text:
                print(part.text)

    session_service.close()

asyncio.run(main())

See examples/quickstart.py for the complete file (needs a GOOGLE_API_KEY to actually call the model).

Connection URIs

All three services accept the same aerospike:// URI scheme:

aerospike://[user:pass@]host[:3000][,host2[:port],…]/<namespace>[?option=value]

Examples:

aerospike://localhost:3000/test
aerospike://user:pass@host1:3000,host2:3000/adk?set_prefix=prod_&tls=true

Query parameters:

  • set_prefix=adk_ — default set-name prefix (lets multiple installations share one namespace)
  • tls=true — enables TLS (use tls_config=... kwarg for mTLS details)

Example: SessionService

import asyncio
from google.adk.events import Event, EventActions
from google.genai import types
from adk_aerospike import AerospikeSessionService

async def main() -> None:
    svc = AerospikeSessionService.from_uri("aerospike://localhost:3000/test")

    # Create a session with initial state (mixes session-, app-, and user-scoped keys)
    session = await svc.create_session(
        app_name="support_bot",
        user_id="alice",
        state={
            "topic": "billing",            # session-scoped
            "app:tenant": "acme-corp",     # shared across all users of the app
            "user:nickname": "Allie",      # shared across alice's sessions
            "temp:scratch": "throwaway",   # in-process only — never persisted
        },
    )
    print(f"session id: {session.id}")

    # Append an event (one server-side atomic op — list_append + state delta + ts bump)
    await svc.append_event(
        session,
        Event(
            invocation_id="i1",
            author="user",
            content=types.Content(role="user",
                                  parts=[types.Part(text="Where's my invoice?")]),
            actions=EventActions(state_delta={"turn": 1}),
        ),
    )

    # Fetch — single batch_read across session + app_state + user_state (1 RTT)
    fetched = await svc.get_session(
        app_name="support_bot", user_id="alice", session_id=session.id
    )
    print(fetched.state)
    # {'topic': 'billing', 'turn': 1, 'app:tenant': 'acme-corp', 'user:nickname': 'Allie'}

    # List a user's sessions (events/state stripped per ADK contract)
    resp = await svc.list_sessions(app_name="support_bot", user_id="alice")
    print(f"{len(resp.sessions)} sessions")

    # Delete cascades to every sealed chunk
    await svc.delete_session(
        app_name="support_bot", user_id="alice", session_id=session.id
    )

    svc.close()

asyncio.run(main())

State scoping (matches google.adk.sessions.state.State):

Prefix Storage location Visibility
app:foo adk_app_state (one record per app) All users of this app
user:foo adk_user_state (one record per (app, user)) This user across sessions
temp:foo NOT PERSISTED In-process, current invocation only
(unprefixed) On the session record This session only

Example: ArtifactService

import asyncio
from google.genai import types
from adk_aerospike import AerospikeArtifactService

async def main() -> None:
    svc = AerospikeArtifactService.from_uri("aerospike://localhost:3000/test")

    # Session-scoped: visible only within this session
    v = await svc.save_artifact(
        app_name="support_bot", user_id="alice", session_id="s-1",
        filename="receipt.png",
        artifact=types.Part(
            inline_data=types.Blob(mime_type="image/png", data=b"\x89PNG..."),
        ),
    )
    print(f"saved version {v}")  # 0

    # Save again → version 1
    await svc.save_artifact(
        app_name="support_bot", user_id="alice", session_id="s-1",
        filename="receipt.png",
        artifact=types.Part(
            inline_data=types.Blob(mime_type="image/png", data=b"\x89PNG..updated"),
        ),
    )

    # Load latest (or pass version=0 for the first)
    latest = await svc.load_artifact(
        app_name="support_bot", user_id="alice", session_id="s-1",
        filename="receipt.png",
    )
    print(latest.inline_data.mime_type, len(latest.inline_data.data))

    # User-scoped: 'user:' prefix → cross-session visible
    await svc.save_artifact(
        app_name="support_bot", user_id="alice", session_id="s-1",
        filename="user:avatar.jpg",
        artifact=types.Part(text="<jpeg bytes here>"),
    )
    # Same artifact is now visible from any session:
    avatar = await svc.load_artifact(
        app_name="support_bot", user_id="alice", session_id="s-2",  # different session
        filename="user:avatar.jpg",
    )
    assert avatar is not None

    # list_artifact_keys merges session-scoped + user-scoped
    keys = await svc.list_artifact_keys(
        app_name="support_bot", user_id="alice", session_id="s-1",
    )
    print(keys)  # ['receipt.png', 'user:avatar.jpg']

    # ADK 2.x metadata methods
    versions = await svc.list_artifact_versions(
        app_name="support_bot", user_id="alice", session_id="s-1",
        filename="receipt.png",
    )
    for v in versions:
        print(v.version, v.canonical_uri, v.mime_type, v.create_time)

    svc.close()

asyncio.run(main())

Example: MemoryService

Lexical word-overlap search — same semantics as InMemoryMemoryService, via per-token posting-list primary keys (app:user:kw:<token>). No embedder.

import asyncio
from google.adk.events import Event, EventActions
from google.adk.sessions import Session
from google.genai import types
from adk_aerospike import AerospikeMemoryService

async def main() -> None:
    memory = AerospikeMemoryService.from_uri(
        "aerospike://localhost:3000/test", top_k=10,
    )

    # Persist a session's text events to long-term memory. Text is tokenized
    # into keywords; each token updates a posting-list row and a memory row.
    session = Session(
        id="s-1", app_name="support_bot", user_id="alice",
        events=[
            Event(invocation_id="i", author="user",
                  content=types.Content(role="user",
                      parts=[types.Part(text="Python uses duck typing.")]),
                  actions=EventActions()),
            Event(invocation_id="j", author="user",
                  content=types.Content(role="user",
                      parts=[types.Part(text="My favorite color is blue.")]),
                  actions=EventActions()),
        ],
    )
    await memory.add_session_to_memory(session)

    # Search — batch_read posting lists per query token, union refs,
    # batch_read memory rows, rank by token overlap.
    resp = await memory.search_memory(
        app_name="support_bot", user_id="alice", query="python duck typing",
    )
    for m in resp.memories:
        print(m.author, m.timestamp, m.content.parts[0].text)
    # → user 2026-... Python uses duck typing.

    memory.close()

asyncio.run(main())

Use with adk web / adk run

Register the URI scheme once, e.g. in a services.py next to your agent:

# services.py
import adk_aerospike
adk_aerospike.register()

Then drive the CLI normally:

adk web \
  --session_db_url=aerospike://localhost:3000/adk \
  --artifact_storage_uri=aerospike://localhost:3000/adk \
  --memory_service_uri=aerospike://localhost:3000/adk

Storage shape — what ends up in Aerospike

Five sets in a single namespace (default prefix adk_):

adk_sessions      app:user:session                ← session record (state + hot tail)
                  app:user:session:c:NNNNNNNN  ← sealed chunk record (older events)
                  app:user:sl                    ← session-id manifest (list_sessions)

adk_app_state     app                                   ← one per (app)
adk_user_state    app:user                           ← one per (app, user)

adk_artifacts     app:user:session:fname:NNNNNNNN
                  app:user:user:user:fname:NNNNNNNN   ← user-scoped (sentinel "user")

adk_memory        app:user:session:eventid       ← memory row
                  app:user:kw:token              ← posting list ({eid,sid,ts} refs)

The session record is the hot path — events accumulate in an inline List bin (the hot tail) until it reaches a 256 KiB threshold, then flush to a sealed chunk record. Most append_event calls are a single server-side atomic operate(); get_session is a single batch_read (session + app_state + user_state in one RTT) plus chunk reads only when the requested event window exceeds the tail.

For the full design (chunking, atomicity, key formats, indexes, trade-offs), see design.md.

Running tests

CI runs on every push to main and on pull requests (see .github/workflows/tests.yml):

  • Unit — pytest -m "not aerospike" on Python 3.11 and 3.12 (no Docker).
  • Integration — starts Aerospike CE with scripts/start_aerospike_ce.sh (docker run aerospike/aerospike-server:latest), then pytest -m aerospike (~4 minutes).

Locally:

pip install -e ".[dev]"

# Unit tests only (no Docker required, ~2s)
pytest -m "not aerospike"

# Integration — explicit Aerospike CE container (matches CI)
./scripts/start_aerospike_ce.sh
set -a && source .aerospike-ci.env && set +a
pytest -m aerospike
./scripts/stop_aerospike_ce.sh

# Integration — or let testcontainers start Aerospike for you (no script)
pytest -m aerospike

# Full suite (testcontainers path if env vars unset)
pytest

Documentation

Comparison with other ADK storage integrations

Integration Maintainer Sess Art Mem Architecture
adk-aerospike (this) Aerospike ✓ ✓ ✓ semantic In-process, single backend
adk-redis Redis Inc. ✓ ✗ ✓ HTTP sidecar (Agent Memory Server) + RedisVL
adk-python built-in Google ✓ ✓ ✓ In-process / Vertex managed
adk-extra-services Community ✓ Mongo/Redis ✓ S3/Local/Azure ✗ In-process
google-adk-extras Community ✓ SQL/Mongo/Redis ✓ Local/S3/SQL ✓ keyword-only In-process
Pinecone / Qdrant / Couchbase / Chroma Vendors ✗ ✗ ✗ (just tools) MCP server

We're the only package shipping all three storage interfaces with embedding-based semantic memory, backed by a single in-process database.

License

Apache-2.0

Metadata

Release files for adk-aerospike 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for adk-aerospike 0.1.0
File Size Uploaded Provenance
adk_aerospike-0.1.0.tar.gz 129.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for adk-aerospike 0.1.0
File Interpreter ABI Platform
adk_aerospike-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 174.6 kB

Release files / adk_aerospike-0.1.0.tar.gz

Download URL adk_aerospike-0.1.0.tar.gz
Size 129.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8503d4ed96ec22dcc91c2a475687d44e737f56bf553911da98225573676138da
BLAKE2b-256 checksum
How to use checksums
543f84c4ef4c485bc4796fbd53fbd6e78d2dbdd888ac5c1dfe95f21a81139a6e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 22, 2026.

Transparency log

Release files / adk_aerospike-0.1.0-py3-none-any.whl

Download URL adk_aerospike-0.1.0-py3-none-any.whl
Size 45.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1825ea4b0893b3d67d2503d2f5225a409cb1b639eaf142f8cbcd59715f385461
BLAKE2b-256 checksum
How to use checksums
9be9b8e346f3d2f0ae874cc61a613d6e58763c080f27b21920b74a84862f8d3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 22, 2026.

Transparency log

Security for adk-aerospike 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for adk_aerospike-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page