Documentation Index Fetch the complete documentation index at: https://mintlify.com/dallay/corvus/llms.txt
Use this file to discover all available pages before exploring further.
Corvus is built on a trait-driven architecture that enables clean extensibility without tight coupling. This page explains the core traits and how to implement them.
Why Traits?
The trait-based design provides several key benefits:
Each component is self-contained and can be developed, tested, and deployed independently.
Add new providers, tools, or channels without modifying core code.
Rust’s type system ensures implementations meet contracts at compile time.
Mock implementations make testing straightforward.
Zero-cost abstractions with static dispatch where possible.
Core Traits
Corvus defines six primary traits that form the extension surface:
Provider Trait
The Provider trait abstracts LLM API interactions.
Location: clients/agent-runtime/src/providers/traits.rs
#[async_trait]
pub trait Provider : Send + Sync {
/// Query provider capabilities (native tool calling, streaming, etc.)
fn capabilities ( & self ) -> ProviderCapabilities {
ProviderCapabilities :: default ()
}
/// Convert tool specs to provider-native format
fn convert_tools ( & self , tools : & [ ToolSpec ]) -> ToolsPayload {
ToolsPayload :: PromptGuided {
instructions : build_tool_instructions_text ( tools ),
}
}
/// One-shot chat with optional system prompt
async fn chat_with_system (
& self ,
system_prompt : Option < & str >,
message : & str ,
model : & str ,
temperature : f64 ,
) -> anyhow :: Result < String >;
/// Multi-turn conversation with history
async fn chat_with_history (
& self ,
messages : & [ ChatMessage ],
model : & str ,
temperature : f64 ,
) -> anyhow :: Result < String > { /* default impl */ }
/// Structured chat API for agent loop (with tool calls)
async fn chat (
& self ,
request : ChatRequest <' _ >,
model : & str ,
temperature : f64 ,
) -> anyhow :: Result < ChatResponse > { /* default impl */ }
/// Chat with native tool calling support
async fn chat_with_tools (
& self ,
messages : & [ ChatMessage ],
tools : & [ serde_json :: Value ],
model : & str ,
temperature : f64 ,
) -> anyhow :: Result < ChatResponse > { /* default impl */ }
/// Whether provider supports streaming responses
fn supports_streaming ( & self ) -> bool {
false
}
/// Streaming chat with optional system prompt
fn stream_chat_with_system (
& self ,
system_prompt : Option < & str >,
message : & str ,
model : & str ,
temperature : f64 ,
options : StreamOptions ,
) -> stream :: BoxStream <' static , StreamResult < StreamChunk >> { /* default */ }
/// Warm up HTTP connection pool
async fn warmup ( & self ) -> anyhow :: Result <()> {
Ok (())
}
}
Key types:
pub struct ChatMessage {
pub role : String , // "system" | "user" | "assistant" | "tool"
pub content : String ,
}
pub struct ChatResponse {
pub text : Option < String >,
pub tool_calls : Vec < ToolCall >,
}
pub struct ToolCall {
pub id : String ,
pub name : String ,
pub arguments : String , // JSON
}
pub struct ProviderCapabilities {
pub native_tool_calling : bool ,
}
Example implementation:
use async_trait :: async_trait;
pub struct CustomProvider {
client : reqwest :: Client ,
api_key : String ,
}
#[async_trait]
impl Provider for CustomProvider {
fn capabilities ( & self ) -> ProviderCapabilities {
ProviderCapabilities {
native_tool_calling : true ,
}
}
async fn chat_with_system (
& self ,
system_prompt : Option < & str >,
message : & str ,
model : & str ,
temperature : f64 ,
) -> anyhow :: Result < String > {
// Make API request to your LLM provider
let response = self . client
. post ( "https://api.custom-llm.com/v1/chat" )
. json ( & serde_json :: json! ({
"model" : model ,
"messages" : [
{ "role" : "system" , "content" : system_prompt . unwrap_or ( "" )},
{ "role" : "user" , "content" : message }
],
"temperature" : temperature
}))
. send ()
. await ?
. json :: < serde_json :: Value >()
. await ? ;
Ok ( response [ "choices" ][ 0 ][ "message" ][ "content" ]
. as_str ()
. unwrap_or ( "" )
. to_string ())
}
}
Channel Trait
The Channel trait abstracts messaging platform interactions.
Location: clients/agent-runtime/src/channels/traits.rs
#[async_trait]
pub trait Channel : Send + Sync {
/// Human-readable channel name
fn name ( & self ) -> & str ;
/// Send a message through this channel
async fn send ( & self , message : & SendMessage ) -> anyhow :: Result <()>;
/// Start listening for incoming messages (long-running)
async fn listen (
& self ,
tx : tokio :: sync :: mpsc :: Sender < ChannelMessage >,
) -> anyhow :: Result <()>;
/// Check if channel is healthy
async fn health_check ( & self ) -> bool {
true
}
/// Signal that the bot is processing (typing indicator)
async fn start_typing ( & self , recipient : & str ) -> anyhow :: Result <()> {
Ok (())
}
/// Stop typing indicator
async fn stop_typing ( & self , recipient : & str ) -> anyhow :: Result <()> {
Ok (())
}
/// Whether this channel supports progressive message updates
fn supports_draft_updates ( & self ) -> bool {
false
}
/// Send an initial draft message (returns message ID)
async fn send_draft (
& self ,
message : & SendMessage ,
) -> anyhow :: Result < Option < String >> {
Ok ( None )
}
/// Update a previously sent draft with new content
async fn update_draft (
& self ,
recipient : & str ,
message_id : & str ,
text : & str ,
) -> anyhow :: Result <()> {
Ok (())
}
/// Finalize draft with complete response
async fn finalize_draft (
& self ,
recipient : & str ,
message_id : & str ,
text : & str ,
) -> anyhow :: Result <()> {
Ok (())
}
}
Key types:
pub struct ChannelMessage {
pub id : String ,
pub sender : String ,
pub reply_target : String ,
pub content : String ,
pub channel : String ,
pub timestamp : u64 ,
}
pub struct SendMessage {
pub content : String ,
pub recipient : String ,
pub subject : Option < String >,
}
The Tool trait defines agent capabilities.
Location: clients/agent-runtime/src/tools/traits.rs
#[async_trait]
pub trait Tool : Send + Sync {
/// Tool name (used in LLM function calling)
fn name ( & self ) -> & str ;
/// Human-readable description
fn description ( & self ) -> & str ;
/// JSON schema for parameters
fn parameters_schema ( & self ) -> serde_json :: Value ;
/// Execute the tool with given arguments
async fn execute (
& self ,
args : serde_json :: Value ,
) -> anyhow :: Result < ToolResult >;
/// Get the full spec for LLM registration
fn spec ( & self ) -> ToolSpec {
ToolSpec {
name : self . name () . to_string (),
description : self . description () . to_string (),
parameters : self . parameters_schema (),
source : None ,
}
}
}
Key types:
pub struct ToolResult {
pub success : bool ,
pub output : String ,
pub error : Option < String >,
}
pub struct ToolSpec {
pub name : String ,
pub description : String ,
pub parameters : serde_json :: Value , // JSON Schema
pub source : Option < ToolSourceMetadata >,
}
Example implementation:
use async_trait :: async_trait;
pub struct WeatherTool ;
#[async_trait]
impl Tool for WeatherTool {
fn name ( & self ) -> & str {
"get_weather"
}
fn description ( & self ) -> & str {
"Get current weather for a location"
}
fn parameters_schema ( & self ) -> serde_json :: Value {
serde_json :: json! ({
"type" : "object" ,
"properties" : {
"location" : {
"type" : "string" ,
"description" : "City name or zip code"
},
"units" : {
"type" : "string" ,
"enum" : [ "celsius" , "fahrenheit" ],
"default" : "celsius"
}
},
"required" : [ "location" ]
})
}
async fn execute ( & self , args : serde_json :: Value ) -> anyhow :: Result < ToolResult > {
let location = args [ "location" ] . as_str ()
. ok_or_else ( || anyhow :: anyhow! ( "Missing location" )) ? ;
let units = args [ "units" ] . as_str () . unwrap_or ( "celsius" );
// Call weather API
let temp = fetch_weather ( location , units ) . await ? ;
Ok ( ToolResult {
success : true ,
output : format! ( "Temperature in {}: {}°" , location , temp ),
error : None ,
})
}
}
Memory Trait
The Memory trait abstracts persistence backends.
Location: clients/agent-runtime/src/memory/traits.rs
#[async_trait]
pub trait Memory : Send + Sync {
/// Backend name
fn name ( & self ) -> & str ;
/// Store a memory entry
async fn store (
& self ,
key : & str ,
content : & str ,
category : MemoryCategory ,
session_id : Option < & str >,
) -> anyhow :: Result <()>;
/// Recall memories matching a query
async fn recall (
& self ,
query : & str ,
limit : usize ,
session_id : Option < & str >,
) -> anyhow :: Result < Vec < MemoryEntry >>;
/// Get a specific memory by key
async fn get ( & self , key : & str ) -> anyhow :: Result < Option < MemoryEntry >>;
/// List all memory keys
async fn list (
& self ,
category : Option < & MemoryCategory >,
session_id : Option < & str >,
) -> anyhow :: Result < Vec < MemoryEntry >>;
/// Remove a memory by key
async fn forget ( & self , key : & str ) -> anyhow :: Result < bool >;
/// Count total memories
async fn count ( & self ) -> anyhow :: Result < usize >;
/// Health check
async fn health_check ( & self ) -> bool ;
/// Validate an AI response against memory-backed rules
async fn validate_response (
& self ,
user_query : & str ,
response : & str ,
session_id : Option < & str >,
) -> anyhow :: Result < MemoryValidationResult > {
Ok ( MemoryValidationResult :: default ()) // Default: permissive
}
}
Key types:
pub struct MemoryEntry {
pub id : String ,
pub key : String ,
pub content : String ,
pub category : MemoryCategory ,
pub timestamp : String ,
pub session_id : Option < String >,
pub score : Option < f64 >,
}
pub enum MemoryCategory {
Core , // Long-term facts
Daily , // Session logs
Conversation , // Chat context
Custom ( String ), // User-defined
}
RuntimeAdapter Trait
The RuntimeAdapter trait abstracts execution environments.
Location: clients/agent-runtime/src/runtime/traits.rs
pub trait RuntimeAdapter : Send + Sync {
/// Human-readable runtime name
fn name ( & self ) -> & str ;
/// Whether this runtime supports shell access
fn has_shell_access ( & self ) -> bool ;
/// Whether this runtime supports filesystem access
fn has_filesystem_access ( & self ) -> bool ;
/// Base storage path for this runtime
fn storage_path ( & self ) -> PathBuf ;
/// Whether long-running processes are supported
fn supports_long_running ( & self ) -> bool ;
/// Maximum memory budget in bytes (0 = unlimited)
fn memory_budget ( & self ) -> u64 {
0
}
/// Build a shell command process for this runtime
fn build_shell_command (
& self ,
command : & str ,
workspace_dir : & Path ,
) -> anyhow :: Result < tokio :: process :: Command >;
}
See Runtime Model for implementation details.
Observer Trait
The Observer trait enables observability and monitoring.
Location: clients/agent-runtime/src/observability/traits.rs
pub trait Observer : Send + Sync + ' static {
/// Record a discrete event
fn record_event ( & self , event : & ObserverEvent );
/// Record a numeric metric
fn record_metric ( & self , metric : & ObserverMetric );
/// Flush any buffered data
fn flush ( & self ) {}
/// Human-readable name
fn name ( & self ) -> & str ;
/// Downcast to `Any` for backend-specific operations
fn as_any ( & self ) -> & dyn std :: any :: Any ;
}
Event types:
pub enum ObserverEvent {
AgentStart { provider : String , model : String },
LlmRequest { provider : String , model : String , messages_count : usize },
LlmResponse { provider : String , model : String , duration : Duration , success : bool },
ToolCallStart { tool : String },
ToolCall { tool : String , duration : Duration , success : bool },
TurnComplete ,
ChannelMessage { channel : String , direction : String },
Error { component : String , message : String },
// ... and more
}
Factory Pattern
Corvus uses factory functions to construct trait implementations from configuration:
// Provider factory
pub fn create_provider ( config : & ProviderConfig ) -> anyhow :: Result < Box < dyn Provider >> {
match config . kind . as_str () {
"openai" => Ok ( Box :: new ( OpenAIProvider :: new ( config ) ? )),
"anthropic" => Ok ( Box :: new ( AnthropicProvider :: new ( config ) ? )),
"gemini" => Ok ( Box :: new ( GeminiProvider :: new ( config ) ? )),
_ => anyhow :: bail! ( "Unknown provider: {}" , config . kind),
}
}
// Runtime factory
pub fn create_runtime ( config : & RuntimeConfig ) -> anyhow :: Result < Box < dyn RuntimeAdapter >> {
match config . kind . as_str () {
"native" => Ok ( Box :: new ( NativeRuntime :: new ())),
"docker" => Ok ( Box :: new ( DockerRuntime :: new ( config . docker . clone ()))),
_ => anyhow :: bail! ( "Unknown runtime: {}" , config . kind),
}
}
Best Practices
When implementing Corvus traits:
Use async_trait for async trait methods
Return anyhow::Result for fallible operations
Implement Send + Sync for thread safety
Provide default implementations where sensible
Validate inputs early and return descriptive errors
Use structured logging (not println!)
Panic in trait implementations
Block the async runtime with synchronous I/O
Log sensitive data (credentials, tokens)
Ignore error handling
Make blocking HTTP calls without timeouts
Testing Traits
Mock implementations make testing straightforward:
struct MockProvider ;
#[async_trait]
impl Provider for MockProvider {
async fn chat_with_system (
& self ,
_system : Option < & str >,
_message : & str ,
_model : & str ,
_temperature : f64 ,
) -> anyhow :: Result < String > {
Ok ( "mocked response" . to_string ())
}
}
#[tokio :: test]
async fn test_agent_with_mock_provider () {
let provider = MockProvider ;
let response = provider
. chat_with_system ( None , "test" , "model" , 0.7 )
. await
. unwrap ();
assert_eq! ( response , "mocked response" );
}
Next Steps
Architecture Overview See how traits fit into the system
Building Custom Providers Implement your own LLM provider
Creating Tools Extend agent capabilities
Runtime Model Understand execution environments