Example: Code agent

An agent whose model emits executable shell snippets that a LocalCodeExecutor runs in a subprocess, using a scripted MockModel so no API key is needed.

code_agent wires a CodeExecutor into an LlmAgent: the model emits an ExecutableCode part, the framework runs it in a local subprocess, and the execution result is fed back to the model for a summary. The model is a pre-scripted MockModel, so the demo runs offline with no API key.

What it demonstrates

  • Attaching a code executor with LlmAgent::builder(...).code_executor(...).
  • Configuring LocalCodeExecutor away from its python3 default to /bin/sh -s via with_interpreter / with_args.
  • Scripting deterministic model turns with adk_rs::core::testing::MockModel (push_response, push_text) — see Testing.
  • The code-execution event shapes: Part::ExecutableCode in, Part::CodeExecutionResult out.
  • Pattern-matching on individual Part variants instead of using text_concat.

Run it

bashbash
cargo run --example code_agent --features "code-exec,testing"

No environment variables are required: the testing feature exposes MockModel outside the crate’s own tests, and code-exec provides LocalCodeExecutor. The executor here spawns /bin/sh, so the demo assumes a Unix-like host.

Full source

examples/code_agent.rsrust
use std::sync::Arc;

use adk_rs::agents::LlmAgent;
use adk_rs::code_exec::local::LocalCodeExecutor;
use adk_rs::core::testing::MockModel;
use adk_rs::core::{LlmResponse, Model, SessionService};
use adk_rs::genai_types::part::ExecutableCode;
use adk_rs::genai_types::{Content, Part, Role};
use adk_rs::runner::Runner;
use adk_rs::services::mem::InMemorySessionService;
use futures::StreamExt;

#[tokio::main]
async fn main() -> adk_rs::Result<()> {
    // Pre-script a model that emits a single ExecutableCode part.
    let model = Arc::new(MockModel::new("mock-code"));
    model.push_response(LlmResponse {
        content: Some(Content {
            role: Role::Model,
            parts: vec![Part::ExecutableCode(ExecutableCode {
                language: "shell".into(),
                code: "echo hello && echo 'computed in container'".into(),
            })],
        }),
        ..LlmResponse::default()
    });
    // Second turn: model summarises the result.
    model.push_text("I ran the script and got 'hello' plus a summary line.");

    let executor = Arc::new(
        LocalCodeExecutor::new()
            .with_interpreter("/bin/sh")
            .with_args(vec!["-s".into()]),
    );

    let agent = Arc::new(
        LlmAgent::builder("coder")
            .model(model as Arc<dyn Model>)
            .instruction("Solve problems by emitting shell snippets and explaining the output.")
            .code_executor(executor)
            .build()?,
    );
    let svc: Arc<dyn SessionService> = Arc::new(InMemorySessionService::new());
    let runner = Runner::builder()
        .app_name("code-demo")
        .agent(agent)
        .session_service(svc)
        .build()?;

    let mut stream = runner.run("user", None, "Print a greeting.").await?;
    while let Some(ev) = stream.next().await {
        let ev = ev?;
        if let Some(c) = &ev.response.content {
            for p in &c.parts {
                match p {
                    Part::Text(t) => println!("[{}] {t}", ev.author),
                    Part::ExecutableCode(ec) => {
                        println!("[{}] code ({})\n{}", ev.author, ec.language, ec.code);
                    }
                    Part::CodeExecutionResult(r) => {
                        println!(
                            "[{}] result outcome={:?} output={:?}",
                            ev.author, r.outcome, r.output
                        );
                    }
                    _ => {}
                }
            }
        }
    }
    Ok(())
}

Setup: a scripted model

MockModel returns pre-queued LlmResponses in order, which makes the demo deterministic and key-free. The first queued response contains a single Part::ExecutableCode with language: "shell"; the second, queued via the push_text shorthand, is the plain-text summary the "model" gives after seeing the execution result. A real agent would use a live provider here — the rest of the program is unchanged.

The executor

LocalCodeExecutor::new() defaults to python3 - with a 30-second timeout and 2 retries; the example overrides it to /bin/sh -s, so the shell reads the snippet from stdin. This executor is subprocess isolation only — not a security boundary. For untrusted code, use ContainerCodeExecutor behind the code-exec-docker feature, which is locked down by default (no network, read-only rootfs, memory/CPU/pids caps, non-root). See Code execution.

The execution roundtrip

With .code_executor(...) set, the agent watches model output for ExecutableCode parts. When one appears, the executor runs it and the framework appends a CodeExecutionResult part (with an Outcome such as OutcomeOk or OutcomeFailed, plus captured output) before invoking the model again — mirroring the function-tool roundtrip in weather_agent, but for code instead of declared functions.

Expected output

texttext
[coder] code (shell)
echo hello && echo 'computed in container'
[coder] result outcome=OutcomeOk output="hello\ncomputed in container\n"
[coder] I ran the script and got 'hello' plus a summary line.

Three phases, three event groups: the emitted code, the execution result with outcome=OutcomeOk and the captured stdout, and the scripted summary text.

  • Code executionCodeExecutor, LocalCodeExecutor, and the Docker executor’s hardening flags.
  • TestingMockModel and the testing feature.
  • EventsPart variants and how they appear on the stream.
  • Security — why local execution is not a sandbox.