CodeActAgent (CodeAct)

CodeActAgent LlmAgent का एक समकक्ष है जो एक समय में एक tool call emit करने के बजाय कोड लिखकर और चलाकर कार्य करता है। हर turn में model एक single script बनाता है; tools callable functions के रूप में उपलब्ध कराए जाते हैं जिन्हें script compose कर सकती है; और script अपना result एक tagged value लौटाकर संप्रेषित करती है।

यह CodeAct pattern है: call tool A → observe → call tool B करने के बजाय, model एक script में b(a(x)) लिखता है, इसलिए multi-step work एक ही turn में हो जाता है। यह adk-agent पर codeact feature द्वारा enabled है।

कब उपयोग करें

  • ऐसे tasks जो प्रति turn कई tools को chain या combine करते हैं (data wrangling, batch operations, glue logic).
  • Code generation के लिए post-trained models.
  • ऐसे workflows जहाँ action substrate के रूप में एक वास्तविक interpreter (जैसे Python) उपलब्ध हो।

Native tool-calling के लिए LlmAgent को प्राथमिकता दें। Sandboxed file/shell coding harness के लिए Coding Agent देखें।

Loop कैसे काम करता है

हर turn:

  1. Model एक fenced code block (एक script) emit करता है।
  2. Script एक [CodeRuntime] पर चलती है; tool calls host तक surface होते हैं, जो tool execute करता है और result के साथ script को resume करता है।
  3. Script एक tagged ScriptOutput लौटाती है:
    • observation — model को वापस fed किया जाता है; loop जारी रहता है।
    • error — message के रूप में वापस fed किया जाता है; loop जारी रहता है।
    • final_result — caller को लौटाया जाता है; loop समाप्त हो जाता है।
    • transfer_to_agent — control दूसरे agent को सौंपता है; loop समाप्त हो जाता है।

Framework language-agnostic है: CodeRuntime trait step-wise interpreter seam है, और यह freeform prompt के माध्यम से model को अपनी language/environment स्वयं रिपोर्ट करता है। Intended production adapter Monty को wrap करता है, जो Rust-native Python interpreter है।

Durability: suspend और resume

CodeActAgent invocations के बीच stateless है — durable state session में रहती है, बिल्कुल LlmAgent की तरह। दो स्थितियाँ run को suspend करती हैं:

  • एक confirmation-gated tool जिसके लिए अभी decision नहीं आया है (HITL), और
  • एक long-running tool जिसका result out-of-band आता है।

Suspend होने पर, live interpreter continuation को CodeActCheckpoint में serialize किया जाता है और session state में लिखा जाता है; अगला run() इसे वापस पढ़ता है और resume करता है — confirmation decision RunConfig::tool_confirmation_decisions के माध्यम से आता है, और long-running result अगले message में एक FunctionResponse के रूप में आता है। Inline tool calls को write-ahead (SAVE-BEFORE) और SAVE-AFTER checkpoints से bracket किया जाता है: एक बार SAVE-AFTER checkpoint persist हो जाने पर, recovery stored result के साथ resume होती है और tool को कभी दोबारा नहीं चलाती। Tool के side effect के बाद लेकिन उसका SAVE-AFTER checkpoint landing से पहले आने वाला crash recovery पर tool को फिर से चलाएगा, इसलिए जो tools idempotent नहीं हैं उन्हें उससे बचाव करना चाहिए (LlmAgent जैसी same at-least-once boundary)।

इसके लिए ऐसा runtime चाहिए जो paused call का snapshot ले सके। जो runtime ऐसा नहीं कर सकता, वह long-running tools को inline चलाता है और confirmation pauses को अस्वीकार करता है।

एक 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 आवश्यक हैं; बाकी सबका default है।

LlmAgent के साथ समानता

Builder LlmAgentBuilder की mirror करता है:

  • Model: generate_content_config और temperature/top_p/top_k/ max_output_tokens shorthand।
  • Instructions: instruction/instruction_provider, global_instruction/global_instruction_provider, साथ में {state.key} template injection; और skills (skills feature)।
  • History: include_contents
  • Tools: static tools और per-invocation toolsets; tool_timeout, default_retry_budget/tool_retry_budget, circuit_breaker_threshold, और on_tool_error fallbacks।
  • Authorization: ToolConfirmationPolicy (require_tool_confirmation/require_tool_confirmation_for_all)।
  • Transfer: sub_agents और disallow_transfer_to_parent/ disallow_transfer_to_peers
  • Output: output_key, output_schema/output_type के साथ एक correction-retry loop (output_max_retries)।
  • Callbacks: before_callback/after_callback, before_model_callback/after_model_callback, और before_tool_callback/after_tool_callback/after_tool_callback_full। After-tool callbacks structured execution metadata को CallbackContext::tool_outcome() के माध्यम से inspect कर सकते हैं।
  • Feature-gated: input/output guardrails (guardrails) और EnhancedPlugin pipeline (enhanced-plugins)।

हर tool call को एक नया ToolContext मिलता है जो interpreter call id carry करता है और artifacts, memory, shared state, user scopes, तथा secrets को live invocation तक delegate करता है — इसलिए tool CodeActAgent या LlmAgent के अंतर्गत identically व्यवहार करता है।

जानबूझकर अंतर

  • Code-execution sandboxing CodeRuntime की responsibility है, कोई bolt-on नहीं।
  • Tool dispatch जानबूझकर sequential है (एक single continuation को एक call boundary पर snapshot किया जाता है), इसलिए कोई parallel tool_execution_strategy नहीं है।
  • skip_summarization builder option नहीं है — model loop को स्वयं final_result के माध्यम से समाप्त करता है — हालांकि जो tool अपनी actions पर skip_summarization set करता है, वह फिर भी run समाप्त कर देता है।

उदाहरण

एक runnable, dependency-free end-to-end demo — एक self-contained CodeRuntime और एक deterministic model — यहाँ मौजूद है: examples/codeact_agent:

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

एक CodeRuntime लागू करना

एक CodeRuntime एक script को parse और step करता है, और एक समय में एक external call surface करता है:

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 struct variants का एक सेट है — Call { call, stdout }, Complete { value, stdout }, और Raised { message, stdout }। इन्हें RunStep::call / RunStep::complete / RunStep::raised helpers के साथ बनाएं और captured output को .with_stdout(..) के साथ जोड़ें। RunStep::Call बिल्कुल एक pending call surface करता है; उसे एक value या error के साथ resume करें, या उसकी continuation को suspend करने के लिए dump() करें। stdout जिसे एक runtime attach करता है, model को वापस surface किया जाता है और checkpoints में persisted रहता है, इसलिए यह suspend/resume के बाद भी बना रहता है।
  • एक PendingCall अपने arguments उसी तरह report करता है जैसे interpreter ने उन्हें produce किया — positional_args() और keyword_args() अलग-अलग। positional arguments को names में स्वयं map न करें: driver उन्हें centrally tool के parameters पर via adk_agent::codeact::bind_call_args bind करता है, इसलिए एक runtime को call boundary पर कोई tool schema की आवश्यकता नहीं होती और render_tools tool slice का एक pure function हो सकता है।
  • Script vs. host errors. जो भी model अलग code लिखकर ठीक कर सकता है — एक syntax/parse error, an uncaught exception, a resource-limit cancellation — वह एक RunStep::Raised है (एक opaque string जो model को verbatim वापस feed किया जाता है)। RuntimeError वास्तविक host failures के लिए reserved है (snapshot (de)serialization, internal interpreter errors) और run को abort करता है।
  • HITL और long-running deferral को enable करने के लिए RuntimeCapabilities::supports_suspension का true होना आवश्यक है; prompt model को language/environment का वर्णन करता है।

पूर्ण, minimal implementation के लिए examples/codeact_agent/src/runtime.rs देखें जो suspend/resume का support करता है।

Monty के माध्यम से Python

इच्छित production adapter है adk-codeact-monty, जो Pydantic Monty द्वारा backed एक CodeRuntime है। यह model को Python लिखकर act करने देता है, बिना container या subprocess के in-process चलता है, और एक paused run को bytes में snapshot करता है — बिल्कुल वही जो suspend/resume को चाहिए। यह एक regular workspace member है — Monty crates.io पर 0.0.19 से मौजूद है (crates monty, monty-types, और monty-fs) और rustc 1.95+ की आवश्यकता होती है।

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 access

एक script द्वारा attempted operating-system effects — filesystem reads/writes, os.getenv/os.environ, और date.today()/datetime.now() — host-controlled policy के विरुद्ध in place serviced होते हैं। ये tools नहीं हैं और agent loop को कभी pause नहीं करते। डिफ़ॉल्ट रूप से एक runtime पूरी तरह sandboxed होता है (कोई filesystem access नहीं, खाली environment, host clock enabled)। builder के साथ specific access दें:

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

Network और subprocess access का कोई Monty OS-call surface नहीं है और policy के बावजूद उपलब्ध नहीं रहते। दी गई access model को system prompt में described की जाती है, ताकि उसे पता रहे कि वह किन paths को read या write कर सकता है और कौन-से environment variables मौजूद हैं।

Monty केवल pathlib.Path का एक subset implement करता है, इसलिए जब paths mounted हों तो prompt exact supported methods list करता है (कोई भी अन्य AttributeError raise करता है):

  • Read/query (any mount): exists(), is_file(), is_dir(), is_symlink(), read_text(), read_bytes(), stat(), iterdir(), resolve(), absolute(), open("r")
  • Write (सिर्फ read-write mounts): write_text(), write_bytes(), append_text(), append_bytes(), mkdir(), unlink(), rmdir(), rename(), open("w")/open("a")
  • Pure path ops (कोई I/O नहीं): the / operator और joinpath(), is_absolute(), with_name(), with_stem(), with_suffix(), as_posix(), और .name, .parent, .stem, .suffix, .suffixes, .parts properties।

Tools को एक single built-in function के माध्यम से invoke किया जाता है, call_tool("name", {"arg": value, ...}) — tool call करने का यही एकमात्र तरीका है; वे कभी भी bare callables के रूप में scope में नहीं होते। Tool का नाम एक string literal है और हर argument एक dict में string-keyed entry होता है, इसलिए वास्तविक name serialized continuation के अंदर travel करता है (host-side name table के बिना suspend/resume में survive करते हुए), एक tool और हर argument कोई भी name रख सकता है ("fetch-cart" जैसा valid Python identifier नहीं, एक Python keyword, या यहाँ तक कि "call_tool" भी नहीं), और driver dict की entries को नाम से बिल्कुल bind करता है — कोई positional inference नहीं। हर tool prompt में एक call_tool("name", {...}) usage line के रूप में अपने parameters और description के साथ दिखाई देता है। इस एक form के अलावा कुछ भी — एक bare fetch_cart(...), keyword arguments, non-dict argument, या non-string key — corrective error के साथ refused किया जाता है ना कि silently dispatched, इसलिए model को सीखने के लिए exactly एक calling form मिलता है।

चलाने योग्य examples/codeact_monty_agent पूरी तरह offline real Python के विरुद्ध एक CodeActAgent चलाता है।