#![allow(unsafe_op_in_unsafe_fn)]
#![doc = include_str!("../README.md")]
extern crate self as agy_bridge;
pub mod agent;
pub mod config;
pub mod content;
pub mod error;
pub mod hooks;
pub mod policies;
pub mod quota;
pub mod runtime;
pub mod streaming;
pub mod tools;
pub mod triggers;
pub mod types;
pub use config::{
AgentConfig, BuiltinTools, CapabilitiesConfig, GeminiConfig, LocalAgentConfig, McpServer,
McpSseServer, McpStdioServer, McpStreamableHttpServer, SystemInstructions,
};
pub use content::{Audio, Content, ContentPrimitive, Document, Image, Video};
pub use error::Error;
pub use hooks::{HookCallback, HookEntry, HookPoint, HookResult, HookSet, Hooks};
pub use llm_tool_macros::llm_tool;
pub use policies::{AskUserHandler, PolicyDecision, PolicyRule, PolicySet};
pub use runtime::RuntimeConfig;
pub use streaming::{ChatResponseHandle, ChatResult, ResponseEvent, StreamChunk};
pub use tools::{RustTool, ToolContext, ToolDefinition, ToolError, ToolOutput, ToolRegistry};
pub use triggers::{TriggerConfig, TriggerEntry};
pub use types::{ConversationMessage, MessageRole, Step, UsageMetadata};
pub mod prelude {
pub use llm_tool_macros::llm_tool;
pub use crate::{
Agent, AgyBridge,
config::{
AgentConfig, BuiltinTools, CapabilitiesConfig, GeminiConfig, LocalAgentConfig,
McpServer, McpSseServer, McpStdioServer, McpStreamableHttpServer, SystemInstructions,
},
content::{Audio, Content, ContentPrimitive, Document, Image, Video},
error::Error,
hooks::{HookPoint, HookResult, Hooks},
policies::{AskUserHandler, PolicyDecision, PolicyRule, PolicySet},
streaming::{ChatResponseHandle, ChatResult, ResponseEvent, StreamChunk},
tools::{RustTool, ToolContext, ToolDefinition, ToolError, ToolOutput, ToolRegistry},
triggers::{TriggerConfig, TriggerEntry},
types::{ConversationMessage, MessageRole, Step, UsageMetadata},
};
}
use std::sync::Arc;
pub fn load_dotenv() -> &'static std::collections::HashMap<String, String> {
use std::sync::OnceLock;
static CACHED: OnceLock<std::collections::HashMap<String, String>> = OnceLock::new();
CACHED.get_or_init(|| {
let start = std::env::var_os("CARGO_MANIFEST_DIR").map_or_else(
|| {
std::env::current_dir().unwrap_or_else(|e| {
tracing::debug!("load_dotenv: current_dir() failed: {e}, using fallback \".\"");
std::path::PathBuf::from(".")
})
},
std::path::PathBuf::from,
);
let mut dir = start.as_path();
loop {
let candidate = dir.join(".env");
if candidate.is_file() {
let mut env_map = std::collections::HashMap::new();
if let Ok(contents) = std::fs::read_to_string(&candidate) {
for line in contents.lines() {
if let Some((k, v)) = parse_dotenv_line(line)
&& std::env::var_os(k).is_none()
{
unsafe {
std::env::set_var(k, v);
}
env_map.insert(k.to_owned(), v.to_owned());
}
}
}
return env_map;
}
match dir.parent() {
Some(parent) => dir = parent,
None => return std::collections::HashMap::new(),
}
}
})
}
pub(crate) fn parse_dotenv_line(line: &str) -> Option<(&str, &str)> {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
let (k, v) = line.split_once('=')?;
let k = k.trim();
if k.is_empty() {
return None;
}
let v = v.trim();
let v = v
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.or_else(|| v.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
.unwrap_or(v);
Some((k, v))
}
pub type Agent = agent::AgentHandle<runtime::PythonRuntime>;
pub struct AgyBridge {
runtime: Arc<runtime::PythonRuntime>,
}
pub struct AgyBridgeBuilder {
config: runtime::RuntimeConfig,
}
impl AgyBridgeBuilder {
#[must_use]
pub fn channel_capacity(mut self, capacity: usize) -> Self {
self.config.channel_capacity = capacity;
self
}
#[must_use]
pub fn operation_timeout(mut self, timeout: std::time::Duration) -> Self {
self.config.operation_timeout = timeout;
self
}
#[must_use]
pub fn shutdown_timeout(mut self, timeout: std::time::Duration) -> Self {
self.config.shutdown_timeout = timeout;
self
}
#[must_use]
pub fn chat_timeout(mut self, timeout: std::time::Duration) -> Self {
self.config.chat_timeout = timeout;
self
}
#[must_use]
pub fn inter_agent_delay(mut self, delay: std::time::Duration) -> Self {
self.config.inter_agent_delay = delay;
self
}
#[must_use]
pub fn runtime_config(mut self, config: runtime::RuntimeConfig) -> Self {
self.config = config;
self
}
pub fn build(self) -> Result<AgyBridge, error::Error> {
Ok(AgyBridge {
runtime: Arc::new(runtime::PythonRuntime::new(self.config)?),
})
}
}
impl AgyBridge {
#[must_use]
pub fn builder() -> AgyBridgeBuilder {
AgyBridgeBuilder {
config: runtime::RuntimeConfig::default(),
}
}
#[must_use]
pub fn agent(&self, config: config::AgentConfig) -> AgentBuilder<'_> {
AgentBuilder {
bridge: self,
config,
registry: None,
hooks: None,
policy_handler: None,
}
}
#[must_use]
pub fn default_agent(&self) -> AgentBuilder<'_> {
self.agent(config::AgentConfig::default())
}
}
pub struct AgentBuilder<'a> {
bridge: &'a AgyBridge,
config: config::AgentConfig,
registry: Option<tools::ToolRegistry>,
hooks: Option<hooks::Hooks>,
policy_handler: Option<Arc<dyn policies::AskUserHandler>>,
}
impl AgentBuilder<'_> {
#[must_use]
pub fn tools(mut self, registry: tools::ToolRegistry) -> Self {
self.registry = Some(registry);
self
}
#[must_use]
pub fn hooks(mut self, hooks: hooks::Hooks) -> Self {
self.hooks = Some(hooks);
self
}
#[must_use]
pub fn policy_handler(mut self, handler: impl policies::AskUserHandler + 'static) -> Self {
self.policy_handler = Some(Arc::new(handler));
self
}
#[must_use]
pub fn conversation_id(mut self, id: impl Into<String>) -> Self {
self.config.conversation_id = Some(id.into());
self
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
self.config.model = model.into();
self
}
#[must_use]
pub fn system_instructions(
mut self,
instructions: impl Into<config::SystemInstructions>,
) -> Self {
self.config.system_instructions = Some(instructions.into());
self
}
#[must_use]
pub fn workspaces(
mut self,
workspaces: impl IntoIterator<Item = impl Into<std::path::PathBuf>>,
) -> Self {
self.config
.workspaces
.extend(workspaces.into_iter().map(Into::into));
self
}
#[must_use]
pub fn policies(
mut self,
policies: impl IntoIterator<Item = impl Into<policies::PolicyRule>>,
) -> Self {
self.config
.policies
.extend(policies.into_iter().map(Into::into));
self
}
#[must_use]
pub fn triggers(
mut self,
triggers: impl IntoIterator<Item = impl Into<triggers::TriggerEntry>>,
) -> Self {
self.config
.triggers
.extend(triggers.into_iter().map(Into::into));
self
}
#[must_use]
pub fn mcp_servers(
mut self,
servers: impl IntoIterator<Item = impl Into<config::McpServer>>,
) -> Self {
self.config
.mcp_servers
.extend(servers.into_iter().map(Into::into));
self
}
#[must_use]
pub fn skills(
mut self,
skills: impl IntoIterator<Item = impl Into<std::path::PathBuf>>,
) -> Self {
self.config
.skills
.extend(skills.into_iter().map(Into::into));
self
}
pub async fn build(mut self) -> Result<Agent, error::Error> {
if let Some(ref caps) = self.config.capabilities {
caps.validate().map_err(|msg| error::Error::InvalidConfig {
message: msg.to_string(),
})?;
}
let arc_registry = if let Some(registry) = self.registry {
if !self.config.tools.is_empty() {
return Err(error::Error::InvalidConfig {
message: "config.tools is non-empty and a ToolRegistry was also provided; \
pass tools via the registry or via config.tools, not both"
.to_string(),
});
}
self.config.tools = registry.definitions();
Some(Arc::new(registry))
} else {
None
};
let arc_hooks = if let Some(hooks) = self.hooks {
if !self.config.hooks.is_empty() {
return Err(error::Error::InvalidConfig {
message: "config.hooks is non-empty and a Hooks instance was also provided; \
configure hooks via Hooks or config.hooks, not both"
.to_string(),
});
}
self.config.hooks = hooks.entries();
Some(Arc::new(hooks))
} else {
None
};
let arc_policy = self.policy_handler;
agent::AgentHandle::new(
Arc::clone(&self.bridge.runtime),
self.config,
arc_registry,
arc_hooks,
arc_policy,
)
.await
}
}
impl<'a> std::future::IntoFuture for AgentBuilder<'a> {
type Output = Result<Agent, error::Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.build())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dotenv_strips_double_quotes() {
let (k, v) = parse_dotenv_line(r#"API_KEY="my-secret""#).unwrap();
assert_eq!(k, "API_KEY");
assert_eq!(v, "my-secret");
}
#[test]
fn dotenv_strips_single_quotes() {
let (k, v) = parse_dotenv_line("TOKEN='abc123'").unwrap();
assert_eq!(k, "TOKEN");
assert_eq!(v, "abc123");
}
#[test]
fn dotenv_unquoted_value_unchanged() {
let (k, v) = parse_dotenv_line("FOO=bar").unwrap();
assert_eq!(k, "FOO");
assert_eq!(v, "bar");
}
#[test]
fn dotenv_mismatched_quotes_preserved() {
let (k, v) = parse_dotenv_line(r#"KEY="value'"#).unwrap();
assert_eq!(k, "KEY");
assert_eq!(v, r#""value'"#);
}
#[test]
fn dotenv_empty_quoted_value() {
let (k, v) = parse_dotenv_line(r#"EMPTY="""#).unwrap();
assert_eq!(k, "EMPTY");
assert_eq!(v, "");
}
#[test]
fn dotenv_whitespace_around_key_value() {
let (k, v) = parse_dotenv_line(" MY_VAR = \"hello world\" ").unwrap();
assert_eq!(k, "MY_VAR");
assert_eq!(v, "hello world");
}
#[test]
fn dotenv_comment_line_is_none() {
assert!(parse_dotenv_line("# this is a comment").is_none());
}
#[test]
fn dotenv_blank_line_is_none() {
assert!(parse_dotenv_line(" ").is_none());
}
#[test]
fn dotenv_empty_key_is_none() {
assert!(parse_dotenv_line("=value").is_none());
}
#[test]
fn dotenv_no_equals_is_none() {
assert!(parse_dotenv_line("JUSTKEY").is_none());
}
#[test]
fn dotenv_value_with_internal_equals() {
let (k, v) = parse_dotenv_line("DSN=postgres://host:5432/db?opt=1").unwrap();
assert_eq!(k, "DSN");
assert_eq!(v, "postgres://host:5432/db?opt=1");
}
#[test]
fn dotenv_value_with_embedded_quotes_not_stripped() {
let (k, v) = parse_dotenv_line(r#"MSG=say "hello""#).unwrap();
assert_eq!(k, "MSG");
assert_eq!(v, r#"say "hello""#);
}
#[test]
fn test_load_dotenv_returns_static_reference_identity() {
let map1 = load_dotenv();
let map2 = load_dotenv();
assert!(std::ptr::eq(map1, map2));
}
}