CodeActAgent (CodeAct)

CodeActAgent은 한 번에 하나의 도구 호출을 내보내는 대신 코드를 작성하고 실행하여 동작하는 LlmAgent의 동료입니다. 매 턴마다 모델은 하나의 스크립트를 생성하고, 도구는 스크립트가 조합하여 호출할 수 있는 함수로 노출되며, 스크립트는 태그가 지정된 값을 반환하여 결과를 전달합니다.

이는 CodeAct 패턴입니다. call tool A → observe → call tool B하는 대신 모델이 하나의 스크립트에서 b(a(x))을 작성하므로, 여러 단계의 작업이 한 번의 턴에서 수행됩니다. 이는 adk-agentcodeact 기능으로 활성화됩니다.

사용 시점

  • 턴마다 여러 도구를 연결하거나 결합하는 작업(데이터 처리, 일괄 작업, 접착 로직).
  • 코드 생성용으로 사후 학습된 모델.
  • 실제 인터프리터(예: Python)를 동작 기반으로 사용할 수 있는 워크플로.

네이티브 도구 호출에는 LlmAgent을 사용하는 것이 좋습니다. 샌드박스 파일/셸 코딩 하네스는 코딩 에이전트를 참조하세요.

루프 작동 방식

각 턴에서:

  1. 모델이 펜스로 묶인 코드 블록 하나(스크립트)를 내보냅니다.
  2. 스크립트가 [CodeRuntime]에서 실행됩니다. 도구 호출은 호스트에 전달되고, 호스트가 도구를 실행한 다음 결과와 함께 스크립트를 재개합니다.
  3. 스크립트가 태그가 지정된 ScriptOutput를 반환합니다.
    • observation — 모델에 다시 전달되며 루프가 계속됩니다.
    • error — 메시지로 다시 전달되며 루프가 계속됩니다.
    • final_result — 호출자에게 반환되며 루프가 종료됩니다.
    • transfer_to_agent — 제어를 다른 에이전트에 넘기며 루프가 종료됩니다.

프레임워크는 언어에 구애받지 않습니다. CodeRuntime 트레이트가 단계별 인터프리터 접점이며, 자유 형식 프롬프트를 통해 자체 언어 및 환경을 모델에 보고합니다. 프로덕션용으로 설계된 어댑터는 Rust 네이티브 Python 인터프리터인 Monty를 래핑합니다.

지속성: 일시 중단 및 재개

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

modelruntime는 필수이며, 나머지는 모두 기본값이 있습니다.

LlmAgent와의 동등성

빌더는 LlmAgentBuilder을 반영합니다.

  • 모델: generate_content_configtemperature/top_p/top_k/ max_output_tokens 단축형.
  • 지침: instruction/instruction_provider, global_instruction/global_instruction_provider, {state.key} 템플릿 주입 포함; 그리고 스킬(skills 기능).
  • 기록: include_contents.
  • 도구: 정적 tools 및 호출별 toolsets; tool_timeout, default_retry_budget/tool_retry_budget, circuit_breaker_thresholdon_tool_error 대체 수단.
  • 권한 부여: ToolConfirmationPolicy (require_tool_confirmation/require_tool_confirmation_for_all).
  • 전달: sub_agents 및 disallow_transfer_to_parent/ disallow_transfer_to_peers.
  • 출력: output_key, 수정-재시도 루프(output_max_retries)가 포함된 output_schema/output_type.
  • 콜백: before_callback/after_callback, before_model_callback/after_model_callbackbefore_tool_callback/after_tool_callback/after_tool_callback_full. 도구 실행 후 콜백은 CallbackContext::tool_outcome()을 통해 구조화된 실행 메타데이터를 검사할 수 있습니다.
  • 기능 플래그 적용: 입력/출력 가드레일(guardrails) 및 EnhancedPlugin 파이프라인(enhanced-plugins).

각 도구 호출은 인터프리터 호출 id를 전달하는 새로운 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는 실제 호스트 오류 (스냅샷 (역)직렬화, 내부 인터프리터 오류)에만 사용되며 실행을 중단합니다.
  • HITL 및 장시간 지연을 활성화하려면 RuntimeCapabilities::supports_suspensiontrue해야 합니다. prompt는 모델에 언어/환경을 설명합니다.

일시 중단 및 재개를 지원하는 완전한 최소 구현은 examples/codeact_agent/src/runtime.rs를 참조하세요.

Monty를 통한 Python

프로덕션에서 사용할 어댑터는 adk-codeact-monty이며, Pydantic Monty를 기반으로 하는 CodeRuntime입니다. 이 어댑터를 사용하면 모델이 Python을 작성하여 동작하고, 컨테이너나 서브프로세스 없이 프로세스 내에서 실행되며, 일시 중지된 실행을 바이트로 스냅샷할 수 있습니다. 이는 suspend/resume에 정확히 필요한 기능입니다. Monty 인터프리터는 adk-codeembedded-python 커널을 통해 사용되며, 이 커널은 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 액세스

스크립트가 시도하는 운영 체제 효과 — 파일 시스템 읽기/쓰기, 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 OS 호출 인터페이스가 없으므로, 정책과 관계없이 사용할 수 없습니다. 허용된 액세스는 시스템 프롬프트에서 모델에 설명되므로, 모델은 읽거나 쓸 수 있는 경로와 존재하는 환경 변수를 알 수 있습니다.

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").
  • 순수 경로 연산(I/O 없음): / 연산자와 joinpath(), is_absolute(), with_name(), with_stem(), with_suffix(), as_posix(), 그리고 .name, .parent, .stem, .suffix, .suffixes, .parts 속성.

도구는 단일 내장 함수 call_tool("name", {"arg": value, ...})를 통해 호출됩니다. 도구를 호출하는 유일한 방법이며, 독립 호출 가능 객체로 직접 스코프에 들어오는 일은 없습니다. 도구 이름은 문자열 리터럴이고 모든 인수는 하나의 딕셔너리에서 문자열 키로 지정된 항목이므로, 실제 이름은 직렬화된 연속 상태 내부에 전달됩니다(호스트 측 이름 테이블 없이도 일시 중지/재개 시 유지됨). 도구와 각 인수에는 어떤 이름이든 사용할 수 있으며("fetch-cart"과 같은 유효한 Python 식별자, Python 키워드, 심지어 "call_tool"가 아니어도 됨), 드라이버는 딕셔너리의 항목을 이름으로 정확히 바인딩합니다. 위치 기반 추론은 사용하지 않습니다. 각 도구는 매개변수와 설명이 포함된 call_tool("name", {...}) 사용 행으로 프롬프트에 표시됩니다. 이 한 가지 형식 이외의 모든 방식 — 독립적인 fetch_cart(...), 키워드 인수, 딕셔너리가 아닌 인수 또는 문자열이 아닌 키 — 은 조용히 전달되지 않고 수정 안내 오류와 함께 거부되므로, 모델이 학습해야 할 호출 형식은 정확히 하나뿐입니다.

실행 가능한 examples/codeact_monty_agent는 실제 Python에 대해 완전히 오프라인으로 CodeActAgent을 실행합니다.