الأدوات في الجلسات الفورية

الميزة الحاسمة في وكيل فوري (مقارنةً بـ voice bot) هي أنه يستطيع تنفيذ إجراءات حقيقية أثناء المحادثة: البحث عن شيء، معالجة استرداد، أو تحويل المحادثة إلى إنسان — ثم نطق النتيجة. تعمل الأدوات على جانب الخادم، لذلك لا تلامس منطق أعمالك وبيانات الاعتماد الخاصة بك العميل أبدًا.

كيف تتدفق دورة الأداة

  1. يقرر النموذج أنه يحتاج إلى أداة ويصدر FunctionCallDone { name, arguments, call_id }.
  2. يبحث RealtimeRunner عن المعالج الخاص بـ name ويشغّله.
  3. تُرسل نتيجة JSON الخاصة بالمعالج إلى النموذج مرة أخرى كمخرجات الأداة.
  4. يشغّل runner ردًا متابعة واحدًا؛ وينطق النموذج الإجابة، معتمدًا على النتيجة.

أنت لا تستدعي create_response() لهذا — إذ يتولى runner عملية الذهاب والإياب عندما يكون 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}.") }))
    })
}

سجّلها على builder باستخدام .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 الخاصة بك، أو adk-tool المدمجة مثل معرفة-الرسم البياني remember/relate)، فقم بإرفاقها باستخدام .adk_tool(...) — دون إعادة كتابة. تقوم طبقة الدمج بتغليف كل واحدة داخل ToolHandler وتوليد ToolContext مخصص لنطاق (app_name, user_id, session_id) الخاص بالجلسة:

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" ويُجمّد الجلسة. يتجنب runner ذلك عبر فصل "إرسال مخرجات الأداة" (send_tool_output) عن "تشغيل الرد" (respond_after_tools، يُستدعى مرة واحدة عند إرسال ResponseDone). تحصل على هذا مجانًا؛ فقط انتبه عند قراءة الأحداث إلى أن دورة الأداة تمتد عبر ردّين (انظر Architecture).

قراءة أحداث الأدوات في واجهة مستخدم

لإظهار نشاط الأداة (مثل شارة "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 فهو probe بلا واجهة يختبر دورات الأداة الواحدة، والأدوات المتوازية، ودورات الآلة الحاسبة على كلا المزوّدين.

التالي: Multimodal →

أي الأدوات تخضع للإدارة

IntegratedRealtimeRunner يوجّه استدعاءات الأدوات وفقًا للطريقة التي سُجِّلت بها الأداة:

مسجل كـالإرسالالسياسة المطبقة
adk_tool(...) — ADK Toolخط أنابيب سياسة التكاملالإضافات المُهيأة، تسجيل النص، استمرارية أحداث الأدوات
معالج أصلي في الوقت الحقيقيإرسال RealtimeRunnerلا شيء — المعالج موثوق به بحكم التصميم

كانت أداة ADK تصل سابقًا إلى المزوّد عبر ToolBridgeAdapter، والذي ينشئ سياقًا ويستدعي Tool::execute من دون أي إضافات، أو ردود نداء، أو تأكيد. لذلك كانت الأداة التي يحكمها حلقة الوكيل القياسية تعمل من دون حوكمة في الوقت الفعلي. وأصبح تجاوز معالج الأصل الآن الاستثناء الصريح بدلًا من السلوك الافتراضي لكل شيء.

فشل الإضافات يفشل بشكل مغلق

إذا أعادت سلسلة before_tool_call خطأً، تُـرفض الأداة:

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

مهم: كان هذا المسار يسجّل سابقًا خطأ الإضافة على أنه غير حرج ثم ينفّذ الأداة. بما أن التفويض، والتنقيح، والسياسة توجد في إضافات ما قبل الأداة، فإن حارسًا معطّلًا أصبح بلا حراسة.

أما أخطاء إضافات ما بعد الأداة فتُبقي نتيجة الأداة نفسها كما هي، لأن الأداة تكون قد نُفِّذت بالفعل.

ردود نداء الأداة في الوكيل المباشر

تطبّق RealtimeAgent ردود نداء ما قبل الأداة وما بعدها بنفس العقد مثل حلقة الوكيل القياسية:

إرجاع الاستدعاءالأثر
Ok(None)تعمل الأداة
Ok(Some(content)) من استدعاء قبليصبح المحتوى هو النتيجة؛ لا تعمل الأداة
Err(e) من نداء beforeيصبح الخطأ هو النتيجة، ولا تعمل الأداة، وتُتخطّى نداءات after
Ok(Some(content)) من نداء afterيستبدل المحتوى نتيجة الأداة
Err(e) من نداء afterيستبدل الخطأ نتيجة الأداة

يتم تحويل Content الخاص بـ callback إلى نتيجة JSON التي يتوقعها المزوّد: يساهم جزء FunctionResponse بحمولته، وأي شيء آخر يساهم بنصّه تحت مفتاح result.

Important: قبل الالتزام بهذا العقد، كان يتم احتساب قرار before-callback ثم تجاهله، لذلك كان الأداة تُشغَّل بغضّ النظر — بوابة كانت تُبلِّغ عن رفض من دون فرضه. كما كانت نتائج after-callback، بما في ذلك الأخطاء، تُحذف.

سياق الأداة في Realtime

الأداة التي يتم استدعاؤها من RealtimeAgent ترى القدرات نفسها التي تراها تحت Runner:

القدرةالمصدر
user_scopes()سياق الاستدعاء الأب
get_secret(name)سياق الاستدعاء الأب
shared_state()سياق الاستدعاء الأب
search_memory(query)خدمة الذاكرة الخاصة بالأب
الهوية (app_name, user_id, session_id, branch)سياق الاستدعاء الأب

ملاحظة: كانت هذه العناصر سابقًا تتسرب إلى القيم الافتراضية للصفة — قائمة نطاق فارغة، None للأسرار، وNone للحالة المشتركة — لذا فإن أداة التحقق من النطاق أو الأسرار كانت تتصرف بشكل مختلف في الوقت الفعلي مقارنةً بـ Runner، ولم تكن قادرة على التمييز بين مستدعٍ غير موثّق وبين سياق فشل ببساطة في تمرير النطاقات.

تزامن الأدوات

RunnerConfig::max_concurrent_tools (الافتراضي 4) يحدّ عدد معالجات الأدوات التي تعمل في الوقت نفسه. عندما يرسل ردّ عدة استدعاءات، يقوم المنفذ بوضع كل استدعاء في قائمة الانتظار على حلقة الأحداث الخاصة به ويسمح بتنفيذه كلما تحررت إشارة:

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()?;

ينتج عن ذلك خاصيتان، وكلتاهما مغطاة بالاختبارات:

  • يستمر استقبال الأحداث أثناء تنفيذ الأداة. يتم التعامل مع تدرجات الصوت، والنصوص المنقولة، و المقاطعات بينما تعمل الأدوات. المعالج الذي ينتظر وصول شيء لاحقًا في الجلسة لن يسبب بعد الآن توقفًا متبادلًا للجلسة.
  • ردّ متابعة واحد، بعد آخر مخرجات. عندما يُرسل خرج الأداة تلقائيًا، يكون على النموذج ردّ create_response واحد. ويُصدر مرة واحدة بعد أن يُغلق الردّ الذي يقوم بالإرسال وبعد أن تبلغ كل أداة مُرسلة — بأي ترتيب، لأن الردّ يمكنه الآن الإغلاق بينما لا تزال الأدوات تعمل.

مهم: هذا الحد يحكم التزامن، لا التوازي. تشارك المعالجات في مهمة المنفذ، لذا فإن المعالج الذي يحظر الخيط — إدخال/إخراج متزامن للملفات أو الشبكة، أو حسابات ثقيلة — سيؤدي مع ذلك إلى إبطاء الحلقة. استخدم tokio::task::spawn_blocking لهذه الحالات.

سياسة قطع الاتصال

لا يعيد المنفذ الاتصال تلقائيًا. عند فقدان النقل، يسمح للأدوات المرسلة بالانتهاء، ويستدعي EventHandler::on_disconnect، ويعود من run:

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(())
    }
}

يبقى إعادة الاتصال على عاتق المستدعي لأنه يتطلب تحديد السياق الذي يجب إعادة تشغيله ولأنّه، في Gemini، ينبغي التحقق مما إذا كان رمز الاستئناف المخزن لا يزال صالحًا. يوجد الخطاف on_disconnect لكي يمكن التمييز بين فقدان النقل وclose الهادئ — run يعيد Ok(()) لكليهما.