enki_runtime/
lib.rs

1//! # Enki Runtime
2//!
3//! A Rust-based agent mesh framework for building local and distributed AI agent systems.
4//!
5//! ## Architecture
6//!
7//! Enki Runtime is modular, split into focused sub-crates that are re-exported here:
8//!
9//! | Crate | Description |
10//! |-------|-------------|
11//! | [`core`] | Core abstractions: Agent, Memory, Mesh, Message |
12//! | [`llm`] | LLM integration with 13+ providers |
13//! | [`local`] | Local mesh implementation |
14//! | [`memory`] | Memory backend implementations |
15//! | [`logging`]/[`metrics`] | Observability |
16//! | [`mcp`] | Model Context Protocol (feature: `mcp`) |
17//!
18//! ## Features
19//!
20//! - **Agent Framework**: Build autonomous AI agents with a simple trait-based API
21//! - **Local Mesh**: Connect multiple agents for inter-agent communication
22//! - **LLM Integration**: Built-in support for OpenAI, Anthropic, Ollama, Google, and 10+ more
23//! - **Memory Backends**: Pluggable memory (in-memory, SQLite, Redis)
24//! - **TOML Configuration**: Load agents from config files
25//! - **Async-first**: Built on Tokio for high performance
26//! - **Observability**: Structured logging and metrics
27//!
28//! ## Quick Start
29//!
30//! ### Custom Agent
31//!
32//! ```rust,no_run
33//! use enki_runtime::{Agent, AgentContext, LocalMesh, Message};
34//! use enki_runtime::core::error::Result;
35//! use enki_runtime::core::mesh::Mesh;
36//! use async_trait::async_trait;
37//!
38//! struct MyAgent { name: String }
39//!
40//! #[async_trait]
41//! impl Agent for MyAgent {
42//!     fn name(&self) -> String { self.name.clone() }
43//!
44//!     async fn on_message(&mut self, msg: Message, _ctx: &mut AgentContext) -> Result<()> {
45//!         println!("Received: {:?}", msg.topic);
46//!         Ok(())
47//!     }
48//! }
49//!
50//! #[tokio::main]
51//! async fn main() -> anyhow::Result<()> {
52//!     let mesh = LocalMesh::new("my-mesh");
53//!     mesh.add_agent(Box::new(MyAgent { name: "agent".into() })).await?;
54//!     mesh.start().await?;
55//!     Ok(())
56//! }
57//! ```
58//!
59//! ### LLM Agent
60//!
61//! ```rust,no_run
62//! use enki_runtime::{LlmAgent, AgentContext};
63//!
64//! #[tokio::main]
65//! async fn main() -> anyhow::Result<()> {
66//!     let mut agent = LlmAgent::builder("assistant", "ollama::gemma3:latest")
67//!         .with_system_prompt("You are helpful.")
68//!         .with_temperature(0.7)
69//!         .build()?;
70//!
71//!     let mut ctx = AgentContext::new("test".into(), None);
72//!     let response = agent.send_message_and_get_response("Hello!", &mut ctx).await?;
73//!     println!("{}", response);
74//!     Ok(())
75//! }
76//! ```
77//!
78//! ### TOML Configuration
79//!
80//! ```rust,no_run
81//! use enki_runtime::{LlmAgent, LlmAgentFromConfig};
82//! use enki_runtime::config::AgentConfig;
83//!
84//! let config = AgentConfig::from_file("agent.toml").unwrap();
85//! let agent = LlmAgent::from_config(config).unwrap();
86//! ```
87//!
88//! ## Feature Flags
89//!
90//! - `sqlite` - SQLite memory backend
91//! - `redis` - Redis memory backend  
92//! - `mcp` - Model Context Protocol support
93//! - `full` - All optional features
94
95// Configuration module
96pub mod config;
97
98// Re-export sub-crates as modules
99pub use enki_core as core;
100pub use enki_llm as llm;
101pub use enki_local as local;
102pub use enki_memory as memory;
103pub use enki_observability::logging;
104pub use enki_observability::metrics;
105
106#[cfg(feature = "mcp")]
107pub use enki_mcp as mcp;
108
109// Convenience re-exports at crate root
110pub use config::{AgentConfig, MemoryBackendType, MemoryConfig, MeshConfig};
111pub use enki_core::{
112    Agent, AgentContext, AgentEvent, Memory, MemoryEntry, MemoryQuery, Mesh, Message, VectorMemory,
113};
114pub use enki_llm::{LLMConfig, LlmAgent, LlmAgentBuilder, UniversalLLMClient};
115pub use enki_local::LocalMesh;
116pub use enki_memory::InMemoryBackend;
117
118#[cfg(feature = "sqlite")]
119pub use enki_memory::SqliteBackend;
120
121#[cfg(feature = "redis")]
122pub use enki_memory::RedisBackend;
123
124#[cfg(feature = "qdrant")]
125pub use enki_memory::QdrantBackend;
126
127#[cfg(feature = "chromadb")]
128pub use enki_memory::ChromaDBBackend;
129
130#[cfg(feature = "pinecone")]
131pub use enki_memory::PineconeBackend;
132
133#[cfg(feature = "mcp")]
134pub use enki_mcp::{McpAgentServer, McpClient, McpTransport};
135
136/// Extension trait for creating [`LlmAgent`] from TOML configuration.
137///
138/// Import this trait to use [`LlmAgent::from_config`]:
139///
140/// ```rust,no_run
141/// use enki_runtime::{LlmAgent, LlmAgentFromConfig};
142/// use enki_runtime::config::AgentConfig;
143///
144/// let config = AgentConfig::from_file("agent.toml").unwrap();
145/// let agent = LlmAgent::from_config(config).unwrap();
146/// ```
147pub trait LlmAgentFromConfig {
148    /// Create an LlmAgent from an [`AgentConfig`].
149    fn from_config(config: AgentConfig) -> core::error::Result<LlmAgent>;
150}
151
152impl LlmAgentFromConfig for LlmAgent {
153    fn from_config(config: AgentConfig) -> core::error::Result<LlmAgent> {
154        let mut builder = LlmAgentBuilder::new(&config.name, &config.model)
155            .with_system_prompt(&config.system_prompt);
156
157        if let Some(ref api_key) = config.api_key {
158            builder = builder.with_api_key(api_key);
159        }
160
161        if let Some(temperature) = config.temperature {
162            builder = builder.with_temperature(temperature);
163        }
164
165        if let Some(max_tokens) = config.max_tokens {
166            builder = builder.with_max_tokens(max_tokens);
167        }
168
169        builder.build()
170    }
171}
172
173/// Extension trait for creating memory backends from TOML configuration.
174///
175/// # Example
176///
177/// ```rust,no_run
178/// use enki_runtime::{InMemoryBackend, MemoryFromConfig};
179/// use enki_runtime::config::MemoryConfig;
180///
181/// # async fn example() -> anyhow::Result<()> {
182/// let config = MemoryConfig::in_memory().with_max_entries(1000);
183/// let memory = InMemoryBackend::from_config(&config);
184/// # Ok(())
185/// # }
186/// ```
187pub trait MemoryFromConfig {
188    /// Create a memory backend from a [`MemoryConfig`].
189    fn from_config(config: &MemoryConfig) -> Self;
190}
191
192impl MemoryFromConfig for InMemoryBackend {
193    fn from_config(config: &MemoryConfig) -> Self {
194        let mut backend = InMemoryBackend::new();
195
196        if let Some(max_entries) = config.max_entries {
197            backend = backend.with_max_entries(max_entries);
198        }
199
200        if let Some(ttl_seconds) = config.ttl_seconds {
201            backend = backend.with_ttl_seconds(ttl_seconds);
202        }
203
204        backend
205    }
206}
207
208/// Async trait for creating memory backends that require async initialization.
209///
210/// Used for SQLite, Redis, and vector database backends that need async connection setup.
211#[cfg(any(
212    feature = "sqlite",
213    feature = "redis",
214    feature = "qdrant",
215    feature = "chromadb",
216    feature = "pinecone"
217))]
218#[async_trait::async_trait]
219pub trait MemoryFromConfigAsync {
220    /// Create a memory backend from a [`MemoryConfig`] asynchronously.
221    async fn from_config_async(config: &MemoryConfig) -> core::error::Result<Self>
222    where
223        Self: Sized;
224}
225
226#[cfg(feature = "sqlite")]
227#[async_trait::async_trait]
228impl MemoryFromConfigAsync for SqliteBackend {
229    async fn from_config_async(config: &MemoryConfig) -> core::error::Result<Self> {
230        let path = config.path.as_deref().unwrap_or(":memory:");
231        SqliteBackend::new(path).await
232    }
233}
234
235#[cfg(feature = "redis")]
236#[async_trait::async_trait]
237impl MemoryFromConfigAsync for RedisBackend {
238    async fn from_config_async(config: &MemoryConfig) -> core::error::Result<Self> {
239        let url = config.url.as_deref().unwrap_or("redis://localhost:6379");
240        let mut backend = RedisBackend::new(url).await?;
241
242        if let Some(ref prefix) = config.prefix {
243            backend = backend.with_prefix(prefix);
244        }
245
246        if let Some(ttl_seconds) = config.ttl_seconds {
247            backend = backend.with_ttl_seconds(ttl_seconds);
248        }
249
250        Ok(backend)
251    }
252}
253
254#[cfg(feature = "qdrant")]
255#[async_trait::async_trait]
256impl MemoryFromConfigAsync for QdrantBackend {
257    async fn from_config_async(config: &MemoryConfig) -> core::error::Result<Self> {
258        let grpc_url = config
259            .grpc_url
260            .as_deref()
261            .unwrap_or("http://localhost:6334");
262        let collection = config.collection.as_deref().unwrap_or("enki_memory");
263        let dimensions = config.dimensions.unwrap_or(384);
264
265        QdrantBackend::new(grpc_url, collection, dimensions).await
266    }
267}
268
269#[cfg(feature = "chromadb")]
270#[async_trait::async_trait]
271impl MemoryFromConfigAsync for ChromaDBBackend {
272    async fn from_config_async(config: &MemoryConfig) -> core::error::Result<Self> {
273        let url = config.url.as_deref().unwrap_or("http://localhost:8000");
274        let collection = config.collection.as_deref().unwrap_or("enki_memory");
275
276        ChromaDBBackend::new(url, collection).await
277    }
278}
279
280#[cfg(feature = "pinecone")]
281#[async_trait::async_trait]
282impl MemoryFromConfigAsync for PineconeBackend {
283    async fn from_config_async(config: &MemoryConfig) -> core::error::Result<Self> {
284        let api_key = config
285            .api_key
286            .as_deref()
287            .ok_or_else(|| core::error::Error::MemoryError("Pinecone requires api_key".into()))?;
288        let collection = config.collection.as_deref().unwrap_or("enki-memory");
289        let dimensions = config.dimensions.unwrap_or(1536);
290
291        PineconeBackend::new(api_key, collection, dimensions).await
292    }
293}