|
| 1 | +# Session and BaseSessionService |
| 2 | + |
| 3 | +`Session` is the conversation record — its id, its owner, its state, and its |
| 4 | +ordered event history. `BaseSessionService` is the storage interface that |
| 5 | +creates, reads, lists, and deletes those records and appends events to them. |
| 6 | + |
| 7 | +## Introduction |
| 8 | + |
| 9 | +An agent run is stateless on its own: the model sees only what you give it. A |
| 10 | +`Session` is what carries a conversation across turns, holding the event history |
| 11 | +that becomes the model's context and a `state` dict that agents and tools read |
| 12 | +and write. |
| 13 | + |
| 14 | +`Session` is a plain Pydantic model and never talks to storage itself. |
| 15 | +Everything that persists a session goes through a `BaseSessionService`, which |
| 16 | +declares four abstract methods — `create_session`, `get_session`, |
| 17 | +`list_sessions`, `delete_session` — plus a concrete `append_event` that every |
| 18 | +backend inherits. That split is why the same agent code runs unchanged against |
| 19 | +an in-process dict during development and a shared database in production: you |
| 20 | +swap the service, not the agent. `Runner` takes a `session_service` as a |
| 21 | +required argument and drives `get_session` and `append_event` for you, so most |
| 22 | +applications call the service directly only to create, list, and delete |
| 23 | +sessions. |
| 24 | + |
| 25 | +## Get started |
| 26 | + |
| 27 | +`InMemorySessionService` needs no configuration. This example creates a |
| 28 | +session, appends two events, and reads the result back. |
| 29 | + |
| 30 | +```python |
| 31 | +import asyncio |
| 32 | + |
| 33 | +from google.adk.events import Event |
| 34 | +from google.adk.sessions import InMemorySessionService |
| 35 | + |
| 36 | +APP_NAME = "hello_world" |
| 37 | +USER_ID = "user-123" |
| 38 | + |
| 39 | + |
| 40 | +async def main() -> None: |
| 41 | + session_service = InMemorySessionService() |
| 42 | + |
| 43 | + # 1. Create. Omit session_id to have one generated for you. |
| 44 | + session = await session_service.create_session( |
| 45 | + app_name=APP_NAME, |
| 46 | + user_id=USER_ID, |
| 47 | + state={"locale": "en-US"}, |
| 48 | + ) |
| 49 | + |
| 50 | + # 2. Append events. Each one lands in session.events, and any state the |
| 51 | + # event carries is merged into session.state. |
| 52 | + await session_service.append_event( |
| 53 | + session, Event(author="user", message="What is the weather?") |
| 54 | + ) |
| 55 | + await session_service.append_event( |
| 56 | + session, |
| 57 | + Event( |
| 58 | + author="weather_agent", |
| 59 | + message="It is sunny.", |
| 60 | + state={"last_city": "Zurich"}, |
| 61 | + ), |
| 62 | + ) |
| 63 | + |
| 64 | + # 3. Read it back. get_session returns None when nothing is stored. |
| 65 | + loaded = await session_service.get_session( |
| 66 | + app_name=APP_NAME, user_id=USER_ID, session_id=session.id |
| 67 | + ) |
| 68 | + assert loaded is not None |
| 69 | + print(len(loaded.events), loaded.state) |
| 70 | + |
| 71 | + |
| 72 | +if __name__ == "__main__": |
| 73 | + asyncio.run(main()) |
| 74 | +``` |
| 75 | + |
| 76 | +This prints `2 {'locale': 'en-US', 'last_city': 'Zurich'}`. |
| 77 | + |
| 78 | +Every method is keyword-only except `append_event`, which takes the session and |
| 79 | +the event positionally. A session is identified by the triple |
| 80 | +`(app_name, user_id, session_id)`, not by `session_id` alone, so all three are |
| 81 | +required on every read. |
| 82 | + |
| 83 | +## How it works |
| 84 | + |
| 85 | +### The lifecycle |
| 86 | + |
| 87 | +`create_session` generates a UUID when you do not pass `session_id`, and raises |
| 88 | +`AlreadyExistsError` (from `google.adk.errors.already_exists_error`) when you |
| 89 | +pass one that is already taken. `get_session` returns `None` for a missing |
| 90 | +session rather than raising. `list_sessions` returns a `ListSessionsResponse` |
| 91 | +ordered by `last_update_time`, oldest first, with the event history omitted. |
| 92 | + |
| 93 | +`append_event` is where the two copies of a session meet. The base |
| 94 | +implementation applies the event's `actions.state_delta` to the in-memory |
| 95 | +`Session` you hold and appends to `session.events`; each backend overrides it to |
| 96 | +write the event to storage as well. Partial events (`event.partial` is true) are |
| 97 | +returned untouched and never stored, which is how streaming chunks stay out of |
| 98 | +the history. |
| 99 | + |
| 100 | +### State scoping |
| 101 | + |
| 102 | +Keys in `state` are scoped by prefix, and the prefixes are constants on `State`: |
| 103 | + |
| 104 | +| Prefix | Constant | Scope | |
| 105 | +| --- | --- | --- | |
| 106 | +| none | | This session only. | |
| 107 | +| `app:` | `State.APP_PREFIX` | Every session of the app. | |
| 108 | +| `user:` | `State.USER_PREFIX` | Every session of this user within the app. | |
| 109 | +| `temp:` | `State.TEMP_PREFIX` | The current invocation only; never persisted. | |
| 110 | + |
| 111 | +Write prefixed keys like any other key, in `create_session(state=...)` or in an |
| 112 | +event's state delta. The service routes them to the right storage scope and |
| 113 | +merges them back into `session.state` on read, prefix included. `temp:` keys are |
| 114 | +the exception: they are applied to the in-memory session so later agents in the |
| 115 | +same invocation can read them, then stripped from the event before it is |
| 116 | +written. |
| 117 | + |
| 118 | +`get_user_state(app_name=..., user_id=...)` reads user-scoped state without a |
| 119 | +session id, returning raw keys with the `user:` prefix removed — useful for |
| 120 | +bootstrapping context before `create_session`. It is not abstract, and the |
| 121 | +default implementation raises `NotImplementedError`, so a custom backend that |
| 122 | +does not override it will fail this call. |
| 123 | + |
| 124 | +### Trimming what you load |
| 125 | + |
| 126 | +Pass a `GetSessionConfig` to bound the history you read back. It lives in |
| 127 | +`google.adk.sessions.base_session_service`, not in the package root: |
| 128 | + |
| 129 | +```python |
| 130 | +from google.adk.sessions.base_session_service import GetSessionConfig |
| 131 | + |
| 132 | +# The 20 most recent events. Use num_recent_events=0 for metadata and state |
| 133 | +# only, or after_timestamp=<unix seconds> to cut the history by time instead. |
| 134 | +recent = await session_service.get_session( |
| 135 | + app_name=APP_NAME, |
| 136 | + user_id=USER_ID, |
| 137 | + session_id=session_id, |
| 138 | + config=GetSessionConfig(num_recent_events=20), |
| 139 | +) |
| 140 | +``` |
| 141 | + |
| 142 | +The service applies these filters, so on a database backend they reduce what is |
| 143 | +read, not just what you see. |
| 144 | + |
| 145 | +## Choosing a session service |
| 146 | + |
| 147 | +| Service | Import | Use it when | |
| 148 | +| --- | --- | --- | |
| 149 | +| `InMemorySessionService` | `google.adk.sessions` | Developing and testing. State lives in process dicts and the class documents itself as unsuitable for multi-threaded production. | |
| 150 | +| `DatabaseSessionService` | `google.adk.sessions` | You need durability, or several processes sharing one conversation. Backed by a SQLAlchemy async engine; requires the `db` extra. | |
| 151 | +| `VertexAiSessionService` | `google.adk.sessions` | You are deploying on Vertex AI Agent Engine and want its managed session store. Requires the `gcp` extra. | |
| 152 | +| `SqliteSessionService` | `google.adk.sessions.sqlite_session_service` | You want a local SQLite file and no server. This is what the ADK CLI uses; note it is not re-exported from the package root. | |
| 153 | + |
| 154 | +`DatabaseSessionService` takes either a URL or an engine you already own, and |
| 155 | +exactly one of the two: |
| 156 | + |
| 157 | +```python |
| 158 | +from google.adk.sessions import DatabaseSessionService |
| 159 | + |
| 160 | +async with DatabaseSessionService("sqlite+aiosqlite:///./sessions.db") as svc: |
| 161 | + await svc.prepare_tables() # optional; otherwise done on first use |
| 162 | + session = await svc.create_session(app_name=APP_NAME, user_id=USER_ID) |
| 163 | +``` |
| 164 | + |
| 165 | +Use an async driver in the URL — `sqlite+aiosqlite`, `postgresql+asyncpg`, and |
| 166 | +so on. Passing `db_engine=<AsyncEngine>` instead reuses your application's |
| 167 | +engine, and the service will not dispose of one it did not create. As an async |
| 168 | +context manager it closes the engine it owns on exit; call `close()` yourself |
| 169 | +otherwise. |
| 170 | + |
| 171 | +`VertexAiSessionService` differs in one respect worth knowing before you switch |
| 172 | +to it: `app_name` is not a free-form string there. It must be the reasoning |
| 173 | +engine id or the full `projects/.../locations/.../reasoningEngines/N` resource |
| 174 | +name, unless you pass `agent_engine_id` to the constructor. |
| 175 | + |
| 176 | +## Advanced applications |
| 177 | + |
| 178 | +### Wiring a service into a Runner |
| 179 | + |
| 180 | +* **Problem solved**: one place decides where every conversation is stored. |
| 181 | +* **Implementation**: pass the service to `Runner(session_service=...)` and |
| 182 | + create the session before the first run. `Runner` defaults |
| 183 | + `auto_create_session` to `False`, so an unknown `session_id` raises |
| 184 | + `SessionNotFoundError` instead of silently starting a new conversation. |
| 185 | + |
| 186 | +### Writing your own backend |
| 187 | + |
| 188 | +* **Problem solved**: your sessions belong in a store ADK does not ship. |
| 189 | +* **Implementation**: subclass `BaseSessionService` and implement the four |
| 190 | + abstract methods. Override `append_event` to persist the event and call |
| 191 | + `await super().append_event(session, event)` so the in-memory session stays |
| 192 | + in step. Override `get_user_state` if your store can answer it, and `flush` |
| 193 | + if you buffer writes — the base `flush` is a no-op that `Runner` calls when |
| 194 | + it closes. |
| 195 | + |
| 196 | +### Detecting a stale session |
| 197 | + |
| 198 | +* **Problem solved**: two workers hold the same `Session` object and both |
| 199 | + append, so one would silently overwrite the other's history. |
| 200 | +* **Implementation**: nothing to write. `DatabaseSessionService` tracks a |
| 201 | + storage revision per session and raises `ValueError` from `append_event` |
| 202 | + when the in-memory copy has fallen behind. Recover by calling `get_session` |
| 203 | + again and replaying the append against the fresh session. |
| 204 | + |
| 205 | +## Limitations |
| 206 | + |
| 207 | +* **`InMemorySessionService` is not for production**: nothing survives a |
| 208 | + restart, nothing is shared between workers, and it does not lock. |
| 209 | +* **`append_event` fails differently per backend**: appending to a session |
| 210 | + that storage does not know about raises `SessionNotFoundError` on |
| 211 | + `DatabaseSessionService`, while `InMemorySessionService` logs a warning and |
| 212 | + returns the event unstored. |
| 213 | +* **`list_sessions` returns partial sessions**: the event history is dropped, |
| 214 | + and how much of `state` is populated depends on the backend. Load what you |
| 215 | + need with `get_session`. |
0 commit comments