Skip to main content

anda_core/
lib.rs

1//! Core traits, data models, and utility helpers for the Anda agent framework.
2//!
3//! `anda_core` defines the stable interfaces shared by Anda runtimes,
4//! agents, tools, model adapters, and clients. It intentionally keeps runtime
5//! orchestration and provider integrations out of this crate; those pieces are
6//! implemented by higher-level crates such as `anda_engine`.
7//!
8//! The main building blocks are:
9//! - [`Agent`] and [`AgentSet`] for registering and running AI agents.
10//! - [`Tool`] and [`ToolSet`] for type-safe tool implementations.
11//! - [`BaseContext`] and [`AgentContext`] for execution capabilities.
12//! - [`Message`], [`ContentPart`], [`CompletionRequest`], and related model
13//!   types for LLM provider adapters.
14//! - HTTP and CBOR/Candid RPC helpers for remote engine and canister calls.
15
16use object_store::path::DELIMITER;
17use std::{future::Future, pin::Pin};
18
19pub mod agent;
20pub mod context;
21pub mod http;
22pub mod json;
23pub mod model;
24pub mod tool;
25
26pub use agent::*;
27pub use context::*;
28pub use http::*;
29pub use json::*;
30pub use model::*;
31pub use tool::*;
32
33/// A type alias for a boxed error that is thread-safe and sendable across threads.
34/// This is commonly used as a return type for functions that can return various error types.
35pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
36
37/// A type alias for a boxed future that is thread-safe and sendable across threads.
38pub type BoxPinFut<T> = Pin<Box<dyn Future<Output = T> + Send>>;
39
40/// A type alias for a borrowed boxed future that is thread-safe and sendable.
41pub type BoxFut<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
42
43/// Returns a lowercase copy of an object-store path.
44///
45/// Lowercasing operates on the path's raw (already percent-encoded)
46/// representation and re-parses it with [`Path::parse`], which does not
47/// re-encode. Using `From<String>` here would percent-encode reserved
48/// characters a second time (e.g. `%` -> `%25`), so it is avoided.
49pub fn path_lowercase(path: &Path) -> Path {
50    let mut raw = path.to_string();
51    raw.make_ascii_lowercase();
52    // The input comes from a valid `Path`, so `parse` (which treats the string
53    // as already encoded) succeeds; fall back defensively just in case.
54    Path::parse(&raw).unwrap_or_else(|_| Path::from(raw))
55}
56
57/// Joins two object-store paths without percent-encoding and lowercases the result.
58///
59/// Root paths are treated as empty namespaces: joining root with `b` returns
60/// `b`, and joining `a` with root returns `a` (a root path yields no parts).
61///
62/// Segments are carried over verbatim (already percent-encoded), so the join is
63/// idempotent: re-joining a namespace with a key taken from `store_list` output
64/// does not double-encode reserved characters.
65pub fn path_join(a: &Path, b: &Path) -> Path {
66    path_lowercase(&Path::from_iter(a.parts().chain(b.parts())))
67}
68
69/// Validates a single path component used in agent, tool, or user namespaces.
70///
71/// The value must be non-empty, must not contain the object-store path
72/// delimiter, and must round-trip through [`Path`] without normalization.
73pub fn validate_path_part(part: &str) -> Result<(), BoxError> {
74    if part.is_empty() || part.contains(DELIMITER) || Path::from(part).as_ref() != part {
75        return Err(format!("invalid path part: {}", part).into());
76    }
77
78    Ok(())
79}
80
81/// Validates an agent or tool function name.
82///
83/// # Rules
84/// - Must not be empty
85/// - Must not exceed 64 bytes
86/// - Must start with a lowercase letter
87/// - Can only contain: lowercase letters (a-z), digits (0-9), underscores (_), and hyphens (-)
88pub fn validate_function_name(name: &str) -> Result<(), BoxError> {
89    if name.is_empty() {
90        return Err("empty string".into());
91    }
92
93    if name.len() > 64 {
94        return Err("string length exceeds the limit 64".into());
95    }
96
97    let mut iter = name.chars();
98    if !matches!(iter.next(), Some('a'..='z')) {
99        return Err("name must start with a lowercase letter".into());
100    }
101
102    for c in iter {
103        if !matches!(c, 'a'..='z' | '0'..='9' | '_' | '-') {
104            return Err(format!("invalid character: {}", c).into());
105        }
106    }
107    Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_path_lowercase() {
116        let a = Path::from("a/Foo");
117        assert_eq!(path_lowercase(&a).as_ref(), "a/foo");
118    }
119
120    #[test]
121    fn test_path_join_handles_root() {
122        let root = Path::default();
123        let p = Path::from("a/b");
124        assert_eq!(path_join(&root, &p).as_ref(), "a/b");
125        assert_eq!(path_join(&p, &root).as_ref(), "a/b");
126        assert_eq!(path_join(&root, &root).as_ref(), "");
127        assert_eq!(path_join(&p, &Path::from("c")).as_ref(), "a/b/c");
128    }
129
130    #[test]
131    fn test_path_join_is_idempotent_and_does_not_double_encode() {
132        let ns = Path::from("agents/store");
133        // `*` is percent-encoded by object_store (`a*b` -> `a%2Ab`).
134        let key = Path::from("a*b");
135        let joined = path_join(&ns, &key);
136
137        // The reserved `%` in the encoded key is not itself re-encoded.
138        assert!(!joined.as_ref().contains("%25"));
139
140        // Re-joining an already-encoded key (e.g. a `store_list` location) with a
141        // namespace must not double-encode it.
142        let rejoined = path_join(&Path::default(), &joined);
143        assert_eq!(joined, rejoined);
144        assert_eq!(path_lowercase(&joined), joined);
145    }
146
147    #[test]
148    fn test_validate_path_part() {
149        assert!(validate_path_part("foo").is_ok());
150        assert!(validate_path_part("fOO").is_ok());
151        assert!(validate_path_part("").is_err());
152        assert!(validate_path_part("foo/").is_err());
153        assert!(validate_path_part("/foo").is_err());
154        assert!(validate_path_part("foo/bar").is_err());
155        assert!(validate_path_part("foo/bar/").is_err());
156    }
157
158    #[test]
159    fn test_validate_function_name() {
160        assert!(validate_function_name("foo").is_ok());
161        assert!(validate_function_name("foo_bar9").is_ok());
162        assert!(validate_function_name("foo-bar").is_ok());
163
164        assert!(validate_function_name("").is_err());
165        assert!(validate_function_name("9foo").is_err());
166        assert!(validate_function_name("foo.bar").is_err());
167        assert!(validate_function_name("foo/bar").is_err());
168        assert!(validate_function_name(&"a".repeat(65)).is_err());
169    }
170}