{"id":46613160,"url":"https://github.com/vamsiramakrishnan/gemini-rs","last_synced_at":"2026-04-01T18:18:02.573Z","repository":{"id":341715992,"uuid":"1169765266","full_name":"vamsiramakrishnan/gemini-rs","owner":"vamsiramakrishnan","description":"Full Rust SDK for the Gemini Multimodal Live API — wire protocol, agent runtime, and fluent DX in three layered crates.","archived":false,"fork":false,"pushed_at":"2026-03-17T12:39:33.000Z","size":1891,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-17T22:03:45.973Z","etag":null,"topics":["adk","agent-framework","async-rust","function-calling","gemini","gemini-api","google-ai","llm","multimodal","real-time","rust","tokio","vertex-ai","voice-agents","websocket"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/rs-genai","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vamsiramakrishnan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-01T07:06:43.000Z","updated_at":"2026-03-17T12:39:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vamsiramakrishnan/gemini-rs","commit_stats":null,"previous_names":["vamsiramakrishnan/gemini-rs"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/vamsiramakrishnan/gemini-rs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vamsiramakrishnan%2Fgemini-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vamsiramakrishnan%2Fgemini-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vamsiramakrishnan%2Fgemini-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vamsiramakrishnan%2Fgemini-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vamsiramakrishnan","download_url":"https://codeload.github.com/vamsiramakrishnan/gemini-rs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vamsiramakrishnan%2Fgemini-rs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290807,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adk","agent-framework","async-rust","function-calling","gemini","gemini-api","google-ai","llm","multimodal","real-time","rust","tokio","vertex-ai","voice-agents","websocket"],"created_at":"2026-03-07T19:04:09.217Z","updated_at":"2026-04-01T18:18:02.545Z","avatar_url":"https://github.com/vamsiramakrishnan.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gemini-rs\n\n\u003e Full Rust SDK for the Gemini Multimodal Live API -- wire protocol, agent runtime, and fluent DX in three layered crates.\n\n[![CI](https://github.com/vamsiramakrishnan/gemini-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/vamsiramakrishnan/gemini-rs/actions/workflows/ci.yml)\n[![Docs](https://github.com/vamsiramakrishnan/gemini-rs/actions/workflows/docs.yml/badge.svg)](https://github.com/vamsiramakrishnan/gemini-rs/actions/workflows/docs.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![crates.io](https://img.shields.io/crates/v/gemini-live.svg)](https://crates.io/crates/gemini-live)\n[![Rust](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](https://www.rust-lang.org)\n\n---\n\n## Why gemini-rs?\n\nGoogle's Gemini Multimodal Live API enables full-duplex, real-time voice and\ntext conversations with tool calling, streaming audio, and mid-session\ninstruction updates. Building on it raw means wrestling with WebSocket frame\nparsing, binary/text codec differences between Google AI and Vertex AI,\nauthentication token management, voice activity detection, barge-in handling,\nand turn lifecycle -- before you write a single line of agent logic.\n\n**gemini-rs** eliminates that friction. It gives you a layered Rust SDK where\neach crate adds exactly the abstraction you need:\n\n- **Wire-level access** for custom transports, proxies, or non-standard\n  deployments (`gemini-live`).\n- **Agent runtime** with typed state, phase machines, tool dispatch, text agent\n  combinators, and a three-lane processor architecture (`gemini-adk`).\n- **Fluent builder API** where a production voice agent is 20 lines of\n  declarative Rust, not 200 lines of boilerplate (`gemini-adk-fluent`).\n\nEvery layer is independently usable. Pick the altitude that fits your problem.\n\n### Raw WebSocket vs. Fluent API\n\n\u003ctable\u003e\n\u003ctr\u003e\u003cth\u003eRaw WebSocket (L0 only)\u003c/th\u003e\u003cth\u003eFluent API (L2)\u003c/th\u003e\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\n\n```rust\n// Connect, subscribe, send, match events,\n// handle tool calls, manage turns, track\n// state, parse audio frames ...\nlet session = quick_connect(\n    \"KEY\", \"gemini-2.0-flash-live-001\"\n).await?;\nsession.send_text(\"Hello\").await?;\nlet mut events = session.subscribe();\nwhile let Ok(event) = events.recv().await {\n    match event {\n        SessionEvent::Audio(data) =\u003e {\n            /* decode, buffer, play */\n        }\n        SessionEvent::TextDelta(t) =\u003e {\n            print!(\"{t}\");\n        }\n        SessionEvent::ToolCall(calls) =\u003e {\n            // dispatch, build responses,\n            // send back ...\n        }\n        SessionEvent::TurnComplete =\u003e break,\n        _ =\u003e {}\n    }\n}\n```\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```rust\nlet handle = Live::builder()\n    .instruction(\"You are a helpful assistant.\")\n    .greeting(\"Say hello to the user.\")\n    .on_audio(|data| speaker.send(data))\n    .on_text(|t| print!(\"{t}\"))\n    .on_tool_call(|calls, state| async move {\n        // auto-dispatched with .tools()\n        None\n    })\n    .connect_google_ai(\"KEY\")\n    .await?;\n\nhandle.send_text(\"Hello\").await?;\n```\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n---\n\n## Architecture\n\n```\n+----------------------------------------------------------------------+\n|  gemini-adk-fluent  (L2 -- Fluent DX)                                    |\n|                                                                      |\n|  Live::builder()  .  AgentBuilder  .  S.C.T.P.M.A operators         |\n|  PhaseBuilder  .  WatchBuilder  .  Temporal patterns                 |\n+----------------------------------------------------------------------+\n|  gemini-adk  (L1 -- Agent Runtime)                                       |\n|                                                                      |\n|  LiveSessionBuilder  .  LiveHandle  .  Three-lane processor          |\n|  State (prefix-scoped)  .  PhaseMachine  .  ToolDispatcher           |\n|  TextAgent combinators  .  Extractors  .  Watchers  .  Telemetry    |\n|  LlmAgent  .  Runner  .  SessionService  .  MCP  .  A2A            |\n+----------------------------------------------------------------------+\n|  gemini-live  (L0 -- Wire Protocol)                                     |\n|                                                                      |\n|  Transport (WebSocket + Mock)  .  Codec (JSON)  .  Auth providers    |\n|  SessionHandle  .  Protocol types  .  VAD  .  Jitter buffer         |\n|  Telemetry (OTel + Prometheus)  .  REST APIs (feature-gated)         |\n+----------------------------------------------------------------------+\n```\n\nEach layer depends only on the one below it. Application code imports from the\nhighest layer it needs (`gemini_adk_fluent::prelude::*` re-exports all three).\n\n---\n\n## Core Concepts \u0026 How They Interplay\n\nA gemini-rs voice session is built from six core concepts that work together.\nThis section shows what each one does and how they connect.\n\n```\n                         +------------------+\n                         |   Live::builder  |  (L2 Fluent API)\n                         +--------+---------+\n                                  |  configures\n          +-----------+-----------+-----------+-----------+\n          |           |           |           |           |\n     +----v---+  +----v----+  +--v---+  +----v----+  +--v--------+\n     | Phases |  |Extractors| | Tools |  |Watchers |  | Telemetry |\n     +----+---+  +----+----+  +--+---+  +----+----+  +-----+-----+\n          |           |          |           |              |\n          +-----+-----+----+----+-----+-----+              |\n                |          |          |                     |\n          +-----v----------v----------v-----+        +-----v-----+\n          |            State                |        | Signals \u0026 |\n          |  (prefix-scoped, concurrent)    |\u003c-------+ Counters  |\n          +---------------------------------+        +-----------+\n```\n\n### 1. State -- The Shared Spine\n\nEverything reads from and writes to `State`. It is the single source of truth\nfor a session -- a concurrent, typed key-value store with prefix-scoped\nnamespaces.\n\n```\nState\n  |\n  +-- app:caller_name = \"Alice\"          (application state)\n  +-- session:turn_count = 5             (auto-tracked by SessionSignals)\n  +-- session:total_token_count = 1284   (auto-tracked from UsageMetadata)\n  +-- derived:risk_level = \"high\"        (computed variable, read-only)\n  +-- turn:transcript = \"I need help\"    (cleared each turn)\n  +-- bg:verification_status = \"pending\" (background agent result)\n```\n\n**Why it matters:** Phase transitions check state. Extractors write to state.\nWatchers fire when state changes. Computed variables derive from state.\nTelemetry auto-populates state. Everything converges here.\n\n### 2. Phases -- Conversation Structure\n\nPhases define the *shape* of a conversation: what the model should do, what\ntools are available, and when to move on.\n\n```\n  [greeting] ---\u003e [identify_caller] ---\u003e [handle_request] ---\u003e [farewell]\n       |               |                       |                    |\n   instruction:    instruction:            instruction:         instruction:\n   \"Welcome...\"   \"Get name...\"          \"Help with...\"       \"Say goodbye\"\n       |               |                       |\n   tools: []       tools: [lookup]         tools: [search, calc]\n       |               |                       |\n   transition:     transition:             transition:\n   caller_name     request_type            resolved == true\n   is_some()       is_some()\n```\n\nEach phase declares:\n- **Instruction**: what the model should do (static or state-driven dynamic)\n- **Tools**: which tools are available in this phase\n- **Transitions**: state predicates that trigger moves to the next phase\n- **Guards**: predicates that must be true before entering a phase\n- **Needs**: state keys still required (drives navigation context)\n- **Lifecycle hooks**: `on_enter` / `on_exit` for side effects\n\nPhases don't micromanage the model. They set guardrails -- the LLM naturally\nasks follow-up questions until the transition predicate becomes true.\n\n### 3. Extractors -- Structured Data from Conversation\n\nExtractors run out-of-band LLM calls to pull structured data from the\nconversation transcript and write it into State.\n\n```\n Conversation transcript        OOB LLM call           State\n +-----------------------+     +---------------+     +------------------+\n | \"Hi, I'm Alice from   | --\u003e | Extract with  | --\u003e | caller_name:     |\n |  Acme Corp, I need    |     | JSON Schema   |     |   \"Alice\"        |\n |  help with billing.\"  |     +---------------+     | caller_org:      |\n +-----------------------+                           |   \"Acme Corp\"    |\n                                                     | request_type:    |\n                                                     |   \"billing\"      |\n                                                     +------------------+\n                                                           |\n                                                    triggers phase\n                                                    transition!\n```\n\n**Extraction triggers** control *when* extractors fire:\n\n| Trigger | When it fires | Use case |\n|---------|--------------|----------|\n| `EveryTurn` | After every TurnComplete | Default, high-frequency extraction |\n| `Interval(n)` | Every N turns | Reduce LLM costs for slow-changing data |\n| `AfterToolCall` | After tool dispatch completes | Extract from tool results |\n| `OnPhaseChange` | When phase transitions fire | Re-extract on context shift |\n\n### 4. Watchers \u0026 Temporal Patterns -- Reactive State\n\nWatchers observe state changes and fire callbacks. Temporal patterns detect\nconditions that persist over time or turns.\n\n```\n  State change: app:score = 0.85 --\u003e 0.95\n                    |\n            +-------v--------+\n            | Watcher:       |\n            | crossed_above  |\n            | threshold=0.9  |\n            +-------+--------+\n                    |\n            fires callback:\n            state.set(\"alert\", true)\n\n\n  Condition held for 30s:          3 consecutive turns:\n  +-------------------------+     +-------------------------+\n  | when_sustained:         |     | when_turns:             |\n  | confused == true        |     | repeating == true       |\n  | for 30 seconds          |     | for 3 turns             |\n  | --\u003e offer help          |     | --\u003e break loop           |\n  +-------------------------+     +-------------------------+\n```\n\n### 5. Tools -- Model Actions\n\nTools give the model the ability to take actions. gemini-rs supports typed\ntools (auto-schema from Rust structs), simple tools (raw JSON), built-in\ntools (Google Search, code execution), and agent-as-tool (text agent pipelines\ncallable by the live model).\n\n```\n  Model decides to call tool\n           |\n  +--------v---------+\n  |  ToolDispatcher   |  Routes by function name\n  +--+-----+-----+---+\n     |     |     |\n  +--v-+ +-v--+ +v---------+\n  |get_| |calc| |verify_   |\n  |wx  | |pay | |identity  |\n  +----+ +----+ +----------+\n  Simple  Typed   AgentTool\n  Tool    Tool    (text agent\n                   pipeline)\n\n  Background tools: model continues talking\n  while the tool executes asynchronously.\n```\n\n**Background tool execution** eliminates dead air in voice sessions. Mark\ntools as background and the model receives a \"processing\" acknowledgment\nimmediately, continuing the conversation while the tool runs:\n\n```rust\nLive::builder()\n    .tools(dispatcher)\n    .tool_background(\"search_kb\")  // runs async, no dead air\n```\n\n### 6. Telemetry -- Observability Pipeline\n\nTelemetry flows through two complementary systems, both running on the\ntelemetry lane (off the hot path):\n\n```\n  SessionEvent stream\n        |\n  +-----v--------------+     +------------------+\n  | SessionSignals      |     | SessionTelemetry |\n  | (State keys)        |     | (Atomic counters)|\n  +-----+---------------+     +--------+---------+\n        |                              |\n        v                              v\n  session:turn_count          audio_chunks_out: 1482\n  session:total_token_count   avg_latency_ms: 340\n  session:is_speaking         interruptions: 3\n  session:silence_ms          total_token_count: 5280\n        |                              |\n        v                              v\n  Available to phases,         snapshot() --\u003e JSON\n  watchers, extractors,        for devtools UI\n  transition guards\n```\n\n**SessionSignals** writes to State -- so phases, watchers, and extractors can\nreact to session-level metrics (e.g., transition after N turns, alert when\ntokens exceed budget).\n\n**SessionTelemetry** tracks lock-free atomic counters (~1ns per operation) for\nperformance metrics: audio throughput, response latency (min/avg/max via CAS),\nturn duration, token usage, and interruption counts.\n\n**UsageMetadata** from the Gemini API is automatically tracked at all layers:\n- L0 emits `SessionEvent::Usage(UsageMetadata)` with full token breakdowns\n- L1 records in both SessionSignals (state keys) and SessionTelemetry (atomics)\n- L2 exposes `.on_usage(|metadata| ...)` callback for real-time observation\n\n### How They Work Together\n\nHere's the flow for a single model turn in a phased conversation:\n\n```\n  User speaks: \"I'm Alice from Acme Corp\"\n       |\n  [1]  v  Fast lane: on_audio, on_input_transcript (sync, \u003c1ms)\n       |\n  [2]  v  Model responds, turn completes\n       |\n  [3]  v  Control lane: TranscriptBuffer records the turn\n       |\n  [4]  v  Extractors run (OOB LLM call)\n       |    --\u003e writes caller_name=\"Alice\", caller_org=\"Acme Corp\" to State\n       |\n  [5]  v  Watchers fire on state changes\n       |    --\u003e crossed_above, became_true, changed_to callbacks\n       |\n  [6]  v  Computed variables recompute\n       |    --\u003e derived:risk_level updates based on new state\n       |\n  [7]  v  Phase machine evaluates transitions\n       |    --\u003e caller_name.is_some() == true\n       |    --\u003e transition: identify_caller --\u003e handle_request\n       |\n  [8]  v  Phase on_exit / on_enter hooks fire\n       |    --\u003e instruction updated, navigation context regenerated\n       |\n  [9]  v  Telemetry lane: SessionSignals + SessionTelemetry update\n            --\u003e session:turn_count++, latency recorded, tokens tracked\n```\n\n---\n\n## Quick Start\n\n### Google AI (API Key)\n\n```rust\nuse gemini_adk_fluent::prelude::*;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let handle = Live::builder()\n        .model(GeminiModel::Gemini2_0FlashLive)\n        .instruction(\"You are a friendly assistant.\")\n        .on_text(|t| print!(\"{t}\"))\n        .on_turn_complete(|| async { println!(\"\\n---\") })\n        .connect_google_ai(std::env::var(\"GEMINI_API_KEY\")?)\n        .await?;\n\n    handle.send_text(\"What is the speed of light?\").await?;\n    tokio::signal::ctrl_c().await?;\n    handle.disconnect().await?;\n    Ok(())\n}\n```\n\n### Vertex AI\n\n```rust\nlet handle = Live::builder()\n    .model(GeminiModel::Gemini2_0FlashLive)\n    .voice(Voice::Kore)\n    .instruction(\"You are a customer support agent.\")\n    .on_audio(|data| playback_tx.send(data.clone()).ok())\n    .on_text(|t| print!(\"{t}\"))\n    .connect_vertex(\"my-project\", \"us-central1\", access_token)\n    .await?;\n```\n\n### Wire Level Only (L0)\n\n```rust\nuse gemini_live::prelude::*;\n\nlet session = gemini_live::quick_connect(\n    \"API_KEY\", \"gemini-2.0-flash-live-001\"\n).await?;\nsession.send_text(\"What is the speed of light?\").await?;\n\nlet mut events = session.subscribe();\nwhile let Ok(event) = events.recv().await {\n    if let SessionEvent::TextDelta(ref text) = event {\n        print!(\"{text}\");\n    }\n    if let SessionEvent::TurnComplete = event { break; }\n}\n```\n\n---\n\n## Crate Overview\n\n| Crate | Layer | Description |\n|-------|-------|-------------|\n| [`gemini-live`](crates/gemini-live) | L0 -- Wire | Protocol types, WebSocket transport, auth providers, VAD, jitter buffer, REST APIs (feature-gated). Full Rust equivalent of Google's `@google/genai`. |\n| [`gemini-adk`](crates/gemini-adk) | L1 -- Runtime | Agent runtime with state management, phase machines, tool dispatch, text agent combinators, extractors, watchers, telemetry. Full Rust equivalent of Google's `@google/adk`. |\n| [`gemini-adk-fluent`](crates/gemini-adk-fluent) | L2 -- Fluent | `Live::builder()` API, `AgentBuilder`, S.C.T.P.M.A operator algebra, composition patterns, test utilities. |\n\n---\n\n## Features\n\n### Voice / Live Sessions\n\nBuild full-duplex voice sessions with callbacks for every event type. Audio,\ntext, transcription, interruptions, and turn lifecycle are all handled.\n\n```rust\nlet handle = Live::builder()\n    .model(GeminiModel::GeminiLive2_5FlashNativeAudio)\n    .voice(Voice::Puck)\n    .instruction(\"You are a weather assistant.\")\n    .greeting(\"Greet the user and ask how you can help.\")\n    .transcription(true, true)          // input + output transcription\n    .thinking(1024)                     // enable thinking with token budget\n    .include_thoughts()                 // receive thought summaries\n    .affective_dialog(true)             // emotionally expressive responses\n    .context_compression(4000, 2000)    // auto-compress context window\n    .on_audio(|data| speaker.write(data))\n    .on_thought(|text| println!(\"[Thought] {text}\"))\n    .on_input_transcript(|text, _final| println!(\"[User] {text}\"))\n    .on_output_transcript(|text, _final| println!(\"[Agent] {text}\"))\n    .on_interrupted(|| async { speaker.flush().await })\n    .on_turn_complete(|| async { println!(\"--- turn complete ---\") })\n    .on_usage(|usage| {\n        if let Some(total) = usage.total_token_count {\n            println!(\"Tokens used: {total}\");\n        }\n    })\n    .connect_vertex(project, location, token)\n    .await?;\n```\n\n**Available voices:** `Aoede`, `Charon`, `Fenrir`, `Kore`, `Puck` (default), or `Voice::Custom(\"name\")`.\n\n### Thinking (Gemini 2.5+)\n\nThe `gemini-2.5-flash-native-audio-preview-12-2025` model supports thinking\ncapabilities with dynamic thinking enabled by default. Control the thinking\nbudget and receive thought summaries in your session:\n\n```rust\nlet handle = Live::builder()\n    .model(GeminiModel::Custom(\n        \"models/gemini-2.5-flash-native-audio-preview-12-2025\".into(),\n    ))\n    .thinking(1024)           // set thinking token budget (0 = disable)\n    .include_thoughts()       // receive thought summaries via on_thought\n    .on_thought(|text| println!(\"[Thought] {text}\"))\n    .on_text(|t| print!(\"{t}\"))\n    .connect_google_ai(api_key)\n    .await?;\n```\n\n**How it works in the three-lane architecture:**\n\n- `thinkingConfig` (`thinkingBudget`, `includeThoughts`) is sent in the setup\n  message's `generationConfig`\n- When `includeThoughts` is true, thought parts arrive as `Part::Thought` in\n  `model_turn` content — emitted as `SessionEvent::Thought(String)`\n- Thought events are routed to the **fast lane** and delivered via the\n  `on_thought` sync callback (\u003c 1ms, no allocations)\n\n**Platform support:** Google AI only. On Vertex AI, `thinkingConfig` is\nautomatically stripped from the setup message — no code changes needed.\n\n### Tool Calling\n\nDeclare function tools with JSON Schema parameters. The SDK auto-dispatches\ntool calls when you provide a `ToolDispatcher`, or you can handle them manually\nin `on_tool_call`.\n\n```rust\nlet handle = Live::builder()\n    .instruction(\"You can check the weather and do math.\")\n    .on_tool_call(|calls, state| async move {\n        let responses: Vec\u003cFunctionResponse\u003e = calls.iter().map(|call| {\n            let result = match call.name.as_str() {\n                \"get_weather\" =\u003e json!({\"temp\": 22, \"condition\": \"sunny\"}),\n                _ =\u003e json!({\"error\": \"unknown tool\"}),\n            };\n            FunctionResponse {\n                name: call.name.clone(),\n                response: result,\n                id: call.id.clone(),\n                scheduling: None,\n            }\n        }).collect();\n        Some(responses)\n    })\n    .connect_google_ai(api_key)\n    .await?;\n```\n\nOr use built-in tools directly:\n\n```rust\nLive::builder()\n    .google_search()        // Google Search grounding\n    .code_execution()       // Sandbox code execution\n    .url_context()          // URL content retrieval\n```\n\n### State Management\n\nA concurrent, type-safe `State` container with prefix-scoped namespaces,\natomic read-modify-write, delta tracking, and transparent derived fallbacks.\n\n```rust\nuse gemini_adk::State;\nuse gemini_adk::state::StateKey;\n\n// Typed keys eliminate typo bugs\nconst TURN_COUNT: StateKey\u003cu32\u003e = StateKey::new(\"session:turn_count\");\nconst SENTIMENT: StateKey\u003cString\u003e = StateKey::new(\"derived:sentiment\");\n\nlet state = State::new();\n\n// Prefix-scoped accessors\nstate.app().set(\"flag\", true);              // writes to \"app:flag\"\nstate.user().set(\"name\", \"Alice\");          // writes to \"user:name\"\nstate.session().set(\"turn_count\", 0u32);    // writes to \"session:turn_count\"\nstate.turn().set(\"transcript\", \"hello\");    // writes to \"turn:transcript\"\n\n// Atomic read-modify-write\nstate.modify(\"session:turn_count\", 0u32, |n| n + 1);\n\n// Transparent derived fallback: get(\"risk\") auto-checks \"derived:risk\"\nstate.set(\"derived:risk\", 0.85);\nlet risk: Option\u003cf64\u003e = state.get(\"risk\");  // returns Some(0.85)\n\n// Delta tracking for transactional state\nlet tracked = state.with_delta_tracking();\ntracked.set(\"temp:scratch\", 42);\ntracked.commit();   // merge into main store\n// or: tracked.rollback();\n```\n\n**Prefix namespaces:**\n\n| Prefix | Purpose | Lifetime |\n|--------|---------|----------|\n| `session:` | Auto-tracked signals (turn count, tokens, timing) | Session |\n| `derived:` | Read-only computed variables | Session |\n| `turn:` | Cleared each turn | Turn |\n| `app:` | Application state | Session |\n| `bg:` | Background task state | Session |\n| `user:` | User-scoped state | Session |\n| `temp:` | Scratch space | Explicit |\n\n### Phase System\n\nDeclarative conversation phase management with guard-based transitions,\nper-phase tool filtering, instruction composition, and async lifecycle callbacks.\n\n```rust\nlet handle = Live::builder()\n    .phase(\"greeting\")\n        .instruction(\"Welcome the user warmly.\")\n        .prompt_on_enter(true)\n        .transition_with(\"identify\", |s| {\n            s.get::\u003cString\u003e(\"caller_name\").is_some()\n        }, \"when caller provides their name\")\n        .done()\n    .phase(\"identify\")\n        .instruction(\"Confirm the caller's identity.\")\n        .needs(\u0026[\"caller_name\", \"caller_org\"])\n        .tools(vec![\"lookup_contact\".into()])\n        .transition_with(\"handle\", |s| {\n            s.get::\u003cbool\u003e(\"verified\").unwrap_or(false)\n        }, \"when identity is verified\")\n        .done()\n    .phase(\"handle\")\n        .dynamic_instruction(|s| {\n            let topic: String = s.get(\"topic\").unwrap_or_default();\n            format!(\"Help the caller with: {topic}\")\n        })\n        .tools(vec![\"search\".into(), \"calc\".into()])\n        .transition_with(\"farewell\", |s| {\n            s.get::\u003cbool\u003e(\"resolved\").unwrap_or(false)\n        }, \"when the request is resolved\")\n        .done()\n    .phase(\"farewell\")\n        .instruction(\"Say goodbye and provide a reference number.\")\n        .terminal()\n        .done()\n    .initial_phase(\"greeting\")\n    // Phase defaults inherited by all phases\n    .phase_defaults(|p| {\n        p.with_state(\u0026[\"caller_name\", \"caller_org\"])\n         .navigation()  // inject phase navigation context\n    })\n    // Recommended: set persona once, steer via context injection\n    .steering_mode(SteeringMode::ContextInjection)\n    .connect_vertex(project, location, token)\n    .await?;\n```\n\n#### Steering Modes\n\nControl how the SDK delivers phase instructions to the model. This is the most\nimpactful configuration choice for multi-phase apps:\n\n| Mode | System Instruction | Phase Instructions | Best For |\n|------|--------------------|--------------------|----------|\n| `ContextInjection` | Set once at connect | Delivered as model-role context turns | Multi-phase apps with stable persona (**recommended**) |\n| `InstructionUpdate` | Replaced on every transition | Baked into system instruction | Agents with radically different personas per phase |\n| `Hybrid` | Replaced on transition | Modifiers as context turns | Persona shifts + per-turn steering |\n\n```rust\n// Recommended: base persona at connect, phase context injected per turn\nLive::builder()\n    .instruction(\"You are a helpful assistant.\")\n    .steering_mode(SteeringMode::ContextInjection)\n```\n\n#### Context Delivery Timing\n\nControl when model-role context turns hit the wire:\n\n| Mode | Behavior | Best For |\n|------|----------|----------|\n| `Immediate` (default) | Send as single batched frame during TurnComplete | Low-latency, text-only apps |\n| `Deferred` | Queue until next user send (audio/text/video) | Voice apps — eliminates mid-silence frames |\n\n```rust\n// Voice app: flush context alongside user audio, not during silence\nLive::builder()\n    .steering_mode(SteeringMode::ContextInjection)\n    .context_delivery(ContextDelivery::Deferred)\n```\n\nWith `Deferred`, the `DeferredWriter` wraps the session writer and drains pending context before each `send_audio`/`send_text`/`send_video`. Context that requires a prompt (e.g. `prompt_on_enter`) is always sent immediately.\n\nSee the [Steering Modes guide](docs/user-guide/steering-modes.md) for the full\ndecision matrix, anti-patterns, and implementation details.\n\n#### Phase Navigation Context\n\nThe `.navigation()` modifier injects a structured description of the current\nphase graph into the model's instruction, giving it awareness of where it is,\nwhat it still needs, and where it can go:\n\n```\n[Navigation]\nCurrent phase: identify -- Confirm the caller's identity.\nPrevious: greeting (turn 2)\nStill needed: caller_org\nPossible next:\n  -\u003e handle: when identity is verified\n```\n\nThis is auto-generated from `.needs()`, `.transition_with()` descriptions, and\nphase history. The model can use this to guide the conversation naturally.\n\n### Extraction Pipeline\n\nRun out-of-band LLM calls to extract structured data from the conversation\ntranscript. Schema-guided via `schemars::JsonSchema`.\n\n```rust\nuse schemars::JsonSchema;\n\n#[derive(Deserialize, Serialize, JsonSchema)]\nstruct CallerInfo {\n    caller_name: Option\u003cString\u003e,\n    caller_org: Option\u003cString\u003e,\n    request_type: Option\u003cString\u003e,\n}\n\nlet handle = Live::builder()\n    .instruction(\"You are a receptionist.\")\n    // Extract every 2 turns instead of every turn (reduces LLM costs)\n    .extract_turns_triggered::\u003cCallerInfo\u003e(\n        flash_llm,\n        \"Extract caller name, organization, and request type\",\n        5,  // transcript window size\n        ExtractionTrigger::Interval(2),\n    )\n    .on_extracted(|name, value| async move {\n        println!(\"Extracted {name}: {value}\");\n    })\n    .connect_vertex(project, location, token)\n    .await?;\n\n// Read latest extraction at any time\nlet info: Option\u003cCallerInfo\u003e = handle.extracted(\"CallerInfo\");\n```\n\nExtractors automatically enable transcription and warm up the OOB LLM\nconnection at session start for fast first-extraction latency.\n\n### State Watchers \u0026 Temporal Patterns\n\nReact to state changes and time-based conditions declaratively:\n\n```rust\nLive::builder()\n    // Fire when app:score crosses above 0.9\n    .watch(\"app:score\")\n        .crossed_above(0.9)\n        .then(|_old, _new, state| async move {\n            state.set(\"high_score_alert\", true);\n        })\n    // Fire when a boolean becomes true\n    .watch(\"app:escalated\")\n        .became_true()\n        .blocking()   // block turn processing until complete\n        .then(|_old, _new, _state| async move {\n            notify_supervisor().await;\n        })\n    // Fire when condition holds for 30 seconds continuously\n    .when_sustained(\"user_confused\",\n        |s| s.get::\u003cbool\u003e(\"confused\").unwrap_or(false),\n        Duration::from_secs(30),\n        |_state, writer| async move { /* offer help */ },\n    )\n    // Fire after 3 consecutive turns matching condition\n    .when_turns(\"stuck_in_loop\",\n        |s| s.get::\u003cbool\u003e(\"repeating\").unwrap_or(false),\n        3,\n        |_state, writer| async move { /* break loop */ },\n    )\n```\n\n### Computed (Derived) State\n\nRegister reactive computed variables that update when their dependencies change:\n\n```rust\nLive::builder()\n    .computed(\"risk_level\", \u0026[\"app:sentiment_score\"], |state| {\n        let score: f64 = state.get(\"app:sentiment_score\")?;\n        if score \u003c 0.3 { Some(json!(\"high\")) }\n        else { Some(json!(\"low\")) }\n    })\n    // Read transparently: state.get(\"risk_level\") auto-checks \"derived:risk_level\"\n```\n\n### Text Agent Combinators\n\nBuild complex request/response LLM pipelines that can be dispatched from\nLive session hooks. These use standard `generate()` calls (not WebSocket\nsessions), enabling background processing during a voice conversation.\n\n| Combinator | Purpose |\n|-----------|---------|\n| `LlmTextAgent` | Core agent -- generate, tool dispatch, loop |\n| `FnTextAgent` | Zero-cost state transform (no LLM call) |\n| `SequentialTextAgent` | Run children in order, state flows forward |\n| `ParallelTextAgent` | Run children concurrently via `tokio::spawn` |\n| `LoopTextAgent` | Repeat until max iterations or predicate |\n| `FallbackTextAgent` | Try each child, first success wins |\n| `RouteTextAgent` | State-driven deterministic branching |\n| `RaceTextAgent` | Run concurrently, first to finish wins |\n| `TimeoutTextAgent` | Wrap an agent with a time limit |\n| `MapOverTextAgent` | Iterate an agent over a list in state |\n| `TapTextAgent` | Read-only observation (no mutation) |\n| `DispatchTextAgent` | Fire-and-forget background tasks |\n| `JoinTextAgent` | Wait for dispatched tasks |\n\nRegister text agents as tools the live model can call. The agent shares\nthe session's `State`, so mutations are visible to watchers and phase\ntransitions:\n\n```rust\nLive::builder()\n    .agent_tool(\"verify_identity\", \"Verify caller identity\", verifier_agent)\n    .agent_tool(\"calc_payment\", \"Calculate payment plans\", calc_pipeline)\n```\n\n### S.C.T.P.M.A Composition\n\nSix operator namespaces for composing different aspects of agent configuration:\n\n| Namespace | Operator | Purpose | Example |\n|-----------|----------|---------|---------|\n| `S::` | `\u003e\u003e` | State transforms | `S::set(\"key\", val) \u003e\u003e S::rename(\"a\", \"b\")` |\n| `C::` | `+` | Context engineering | `C::last_n(5) + C::system_only()` |\n| `T::` | `\\|` | Tool composition | `T::function(search) \\| T::google_search()` |\n| `P::` | `+` | Prompt composition | `P::role(\"assistant\") + P::task(\"summarize\")` |\n| `M::` | `\\|` | Middleware composition | `M::log() \\| M::rate_limit(10)` |\n| `A::` | `+` | Artifact schemas | `A::produces(schema) + A::consumes(schema)` |\n\n**Prompt composition example:**\n\n```rust\nuse gemini_adk_fluent::prelude::*;\n\nlet prompt = P::role(\"a customer support agent for Acme Corp\")\n    + P::task(\"help customers with billing inquiries\")\n    + P::constraint(\"never reveal internal pricing formulas\")\n    + P::guidelines(vec![\n        \"Be empathetic and professional\",\n        \"Confirm resolution before closing\",\n    ]);\n\nlet instruction = prompt.render();\n```\n\n### Callback Modes\n\nControl-lane callbacks support two execution modes:\n\n| Mode | Method suffix | Behavior |\n|------|--------------|----------|\n| **Blocking** | `.on_turn_complete()` | Awaited inline -- event loop waits |\n| **Concurrent** | `.on_turn_complete_concurrent()` | Spawned as detached task -- fire and forget |\n\nUse concurrent mode for logging, analytics, webhook dispatch, or background\nagent triggering where you don't need ordering guarantees.\n\n### REST APIs (Feature-Gated)\n\nThe L0 crate also provides feature-gated access to Gemini REST APIs beyond\nthe Live WebSocket connection:\n\n```toml\n[dependencies]\ngemini-live = { version = \"0.1\", features = [\"generate\", \"embed\", \"files\"] }\n# Or enable everything:\n# gemini-live = { version = \"0.1\", features = [\"all-apis\"] }\n```\n\n| Feature | API |\n|---------|-----|\n| `generate` | Content generation (`generateContent`) |\n| `embed` | Text embeddings |\n| `files` | File upload and management |\n| `models` | Model listing and info |\n| `tokens` | Token counting |\n| `caches` | Context caching |\n| `tunings` | Fine-tuning jobs |\n| `batches` | Batch prediction |\n| `chats` | Multi-turn chat sessions |\n\n---\n\n## Three-Lane Processor Architecture\n\nAll Live session events are routed through a zero-copy dispatcher into three\nindependent lanes, each optimized for its latency profile:\n\n```\n  SessionEvent (broadcast from L0)\n         |\n    +----+----+\n    |  Router  |   Zero-work dispatcher -- NO state access on hot path\n    +--+--+--+-+\n       |  |  |\n       |  |  +------------------------------+\n       |  +----------------+                 |\n       |                   |                 |\n  +----v---------+   +-----v----------+  +---v--------------+\n  | Fast Lane    |   | Control Lane   |  | Telemetry Lane   |\n  | (sync \u003c1ms)  |   | (async)        |  | (own broadcast)  |\n  +--------------+   +--------------  +  +------------------+\n  | on_audio     |   | on_tool_call   |  | SessionSignals   |\n  | on_text      |   | on_interrupted |  |  (State keys)    |\n  | on_vad_*     |   | Phase trans.   |  | SessionTelemetry |\n  | on_input_    |   | Extractors     |  |  (AtomicU64)     |\n  |   transcript |   |  (concurrent)  |  | on_usage cb      |\n  | on_output_   |   | Watchers       |  | Debounced 100ms  |\n  |   transcript |   | Computed state |  |   flush          |\n  +--------------+   | Temporal ptns  |  +------------------+\n                     | TranscriptBuf  |\n                     |  (owned, no    |\n                     |   mutex)       |\n                     +----------------+\n```\n\n**Design constraints:**\n- Fast lane callbacks must be sync and complete in \u003c 1ms (no allocations, no locks, no async)\n- Control lane owns the `TranscriptBuffer` exclusively (no `Arc\u003cMutex\u003c\u003e\u003e`)\n- Telemetry lane runs on its own broadcast receiver (never blocks the router)\n- Extractors run concurrently via `futures::future::join_all`\n\n---\n\n## Examples\n\nThe `examples/` directory contains runnable examples organized by complexity.\nEach demonstrates specific SDK features at the layer you need.\n\n### Getting Started\n\n```bash\n# 1. Configure credentials\ncp .env.example .env\n# Edit .env: set GEMINI_API_KEY (Google AI) or GOOGLE_CLOUD_PROJECT + GOOGLE_CLOUD_LOCATION (Vertex AI)\n\n# 2. Run a standalone example\ncargo run -p text-chat       # http://127.0.0.1:3001\ncargo run -p voice-chat      # http://127.0.0.1:3002\ncargo run -p tool-calling    # http://127.0.0.1:3003\ncargo run -p transcription   # http://127.0.0.1:3004\n\n# 3. Run the multi-app Web UI (all apps + devtools panel)\ncargo run -p gemini-adk-web         # http://127.0.0.1:3000\n```\n\n### Standalone Examples\n\nThese run independently with their own Axum server and minimal UI.\n\n| Example | Port | Layer | What You Learn |\n|---------|------|-------|----------------|\n| [`text-chat`](examples/text-chat) | 3001 | L0 | Wire protocol basics — connect, send text, receive streaming deltas |\n| [`voice-chat`](examples/voice-chat) | 3002 | L0 | Bidirectional audio, voice selection, VAD events, transcription |\n| [`tool-calling`](examples/tool-calling) | 3003 | L1 | `TypedTool` with auto-generated JSON Schema, `ToolDispatcher` routing |\n| [`transcription`](examples/transcription) | 3004 | L0 | Every Gemini Live config option: VAD, activity handling, affective dialog, context compression, session resumption |\n| [`agents`](examples/agents) | CLI | L1/L2 | Text agent combinators (`\u003e\u003e`, `\\|`, `/`), `TypedTool`, copy-on-write builders |\n\n### ADK Web UI (`gemini-adk-web`)\n\nThe Web UI bundles all apps below into a single Axum server with a shared\ndevtools panel showing real-time state, timeline, transcript, and telemetry.\n\n#### Crawl (Beginner)\n\n| App | What It Demonstrates | Key SDK Features |\n|-----|---------------------|-----------------|\n| **text-chat** | Minimal text-only session — no microphone needed | `Live::builder().text_only()`, text streaming |\n| **voice-chat** | Native audio chat with real-time transcription | `Modality::Audio`, voice selection, input/output transcription |\n| **tool-calling** | Three demo tools: weather, time, calculator | `FunctionDeclaration`, `on_tool_call`, `NonBlocking` behavior, `WhenIdle` scheduling |\n\n#### Walk (Intermediate)\n\n| App | What It Demonstrates | Key SDK Features |\n|-----|---------------------|-----------------|\n| **all-config** | Configuration playground — every Gemini Live option in one app | Dynamic tool creation, modality switching, Google Search, code execution, context compression |\n| **guardrails** | Real-time policy monitoring with corrective injection | `RegexExtractor`, `.watch()` state reactions, `.instruction_amendment()`, PII/off-topic/sentiment detection |\n| **playbook** | 6-phase customer support flow with state extraction | `.phase()` chains, `.transition_with()` guards, `.greeting()`, `.with_context()`, `RegexExtractor` |\n\n#### Run (Advanced)\n\n| App | What It Demonstrates | Key SDK Features |\n|-----|---------------------|-----------------|\n| **support-assistant** | Multi-agent handoff between billing and technical support | Dual state machines (10 phases), `.computed()` derived state, cross-agent transitions, telemetry |\n| **call-screening** | Incoming call screening with sentiment analysis and smart routing | Phase machine, tool calling (`check_contact_list`, `check_calendar`, `take_message`, `transfer_call`, `block_caller`), `NonBlocking` tools |\n| **clinic** | HIPAA-aware telehealth scheduling with clinical triage | 8 tools (`verify_patient`, `check_availability`, `book_appointment`, etc.), patient intake flow, department routing |\n| **restaurant** | Restaurant reservation and ordering system | 6 tools (`check_availability`, `make_reservation`, `get_menu`, etc.), dietary handling, occasion tracking |\n| **debt-collection** | FDCPA-compliant debt collection with compliance gates | `StateKey\u003cT\u003e`, identity verification, payment negotiation, cease-and-desist handling, compliance watchers |\n\n### Platform Support\n\nAll examples work with both **Google AI** (API key) and **Vertex AI** (project/location).\nThe SDK auto-strips unsupported features on Vertex AI — no code changes needed:\n\n| Feature | Google AI | Vertex AI |\n|---------|-----------|-----------|\n| Async tool calling (`NonBlocking`, `WhenIdle`/`Silent`) | Supported | Stripped automatically |\n| Thinking (`thinkingConfig`) | Supported | Stripped automatically |\n\n---\n\n## Common Errors \u0026 Solutions\n\n### Vertex AI sends binary WebSocket frames\n\n**Symptom:** `serde_json::from_str` fails on messages from Vertex AI.\n\n**Cause:** Vertex AI sends Binary WebSocket frames, not Text frames (unlike\nGoogle AI).\n\n**Solution:** Already handled by `TungsteniteTransport::recv()`. If you build a\ncustom transport, handle both `Message::Text` and `Message::Binary`.\n\n### Native audio model only supports AUDIO output modality\n\n**Symptom:** Error when requesting `Modality::Text` with\n`GeminiLive2_5FlashNativeAudio`.\n\n**Solution:** Use `Modality::Audio` only, or switch to `Gemini2_0FlashLive`\nwhich supports text output:\n\n```rust\n// Correct for native audio model:\nconfig.response_modalities(vec![Modality::Audio])\n\n// For text output, use the non-native model:\n.model(GeminiModel::Gemini2_0FlashLive)\n```\n\n### Vertex AI endpoint URL\n\n**Symptom:** Connection fails to `global-aiplatform.googleapis.com`.\n\n**Solution:** Use `aiplatform.googleapis.com` (no `global-` prefix). The SDK\nhandles this automatically via the `Platform` enum.\n\n### Tool declarations cannot be updated mid-session\n\n**Symptom:** Attempting to add or remove tools after `connect()`.\n\n**Cause:** The Gemini Live API does not support updating tool definitions after\nsession setup.\n\n**Solution:** Declare all tools upfront. Use per-phase `tools_enabled` to\ncontrol which tools the model can call at any given point in the conversation.\n\n### Extraction returns stale data\n\n**Symptom:** `handle.extracted::\u003cT\u003e(name)` returns the previous turn's data.\n\n**Cause:** Extractors run asynchronously on the control lane after each turn\ncompletes.\n\n**Solution:** Use the `on_extracted` callback for real-time notifications, or\npoll `handle.extracted()` after the turn-complete event.\n\n### State key not found despite being set\n\n**Symptom:** `state.get(\"risk\")` returns `None` even though you called\n`state.set(\"derived:risk\", 0.85)`.\n\n**Solution:** The derived fallback works correctly: `get(\"risk\")` checks\n`derived:risk` automatically. However, `get(\"app:risk\")` does NOT trigger the\nfallback -- prefixed keys are looked up exactly as specified.\n\n### Session disconnects after inactivity\n\n**Symptom:** Server sends `GoAway` and closes the connection.\n\n**Solution:** Handle gracefully with `.on_go_away(|ttl| async move { ... })`.\nEnable session resumption with `.session_resume(true)` for transparent reconnect\nsupport.\n\n### Context window fills up in long conversations\n\n**Symptom:** Model responses degrade in quality after many turns.\n\n**Solution:** Enable context window compression:\n\n```rust\nLive::builder()\n    .context_compression(4000, 2000)  // trigger at 4k tokens, compress to 2k\n```\n\n---\n\n## Development\n\n### Prerequisites\n\n| Requirement | Version | Purpose |\n|------------|---------|---------|\n| **Rust** | 1.75+ | Language toolchain ([install](https://rustup.rs/)) |\n| **cargo** | (bundled) | Build system and package manager |\n| **pkg-config** | any | Locates system libraries |\n| **OpenSSL** | 1.1+ | TLS for WebSocket connections |\n| **ALSA dev** (Linux) | any | Audio I/O for voice examples |\n\n**Quick setup (Ubuntu/Debian):**\n\n```bash\n# Install Rust\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\nsource $HOME/.cargo/env\n\n# Install system dependencies\nsudo apt-get update\nsudo apt-get install -y pkg-config libssl-dev libasound2-dev build-essential\n```\n\n**Quick setup (macOS):**\n\n```bash\n# Install Rust\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n\n# System deps (OpenSSL via Homebrew)\nbrew install openssl pkg-config\n```\n\n**Environment variables:**\n\n```bash\n# Google AI (API key auth)\nexport GEMINI_API_KEY=\"your-api-key\"\n\n# Vertex AI (service account auth)\nexport GOOGLE_CLOUD_PROJECT=\"your-project-id\"\nexport GOOGLE_CLOUD_LOCATION=\"us-central1\"\n```\n\n### Build\n\n```bash\ncargo build --workspace\n```\n\n### Test\n\n```bash\ncargo test --workspace\n```\n\n### Lint\n\n```bash\ncargo clippy --workspace --all-targets -- -D warnings\ncargo fmt --all -- --check\n```\n\n### Run the Web UI\n\n```bash\ncd apps/gemini-adk-web\nGEMINI_API_KEY=\"your-key\" cargo run\n# Open http://localhost:3000\n```\n\n### Generate documentation\n\n```bash\ncargo doc --workspace --no-deps --open\n```\n\n### Feature flags (gemini-live)\n\n```bash\n# Default: live + vad + tracing\ncargo build -p gemini-live\n\n# With REST APIs\ncargo build -p gemini-live --features generate,embed,files\n\n# Everything\ncargo build -p gemini-live --features all-apis,metrics,opus\n```\n\n---\n\n## Project Structure\n\n```\ngemini-rs/\n  crates/\n    gemini-live/              L0: Wire protocol, transport, types\n    gemini-adk/                L1: Agent runtime, state, phases, tools\n    gemini-adk-fluent/         L2: Fluent builder API, operators\n  examples/\n    text-chat/             Minimal text-only session (L0)\n    voice-chat/            Bidirectional audio chat (L0)\n    tool-calling/          TypedTool + ToolDispatcher (L1)\n    transcription/         Every Gemini Live config option (L0)\n    agents/                Text agent combinators (L1/L2)\n    INDEX.md               Full example reference with per-app docs\n  apps/\n    gemini-adk-web/               Multi-app Web UI with devtools (L2)\n      src/apps/            13 showcase apps (see examples/INDEX.md)\n  tools/\n    gemini-adk-transpiler/        Python ADK to Rust transpiler\n  Cargo.toml               Workspace root\n```\n\n---\n\n## License\n\nLicensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for\ndetails.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvamsiramakrishnan%2Fgemini-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvamsiramakrishnan%2Fgemini-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvamsiramakrishnan%2Fgemini-rs/lists"}