Embedded Library (Single Process)
The simplest topology: depend on paladin-ai (library name paladin) and build your
agents directly in your own binary. Paladin is designed for this — the root crate is a
composition root, not a framework that owns your process — so "embed it as a library and
build each agent's behaviour in your app" is the grain of the design, not a workaround.
The code blocks below are compiled examples pulled from the
paladin-doc-examplescrate via mdBook{{#include}}, so they are guaranteed to match the current API.
When to choose it
- Choose it when you control invocation in-code, all agents share one process, and you want the least moving parts. It is the right starting point for almost every project.
- Look elsewhere when an external client needs to call your agents (HTTP service host), you need scale-out or backpressure (queue / worker), or the agents collaborate on a single task (Battalion orchestration).
One agent
Build an agent with the fluent PaladinBuilder, then run it through a
PaladinExecutionService. The mock LLM keeps the example offline; swap in
OpenAIAdapter::from_env()? (or another adapter) for real use.
use std::sync::Arc; use std::time::Duration; use paladin::MockLlmAdapter; use paladin::application::services::paladin::paladin_execution_service::PaladinExecutionService; use paladin::infrastructure::resilience::circuit_breaker::CircuitBreaker; use paladin::prelude::*; // PaladinBuilder, LlmPort, Paladin, ... #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // An offline mock LLM so this runs without an API key. // For real use: `Arc::new(OpenAIAdapter::from_env()?)`. let llm: Arc<dyn LlmPort> = Arc::new(MockLlmAdapter::new().with_response("Hello from Paladin!")); // Build an agent with the fluent builder. let agent = PaladinBuilder::new(llm.clone()) .name("Greeter") .system_prompt("You are a friendly assistant.") .build() .await?; // Execute it and print the result. let breaker = Arc::new(CircuitBreaker::new(5, 2, Duration::from_secs(30))); let service = PaladinExecutionService::new(llm, breaker, None, None); let result = service .execute(&agent, "Say hello in one sentence.") .await?; println!("{}", result.output); Ok(()) }
See the Paladin Agents guide for the full builder API — system prompt, model, temperature, loops, stop words, vision, memory (Garrison), and tools (Arsenal).
Multiple distinct agents in one process
Because Paladins are Send + Sync and everything runs on tokio, you can keep many
different agents resident in one process and route to them. A small agent registry —
a map from a name to an agent plus its execution service — is all you need:
#![allow(unused)] fn main() { use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use paladin::MockLlmAdapter; use paladin::application::services::paladin::paladin_execution_service::PaladinExecutionService; use paladin::infrastructure::resilience::circuit_breaker::CircuitBreaker; use paladin::prelude::*; // PaladinBuilder, LlmPort, Paladin, PaladinResult /// Several *distinct* agents, each with its own execution service, all resident /// in one process. Build the registry once, then route each request to an agent /// by name. This is the in-process foundation the HTTP-host topology serves. pub struct AgentRegistry { agents: HashMap<String, (Paladin, Arc<PaladinExecutionService>)>, } impl AgentRegistry { /// Construct a registry of agents that differ by system prompt (and could /// differ by model, tools, or memory). One shared LLM port and circuit /// breaker are reused across them here; give each its own if they diverge. pub async fn new() -> Result<Self, Box<dyn std::error::Error>> { let llm: Arc<dyn LlmPort> = Arc::new(MockLlmAdapter::new()); let breaker = Arc::new(CircuitBreaker::new(5, 2, Duration::from_secs(30))); let mut agents = HashMap::new(); for (name, prompt) in [ ( "researcher", "You research topics thoroughly and cite sources.", ), ("summarizer", "You write concise, faithful summaries."), ] { let agent = PaladinBuilder::new(llm.clone()) .name(name) .system_prompt(prompt) .build() .await?; let service = Arc::new(PaladinExecutionService::new( llm.clone(), breaker.clone(), None, // garrison (memory) — none in this minimal example None, // arsenal (tools) — none in this minimal example )); agents.insert(name.to_string(), (agent, service)); } Ok(Self { agents }) } /// Route an input to a named agent and run it in-process. pub async fn run( &self, agent: &str, input: &str, ) -> Result<String, Box<dyn std::error::Error>> { let (paladin, service) = self .agents .get(agent) .ok_or_else(|| format!("no agent named '{agent}'"))?; let result: PaladinResult = service.execute(paladin, input).await?; Ok(result.output) } } }
Each entry can differ by system prompt, model, tools, or memory — that is what makes them
"different agents." Calls are independent and run concurrently on the runtime, so several
run(..) futures can be in flight at once.
This registry is also the foundation of the next topology: the HTTP service host wraps exactly this map behind an HTTP handler so an external client can invoke each agent. When the agents instead collaborate on one task, reach for Battalion orchestration.
← Back to Choosing a topology