CodeActAgent (CodeAct)

CodeActAgent هو نظير لـ LlmAgent الذي ينفّذ الإجراءات عبر كتابة التعليمات البرمجية وتشغيلها بدلاً من إصدار استدعاء أداة واحد في كل مرة. في كل دورة، ينتج النموذج نصًا برمجيًا واحدًا؛ وتُعرَض الأدوات كدوال قابلة للاستدعاء يمكن للنص البرمجي تركيبها؛ ويتواصل النص البرمجي بنتيجته من خلال إرجاع قيمة موسومة.

هذا هو نمط CodeAct: بدلاً من call tool A → observe → call tool B، يكتب النموذج b(a(x)) في نص برمجي واحد، ولذلك يحدث العمل متعدد الخطوات في دورة واحدة. ويتم تمكينه بواسطة ميزة codeact في adk-agent.

متى يُستخدم

  • المهام التي تسلسل أو تدمج عدة أدوات في كل دورة (معالجة البيانات، والعمليات الدفعية، والمنطق الوسيط).
  • النماذج التي أُعيد تدريبها بعديًا لتوليد التعليمات البرمجية.
  • سير العمل الذي يتوفر فيه مفسّر حقيقي (مثل Python) ليكون طبقة تنفيذ الإجراءات.

لاستدعاء الأدوات الأصلي، يُفضّل استخدام LlmAgent. وللاطلاع على بيئة معزولة لبرمجة الملفات/الصدفة، راجع وكيل البرمجة.

كيف تعمل الحلقة

في كل دورة:

  1. يُصدر النموذج كتلة تعليمات برمجية واحدة محاطة بسياج (نصًا برمجيًا).
  2. يعمل النص البرمجي على [CodeRuntime]؛ وتظهر استدعاءات الأدوات للمضيف، الذي ينفّذ الأداة ثم يستأنف النص البرمجي بالنتيجة.
  3. يُرجع النص البرمجي قيمة ScriptOutput موسومة:
    • observation — تُعاد إلى النموذج؛ وتستمر الحلقة.
    • error — تُعاد كرسالة؛ وتستمر الحلقة.
    • final_result — تُعاد إلى المستدعي؛ وتنتهي الحلقة.
    • transfer_to_agent — تنقل التحكم إلى وكيل آخر؛ وتنتهي الحلقة.

الإطار محايد تجاه اللغة: إذ إن سمة CodeRuntime هي نقطة التقاء المفسّر التدريجي، كما أنها تُبلّغ النموذج بلغتها وبيئتها الخاصتين عبر مطالبة حرة الصياغة. ويغلّف المكيّف المخصص للإنتاج Monty، وهو مفسّر Python أصلي لـ Rust.

المتانة: التعليق والاستئناف

CodeActAgent عديم الحالة بين الاستدعاءات — وتعيش الحالة الدائمة في الجلسة، تمامًا مثل LlmAgent. هناك حالتان تؤديان إلى تعليق التشغيل:

  • أداة تتطلب تأكيدًا ولم يُتخذ قرار بعد (HITL)، و
  • أداة طويلة التشغيل تصل نتيجتها خارج النطاق.

عند التعليق، تُسلسل استمرارية المفسّر النشطة في CodeActCheckpoint وتُكتب إلى حالة الجلسة؛ ثم يقرأها run() التالي ويستأنف التنفيذ — يصل قرار التأكيد عبر RunConfig::tool_confirmation_decisions، وتصل نتيجة الأداة طويلة التشغيل بصفتها FunctionResponse في الرسالة التالية. تُحاط استدعاءات الأدوات المضمّنة بنقاط تحقق للكتابة المسبقة (SAVE-BEFORE) وللحفظ اللاحق (SAVE-AFTER): وبمجرد استمرار نقطة تحقق SAVE-AFTER، يستأنف الاسترداد باستخدام النتيجة المخزنة ولا يعيد تشغيل الأداة مطلقًا. وسيؤدي حدوث عطل في النافذة الزمنية الضيقة بعد التأثير الجانبي للأداة وقبل تسجيل نقطة تحقق SAVE-AFTER إلى إعادة تشغيل الأداة عند الاسترداد، ولذلك ينبغي للأدوات غير القابلة للتكرار أن تتخذ احتياطات ضد ذلك (وهو حد التسليم مرة واحدة على الأقل نفسه الموجود في LlmAgent).

يتطلب هذا بيئة تشغيل يمكنها أخذ لقطة لاستدعاء معلّق. أما بيئة التشغيل التي لا تستطيع ذلك، فتشغّل الأدوات طويلة التشغيل بشكل مضمن وترفض حالات التعليق بسبب التأكيد.

بناء CodeActAgent

use adk_agent::codeact::CodeActAgent;
use std::sync::Arc;

// `model` implements `adk_core::Llm`; `runtime` implements `CodeRuntime`.
let agent = CodeActAgent::builder()
    .name("analyst")
    .model(model)
    .runtime(runtime)
    .instruction("Prefer concise, composable steps.")
    .tool(Arc::new(load_csv_tool))
    .output_key("report")
    .build()?;

يُشترط وجود model وruntime؛ أما كل ما عداهما فله قيمة افتراضية.

التكافؤ مع LlmAgent

يعكس المُنشئ LlmAgentBuilder:

  • النموذج: generate_content_config بالإضافة إلى الاختصارات temperature/top_p/top_k/ max_output_tokens.
  • التعليمات: instruction/instruction_provider، global_instruction/global_instruction_provider، مع حقن القالب {state.key}؛ بالإضافة إلى المهارات (ميزة skills).
  • السجل: include_contents.
  • الأدوات: أدوات tool الثابتة وأدوات toolset لكل استدعاء؛ tool_timeout، وdefault_retry_budget/tool_retry_budget، وcircuit_breaker_threshold، وبدائل on_tool_error.
  • التفويض: ToolConfirmationPolicy (require_tool_confirmation/require_tool_confirmation_for_all).
  • النقل: أدوات sub_agent وdisallow_transfer_to_parent/ disallow_transfer_to_peers.
  • المخرجات: output_key، وoutput_schema/output_type مع حلقة تصحيح وإعادة المحاولة (output_max_retries).
  • عمليات الاستدعاء: before_callback/after_callback، before_model_callback/after_model_callback، و before_tool_callback/after_tool_callback/after_tool_callback_full. يمكن لعمليات الاستدعاء بعد الأداة فحص بيانات وصفية منظمة للتنفيذ عبر CallbackContext::tool_outcome().
  • المحكومة بالميزات: ضوابط الإدخال/الإخراج (guardrails) وخط أنابيب EnhancedPlugin (enhanced-plugins).

يحصل كل استدعاء أداة على ToolContext جديد يحمل معرّف استدعاء المفسّر ويفوّض إدارة المصنوعات والذاكرة والحالة المشتركة ونطاقات المستخدم والأسرار إلى الاستدعاء النشط — لذلك تتصرف الأداة بالطريقة نفسها ضمن CodeActAgent أو LlmAgent.

الاختلافات المقصودة

  • تقع مسؤولية وضع الحماية لتنفيذ التعليمات البرمجية على عاتق CodeRuntime، وليست إضافة لاحقة.
  • يُصمَّم توزيع الأدوات ليكون تسلسليًا (إذ تُلتقط متابعة واحدة عند حدّ استدعاء واحد)، ولذلك لا يوجد tool_execution_strategy متوازٍ.
  • لا يوجد خيار منشئ skip_summarization — إذ ينهي النموذج الحلقة بنفسه عبر final_result — مع أن أداةً تضبط skip_summarization في إجراءاتها تنهي التشغيل أيضًا.

مثال

يوجد عرض توضيحي شامل قابل للتشغيل وخالٍ من التبعيات — يتكون من CodeRuntime مكتفٍ ذاتيًا بالإضافة إلى نموذج حتمي — في examples/codeact_agent:

cargo run --manifest-path examples/codeact_agent/Cargo.toml

تنفيذ CodeRuntime

يقوم CodeRuntime بتحليل نص برمجي وتنفيذه خطوة بخطوة، مع إظهار استدعاء خارجي واحد في كل مرة:

pub trait CodeRuntime: Send + Sync {
    fn start(&self, script: &str, script_name: &str) -> Result<RunStep, RuntimeError>;
    fn resume(&self, snapshot: &[u8], with: ResumeWith) -> Result<RunStep, RuntimeError>;
    fn capabilities(&self) -> RuntimeCapabilities { /* default */ }
    fn render_tools(&self, tools: &[Arc<dyn Tool>]) -> String { /* default */ }
}
  • إن RunStep عبارة عن مجموعة من متغيرات البنى — Call { call, stdout }، وComplete { value, stdout }، وRaised { message, stdout }. أنشئها باستخدام مساعدات RunStep::call / RunStep::complete / RunStep::raised، وأرفق المخرجات الملتقطة باستخدام .with_stdout(..). يعرض RunStep::Call استدعاءً معلّقًا واحدًا بالضبط؛ استأنفه بقيمة أو بخطأ، أو dump() استمراريته لتعليقه. وتُعرض stdout التي يرفقها وقت التشغيل مجددًا إلى النموذج وتُحفظ في نقاط التحقق، ولذلك تبقى متاحة بعد التعليق والاستئناف.
  • يعرض PendingCall وسائطه بالطريقة التي أنتجها بها المفسّر — positional_args() وkeyword_args() كلٌّ على حدة. لا تربط الوسائط الموضعية بالأسماء بنفسك: إذ يربطها برنامج التشغيل بمعلمات الأداة مركزيًا عبر adk_agent::codeact::bind_call_args، ولذلك لا يحتاج وقت التشغيل إلى مخطط الأداة عند حدّ الاستدعاء، ويمكن أن يكون render_tools دالةً صرفةً في شريحة الأداة.
  • أخطاء النص البرمجي مقابل أخطاء المضيف. كل ما يمكن للنموذج إصلاحه بكتابة تعليمات برمجية مختلفة — خطأ نحوي/خطأ في التحليل، أو استثناء غير ملتقط، أو إلغاء بسبب تجاوز حدّ الموارد — هو RunStep::Raised (سلسلة نصية مبهمة تُعاد إلى النموذج حرفيًا). أما RuntimeError فهو مخصص لحالات فشل المضيف الحقيقية (إلغاء تسلسل اللقطة أو إجراء تسلسل لها، وأخطاء المفسّر الداخلية)، ويوقف التشغيل.
  • يجب أن يكون RuntimeCapabilities::supports_suspension هو true لتمكين HITL والتأجيل طويل الأمد؛ ويصف prompt اللغة/البيئة للنموذج.

راجع examples/codeact_agent/src/runtime.rs للاطلاع على تنفيذ كامل ومبسّط يدعم التعليق والاستئناف.

Python عبر Monty

محوّل الإنتاج المقصود هو adk-codeact-monty، وهو CodeRuntime مدعوم بواسطة Pydantic Monty. إذ يتيح للنموذج التصرّف من خلال كتابة Python، ويعمل داخل العملية نفسها من دون حاوية أو عملية فرعية، ويلتقط حالة تشغيل متوقفة مؤقتًا إلى بايتات — وهو بالضبط ما تحتاجه وظيفة التعليق/الاستئناف. يُستهلك مفسّر Monty عبر نواة embedded-python الخاصة بـ adk-code، والتي تثبّت حزم monty في مكان واحد؛ ويتطلب ذلك rustc 1.95+.

[dependencies]
adk-agent = { version = "2.1.0", features = ["codeact"] }
adk-codeact-monty = "2.1.0"

أو عبر الحزمة الجامعة (المُعاد تصديرها باسم adk_rust::codeact_monty):

[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "codeact-monty"] }
use adk_codeact_monty::MontyRuntime;

// Conservative default resource limits (per-advance time + memory caps) make
// `new()` safe for untrusted, LLM-generated code.
let runtime = Arc::new(MontyRuntime::new());

// Tighten or relax with the builder; `unlimited()` removes the caps for
// trusted scripts only.
let runtime = Arc::new(
    MontyRuntime::builder()
        .max_duration(std::time::Duration::from_secs(2))
        .max_memory(64 * 1024 * 1024)
        .build(),
);

الوصول إلى نظام التشغيل

تُنفَّذ تأثيرات نظام التشغيل التي يحاول البرنامج النصي إحداثها — قراءات/كتابات نظام الملفات، os.getenv/os.environ، وdate.today()/datetime.now()في موضعها وفق سياسة يتحكم بها المضيف. وهي ليست أدوات ولا توقف حلقة الوكيل مؤقتًا. يكون وقت التشغيل، افتراضيًا، معزولًا بالكامل (من دون وصول إلى نظام الملفات، وبيئة فارغة، مع تفعيل ساعة المضيف). امنح صلاحية وصول محددة باستخدام المُنشئ:

use adk_codeact_monty::{MontyRuntime, PathAccess};

let runtime = Arc::new(
    MontyRuntime::builder()
        // Mount host directories at virtual paths; Monty enforces the boundary
        // (canonicalization + symlink-escape detection) so a script can never
        // escape a mount. Reads/writes outside every mount raise PermissionError.
        .allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
        .allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
        // Expose an explicit environment map to os.getenv / os.environ. Empty by
        // default — the host process environment is never exposed implicitly.
        .environ_var("PROJECT", "acme")
        // date.today() / datetime.now() read the host clock (enabled by default).
        .system_clock(true)
        .build(),
);

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

ينفّذ Monty مجموعة فرعية فقط من pathlib.Path، لذلك عند تركيب المسارات تسرد المطالبة الأساليب المدعومة بدقة (وأي أسلوب آخر يرفع AttributeError):

  • القراءة/الاستعلام (أي نقطة ربط): exists()، is_file()، is_dir()، is_symlink()، read_text()، read_bytes()، stat()، iterdir()، resolve()، absolute()، open("r").
  • الكتابة (نقاط الربط القابلة للقراءة والكتابة فقط): write_text()، write_bytes()، append_text()، append_bytes()، mkdir()، unlink()، rmdir()، rename()، open("w")/open("a").
  • عمليات المسار الخالصة (من دون إدخال/إخراج): عامل / وjoinpath()، is_absolute()، with_name()، with_stem()، with_suffix()، as_posix()، والخصائص .name، .parent، .stem، .suffix، .suffixes، .parts.

يُستدعى كلٌّ من الأدوات من خلال دالة مضمّنة واحدة، call_tool("name", {"arg": value, ...}) — وهي الطريقة الوحيدة لاستدعاء أداة؛ ولا تكون الأدوات متاحة مطلقًا كدوال قابلة للاستدعاء مجرّدة. يكون اسم الأداة قيمة نصية حرفية، وكل وسيط عبارة عن إدخال ذي مفتاح نصي في قاموس واحد، ولذلك ينتقل الاسم الفعلي داخل الاستمرار المتسلسل (ويظل محفوظًا عبر التعليق/الاستئناف من دون جدول أسماء من جهة المضيف)، ويمكن أن تحمل الأداة وكل وسيط أي اسم (ليس بالضرورة معرّفًا صالحًا في Python مثل "fetch-cart"، أو كلمة محجوزة في Python، أو حتى "call_tool")، ويربط المشغّل إدخالات القاموس حسب الاسم تمامًا — من دون استنتاج موضعي. تظهر كل أداة في المطالبة كسطر استخدام call_tool("name", {...}) يتضمن معاملاتها ووصفها. ويُرفض أي شيء بخلاف هذا الشكل — مثل fetch_cart(...) مجرّد، أو وسائط كلمات مفتاحية، أو وسيط ليس قاموسًا، أو مفتاح غير نصي — مع ظهور خطأ تصحيحي بدلًا من الإرسال بصمت، بحيث لا يتعين على النموذج تعلّم سوى صيغة استدعاء واحدة.

يشغّل examples/codeact_monty_agent CodeActAgent قابلًا للتنفيذ مقابل Python حقيقي بالكامل دون اتصال بالإنترنت.

CodeActAgent (CodeAct) - وثائق ADK-Rust | ADK-Rust