| name | afd-rust |
| description | Rust implementation patterns for AFD commands using the afd crate, CommandResult types, and async handlers. Covers command definition, schema design with JSON Schema, error handling, registry patterns, and testing. Use when: implementing commands in Rust, building Rust MCP servers, working with CommandResult types, or debugging Rust AFD code. Triggers: rust afd, rs command, CommandResult rust, CommandHandler, rust implementation, afd crate, cargo afd.
|
AFD Rust Implementation
Patterns for implementing AFD commands in Rust.
Parity Rule
Rust implementations SHOULD match the shared AFD capability set and agent-visible behavior, while keeping Rust-idiomatic data types, traits, and module boundaries where that improves fit.
- Core command surfaces MUST stay framework-agnostic.
- React or browser integrations SHOULD stay in examples or ecosystem layers.
- Cross-language parity does NOT require a 1:1 port of TypeScript or Python helper names.
- Shared parity features to evaluate include output schemas, validated examples, prerequisite metadata, context scoping, grouped/lazy discovery strategies, and the common meta-tool patterns exposed to agents.
Crate Imports
use afd::{
success, failure, success_with,
CommandResult, ResultOptions,
is_success, is_failure,
};
use afd::{
CommandError,
validation_error, not_found_error, internal_error,
error_codes,
};
use afd::{
CommandDefinition, CommandParameter, CommandContext,
CommandExample, CommandHandler, CommandRegistry,
ExposeOptions, default_expose, validate_command_name,
JsonSchema, JsonSchemaType,
};
use afd::{
Warning, Source, PlanStep, Alternative,
create_warning, create_source, create_step,
};
use afd::{
BatchRequest, BatchResult, BatchCommand,
StreamChunk, StreamableCommand,
create_progress_chunk, create_data_chunk,
create_complete_chunk, create_error_chunk,
};
use afd::{
create_handoff, create_mcp_request, create_mcp_response,
create_telemetry_event, execute_pipeline,
calculate_similarity, find_similar_tools,
};
Command Result Types
Success Response
use afd::{success, success_with, ResultOptions};
fn get_user() -> CommandResult<User> {
let user = User { id: "123".into(), name: "Alice".into() };
success(user)
}
fn create_todo(title: &str) -> CommandResult<Todo> {
let todo = Todo::new(title);
success_with(todo, ResultOptions {
reasoning: Some(format!("Created todo '{}'", title)),
confidence: Some(1.0),
..Default::default()
})
}
fn delete_todo(id: &str) -> CommandResult<DeleteResult> {
let result = DeleteResult { id: id.to_string(), deleted: true };
success_with(result, ResultOptions {
reasoning: Some("Todo deleted permanently".to_string()),
warnings: Some(vec![
Warning::new("PERMANENT", "This action cannot be undone"),
]),
..Default::default()
})
}
Failure Response
use afd::{failure, CommandError};
fn get_todo(id: &str) -> CommandResult<Todo> {
let todo = store.get(id);
match todo {
Some(t) => success(t),
None => failure(CommandError::not_found("Todo", id)),
}
}
fn create_user(email: &str) -> CommandResult<User> {
if store.email_exists(email) {
return failure(CommandError::new(
"CONFLICT",
format!("Email '{}' already registered", email),
).with_suggestion("Use user-login instead, or reset password"));
}
}
fn update_priority(priority: &str) -> CommandResult<Todo> {
let valid = ["low", "medium", "high"];
if !valid.contains(&priority) {
return failure(CommandError::validation(
&format!("Invalid priority: {}. Must be one of {:?}", priority, valid),
Some("Use 'low', 'medium', or 'high'"),
));
}
}
CommandError Constructors
use afd::CommandError;
let err = CommandError::not_found("Todo", "123");
let err = CommandError::validation(
"Title cannot be empty",
Some("Provide a title between 1 and 200 characters"),
);
let err = CommandError::internal("Database connection failed");
let err = CommandError::new(
"RATE_LIMITED",
"Too many requests",
).with_suggestion("Wait 60 seconds before retrying");
let err = CommandError {
code: "TIMEOUT".to_string(),
message: "Request timed out".to_string(),
suggestion: Some("Try again in a few seconds".to_string()),
retryable: Some(true),
details: None,
cause: None,
};
Command Definition
Using CommandHandler Trait
use async_trait::async_trait;
use std::sync::Arc;
use afd::{CommandContext, CommandError, CommandHandler, CommandResult, failure, success};
use serde_json::Value;
struct CreateTodoHandler {
store: Arc<TodoStore>,
}
#[async_trait]
impl CommandHandler for CreateTodoHandler {
async fn execute(
&self,
input: Value,
_context: CommandContext,
) -> CommandResult<Value> {
let title = match input.get("title").and_then(|value| value.as_str()) {
Some(title) => title,
None => return failure(CommandError::validation("title is required", None)),
};
let todo = self.store.create(title).await;
success(serde_json::to_value(todo).unwrap())
}
}
Building CommandDefinition
use afd::{CommandDefinition, CommandParameter, JsonSchemaType};
let create_cmd = CommandDefinition::new(
"todo-create",
"Create a new todo item",
vec![
CommandParameter::required_string("title", "The todo title"),
CommandParameter::optional_string("description", "Optional description")
.with_default(serde_json::json!(null)),
CommandParameter::required_string("priority", "Priority level")
.with_enum(vec![
serde_json::json!("low"),
serde_json::json!("medium"),
serde_json::json!("high"),
])
.with_default(serde_json::json!("medium")),
],
CreateTodoHandler { store: store.clone() },
)
.with_category("todo")
.as_mutation();
CommandParameter Builders
use afd::{CommandParameter, JsonSchemaType};
let param = CommandParameter::required_string("title", "The todo title");
let param = CommandParameter::optional_string("description", "Optional description");
let param = CommandParameter::required_number("count", "Number of items");
let param = CommandParameter::required_boolean("completed", "Completion status");
let param = CommandParameter::required_string("priority", "Priority level")
.with_default(serde_json::json!("medium"));
let param = CommandParameter::required_string("priority", "Priority level")
.with_enum(vec![
serde_json::json!("low"),
serde_json::json!("medium"),
serde_json::json!("high"),
]);
Command Registry
use afd::{CommandDefinition, CommandRegistry, validate_command_name};
let mut registry = CommandRegistry::new();
registry.register(create_todo_cmd).expect("valid command definition");
registry.register(list_todos_cmd).expect("valid command definition");
registry.register(get_todo_cmd).expect("valid command definition");
validate_command_name("todo-create").expect("valid command name");
if registry.has("todo-create") {
println!("Command registered");
}
if let Some(cmd) = registry.get("todo-create") {
println!("Description: {}", cmd.description);
}
let result = registry.execute(
"todo-create",
serde_json::json!({"title": "Test"}),
None,
).await;
Batch Execution
use afd::{BatchRequest, BatchCommand, BatchOptions};
let request = BatchRequest {
commands: vec![
BatchCommand::new("1", "todo-create", serde_json::json!({"title": "First"})),
BatchCommand::new("2", "todo-create", serde_json::json!({"title": "Second"})),
],
options: BatchOptions {
continue_on_error: true,
max_concurrency: Some(4),
..Default::default()
},
context: None,
};
let result = registry.execute_batch(request).await;
println!("Succeeded: {}", result.summary.succeeded);
println!("Failed: {}", result.summary.failed);
Streaming Results
use afd::{
StreamChunk, create_progress_chunk, create_data_chunk,
create_complete_chunk, create_error_chunk,
};
use afd::CommandError;
let progress = create_progress_chunk(50.0, "Processing items...");
let data = create_data_chunk(partial_result, false);
let final_data = create_data_chunk(complete_result, true);
let complete = create_complete_chunk(final_result, Some(1500));
let error = create_error_chunk(CommandError::internal("Stream interrupted"), false);
Metadata Types
Current Parity Note
The Rust crate now includes parity helpers for:
- command ergonomics:
CommandExample, ExposeOptions, default_expose, validate_command_name
- streaming:
StreamableCommand, consume_stream, create_timeout_controller
- handoff and telemetry:
create_handoff, default_reconnect_policy, TelemetryEvent
- MCP and pipelines:
create_mcp_request, create_mcp_response, execute_pipeline
- discovery helpers:
calculate_similarity, find_similar_tools
Status clarity for cross-language planning:
- Shared today: output schemas, examples, exposure metadata, pipelines, discovery helpers, telemetry, handoff
- Not yet part of the Rust crate surface: context-scoped command registration and the full server-side tool-strategy/bootstrap workflow exposed in TypeScript and Python
- Parity decisions SHOULD compare agent-visible behavior first, then document Rust-specific gaps explicitly instead of assuming every TS/Python feature already exists in the crate
Warnings
use afd::{Warning, WarningSeverity, create_warning};
let warning = create_warning("DEPRECATION", "This field is deprecated", None);
let warning = Warning {
code: "PERMANENT".to_string(),
message: "This action cannot be undone".to_string(),
severity: Some(WarningSeverity::High),
context: None,
};
Sources
use afd::{Source, SourceType, create_source};
let source = create_source("API Response", SourceType::Api, None);
let source = Source {
name: "User Database".to_string(),
source_type: SourceType::Database,
url: Some("postgres://...".to_string()),
accessed_at: None,
relevance: Some(0.99),
snippet: None,
};
Plan Steps
use afd::{PlanStep, PlanStepStatus, create_step, update_step_status};
let step = create_step(1, "Validate input", PlanStepStatus::Pending);
let mut step = PlanStep::new(1, "Process data");
update_step_status(&mut step, PlanStepStatus::Running, None, None);
update_step_status(&mut step, PlanStepStatus::Completed, Some(120), None);
JSON Serialization
All AFD types use camelCase for JSON serialization:
let result = success(serde_json::json!({"name": "test"}));
let json = serde_json::to_string(&result).unwrap();
Type Guards
use afd::{is_success, is_failure, CommandResult};
fn process_result<T>(result: &CommandResult<T>) {
if is_success(result) {
println!("Success: {:?}", result.data);
} else if is_failure(result) {
println!("Error: {:?}", result.error);
}
}
Error Handling Patterns
Using Result Internally
Use Result<T, CommandError> inside helpers, then convert to CommandResult<T> at the command boundary.
use afd::{failure, success_with, CommandError, CommandResult, ResultOptions};
use serde_json::Value;
fn parse_title(input: &Value) -> Result<&str, CommandError> {
input
.get("title")
.and_then(|value| value.as_str())
.ok_or_else(|| CommandError::validation("title is required", None))
}
async fn create_todo(input: Value, store: &TodoStore) -> CommandResult<Todo> {
let title = match parse_title(&input) {
Ok(title) => title,
Err(error) => return failure(error),
};
if title.is_empty() {
return failure(CommandError::validation(
"title cannot be empty",
Some("Provide a non-empty title"),
));
}
let todo = match store.create(title).await {
Ok(todo) => todo,
Err(error) => return failure(CommandError::internal(&error.to_string())),
};
success_with(todo, ResultOptions {
reasoning: Some(format!("Created todo '{}'", title)),
..Default::default()
})
}
Wrapping External Errors
use afd::{failure, success, CommandError, CommandResult};
impl From<sqlx::Error> for CommandError {
fn from(err: sqlx::Error) -> Self {
CommandError::internal(&format!("Database error: {}", err))
}
}
async fn load_user(id: &str) -> Result<User, CommandError> {
db.get_user(id).await.map_err(CommandError::from)
}
async fn get_user(id: &str) -> CommandResult<User> {
match load_user(id).await {
Ok(user) => success(user),
Err(error) => failure(error),
}
}
Testing
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_success_result() {
let result = success("hello".to_string());
assert!(result.success);
assert_eq!(result.data, Some("hello".to_string()));
assert!(result.error.is_none());
}
#[test]
fn test_failure_result() {
let error = CommandError::not_found("Todo", "123");
let result: CommandResult<()> = failure(error);
assert!(!result.success);
assert!(result.error.is_some());
assert_eq!(result.error.unwrap().code, "NOT_FOUND");
}
#[test]
fn test_type_guards() {
let success_result = success("data".to_string());
assert!(is_success(&success_result));
assert!(!is_failure(&success_result));
let failure_result: CommandResult<String> =
failure(CommandError::validation("bad", None));
assert!(is_failure(&failure_result));
assert!(!is_success(&failure_result));
}
}
Async Tests
#[tokio::test]
async fn test_command_execution() {
let mut registry = CommandRegistry::new();
registry.register(create_test_command()).unwrap();
let result = registry.execute(
"test-echo",
serde_json::json!({"message": "hello"}),
None,
).await;
assert!(result.success);
}
#[tokio::test]
async fn test_command_not_found() {
let registry = CommandRegistry::new();
let result = registry.execute("nonexistent", serde_json::json!({}), None).await;
assert!(!result.success);
assert_eq!(result.error.unwrap().code, "COMMAND_NOT_FOUND");
}
Cargo.toml Configuration
[package]
name = "my-afd-app"
version = "0.1.0"
edition = "2021"
[dependencies]
afd = "0.1"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
async-trait = "0.1"
[dev-dependencies]
tokio-test = "0.4"
Feature Flags
if afd::is_native() {
println!("Running with tokio support");
}
if afd::is_wasm() {
println!("Running in WebAssembly");
}
Related Skills
afd-developer - Core AFD methodology
afd-typescript - TypeScript implementation patterns
afd-python - Python implementation patterns