Google ADK
Verified 2026-08-19 — PASS, 4s. Session persistence: native DatabaseSessionService.
ADK reaches the route through LiteLLM, which is the one thing that makes this integration different from the rest.
The runner
Section titled “The runner”import sys, os, asynciosys.path.insert(0, os.path.expanduser("~/agenttests"))import azcfg, hetoolfrom google.adk.agents import LlmAgentfrom google.adk.runners import Runnerfrom google.adk.sessions import DatabaseSessionServicefrom google.adk.models.lite_llm import LiteLlmfrom google.genai import types
DB = "sqlite+aiosqlite:///" + os.path.expanduser("~/agenttests/googleadk/sessions.db")APP, USER = "hexa", "u1"
def get_working_folder() -> str: """Return the agent's current working folder (absolute path).""" return hetool.get_working_folder()
def run_shell(command: str) -> str: """Run a shell command in the working folder and return its output.""" return hetool.run_shell(command)
async def invoke(sid, message): c = azcfg.load() # LiteLlm's openai/<model> route reads OPENAI_API_BASE / OPENAI_API_KEY from the # ENVIRONMENT, not from constructor arguments. Feed it the injected proxy endpoint # so the call routes through HBIA. os.environ["OPENAI_API_KEY"] = c["api_key"] os.environ["OPENAI_API_BASE"] = c["base_url"]
agent = LlmAgent(name="assistant", model=LiteLlm(model="openai/" + c["model"]), instruction="Be concise. Use tools for operational questions.", tools=[get_working_folder, run_shell]) svc = DatabaseSessionService(db_url=DB) sess = await svc.get_session(app_name=APP, user_id=USER, session_id=sid) if sess is None: sess = await svc.create_session(app_name=APP, user_id=USER, session_id=sid)
runner = Runner(agent=agent, app_name=APP, session_service=svc) content = types.Content(role="user", parts=[types.Part(text=message)]) reply = "" async for ev in runner.run_async(user_id=USER, session_id=sid, new_message=content): if ev.is_final_response() and ev.content and ev.content.parts: reply = ev.content.parts[0].text return (reply or "").strip()Engine definition
Section titled “Engine definition”{ "type": "process", "file": "/home/you/googleadk/.venv/bin/python3", "nativeSession": true, "argsNew": ["/home/you/he_adapter.py", "/home/you/googleadk_session.py", "", "{message}"], "argsResume": ["/home/you/he_adapter.py", "/home/you/googleadk_session.py", "{sessionId}", "{message}"], "replyField": "result", "sessionIdField": "session_id", "approve": false, "workdir": "/home/you/work", "proxy": { "shape": "openai", "baseUrlEnv": "OPENAI_BASE_URL", "keyEnv": "OPENAI_API_KEY", "modelEnv": "OPENAI_MODEL", "model": "<route>|<provider model>" }}Adoption
Section titled “Adoption”- Keep your
LlmAgentand tools as they are. - Set
OPENAI_API_BASE/OPENAI_API_KEYfromazcfg.load()before constructingLiteLlm. - Use
DatabaseSessionServicewith HBIA’ssidas the ADKsession_id. - Get-or-create the session —
get_sessionreturnsNoneon the first turn. - Seal the engine, point
external.engineat it, restart.
Gotchas
Section titled “Gotchas”LiteLLM reads the endpoint from the environment, not from arguments. This is the trap. Passing a
base URL to the constructor silently does nothing; LiteLLM picks up OPENAI_API_BASE and
OPENAI_API_KEY from os.environ. Get this wrong and the agent works perfectly while bypassing the
proxy entirely — calling the provider directly with whatever key it found, so the turn succeeds and
nothing is metered or gated. An earlier version of this runner did exactly that.
Note the variable is OPENAI_API_BASE (LiteLLM’s name), while HBIA injects OPENAI_BASE_URL — hence
the explicit copy.
get_session returns None for an unknown id, it does not create one. Create explicitly, or the
runner throws on the first turn of every conversation.
Async DB URL. sqlite+aiosqlite:/// — the sync sqlite:/// driver will not work with
DatabaseSessionService.
Iterate to the final event. run_async yields a stream; only ev.is_final_response() carries the
answer. Taking the first event gives you a partial or a tool call.