{
  "source_file": "0930",
  "source_platform": "Hashnode",
  "author": "Juan Carlos Olamendy",
  "title_listed": "The Runner-Session Architecture in Google ADK: Orchestrating Stateful AI Conversations",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://juancolamendy.hashnode.dev/the-runner-session-architecture-in-google-adk-orchestrating-stateful-ai-conversations",
  "publish_date": "2025-10-20",
  "corpus_stage": "",
  "notes": "",
  "id": "b8a225081a2f",
  "collected_at": "2026-10-02T14:10:48",
  "title": "The Runner-Session Architecture in Google ADK: Orchestrating Stateful AI Conversations",
  "body_markdown": "The Runner-Session Architecture in Google ADK: Orchestrating Stateful AI Conversations\nPublished\nOctober 20, 2025\n•\n18\nmin read\n•\nView as Markdown\nJ\nJuan Carlos Olamendy\nSenior AI Engineer | Building & Telling Stories about AI/ML Systems | Software Engineer\nYour AI agent remembers that a user's favorite color is blue. It recalls their love for pizza and their excitement about The Matrix.\nBut here's the question that separates amateur implementations from production-grade systems:\nHow does this memory actually flow through your application?\nThe answer lies in understanding the relationship between Runners and Sessions—the twin pillars of stateful conversation management.\nMost developers building AI agents focus on the language model and the prompts.\nThey spend hours crafting perfect instructions but ignore the infrastructure that makes conversations actually work.\nThe Runner-Session architecture in Google ADK solves this.\nIn this guide, you'll master how Runners orchestrate stateful conversations by connecting agents to persistent sessions.\nYou'll learn how messages flow through the system, how state persists across interactions, and how to build AI agents that actually remember your users.\nBy the end, you'll transform forgetful chatbots into intelligent assistants with perfect recall.\nThe Fundamental Problem\nEvery conversational AI system faces a critical challenge: bridging the gap between stateless language models and stateful applications.\nLanguage models process individual messages without inherent memory of previous interactions.\nYour application needs to maintain context, user preferences, and conversation history across multiple turns. This mismatch creates complexity that can sink even well-designed systems.\nSessions provide the memory—a persistent storage layer that survives across requests.\nAgents provide the intelligence—the language model that processes user input and generates responses.\nBut between these two components lies a coordination problem that demands a sophisticated orchestration layer. This is where the Runner enters the picture.\nThe Runner acts as the central nervous system of your conversational AI.\nIt coordinates message flow, manages session lifecycle, executes tool calls, and ensures state changes persist correctly.\nWithout this orchestration layer, developers must manually handle dozens of edge cases and integration points. The Runner abstracts this complexity into a simple, elegant API.\nAnatomy of a Runner\nThe Runner's initialization reveals its core purpose: connecting an agent to a session service.\nThese three components—the Runner itself, the agent, and the session service—form a complete conversational system.\nUnderstanding how they wire together illuminates the entire architecture.\ndef\ncreate_runner\n(\napp_name: str, session_service: InMemorySessionService, agent: Agent\n):\nrunner = Runner(\n        app_name=app_name,\n        session_service=session_service,\n        agent=agent\n    )\nreturn\nrunner\nThe\napp_name\nparameter establishes the namespace for session operations. Multiple applications can share the same session service by using different app names.\nThis isolation prevents one application's sessions from interfering with another's. Think of app_name as the top-level organizational boundary in your session hierarchy.\nThe\nsession_service\nparameter injects the actual session storage backend.\nThe Runner doesn't care whether you use\nInMemorySessionService\n,\nDatabaseSessionService\n, or\nVertexAISessionService\n.\nThis abstraction enables seamless transitions between development and production environments.\nYou can develop with in-memory sessions, test with a local database, and deploy with a managed cloud service—all without changing Runner code.\nThe\nagent\nparameter connects the conversational intelligence to the orchestration layer.\nThe Runner delegates all language processing to this agent but manages everything around it. It handles message formatting, tool execution coordination, and response streaming.\nThe agent focuses purely on understanding intent and generating responses while the Runner manages the operational complexity.\nSession Lifecycle\nBefore a Runner can orchestrate a conversation, a session must exist.\nSession creation establishes the memory container that persists throughout the interaction.\nThe relationship between session creation and Runner usage follows a specific pattern that ensures proper state management.\nasync\ndef\ncreate_session\n(\nsession_service: InMemorySessionService, app_name: str, user_id: str\n):\nsession =\nawait\nsession_service.create_session(\n        app_name=app_name,\n        user_id=user_id,\n        state=init_state()\n    )\nreturn\nsession\nThe\ncreate_session\nfunction operates independently of the Runner.\nSessions are created through the session service, not through the Runner itself.\nThis separation of concerns allows flexible session management strategies—you might create sessions at user registration, at first message, or through external triggers.\nThe Runner consumes sessions; it doesn't create them.\nThe\napp_name\nand\nuser_id\nparameters form a composite key for session management.\nThe app_name scopes sessions to your application, preventing conflicts in multi-tenant environments.\nThe user_id identifies which user owns the session, enabling proper authorization and session isolation. Together, these parameters ensure that sessions are organized, secure, and retrievable.\nThe\nstate\nparameter initializes the session with default data. In this example,\ninit_state()\nreturns a dictionary containing default preferences.\nThis initial state ensures your agent has something to work with even before users explicitly share information. Setting sensible defaults improves the user experience by enabling immediate functionality.\ndef\ninit_state\n():\nreturn\n{\n\"preferences\"\n: {\n\"favorite_color\"\n:\n\"blue\"\n,\n\"favorite_food\"\n:\n\"pizza\"\n}\n    }\nThe session service returns a\nSession\nobject containing an auto-generated unique ID. This session ID becomes the key for all subsequent interactions.\nYou must preserve this ID throughout the conversation to maintain continuity.\nLosing the session ID effectively creates amnesia—the agent forgets everything about that conversation.\nSession retrieval follows a similar pattern, using the same identifying parameters.\nThe Runner accesses sessions through these retrieval operations during conversation processing.\nUnderstanding this access pattern helps you design proper session management strategies in your application architecture.\nasync\ndef\nget_session\n(\nsession_service: InMemorySessionService, app_name: str, user_id: str, session_id: str\n):\nsession =\nawait\nsession_service.get_session(\n        app_name=app_name,\n        user_id=user_id,\n        session_id=session_id\n    )\nreturn\nsession\nThe\nRunner.run\n() Method\nThe Runner's\nrun()\nmethod is where session and agent unite.\nThis method orchestrates the entire conversation flow, handling complexity that would otherwise require hundreds of lines of boilerplate code.\nEvery message your users send passes through this orchestration layer.\nfor\nevent\nin\nrunner.run(\n    user_id=user_id,\n    session_id=session_id,\n    new_message=user_message\n):\nif\nevent.is_final_response():\nif\nevent.content\nand\nevent.content.parts:\n            print(\nf\"Agent:\n{event.content.parts[\n0\n].text}\n\"\n)\nThe\nrun()\nmethod accepts three critical parameters that bind the conversation to its context.\nThe\nuser_id\nparameter identifies who is sending the message, enabling proper session lookup and authorization.\nThe\nsession_id\nparameter specifies exactly which conversation thread this message belongs to. The\nnew_message\nparameter contains the actual user input that needs processing.\nNotice what the Runner handles implicitly: it loads the session using the provided identifiers, extracts the current state, passes relevant context to the agent, executes any tool calls the agent requests, updates the session state with changes, and streams back responses as events.\nThis automatic orchestration eliminates entire categories of bugs related to state synchronization and message handling.\nThe Runner guarantees that state changes persist correctly and that the agent always works with current data.\nThe return value from\nrun()\nis an iterator of events, not a simple response string.\nThis streaming approach enables real-time response delivery and supports complex multi-step reasoning.\nThe agent might call multiple tools, think through a problem, and gradually build a response—all visible through the event stream. This transparency helps developers understand agent behavior and debug issues.\nEvent-Driven Architecture\nThe event iterator returned by\nrun()\nrepresents a fundamental architectural choice.\nRather than blocking until the entire response is ready, the Runner streams events as they occur.\nThis design supports streaming responses to users, provides visibility into agent reasoning, and enables sophisticated conversation flows.\nEach event in the stream represents a discrete occurrence in the conversation processing pipeline.\nTool call events indicate when the agent invokes a tool to access external data or modify state.\nThinking events might show intermediate reasoning steps (depending on model configuration).\nResponse events contain the actual text being generated for the user. The final response event marks the completion of processing for this conversation turn.\nThe\nis_final_response()\nmethod identifies the terminal event containing the agent's complete response. Most applications only care about this final event, but having access to intermediate events enables advanced use cases.\nYou might display tool calls to users for transparency, show thinking steps for educational purposes, or implement custom logic based on specific events.\nThe event stream gives you complete visibility into the conversation processing lifecycle.\nThe event-driven architecture also enables proper error handling. If a tool call fails, you receive an error event that you can handle appropriately.\nIf the language model encounters an issue, the event stream terminates with an error event.\nThis structured error reporting beats the alternative of exceptions breaking your entire conversation flow.\nHow the Runner Loads Session State\nThe Runner's integration with the session service happens transparently during\nrun()\nexecution.\nUnderstanding this integration reveals how state flows from persistent storage into the agent's context.\nThis knowledge is crucial for designing effective state management strategies.\nWhen you call\nrunner.run\n()\nwith a user_id and session_id, the first action is session retrieval.\nThe Runner internally calls\nsession_service.get_session()\nwith the provided app_name, user_id, and session_id. This retrieval operation loads the complete session object including all state data.\nThe Runner validates that the session exists and belongs to the specified user before proceeding.\nOnce loaded, the Runner extracts the state dictionary from the session object. This state becomes available to tools through the\nToolContext\nparameter.\nThe Runner doesn't interpret or modify the state structure—it simply makes it available. This design gives you complete freedom to structure state according to your application's needs.\nThe session's event history also loads during this phase. Previous messages, tool calls, and responses are accessible to provide context for the current turn.\nThe language model uses this history to understand the conversation flow and generate contextually appropriate responses.\nThe Runner manages this history automatically, appending new events as the conversation progresses.\nTools, ToolContext, and State Access\nThe ToolContext mechanism creates the bridge between tools and session state.\nUnderstanding this connection is essential for building tools that effectively manipulate persistent data.\nThe Runner manages ToolContext injection, ensuring tools always have access to current state.\ndef\nget_user_state\n(\ntool_context: ToolContext\n) -> Dict[str, Any]:\n\"\"\"Retrieve all user information from the session state.\"\"\"\npreferences = tool_context.state.get(\n\"preferences\"\n, {})\nreturn\n{\n\"status\"\n:\n\"success\"\n,\n\"preferences\"\n: preferences,\n\"retrieved_at\"\n: datetime.now().isoformat()\n    }\nWhen the agent decides to call a tool, the Runner intercepts this decision. It examines the tool's signature and identifies parameters that need injection.\nThe\nToolContext\nparameter is never provided by the agent—the Runner automatically injects it. This injection includes a reference to the current session's state dictionary.\nThe\ntool_context.state\nproperty provides direct dictionary access to the session state.\nTools can read values using standard dictionary operations like\nget()\n. This simple interface makes state access intuitive and error-resistant.\nYou don't need to worry about serialization, deserialization, or storage mechanics—the Runner handles everything.\nState modifications through ToolContext persist automatically after tool execution completes. When a tool modifies\ntool_context.state\n, the Runner detects these changes.\nAfter the tool returns, the Runner saves the modified state back to the session.\nThis automatic persistence eliminates a entire class of bugs where developers forget to save state changes.\ndef\nupdate_preference\n(\npreference_key: str, \n    preference_value: str, \n    tool_context: ToolContext\n) -> Dict[str, Any]:\npreferences = tool_context.state.get(\n\"preferences\"\n, {})\n    preferences[preference_key] = preference_value\n    tool_context.state[\n\"preferences\"\n] = preferences\nreturn\n{\n\"status\"\n:\n\"success\"\n,\n\"message\"\n:\nf\"Updated\n{preference_key}\nto\n{preference_value}\n\"\n,\n\"updated_preferences\"\n: preferences,\n\"updated_at\"\n: datetime.now().isoformat()\n    }\nThe update_preference tool demonstrates the full cycle of state modification. It reads the current preferences from\ntool_context.state\n, modifies the dictionary, and writes it back.\nThe Runner observes this write operation and schedules a session update. By the time the tool returns and the agent generates its response, the state is already persisted.\nMessage Flow: From User Input to Agent Response\nUnderstanding the complete message flow through the Runner illuminates how sessions and agents collaborate.\nEach conversation turn follows a specific sequence of operations. This sequence ensures proper state management and consistent behavior.\ndef\ninvoke_message\n(\nrunner: Runner, user_id: str, session_id: str, message_text: str\n):\nuser_message = types.Content(\n        role=\n\"user\"\n,\n        parts=[types.Part(text=message_text)]\n    )\nfor\nevent\nin\nrunner.run(\n        user_id=user_id,\n        session_id=session_id,\n        new_message=user_message\n    ):\nif\nevent.is_final_response():\nif\nevent.content\nand\nevent.content.parts:\n                print(\nf\"Agent:\n{event.content.parts[\n0\n].text}\n\"\n)\nThe flow begins with message construction using Google's\ntypes.Content\nformat.\nThis structure supports rich content types beyond simple text. The\nrole\nfield identifies who sent the message—user, model, or tool. The\nparts\narray contains the actual content, allowing multiple content pieces in a single message.\nWhen you call\nrunner.run\n()\n, the Runner immediately loads the session as described earlier.\nIt retrieves the session state and conversation history, preparing the context for agent processing.\nThe new message appends to the conversation history, becoming part of the context. The agent receives this complete context when making decisions.\nThe agent analyzes the message and context to determine appropriate actions. It might generate a direct response, call one or more tools, or perform complex reasoning.\nTool calls flow back through the Runner, which executes them with proper ToolContext injection. The agent receives tool results and incorporates them into its response generation.\nFinally, the agent produces its response, which the Runner streams back as events. The Runner appends this response to the conversation history in the session. State changes from tool execution have already persisted.\nThe session now reflects the complete conversation turn including user message, tool calls, and agent response.\nSession Isolation and Multi-User Support\nThe Runner's design inherently supports multiple concurrent users through session isolation. Each user gets their own session with independent state. The Runner ensures that state from one session never leaks into another.\nThis isolation is critical for building multi-tenant applications.\nasync\ndef\nmain\n():\napp_name =\n\"personal_assistant\"\nuser_id =\n\"user_12345\"\nsession_service = InMemorySessionService()\n    session =\nawait\ncreate_session(session_service, app_name, user_id)\n    session_id = session.id\n\n    agent = create_agent(app_name)\n    runner = create_runner(app_name, session_service, agent)\nThe\nuser_id\nparameter in every Runner operation enforces this isolation. When loading a session, the Runner verifies that the session belongs to the specified user.\nA single Runner instance can handle requests from multiple users safely. The Runner itself is stateless—all state resides in sessions.\nYou can create one Runner at application startup and use it for all users. Each\nrun()\ninvocation operates on its own session, maintaining complete isolation.\nThis architecture enables horizontal scaling of your application. Multiple server instances can share the same session service (like a database).\nEach server runs its own Runner instances, but all access the same session storage. The Runner's stateless design eliminates the need for sticky sessions or session affinity in load balancers.\nPractical Example: The Complete Flow\nLet's trace a complete conversation turn through the Runner-Session architecture.\nThis concrete example clarifies how all the pieces fit together.\nWe'll follow the message \"My favorite movie is The Matrix\" from input to persisted state.\nimport\nasyncio\nfrom\ndatetime\nimport\ndatetime\nfrom\ntyping\nimport\nDict, Any\nfrom\ngoogle.adk.sessions\nimport\nInMemorySessionService\nfrom\ngoogle.adk.agents\nimport\nAgent\nfrom\ngoogle.adk.runners\nimport\nRunner\nfrom\ngoogle.adk.tools.tool_context\nimport\nToolContext\nfrom\ngoogle.genai\nimport\ntypes\nfrom\ndotenv\nimport\nload_dotenv\n\nload_dotenv()\n# Custom functions to manage session state\ndef\nget_user_state\n(\ntool_context: ToolContext\n) -> Dict[str, Any]:\n\"\"\"Retrieve all user information from the session state.\n\n    Args:\n        tool_context: Automatically injected by ADK\n\n    Returns:\n        dict: User profile including name and preferences\n    \"\"\"\npreferences = tool_context.state.get(\n\"preferences\"\n, {})\nreturn\n{\n\"status\"\n:\n\"success\"\n,\n\"preferences\"\n: preferences,\n\"retrieved_at\"\n: datetime.now().isoformat()\n    }\ndef\nupdate_preference\n(\npreference_key: str, \n    preference_value: str, \n    tool_context: ToolContext\n) -> Dict[str, Any]:\n\"\"\"Update or add a user preference to the session state.\n\n    Args:\n        preference_key: The preference name (e.g., 'favorite_color', 'favorite_food')\n        preference_value: The new preference value\n        tool_context: Automatically injected by ADK\n\n    Returns:\n        dict: Operation status and updated preferences\n    \"\"\"\n# Get current preferences or initialize empty dict\npreferences = tool_context.state.get(\n\"preferences\"\n, {})\n# Update the specific preference\npreferences[preference_key] = preference_value\n# Save back to state\ntool_context.state[\n\"preferences\"\n] = preferences\nreturn\n{\n\"status\"\n:\n\"success\"\n,\n\"message\"\n:\nf\"Updated\n{preference_key}\nto\n{preference_value}\n\"\n,\n\"updated_preferences\"\n: preferences,\n\"updated_at\"\n: datetime.now().isoformat()\n    }\ndef\ninit_state\n():\nreturn\n{\n\"preferences\"\n: {\n\"favorite_color\"\n:\n\"blue\"\n,\n\"favorite_food\"\n:\n\"pizza\"\n}\n    }\nasync\ndef\ncreate_session\n(\nsession_service: InMemorySessionService, app_name: str, user_id: str\n):\nsession =\nawait\nsession_service.create_session(\n        app_name=app_name,\n        user_id=user_id,\n        state=init_state()\n    )\nreturn\nsession\nasync\ndef\nget_session\n(\nsession_service: InMemorySessionService, app_name: str, user_id: str, session_id: str\n):\nsession =\nawait\nsession_service.get_session(\n        app_name=app_name,\n        user_id=user_id,\n        session_id=session_id\n    )\nreturn\nsession\ndef\ncreate_agent\n(\napp_name: str\n):\nagent = Agent(\n        name=app_name,\n        model=\n\"gemini-2.0-flash-exp\"\n,\n        instruction=\n\"\"\"\n        You are a helpful personal assistant with access to user preferences and state information.\n\n        IMPORTANT GUIDELINES:\n        - When a user asks about their preferences, use the get_user_state tool to retrieve their information from the session state.\n        - When a user tells you about a new preference or wants to update an existing one, use the update_preference tool to save it to the session state.\n        - Provide personalized responses based on the user's stored preferences.\n\n        TOOL USAGE:\n        - get_user_state: Retrieves all user preferences\n        - update_preference: Updates or adds a new preference (takes preference_key and preference_value)\n\n        Be conversational and helpful. If you don't know something that isn't in the state, say so politely.        \n        \"\"\"\n,\n        tools=[get_user_state, update_preference]\n    )\nreturn\nagent\ndef\ncreate_runner\n(\napp_name: str, session_service: InMemorySessionService, agent: Agent\n):\nrunner = Runner(\n        app_name=app_name,\n        session_service=session_service,\n        agent=agent\n    )\nreturn\nrunner\ndef\ninvoke_message\n(\nrunner: Runner, user_id: str, session_id: str, message_text: str\n):\n\"\"\"Send a message to the agent and print the response.\n\n    Args:\n        runner: The Runner instance\n        user_id: The user ID\n        session_id: The session ID\n        message_text: The text message to send\n    \"\"\"\nuser_message = types.Content(\n        role=\n\"user\"\n,\n        parts=[types.Part(text=message_text)]\n    )\nfor\nevent\nin\nrunner.run(\n        user_id=user_id,\n        session_id=session_id,\n        new_message=user_message\n    ):\nif\nevent.is_final_response():\nif\nevent.content\nand\nevent.content.parts:\n                print(\nf\"Agent:\n{event.content.parts[\n0\n].text}\n\"\n)\nasync\ndef\nmain\n():\n# Variables\napp_name =\n\"personal_assistant\"\nuser_id =\n\"user_12345\"\n# Initialize session service\nsession_service = InMemorySessionService()\n    session =\nawait\ncreate_session(session_service, app_name, user_id)\n    session_id = session.id\n# Create agent\nagent = create_agent(app_name)\n# Initialize runner with agent and session service\nrunner = create_runner(app_name, session_service, agent)\n# Define test interactions\ntest_messages = [\n\"What is my favorite color?\"\n,\n\"What are all my preferences?\"\n,\n\"My favorite movie is The Matrix\"\n,\n\"What's my favorite movie?\"\n,\n    ]\n\n    print(\nf\"Session created with ID:\n{session_id}\n\"\n)\n    print(\nf\"Initial state: preferences=\n{session.state.get(\n'preferences'\n)}\n\\n\"\n)\n    print(\n\"=\"\n*\n80\n)\n# Execute multiple conversations to demonstrate state management\nfor\ni, message_text\nin\nenumerate(test_messages,\n1\n):\n        print(\nf\"\\n[Interaction\n{i}\n]\"\n)\n        print(\nf\"User:\n{message_text}\n\"\n)\n\n        invoke_message(runner, user_id, session_id, message_text)\n\n        print(\n\"-\"\n*\n80\n)\n# Show final state by retrieving the latest session\nfinal_session =\nawait\nsession_service.get_session(\n        app_name=app_name,\n        user_id=user_id,\n        session_id=session_id\n    )\n    print(\nf\"\\nFinal session state:\"\n)\nif\nfinal_session:\n        print(\nf\"  preferences:\n{final_session.state.get(\n'preferences'\n)}\n\"\n)\nelse\n:\n        print(\n\"  Session not found!\"\n)\nif\n__name__ ==\n\"__main__\"\n:\n    asyncio.run(main())\nWhen this message enters\ninvoke_message()\n, the function constructs a\ntypes.Content\nobject. The message text wraps in a Part, which wraps in a Content with role \"user\". This structured format ensures consistent message handling across the ADK.\nThe\nrunner.run\n()\ncall begins by loading the session. Using the provided user_id and session_id, the Runner retrieves the session from the service. The current state loads into memory:\n{\"preferences\": {\"favorite_color\": \"blue\", \"favorite_food\": \"pizza\"}}\n. The conversation history also loads, containing previous messages and responses.\nThe agent receives the user message along with its instructions and available tools. The language model analyzes the input: \"My favorite movie is The Matrix.\" Based on the instructions, it recognizes this as a preference declaration requiring the\nupdate_preference\ntool. The agent generates a tool call:\nupdate_preference(preference_key=\"favorite_movie\", preference_value=\"The Matrix\")\n.\nThe Runner intercepts this tool call before execution. It examines the\nupdate_preference\nfunction signature and identifies the\nToolContext\nparameter. The Runner constructs a ToolContext object containing a reference to the current session state. It injects this context along with the explicitly provided parameters: preference_key and preference_value.\nThe tool executes with full access to the session state. It retrieves the preferences dictionary:\n{\"favorite_color\": \"blue\", \"favorite_food\": \"pizza\"}\n. It adds the new preference:\npreferences[\"favorite_movie\"] = \"The Matrix\"\n. It writes the updated dictionary back to\ntool_context.state[\"preferences\"]\n.\nThe Runner detects the state modification after the tool returns. It immediately persists the updated state to the session service. The session now contains:\n{\"preferences\": {\"favorite_color\": \"blue\", \"favorite_food\": \"pizza\", \"favorite_movie\": \"The Matrix\"}}\n. The last_update_time timestamp updates to reflect this change.\nThe agent receives the tool's return value confirming the update. It generates a natural language response: \"I've saved your preference. Your favorite movie is now The Matrix!\" The Runner streams this response as events through the iterator. The final event contains the complete response text.\nThe conversation turn completes with all state changes persisted. Future messages in this session will see the updated preferences. When the next message asks \"What's my favorite movie?\", the agent finds the answer in the state. The Runner ensures perfect continuity across the entire conversation.\nPerformance Considerations in the Runner-Session Architecture\nThe Runner's orchestration introduces overhead that impacts conversation latency.\nUnderstanding performance characteristics helps you optimize for your application's requirements. Session operations, tool execution, and model inference all contribute to total response time.\nSession retrieval latency depends entirely on your session service choice.\nInMemorySessionService offers sub-millisecond access times.\nDatabaseSessionService introduces network and database query latency, typically 5-50ms.\nVertexAISessionService has similar characteristics to database storage but with additional managed service overhead.\nConsider implementing caching layers between the Runner and session storage. Frequently accessed sessions can cache in memory with Redis or Memcached.\nThis optimization dramatically reduces latency for active conversations. Cache invalidation strategies ensure users always see current state despite caching.\nTool execution time varies widely based on tool complexity.\nSimple state access tools complete in microseconds. Tools that call external APIs might take hundreds of milliseconds.\nThe Runner executes tools serially, so multiple tool calls compound latency.\nState persistence overhead depends on state size and session service implementation.\nSmall states (< 1KB) persist quickly even with database backends. Large states (> 100KB) introduce noticeable latency during updates.\nConsider normalizing your state structure or using references to external storage for large data.\nAdvanced Patterns: Session Sharing and Handoffs\nSome applications require advanced session management patterns. Multiple agents might collaborate on a single session. Conversations might transfer between channels or modalities. The Runner-Session architecture supports these scenarios through careful design.\nSession sharing allows different agents to access the same conversation state. Create multiple agents with different capabilities or personalities. Use the same session ID when invoking different runners. State changes from one agent become visible to others, enabling collaboration.\nConversation handoffs transfer sessions between systems. A chatbot might escalate to human support, maintaining session continuity. The human agent accesses the same session, seeing full conversation history and state. After resolution, the conversation can return to the AI agent using the same session.\nCross-channel continuity allows users to switch between interfaces seamlessly. A conversation might start in a web chat, continue on mobile, and finish via API. As long as all channels use the same session ID, state and history persist. The Runner's abstraction makes these complex scenarios straightforward to implement.\nConclusion\nThe Runner serves as the orchestration backbone of stateful conversational AI.\nIt bridges the gap between stateless language models and stateful applications.\nBy managing sessions, executing tools, and coordinating message flow, the Runner eliminates complexity that would otherwise plague your implementation.\nSessions provide the persistent memory that makes conversations truly intelligent. They store user preferences, conversation history, and application state.\nThe Runner ensures this state remains consistent, accessible, and secure throughout the conversation lifecycle.\nTogether, the Runner-Session architecture creates a robust foundation for production conversational AI.\nYou can focus on building great user experiences and powerful tools instead of wrestling with infrastructure concerns.\nThe patterns demonstrated here—session management, state persistence, tool integration, and conversation orchestration—scale from simple prototypes to systems serving millions of users.\nMaster this architecture and you master the fundamental challenge of conversational AI: making artificial intelligence truly remember and understand your users.\nThe code patterns shown here provide a solid starting point for building sophisticated agents that maintain context, learn preferences, and deliver increasingly personalized experiences over time.\nStart with the basic Runner-Session flow demonstrated in this article. Expand gradually as your requirements grow.\nThe architecture's flexibility supports evolution without requiring complete rewrites.\nYour agents will grow from simple question-answering systems into sophisticated assistants that truly understand and remember their users.\nPS:\nIf you like this article, share it with others ♻️\nWould help a lot ❤️\nAnd feel free to follow me for more content like this.\n#\nartificial-intelligence\n#\nagentic-ai\n#\nllm\n#\ngoogle-adk\n#\nlarge-language-models",
  "metadata": {
    "title": "The Runner-Session Architecture in Google ADK: Orchestrating Stateful AI Conversations",
    "description": "Your AI agent remembers that a user's favorite color is blue. It recalls their love for pizza and their excitement about The Matrix.\nBut here's the question that separates amateur implementations from production-grade systems: How does this memory ac...",
    "keywords": null,
    "author": null,
    "og:title": "The Runner-Session Architecture in Google ADK: Orchestrating Stateful AI Conversations",
    "og:description": "Your AI agent remembers that a user's favorite color is blue. It recalls their love for pizza and their excitement about The Matrix.\nBut here's the question that separates amateur implementations from production-grade systems: How does this memory ac...",
    "og:url": "https://juancolamendy.hashnode.dev/the-runner-session-architecture-in-google-adk-orchestrating-stateful-ai-conversations",
    "og:site_name": "Juan C Olamendy",
    "og:image": "https://cdn.hashnode.com/res/hashnode/image/upload/v1760970060768/390fcc54-7090-47c7-b3fc-3c11427488c6.png",
    "og:image:width": "1200",
    "og:image:height": "630",
    "og:type": "article",
    "twitter:card": "summary_large_image",
    "twitter:creator": "https://twitter.com/juancolamendy",
    "twitter:title": "The Runner-Session Architecture in Google ADK: Orchestrating Stateful AI Conversations",
    "twitter:description": "Your AI agent remembers that a user's favorite color is blue. It recalls their love for pizza and their excitement about The Matrix.\nBut here's the question that separates amateur implementations from production-grade systems: How does this memory ac...",
    "twitter:image": "https://cdn.hashnode.com/res/hashnode/image/upload/v1760970060768/390fcc54-7090-47c7-b3fc-3c11427488c6.png",
    "article:published_time": "2025-10-20T14:22:51.715Z",
    "article:author": "https://juancolamendy.hashnode.dev/@juancolamendy"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/b8a225081a2f/code/body-000.txt",
      "chars": 222,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-001.txt",
      "chars": 252,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-002.txt",
      "chars": 147,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-003.txt",
      "chars": 266,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-004.txt",
      "chars": 248,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-005.txt",
      "chars": 325,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-006.txt",
      "chars": 503,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-007.txt",
      "chars": 474,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-008.txt",
      "chars": 325,
      "language": "text"
    },
    {
      "file": "attachments/b8a225081a2f/code/body-009.txt",
      "chars": 5970,
      "language": "text"
    }
  ],
  "images": [
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1760970060768/390fcc54-7090-47c7-b3fc-3c11427488c6.png",
      "bytes": 1642067,
      "reused": true,
      "local_path": "attachments/b8a225081a2f/images/000.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1760970060768/390fcc54-7090-47c7-b3fc-3c11427488c6.png",
      "alt": "The Runner-Session Architecture in Google ADK: Orchestrating Stateful AI Conversations",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1760969396065/06afcada-3152-44c3-b1b0-1e718693e3d0.png",
      "bytes": 125215,
      "reused": true,
      "local_path": "attachments/b8a225081a2f/images/002.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1760969396065/06afcada-3152-44c3-b1b0-1e718693e3d0.png",
      "alt": "",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://juancolamendy.hashnode.dev/the-runner-session-architecture-in-google-adk-orchestrating-stateful-ai-conversations.md",
      "label": "View as Markdown",
      "type": "a"
    },
    {
      "href": "https://hashnode.com/@juancolamendy",
      "label": "J",
      "type": "a"
    },
    {
      "href": "https://twitter.com/juancolamendy",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://www.linkedin.com/in/juancolamendy/",
      "label": "",
      "type": "a"
    },
    {
      "href": "http://Runner.run/",
      "label": "Runner.run",
      "type": "a"
    },
    {
      "href": "http://runner.run/",
      "label": "runner.run",
      "type": "a"
    },
    {
      "href": "https://juancolamendy.hashnode.dev/tag/artificial-intelligence",
      "label": "# artificial-intelligence",
      "type": "a"
    },
    {
      "href": "https://juancolamendy.hashnode.dev/tag/agentic-ai",
      "label": "# agentic-ai",
      "type": "a"
    },
    {
      "href": "https://juancolamendy.hashnode.dev/tag/llm",
      "label": "# llm",
      "type": "a"
    },
    {
      "href": "https://juancolamendy.hashnode.dev/tag/google-adk",
      "label": "# google-adk",
      "type": "a"
    },
    {
      "href": "https://juancolamendy.hashnode.dev/tag/large-language-models",
      "label": "# large-language-models",
      "type": "a"
    }
  ],
  "retry_at": "2026-10-03T08:05:48",
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:50:34Z",
  "source_url": "https://juancolamendy.hashnode.dev/the-runner-session-architecture-in-google-adk-orchestrating-stateful-ai-conversations",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 30880,
  "expected_body_images": 2,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/b8a225081a2f/pages/source.html",
  "local_html": "attachments/b8a225081a2f/pages/article.html",
  "body_base_path": "attachments/b8a225081a2f/pages/",
  "body_markdown_path": "attachments/b8a225081a2f/pages/article.md"
}