2025, Oct 22 21:00
Fix google.adk Stateful Chat Errors: Await create_session/get_session and use run_async properly
Learn to fix google.adk stateful chat errors in Python: await create_session/get_session, use run_async with async for, and avoid coroutine never awaited errors
When you wire up a stateful chat flow with google.adk in Python, mixing synchronous control flow with async APIs is an easy way to hit confusing errors. A typical scenario is starting a session via InMemorySessionService, sending a user message through a question_answering_agent, and then dumping the final session state. If you call coroutine-based methods without awaiting them or iterate a non-awaited async generator, you’ll see failures cascading through the run.
Symptoms
Trying to initialize a session, interact with an agent, and then read session.state produced multiple issues at once.
AttributeError: 'coroutine' object has no attribute 'state' and ValueError: Session not found (with RuntimeWarning: coroutine '...' was never awaited)
Problem example
The following script sets up a session and runs a question answering agent, but executes async methods as if they were synchronous.
import uuid
from dotenv import load_dotenv
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from question_answering_agent import question_answering_agent as qa_agent_node
load_dotenv()
# Store state in memory
store_backend = InMemorySessionService()
seed_state = {
    "user_name": "John Doe",
    "user_preferences": """
        I like to play Pickleball, Disc Golf, and Tennis.
        My favorite food is Mexican.
        My favorite TV show is Game of Thrones.
        Loves it when people like and subscribe to his YouTube channel.
    """,
}
# Create a NEW session (incorrectly, without awaiting the coroutine)
APP_TITLE = "John Doe Bot"
ACCOUNT_ID = "john_doe"
TRACKING_ID = str(uuid.uuid4())
session_live = store_backend.create_session(
    app_name=APP_TITLE,
    user_id=ACCOUNT_ID,
    session_id=TRACKING_ID,
    state=seed_state,
)
print("CREATED NEW SESSION:")
print(f"\tSession ID: {TRACKING_ID}")
exec_engine = Runner(
    agent=qa_agent_node,
    app_name=APP_TITLE,
    session_service=store_backend,
)
payload_in = types.Content(
    role="user", parts=[types.Part(text="What is Johns favorite TV show?")]
)
# Using the sync iterator instead of the async generator
for evt in exec_engine.run(
    user_id=ACCOUNT_ID,
    session_id=TRACKING_ID,
    new_message=payload_in,
):
    if evt.is_final_response():
        if evt.content and evt.content.parts:
            print(f"Final Response: {evt.content.parts[0].text}")
print("==== Session Event Exploration ====")
retrieved = store_backend.get_session(
    app_name=APP_TITLE, user_id=ACCOUNT_ID, session_id=TRACKING_ID
)
# Log final Session state (fails because retrieved is a coroutine, not a session)
print("=== Final Session State ===")
for k, v in retrieved.state.items():
    print(f"{k}: {v}")
Agent definition used by the runner:
from google.adk.agents import Agent
qa_agent_node = Agent(
    name="qa_agent_node",
    model="gemini-2.0-flash",
    description="Question answering agent",
    instruction="""
    You are a helpful assistant that answers questions about the user's preferences.
    Here is some information about the user:
    Name: 
    {user_name}
    Preferences: 
    {user_preferences}
    """,
)
What actually went wrong
The core of the problem is asynchronous flow control. InMemorySessionService.create_session returns a coroutine and must be awaited. The same applies to InMemorySessionService.get_session. The runner exposes an asynchronous generator via run_async that should be consumed with an async for loop. Treating these as synchronous calls leads to RuntimeWarning about coroutines never awaited, then to downstream errors like AttributeError when trying to access .state on a coroutine, and ValueError about a missing session.
Fix: wrap in asyncio and await the right calls
The remedy is to execute inside an event loop, await session creation and retrieval, and iterate the runner’s asynchronous generator. The environment is loaded before use, and the same identifiers are used across all calls. Below is a complete, working version using asyncio.
import uuid
import asyncio
import os
from dotenv import load_dotenv
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from google.genai.client import Client
from question_answering_agent import question_answering_agent as qa_agent_node
# Load environment variables
load_dotenv()
api_key_value = os.getenv("GOOGLE_API_KEY")
async def boot():
    # Stateful storage for the session
    store_backend = InMemorySessionService()
    # Initial state shared with the agent
    seed_state = {
        "user_name": "John Doe",
        "user_preferences": """
            I like to play Pickleball, Disc Golf, and Tennis.
            My favorite food is Mexican.
            My favorite TV show is Game of Thrones.
            Loves it when people like and subscribe to his YouTube channel.
        """,
    }
    # Create a NEW session
    APP_TITLE = "John Doe Bot"
    ACCOUNT_ID = "john_doe"
    TRACKING_ID = str(uuid.uuid4())
    # Await the coroutine to actually create the session
    created_session = await store_backend.create_session(
        app_name=APP_TITLE,
        user_id=ACCOUNT_ID,
        session_id=TRACKING_ID,
        state=seed_state,
    )
    print("CREATED NEW SESSION:")
    print(f"\tSession ID: {TRACKING_ID}")
    # Short pause before running the agent
    await asyncio.sleep(0.1)
    exec_engine = Runner(
        agent=qa_agent_node,
        app_name=APP_TITLE,
        session_service=store_backend,
    )
    payload_in = types.Content(
        role="user", parts=[types.Part(text="What is Johns favorite TV show?")]
    )
    # Consume the asynchronous event stream
    async for evt in exec_engine.run_async(
        user_id=ACCOUNT_ID,
        session_id=TRACKING_ID,
        new_message=payload_in,
    ):
        if evt.is_final_response():
            if evt.content and evt.content.parts:
                print(f"Final Response: {evt.content.parts[0].text}")
    print("==== Session Event Exploration ====")
    # Await retrieval of the session to inspect final state
    fetched_session = await store_backend.get_session(
        app_name=APP_TITLE, user_id=ACCOUNT_ID, session_id=TRACKING_ID
    )
    print("=== Final Session State ===")
    if fetched_session and hasattr(fetched_session, "state"):
        for k, v in fetched_session.state.items():
            print(f"{k}: {v}")
    else:
        print("Session or session state could not be retrieved.")
if __name__ == "__main__":
    asyncio.run(boot())
Why this matters
Stateful orchestration with google.adk depends on asynchronous boundaries. Ignoring await points not only breaks the event stream; it also leaves the session uninitialized from the caller’s perspective, so any attempt to read session.state fails or appears as if the session doesn’t exist. Aligning your control flow with the library’s async model ensures the agent receives context, emits final responses, and the session remains consistent for inspection.
Takeaways
Drive the whole flow inside an event loop, await create_session and get_session, and iterate the runner’s run_async stream with async for. Keep a single session_id across creation and consumption, and make sure environment variables are loaded before you start. With those pieces in place, the stateful session runs end-to-end and the final state is available for logging.
The article is based on a question from StackOverflow by PinkBanter and an answer by PinkBanter.