Skip to content

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.


import sys, os, asyncio
sys.path.insert(0, os.path.expanduser("~/agenttests"))
import azcfg, hetool
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import DatabaseSessionService
from google.adk.models.lite_llm import LiteLlm
from 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()

{
"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>" }
}

  1. Keep your LlmAgent and tools as they are.
  2. Set OPENAI_API_BASE / OPENAI_API_KEY from azcfg.load() before constructing LiteLlm.
  3. Use DatabaseSessionService with HBIA’s sid as the ADK session_id.
  4. Get-or-create the session — get_session returns None on the first turn.
  5. Seal the engine, point external.engine at it, restart.

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.