रीयलटाइम सत्रों में टूल्स

रीयलटाइम एजेंट (वॉयस बॉट के मुकाबले) की परिभाषित विशेषता यह है कि वह बातचीत के बीच में वास्तविक कार्रवाइयाँ कर सकता है: कुछ खोज सकता है, रिफंड प्रोसेस कर सकता है, किसी मानव को हैंड ऑफ कर सकता है — और फिर परिणाम बोल सकता है। टूल्स सर्वर-साइड पर चलते हैं, इसलिए आपका बिज़नेस लॉजिक और क्रेडेंशियल्स कभी क्लाइंट तक नहीं पहुँचते।

एक टूल टर्न कैसे प्रवाहित होता है

  1. मॉडल तय करता है कि उसे एक टूल चाहिए और FunctionCallDone { name, arguments, call_id } उत्सर्जित करता है।
  2. RealtimeRunner name के लिए हैंडलर खोजता है और उसे चलाता है।
  3. हैंडलर का JSON परिणाम टूल आउटपुट के रूप में मॉडल को वापस भेजा जाता है।
  4. रनर एक फॉलो-अप प्रतिक्रिया ट्रिगर करता है; मॉडल परिणाम के आधार पर उत्तर बोलता है।

इसके लिए आप कभी create_response() नहीं कॉल करते — जब auto_respond_tools चालू होता है (डिफ़ॉल्ट), रनर राउंड-ट्रिप को संभालता है।

नेटिव टूल्स: ToolDefinition + FnToolHandler

हल्का-फुल्का रास्ता। एक ToolDefinition वह JSON स्कीमा है जिसे मॉडल देखता है; एक FnToolHandler एक सिंक्रोनस क्लोज़र है जो बुलाए जाने पर चलता है।

use adk_realtime::config::ToolDefinition;
use adk_realtime::events::ToolCall;
use adk_realtime::runner::FnToolHandler;
use serde_json::json;

fn process_refund_def() -> ToolDefinition {
    ToolDefinition {
        name: "process_refund".into(),
        description: Some("Issue a refund for an order. Only when clearly warranted.".into()),
        parameters: Some(json!({
            "type": "object",
            "properties": {
                "order_id": { "type": "string", "description": "e.g. 'A-10293'" },
                "reason":   { "type": "string", "description": "Short reason" }
            },
            "required": ["order_id", "reason"]
        })),
    }
}

fn process_refund_tool()
-> FnToolHandler<impl Fn(&ToolCall) -> adk_realtime::error::Result<serde_json::Value> + Send + Sync> {
    FnToolHandler::new(|call: &ToolCall| {
        let order = call.arguments.get("order_id").and_then(|v| v.as_str()).unwrap_or("unknown");
        // …do the work…
        Ok(json!({ "status": "approved", "order_id": order,
                   "message": format!("Refund approved for {order}.") }))
    })
}

इसे बिल्डर पर .tool(definition, handler) के साथ रजिस्टर करें:

let runner = IntegratedRealtimeRunner::builder()
    .model(model)
    .config(config)
    .identity("support", "customer", &session_id)
    .session_service(sessions)
    .tool(process_refund_def(), process_refund_tool())
    .tool(connect_to_human_def(), connect_to_human_tool())
    .build()?;

हैंडलर एक serde_json::Value लौटाता है; आप जो भी लौटाते हैं वही मॉडल देखता है, इसलिए एक मानव-पठनीय message शामिल करें जिसे एजेंट पैराफ़्रेज़ कर सके।

हैंडलर सर्वर-साइड और सिंक्रोनस रूप से इवेंट लूप के भीतर चलते हैं। उन्हें तेज़ रखें; धीमे काम के लिए, "started" स्थिति लौटाएँ और बाद में आउट-ऑफ़-बैंड फॉलो अप करें।

ब्रिज्ड टूल्स: कोई भी adk_core::Tool

अगर आपके पास पहले से adk-core टूल्स हैं (आपके अपने FunctionTool, या knowledge-graph जैसे adk-tool के built-ins remember/relate), तो उन्हें .adk_tool(...) के साथ जोड़ें — किसी rewrite की ज़रूरत नहीं। इंटीग्रेशन लेयर हर एक को ToolHandler में लपेटती है और सत्र के (app_name, user_id, session_id) के दायरे में एक ToolContext तैयार करती है:

use adk_tool::{RememberTool, RelateTool};

let runner = IntegratedRealtimeRunner::builder()
    .model(model).config(config).identity("app", "user", &sid)
    .memory_service(kg.clone())
    .adk_tool(Arc::new(RememberTool::new(kg.clone())))   // adk_core::Tool
    .adk_tool(Arc::new(RelateTool::new(kg)))
    .tool(get_weather_def(), get_weather())              // native handler — mix freely
    .build()?;

यही वह तरीका है जिससे एजेंट अपनी memory को स्वयं क्यूरेट करता है। ब्रिज लोकली-एक्ज़ीक्यूटेड, कॉन्टेक्स्ट-इंडिपेंडेंट टूल्स के लिए अच्छा काम करता है; जिन टूल्स को समृद्ध एजेंट स्टेट की ज़रूरत होती है, वे नेटिव FnToolHandler के रूप में बेहतर लिखे जाते हैं।

समानांतर टूल कॉल्स

एक मॉडल एक प्रतिक्रिया में कई टूल्स का अनुरोध कर सकता है (जैसे, "लंदन में मौसम और समय क्या है?")। ADK-Rust इसे सही तरीके से संभालता है: यह हर टूल का आउटपुट उसके पूरा होते ही भेजता है, फिर डिस्पैच प्रतिक्रिया समाप्त होने पर ठीक एक response.create जारी करता है।

यह महत्वपूर्ण है क्योंकि भोला तरीका — हर टूल पर एक प्रतिक्रिया भेजना — OpenAI की "conversation already has an active response in progress" त्रुटि को ट्रिगर करता है और सत्र को रोक देता है। रनर इसे "टूल आउटपुट भेजो" (send_tool_output) को "प्रतिक्रिया ट्रिगर करो" (respond_after_tools, जो डिस्पैच ResponseDone पर एक बार कॉल होता है) से अलग करके टालता है। आपको यह मुफ्त में मिलता है; बस इवेंट पढ़ते समय ध्यान रखें कि एक टूल टर्न दो प्रतिक्रियाओं में फैलता है (देखें Architecture)।

UI में टूल इवेंट्स पढ़ना

टूल गतिविधि को दिखाने के लिए (जैसे "Processing refund…" चिप), FunctionCallDone पर नज़र रखें:

ServerEvent::FunctionCallDone { name, arguments, .. } => {
    // `arguments` is a JSON string of the call args
    ui_show_tool_activity(&name, &arguments);
}

मौखिक पुष्टि बाद में TranscriptDelta के रूप में आती है, जब टूल परिणाम को फॉलो-अप प्रतिक्रिया में समाहित कर लिया जाता है।

इसे काम करते देखें

customer_service उदाहरण process_refund और connect_to_human को जोड़ता है; realtime_tools उदाहरण एक हेडलेस प्रोब है जो दोनों प्रदाताओं पर सिंगल-टूल, समानांतर-टूल, और कैलकुलेटर टर्न्स को चलाता है।

अगला: Multimodal →

किन टूल्स को नियंत्रित किया जाता है

IntegratedRealtimeRunner टूल कॉल्स को इस आधार पर रूट करता है कि टूल कैसे रजिस्टर किया गया था:

के रूप में पंजीकृतडिस्पैचलागू नीति
adk_tool(...) — एक ADK Toolएकीकरण नीति पाइपलाइनकॉन्फ़िगर किए गए प्लगइन्स, ट्रांसक्रिप्ट रिकॉर्डिंग, टूल-इवेंट स्थायित्व
एक मूल रियलटाइम हैंडलरRealtimeRunner डिस्पैचकोई नहीं — हैंडलर निर्माण के अनुसार विश्वसनीय है

एक ADK टूल पहले ToolBridgeAdapter के माध्यम से प्रदाता तक पहुँचा, जो एक context बनाता है और Tool::execute को बिना plugins, callbacks, या confirmation के कॉल करता है। इसलिए standard agent loop में governed एक tool realtime में ungoverned रूप से चला। native-handler bypass अब हर चीज़ के लिए default के बजाय स्पष्ट exception है।

Plugin विफलताएँ fail closed करती हैं

यदि before_tool_call pipeline error लौटाती है, तो tool refused होता है:

{ "error": "tool guarded was refused: its before-tool plugin pipeline failed (...). Execution is refused rather than proceeding without policy." }

Important: इस path ने पहले plugin error को non-fatal के रूप में log किया और फिर tool execute किया। Authorization, redaction, और policy before-tool plugins में रहते हैं, इसलिए एक broken guard कोई guard नहीं बन गया।

After-tool plugin errors tool के अपने result को यथास्थान रखते हैं, क्योंकि tool पहले ही चल चुका है।

Direct agent पर tool callbacks

RealtimeAgent standard agent loop के समान contract के साथ before- और after-tool callbacks लागू करता है:

कॉलबैक लौटाता हैप्रभाव
Ok(None)टूल चलता है
Ok(Some(content)) एक before कॉलबैक सेसामग्री परिणाम बन जाती है; टूल नहीं चलता
Err(e) एक before callback सेत्रुटि परिणाम बन जाती है, tool नहीं चलता, और after-callbacks छोड़ दिए जाते हैं
Ok(Some(content)) एक after callback सेसामग्री tool के result को प्रतिस्थापित करती है
Err(e) एक after callback सेत्रुटि tool के result को प्रतिस्थापित करती है

एक callback की Content को provider द्वारा अपेक्षित JSON परिणाम में बदला जाता है: एक FunctionResponse भाग अपना payload योगदान करता है, बाकी सब कुछ अपना text एक result key के अंतर्गत योगदान करता है।

महत्वपूर्ण: इस contract के सम्मानित होने से पहले, before-callback का निर्णय compute किया जाता था और discard कर दिया जाता था, इसलिए tool वैसे भी run होता था — एक gate जो denial report करता था लेकिन उसे enforce नहीं करता था। After-callback results, errors सहित, dropped कर दिए जाते थे।

Realtime tool context

RealtimeAgent से invoked tool वही capabilities देखता है जो वह एक Runner के तहत देखता है:

क्षमतास्रोत
user_scopes()पैरेंट इनवोकेशन संदर्भ
get_secret(name)पैरेंट इनवोकेशन संदर्भ
shared_state()मूल invocation context
search_memory(query)parent की memory service
Identity (app_name, user_id, session_id, branch)मूल invocation context

नोट: ये पहले trait defaults तक गिर जाते थे — एक खाली scope list, None secrets के लिए, और shared state के लिए None — इसलिए scope- या secret-checking tool realtime में Runner के तहत होने की तुलना में अलग व्यवहार करता था, और unauthenticated caller को उस context से अलग नहीं कर सकता था जो बस scopes को आगे पास करने में विफल रहा।

Tool concurrency

RunnerConfig::max_concurrent_tools (default 4) यह सीमित करता है कि कितने tool handlers एक साथ चल सकते हैं। जब कोई response कई calls dispatch करता है, तो runner event loop पर प्रत्येक को queue करता है और जैसे ही एक permit मुक्त होता है, उसे execution में प्रवेश देता है:

use adk_realtime::{RealtimeRunner, RunnerConfig};

let runner = RealtimeRunner::builder()
    .model(model)
    .runner_config(RunnerConfig {
        auto_execute_tools: true,
        auto_respond_tools: true,
        max_concurrent_tools: 3,
    })
    .build()?;

इसके दो गुण निकलते हैं, और दोनों tests द्वारा covered हैं:

  • Tool execution के दौरान event intake जारी रहता है। Audio deltas, transcripts, और interruptions को tools के चलने के दौरान handle किया जाता है। जो handler session में बाद में आने वाली किसी चीज़ का इंतज़ार करता है, वह अब session को deadlock नहीं करता।
  • अंतिम output के बाद एक follow-up response। जब tool output automatically भेजा जाता है, तो model को एक single create_response देय होता है। यह तब जारी किया जाता है जब dispatching response बंद हो चुका हो और हर dispatched tool ने report कर दिया हो — किसी भी क्रम में, क्योंकि अब response tools के अभी भी चल रहे होने के दौरान भी close हो सकता है।

महत्वपूर्ण: यह bound concurrency को govern करता है, parallelism को नहीं। Handlers runner के task को share करते हैं, इसलिए जो handler thread को block करता है — synchronous file या network I/O, भारी computation — वह फिर भी loop को stall कर देता है। इनके लिए tokio::task::spawn_blocking का उपयोग करें।

Disconnect policy

Runner अपने-आप reconnect नहीं करता। Transport loss होने पर यह dispatched tools को finish होने देता है, EventHandler::on_disconnect को call करता है, और run से return करता है:

use adk_realtime::{EventHandler, Result};

struct Reconnecting;

#[async_trait::async_trait]
impl EventHandler for Reconnecting {
    async fn on_disconnect(&self) -> Result<()> {
        tracing::warn!("realtime transport ended");
        Ok(())
    }
}

Reconnection caller के पास ही रहता है क्योंकि इसके लिए यह तय करना पड़ता है कि किस context को replay करना है और, Gemini पर, क्या stored resumption token अभी भी valid है। on_disconnect hook इसलिए मौजूद है ताकि transport loss को graceful close से अलग पहचाना जा सके — run दोनों के लिए Ok(()) लौटाता है।