Three Lanes Below One Millisecond: A Rust SDK for Gemini Live

Why Rust for Full-Duplex Voice Agents

The speaker introduces the motivation for the talk: sharing hard-learned lessons from building full-duplex voice agents in Python and why the team ultimately moved to Rust. They explain that Python's garbage collection caused memory consumption issues and latency glitches at scale (500,000+ users), and that Rust was chosen for its strong memory control, especially for cost-sensitive deployments like calling 10 million users per week in India.

Half-Duplex vs True Speech-to-Speech Systems

The speaker distinguishes between the common STT+LLM+TTS 'half duplex' pattern and true full-duplex speech-in speech-out systems, polling the audience on their experience with each. They highlight that while voice-to-voice models offer great expressive fidelity for use cases like customer support or virtual receptionists, they suffer from limitations like static session-level instructions and poor dynamic tool-calling support.

The Transcript as the Control Loop

The speaker argues that for full-duplex streaming models, the transcript—not the system prompt—is the most valuable control loop for steering conversations deterministically without sacrificing tool calling. They describe a process of tracking turn boundaries, computing state from the streaming transcript, and mapping that state to conversational phases, noting that lightweight regex-based state machines can often replace expensive LLM calls for this purpose.</br>

State Watches and the Cost of Latency

Building on the regex-based approach, the speaker explains 'state watches' that detect changes or composite changes in conversation state to trigger actions, turning raw transcript text into structured, GPS-like state tracking. They discuss the immense computational challenge of recomputing this state every 20 milliseconds as audio and transcript data stream in, and how async processing delays can cause noticeable, uncomfortable pauses in voice conversations.

Real-Time Constraints: Frame Rates and Barge-In Handling

The speaker details the hard real-time constraints of voice agents, including audio frames arriving every 20ms (50 frames/second) with no tolerance for delay, and the complexity of handling user 'barge-in' events where the model must pause, listen, and decide whether to repeat or pivot its planned response. This sets up the rationale for a specialized low-latency architecture built with Rust crates.

The Three-Layer Crate Architecture (L0, Agent, Fluent)

The speaker introduces their open-source Rust package built as three layered crates: an L0 layer for low-level WebSocket and memory control, a middle agent layer (a Rust rebuild of Google's ADK framework comparable to LangGraph or CrewAI), and a top 'fluent' layer using stream-processing style operators familiar to data engineers working with Spark or Apache Beam. A Python FFI wrapper is planned so developers can leverage these capabilities without learning Rust.

Three-Lane Processing: Fast, Control, and Telemetry

The speaker describes the three-lane processor design for production voice agents: a sub-millisecond fast lane for deterministic operations like regex, an async control lane for variable-latency tasks like LLM calls and tool dispatch, and a telemetry lane for monitoring WebSocket health, token consumption, and context window usage to support long-duration calls (e.g., 60-minute sales calls) via dynamic context compression.

Shared State Spine and Deterministic Control

The speaker clarifies that rather than building yet another agent framework, they created a shared state spine over the transcript that multiple consumers can read from at different paces, enabling both reactive and synchronous processing patterns. They emphasize that this brings necessary determinism to otherwise arbitrary model behavior, filling a gap not yet addressed by frontier model SDKs, and reiterate that Python ADK is unsuitable for real-time, millisecond-budget production voice systems.

Production Scale Results and Open Source Release

The speaker closes by sharing production results from their Rust-based system: over a million outbound calls per day and 100,000+ inbound calls per day for demanding vernacular-language contact centers in India. They announce the project is open-sourced under Apache 2.0 license (which will remain unchanged), share the repository and crates link, and invite community contributions.

Thank you so much. Let me know when you guys can hear me fine. So one of the key reasons why we wanna have this talk was to share a little bit from the trenches what we experienced when we started building full duplex voice agents with Python, and the sort of challenges that we had, and when we were scaling for countries like India where they were looking at cost effective means of potentially calling probably 10,000,000 users per week, or even high value customers where even the slightest glitch or pause would really ruin the voice user experience. And and these these are hard learned lessons, and there's a reason why we chose Rust.

I could literally complete this entire conversation by saying that, hey, Python's garbage collection is really bad. And when you start scaling to 500,000 users, you eventually see that the memory consumption goes haywire, and things start of pausing no matter how horizontally scalable your system is. And therefore, why Rust? Because I'm a sucker for punishment, and I like experimenting with new languages and its capabilities.

We wanted extremely strong controls on memory consumption, and therefore we chose Rust as the platform. So just a quick show of hands, how many of you have worked with or productionized speech to speech models? Amazing. So I'm sure there are some people who felt the same pains that I have. It's great to see some of my comrades in the group.

Not the STT plus LLM plus TTS pattern, which is known as half duplex, where you essentially fake the experience of a real voice to voice application, but a true speech in speech out full duplex system. Can I see a show of hands for those who've at least played around with or experimented with? Amazing. I see a few people there as well.

Look, the reason why this is so critical is the applications are really vast and wide. We could start from customer support to being a virtual receptionist for a small boutique practice. The kind of things that we could monetize this voice to voice capability is immense. However, the problem is that these voice to voice models trade granularity for raw expressive power.

While they give you a really great sense that you're talking to a human, or at least the fidelity is great, the fundamental challenge is there's something like session boundaries, you can only send a single instruction, and it usually tends to be massive. And it sucks at tool calling, and you can only define a static tool at the beginning of the session, or a collection of tools at the beginning of the session.

And you struggle when you have to dynamically inject tools. While they handle arbitrariness fantastically well, it's almost impossible to sort of get the determinism that we need by essentially having a conversational flow. What we need is a face machine that steers the conversation in a very specific way while not compromising on tool calling.

And at the same time, you cannot wait for these, say, complex state machines to compute. You'll need absolutely low computational footprint to make this work. So if you look at it, what we're proposing is that for full duplex streaming models, the transcript is the most valuable part that you can use as a control loop. Most people try to use the system prompt that you send to the native voice model as the control loop, is okay if you're doing a toy use case.

But for anything of economic consequence, you will have to operate on the transcript. And the transcript is a streaming transcript, and it fills at different phases. There are turn boundaries. The user speaks, the model speaks. You've got to constantly sort of do this tightrope dance of figuring out where the turn boundaries are, look at the transcript, compute state from that, and then use that state to compute a conversational phase, or essentially map that to a conversational phase, and then use that phase to control how the conversation flows through.

Right? Pretty simple. The idea is that, yeah, state machines and regex go really well together. You don't need an LLM to constantly operate on the transcript. You could essentially have a regex of a million odd patterns to essentially do what a small capable LLM can do. This is one of those things that we completely forgot.

Like, for example, if you want the customer to repeat after what you just said, as an example, you're collecting debt, you want to quote a regulation and say that, do you understand? You've got to repeat to consent. These are sorts of things that you can compute with regex in a conversational flow, and it gives ultra low latency.

You don't need to waste an LLM call to do that. Right? The second thing is you've got to chop, detect, and steer. What this means is you've got to put what you call state watches. Like a lot of conversations can be modeled by the changes in state, meaning that if state a went to state b, then trigger this, and so on and so forth.

Or it could be composite state where you say state a, state b, state c, they all change at the same time, or in the same turn, then trigger. So what you're really doing is you're using the raw text transcript to construct a structured output. And the structured output becomes your GPS to detect where you are in the global conversation state, and then you define how the conversation should go forward. So the problem is that this takes a lot of computation in memory.

And this happens every twenty milliseconds because audio frames and transcript arrive in streaming format. When do you decide how to recompute your state? How do you do this consistently? Like, how many loops can you run? These are sorts of questions that, you know, voice agents can make it extremely hard because the problem is if you await and trust an async process to run, there could be a glitch or a pause that you can't control, which you can feel in the voice conversation. Most people may have felt this awkward silence when sometimes you have a conversation with the model.

The awkward silence is horrendous. You can't necessarily process that because can't make up for a lag by processing ahead of time much faster because voice is sort of this continuous interaction that happens. So these are challenges that exist. The cost of a pause is really massive. The fundamental problem that we faced this has nothing to do with Google's ADK framework. It was built on Python.

We have a Go framework and JavaScript framework right now, but nothing comes quite close to rust when it comes to memory efficiency and how much control you can pin down on memory. But we don't want all of our customers or developers to become suckers for punishment by developing this from the ground up for rust. So we developed this and open sourced this package.

See, the the the first reality that we've got to account for is that audio frames arrive every 20 in a streaming audio. Whether it's GPT real time or Gemini live, the speech to speech models will keep streaming audio back in, And that's about 50 frames per second and one round trip per frame. It's not going to cut any slack for you.

Right? And then you've got to think about barge in, meaning that the user can essentially speak. While the model's speaking, you've got to make sure you pause and you listen, and then you understand what to do with the parts that you didn't play. For example, in some cases, the model may have said something of consequence and may have planned to say something on consequence and the user barged in. In those cases, you've got to repeat what you previously planned to say.

Or in this case, you might say, okay, the conversation went to a completely different state. Now I cannot talk about that topic. Now we're moving to a different topic. Think about this intuitiveness of how a human does this. They're computing all of this and deciding where they are at in this conversation phase, where you begin, where you are at, and where you end. So these are computations that you need to do, and you can't wait. So we decided to do a three layer crate build. So for those of you who worked on Rust, think of crates like packages, essentially.

And we said, Okay, for those of you that love working on the wire, who know how to work with WebSocket binary frames, who want to have even more control over how memory buffers and other things get allocated, go operate on the L0 layer. For those of you who are used to the agent way of doing things, which means that you're defining agent one, agent two, they communicate with each other in this way.

They pass this information. For those of you who built on Landgraf, CrewAI, or say ADK, this is the layer that we did. We rebuilt ADK on Rust so that we can do it. What we're trying to do is wrap this with a Python FFI so that you don't need to learn Rust, even at an SDK level, to take advantage of this.

So that's our next part of the project. And the top layer is the fluent layer. For those of you who understand what fluent operators are, any data engineers in the group here, people that write Apache Beam or Spark pipelines, they would realize that you operate most of this voice as a stream of data operations. So the transcript is a streaming text transcript, and you model your operations like a DAG or a directed acyclic graph.

So for those of you who are more comfortable with doing that, we created those verbs and nouns. And think of this like syntactic sugar. It uses the layers below to give you the same level of capability. So we're saying that a production voice agent should look like this, instruction, greeting, on audio, on text, on tool call.

Think of these as events, and you can write and compute state at all of these key events in the life cycle of a voice conversation. And we've created a three lane processor to make sure that the right things go to the right swim lanes. The idea is that we do not want to actually, you know, make the routers compute state so that there's no locks and no await that that breaks your overall computational loop.

The idea is that we created three lanes. One is a fast lane, which runs less than one millisecond. The second one is a control lane or an async lane. This is the place where you can't necessarily tell how long it will take to process it. For example, a transcript sending to an LLM can respond in one second, three seconds, or five seconds.

Right? But a regex, you can mostly pin it down. Right? Or if you write maybe an open source NLP model that you're using may not be transformer based, Their latency profile might be different. So you want to give a flexibility on the tool dispatch, face detection, control loops, and so on. And the last part is the telemetry line.

This is super critical because what you're looking at is the health of the WebSocket connection between the client and the server. You're looking at the number of frames that are going in and out. You're looking at token consumption. You're looking at how much context in the voice session has gotten filled to trigger certain things like summarization. Meaning that if you have to have a really long voice call, like most of these, models support maybe ten to twenty minutes of uninterrupted conversation, if you want to extend the life cycle of that call for much longer, say sixty minutes, if you're doing a selling pitch for an outbound SDR and you want the call to last for maybe sixty minutes, if you wanna do things like that, you've gotta dynamically trigger context compression and so on and so forth.

So we're creating another lane to make sure that those life cycle operations can stay away and stop interrupting with these. So the idea is we need something less than one millisecond. For control loops, it's async because you can't control the latency profile. For telemetry, you are okay with one millisecond as well, but it needs to come through a separate lane.

There are certain things that broke in the native Python ADK. I didn't wanna go into the details. I think I spoke about some of these things. While it may seem like we're rebuilding another agent building framework, we did not. The way we sort of want to model this is there's a shared state spine which operates on top of the transcript.

Every other person that consumes from it is a consumer, and they consume at different pace. They consume at different intervals. They recompute, some are reactive, some are synchronous. So think of these like a collection of utilities that can help you really steer and bring in a certain amount of determinism into a really capable model that can handle arbitrariness.

Right? So it needs to be deterministic where it must be. Example, if you want to model it as a DAG, let's say it needs to take these three steps before the fourth step needs to be taken, you need that kind of control loops. That's the sort of capabilities that we're building. No SDKs that these frontier model companies are building have this because they've not seen too many customers deploy this at this scale.

What we're doing is seeing the initial bunch of customers who are really pushing the limits of this. Eventually, you'll see these capabilities sort of absorb get absorbed back into the product, but right now, it's really the Wild West. So I'm not saying Python ADK is bad. It it is not suitable for a real time hot path. If you want to use production voice agents at scale with a millisecond budget, you know, you've gotta move to rust.

And the idea is that the latency budget is the architecture here for voice agents. You do not have any, you know, sort of, let's say, headroom for you to absorb, like text agent, where the model's probably thinking, the user's probably reading. You don't have any UX forgiveness. It's completely unforgiving. So therefore, we chose Rust and we rebuilt ADK, And we sort of were able to make a million calls a day outbound at scale, receive over 100,000 calls a day inbound, fairly demanding contact centers on Vernac languages in India.

But it's sort of a testimony to what sort of scale the system can sustain. That is it. I've shared the repo along with the crates link. Please do take it around for a spin. I have a contributing guide in there. I'm happy, and I'm looking for contributors from the community. We will not change the license type. It will be Apache two dot O, and it will continue to be Apache two DotO.

Thank you.

Real-time voice • Rust • Gemini Live

Three Lanes Below One Millisecond.

A Rust SDK for Gemini Live. Because you can't await audio frames

Vamsi Ramakrishnan

AI Lead Engineer, APAC - Google Cloud

Audience poll

Show of hands,
how many of you have worked with /
productionized speech-to-speech models?

aie2023.io - webster umapathy

VOICE AGENTS

Calls in, calls out.

The same sub-millisecond runtime, pointed two directions.

INBOUND

the line rings

  1. Customer support

    Triages, answers, resolves - escalates with the full transcript.

  2. Virtual receptionist

    Books, reschedules and confirms while you're with someone else.

    Doctor, Lawyer, Nail salon

OUTBOUND

it makes the call

  1. Payment reminders

    Follows up on overdue balances - polite, scheduled, tireless.

  2. Parts sourcing

    Rings suppliers to confirm stock and lead times.

  3. Quote collection

    Calls a list of vendors and returns the quotes side by side.

Two diagrams show a circle with an arrow: one points into the circle (labeled INBOUND), and one points out of the circle (labeled OUTBOUND). Below the content, a waveform visualization with red and grey vertical bars indicates audio activity.

The S2S architecture

Native voice models (S2S) trade granularity for raw expressive power.

01 / LIMITS

  • Session boundaries.
  • Unlike text streams, speech sessions have hard temporal limits (e.g. Gemini Live, GPT Realtime) that dictate context window management.

02 / STEERING

  • Single instruction.
  • The model operates under a single system prompt. You cannot pivot the core behavior mid-stream without a session reset.

03 / TOOLS

  • Static tool calls.
  • Tool definitions are locked at runtime. Modifying capabilities on the fly is not supported in native speech-to-speech loops.

04 / THE PAYOFF

  • Handle arbitrariness.
  • Despite constraints, they are extremely powerful at navigating the messy, non-linear nature of human speech and interruption.

Voice Agents

Calls in, calls out.

The same sub-millisecond runtime, pointed two directions.

Inbound

The line rings

  1. Customer support

    Triages, answers, resolves — escalates with the full transcript.

  2. Virtual receptionist

    Books, reschedules and confirms while you're with someone else.

    Examples: Doctor Lawyer Nail salon

Outbound

It makes the call

  1. Payment reminders

    Follows up on overdue balances — polite, scheduled, tireless.

  2. Parts sourcing

    Rings suppliers to confirm stock and lead times.

  3. Quote collection

    Calls a list of vendors and returns the quotes side by side.

A graphic representing a sound wave or audio equalizer is displayed at the bottom of the slide, with some bars highlighted in red.

The architecture

The control loop for native S2S models.

01 / I/O ENGINE

Full-duplex streaming.

Simultaneous handling of audio and text frames to ensure zero-latency feedback loops.

02 / STATE DERIVATION

Low-latency observer.

A separate text model monitors the transcript stream to compute state and inject phase controls into the conversation.

gemini-rs • system architecture

Diagram illustrating a control loop. A central circular element labeled 'S2S MODEL' has two inputs: 'Audio In' and 'Transcript In'. It has two outputs: 'Audio Out' and 'Transcript Out'. Lines indicate data flow from inputs to the S2S model, and from the model to outputs. A dashed line from the 'Transcript Out' output leads back to the 'I/O Engine' section and also to the 'State Derivation' section, indicating a feedback loop.

The control. plane

The real-time transcript is an out-of-band control mechanism.

01 / DERIVE STATE

Compute state from the stream.

Use the live text buffer as a high-value signal to maintain a deterministic foundation for the conversation.

02 / LOGIC ENGINE

State machines & regex.

Drive logic via simple regex or low-latency LLMs to trigger transitions without waiting for the full audio response.

03 / FLOW CONTROL

Chop, detect, and steer.

Detect turn boundaries instantly, prune system prompts dynamically, and inject state to steer the model cadence.

The asymmetry

Full-duplex voice punishes the choices text-only agents forgive.

You can't await an audio frame. You can't ship a stutter.

01 - The Problem

The cost of a pause.

Why real-time voice breaks every assumption text agents are built on.

The starting point

Google's Python ADK is a beautiful kit for building agents — until audio frames refuse to wait.

Text agent

A 50 ms hiccup is invisible. The user is still reading the last token.

Voice agent

A 50 ms hiccup is a seam. The user hears the model think.

The cadence

20.

Audio frames arrive every twenty milliseconds.

They refuse to wait.

  • 50 frames per second - one round trip per frame - no slack

goanna.rs - the promise

Two ways voice breaks.

  • 50. A garbage-collection pause is a perceptible glitch. The user hears the seam.
  • barge-in A blocked event loop is a user talking over your model. Full-duplex means both can speak at once — and you must keep listening.

02 – The Rebuild

Three layered crates.

Wire protocol, agent runtime, fluent DX – rebuilt from scratch in Rust.

The stack

Pick the altitude that fits the problem.

L2 gemini-adk-fluent-rs
Fluent builder - SCT.P.M.A operator algebra - a voice agent in 20 lines
L1 gemini-adk-rs
Agent runtime - phase machines - typed prefix-scoped state - tool-dispatch - governed flows
L0 gemini-genai-rs
Zero-copy Websocket transport - JSON codec - auth - VAD - jitter buffer

Each layer depends only on the one below it; import from the altitude you need.

gemini-rs - the rebuild

A production voice agent.

Declarative Rust – minimal boilerplate, maximum control.

  • Closures, not callbacks.
    Direct event handling without the boilerplate of traits.
  • Builders, not configs.
    Type-safe construction that catches errors at compile time.
  • The wire is invisible.
    Zero-copy transport and streaming managed automatically.
let handle = live::builder()
    .instruction("You are a helpful assistant.")
    .greeting("Say hello to the user.")
    .on_audio(|data| speaker.send(data))
    .on_text(|t| print!("{:?}", t))
    .on_tool_call(|calls, state| async move {
        // None // auto-dispatched via tools()
    })
    .connect_google_ai(api_key)
    .await?;
handle.send_text("Hello").await?;

Closures, not callbacks. Builders, not configs. The wire is invisible.
go.ag.rs – the website

The three-lane processor.

One router. Three lanes. Each tuned to its own latency profile.

The hot-path rule

The router does no work.

No state. No locks. No await.

Every event is dispatched into one of three lanes –
and the dispatcher itself never blocks.

Hot path ↔ cold path

gemini-rs • the keystone

Three-lane processor

router

zero-work dispatcher - no state on the hot path

  • Fast lane
    • sync < 1 ms
    • audio frames
    • VAD events
    • transcripts
    • thought parts
    • no alloc . no lock . no async
  • Control lane
    • async
    • tool dispatch
    • phase transitions
    • extractors (parallel)
    • watchers . computed
    • owns the TranscriptBuffer
  • Telemetry lane
    • own broadcast - ~1 ns
    • SessionSignals → state
    • atomic counters
    • on_usage callback
    • debounced 100 ms flush

gemini-rs . the keystone

hot path + cold path

Diagram illustrating a "router" component at the top, which feeds into three distinct lanes: a Fast lane, a Control lane, and a Telemetry lane. The Fast lane is emphasized with a red border, representing the "hot path", while the Control and Telemetry lanes represent the "cold path".

Lane 1 • Fast

< 1ms

Synchronous. No allocations. No locks. No await.

You cannot await an audio frame.

Lane 2 • Control

async

Tool dispatch. Phase transitions. Extractors. Watchers.

It owns the transcript buffer outright — no Arc<Mutex<>>.

gemini-rs • the keystone

Lane 3 - Telemetry

~1.

Atomic counters on their own broadcast. Debounced 100 ms flush.

Observability that never touches the hot path.

goinfra.rs the system

04 - Lessons from the Port

What broke in the naive translation.

Two patterns the Python idiom rewards ~ and the wire punishes.

Async all the way down.

The port

Every callback is async. The audio handler awaits a future, yields to the executor, and the frame waits behind everything else in the queue.

The fix

A synchronous fast lane. Audio and VAD run inline, off the executor entirely – the frame is handled before anything can preempt it.

  • async is a tool, not a style.

Trap 02

Arc<Mutex<>> on the hot path.

The port

Shared, locked transcript state. Under barge-in, the audio lane and the control lane contend for the same mutex – and the speaker stutters.

The fix

Single ownership. The control lane owns the transcript buffer outright; no other lane can touch it. No lock to contend for.

• If you can name an owner, you don't need a lock.

Tools without rebuilding LangChain.

Phases, tools, extractors, watchers – combinators over one shared store.

The shared spine

Everything converges on one typed store.

  • Phases: structure
  • Tools: actions
  • Extractors: facts
  • Watchers: reactions
  • Telemetry: signals

STATE: the shared spine - concurrent - typed - prefix-scoped (app | session | derived | turn | bg)

Phases gate. Extractors write. Watchers fire. Telemetry populates. Everything reads from, and writes to, one place.

Combinators over one store — not a framework you inherit.

A diagram illustrates a central "STATE" store. Five components, "Phases", "Tools", "Extractors", "Watchers", and "Telemetry", are positioned above the central store. Lines connect these components downwards to the "STATE" store, visually representing how everything converges on this single typed store.

governed agents

Deterministic where it must be.

  • FLOW
    governed DAG - gates calls, enforces order
  • EXTRACT
    typed facts via CPU recognizers, no model
  • RESOLVER
    sub-agent / fetch / LLM → result into State
let Flow = Flow::new()
  .step("verify").allow(["lookup_account"])
  .done(Guard::is_true("verified"))
  .step("take_payment").after("verify")
  .allow(["charge_card"]).never("charge_card")
  .until(Guard::is_true("ptp"))
  .build();

live::builder().tools(d).govern(flow);

Three lenses over one spine.
Model-driven only where it must be.

pontus.rs - composition

Division of labor

Where the Python ADK still belongs.

  • python
    • exploration & notebooks
    • the breadth of the ecosystem
    • research, evals, batch work
    • where iteration speed wins
  • rust
    • the real-time hot path
    • production voice at scale
    • the millisecond budget
    • where the wire is unforgiving

Not a replacement. A complement — at the altitude the wire demands.

gemini-rs • division of labor

In closing

Three lanes.
Below one millisecond.

The latency budget is the architecture.

Thank you.

Vamsi Ramakrishnan
AI Lead Engineer, APAC - Google Cloud

Apache-2.0 · 90% Rust · three layered crates

gemini-rs · v0.7.0

Technologies & Tools

  • Apache Beam
  • Go
  • JavaScript
  • Python
  • Python FFI
  • Regex
  • Rust
  • Spark

Standards & Specs

  • Apache 2.0 License
  • WebSocket

Concepts & Methods

  • Barge-in
  • Context compression
  • Crates
  • Directed Acyclic Graph
  • Full duplex voice agents
  • Half duplex
  • Speech-to-speech models
  • State machines
  • STT plus LLM plus TTS

Organisations & Products

  • CrewAI
  • Gemini Live
  • Google ADK
  • GPT Realtime
  • LangGraph