LlmAgent

The LLM-powered agent: its builder, instruction templating, conversation history control, and the LLM-to-tool loop.

LlmAgent is the workhorse of adk-rs: an agent that sends the session history to a model, dispatches any function calls the model makes through a gate pipeline (confirmation → auth → run), feeds the results back, and repeats until the model produces a final answer.

Building one

Minimal constructionrust
use std::sync::Arc;
use adk_rs::agents::LlmAgent;
use adk_rs::providers::gemini::Gemini;

let agent = LlmAgent::builder("assistant")
    .description("Answers questions about the product.")
    .model(Arc::new(Gemini::from_env("gemini-2.5-flash")?))
    .instruction("You are a concise, factual assistant.")
    .build()?;

LlmAgent::builder(name) returns an LlmAgentBuilder. build() fails if name is empty or no model was set. The crate exports adk_rs::agents::DEFAULT_MODEL ("gemini-2.5-flash") as a convenient model id constant, but the builder always requires an explicit .model(...).

Builder reference

pub fn description(self, d: impl Into<String>) -> Self
Human-readable description, shown to a parent model when deciding whether to delegate to this agent.
pub fn model(self, m: Arc<dyn Model>) -> Self
The provider model (required). Any Model implementation: Gemini, Anthropic, OpenAI-compatible, or your own.
pub fn instruction(self, s: impl Into<String>) -> Self
Static system instruction. Templated with {key} placeholders against session state on every turn (see below).
pub fn instruction_dyn(self, p: InstructionProvider) -> Self
Dynamic instruction: an async function Fn(&ReadonlyContext) -> BoxFuture<Result<String>> evaluated per turn. Bypasses templating — the provider reads state itself.
pub fn global_instruction(self, s: impl Into<String>) -> Self
Static instruction prefixed before instruction (separated by a blank line). Also templated.
pub fn static_instruction(self, s: impl Into<String>) -> Self
Cache-stable instruction prefix. Sent verbatim — never templated or re-evaluated — so the system instruction stays byte-identical across turns; when set, the dynamic instruction rides in the request contents instead. Pair with context caching.
pub fn static_instruction_content(self, c: Content) -> Self
Like static_instruction but accepts arbitrary Content (e.g. multimodal parts).
pub fn tool(self, t: Arc<dyn DynTool>) -> Self
Registers one tool.
pub fn tools(self, ts: impl IntoIterator<Item = Arc<dyn DynTool>>) -> Self
Registers many tools at once (e.g. the output of an OpenAPI toolset).
pub fn sub_agent(self, a: Arc<dyn BaseAgent>) -> Self
Registers a child agent. Call repeatedly for multiple children. Declaring a sub-agent auto-registers the transfer_to_agent tool and advertises the sub-agents’ names and descriptions in the system instruction. See Multi-agent systems.
pub fn disable_transfer(self, yes: bool) -> Self
When true, transfer_to_agent requests raised by tools are ignored and the agent keeps control. Also suppresses the auto-registration of the transfer tool and the sub-agent roster in the system instruction.
pub fn max_iterations(self, n: u32) -> Self
Caps iterations of the LLM↔tool loop within a single run. Default: 16.
pub fn output_key(self, key: impl Into<String>) -> Self
Saves the agent’s final response into session state under this key, via the final event’s state_delta. With output_schema, the stored value is the parsed JSON rather than raw text.
pub fn output_schema(self, schema: Schema) -> Self
Forces structured JSON output conforming to schema (sets the request’s response schema and application/json mime type). See Structured output.
pub fn include_contents(self, ic: IncludeContents) -> Self
Controls how much conversation history the model sees (see below).
pub fn code_executor(self, ex: Arc<dyn CodeExecutor>) -> Self
(feature code-exec) Executes ExecutableCode parts emitted by the model and feeds back CodeExecutionResult parts. See Code execution.
pub fn before_agent_callback(self, cb: BeforeAgentCallback) -> Self
Hook before the agent runs; returning Some(content) short-circuits the run. See Callbacks & plugins.
pub fn after_agent_callback(self, cb: AfterAgentCallback) -> Self
Hook after the agent completes; returning Some(content) appends one more event.
pub fn before_model_callback(self, cb: BeforeModelCallback) -> Self
Hook before every model call; may rewrite the LlmRequest in place or return Some(response) to skip the call.
pub fn after_model_callback(self, cb: AfterModelCallback) -> Self
Hook after every model call; returning Some(response) replaces the model’s response.
pub fn on_model_error_callback(self, cb: OnModelErrorCallback) -> Self
Recovery hook for failed model calls; returning Some(response) recovers the turn instead of failing the run.
pub fn before_tool_callback(self, cb: BeforeToolCallback) -> Self
Hook before every tool run; may rewrite the args in place or return Some(value) to skip the tool.
pub fn after_tool_callback(self, cb: AfterToolCallback) -> Self
Hook after every tool run; returning Some(value) replaces the tool’s result.
pub fn on_tool_error_callback(self, cb: OnToolErrorCallback) -> Self
Recovery hook for failed tool calls; returning Some(value) recovers the call (otherwise the model sees {"error": ...}).
pub fn build(self) -> Result<LlmAgent>
Validates (non-empty name, model present) and constructs the agent.

Conversation history: IncludeContents

VariantEffect
IncludeContents::DefaultFull session history is sent (assembled through event compaction when summaries exist), followed by the current user turn.
IncludeContents::NoneNo history: only the system instruction and the current turn’s user content are sent. Useful for stateless steps in workflow pipelines.

Instruction templating

Static instructions (set via instruction or global_instruction) pass through adk_rs::agents::inject_session_state, which replaces {placeholder} references before each model call. Dynamic providers and static_instruction bypass injection entirely.

SyntaxMeaning
{key}Replaced with the session-state value for key. Errors if the key is missing.
{key?}Optional: replaced with the empty string when missing.
{app:key} / {user:key} / {temp:key}Prefixed state keys, matching the state scopes.
{artifact.name}Replaced with the named artifact’s content (text parts verbatim, other parts as JSON). Requires an artifact service; errors if the artifact is missing unless suffixed with ?.
anything elseBodies that are not valid state names — {"a": 1}, {1, 2}, { } — are left untouched, so JSON snippets in instructions survive.
rustrust
let agent = LlmAgent::builder("support")
    .model(model)
    .instruction("Speak in {language}. Customer tier: {user:tier?}.")
    .build()?;

The LLM↔tool loop

  1. Build the request: system instruction (static prefix + resolved dynamic part), output schema, registered tools, then the history per include_contents and the current user content. When sub-agents are declared, the sub-agent roster and the transfer_to_agent tool are appended too.
  2. Check cancellation and the RunConfig::max_llm_calls budget, then call the model. Under StreamingMode::Sse this calls stream_generate_content, yields each content chunk as a partial event, and aggregates the chunks into the final persisted event. The call is wrapped by the before_model / after_model / on_model_error callbacks.
  3. Persist the model event into the session and inspect it. No function calls? It is the final response — output_key stamps the text (or parsed JSON) into the event’s state_delta, the event is yielded, and the stream ends.
  4. Otherwise dispatch every call through the gate pipeline: tool confirmation first, then auth resolution, then the tool’s run — wrapped by the before_tool / after_tool / on_tool_error callbacks. Tool errors become {"error": ...} values the model can react to.
  5. Yield a tool-response event, then act on side effects: a transfer_to_agent hands the rest of the invocation to the named sub-agent; escalate emits a marker event and stops; a long-running or consent-gated call pauses the invocation for a later resume.
  6. Append the assistant turn and tool responses to the request contents and loop — up to max_iterations times, after which a fail-safe event with error_code: "MAX_ITERATIONS" is emitted.

Worked example

A tool-using agent with structured state outputrust
use std::sync::Arc;
use adk_rs::agents::{IncludeContents, LlmAgent};
use adk_rs::genai_types::Schema;
use adk_rs::providers::gemini::Gemini;

let model = Arc::new(Gemini::from_env("gemini-2.5-flash")?);

let extractor = LlmAgent::builder("extractor")
    .description("Extracts structured facts from a support ticket.")
    .model(model)
    .instruction("Extract the customer's issue from the ticket. Locale: {user:locale?}")
    .include_contents(IncludeContents::None)   // stateless pipeline step
    .max_iterations(4)
    .output_schema(
        Schema::object()
            .property("summary", Schema::string())
            .property("severity", Schema::string()),
    )
    .output_key("ticket_info")                 // parsed JSON lands in state
    .build()?;

A downstream agent in the same SequentialAgent can now reference {ticket_info} in its instruction, or a callback-free plugin can read it from the session state.