{"slug": "step-by-step-guide-build-a-voice-agent-that-never-goes-silent-using-adk", "title": "Step by Step guide: Build a Voice Agent That Never Goes Silent using ADK", "summary": "Switching a voice agent's sales-report tool from a blocking call to a streaming tool in Google's open-source Agent Development Kit (ADK) with gemini-3.8-live cut the time to the agent's first words from 14.4 seconds to 1.2 seconds in a benchmark workflow, according to the guide. The guide shows that changing a tool's `return` to `yield` makes ADK mark the tool declaration as NON_BLOCKING and return an immediate status acknowledgement to the model, letting the agent narrate progress stages while a slow query runs instead of producing dead air. The approach targets live voice sessions via `run_live`, where a 13-second blocking database query otherwise yields 13 seconds of silence that leads users to assume the call dropped.", "body_md": "*A guide to* *streaming tools**, non-blocking function calls, and response scheduling in the* *Agent Development Kit (ADK) Gemini Live API Toolkit**.*\n\nIn text chat, a 13-second database query or RAG pipeline shows a loading spinner. In a live voice conversation, 13 seconds of blocking tool execution produces 13 seconds of dead air, causing users to assume the call dropped and hang up.\n\nYou cannot always make backend queries sub-second, so the fix must be architectural: keep the agent conversational while slow tools run in the background. Rather than forcing you to wire up custom client-side WebSocket event loops to manage asynchronous tool state, the open-source ADK and gemini-3.8-live let you implement non-blocking voice tools natively in Python. In our benchmark sales-report workflow on gemini-3.8-live, switching from a blocking tool to a streaming tool cut the time to the agent's first words from 14.4 seconds to 1.2 seconds.\n\nThis guide covers how to use streaming tools, non-blocking FunctionTool definitions, and response_scheduling to build responsive, natural voice agents.\n\nWatch the 1-minute ADK Live AI Concierge demo video to hear scheduled responses in action:\n\nImplementing non-blocking tool calls in ADK provides several immediate user experience benefits:\n\nIn standard agent development, a tool is defined as an async def function that completes its work and returns a single value:\n\n``` php\nfrom typing import Any\n\nasync def query_sales_database(region: str) -> dict[str, Any]:\n    \"\"\"Runs the quarterly sales report query for a region.\n\n    Args:\n      region: Sales region to report on, e.g. \"EMEA\" or \"APAC\".\n\n    Returns:\n      A dict containing report metrics upon completion.\n    \"\"\"\n    report = await run_expensive_query(region)       # e.g., 13 seconds of work\n    return {\"status\": \"success\", \"report\": report}   # Single return at the end\n```\n\nWhen passed directly to an `Agent` in ADK, this function creates a blocking tool call. During a Gemini Live API bidirectional voice session (`run_live`), the execution loop awaits the tool's return before generating further model turns.\n\nIf `query_sales_database` takes 13 seconds, the agent produces 13 seconds of complete silence. Prompting instructions such as \"tell the user you are working on it\" cannot resolve this, because the model's turn is blocked waiting for the function response payload.\n\nTo keep the conversation fluid, you can choose between two main non-blocking patterns depending on the desired user experience.\n\nWhen an operation consists of meaningful stages (such as connecting to a database, aggregating records, and formatting results), a **streaming tool** lets the agent narrate progress as it happens.\n\nIn ADK, turning a standard tool into a streaming tool requires changing only `return` to `yield`. By defining the function as an `AsyncGenerator`, ADK automatically marks the tool declaration as `NON_BLOCKING` and returns an immediate status acknowledgement to the model.\n\n``` python\nimport asyncio\nfrom typing import AsyncGenerator\nfrom google.adk.agents import Agent\n\nasync def query_sales_database(region: str) -> AsyncGenerator[str, None]:\n    \"\"\"Runs the quarterly sales report. Call this ONCE; it pushes updates automatically.\n\n    Args:\n      region: Sales region to report on, e.g. \"EMEA\" or \"APAC\".\n\n    Yields:\n      Progress updates while the query runs, followed by the final summary.\n    \"\"\"\n    stages = [\n        (\"Connecting to the sales warehouse...\", 1.5),\n        (\"Scanning order records...\", 3.5),\n        (\"Aggregating revenue by product line...\", 3.5),\n        (\"Formatting the report...\", 1.5),\n    ]\n    for message, delay in stages:\n        yield f\"PROGRESS: {message}\"\n        await asyncio.sleep(delay)\n\n    yield f\"DONE: {region} Q2 revenue reached $4.81M, up 12.4% year over year.\"\n\nagent = Agent(\n    name=\"sales_analyst\",\n    model=\"gemini-3.8-live\",\n    tools=[query_sales_database],\n    instruction=(\n        \"You are a helpful sales assistant. When the user asks about sales data, \"\n        \"call query_sales_database once. Acknowledge the request immediately, \"\n        \"briefly paraphrase each progress update, and present the final figures \"\n        \"when completed. Keep answers conversational and concise.\"\n    ),\n)\n```\n\n`query_sales_database`, ADK immediately dispatches an asynchronous status event to the model. The agent speaks right away (\"Looking into the EMEA figures for you now...\").`yield` sends a partial function response upstream. The model paraphrases each update as it arrives (\"Connecting to the warehouse...\", \"Aggregating product line data...\"), though narration can trail the tool by several seconds.`stop_streaming(function_name: str)` in the agent's tools list, ADK intercepts the call and cancels the generator's underlying `asyncio.Task`, raising `asyncio.CancelledError` within your function so you can cleanly close database connections or file handles.`interrupted=True` so your client can stop audio playback. Running streaming or background tools are not cancelled, and their results are still delivered. To stop a tool, the model must call `stop_streaming`.\nContinuous progress commentary is not always appropriate. If a user asks for a report and immediately pivots to another topic, an agent that continually interrupts with status updates can feel intrusive.\n\nWhen the wait has no story to narrate, run the task quietly in the background.\n\nAn effective way to get an immediate verbal acknowledgment without ongoing progress chatter is an async generator that yields only once, upon completion:\n\n``` php\nasync def query_sales_database(region: str) -> AsyncGenerator[dict, None]:\n    \"\"\"Runs the quarterly sales report. Call this ONCE; results arrive when ready.\"\"\"\n    report = await run_expensive_query(region)   # Background execution (~13s)\n    yield report                                 # Single yield upon completion\n```\n\nBecause this is an async generator, ADK issues an immediate pending response. The agent promptly acknowledges the request (\"I'm pulling that report together for you now\"), remains silent during computation, and presents the figures once ready or when the user finishes speaking.\n\nIf you prefer to keep standard coroutines (`async def ... -> dict`), wrap the function in a `FunctionTool` and configure `response_scheduling`:\n\n``` python\nfrom google.adk.tools import FunctionTool\nfrom google.genai import types\n\nsales_tool = FunctionTool(query_sales_database)\nsales_tool.response_scheduling = types.FunctionResponseScheduling.WHEN_IDLE\n(Note: In ADK 2.10+, you can also set `sales_tool.behavior = types.Behavior.NON_BLOCKING` to run a tool in the background without modifying scheduling mode.)\n```\n\n**Understanding the Trade-Off:** When using `response_scheduling` with a return-once coroutine, the model receives no payload until the entire function completes. As a result, the model typically does not utter an initial verbal acknowledgment unless the user continues speaking. This pattern is ideal when the user immediately continues with other questions and you want the result woven into the dialogue at the next natural pause.\n\nThe `response_scheduling` attribute controls *when* the model reacts to an incoming tool response. `WHEN_IDLE` never cuts into a turn in progress: if the model is still speaking or the user is still talking, the result waits until that turn ends, and the model responds at the next natural pause. ADK supports three scheduling behaviors:\n\nIn benchmark sessions with `gemini-3.8`-live:\n\n`WHEN_IDLE`, an incoming sales report waited politely while the model finished answering a separate 20-second question, then delivered the figures seamlessly.`INTERRUPT`, the incoming result cut off current speech mid-sentence; the model then finished its answer and appended the figures.`SILENT`, the result was added to the conversation history, available for subsequent turns without prompting an unrequested announcement.\nExecution shape (streaming vs. single-return) and delivery timing (`response_scheduling`) are independent and can be composed. Setting `response_scheduling` on a streaming tool stamps every partial update with that behavior.\n\nFurthermore, in ADK 2.8 and newer, streaming tools can dynamically specify scheduling per update by yielding `types.FunctionResponse`:\n\n```\n# Acknowledge and keep intermediate progress quiet:\nyield types.FunctionResponse(\n    response={\"stage\": \"aggregating\"},\n    scheduling=types.FunctionResponseScheduling.SILENT,\n)\n\n# Deliver the final result when the speaker finishes:\nyield types.FunctionResponse(\n    response={\"report\": final_data},\n    scheduling=types.FunctionResponseScheduling.WHEN_IDLE,\n)\n```\n\nThe rule of thumb: **If the wait has a story, stream it. If the wait is just a wait, run it in the background and let the result land when idle.**\n\nNon-blocking tools do not reduce backend execution time: a 13-second query still takes 13 seconds. What changes is the conversational presence of your agent.\n\nA web application that takes ten seconds to load is perceived as slow; a conversational voice agent that goes silent for ten seconds is perceived as broken. By pairing ADK's streaming tools and flexible response scheduling with the real-time capabilities of `gemini-3.8-live`, you can build voice assistants that remain attentive, natural, and present throughout complex operations.\n\n**Try it yourself:** the [interactive demo](https://adk-streaming-tools-761793285222.us-central1.run.app/demo) runs the same 13-second query through every tool pattern and response_scheduling mode on gemini-3.8-live, so you can hear the difference.\n\n`google-adk >= 2.10.0`` gemini-3.8-live` on `run_live`).", "url": "https://wpnews.pro/news/step-by-step-guide-build-a-voice-agent-that-never-goes-silent-using-adk", "canonical_source": "https://developers.googleblog.com/step-by-step-guide-build-a-voice-agent-that-never-goes-silent-using-adk/", "published_at": "2026-10-19 16:18:17.075381+00:00", "updated_at": "2026-10-19 16:18:19.096471+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "large-language-models", "generative-ai"], "entities": ["Agent Development Kit", "gemini-3.8-live", "Gemini Live API", "Google"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/step-by-step-guide-build-a-voice-agent-that-never-goes-silent-using-adk", "markdown": "https://wpnews.pro/news/step-by-step-guide-build-a-voice-agent-that-never-goes-silent-using-adk.md", "text": "https://wpnews.pro/news/step-by-step-guide-build-a-voice-agent-that-never-goes-silent-using-adk.txt", "jsonld": "https://wpnews.pro/news/step-by-step-guide-build-a-voice-agent-that-never-goes-silent-using-adk.jsonld"}}