Skip to content

Composition: S, C, T, P, M, G, E, A

Namespace modules for composing agent configuration: State, Context, Tools, Prompt, Middleware, Guards, Evaluation, and Artifacts.

  • a >> b means “then”. Order matters: b applies after a, to what a produced. Used for state transforms, context rewrites, middleware layers and agent pipelines.
  • a + b means “together”. Both apply, and neither sees the other’s result. Used for prompt sections, tools, guards (every guard must pass), evaluation criteria and artifact declarations.

Agents also take | (run in parallel), * (loop) and / (fallback).

Namespace Operator Purpose Consumed by Key methods
S:: >> State transforms a pipeline step (agent >> S::pick(..) >> agent), loop and branch predicates pick, rename, merge, flatten, set, defaults, drop, map
C:: >> Context rewrites, applied in order AgentBuilder::context window, user_only, model_only, head, truncate, filter, from_state
M:: >> Middleware layers, outermost first .middleware(..) log, latency, timeout, retry, audit, circuit_breaker
P:: + Prompt sections .instruction(..) role, task, constraint, format, example, persona, guidelines
T:: + Tools .tools(..) simple, contextual, typed, google_search, code_execution, mcp
G:: + Output guards, all must pass AgentBuilder::guard pii, length, json, regex, toxicity
E:: + Evaluation criteria E::suite().criteria(..) response_match, contains_match, trajectory, safety
A:: + Artifact declarations AgentBuilder::artifacts, check_contracts json_output, json_input, text_output, text_input

Before 3.0, C chained with +, M and T with |, and G and E with | (which read as “either” but meant “all”). See the migration guide.

State transforms mutate a serde_json::Value representing agent state. Chain them with >> for sequential application.

A transform chain is also a pipeline step: between two agents it reshapes the session state, and the pipeline’s text passes through unchanged.

let workflow = researcher
>> (S::pick(&["findings", "input"]) >> S::rename(&[("findings", "notes")]))
>> writer;
Method What it does
S::pick(&["a", "b"]) Keep only the listed keys, drop everything else
S::drop(&["x"]) Remove the listed keys
S::rename(&[("old", "new")]) Rename keys according to mappings
S::merge(&["x", "y"], "combined") Merge listed keys into a single nested object
S::flatten("nested") Flatten a nested object into the top level
S::set("key", json!(42)) Set a key to a fixed value
S::defaults(json!({"k": "v"})) Set default values for missing keys
S::map(fn) Apply a custom transformation function

S also provides predicates for phase transition guards and .when() modifiers:

Predicate What it checks
S::is_true("key") Key holds a truthy boolean
S::eq("key", "value") Key equals a specific string
S::one_of("key", &["a", "b"]) Key matches any of the listed strings
use gemini_adk_fluent_rs::compose::S;
// Chain transforms: pick keys, then rename
let transform = S::pick(&["name", "age"]) >> S::rename(&[("name", "customer_name")]);
let mut state = json!({"name": "Alice", "age": 30, "internal_id": "x123"});
transform.apply(&mut state);
// Result: {"customer_name": "Alice", "age": 30}
// Predicates for phase transitions
Live::builder()
.phase("verify")
.instruction("Verify the customer's identity")
.transition("main", S::is_true("verified"))
.done()
.phase("main")
.instruction("Handle the request")
.transition("billing", S::eq("issue_type", "billing"))
.transition("tech", S::one_of("issue_type", &["technical", "setup"]))
.done()

Context policies filter and transform conversation history. Chain them with >>: each rewrites the history the one before produced, so C::prepend(..) >> C::window(10) and C::window(10) >> C::prepend(..) differ.

Method What it does
C::window(n) Keep only the last n messages
C::head(n) Keep only the first n messages
C::user_only() Keep only user messages
C::model_only() Keep only model messages
C::text_only() Keep only messages containing text parts
C::exclude_tools() Remove messages with function call/response parts
C::sample(n) Keep every n-th message
C::truncate(max_chars) Truncate to approximately max_chars total text (keeps most recent)
C::prepend(content) Add a message at the start of context
C::append(content) Add a message at the end of context
C::from_state(&["key1", "key2"]) Inject state values as a context preamble
C::dedup() Remove adjacent duplicate messages
C::empty() Return empty context (for isolated agents)
C::filter(fn) Filter messages by a custom predicate on Content
C::map(fn) Transform each message with a custom function
C::custom(fn) Full custom filter over the entire history
use gemini_adk_fluent_rs::compose::C;
// Keep recent context, no tool noise, inject state
let policy = C::window(20) >> C::exclude_tools() >> C::from_state(&["user:name", "app:balance"]);
// For isolated sub-agents that should not see conversation history
let isolated = C::empty();
// Character-budget context for cost control
let budget = C::truncate(4000) >> C::dedup();

Combine tools with +. Mix runtime function tools with built-in Gemini tools.

Method What it does
a #[tool] fn’s value A documented async fn; converts into a composite directly
T::typed(name, desc, fn) A closure tool whose argument type derives JsonSchema
T::simple(name, desc, fn) A closure tool that takes no parameters
T::function(arc_fn) Register an existing Arc<dyn ToolFunction>
T::google_search() Add built-in Google Search
T::url_context() Add built-in URL context fetching
T::code_execution() Add built-in code execution
T::toolset(vec) Combine multiple tool functions into one composite
use gemini_adk_fluent_rs::compose::T;
#[derive(serde::Deserialize, schemars::JsonSchema)]
struct City {
/// The city to report on.
city: String,
}
// Combine custom tools with built-ins
let tools = T::typed("get_weather", "Get weather for a city", |args: City| async move {
Ok(json!({"temp": 22, "city": args.city}))
})
+ T::google_search()
+ T::code_execution();
assert_eq!(tools.len(), 3);
// Use in a Live session builder
Live::builder()
.tools(tools)

Compose structured prompt sections with +. Each section has a semantic kind that determines its rendering format.

Method Renders as Kind
P::role("analyst") "You are analyst." Role
P::task("analyze data") "Your task: analyze data" Task
P::constraint("be concise") "Constraint: be concise" Constraint
P::format("JSON") "Output format: JSON" Format
P::example("input", "output") "Example:\nInput: ...\nOutput: ..." Example
P::context("background info") "Context: background info" Context
P::persona("friendly, direct") "Persona: friendly, direct" Persona
P::guidelines(&["be clear", ...]) "Guidelines:\n- be clear\n- ..." Guidelines
P::text("free-form text") The text as-is Text

The PromptSectionKind enum provides semantic categories:

Kind Purpose
Role Agent role definition
Task Task description
Constraint Behavioral constraint
Format Output format specification
Example Input/output example
Context Background context
Persona Personality description
Guidelines Bulleted guideline list
Text Free-form text

P also provides instruction modifier factories that bridge the prompt module to the live phase system:

Method What it does
P::show_state(&["key1", "key2"]) Append selected state keys to the instruction
P::when(predicate, text) Conditionally append text based on state
P::context_fn(fn) Append dynamic text from a formatting function
use gemini_adk_fluent_rs::compose::P;
// Build a structured prompt
let prompt = P::role("a senior financial analyst")
+ P::task("Review the quarterly earnings report and identify trends")
+ P::constraint("Use only data from the provided report")
+ P::constraint("Flag any numbers that seem inconsistent")
+ P::format("Markdown with headers for each section")
+ P::guidelines(&[
"Start with an executive summary",
"Include specific numbers when citing trends",
"End with a risk assessment",
]);
// Render to a single instruction string
let instruction: String = prompt.into();
// "You are a senior financial analyst.\n\nYour task: Review the quarterly...\n\n..."
// Instruction modifiers for phases
Live::builder()
.phase("negotiation")
.instruction("Negotiate a payment arrangement")
.modifiers(vec![
P::show_state(&["emotional_state", "willingness_to_pay"]),
P::when(
|s| s.get::<String>("risk").unwrap_or_default() == "high",
"IMPORTANT: Show extra empathy and offer flexible options.",
),
P::context_fn(|s| {
let name = s.get::<String>("user:name").unwrap_or_default();
format!("Customer: {name}")
}),
])
.done()

Stack middleware layers with >>, outermost first. Middleware intercepts agent events, tool calls, and errors.

Method What it does
M::log() Log all agent events
M::latency() Track execution latency
M::timeout(duration) Enforce a time limit
M::retry(max) Retry on failure up to max times
M::cost() Track tool call counts as a cost proxy
M::rate_limit(rps) Enforce max requests per second
M::circuit_breaker(threshold) Open circuit after consecutive failures
M::trace() Create distributed tracing spans
M::audit() Record all tool calls for review
M::tap(fn) Custom event observer
M::before_tool(fn) Custom filter before each tool invocation
M::validate(fn) Validate tool input arguments
use gemini_adk_fluent_rs::compose::M;
use std::time::Duration;
// Production middleware stack
let middleware = M::log()
>> M::latency()
>> M::timeout(Duration::from_secs(30))
>> M::retry(3)
>> M::circuit_breaker(5)
>> M::audit();
assert_eq!(middleware.len(), 6);
// Custom validation
let validated = M::validate(|call| {
if call.name == "delete_account" && call.args.get("confirm").is_none() {
return Err("delete_account requires 'confirm' argument".into());
}
Ok(())
});

Declare input/output artifact schemas with +, and attach them to an agent with .artifacts(..). Each input counts as a read and each output as a write of artifact:{name}, so check_contracts reports an artifact input no agent produces.

Method What it does
A::output(name, mime, desc) Declare an output artifact
A::input(name, mime, desc) Declare an input artifact
A::json_output(name, desc) Shorthand for application/json output
A::json_input(name, desc) Shorthand for application/json input
A::text_output(name, desc) Shorthand for text/plain output
A::text_input(name, desc) Shorthand for text/plain input
use gemini_adk_fluent_rs::compose::A;
// Declare what an analysis agent produces and consumes
let artifacts = A::text_input("source_document", "The document to analyze")
+ A::json_output("analysis_report", "Structured analysis results")
+ A::json_output("risk_assessment", "Risk scores and flags");
assert_eq!(artifacts.all_inputs().len(), 1);
assert_eq!(artifacts.all_outputs().len(), 2);

Beyond the six namespace modules, the operators module provides structural composition via Rust operators on AgentBuilder:

Operator Type Meaning
>> Shr Sequential pipeline
| BitOr Parallel fan-out
* Mul<u32> Fixed-count loop
* Mul<LoopPredicate> Conditional loop
/ Div Fallback chain

These produce Composable nodes that form a tree. Call .compile(llm) to turn the tree into an executable TextAgent.

use gemini_adk_fluent_rs::prelude::*;
// Build a tree
let workflow = AgentBuilder::new("research").instruction("Research the topic")
>> (AgentBuilder::new("tech").instruction("Technical review")
| AgentBuilder::new("biz").instruction("Business review"))
>> AgentBuilder::new("merge").instruction("Merge perspectives");
// Compile and execute
let agent = workflow.compile(llm)?;
let result = agent.run(&state).await?;

The tree auto-flattens: a >> b >> c produces a single Pipeline with 3 steps, not nested pipelines.

Combining Operators into Full Agent Configuration

Section titled “Combining Operators into Full Agent Configuration”

The six namespaces compose orthogonally. Each configures a separate dimension:

use gemini_adk_fluent_rs::prelude::*;
// S: transform state before agent sees it
let state_prep = S::pick(&["customer", "order"]) >> S::defaults(json!({"priority": "normal"}));
// C: control what context the agent sees
let context = C::window(10) >> C::exclude_tools();
// T: equip the agent with tools
let tools = order_status() // a #[tool] fn
+ T::google_search();
// P: compose the instruction
let prompt = P::role("a customer support specialist")
+ P::task("Help the customer with their order inquiry")
+ P::constraint("Never reveal internal order IDs")
+ P::format("Conversational, friendly tone");
// A: declare I/O artifacts
let artifacts = A::json_output("resolution", "How the issue was resolved");
// M: add operational middleware
let middleware = M::log() >> M::latency() >> M::audit();

AgentBuilder is the entry point for compiling these into executable agents:

use gemini_adk_fluent_rs::builder::AgentBuilder;
let agent = AgentBuilder::new("support")
.model(ModelId::FLASH_LATEST)
.instruction("Help the customer") // or use P:: composition
.temperature(0.5)
.google_search() // or use T:: composition
.thinking(2048)
.writes("resolution")
.reads("customer_name")
.build(llm)?;
let result = agent.run(&state).await?;

Copy-on-write semantics mean every setter returns a new builder. Use builders as templates:

let base = AgentBuilder::new("analyst")
.instruction("You are a data analyst")
.temperature(0.3);
// Variants share the base configuration
let conservative = base.clone().temperature(0.1);
let creative = base.clone().temperature(0.9);

The patterns module provides common multi-agent workflows built from these operators:

use gemini_adk_fluent_rs::patterns::*;
// Review loop: worker -> reviewer -> repeat until quality target
let reviewed = review_loop(writer, reviewer, "quality", "good", 3);
// Cascade: try each agent until one succeeds
let robust = cascade(vec![primary, secondary, fallback]);
// Fan-out merge: run all in parallel, merge results
let multi = fan_out_merge(vec![analyst_a, analyst_b, analyst_c]);
// Supervised: worker -> supervisor -> repeat until approval
let approved = supervised(drafter, supervisor, "approved", 5);
// Map-over: apply agent to every item of the JSON array at state["items"]
// (a Composable node — compiles to MapOverTextAgent, composes with >>)
let batch = map_over(item_processor, "items");