├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .vscode └── mcp.json ├── AP2_IMPLEMENTATION_PLAN.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── a2a-agents ├── .gitignore ├── Cargo.toml ├── README.md ├── TODO.md ├── bin │ └── reimbursement_server.rs ├── config.apikey.example.json ├── config.auth.example.json ├── config.example.json ├── config.sqlx.example.json ├── examples │ ├── test_config_demo.rs │ ├── test_handler.rs │ ├── test_metadata.rs │ ├── test_metrics.rs │ └── test_sqlx_storage.rs ├── migrations │ ├── 001_create_reimbursements.sql │ ├── 001_create_reimbursements_rollback.sql │ └── README.md └── src │ ├── lib.rs │ └── reimbursement_agent │ ├── config.rs │ ├── handler.rs │ ├── mod.rs │ ├── server.rs │ └── types.rs ├── a2a-client ├── Cargo.toml ├── README.md ├── TODO.md ├── index.html ├── src │ ├── bin │ │ └── server.rs │ └── styles.css └── templates │ ├── chat.html │ └── index.html ├── a2a-mcp ├── Cargo.toml ├── README.md ├── examples │ └── minimal_example.rs └── src │ ├── adapter │ ├── agent_to_tool.rs │ ├── mod.rs │ └── tool_to_agent.rs │ ├── client.rs │ ├── error.rs │ ├── lib.rs │ ├── message.rs │ ├── server.rs │ ├── tests │ └── mod.rs │ ├── transport │ ├── a2a_to_rmcp.rs │ ├── mod.rs │ └── rmcp_to_a2a.rs │ └── util.rs ├── a2a-rs ├── .gitignore ├── Cargo.toml ├── README.md ├── benches │ └── a2a_performance.rs ├── docs │ └── SQLx_Storage.md ├── examples │ ├── builder_patterns.rs │ ├── common │ │ ├── mod.rs │ │ └── simple_agent_handler.rs │ ├── http_client_server.rs │ ├── sqlx_storage_demo.rs │ ├── storage_comparison.rs │ ├── v03_security_example.rs │ └── websocket_client_server.rs ├── migrations │ ├── 001_initial_schema.sql │ └── 001_initial_schema_postgres.sql ├── src │ ├── adapter │ │ ├── auth │ │ │ ├── authenticator.rs │ │ │ ├── jwt.rs │ │ │ ├── mod.rs │ │ │ └── oauth2.rs │ │ ├── business │ │ │ ├── agent_info.rs │ │ │ ├── message_handler.rs │ │ │ ├── mod.rs │ │ │ ├── push_notification.rs │ │ │ └── request_processor.rs │ │ ├── error │ │ │ ├── client.rs │ │ │ ├── mod.rs │ │ │ └── server.rs │ │ ├── mod.rs │ │ ├── storage │ │ │ ├── database_config.rs │ │ │ ├── mod.rs │ │ │ ├── sqlx_storage.rs │ │ │ └── task_storage.rs │ │ └── transport │ │ │ ├── http │ │ │ ├── client.rs │ │ │ ├── mod.rs │ │ │ └── server.rs │ │ │ ├── mod.rs │ │ │ └── websocket │ │ │ ├── client.rs │ │ │ ├── mod.rs │ │ │ └── server.rs │ ├── application │ │ ├── handlers │ │ │ ├── agent.rs │ │ │ ├── message.rs │ │ │ ├── mod.rs │ │ │ ├── notification.rs │ │ │ └── task.rs │ │ ├── json_rpc.rs │ │ └── mod.rs │ ├── domain │ │ ├── core │ │ │ ├── agent.rs │ │ │ ├── message.rs │ │ │ ├── mod.rs │ │ │ └── task.rs │ │ ├── error.rs │ │ ├── events │ │ │ ├── mod.rs │ │ │ └── task_events.rs │ │ ├── mod.rs │ │ ├── protocols │ │ │ ├── json_rpc.rs │ │ │ └── mod.rs │ │ ├── tests.rs │ │ └── validation │ │ │ └── mod.rs │ ├── lib.rs │ ├── observability │ │ └── mod.rs │ ├── port │ │ ├── authenticator.rs │ │ ├── message_handler.rs │ │ ├── mod.rs │ │ ├── notification_manager.rs │ │ ├── streaming_handler.rs │ │ └── task_manager.rs │ └── services │ │ ├── client.rs │ │ ├── mod.rs │ │ └── server.rs └── tests │ ├── common │ ├── mod.rs │ └── test_handler.rs │ ├── integration_test.rs │ ├── multi_transport_integration_test.rs │ ├── property_based_test.proptest-regressions │ ├── property_based_test.rs │ ├── push_notification_test.rs │ ├── spec_compliance_test.rs │ ├── sqlx_storage_test.rs │ ├── streaming_events_test.rs │ └── websocket_test.rs └── spec ├── CHANGELOG.md ├── README.md ├── agent.json ├── ap2.json ├── errors.json ├── events.json ├── jsonrpc.json ├── message.json ├── notifications.json ├── requests.json ├── security.json ├── specification.json ├── task.json └── types.ts /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | name: Build and Test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Install Rust toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | 26 | - name: Cache dependencies 27 | uses: actions/cache@v3 28 | with: 29 | path: | 30 | ~/.cargo/registry 31 | ~/.cargo/git 32 | target 33 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 34 | restore-keys: ${{ runner.os }}-cargo- 35 | 36 | - name: Build 37 | uses: actions-rs/cargo@v1 38 | with: 39 | command: build 40 | args: --verbose 41 | 42 | - name: Run tests 43 | uses: actions-rs/cargo@v1 44 | with: 45 | command: test 46 | args: --verbose 47 | 48 | clippy: 49 | name: Clippy 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | 54 | - name: Install Rust toolchain 55 | uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | components: clippy 61 | 62 | - name: Cache dependencies 63 | uses: actions/cache@v3 64 | with: 65 | path: | 66 | ~/.cargo/registry 67 | ~/.cargo/git 68 | target 69 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 70 | restore-keys: ${{ runner.os }}-cargo- 71 | 72 | - name: Run clippy 73 | uses: actions-rs/clippy-check@v1 74 | with: 75 | token: ${{ secrets.GITHUB_TOKEN }} 76 | args: -- -D warnings 77 | 78 | rustfmt: 79 | name: Format 80 | runs-on: ubuntu-latest 81 | steps: 82 | - uses: actions/checkout@v3 83 | 84 | - name: Install Rust toolchain 85 | uses: actions-rs/toolchain@v1 86 | with: 87 | profile: minimal 88 | toolchain: stable 89 | override: true 90 | components: rustfmt 91 | 92 | - name: Check formatting 93 | uses: actions-rs/cargo@v1 94 | with: 95 | command: fmt 96 | args: --all -- --check 97 | 98 | docs: 99 | name: Doc Check 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: actions/checkout@v3 103 | 104 | - name: Install Rust toolchain 105 | uses: actions-rs/toolchain@v1 106 | with: 107 | profile: minimal 108 | toolchain: stable 109 | override: true 110 | 111 | - name: Cache dependencies 112 | uses: actions/cache@v3 113 | with: 114 | path: | 115 | ~/.cargo/registry 116 | ~/.cargo/git 117 | target 118 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 119 | restore-keys: ${{ runner.os }}-cargo- 120 | 121 | - name: Check documentation 122 | uses: actions-rs/cargo@v1 123 | with: 124 | command: doc 125 | args: --no-deps --all-features -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | */target 3 | **/.claude/settings.local.json 4 | CLAUDE.md 5 | a2a-client/leptos/ 6 | 7 | -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": { 3 | "datapilot": { 4 | "url": "http://localhost:7704/sse", 5 | "type": "sse" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["a2a-rs", "a2a-agents", "a2a-client"] 4 | -------------------------------------------------------------------------------- /a2a-agents/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /a2a-agents/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "a2a-agents" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Emil Lindfors "] 6 | description = "Example agent implementations for the A2A Protocol" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | a2a-rs = { path = "../a2a-rs", features = ["full"] } 11 | 12 | # Core dependencies 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | chrono = { version = "0.4", features = ["serde"] } 16 | thiserror = "1.0" 17 | uuid = { version = "1.4", features = ["v4", "serde"] } 18 | bon = "2.3" 19 | 20 | # Async foundation 21 | tokio = { version = "1.32", features = ["rt", "rt-multi-thread", "macros", "net", "io-util", "sync", "time"] } 22 | async-trait = "0.1" 23 | 24 | # Command line interface 25 | clap = { version = "4.4", features = ["derive"] } 26 | 27 | # Logging 28 | tracing = "0.1" 29 | tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 30 | 31 | # Required dependencies 32 | lazy_static = "1.4" # Used for static request ID storage in message_handler 33 | regex = "1.10" # Used for text parsing in improved handler 34 | 35 | # Optional AI integration (for future use) 36 | # reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false, optional = true } 37 | 38 | [features] 39 | default = ["reimbursement-agent"] 40 | reimbursement-agent = [] 41 | sqlx = ["a2a-rs/sqlx-storage"] 42 | auth = ["a2a-rs/auth"] 43 | # Future agent types can be added as features 44 | # document-agent = ["dep:reqwest"] 45 | # research-agent = ["dep:reqwest"] 46 | 47 | 48 | [[bin]] 49 | name = "reimbursement_server" 50 | path = "bin/reimbursement_server.rs" 51 | required-features = ["reimbursement-agent"] 52 | 53 | [[example]] 54 | name = "test_handler" 55 | path = "examples/test_handler.rs" 56 | required-features = ["reimbursement-agent"] 57 | 58 | 59 | -------------------------------------------------------------------------------- /a2a-agents/README.md: -------------------------------------------------------------------------------- 1 | # A2A Agents - Modern Framework Examples 2 | 3 | This crate provides example agent implementations using the modern A2A protocol framework in Rust. Currently featuring a reimbursement agent that demonstrates best practices for building production-ready agents. 4 | 5 | ## Overview 6 | 7 | This implementation showcases the modern A2A framework architecture: 8 | 9 | 1. **Hexagonal Architecture**: Clean separation between domain logic and adapters 10 | 2. **Framework Integration**: Uses `DefaultBusinessHandler` and `InMemoryTaskStorage` 11 | 3. **Protocol Compliance**: Full A2A protocol support with HTTP and WebSocket transports 12 | 4. **Modern Patterns**: Async/await, builder patterns, and structured error handling 13 | 14 | ## Architecture 15 | 16 | ### ReimbursementMessageHandler 17 | 18 | The core business logic implementing `AsyncMessageHandler`: 19 | 20 | - Processes reimbursement requests using the A2A message format 21 | - Generates interactive forms for expense submissions 22 | - Validates and approves reimbursement requests 23 | - Returns structured responses with proper task states 24 | 25 | ### ModernReimbursementServer 26 | 27 | The server implementation using framework components: 28 | 29 | - Integrates with `DefaultBusinessHandler` for request processing 30 | - Uses `InMemoryTaskStorage` for task persistence 31 | - Configures `SimpleAgentInfo` with agent capabilities 32 | - Supports both HTTP and WebSocket transports 33 | 34 | ## Usage 35 | 36 | Run the modern reimbursement agent server: 37 | 38 | ```bash 39 | # Run both HTTP and WebSocket servers (default) 40 | cargo run --bin reimbursement_server 41 | 42 | # Run HTTP server only on custom port 43 | cargo run --bin reimbursement_server -- --mode http --port 8080 44 | 45 | # Run WebSocket server only 46 | cargo run --bin reimbursement_server -- --mode websocket 47 | 48 | # Custom host and port 49 | cargo run --bin reimbursement_server -- --host 0.0.0.0 --port 9000 --mode both 50 | ``` 51 | 52 | ### Available Endpoints 53 | 54 | **HTTP Server (default port 10002):** 55 | - Agent Card: `http://localhost:10002/agent-card` 56 | - Skills List: `http://localhost:10002/skills` 57 | - A2A Protocol: `http://localhost:10002/` (JSON-RPC) 58 | 59 | **WebSocket Server (default port 10003):** 60 | - WebSocket Endpoint: `ws://localhost:10003/` 61 | 62 | ## Example Conversation 63 | 64 | Here's an example conversation with the reimbursement agent: 65 | 66 | 1. User: "Can you reimburse me $50 for the team lunch yesterday?" 67 | 68 | 2. Agent: *Returns a form* 69 | ```json 70 | { 71 | "type": "form", 72 | "form": { 73 | "type": "object", 74 | "properties": { 75 | "date": { 76 | "type": "string", 77 | "format": "date", 78 | "description": "Date of expense", 79 | "title": "Date" 80 | }, 81 | "amount": { 82 | "type": "string", 83 | "format": "number", 84 | "description": "Amount of expense", 85 | "title": "Amount" 86 | }, 87 | "purpose": { 88 | "type": "string", 89 | "description": "Purpose of expense", 90 | "title": "Purpose" 91 | }, 92 | "request_id": { 93 | "type": "string", 94 | "description": "Request id", 95 | "title": "Request ID" 96 | } 97 | }, 98 | "required": ["request_id", "date", "amount", "purpose"] 99 | }, 100 | "form_data": { 101 | "request_id": "request_id_1234567", 102 | "date": "", 103 | "amount": "50", 104 | "purpose": " the team lunch yesterday" 105 | } 106 | } 107 | ``` 108 | 109 | 3. User: *Submits the filled form* 110 | ```json 111 | { 112 | "request_id": "request_id_1234567", 113 | "date": "2023-10-15", 114 | "amount": "50", 115 | "purpose": "team lunch with product team" 116 | } 117 | ``` 118 | 119 | 4. Agent: "Your reimbursement request has been approved. Request ID: request_id_1234567" 120 | 121 | ## Current Limitations 122 | 123 | This example implementation demonstrates the framework architecture but has simplified business logic: 124 | 125 | - **Message Processing**: Basic pattern matching instead of LLM integration 126 | - **Storage**: In-memory storage (framework supports SQLx for production) 127 | - **Authentication**: Not implemented (framework supports Bearer/OAuth2) 128 | - **Form Processing**: Simple JSON forms without complex validation 129 | 130 | ## Future Enhancements 131 | 132 | See [TODO.md](./TODO.md) for the comprehensive modernization roadmap including: 133 | 134 | 1. **Phase 2**: Production features (SQLx storage, authentication) 135 | 2. **Phase 3**: AI/LLM integration for natural language processing 136 | 3. **Phase 4**: Additional agent examples (document analysis, research assistant) 137 | 4. **Phase 5**: Comprehensive testing and documentation 138 | 5. **Phase 6**: Docker support and production deployment 139 | 140 | ## Framework Features Demonstrated 141 | 142 | - ✅ **AsyncMessageHandler** trait implementation 143 | - ✅ **DefaultBusinessHandler** integration 144 | - ✅ **InMemoryTaskStorage** for task persistence 145 | - ✅ **SimpleAgentInfo** for agent metadata 146 | - ✅ **HTTP and WebSocket** transport support 147 | - ✅ **Structured error handling** with A2AError 148 | - ✅ **Modern async/await** patterns 149 | - ✅ **Builder patterns** for complex objects -------------------------------------------------------------------------------- /a2a-agents/bin/reimbursement_server.rs: -------------------------------------------------------------------------------- 1 | use a2a_agents::reimbursement_agent::{AuthConfig, ReimbursementServer, ServerConfig}; 2 | use clap::Parser; 3 | 4 | /// Command-line arguments for the reimbursement server 5 | #[derive(Parser, Debug)] 6 | #[clap(author, version, about, long_about = None)] 7 | struct Args { 8 | /// Host to bind the server to 9 | #[clap(long, default_value = "127.0.0.1")] 10 | host: String, 11 | 12 | /// Port to listen on (HTTP server) 13 | #[clap(long, default_value = "8080")] 14 | http_port: u16, 15 | 16 | /// WebSocket port 17 | #[clap(long, default_value = "8081")] 18 | ws_port: u16, 19 | 20 | /// Configuration file path (JSON format) 21 | #[clap(long)] 22 | config: Option, 23 | 24 | /// Server mode: http, websocket, or both 25 | #[clap(long, default_value = "both")] 26 | mode: String, 27 | } 28 | 29 | #[tokio::main] 30 | async fn main() -> Result<(), Box> { 31 | // Initialize logging with better formatting 32 | tracing_subscriber::fmt() 33 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 34 | .init(); 35 | 36 | // Parse command-line arguments 37 | let args = Args::parse(); 38 | 39 | // Load configuration 40 | let mut config = if let Some(config_path) = args.config { 41 | println!("📄 Loading config from: {}", config_path); 42 | std::env::set_var("CONFIG_FILE", config_path); 43 | ServerConfig::load()? 44 | } else { 45 | ServerConfig::from_env() 46 | }; 47 | 48 | // Override config with command-line arguments 49 | config.host = args.host; 50 | config.http_port = args.http_port; 51 | config.ws_port = args.ws_port; 52 | 53 | println!("🚀 Starting Modern Reimbursement Agent Server"); 54 | println!("==============================================="); 55 | println!("📍 Host: {}", config.host); 56 | println!("🔌 HTTP Port: {}", config.http_port); 57 | println!("📡 WebSocket Port: {}", config.ws_port); 58 | println!("⚙️ Mode: {}", args.mode); 59 | match &config.storage { 60 | a2a_agents::reimbursement_agent::StorageConfig::InMemory => { 61 | println!("💾 Storage: In-memory (non-persistent)"); 62 | } 63 | a2a_agents::reimbursement_agent::StorageConfig::Sqlx { url, .. } => { 64 | println!("💾 Storage: SQLx ({})", url); 65 | } 66 | } 67 | match &config.auth { 68 | AuthConfig::None => { 69 | println!("🔓 Authentication: None (public access)"); 70 | } 71 | AuthConfig::BearerToken { tokens, format } => { 72 | println!( 73 | "🔐 Authentication: Bearer token ({} token(s){})", 74 | tokens.len(), 75 | format 76 | .as_ref() 77 | .map(|f| format!(", format: {}", f)) 78 | .unwrap_or_default() 79 | ); 80 | } 81 | AuthConfig::ApiKey { 82 | keys, 83 | location, 84 | name, 85 | } => { 86 | println!( 87 | "🔐 Authentication: API key ({} {} '{}', {} key(s))", 88 | location, 89 | name, 90 | name, 91 | keys.len() 92 | ); 93 | } 94 | } 95 | println!(); 96 | 97 | // Create the modern server 98 | let server = ReimbursementServer::from_config(config); 99 | 100 | // Start the server based on mode 101 | match args.mode.as_str() { 102 | "http" => { 103 | println!("🌐 Starting HTTP server only..."); 104 | server.start_http().await?; 105 | } 106 | "websocket" | "ws" => { 107 | println!("🔌 Starting WebSocket server only..."); 108 | server.start_websocket().await?; 109 | } 110 | "both" | "all" => { 111 | println!("🔄 Starting both HTTP and WebSocket servers..."); 112 | server.start_all().await?; 113 | } 114 | _ => { 115 | eprintln!( 116 | "❌ Invalid mode: {}. Use 'http', 'websocket', or 'both'", 117 | args.mode 118 | ); 119 | std::process::exit(1); 120 | } 121 | } 122 | 123 | Ok(()) 124 | } 125 | -------------------------------------------------------------------------------- /a2a-agents/config.apikey.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "127.0.0.1", 3 | "http_port": 8080, 4 | "ws_port": 8081, 5 | "storage": { 6 | "type": "Sqlx", 7 | "url": "sqlite:authenticated_tasks.db", 8 | "max_connections": 10, 9 | "enable_logging": false 10 | }, 11 | "auth": { 12 | "type": "ApiKey", 13 | "keys": ["api-key-123", "api-key-456"], 14 | "location": "header", 15 | "name": "X-API-Key" 16 | } 17 | } -------------------------------------------------------------------------------- /a2a-agents/config.auth.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "127.0.0.1", 3 | "http_port": 8080, 4 | "ws_port": 8081, 5 | "storage": { 6 | "type": "InMemory" 7 | }, 8 | "auth": { 9 | "type": "BearerToken", 10 | "tokens": ["secret-token-123", "another-token-456"], 11 | "format": "JWT" 12 | } 13 | } -------------------------------------------------------------------------------- /a2a-agents/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "127.0.0.1", 3 | "http_port": 8080, 4 | "ws_port": 8081, 5 | "storage": { 6 | "type": "InMemory" 7 | } 8 | } -------------------------------------------------------------------------------- /a2a-agents/config.sqlx.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "127.0.0.1", 3 | "http_port": 8080, 4 | "ws_port": 8081, 5 | "storage": { 6 | "type": "Sqlx", 7 | "url": "sqlite:reimbursement_tasks.db", 8 | "max_connections": 10, 9 | "enable_logging": false 10 | } 11 | } -------------------------------------------------------------------------------- /a2a-agents/examples/test_config_demo.rs: -------------------------------------------------------------------------------- 1 | use a2a_agents::reimbursement_agent::{ 2 | config::{AuthConfig, ServerConfig, StorageConfig}, 3 | server::ReimbursementServer, 4 | }; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<(), Box> { 8 | println!("=== A2A Agents Configuration Demo ===\n"); 9 | 10 | // Example 1: In-memory storage (default) 11 | println!("1. In-Memory Storage Configuration:"); 12 | let config1 = ServerConfig { 13 | host: "127.0.0.1".to_string(), 14 | http_port: 8080, 15 | ws_port: 8081, 16 | storage: StorageConfig::InMemory, 17 | auth: AuthConfig::None, 18 | }; 19 | println!(" Storage: {:?}", config1.storage); 20 | println!(" Auth: {:?}", config1.auth); 21 | println!(); 22 | 23 | // Example 2: SQLx storage configuration 24 | println!("2. SQLx Storage Configuration:"); 25 | let config2 = ServerConfig { 26 | host: "127.0.0.1".to_string(), 27 | http_port: 8080, 28 | ws_port: 8081, 29 | storage: StorageConfig::Sqlx { 30 | url: "sqlite://reimbursement.db".to_string(), 31 | max_connections: 10, 32 | enable_logging: true, 33 | }, 34 | auth: AuthConfig::None, 35 | }; 36 | println!(" Storage: {:?}", config2.storage); 37 | println!(); 38 | 39 | // Example 3: Bearer token authentication 40 | println!("3. Bearer Token Authentication:"); 41 | let config3 = ServerConfig { 42 | host: "127.0.0.1".to_string(), 43 | http_port: 8080, 44 | ws_port: 8081, 45 | storage: StorageConfig::InMemory, 46 | auth: AuthConfig::BearerToken { 47 | tokens: vec![ 48 | "secret_token_123".to_string(), 49 | "another_token_456".to_string(), 50 | ], 51 | format: Some("Bearer {}".to_string()), 52 | }, 53 | }; 54 | println!(" Auth: {:?}", config3.auth); 55 | println!(); 56 | 57 | // Example 4: Combined SQLx + Auth 58 | println!("4. Production Configuration (SQLx + Auth):"); 59 | let config4 = ServerConfig { 60 | host: "0.0.0.0".to_string(), 61 | http_port: 8080, 62 | ws_port: 8081, 63 | storage: StorageConfig::Sqlx { 64 | url: "postgres://user:password@localhost/reimbursement_prod".to_string(), 65 | max_connections: 50, 66 | enable_logging: false, 67 | }, 68 | auth: AuthConfig::BearerToken { 69 | tokens: vec!["prod_token_abc123".to_string()], 70 | format: Some("A2A-Token {}".to_string()), 71 | }, 72 | }; 73 | println!(" Storage: {:?}", config4.storage); 74 | println!(" Auth: {:?}", config4.auth); 75 | println!(); 76 | 77 | // Demonstrate server creation 78 | println!("5. Server Creation Examples:"); 79 | 80 | // Simple server 81 | let _simple_server = ReimbursementServer::new("127.0.0.1".to_string(), 8080); 82 | println!(" Simple server created (in-memory storage, no auth)"); 83 | 84 | // Config-based server 85 | let _config_server = ReimbursementServer::from_config(config1); 86 | println!(" Config-based server created"); 87 | 88 | // Another config-based server 89 | let _another_server = ReimbursementServer::from_config(config2); 90 | println!(" SQLx config server created"); 91 | 92 | println!("\n=== Configuration Files ==="); 93 | println!("Available configuration examples:"); 94 | println!(" - config.example.json (basic in-memory)"); 95 | println!(" - config.sqlx.example.json (SQLx storage)"); 96 | println!(" - config.auth.example.json (with authentication)"); 97 | println!(" - config.apikey.example.json (API key auth)"); 98 | 99 | println!("\n=== Usage Commands ==="); 100 | println!("Start server with different configs:"); 101 | println!(" cargo run --bin reimbursement_server -- --config config.example.json"); 102 | println!(" cargo run --bin reimbursement_server -- --config config.sqlx.example.json"); 103 | println!(" cargo run --bin reimbursement_server -- --config config.auth.example.json"); 104 | 105 | println!("\n=== Automatic Migrations ==="); 106 | println!("SQLx storage automatically handles migrations:"); 107 | println!(" ✅ Base A2A framework tables (tasks, task_history, etc.)"); 108 | println!(" ✅ Reimbursement-specific tables (reimbursement_requests, receipts, etc.)"); 109 | println!(" ✅ No manual migration commands needed!"); 110 | println!(" ✅ Database file created automatically if it doesn't exist"); 111 | 112 | Ok(()) 113 | } 114 | -------------------------------------------------------------------------------- /a2a-agents/examples/test_handler.rs: -------------------------------------------------------------------------------- 1 | use a2a_agents::reimbursement_agent::handler::ReimbursementHandler; 2 | use a2a_rs::domain::{Message, Part, Role}; 3 | use a2a_rs::port::message_handler::AsyncMessageHandler; 4 | use serde_json::json; 5 | use uuid::Uuid; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<(), Box> { 9 | // Initialize logging 10 | tracing_subscriber::fmt().with_env_filter("debug").init(); 11 | 12 | // Create handler 13 | let handler = ReimbursementHandler::new(); 14 | 15 | println!("=== Testing Reimbursement Handler ===\n"); 16 | 17 | // Test 1: Text-based request 18 | println!("1. Testing text-based request:"); 19 | let text_message = Message::builder() 20 | .role(Role::User) 21 | .parts(vec![Part::text( 22 | "I need to get reimbursed $150.50 for client lunch at downtown restaurant".to_string(), 23 | )]) 24 | .message_id(Uuid::new_v4().to_string()) 25 | .context_id("conv-123".to_string()) 26 | .build(); 27 | 28 | let task_id = format!("task_{}", Uuid::new_v4().simple()); 29 | let result = handler 30 | .process_message(&task_id, &text_message, None) 31 | .await?; 32 | println!("Response: {:?}\n", result.status.message); 33 | 34 | // Test 2: Structured data request 35 | println!("2. Testing structured data request:"); 36 | let data_message = Message::builder() 37 | .role(Role::User) 38 | .parts(vec![Part::data( 39 | json!({ 40 | "date": "2024-01-15", 41 | "amount": 250.00, 42 | "purpose": "Team building dinner for Q1 planning", 43 | "category": "meals" 44 | }) 45 | .as_object() 46 | .unwrap() 47 | .clone(), 48 | )]) 49 | .message_id(Uuid::new_v4().to_string()) 50 | .context_id("conv-456".to_string()) 51 | .build(); 52 | 53 | let task_id = format!("task_{}", Uuid::new_v4().simple()); 54 | let result = handler 55 | .process_message(&task_id, &data_message, None) 56 | .await?; 57 | println!("Response: {:?}\n", result.status.message); 58 | 59 | // Test 3: Form submission 60 | println!("3. Testing form submission:"); 61 | let form_message = Message::builder() 62 | .role(Role::User) 63 | .parts(vec![Part::data( 64 | json!({ 65 | "request_id": "req_12345", 66 | "date": "2024-01-20", 67 | "amount": {"amount": 500.00, "currency": "USD"}, 68 | "purpose": "Conference registration and travel expenses", 69 | "category": "travel", 70 | "notes": "Annual tech conference in SF" 71 | }) 72 | .as_object() 73 | .unwrap() 74 | .clone(), 75 | )]) 76 | .message_id(Uuid::new_v4().to_string()) 77 | .context_id("conv-789".to_string()) 78 | .build(); 79 | 80 | let task_id = format!("task_{}", Uuid::new_v4().simple()); 81 | let result = handler 82 | .process_message(&task_id, &form_message, None) 83 | .await?; 84 | println!("Response: {:?}\n", result.status.message); 85 | 86 | // Test 4: Mixed content (text + file reference) 87 | println!("4. Testing mixed content with file:"); 88 | let file_content = a2a_rs::domain::FileContent { 89 | name: Some("receipt_20240115.pdf".to_string()), 90 | mime_type: Some("application/pdf".to_string()), 91 | bytes: Some("SGVsbG8gV29ybGQh".to_string()), // Base64 encoded 92 | uri: None, 93 | }; 94 | 95 | let mixed_message = Message::builder() 96 | .role(Role::User) 97 | .parts(vec![ 98 | Part::text("Here's my receipt for the office supplies".to_string()), 99 | Part::File { 100 | file: file_content, 101 | metadata: json!({"extracted_amount": "$75.50"}) 102 | .as_object().cloned(), 103 | }, 104 | ]) 105 | .message_id(Uuid::new_v4().to_string()) 106 | .build(); 107 | 108 | let task_id = format!("task_{}", Uuid::new_v4().simple()); 109 | let result = handler 110 | .process_message(&task_id, &mixed_message, None) 111 | .await?; 112 | println!("Response: {:?}\n", result.status.message); 113 | 114 | // Test 5: Status query 115 | println!("5. Testing status query:"); 116 | let status_message = Message::builder() 117 | .role(Role::User) 118 | .parts(vec![Part::text( 119 | "What's the status of req_12345?".to_string(), 120 | )]) 121 | .message_id(Uuid::new_v4().to_string()) 122 | .build(); 123 | 124 | let task_id = format!("task_{}", Uuid::new_v4().simple()); 125 | let result = handler 126 | .process_message(&task_id, &status_message, None) 127 | .await?; 128 | println!("Response: {:?}\n", result.status.message); 129 | 130 | println!("All tests completed!"); 131 | Ok(()) 132 | } 133 | -------------------------------------------------------------------------------- /a2a-agents/examples/test_metadata.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{json, Map, Value}; 2 | use uuid::Uuid; 3 | 4 | use a2a_agents::reimbursement_agent::handler::ReimbursementHandler; 5 | use a2a_rs::domain::{Message, Part, Role}; 6 | use a2a_rs::port::message_handler::AsyncMessageHandler; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), Box> { 10 | // Initialize the handler 11 | let handler = ReimbursementHandler::new(); 12 | 13 | // Example 1: Text part with metadata hints 14 | println!("=== Example 1: Text with metadata ==="); 15 | let mut metadata1 = Map::new(); 16 | metadata1.insert( 17 | "expense_type".to_string(), 18 | Value::String("travel".to_string()), 19 | ); 20 | metadata1.insert("currency".to_string(), Value::String("EUR".to_string())); 21 | metadata1.insert("priority".to_string(), Value::String("high".to_string())); 22 | 23 | let message1 = Message::builder() 24 | .role(Role::User) 25 | .message_id(Uuid::new_v4().to_string()) 26 | .parts(vec![Part::Text { 27 | text: "I need reimbursement for 150 euros spent on hotel in Paris on 2024-01-15" 28 | .to_string(), 29 | metadata: Some(metadata1), 30 | }]) 31 | .build(); 32 | 33 | let task1 = handler.process_message("task1", &message1, None).await?; 34 | println!("Response: {:?}\n", task1.status.message); 35 | 36 | // Example 2: Data part with metadata 37 | println!("=== Example 2: Data with metadata ==="); 38 | let mut data2 = Map::new(); 39 | data2.insert("date".to_string(), Value::String("2024-01-20".to_string())); 40 | data2.insert( 41 | "amount".to_string(), 42 | Value::Number(serde_json::Number::from(75)), 43 | ); 44 | data2.insert( 45 | "purpose".to_string(), 46 | Value::String("Client dinner meeting".to_string()), 47 | ); 48 | 49 | let mut metadata2 = Map::new(); 50 | metadata2.insert( 51 | "category_hint".to_string(), 52 | Value::String("meals".to_string()), 53 | ); 54 | metadata2.insert("auto_approve".to_string(), Value::Bool(true)); 55 | 56 | let message2 = Message::builder() 57 | .role(Role::User) 58 | .message_id(Uuid::new_v4().to_string()) 59 | .parts(vec![Part::Data { 60 | data: data2, 61 | metadata: Some(metadata2), 62 | }]) 63 | .build(); 64 | 65 | let task2 = handler.process_message("task2", &message2, None).await?; 66 | println!("Response: {:?}\n", task2.status.message); 67 | 68 | // Example 3: File part with metadata 69 | println!("=== Example 3: File with metadata ==="); 70 | let file_content = a2a_rs::domain::FileContent { 71 | name: Some("receipt_hotel.pdf".to_string()), 72 | mime_type: Some("application/pdf".to_string()), 73 | bytes: Some("SGVsbG8gV29ybGQh".to_string()), // Base64 encoded 74 | uri: None, 75 | }; 76 | 77 | let mut file_metadata = Map::new(); 78 | file_metadata.insert( 79 | "file_name".to_string(), 80 | Value::String("receipt_hotel.pdf".to_string()), 81 | ); 82 | file_metadata.insert( 83 | "size_bytes".to_string(), 84 | Value::Number(serde_json::Number::from(12345)), 85 | ); 86 | file_metadata.insert( 87 | "uploaded_by".to_string(), 88 | Value::String("john.doe@company.com".to_string()), 89 | ); 90 | 91 | let mut data3 = Map::new(); 92 | data3.insert("date".to_string(), Value::String("2024-01-25".to_string())); 93 | data3.insert( 94 | "amount".to_string(), 95 | json!({"amount": 250.0, "currency": "USD"}), 96 | ); 97 | data3.insert( 98 | "purpose".to_string(), 99 | Value::String("Hotel stay for conference".to_string()), 100 | ); 101 | data3.insert("category".to_string(), Value::String("travel".to_string())); 102 | 103 | let message3 = Message::builder() 104 | .role(Role::User) 105 | .message_id(Uuid::new_v4().to_string()) 106 | .parts(vec![ 107 | Part::Data { 108 | data: data3, 109 | metadata: None, 110 | }, 111 | Part::File { 112 | file: file_content, 113 | metadata: Some(file_metadata), 114 | }, 115 | ]) 116 | .build(); 117 | 118 | let task3 = handler.process_message("task3", &message3, None).await?; 119 | println!("Response: {:?}\n", task3.status.message); 120 | 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /a2a-agents/examples/test_metrics.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{Map, Value}; 2 | use uuid::Uuid; 3 | 4 | use a2a_agents::reimbursement_agent::handler::ReimbursementHandler; 5 | use a2a_rs::domain::{Message, Part, Role}; 6 | use a2a_rs::port::message_handler::AsyncMessageHandler; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), Box> { 10 | // Initialize tracing with detailed output 11 | tracing_subscriber::fmt() 12 | .with_env_filter("info,a2a_agents=debug") 13 | .with_target(false) 14 | .with_thread_ids(true) 15 | .with_line_number(true) 16 | .init(); 17 | 18 | // Initialize the handler 19 | let handler = ReimbursementHandler::new(); 20 | 21 | println!("=== Testing Metrics and Logging ===\n"); 22 | 23 | // Test 1: Valid request that gets auto-approved 24 | println!("Test 1: Small amount (auto-approval)"); 25 | let message1 = Message::builder() 26 | .role(Role::User) 27 | .message_id(Uuid::new_v4().to_string()) 28 | .parts(vec![Part::Data { 29 | data: { 30 | let mut data = Map::new(); 31 | data.insert("date".to_string(), Value::String("2024-01-20".to_string())); 32 | data.insert( 33 | "amount".to_string(), 34 | Value::Number(serde_json::Number::from(50)), 35 | ); 36 | data.insert( 37 | "purpose".to_string(), 38 | Value::String("Team lunch".to_string()), 39 | ); 40 | data.insert("category".to_string(), Value::String("meals".to_string())); 41 | data 42 | }, 43 | metadata: None, 44 | }]) 45 | .build(); 46 | 47 | let _task1 = handler.process_message("task1", &message1, None).await?; 48 | println!(); 49 | 50 | // Test 2: Large amount (requires approval) 51 | println!("Test 2: Large amount (requires approval)"); 52 | let message2 = Message::builder() 53 | .role(Role::User) 54 | .message_id(Uuid::new_v4().to_string()) 55 | .parts(vec![Part::Data { 56 | data: { 57 | let mut data = Map::new(); 58 | data.insert("date".to_string(), Value::String("2024-01-25".to_string())); 59 | data.insert( 60 | "amount".to_string(), 61 | Value::Number(serde_json::Number::from(500)), 62 | ); 63 | data.insert( 64 | "purpose".to_string(), 65 | Value::String("Conference attendance".to_string()), 66 | ); 67 | data.insert("category".to_string(), Value::String("travel".to_string())); 68 | data 69 | }, 70 | metadata: None, 71 | }]) 72 | .build(); 73 | 74 | let _task2 = handler.process_message("task2", &message2, None).await?; 75 | println!(); 76 | 77 | // Test 3: Invalid request (validation error) 78 | println!("Test 3: Invalid request (validation error)"); 79 | let message3 = Message::builder() 80 | .role(Role::User) 81 | .message_id(Uuid::new_v4().to_string()) 82 | .parts(vec![Part::Data { 83 | data: { 84 | let mut data = Map::new(); 85 | data.insert("date".to_string(), Value::String("".to_string())); // Empty date 86 | data.insert( 87 | "amount".to_string(), 88 | Value::Number(serde_json::Number::from(100)), 89 | ); 90 | data.insert("purpose".to_string(), Value::String("".to_string())); // Empty purpose 91 | data.insert("category".to_string(), Value::String("meals".to_string())); 92 | data 93 | }, 94 | metadata: None, 95 | }]) 96 | .build(); 97 | 98 | let result3 = handler.process_message("task3", &message3, None).await; 99 | match result3 { 100 | Ok(_) => println!("Unexpected success"), 101 | Err(e) => println!("Expected validation error: {}", e), 102 | } 103 | println!(); 104 | 105 | // Test 4: Initial request (form generation) 106 | println!("Test 4: Initial request (form generation)"); 107 | let message4 = Message::builder() 108 | .role(Role::User) 109 | .message_id(Uuid::new_v4().to_string()) 110 | .parts(vec![Part::Text { 111 | text: "I need to submit a reimbursement request".to_string(), 112 | metadata: None, 113 | }]) 114 | .build(); 115 | 116 | let _task4 = handler.process_message("task4", &message4, None).await?; 117 | println!(); 118 | 119 | // Log final metrics 120 | println!("=== Final Metrics ==="); 121 | handler.log_metrics(); 122 | 123 | // Get metrics programmatically 124 | let metrics = handler.get_metrics(); 125 | println!("\nMetrics Summary:"); 126 | println!(" Total Requests: {}", metrics.total_requests); 127 | println!(" Successful: {}", metrics.successful_requests); 128 | println!(" Validation Errors: {}", metrics.validation_errors); 129 | println!(" Forms Generated: {}", metrics.forms_generated); 130 | println!(" Approvals: {}", metrics.approvals_processed); 131 | println!(" Auto-Approvals: {}", metrics.auto_approvals); 132 | 133 | Ok(()) 134 | } 135 | -------------------------------------------------------------------------------- /a2a-agents/examples/test_sqlx_storage.rs: -------------------------------------------------------------------------------- 1 | use a2a_agents::reimbursement_agent::{ 2 | config::{ServerConfig, StorageConfig}, 3 | server::ReimbursementServer, 4 | }; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<(), Box> { 8 | // Initialize tracing 9 | tracing_subscriber::fmt() 10 | .with_env_filter("info,a2a_rs=debug,a2a_agents=debug") 11 | .init(); 12 | 13 | // Create configuration with SQLx storage 14 | let config = ServerConfig { 15 | host: "127.0.0.1".to_string(), 16 | http_port: 8080, 17 | ws_port: 8081, 18 | storage: StorageConfig::Sqlx { 19 | url: "sqlite://reimbursement_test.db".to_string(), 20 | max_connections: 5, 21 | enable_logging: true, 22 | }, 23 | auth: Default::default(), 24 | }; 25 | 26 | println!("🚀 Starting Reimbursement Agent with SQLx Storage"); 27 | println!("📦 Database: sqlite://reimbursement_test.db"); 28 | println!("✨ Automatic Migrations: Both base A2A tables and reimbursement tables will be created automatically!"); 29 | println!("📝 No manual migration needed - SQLx handles everything!"); 30 | println!(); 31 | 32 | // Create and start server 33 | let server = ReimbursementServer::from_config(config); 34 | 35 | println!("📋 Starting HTTP server..."); 36 | println!(" This will automatically:"); 37 | println!(" 1. Create the database file if it doesn't exist"); 38 | println!(" 2. Run base A2A framework migrations (tasks, task_history, etc.)"); 39 | println!( 40 | " 3. Run reimbursement-specific migrations (reimbursement_requests, receipts, etc.)" 41 | ); 42 | println!(" 4. Start the HTTP server on port 8080"); 43 | println!(); 44 | 45 | server.start_http().await?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /a2a-agents/migrations/001_create_reimbursements.sql: -------------------------------------------------------------------------------- 1 | -- Migration 001: Create reimbursement tables 2 | -- This extends the base a2a-rs task tables with reimbursement-specific data 3 | 4 | -- Create reimbursement_requests table 5 | CREATE TABLE IF NOT EXISTS reimbursement_requests ( 6 | id TEXT PRIMARY KEY, 7 | task_id TEXT NOT NULL, 8 | request_type TEXT NOT NULL, -- 'initial', 'form_submission', 'status_query' 9 | 10 | -- Request data 11 | date TEXT, 12 | amount_value REAL, 13 | amount_currency TEXT, 14 | purpose TEXT, 15 | category TEXT, 16 | notes TEXT, 17 | 18 | -- Status tracking 19 | status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'under_review', 'approved', 'rejected', 'requires_additional_info' 20 | 21 | -- Approval data 22 | approved_amount_value REAL, 23 | approved_amount_currency TEXT, 24 | approval_date TEXT, 25 | approver TEXT, 26 | rejection_reason TEXT, 27 | 28 | -- Metadata 29 | metadata TEXT, -- JSON string 30 | created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 31 | updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 32 | 33 | FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE 34 | ); 35 | 36 | -- Create receipts table 37 | CREATE TABLE IF NOT EXISTS receipts ( 38 | id TEXT PRIMARY KEY, 39 | request_id TEXT NOT NULL, 40 | file_id TEXT NOT NULL, 41 | file_name TEXT NOT NULL, 42 | mime_type TEXT, 43 | size_bytes INTEGER, 44 | upload_timestamp TEXT, 45 | 46 | -- Extracted data (from OCR/processing) 47 | extracted_vendor TEXT, 48 | extracted_date TEXT, 49 | extracted_amount_value REAL, 50 | extracted_amount_currency TEXT, 51 | confidence_score REAL, 52 | extracted_data TEXT, -- JSON string for additional extracted data 53 | 54 | created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 55 | 56 | FOREIGN KEY (request_id) REFERENCES reimbursement_requests(id) ON DELETE CASCADE 57 | ); 58 | 59 | -- Create approval_workflow table for tracking approval steps 60 | CREATE TABLE IF NOT EXISTS approval_workflow ( 61 | id TEXT PRIMARY KEY, 62 | request_id TEXT NOT NULL, 63 | step_number INTEGER NOT NULL, 64 | step_type TEXT NOT NULL, -- 'auto_approval', 'manager_approval', 'finance_approval', etc. 65 | status TEXT NOT NULL, -- 'pending', 'approved', 'rejected', 'skipped' 66 | approver TEXT, 67 | comments TEXT, 68 | decision_date TEXT, 69 | 70 | created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 71 | updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 72 | 73 | FOREIGN KEY (request_id) REFERENCES reimbursement_requests(id) ON DELETE CASCADE 74 | ); 75 | 76 | -- Create indexes for common queries 77 | CREATE INDEX IF NOT EXISTS idx_reimbursement_requests_task_id ON reimbursement_requests(task_id); 78 | CREATE INDEX IF NOT EXISTS idx_reimbursement_requests_status ON reimbursement_requests(status); 79 | CREATE INDEX IF NOT EXISTS idx_reimbursement_requests_created_at ON reimbursement_requests(created_at); 80 | CREATE INDEX IF NOT EXISTS idx_receipts_request_id ON receipts(request_id); 81 | CREATE INDEX IF NOT EXISTS idx_approval_workflow_request_id ON approval_workflow(request_id); 82 | CREATE INDEX IF NOT EXISTS idx_approval_workflow_status ON approval_workflow(status); 83 | 84 | -- Create triggers for updated_at 85 | CREATE TRIGGER IF NOT EXISTS update_reimbursement_requests_updated_at 86 | AFTER UPDATE ON reimbursement_requests 87 | FOR EACH ROW 88 | BEGIN 89 | UPDATE reimbursement_requests SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; 90 | END; 91 | 92 | CREATE TRIGGER IF NOT EXISTS update_approval_workflow_updated_at 93 | AFTER UPDATE ON approval_workflow 94 | FOR EACH ROW 95 | BEGIN 96 | UPDATE approval_workflow SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; 97 | END; -------------------------------------------------------------------------------- /a2a-agents/migrations/001_create_reimbursements_rollback.sql: -------------------------------------------------------------------------------- 1 | -- Rollback migration 001: Drop reimbursement tables 2 | 3 | -- Drop triggers first 4 | DROP TRIGGER IF EXISTS update_reimbursement_requests_updated_at; 5 | DROP TRIGGER IF EXISTS update_approval_workflow_updated_at; 6 | 7 | -- Drop indexes 8 | DROP INDEX IF EXISTS idx_reimbursement_requests_task_id; 9 | DROP INDEX IF EXISTS idx_reimbursement_requests_status; 10 | DROP INDEX IF EXISTS idx_reimbursement_requests_created_at; 11 | DROP INDEX IF EXISTS idx_receipts_request_id; 12 | DROP INDEX IF EXISTS idx_approval_workflow_request_id; 13 | DROP INDEX IF EXISTS idx_approval_workflow_status; 14 | 15 | -- Drop tables in reverse order of dependencies 16 | DROP TABLE IF EXISTS approval_workflow; 17 | DROP TABLE IF EXISTS receipts; 18 | DROP TABLE IF EXISTS reimbursement_requests; -------------------------------------------------------------------------------- /a2a-agents/migrations/README.md: -------------------------------------------------------------------------------- 1 | # Reimbursement Agent Database Migrations 2 | 3 | This directory contains database migrations for the reimbursement agent's persistent storage. 4 | 5 | ## Running Migrations 6 | 7 | ### SQLite 8 | 9 | 1. Create the database file if it doesn't exist: 10 | ```bash 11 | touch reimbursement.db 12 | ``` 13 | 14 | 2. Run the migrations: 15 | ```bash 16 | sqlite3 reimbursement.db < migrations/001_create_reimbursements.sql 17 | ``` 18 | 19 | ### PostgreSQL 20 | 21 | 1. Create the database: 22 | ```bash 23 | createdb reimbursement_agent 24 | ``` 25 | 26 | 2. Run the migrations: 27 | ```bash 28 | psql -d reimbursement_agent -f migrations/001_create_reimbursements.sql 29 | ``` 30 | 31 | ## Rollback 32 | 33 | To rollback a migration: 34 | 35 | ### SQLite 36 | ```bash 37 | sqlite3 reimbursement.db < migrations/001_create_reimbursements_rollback.sql 38 | ``` 39 | 40 | ### PostgreSQL 41 | ```bash 42 | psql -d reimbursement_agent -f migrations/001_create_reimbursements_rollback.sql 43 | ``` 44 | 45 | ## Migration Files 46 | 47 | - `001_create_reimbursements.sql` - Creates the core reimbursement tables: 48 | - `reimbursement_requests` - Main requests table 49 | - `receipts` - Receipt file metadata and extracted data 50 | - `approval_workflow` - Approval workflow tracking 51 | 52 | ## Using with SQLx 53 | 54 | The a2a-rs framework's SqlxTaskStorage will automatically manage the base task tables. These migrations only create the reimbursement-specific tables that extend the base functionality. 55 | 56 | When using SqlxTaskStorage, make sure to: 57 | 58 | 1. Run the a2a-rs base migrations first (handled automatically by SqlxTaskStorage) 59 | 2. Run these reimbursement-specific migrations 60 | 3. Configure your server to use SQLx storage: 61 | 62 | ```json 63 | { 64 | "storage": { 65 | "type": "sqlx", 66 | "url": "sqlite://reimbursement.db", 67 | "max_connections": 5, 68 | "enable_logging": true 69 | } 70 | } 71 | ``` -------------------------------------------------------------------------------- /a2a-agents/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod reimbursement_agent; 2 | -------------------------------------------------------------------------------- /a2a-agents/src/reimbursement_agent/mod.rs: -------------------------------------------------------------------------------- 1 | //! Reimbursement agent implementation 2 | 3 | pub mod config; 4 | pub mod handler; 5 | pub mod server; 6 | pub mod types; 7 | 8 | // Re-export key types for convenience 9 | pub use config::{AuthConfig, ServerConfig, StorageConfig}; 10 | pub use handler::ReimbursementHandler; 11 | pub use server::ReimbursementServer; 12 | pub use types::*; 13 | -------------------------------------------------------------------------------- /a2a-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "a2a-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # Web framework 8 | axum = "0.7" 9 | tokio = { version = "1", features = ["full"] } 10 | tower = "0.5" 11 | tower-http = { version = "0.6", features = ["fs", "cors"] } 12 | 13 | # Templates 14 | askama = "0.12" 15 | askama_axum = "0.4" 16 | 17 | # HTTP client 18 | reqwest = { version = "0.12", features = ["json"] } 19 | 20 | # Serialization 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | 24 | # Error handling 25 | thiserror = "1.0" 26 | anyhow = "1.0" 27 | 28 | # Logging 29 | tracing = "0.1" 30 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 31 | 32 | # A2A integration 33 | a2a-rs = { path = "../a2a-rs", features = ["http-client", "server"], default-features = false } 34 | 35 | # Time handling 36 | chrono = "0.4" 37 | 38 | # Utilities 39 | uuid = { version = "1.0", features = ["v4", "serde"] } 40 | 41 | [[bin]] 42 | name = "server" 43 | path = "src/bin/server.rs" -------------------------------------------------------------------------------- /a2a-client/README.md: -------------------------------------------------------------------------------- 1 | # A2A Client 2 | 3 | A simple web-based chat client for interacting with A2A (Agent-to-Agent) protocol agents. This client provides a clean, server-side rendered interface for communicating with agents like the reimbursement agent. 4 | 5 | ## Features 6 | 7 | - 🌐 **Web-based interface** - No client installation required 8 | - 💬 **Real-time chat** - Send and receive messages with A2A agents 9 | - 🔄 **Auto-refresh** - Automatically updates chat history every 5 seconds 10 | - 🎨 **Clean UI** - Simple, responsive design with styled messages 11 | - 🚀 **Fast** - Server-side rendered with Askama templates 12 | - 🔧 **Easy setup** - Single binary, minimal configuration 13 | 14 | ## Quick Start 15 | 16 | ### Prerequisites 17 | 18 | - Rust 1.70 or later 19 | - An A2A agent running (e.g., reimbursement agent on `http://localhost:8080`) 20 | 21 | ### Building 22 | 23 | ```bash 24 | cd a2a-client 25 | cargo build --release --bin server 26 | ``` 27 | 28 | ### Running 29 | 30 | ```bash 31 | # Run with default settings (connects to http://localhost:8080) 32 | cargo run --bin server 33 | 34 | # Or specify a custom agent URL 35 | AGENT_URL=http://my-agent:8080 cargo run --bin server 36 | 37 | # With logging enabled 38 | RUST_LOG=info cargo run --bin server 39 | ``` 40 | 41 | The server will start on `http://localhost:3000`. 42 | 43 | ## Usage 44 | 45 | 1. **Start the server** using one of the commands above 46 | 2. **Open your browser** and navigate to `http://localhost:3000` 47 | 3. **Enter the agent URL** (or use the default `http://localhost:8080`) 48 | 4. **Click "Start New Chat"** to begin a conversation 49 | 5. **Type your message** and click "Send" to interact with the agent 50 | 51 | ## Architecture 52 | 53 | The client is built with: 54 | 55 | - **[Axum](https://github.com/tokio-rs/axum)** - Web framework 56 | - **[Askama](https://github.com/djc/askama)** - Type-safe templating 57 | - **[a2a-rs](../a2a-rs)** - A2A protocol implementation 58 | - **Server-side rendering** - No JavaScript required 59 | 60 | ### Project Structure 61 | 62 | ``` 63 | a2a-client/ 64 | ├── src/ 65 | │ ├── bin/ 66 | │ │ └── server.rs # Main server binary 67 | │ └── styles.css # CSS styles 68 | ├── templates/ 69 | │ ├── index.html # Home page template 70 | │ └── chat.html # Chat interface template 71 | ├── Cargo.toml 72 | └── README.md 73 | ``` 74 | 75 | ## Configuration 76 | 77 | ### Environment Variables 78 | 79 | - `AGENT_URL` - Default agent URL (default: `http://localhost:8080`) 80 | - `RUST_LOG` - Log level (e.g., `info`, `debug`, `trace`) 81 | 82 | ### Server Settings 83 | 84 | The server runs on port 3000 by default. To change this, modify the address in `src/bin/server.rs`: 85 | 86 | ```rust 87 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 88 | ``` 89 | 90 | ## Message Format 91 | 92 | The client uses the A2A protocol message format: 93 | 94 | - **User messages** are sent with `Role::User` 95 | - **Text content** is wrapped in message parts 96 | - **Message IDs** are automatically generated UUIDs 97 | - **Task IDs** are used to group conversations 98 | 99 | ## Styling 100 | 101 | The UI uses a clean, modern design with: 102 | 103 | - Distinct styling for user and agent messages 104 | - Responsive layout that works on mobile 105 | - Auto-scrolling to the latest message 106 | - Clear visual hierarchy 107 | 108 | To customize the appearance, edit `src/styles.css`. 109 | 110 | ## Development 111 | 112 | ### Adding Features 113 | 114 | To extend the client: 115 | 116 | 1. **New routes** - Add handlers in `src/bin/server.rs` 117 | 2. **New templates** - Create `.html` files in `templates/` 118 | 3. **API changes** - Update the message handling logic 119 | 4. **Styling** - Modify `src/styles.css` 120 | 121 | ### Testing 122 | 123 | Run the client with a mock agent or the reimbursement agent: 124 | 125 | ```bash 126 | # Terminal 1: Start the reimbursement agent 127 | cd ../a2a-agents 128 | cargo run --bin reimbursement_server 129 | 130 | # Terminal 2: Start the client 131 | cd ../a2a-client 132 | RUST_LOG=info cargo run --bin server 133 | 134 | # Terminal 3: Test with curl 135 | curl -X POST http://localhost:3000/chat/new \ 136 | -H "Content-Type: application/x-www-form-urlencoded" \ 137 | -d "agent_url=http://localhost:8080" 138 | ``` 139 | 140 | ## Troubleshooting 141 | 142 | ### Common Issues 143 | 144 | 1. **"Failed to connect to agent"** 145 | - Ensure the agent is running on the specified URL 146 | - Check firewall settings 147 | - Verify the agent URL is correct 148 | 149 | 2. **"No messages appearing"** 150 | - Check the browser console for errors 151 | - Ensure the task ID is valid 152 | - Verify the agent is responding 153 | 154 | 3. **"Build errors"** 155 | - Run `cargo clean` and rebuild 156 | - Ensure you're using Rust 1.70+ 157 | - Check all dependencies are available 158 | 159 | ### Debug Mode 160 | 161 | Enable detailed logging: 162 | 163 | ```bash 164 | RUST_LOG=debug cargo run --bin server 165 | ``` 166 | 167 | ## License 168 | 169 | This project is part of the a2a-rs workspace. See the main project for license information. -------------------------------------------------------------------------------- /a2a-client/TODO.md: -------------------------------------------------------------------------------- 1 | # A2A Client TODO 2 | 3 | ## High Priority 4 | 5 | ### Core Features 6 | - [ ] **WebSocket support** - Add real-time WebSocket connection option alongside HTTP 7 | - [ ] **Streaming responses** - Display agent responses as they stream in 8 | - [ ] **Multiple agent support** - Allow switching between different agents in the UI 9 | - [ ] **Session persistence** - Save chat history to local storage or database 10 | - [ ] **Authentication** - Add proper authentication token support in the UI 11 | 12 | ### UI/UX Improvements 13 | - [ ] **Remove auto-refresh** - Replace with WebSocket or SSE for real-time updates 14 | - [ ] **Loading states** - Show spinner while waiting for agent responses 15 | - [ ] **Error handling UI** - Better error messages and retry options 16 | - [ ] **Markdown rendering** - Support markdown in agent responses 17 | - [ ] **Code syntax highlighting** - Highlight code blocks in messages 18 | 19 | ## Medium Priority 20 | 21 | ### Features 22 | - [ ] **File uploads** - Support sending files to agents 23 | - [ ] **Artifact display** - Show agent artifacts (images, documents, etc.) 24 | - [ ] **Export chat** - Download conversation as text/markdown/PDF 25 | - [ ] **Chat history browser** - View and resume previous conversations 26 | - [ ] **Multi-turn context** - Better handling of conversation context 27 | 28 | ### Technical Improvements 29 | - [ ] **Configuration file** - Support config.toml for server settings 30 | - [ ] **Health check endpoint** - Add /health for monitoring 31 | 32 | ### Developer Experience 33 | - [ ] **API documentation** - Generate OpenAPI/Swagger docs 34 | 35 | ## Low Priority 36 | 37 | ### Advanced Features 38 | - [ ] **Multi-agent chat** - Chat with multiple agents simultaneously 39 | - [ ] **Agent discovery** - Auto-discover available agents on the network 40 | 41 | ### Performance 42 | - [ ] **Response caching** - Cache agent responses where appropriate 43 | - [ ] **Compression** - Enable gzip/brotli compression 44 | - [ ] **Static asset optimization** - Bundle and minify CSS 45 | - [ ] **Connection pooling** - Reuse HTTP connections to agents 46 | 47 | ### Security 48 | - [ ] **CORS configuration** - Proper CORS setup for production 49 | - [ ] **Rate limiting** - Prevent abuse with rate limits 50 | - [ ] **HTTPS support** - Built-in TLS certificate handling 51 | - [ ] **Content Security Policy** - Add CSP headers 52 | - [ ] **Input sanitization** - Prevent XSS attacks 53 | 54 | ## Future Ideas 55 | 56 | ### Experimental 57 | - [ ] **Plugin system** - Allow custom message handlers/transformers 58 | - [ ] **Agent SDK** - JavaScript SDK for embedding the chat 59 | 60 | ### Integrations 61 | - [ ] **Email gateway** - Interact with agents via email 62 | - [ ] **Webhook support** - Send agent responses to webhooks 63 | - [ ] **Slack integration** - Chat with agents via Slack 64 | - [ ] **Discord bot** - Discord bot interface for agents 65 | 66 | ## Known Issues 67 | 68 | ### Bugs to Fix 69 | - [ ] **Agent URL validation** - Currently accepts any string as agent URL 70 | - [ ] **Message ordering** - Ensure messages always appear in correct order 71 | - [ ] **Large message handling** - UI breaks with very long messages 72 | - [ ] **Concurrent requests** - Handle multiple users on same task ID 73 | - [ ] **Memory leak** - Investigate potential memory leak in long sessions 74 | 75 | ### Technical Debt 76 | - [ ] **Error types** - Create proper error types instead of anyhow 77 | - [ ] **State management** - Consider using a proper state store 78 | - [ ] **Template organization** - Split large templates into partials 79 | - [ ] **CSS architecture** - Consider using CSS modules or similar 80 | 81 | ## Notes 82 | 83 | - The current implementation uses HTTP polling with a 5-second refresh. This should be replaced with WebSocket or Server-Sent Events for a better user experience. 84 | - The client currently creates a new HttpClient for each request. Consider implementing connection pooling. 85 | - The UI is intentionally simple to focus on functionality. A more sophisticated design system could be implemented. 86 | - Consider supporting the full A2A protocol specification as it evolves. -------------------------------------------------------------------------------- /a2a-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | A2A Client with Leptos 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /a2a-client/src/styles.css: -------------------------------------------------------------------------------- 1 | /* Reset and base styles */ 2 | * { 3 | box-sizing: border-box; 4 | } 5 | 6 | body { 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 8 | line-height: 1.6; 9 | color: #333; 10 | background: #f0f0f0; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | .container { 16 | max-width: 800px; 17 | margin: 0 auto; 18 | padding: 20px; 19 | } 20 | 21 | h1 { 22 | color: #2c3e50; 23 | margin-bottom: 10px; 24 | } 25 | 26 | /* Forms */ 27 | .agent-form, .message-form { 28 | background: white; 29 | padding: 20px; 30 | border-radius: 8px; 31 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 32 | margin: 20px 0; 33 | } 34 | 35 | .agent-form label { 36 | display: block; 37 | margin-bottom: 8px; 38 | font-weight: bold; 39 | } 40 | 41 | .agent-form input[type="url"], 42 | .message-form input[type="text"] { 43 | width: 100%; 44 | padding: 10px; 45 | border: 1px solid #ddd; 46 | border-radius: 4px; 47 | font-size: 16px; 48 | } 49 | 50 | button { 51 | background: #3498db; 52 | color: white; 53 | border: none; 54 | padding: 10px 20px; 55 | border-radius: 4px; 56 | cursor: pointer; 57 | font-size: 16px; 58 | margin-top: 10px; 59 | } 60 | 61 | button:hover { 62 | background: #2980b9; 63 | } 64 | 65 | /* Info section */ 66 | .info { 67 | background: white; 68 | padding: 20px; 69 | border-radius: 8px; 70 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 71 | margin-top: 20px; 72 | } 73 | 74 | .info h2 { 75 | color: #34495e; 76 | margin-bottom: 10px; 77 | } 78 | 79 | .info ul { 80 | list-style: none; 81 | padding: 0; 82 | } 83 | 84 | .info li { 85 | padding: 5px 0; 86 | } 87 | 88 | code { 89 | background: #f4f4f4; 90 | padding: 2px 4px; 91 | border-radius: 3px; 92 | font-family: 'Consolas', 'Monaco', monospace; 93 | } 94 | 95 | /* Chat styles */ 96 | .task-id { 97 | background: #ecf0f1; 98 | padding: 10px; 99 | border-radius: 4px; 100 | margin-bottom: 20px; 101 | } 102 | 103 | .chat-container { 104 | background: white; 105 | border-radius: 8px; 106 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 107 | overflow: hidden; 108 | } 109 | 110 | .messages { 111 | min-height: 400px; 112 | max-height: 500px; 113 | overflow-y: auto; 114 | padding: 20px; 115 | background: #fafafa; 116 | } 117 | 118 | .message { 119 | margin-bottom: 15px; 120 | padding: 12px; 121 | border-radius: 8px; 122 | box-shadow: 0 1px 2px rgba(0,0,0,0.1); 123 | } 124 | 125 | .message-user { 126 | background: #e3f2fd; 127 | margin-left: 20%; 128 | } 129 | 130 | .message-assistant { 131 | background: white; 132 | margin-right: 20%; 133 | border: 1px solid #e0e0e0; 134 | } 135 | 136 | .message-system { 137 | background: #fff3cd; 138 | text-align: center; 139 | font-style: italic; 140 | } 141 | 142 | .message-header { 143 | display: flex; 144 | justify-content: space-between; 145 | margin-bottom: 8px; 146 | font-size: 0.85em; 147 | color: #666; 148 | } 149 | 150 | .role { 151 | font-weight: bold; 152 | text-transform: capitalize; 153 | } 154 | 155 | .timestamp { 156 | font-size: 0.8em; 157 | } 158 | 159 | .message-content { 160 | white-space: pre-wrap; 161 | word-wrap: break-word; 162 | } 163 | 164 | .no-messages { 165 | text-align: center; 166 | color: #999; 167 | padding: 40px; 168 | } 169 | 170 | /* Message form */ 171 | .message-form { 172 | padding: 15px 20px; 173 | margin: 0; 174 | border-radius: 0; 175 | border-top: 1px solid #e0e0e0; 176 | } 177 | 178 | .input-group { 179 | display: flex; 180 | gap: 10px; 181 | } 182 | 183 | .input-group input { 184 | flex: 1; 185 | margin: 0; 186 | } 187 | 188 | .input-group button { 189 | margin: 0; 190 | } 191 | 192 | /* Actions */ 193 | .actions { 194 | text-align: center; 195 | margin-top: 20px; 196 | } 197 | 198 | .actions a { 199 | color: #3498db; 200 | text-decoration: none; 201 | } 202 | 203 | .actions a:hover { 204 | text-decoration: underline; 205 | } 206 | 207 | /* Responsive */ 208 | @media (max-width: 600px) { 209 | .container { 210 | padding: 10px; 211 | } 212 | 213 | .message-user, 214 | .message-assistant { 215 | margin-left: 0; 216 | margin-right: 0; 217 | } 218 | } -------------------------------------------------------------------------------- /a2a-client/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat - A2A Client 7 | 8 | 9 | 10 | 11 |
12 |

Chat Session

13 |

Task ID: {{ task_id }}

14 | 15 |
16 |
17 | {% for message in messages %} 18 |
19 |
20 | {{ message.role }} 21 |
22 |
{{ message.content }}
23 |
24 | {% endfor %} 25 | 26 | {% if messages.is_empty() %} 27 |

No messages yet. Send a message to start the conversation!

28 | {% endif %} 29 |
30 | 31 |
32 | 33 |
34 | 41 | 42 |
43 |
44 |
45 | 46 |
47 | Start New Chat 48 |
49 |
50 | 51 | 58 | 59 | -------------------------------------------------------------------------------- /a2a-client/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | A2A Chat Client 7 | 8 | 9 | 10 |
11 |

A2A Chat Client

12 |

Connect to an A2A agent and start chatting

13 | 14 |
15 | 16 | 24 | 25 |
26 | 27 |
28 |

Available Agents

29 |
    30 |
  • Reimbursement Agent - Usually runs on http://localhost:8080
  • 31 |
32 |
33 |
34 | 35 | -------------------------------------------------------------------------------- /a2a-mcp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "a2a-mcp" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["A2A-RS Contributors"] 6 | description = "Integration between A2A Protocol and Rusty Model Context Protocol (RMCP)" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/username/a2a-rs" 9 | keywords = ["a2a", "rmcp", "ai", "agent", "protocol"] 10 | categories = ["ai", "network-programming"] 11 | 12 | # Simplify for initial implementation 13 | [dependencies] 14 | # We'll add more dependencies as needed in the future 15 | rmcp = { version = "0.1.5" } 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | 19 | [dev-dependencies] 20 | # Simple test dependencies for now 21 | -------------------------------------------------------------------------------- /a2a-mcp/README.md: -------------------------------------------------------------------------------- 1 | # A2A-RMCP Integration 2 | 3 | A bridge between Agent-to-Agent (A2A) protocol and Rusty Model Context Protocol (RMCP) 4 | 5 | ## Overview 6 | 7 | This crate provides integration between the A2A protocol and RMCP, enabling bidirectional communication between these protocols. It follows a bridge pattern with adapter layers for message conversion and protocol translation. 8 | 9 | ## Key Features 10 | 11 | - Use A2A agents as RMCP tools 12 | - Expose RMCP tools as A2A agents 13 | - Bidirectional message conversion 14 | - State management across protocols 15 | 16 | ## Examples 17 | 18 | See the `examples` directory for working demonstrations: 19 | 20 | ```rust 21 | cargo run --example minimal_example 22 | ``` 23 | 24 | ## Architecture 25 | 26 | ```text 27 | ┌─────────────────────────────────────────────┐ 28 | │ a2a-mcp Crate │ 29 | ├─────────────┬─────────────┬─────────────────┤ 30 | │ RMCP Client │ Translation │ A2A Client │ 31 | │ Interface │ Layer │ Interface │ 32 | ├─────────────┼─────────────┼─────────────────┤ 33 | │ RMCP Server │ Conversion │ A2A Server │ 34 | │ Interface │ Layer │ Interface │ 35 | └─────────────┴─────────────┴─────────────────┘ 36 | ``` 37 | 38 | ## Development Status 39 | 40 | See [TODO.md](TODO.md) for current implementation status and next steps. -------------------------------------------------------------------------------- /a2a-mcp/examples/minimal_example.rs: -------------------------------------------------------------------------------- 1 | //! A minimal example of A2A-RMCP integration 2 | 3 | use serde_json::json; 4 | 5 | fn main() { 6 | println!("A2A-RMCP Integration Demo"); 7 | println!("=========================\n"); 8 | 9 | // Simulate RMCP request 10 | let rmcp_request = json!({ 11 | "jsonrpc": "2.0", 12 | "id": 1, 13 | "method": "calculate", 14 | "params": { 15 | "operation": "add", 16 | "a": 5, 17 | "b": 7 18 | } 19 | }); 20 | 21 | println!("RMCP Request:"); 22 | println!("{}", serde_json::to_string_pretty(&rmcp_request).unwrap()); 23 | println!(); 24 | 25 | // Simulate converting to A2A message 26 | println!("Converting to A2A message..."); 27 | println!("A2A Task would look like:"); 28 | println!("{}", serde_json::to_string_pretty(&json!({ 29 | "id": "task-123456", 30 | "status": { 31 | "state": "submitted", 32 | "message": "Task submitted" 33 | }, 34 | "messages": [ 35 | { 36 | "role": "user", 37 | "parts": [ 38 | { 39 | "type": "text", 40 | "text": "Call method: calculate" 41 | }, 42 | { 43 | "type": "data", 44 | "mime_type": "application/json", 45 | "data": { 46 | "operation": "add", 47 | "a": 5, 48 | "b": 7 49 | } 50 | } 51 | ] 52 | } 53 | ] 54 | })).unwrap()); 55 | println!(); 56 | 57 | // Simulate A2A agent response 58 | println!("A2A Agent Response:"); 59 | println!("{}", serde_json::to_string_pretty(&json!({ 60 | "id": "task-123456", 61 | "status": { 62 | "state": "completed", 63 | "message": "Task completed" 64 | }, 65 | "messages": [ 66 | { 67 | "role": "user", 68 | "parts": [ 69 | { 70 | "type": "text", 71 | "text": "Call method: calculate" 72 | }, 73 | { 74 | "type": "data", 75 | "mime_type": "application/json", 76 | "data": { 77 | "operation": "add", 78 | "a": 5, 79 | "b": 7 80 | } 81 | } 82 | ] 83 | }, 84 | { 85 | "role": "agent", 86 | "parts": [ 87 | { 88 | "type": "data", 89 | "mime_type": "application/json", 90 | "data": { 91 | "result": 12 92 | } 93 | } 94 | ] 95 | } 96 | ] 97 | })).unwrap()); 98 | println!(); 99 | 100 | // Convert back to RMCP response 101 | let rmcp_response = json!({ 102 | "jsonrpc": "2.0", 103 | "id": 1, 104 | "result": { 105 | "result": 12 106 | } 107 | }); 108 | 109 | println!("Converting back to RMCP response..."); 110 | println!("RMCP Response:"); 111 | println!("{}", serde_json::to_string_pretty(&rmcp_response).unwrap()); 112 | } -------------------------------------------------------------------------------- /a2a-mcp/src/adapter/agent_to_tool.rs: -------------------------------------------------------------------------------- 1 | //! Adapter that exposes A2A agents as RMCP tools 2 | 3 | use crate::error::{Error, Result}; 4 | use crate::message::MessageConverter; 5 | use a2a_rs::domain::agent::{AgentCard, Skill}; 6 | use rmcp::{Tool, ToolCall, ToolResponse}; 7 | use std::collections::HashMap; 8 | use std::sync::Arc; 9 | 10 | /// Adapts A2A agents to RMCP tool capabilities 11 | pub struct AgentToToolAdapter { 12 | converter: Arc, 13 | agent_cache: HashMap, 14 | } 15 | 16 | impl AgentToToolAdapter { 17 | /// Create a new adapter 18 | pub fn new() -> Self { 19 | Self { 20 | converter: Arc::new(MessageConverter::new()), 21 | agent_cache: HashMap::new(), 22 | } 23 | } 24 | 25 | /// Add an agent to the cache 26 | pub fn add_agent(&mut self, url: String, card: AgentCard) { 27 | self.agent_cache.insert(url, card); 28 | } 29 | 30 | /// Get an agent from the cache 31 | pub fn get_agent(&self, url: &str) -> Option<&AgentCard> { 32 | self.agent_cache.get(url) 33 | } 34 | 35 | /// Generate RMCP tools from an A2A agent 36 | pub fn generate_tools(&self, agent: &AgentCard, agent_url: &str) -> Vec { 37 | agent.skills.iter().map(|skill| { 38 | self.skill_to_tool(skill, agent, agent_url) 39 | }).collect() 40 | } 41 | 42 | /// Convert an A2A skill to an RMCP tool 43 | fn skill_to_tool(&self, skill: &Skill, agent: &AgentCard, agent_url: &str) -> Tool { 44 | let tool_name = format!("{}:{}", agent_url, skill.name); 45 | 46 | Tool { 47 | name: tool_name, 48 | description: format!("{} - {}", agent.description, skill.description), 49 | parameters: None, // Could generate from skill.inputs if available 50 | } 51 | } 52 | 53 | /// Convert RMCP tool call to A2A task parameters 54 | pub fn tool_call_to_task(&self, call: &ToolCall, agent_card: &AgentCard, method: &str) -> Result { 55 | // Create a message from the tool call 56 | let message = self.converter.tool_call_to_message(call)?; 57 | 58 | // Create a task with the message 59 | Ok(a2a_rs::domain::task::Task { 60 | id: uuid::Uuid::new_v4().to_string(), 61 | status: a2a_rs::domain::task::TaskStatus { 62 | state: a2a_rs::domain::task::TaskState::Submitted, 63 | message: Some("Task submitted from RMCP tool call".to_string()), 64 | }, 65 | messages: vec![message], 66 | artifacts: Vec::new(), 67 | history_ttl: Some(3600), // 1 hour default 68 | metadata: Some(serde_json::json!({ 69 | "skill": method, 70 | "agent": agent_card.name.clone(), 71 | })), 72 | }) 73 | } 74 | 75 | /// Convert A2A task response to RMCP tool response 76 | pub fn task_to_tool_response(&self, task: &a2a_rs::domain::task::Task) -> Result { 77 | // Extract the last agent message 78 | let agent_message = self.converter.extract_agent_message(task)?; 79 | 80 | // Convert to tool response 81 | self.converter.message_to_tool_response(agent_message) 82 | } 83 | 84 | /// Parse tool method string in format "agent_url:method" 85 | pub fn parse_tool_method(&self, tool_method: &str) -> Result<(String, String)> { 86 | let parts: Vec<&str> = tool_method.splitn(2, ':').collect(); 87 | if parts.len() != 2 { 88 | return Err(Error::InvalidToolMethod(tool_method.to_string())); 89 | } 90 | 91 | Ok((parts[0].to_string(), parts[1].to_string())) 92 | } 93 | } -------------------------------------------------------------------------------- /a2a-mcp/src/adapter/mod.rs: -------------------------------------------------------------------------------- 1 | //! Adapters for converting between A2A and RMCP 2 | 3 | mod tool_to_agent; 4 | mod agent_to_tool; 5 | 6 | pub use tool_to_agent::ToolToAgentAdapter; 7 | pub use agent_to_tool::AgentToToolAdapter; -------------------------------------------------------------------------------- /a2a-mcp/src/adapter/tool_to_agent.rs: -------------------------------------------------------------------------------- 1 | //! Adapter that exposes RMCP tools as A2A agents 2 | 3 | use crate::error::{Error, Result}; 4 | use crate::message::MessageConverter; 5 | use a2a_rs::domain::{agent::{AgentCard, Capabilities, Authentication, Skill}, task::{Task, TaskState, TaskStatus}, message::{Message, MessagePart}}; 6 | use rmcp::{Tool, ToolCall, ToolResponse}; 7 | use std::sync::Arc; 8 | use uuid::Uuid; 9 | 10 | /// Adapts RMCP tools to A2A agent capabilities 11 | pub struct ToolToAgentAdapter { 12 | tools: Vec, 13 | agent_name: String, 14 | agent_description: String, 15 | converter: Arc, 16 | } 17 | 18 | impl ToolToAgentAdapter { 19 | /// Create a new adapter with the given tools 20 | pub fn new(tools: Vec, agent_name: String, agent_description: String) -> Self { 21 | Self { 22 | tools, 23 | agent_name, 24 | agent_description, 25 | converter: Arc::new(MessageConverter::new()), 26 | } 27 | } 28 | 29 | /// Generate A2A agent card from RMCP tools 30 | pub fn generate_agent_card(&self) -> AgentCard { 31 | // Create skills from tools 32 | let skills = self.tools.iter().map(|tool| { 33 | Skill { 34 | name: tool.name.clone(), 35 | description: tool.description.clone(), 36 | inputs: None, 37 | outputs: None, 38 | input_modes: Some(vec!["text".to_string(), "data".to_string()]), 39 | output_modes: Some(vec!["text".to_string(), "data".to_string()]), 40 | metadata: None, 41 | } 42 | }).collect(); 43 | 44 | AgentCard { 45 | name: self.agent_name.clone(), 46 | description: self.agent_description.clone(), 47 | url: "https://example.com/agent".to_string(), // Would be configured 48 | version: "1.0.0".to_string(), 49 | capabilities: Capabilities { 50 | streaming: true, 51 | push_notifications: false, 52 | state_transition_history: true, 53 | }, 54 | authentication: Authentication { 55 | schemes: vec!["Bearer".to_string()], 56 | // Other auth fields 57 | }, 58 | default_input_modes: vec!["text".to_string()], 59 | default_output_modes: vec!["text".to_string()], 60 | skills, 61 | metadata: None, 62 | } 63 | } 64 | 65 | /// Map RMCP tool call to A2A task 66 | pub fn tool_call_to_task(&self, call: &ToolCall) -> Result { 67 | // Create an A2A task from an RMCP tool call 68 | let task_id = Uuid::new_v4().to_string(); 69 | 70 | let initial_message = Message { 71 | role: "user".to_string(), 72 | parts: vec![ 73 | MessagePart::Text { 74 | text: format!("Call tool: {}", call.method) 75 | }, 76 | MessagePart::Data { 77 | data: call.params.clone(), 78 | mime_type: Some("application/json".to_string()), 79 | }, 80 | ], 81 | }; 82 | 83 | Ok(Task { 84 | id: task_id, 85 | status: TaskStatus { 86 | state: TaskState::Submitted, 87 | message: Some("Task submitted".to_string()), 88 | }, 89 | messages: vec![initial_message], 90 | artifacts: Vec::new(), 91 | history_ttl: Some(3600), // 1 hour default 92 | metadata: None, 93 | }) 94 | } 95 | 96 | /// Map A2A task result to RMCP tool response 97 | pub fn task_to_tool_response(&self, task: &Task) -> Result { 98 | // Extract the last agent message from the task 99 | let last_message = self.converter.extract_agent_message(task)?; 100 | 101 | // Convert to tool response 102 | self.converter.message_to_tool_response(last_message) 103 | } 104 | 105 | /// Find a tool by name 106 | pub fn find_tool(&self, name: &str) -> Option<&Tool> { 107 | self.tools.iter().find(|tool| tool.name == name) 108 | } 109 | 110 | /// Extract tool name and parameters from an A2A message 111 | pub fn extract_tool_call(&self, message: &Message) -> Result<(String, serde_json::Value)> { 112 | // Try to find a text part with "Call tool: " prefix 113 | let tool_name = message.parts.iter() 114 | .find_map(|part| { 115 | if let MessagePart::Text { text } = part { 116 | if text.starts_with("Call tool: ") { 117 | Some(text.trim_start_matches("Call tool: ").to_string()) 118 | } else { 119 | None 120 | } 121 | } else { 122 | None 123 | } 124 | }) 125 | .ok_or_else(|| Error::Translation("Unable to extract tool name from message".into()))?; 126 | 127 | // Try to find a data part 128 | let params = message.parts.iter() 129 | .find_map(|part| { 130 | if let MessagePart::Data { data, .. } = part { 131 | Some(data.clone()) 132 | } else { 133 | None 134 | } 135 | }) 136 | .unwrap_or(serde_json::Value::Null); 137 | 138 | Ok((tool_name, params)) 139 | } 140 | } -------------------------------------------------------------------------------- /a2a-mcp/src/client.rs: -------------------------------------------------------------------------------- 1 | //! Client for accessing A2A agents as RMCP tools 2 | 3 | use crate::adapter::AgentToToolAdapter; 4 | use crate::error::{Error, Result}; 5 | use a2a_rs::domain::agent::AgentCard; 6 | use a2a_rs::port::client::AsyncA2AClient; 7 | use rmcp::{Tool, ToolCall, ToolResponse}; 8 | use std::collections::HashMap; 9 | use std::sync::{Arc, Mutex}; 10 | use tracing::{info, debug, error}; 11 | 12 | /// A client that accesses A2A agents as RMCP tools 13 | pub struct A2aRmcpClient { 14 | a2a_client: C, 15 | adapter: Arc>, 16 | } 17 | 18 | impl A2aRmcpClient { 19 | /// Create a new client that discovers A2A agents 20 | pub fn new(a2a_client: C) -> Self { 21 | Self { 22 | a2a_client, 23 | adapter: Arc::new(Mutex::new(AgentToToolAdapter::new())), 24 | } 25 | } 26 | 27 | /// Discover A2A agents and convert to RMCP tools 28 | pub async fn discover_agents(&self, urls: &[String]) -> Result> { 29 | let mut tools = Vec::new(); 30 | 31 | for url in urls { 32 | debug!("Discovering agent at {}", url); 33 | 34 | // Fetch agent card 35 | let agent_card = self.a2a_client.fetch_agent_card(url).await 36 | .map_err(|e| Error::A2a(format!("Failed to fetch agent card from {}: {}", url, e)))?; 37 | 38 | info!("Discovered agent: {} with {} skills", 39 | agent_card.name, 40 | agent_card.skills.len()); 41 | 42 | // Cache the agent card 43 | let mut adapter = self.adapter.lock().unwrap(); 44 | adapter.add_agent(url.clone(), agent_card.clone()); 45 | 46 | // Convert agent capabilities to tools 47 | let agent_tools = adapter.generate_tools(&agent_card, url); 48 | tools.extend(agent_tools); 49 | } 50 | 51 | Ok(tools) 52 | } 53 | 54 | /// Call an A2A agent as an RMCP tool 55 | pub async fn call_agent_as_tool(&self, call: ToolCall) -> Result { 56 | // Parse the tool call to extract agent URL and method 57 | let adapter = self.adapter.lock().unwrap(); 58 | let (agent_url, method) = adapter.parse_tool_method(&call.method)?; 59 | 60 | // Get agent card from cache 61 | let agent_card = adapter.get_agent(&agent_url) 62 | .ok_or_else(|| Error::AgentNotFound(agent_url.clone()))? 63 | .clone(); 64 | 65 | drop(adapter); // Release the lock before async calls 66 | 67 | debug!("Calling agent {} with method {}", agent_card.name, method); 68 | 69 | // Convert RMCP tool call to A2A task 70 | let adapter = self.adapter.lock().unwrap(); 71 | let task = adapter.tool_call_to_task(&call, &agent_card, &method)?; 72 | let task_id = task.id.clone(); 73 | drop(adapter); 74 | 75 | // Send task to A2A agent 76 | let response = self.a2a_client.send_task(&agent_url, task).await 77 | .map_err(|e| Error::A2a(format!("Failed to send task to agent: {}", e)))?; 78 | 79 | debug!("Task {} sent to agent, waiting for completion", task_id); 80 | 81 | // Wait for task completion 82 | let completed_task = self.a2a_client.wait_for_completion(&agent_url, &response.id).await 83 | .map_err(|e| Error::A2a(format!("Failed to wait for task completion: {}", e)))?; 84 | 85 | info!("Task {} completed with status {:?}", 86 | completed_task.id, 87 | completed_task.status.state); 88 | 89 | // Convert A2A task result to RMCP tool response 90 | let adapter = self.adapter.lock().unwrap(); 91 | adapter.task_to_tool_response(&completed_task) 92 | } 93 | } -------------------------------------------------------------------------------- /a2a-mcp/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for a2a-mcp integration 2 | 3 | use thiserror::Error; 4 | 5 | /// Errors that can occur in a2a-mcp integration 6 | #[derive(Error, Debug)] 7 | pub enum Error { 8 | /// Error related to A2A protocol 9 | #[error("A2A error: {0}")] 10 | A2a(String), 11 | 12 | /// Error related to RMCP protocol 13 | #[error("RMCP error: {0}")] 14 | Rmcp(String), 15 | 16 | /// Error in protocol translation 17 | #[error("Protocol translation error: {0}")] 18 | Translation(String), 19 | 20 | /// Task not found 21 | #[error("Task not found: {0}")] 22 | TaskNotFound(String), 23 | 24 | /// Error in task processing 25 | #[error("Task processing error: {0}")] 26 | TaskProcessing(String), 27 | 28 | /// Agent not found 29 | #[error("Agent not found: {0}")] 30 | AgentNotFound(String), 31 | 32 | /// Invalid tool method format 33 | #[error("Invalid tool method format: {0}")] 34 | InvalidToolMethod(String), 35 | 36 | /// Server error 37 | #[error("Server error: {0}")] 38 | Server(String), 39 | 40 | /// RMCP tool call error 41 | #[error("RMCP tool call error: {0}")] 42 | RmcpToolCall(String), 43 | 44 | /// JSON serialization/deserialization error 45 | #[error("JSON error: {0}")] 46 | Json(#[from] serde_json::Error), 47 | 48 | /// HTTP request error 49 | #[error("HTTP error: {0}")] 50 | Http(#[from] reqwest::Error), 51 | 52 | /// IO error 53 | #[error("IO error: {0}")] 54 | Io(#[from] std::io::Error), 55 | } 56 | 57 | /// Result type for a2a-mcp operations 58 | pub type Result = std::result::Result; 59 | 60 | /// Convenience function to convert a string error to an Error 61 | pub fn err(e: E) -> Error { 62 | Error::Translation(e.to_string()) 63 | } 64 | 65 | /// Convert a2a_rs error to our Error type 66 | impl From for Error { 67 | fn from(e: a2a_rs::Error) -> Self { 68 | Error::A2a(e.to_string()) 69 | } 70 | } 71 | 72 | // A utility function to convert an RMCP error to A2A error code 73 | pub(crate) fn rmcp_error_to_a2a_code(rmcp_err: &rmcp::ServerJsonRpcMessage) -> i32 { 74 | if let Some(error) = &rmcp_err.error { 75 | match error.code { 76 | -32700 => -32700, // Parse error 77 | -32600 => -32600, // Invalid request 78 | -32601 => -32601, // Method not found 79 | -32602 => -32602, // Invalid params 80 | -32603 => -32603, // Internal error 81 | _ => -32000, // Server error 82 | } 83 | } else { 84 | -32000 // Default server error 85 | } 86 | } -------------------------------------------------------------------------------- /a2a-mcp/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # A2A-RMCP Integration 2 | //! 3 | //! This crate provides integration between the Agent-to-Agent (A2A) protocol 4 | //! and Rusty Model Context Protocol (RMCP), enabling bidirectional 5 | //! communication between these protocols. 6 | //! 7 | //! ## Core Features 8 | //! 9 | //! - Use A2A agents as RMCP tools 10 | //! - Expose RMCP tools as A2A agents 11 | //! - Bidirectional message conversion 12 | //! - State management across protocols 13 | //! 14 | //! ## Architecture 15 | //! 16 | //! The crate follows a bridge pattern with adapter layers: 17 | //! 18 | //! ```text 19 | //! ┌─────────────────────────────────────────────┐ 20 | //! │ a2a-mcp Crate │ 21 | //! ├─────────────┬─────────────┬─────────────────┤ 22 | //! │ RMCP Client │ Translation │ A2A Client │ 23 | //! │ Interface │ Layer │ Interface │ 24 | //! ├─────────────┼─────────────┼─────────────────┤ 25 | //! │ RMCP Server │ Conversion │ A2A Server │ 26 | //! │ Interface │ Layer │ Interface │ 27 | //! └─────────────┴─────────────┴─────────────────┘ 28 | //! ``` 29 | 30 | // For the initial implementation, we'll provide the basic structure 31 | // and a simple example. In a full implementation, this would be expanded 32 | // to include all the modules listed below. 33 | 34 | /* 35 | mod error; 36 | mod message; 37 | mod transport; 38 | mod adapter; 39 | mod client; 40 | mod server; 41 | mod util; 42 | #[cfg(test)] 43 | mod tests; 44 | 45 | // Re-export key components 46 | pub use error::{Error, Result}; 47 | pub use client::A2aRmcpClient; 48 | pub use server::RmcpA2aServer; 49 | pub use adapter::{AgentToToolAdapter, ToolToAgentAdapter}; 50 | pub use message::MessageConverter; 51 | */ 52 | 53 | // Version information 54 | /// Current crate version 55 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 56 | 57 | // Simple placeholder for now 58 | pub fn add(a: i32, b: i32) -> i32 { 59 | a + b 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[test] 67 | fn test_add() { 68 | assert_eq!(add(2, 2), 4); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /a2a-mcp/src/message.rs: -------------------------------------------------------------------------------- 1 | //! Message conversion between A2A and RMCP protocols 2 | 3 | use crate::error::{Error, Result}; 4 | use a2a_rs::domain::{message::{Message, MessagePart}, task::Task}; 5 | use rmcp::{ClientJsonRpcMessage, ServerJsonRpcMessage, ToolCall, ToolResponse}; 6 | use serde_json::Value; 7 | 8 | /// Converts between RMCP and A2A message formats 9 | #[derive(Debug, Default)] 10 | pub struct MessageConverter {} 11 | 12 | impl MessageConverter { 13 | /// Create a new message converter 14 | pub fn new() -> Self { 15 | Self {} 16 | } 17 | 18 | /// Convert RMCP request to A2A message 19 | pub fn rmcp_to_a2a_request(&self, req: &ClientJsonRpcMessage) -> Result { 20 | // Extract method and params from RMCP JSON-RPC request 21 | let method = req.method.clone(); 22 | let params = req.params.clone(); 23 | 24 | // Create A2A message with appropriate content 25 | let mut parts = Vec::new(); 26 | 27 | // Add text part describing the tool call 28 | parts.push(MessagePart::Text { 29 | text: format!("Call method: {}", method) 30 | }); 31 | 32 | // Add data part with the parameters 33 | if let Some(params_value) = params { 34 | parts.push(MessagePart::Data { 35 | data: params_value.clone(), 36 | mime_type: Some("application/json".to_string()), 37 | }); 38 | } 39 | 40 | Ok(Message { 41 | role: "user".to_string(), 42 | parts, 43 | }) 44 | } 45 | 46 | /// Convert A2A message to RMCP response 47 | pub fn a2a_to_rmcp_response(&self, msg: &Message, id: Option) -> Result { 48 | // Extract content from A2A message parts 49 | let mut result_value = Value::Null; 50 | 51 | for part in &msg.parts { 52 | match part { 53 | MessagePart::Data { data, .. } => { 54 | // Use the data part as the result if available 55 | result_value = data.clone(); 56 | break; 57 | }, 58 | MessagePart::Text { text } => { 59 | // If only text is available, convert to string result 60 | if result_value == Value::Null { 61 | result_value = Value::String(text.clone()); 62 | } 63 | }, 64 | _ => continue, 65 | } 66 | } 67 | 68 | // Create RMCP JSON-RPC response 69 | Ok(ServerJsonRpcMessage { 70 | jsonrpc: "2.0".to_string(), 71 | id, 72 | result: Some(result_value), 73 | error: None, 74 | }) 75 | } 76 | 77 | /// Convert RMCP tool call to A2A message 78 | pub fn tool_call_to_message(&self, call: &ToolCall) -> Result { 79 | let mut parts = Vec::new(); 80 | 81 | // Add text part for the method 82 | parts.push(MessagePart::Text { 83 | text: format!("Tool call: {}", call.method) 84 | }); 85 | 86 | // Add data part for the parameters 87 | parts.push(MessagePart::Data { 88 | data: call.params.clone(), 89 | mime_type: Some("application/json".to_string()), 90 | }); 91 | 92 | Ok(Message { 93 | role: "user".to_string(), 94 | parts, 95 | }) 96 | } 97 | 98 | /// Convert A2A message to RMCP tool response 99 | pub fn message_to_tool_response(&self, msg: &Message) -> Result { 100 | let mut result = Value::Null; 101 | 102 | // Extract content from message parts 103 | for part in &msg.parts { 104 | match part { 105 | MessagePart::Data { data, .. } => { 106 | result = data.clone(); 107 | break; 108 | }, 109 | MessagePart::Text { text } => { 110 | if result == Value::Null { 111 | result = Value::String(text.clone()); 112 | } 113 | }, 114 | _ => continue, 115 | } 116 | } 117 | 118 | Ok(ToolResponse { result }) 119 | } 120 | 121 | /// Extract the last agent message from a task 122 | pub fn extract_agent_message<'a>(&self, task: &'a Task) -> Result<&'a Message> { 123 | task.messages.iter() 124 | .filter(|msg| msg.role == "agent" || msg.role == "assistant") 125 | .last() 126 | .ok_or_else(|| Error::TaskProcessing("No agent message found".into())) 127 | } 128 | 129 | /// Extract the last user message from a task 130 | pub fn extract_user_message<'a>(&self, task: &'a Task) -> Result<&'a Message> { 131 | task.messages.iter() 132 | .filter(|msg| msg.role == "user" || msg.role == "human") 133 | .last() 134 | .ok_or_else(|| Error::TaskProcessing("No user message found".into())) 135 | } 136 | } -------------------------------------------------------------------------------- /a2a-mcp/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | //! Tests for a2a-mcp integration 2 | 3 | #[cfg(test)] 4 | mod message_converter_tests { 5 | use crate::message::MessageConverter; 6 | use a2a_rs::domain::message::{Message, MessagePart}; 7 | use rmcp::{ClientJsonRpcMessage, ServerJsonRpcMessage}; 8 | use serde_json::json; 9 | 10 | #[test] 11 | fn test_rmcp_to_a2a_request() { 12 | let converter = MessageConverter::new(); 13 | 14 | let rmcp_request = ClientJsonRpcMessage { 15 | jsonrpc: "2.0".to_string(), 16 | id: Some(json!(1)), 17 | method: "test_method".to_string(), 18 | params: Some(json!({"key": "value"})), 19 | }; 20 | 21 | let a2a_message = converter.rmcp_to_a2a_request(&rmcp_request).unwrap(); 22 | 23 | assert_eq!(a2a_message.role, "user"); 24 | assert_eq!(a2a_message.parts.len(), 2); 25 | 26 | // Check text part 27 | match &a2a_message.parts[0] { 28 | MessagePart::Text { text } => { 29 | assert!(text.contains("test_method")); 30 | }, 31 | _ => panic!("Expected text part"), 32 | } 33 | 34 | // Check data part 35 | match &a2a_message.parts[1] { 36 | MessagePart::Data { data, .. } => { 37 | assert_eq!(data["key"], "value"); 38 | }, 39 | _ => panic!("Expected data part"), 40 | } 41 | } 42 | 43 | #[test] 44 | fn test_a2a_to_rmcp_response() { 45 | let converter = MessageConverter::new(); 46 | 47 | let a2a_message = Message { 48 | role: "agent".to_string(), 49 | parts: vec![ 50 | MessagePart::Text { text: "Test response".to_string() }, 51 | MessagePart::Data { 52 | data: json!({"result": "success"}), 53 | mime_type: Some("application/json".to_string()), 54 | }, 55 | ], 56 | }; 57 | 58 | let id = Some(json!(123)); 59 | let rmcp_response = converter.a2a_to_rmcp_response(&a2a_message, id.clone()).unwrap(); 60 | 61 | assert_eq!(rmcp_response.jsonrpc, "2.0"); 62 | assert_eq!(rmcp_response.id, id); 63 | assert!(rmcp_response.error.is_none()); 64 | 65 | // Data part should be prioritized over text 66 | assert_eq!(rmcp_response.result.unwrap()["result"], "success"); 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod adapter_tests { 72 | use crate::adapter::{ToolToAgentAdapter, AgentToToolAdapter}; 73 | use rmcp::{Tool, ToolCall}; 74 | use serde_json::json; 75 | 76 | #[test] 77 | fn test_tool_to_agent_adapter() { 78 | let tools = vec![ 79 | Tool { 80 | name: "test_tool".to_string(), 81 | description: "A test tool".to_string(), 82 | parameters: None, 83 | }, 84 | ]; 85 | 86 | let adapter = ToolToAgentAdapter::new( 87 | tools, 88 | "Test Agent".to_string(), 89 | "An agent for testing".to_string(), 90 | ); 91 | 92 | let agent_card = adapter.generate_agent_card(); 93 | 94 | assert_eq!(agent_card.name, "Test Agent"); 95 | assert_eq!(agent_card.description, "An agent for testing"); 96 | assert_eq!(agent_card.skills.len(), 1); 97 | assert_eq!(agent_card.skills[0].name, "test_tool"); 98 | } 99 | 100 | #[test] 101 | fn test_tool_call_to_task() { 102 | let tools = vec![ 103 | Tool { 104 | name: "test_tool".to_string(), 105 | description: "A test tool".to_string(), 106 | parameters: None, 107 | }, 108 | ]; 109 | 110 | let adapter = ToolToAgentAdapter::new( 111 | tools, 112 | "Test Agent".to_string(), 113 | "An agent for testing".to_string(), 114 | ); 115 | 116 | let tool_call = ToolCall { 117 | method: "test_tool".to_string(), 118 | params: json!({"input": "test_input"}), 119 | }; 120 | 121 | let task = adapter.tool_call_to_task(&tool_call).unwrap(); 122 | 123 | assert_eq!(task.status.state, a2a_rs::domain::task::TaskState::Submitted); 124 | assert_eq!(task.messages.len(), 1); 125 | assert_eq!(task.messages[0].role, "user"); 126 | } 127 | } -------------------------------------------------------------------------------- /a2a-mcp/src/transport/a2a_to_rmcp.rs: -------------------------------------------------------------------------------- 1 | //! Transport adapter that converts A2A transport to RMCP transport 2 | 3 | use crate::error::Result; 4 | use crate::message::MessageConverter; 5 | use a2a_rs::domain::{message::Message, task::Task}; 6 | use rmcp::{ToolCall, ToolResponse}; 7 | use async_trait::async_trait; 8 | use std::sync::Arc; 9 | 10 | /// Transport adapter that bridges A2A to RMCP 11 | pub struct A2aToRmcpTransport { 12 | converter: Arc, 13 | } 14 | 15 | impl A2aToRmcpTransport { 16 | /// Create a new A2A to RMCP transport adapter 17 | pub fn new(converter: Arc) -> Self { 18 | Self { converter } 19 | } 20 | 21 | /// Convert A2A message to RMCP tool call 22 | pub async fn convert_message_to_tool_call(&self, msg: &Message, method: &str) -> Result { 23 | // Extract parameters from message 24 | let params = msg.parts.iter() 25 | .find_map(|part| { 26 | if let a2a_rs::domain::message::MessagePart::Data { data, .. } = part { 27 | Some(data.clone()) 28 | } else { 29 | None 30 | } 31 | }) 32 | .unwrap_or(serde_json::Value::Null); 33 | 34 | Ok(ToolCall { 35 | method: method.to_string(), 36 | params, 37 | }) 38 | } 39 | 40 | /// Convert RMCP tool response to A2A message 41 | pub async fn convert_tool_response_to_message(&self, resp: &ToolResponse) -> Result { 42 | let mut parts = Vec::new(); 43 | 44 | // Add data part with response result 45 | parts.push(a2a_rs::domain::message::MessagePart::Data { 46 | data: resp.result.clone(), 47 | mime_type: Some("application/json".to_string()), 48 | }); 49 | 50 | // If the result is a string, also add it as text 51 | if let serde_json::Value::String(text) = &resp.result { 52 | parts.push(a2a_rs::domain::message::MessagePart::Text { 53 | text: text.clone() 54 | }); 55 | } 56 | 57 | Ok(Message { 58 | role: "agent".to_string(), 59 | parts, 60 | }) 61 | } 62 | } 63 | 64 | /// Trait for handling A2A to RMCP message conversion 65 | #[async_trait] 66 | pub trait A2aToRmcpHandler { 67 | /// Process an A2A task as RMCP tool calls 68 | async fn process_a2a_task(&self, task: &Task) -> Result; 69 | } -------------------------------------------------------------------------------- /a2a-mcp/src/transport/mod.rs: -------------------------------------------------------------------------------- 1 | //! Transport adapters for A2A and RMCP protocols 2 | 3 | mod rmcp_to_a2a; 4 | mod a2a_to_rmcp; 5 | 6 | pub use rmcp_to_a2a::RmcpToA2aTransport; 7 | pub use a2a_to_rmcp::A2aToRmcpTransport; -------------------------------------------------------------------------------- /a2a-mcp/src/transport/rmcp_to_a2a.rs: -------------------------------------------------------------------------------- 1 | //! Transport adapter that converts RMCP transport to A2A transport 2 | 3 | use crate::error::Result; 4 | use crate::message::MessageConverter; 5 | use a2a_rs::domain::{message::Message, task::Task}; 6 | use rmcp::{ClientJsonRpcMessage, ServerJsonRpcMessage}; 7 | use async_trait::async_trait; 8 | use std::sync::Arc; 9 | 10 | /// Transport adapter that bridges RMCP to A2A 11 | pub struct RmcpToA2aTransport { 12 | converter: Arc, 13 | } 14 | 15 | impl RmcpToA2aTransport { 16 | /// Create a new RMCP to A2A transport adapter 17 | pub fn new(converter: Arc) -> Self { 18 | Self { converter } 19 | } 20 | 21 | /// Convert RMCP request to A2A task 22 | pub async fn convert_request(&self, req: &ClientJsonRpcMessage, task_id: &str) -> Result { 23 | let message = self.converter.rmcp_to_a2a_request(req)?; 24 | 25 | // Create a new task with the converted message 26 | let task = Task { 27 | id: task_id.to_string(), 28 | status: a2a_rs::domain::task::TaskStatus { 29 | state: a2a_rs::domain::task::TaskState::Submitted, 30 | message: Some("Task submitted".to_string()), 31 | }, 32 | messages: vec![message], 33 | artifacts: Vec::new(), 34 | history_ttl: Some(3600), // 1 hour default 35 | metadata: None, 36 | }; 37 | 38 | Ok(task) 39 | } 40 | 41 | /// Convert A2A response to RMCP response 42 | pub async fn convert_response(&self, task: &Task, id: Option) -> Result { 43 | // Get the last agent message 44 | let agent_message = self.converter.extract_agent_message(task)?; 45 | 46 | // Convert to RMCP response 47 | self.converter.a2a_to_rmcp_response(agent_message, id) 48 | } 49 | } 50 | 51 | /// Trait for handling RMCP to A2A message conversion 52 | #[async_trait] 53 | pub trait RmcpToA2aHandler { 54 | /// Process an RMCP request as an A2A task 55 | async fn process_rmcp_request(&self, req: ClientJsonRpcMessage) -> Result; 56 | } -------------------------------------------------------------------------------- /a2a-mcp/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for a2a-mcp integration 2 | 3 | use crate::error::Result; 4 | 5 | /// Ensures that a URL is properly formatted 6 | pub fn validate_url(url: &str) -> Result { 7 | let parsed = url::Url::parse(url) 8 | .map_err(|e| crate::error::Error::Translation(format!("Invalid URL: {}", e)))?; 9 | 10 | // Ensure URL has a scheme and host 11 | if parsed.scheme().is_empty() || parsed.host_str().is_none() { 12 | return Err(crate::error::Error::Translation("URL must have a scheme and host".into())); 13 | } 14 | 15 | // Normalize URL 16 | Ok(parsed.to_string()) 17 | } 18 | 19 | /// Extracts tool name from an RMCP method string 20 | /// For example: "https://example.com/agent:toolName" -> "toolName" 21 | pub fn extract_tool_name(method: &str) -> Option { 22 | method.split(':').last().map(|s| s.to_string()) 23 | } 24 | 25 | /// Extracts agent URL from an RMCP method string 26 | /// For example: "https://example.com/agent:toolName" -> "https://example.com/agent" 27 | pub fn extract_agent_url(method: &str) -> Option { 28 | method.split(':').next().map(|s| s.to_string()) 29 | } 30 | 31 | /// Normalizes a task ID to ensure it's valid 32 | pub fn normalize_task_id(task_id: &str) -> String { 33 | // If task_id is not a valid UUID, generate a new one 34 | if uuid::Uuid::parse_str(task_id).is_err() { 35 | return uuid::Uuid::new_v4().to_string(); 36 | } 37 | task_id.to_string() 38 | } -------------------------------------------------------------------------------- /a2a-rs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /a2a-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "a2a-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Emil Lindfors "] 6 | description = "Rust implementation of the Agent-to-Agent (A2A) Protocol" 7 | license = "MIT" 8 | repository = "https://github.com/emillindfors/a2a-rs" 9 | readme = "README.md" 10 | keywords = ["agent", "protocol", "jsonrpc", "a2a"] 11 | categories = ["api-bindings", "network-programming"] 12 | 13 | [dependencies] 14 | # Core dependencies 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | chrono = { version = "0.4", features = ["serde"] } 18 | thiserror = "1.0" 19 | uuid = { version = "1.4", features = ["v4", "serde"] } 20 | base64 = "0.21" 21 | url = { version = "2.4", features = ["serde"] } 22 | bon = "2.3" 23 | 24 | # Database - optional 25 | sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "chrono", "uuid", "json"], optional = true } 26 | 27 | # Async foundation - optional 28 | tokio = { version = "1.32", features = ["rt", "rt-multi-thread", "macros", "net", "io-util", "sync", "time"], optional = true } 29 | async-trait = { version = "0.1", optional = true } 30 | futures = { version = "0.3", optional = true } 31 | 32 | # HTTP client - optional 33 | reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false, optional = true } 34 | 35 | # WebSocket - optional 36 | tokio-tungstenite = { version = "0.20", features = ["rustls", "connect", "stream", "handshake"], default-features = false, optional = true } 37 | 38 | # HTTP server - optional 39 | axum = { version = "0.8", optional = true } 40 | 41 | # Authentication - optional 42 | jsonwebtoken = { version = "9.3", optional = true } 43 | oauth2 = { version = "4.4", optional = true } 44 | openidconnect = { version = "3.5", optional = true } 45 | 46 | # Logging - optional 47 | tracing = { version = "0.1", optional = true } 48 | tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"], optional = true } 49 | 50 | [dev-dependencies] 51 | # Testing dependencies 52 | proptest = "1.4" 53 | proptest-derive = "0.5" 54 | jsonschema = "0.22" 55 | criterion = { version = "0.5", features = ["html_reports"] } 56 | arbitrary = { version = "1.3", features = ["derive"] } 57 | 58 | [features] 59 | default = ["server", "tracing"] 60 | client = ["dep:tokio", "dep:async-trait", "dep:futures"] 61 | http-client = ["client", "dep:reqwest"] 62 | ws-client = ["client", "dep:tokio-tungstenite"] 63 | server = ["dep:tokio", "dep:async-trait", "dep:futures"] 64 | http-server = ["server", "dep:axum"] 65 | tracing = ["dep:tracing", "dep:tracing-subscriber"] 66 | ws-server = ["server", "dep:tokio-tungstenite"] 67 | auth = ["dep:jsonwebtoken", "dep:oauth2", "dep:openidconnect", "dep:reqwest"] 68 | sqlx-storage = ["server", "dep:sqlx"] 69 | sqlite = ["sqlx-storage", "sqlx/sqlite"] 70 | postgres = ["sqlx-storage", "sqlx/postgres"] 71 | mysql = ["sqlx-storage", "sqlx/mysql"] 72 | full = ["http-client", "ws-client", "http-server", "ws-server", "tracing", "auth", "sqlite", "postgres"] 73 | 74 | 75 | [[example]] 76 | name = "http_client_server" 77 | path = "examples/http_client_server.rs" 78 | required-features = ["http-server", "http-client"] 79 | 80 | [[example]] 81 | name = "websocket_client_server" 82 | path = "examples/websocket_client_server.rs" 83 | required-features = ["ws-server", "ws-client"] 84 | 85 | [[example]] 86 | name = "sqlx_storage_demo" 87 | path = "examples/sqlx_storage_demo.rs" 88 | required-features = ["sqlx-storage", "tracing"] 89 | 90 | [[example]] 91 | name = "storage_comparison" 92 | path = "examples/storage_comparison.rs" 93 | required-features = ["server", "tracing"] 94 | 95 | [[bench]] 96 | name = "a2a_performance" 97 | harness = false 98 | required-features = ["full"] 99 | -------------------------------------------------------------------------------- /a2a-rs/README.md: -------------------------------------------------------------------------------- 1 | # a2a-rs 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/a2a-rs.svg)](https://crates.io/crates/a2a-rs) 4 | [![Documentation](https://docs.rs/a2a-rs/badge.svg)](https://docs.rs/a2a-rs) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | A Rust implementation of the Agent-to-Agent (A2A) Protocol, providing a type-safe, idiomatic way to build agent communication systems. 8 | 9 | ## Features 10 | 11 | - 🚀 **Complete A2A Protocol Implementation** - Full support for the A2A specification 12 | - 🔄 **Multiple Transport Options** - HTTP and WebSocket support 13 | - 📡 **Streaming Updates** - Real-time task and artifact updates 14 | - 🔐 **Authentication & Security** - JWT, OAuth2, OpenID Connect support 15 | - 💾 **Persistent Storage** - SQLx integration for task persistence 16 | - 🎯 **Async-First Design** - Built on Tokio with async/await throughout 17 | - 🧩 **Modular Architecture** - Use only the features you need 18 | - ✅ **Type Safety** - Leverages Rust's type system for protocol compliance 19 | 20 | ## Quick Start 21 | 22 | Add to your `Cargo.toml`: 23 | 24 | ```toml 25 | [dependencies] 26 | a2a-rs = "0.1.0" 27 | 28 | # For HTTP client 29 | a2a-rs = { version = "0.1.0", features = ["http-client"] } 30 | 31 | # For HTTP server 32 | a2a-rs = { version = "0.1.0", features = ["http-server"] } 33 | 34 | # Full feature set 35 | a2a-rs = { version = "0.1.0", features = ["full"] } 36 | ``` 37 | 38 | ### Client Example 39 | 40 | ```rust 41 | use a2a_rs::{HttpClient, Message}; 42 | 43 | #[tokio::main] 44 | async fn main() -> Result<(), Box> { 45 | let client = HttpClient::new("https://api.example.com".to_string()); 46 | 47 | let message = Message::user_text("Hello, agent!".to_string()); 48 | let task = client.send_task_message("task-123", &message, None, None).await?; 49 | 50 | println!("Task created: {:?}", task); 51 | Ok(()) 52 | } 53 | ``` 54 | 55 | ### Server Example 56 | 57 | ```rust 58 | use a2a_rs::{HttpServer, Message, Task, A2AError}; 59 | use a2a_rs::port::{AsyncTaskHandler, AgentInfoProvider}; 60 | 61 | struct MyAgent; 62 | 63 | #[async_trait::async_trait] 64 | impl AsyncTaskHandler for MyAgent { 65 | async fn handle_message( 66 | &self, 67 | task_id: &str, 68 | message: &Message, 69 | session_id: Option<&str>, 70 | ) -> Result { 71 | // Process the message and return updated task 72 | Ok(Task::new(task_id.to_string())) 73 | } 74 | } 75 | 76 | #[tokio::main] 77 | async fn main() -> Result<(), Box> { 78 | let server = HttpServer::new( 79 | MyAgent, 80 | AgentInfo::default(), 81 | "127.0.0.1:8080".to_string(), 82 | ); 83 | 84 | server.start().await?; 85 | Ok(()) 86 | } 87 | ``` 88 | 89 | ## Architecture 90 | 91 | This library follows a hexagonal architecture pattern: 92 | 93 | - **Domain**: Core business logic and types 94 | - **Ports**: Trait definitions for external dependencies 95 | - **Adapters**: Concrete implementations for different transports and storage 96 | 97 | ## Feature Flags 98 | 99 | - `client` - Client-side functionality 100 | - `server` - Server-side functionality 101 | - `http-client` - HTTP client implementation 102 | - `http-server` - HTTP server implementation 103 | - `ws-client` - WebSocket client implementation 104 | - `ws-server` - WebSocket server implementation 105 | - `auth` - Authentication support (JWT, OAuth2, OpenID Connect) 106 | - `sqlx-storage` - SQLx-based persistent storage 107 | - `sqlite` - SQLite database support 108 | - `postgres` - PostgreSQL database support 109 | - `mysql` - MySQL database support 110 | - `tracing` - Structured logging and tracing 111 | - `full` - All features enabled 112 | 113 | ## Examples 114 | 115 | See the [examples](examples/) directory for complete working examples: 116 | 117 | - [HTTP Client/Server](examples/http_client_server.rs) 118 | - [WebSocket Client/Server](examples/websocket_client_server.rs) 119 | - [SQLx Storage Demo](examples/sqlx_storage_demo.rs) 120 | - [Storage Comparison](examples/storage_comparison.rs) 121 | 122 | ## Documentation 123 | 124 | Full API documentation is available on [docs.rs](https://docs.rs/a2a-rs). 125 | 126 | ## License 127 | 128 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 129 | 130 | ## Contributing 131 | 132 | Contributions are welcome! Please feel free to submit a Pull Request. -------------------------------------------------------------------------------- /a2a-rs/examples/builder_patterns.rs: -------------------------------------------------------------------------------- 1 | //! Example demonstrating the new builder patterns for Message and Task 2 | 3 | use a2a_rs::domain::{Message, Part, Role, Task, TaskState, TaskStatus}; 4 | use uuid::Uuid; 5 | 6 | fn main() -> Result<(), Box> { 7 | println!("=== A2A Builder Patterns Demo ===\n"); 8 | 9 | // 1. Building Messages with the new builder pattern 10 | println!("1. Building a Message with the builder pattern:"); 11 | 12 | let message_id = format!("msg-{}", Uuid::new_v4()); 13 | let message = Message::builder() 14 | .role(Role::User) 15 | .message_id(message_id.clone()) 16 | .parts(vec![ 17 | Part::text("Hello, agent!".to_string()), 18 | Part::data(serde_json::Map::new()), 19 | ]) 20 | .task_id("task-123".to_string()) 21 | .build(); 22 | 23 | // Validate the message 24 | message.validate()?; 25 | println!( 26 | " ✓ Built and validated message with ID: {}", 27 | message.message_id 28 | ); 29 | println!(" ✓ Message has {} parts", message.parts.len()); 30 | 31 | // 2. Building a more complex message with file part 32 | println!("\n2. Building a Message with file part:"); 33 | 34 | let file_part = Part::file_builder() 35 | .name("example.txt".to_string()) 36 | .mime_type("text/plain".to_string()) 37 | .bytes("SGVsbG8gV29ybGQ=".to_string()) // "Hello World" in base64 38 | .build()?; 39 | 40 | let complex_message_id = format!("msg-{}", Uuid::new_v4()); 41 | let complex_message = Message::builder() 42 | .role(Role::Agent) 43 | .message_id(complex_message_id.clone()) 44 | .parts(vec![ 45 | Part::text("Here's a file for you:".to_string()), 46 | file_part, 47 | ]) 48 | .context_id("conversation-456".to_string()) 49 | .build(); 50 | 51 | complex_message.validate()?; 52 | println!(" ✓ Built complex message with file attachment"); 53 | println!(" ✓ Message ID: {}", complex_message.message_id); 54 | 55 | // 3. Building Tasks with the builder pattern 56 | println!("\n3. Building a Task with the builder pattern:"); 57 | 58 | let task_id = format!("task-{}", Uuid::new_v4()); 59 | let context_id = format!("ctx-{}", Uuid::new_v4()); 60 | 61 | let task = Task::builder() 62 | .id(task_id.clone()) 63 | .context_id(context_id.clone()) 64 | .history(vec![message, complex_message]) 65 | .build(); 66 | 67 | task.validate()?; 68 | println!(" ✓ Built and validated task with ID: {}", task.id); 69 | println!( 70 | " ✓ Task has {} messages in history", 71 | task.history.as_ref().unwrap().len() 72 | ); 73 | println!(" ✓ Task status: {:?}", task.status.state); 74 | 75 | // 4. Building a Task with custom status 76 | println!("\n4. Building a Task with custom status:"); 77 | 78 | let custom_task_id = format!("task-{}", Uuid::new_v4()); 79 | let working_message_id = format!("msg-{}", Uuid::new_v4()); 80 | let status_message = Message::builder() 81 | .role(Role::Agent) 82 | .message_id(working_message_id) 83 | .parts(vec![Part::text("I'm working on this task...".to_string())]) 84 | .build(); 85 | 86 | let working_task = Task::builder() 87 | .id(custom_task_id.clone()) 88 | .context_id(context_id.clone()) 89 | .status(TaskStatus { 90 | state: TaskState::Working, 91 | message: Some(status_message.clone()), 92 | timestamp: Some(chrono::Utc::now()), 93 | }) 94 | .history(vec![status_message]) 95 | .build(); 96 | 97 | working_task.validate()?; 98 | println!(" ✓ Built working task with custom status"); 99 | println!(" ✓ Task status: {:?}", working_task.status.state); 100 | println!( 101 | " ✓ Status message: {:?}", 102 | working_task.status.message.as_ref().unwrap().parts[0] 103 | ); 104 | 105 | // 5. Demonstrating builder flexibility with metadata 106 | println!("\n5. Building with metadata:"); 107 | 108 | let mut metadata = serde_json::Map::new(); 109 | metadata.insert( 110 | "priority".to_string(), 111 | serde_json::Value::String("high".to_string()), 112 | ); 113 | metadata.insert( 114 | "category".to_string(), 115 | serde_json::Value::String("support".to_string()), 116 | ); 117 | 118 | let metadata_message_id = format!("msg-{}", Uuid::new_v4()); 119 | let metadata_message = Message::builder() 120 | .role(Role::User) 121 | .message_id(metadata_message_id) 122 | .parts(vec![Part::text( 123 | "This is a high priority support request".to_string(), 124 | )]) 125 | .metadata(metadata.clone()) 126 | .reference_task_ids(vec![ 127 | "related-task-1".to_string(), 128 | "related-task-2".to_string(), 129 | ]) 130 | .build(); 131 | 132 | metadata_message.validate()?; 133 | println!(" ✓ Built message with metadata and references"); 134 | println!( 135 | " ✓ Metadata keys: {:?}", 136 | metadata_message 137 | .metadata 138 | .as_ref() 139 | .unwrap() 140 | .keys() 141 | .collect::>() 142 | ); 143 | println!( 144 | " ✓ Referenced tasks: {:?}", 145 | metadata_message.reference_task_ids.as_ref().unwrap() 146 | ); 147 | 148 | println!("\n🎉 All builder patterns work correctly!"); 149 | Ok(()) 150 | } 151 | -------------------------------------------------------------------------------- /a2a-rs/examples/common/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common utilities for examples 2 | 3 | pub mod simple_agent_handler; 4 | 5 | pub use simple_agent_handler::SimpleAgentHandler; 6 | -------------------------------------------------------------------------------- /a2a-rs/examples/http_client_server.rs: -------------------------------------------------------------------------------- 1 | //! A complete HTTP example that runs both server and client together 2 | 3 | use std::time::Duration; 4 | use tokio::time::sleep; 5 | 6 | use a2a_rs::adapter::{ 7 | BearerTokenAuthenticator, DefaultRequestProcessor, HttpClient, HttpServer, InMemoryTaskStorage, 8 | NoopPushNotificationSender, SimpleAgentInfo, 9 | }; 10 | 11 | mod common; 12 | use a2a_rs::domain::{Message, Part, Role}; 13 | use a2a_rs::observability; 14 | use a2a_rs::services::AsyncA2AClient; 15 | use common::SimpleAgentHandler; 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | // Initialize tracing for better observability 20 | observability::init_tracing(); 21 | 22 | println!("🚀 Starting HTTP Full Example"); 23 | println!("=============================="); 24 | 25 | // Start the server in a background task 26 | let server_handle = tokio::spawn(async { 27 | run_server().await.expect("Server failed"); 28 | }); 29 | 30 | // Give the server time to start 31 | sleep(Duration::from_millis(500)).await; 32 | 33 | // Run the client 34 | match run_client().await { 35 | Ok(_) => println!("✅ Client completed successfully"), 36 | Err(e) => println!("❌ Client failed: {}", e), 37 | } 38 | 39 | // Let the server run a bit longer 40 | sleep(Duration::from_millis(1000)).await; 41 | 42 | // Abort the server 43 | server_handle.abort(); 44 | 45 | println!("🏁 HTTP Full Example completed"); 46 | Ok(()) 47 | } 48 | 49 | async fn run_server() -> Result<(), Box> { 50 | println!("🌐 Starting HTTP server..."); 51 | 52 | // Create server components 53 | let push_sender = NoopPushNotificationSender; 54 | let storage = InMemoryTaskStorage::with_push_sender(push_sender); 55 | let handler = SimpleAgentHandler::with_storage(storage); 56 | let test_agent_info = SimpleAgentInfo::new( 57 | "test-agent".to_string(), 58 | "http://localhost:8080".to_string(), 59 | ); 60 | let processor = DefaultRequestProcessor::with_handler(handler, test_agent_info); 61 | 62 | // Create agent info 63 | let agent_info = SimpleAgentInfo::new( 64 | "Example A2A Agent".to_string(), 65 | "http://localhost:8080".to_string(), 66 | ) 67 | .with_description("An example A2A agent using the a2a-protocol crate".to_string()) 68 | .with_provider( 69 | "Example Organization".to_string(), 70 | "https://example.org".to_string(), 71 | ) 72 | .with_documentation_url("https://example.org/docs".to_string()) 73 | .with_streaming() 74 | .add_comprehensive_skill( 75 | "echo".to_string(), 76 | "Echo Skill".to_string(), 77 | Some("Echoes back the user's message".to_string()), 78 | Some(vec!["echo".to_string(), "respond".to_string()]), 79 | Some(vec!["Echo: Hello World".to_string()]), 80 | Some(vec!["text".to_string()]), 81 | Some(vec!["text".to_string()]), 82 | ); 83 | 84 | // Server with bearer token authentication 85 | let tokens = vec!["secret-token".to_string()]; 86 | let authenticator = BearerTokenAuthenticator::new(tokens); 87 | let server = HttpServer::with_auth( 88 | processor, 89 | agent_info, 90 | "127.0.0.1:8080".to_string(), 91 | authenticator, 92 | ); 93 | 94 | println!("🔗 HTTP server listening on http://127.0.0.1:8080"); 95 | server 96 | .start() 97 | .await 98 | .map_err(|e| Box::new(e) as Box) 99 | } 100 | 101 | async fn run_client() -> Result<(), Box> { 102 | println!("📱 Starting HTTP client..."); 103 | 104 | // Create HTTP client with authentication 105 | let client = HttpClient::with_auth( 106 | "http://127.0.0.1:8080".to_string(), 107 | "secret-token".to_string(), 108 | ); 109 | 110 | // Note: HTTP client communicates via JSON-RPC, not direct REST endpoints 111 | println!("📋 HTTP client connected successfully"); 112 | 113 | // Test 3: Create and send message to task 114 | println!("📨 Testing task creation and messaging..."); 115 | 116 | let task_id = uuid::Uuid::new_v4().to_string(); 117 | let task_id = format!("task-{}", task_id); 118 | 119 | let message = Message::builder() 120 | .role(Role::User) 121 | .parts(vec![Part::text( 122 | "Hello from HTTP client! Please echo this message.".to_string(), 123 | )]) 124 | .message_id(uuid::Uuid::new_v4().to_string()) 125 | .build(); 126 | 127 | match client 128 | .send_task_message(&task_id, &message, None, None) 129 | .await 130 | { 131 | Ok(response) => { 132 | println!("✅ Task created with ID: {}", task_id); 133 | println!(" Status: {:?}", response.status.state); 134 | } 135 | Err(e) => { 136 | println!("❌ Failed to send message: {}", e); 137 | return Err(e.into()); 138 | } 139 | } 140 | 141 | // Test 4: Get task back 142 | println!("📤 Testing task retrieval..."); 143 | match client.get_task(&task_id, None).await { 144 | Ok(task) => { 145 | println!("✅ Retrieved task: {}", task.id); 146 | println!(" Status: {:?}", task.status.state); 147 | if let Some(history) = &task.history { 148 | println!(" History entries: {}", history.len()); 149 | } 150 | } 151 | Err(e) => { 152 | println!("❌ Failed to get task: {}", e); 153 | return Err(e.into()); 154 | } 155 | } 156 | 157 | // Test 5: Cancel task 158 | println!("🛑 Testing task cancellation..."); 159 | match client.cancel_task(&task_id).await { 160 | Ok(task) => { 161 | println!("✅ Task canceled: {}", task.id); 162 | println!(" Final status: {:?}", task.status.state); 163 | } 164 | Err(e) => { 165 | println!("❌ Failed to cancel task: {}", e); 166 | return Err(e.into()); 167 | } 168 | } 169 | 170 | println!("🎉 All HTTP client tests passed!"); 171 | Ok(()) 172 | } 173 | -------------------------------------------------------------------------------- /a2a-rs/examples/sqlx_storage_demo.rs: -------------------------------------------------------------------------------- 1 | //! Example demonstrating SQLx-based persistent storage 2 | //! 3 | //! This example shows how to use the SqlxTaskStorage with different databases: 4 | //! - SQLite (file and in-memory) 5 | //! - PostgreSQL 6 | //! 7 | //! Run with: 8 | //! ```bash 9 | //! # SQLite in-memory (default) 10 | //! cargo run --example sqlx_storage_demo --features sqlite 11 | //! 12 | //! # SQLite file 13 | //! DATABASE_URL=sqlite:tasks.db cargo run --example sqlx_storage_demo --features sqlite 14 | //! 15 | //! # PostgreSQL (requires running PostgreSQL server) 16 | //! DATABASE_URL=postgres://user:password@localhost/a2a_test cargo run --example sqlx_storage_demo --features postgres 17 | //! ``` 18 | 19 | #[cfg(feature = "sqlx-storage")] 20 | use a2a_rs::adapter::storage::{DatabaseConfig, SqlxTaskStorage}; 21 | #[cfg(feature = "sqlx-storage")] 22 | use a2a_rs::domain::TaskState; 23 | #[cfg(feature = "sqlx-storage")] 24 | use a2a_rs::port::AsyncTaskManager; 25 | 26 | #[cfg(feature = "sqlx-storage")] 27 | #[tokio::main] 28 | async fn main() -> Result<(), Box> { 29 | // Initialize tracing 30 | tracing_subscriber::fmt::init(); 31 | 32 | println!("🗃️ SQLx Storage Demo"); 33 | println!("====================="); 34 | 35 | // Get database configuration from environment or use default 36 | let config = match DatabaseConfig::from_env() { 37 | Ok(config) => { 38 | println!("📡 Using database configuration from environment"); 39 | config 40 | } 41 | Err(_) => { 42 | println!("📋 Using default configuration (SQLite in-memory)"); 43 | DatabaseConfig::default() 44 | } 45 | }; 46 | 47 | // Validate configuration 48 | config 49 | .validate() 50 | .map_err(|e| format!("Invalid database configuration: {}", e))?; 51 | 52 | println!("🔧 Database type: {}", config.database_type()); 53 | println!("🔗 Database URL: {}", config.url); 54 | println!("📊 Max connections: {}", config.max_connections); 55 | println!(); 56 | 57 | // Create storage instance 58 | println!("🚀 Initializing SQLx storage..."); 59 | let storage = SqlxTaskStorage::new(&config.url).await?; 60 | println!("✅ Storage initialized successfully"); 61 | println!(); 62 | 63 | // Demo: Create tasks 64 | println!("📝 Creating tasks..."); 65 | let task_ids = vec!["demo-task-1", "demo-task-2", "demo-task-3"]; 66 | 67 | for task_id in &task_ids { 68 | let task = storage.create_task(task_id, "demo-context").await?; 69 | println!( 70 | " ✓ Created task: {} (status: {:?})", 71 | task.id, task.status.state 72 | ); 73 | } 74 | println!(); 75 | 76 | // Demo: Update task statuses 77 | println!("🔄 Updating task statuses..."); 78 | storage 79 | .update_task_status("demo-task-1", TaskState::Working, None) 80 | .await?; 81 | println!(" ✓ Updated demo-task-1 to Working"); 82 | 83 | storage 84 | .update_task_status("demo-task-2", TaskState::Working, None) 85 | .await?; 86 | storage 87 | .update_task_status("demo-task-2", TaskState::Completed, None) 88 | .await?; 89 | println!(" ✓ Updated demo-task-2 to Working, then Completed"); 90 | 91 | storage 92 | .update_task_status("demo-task-3", TaskState::Working, None) 93 | .await?; 94 | println!(" ✓ Updated demo-task-3 to Working"); 95 | println!(); 96 | 97 | // Demo: Cancel a task 98 | println!("❌ Canceling a task..."); 99 | let canceled_task = storage.cancel_task("demo-task-3").await?; 100 | println!( 101 | " ✓ Canceled task: {} (status: {:?})", 102 | canceled_task.id, canceled_task.status.state 103 | ); 104 | println!(); 105 | 106 | // Demo: Retrieve tasks and show history 107 | println!("📖 Retrieving tasks with history..."); 108 | for task_id in &task_ids { 109 | let task = storage.get_task(task_id, Some(10)).await?; 110 | println!(" 📋 Task: {} (status: {:?})", task.id, task.status.state); 111 | 112 | if let Some(history) = &task.history { 113 | println!(" History entries: {}", history.len()); 114 | 115 | for (i, message) in history.iter().enumerate() { 116 | println!(" {}. Message ID: {}", i + 1, message.message_id); 117 | } 118 | } else { 119 | println!(" No history available"); 120 | } 121 | println!(); 122 | } 123 | 124 | // Demo: Task existence checks 125 | println!("🔍 Checking task existence..."); 126 | for task_id in &task_ids { 127 | let exists = storage.task_exists(task_id).await?; 128 | println!(" {} exists: {}", task_id, exists); 129 | } 130 | 131 | let exists = storage.task_exists("non-existent-task").await?; 132 | println!(" non-existent-task exists: {}", exists); 133 | println!(); 134 | 135 | // Demo: Show configuration examples 136 | println!("📚 Available configuration examples:"); 137 | let examples = DatabaseConfig::examples(); 138 | for (name, example_config) in examples { 139 | println!(" {} -> {}", name, example_config.url); 140 | } 141 | println!(); 142 | 143 | println!("✅ Demo completed successfully!"); 144 | println!(); 145 | println!("💡 Tips:"); 146 | println!(" - Set DATABASE_URL environment variable to use a different database"); 147 | println!(" - Use 'sqlite:tasks.db' for persistent SQLite storage"); 148 | println!(" - Use 'postgres://user:pass@host/db' for PostgreSQL"); 149 | println!(" - Task history is automatically tracked in the database"); 150 | println!(" - The storage layer handles migrations automatically"); 151 | 152 | Ok(()) 153 | } 154 | 155 | #[cfg(not(feature = "sqlx-storage"))] 156 | fn main() { 157 | eprintln!("❌ This example requires the 'sqlx-storage' feature."); 158 | eprintln!("Run with: cargo run --example sqlx_storage_demo --features sqlite"); 159 | std::process::exit(1); 160 | } 161 | -------------------------------------------------------------------------------- /a2a-rs/migrations/001_initial_schema.sql: -------------------------------------------------------------------------------- 1 | -- Initial schema for A2A task storage 2 | -- This creates the core tables needed for persistent task storage 3 | 4 | -- Tasks table - stores main task information 5 | CREATE TABLE IF NOT EXISTS tasks ( 6 | id TEXT PRIMARY KEY, 7 | context_id TEXT NOT NULL, 8 | status_state TEXT NOT NULL CHECK (status_state IN ('submitted', 'working', 'input-required', 'completed', 'canceled', 'failed', 'rejected', 'auth-required', 'unknown')), 9 | status_message TEXT, 10 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (datetime('now')), 11 | updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (datetime('now')), 12 | metadata JSONB, 13 | artifacts JSONB 14 | ); 15 | 16 | -- Task history table - stores chronological task updates 17 | CREATE TABLE IF NOT EXISTS task_history ( 18 | id INTEGER PRIMARY KEY AUTOINCREMENT, 19 | task_id TEXT NOT NULL, 20 | timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (datetime('now')), 21 | status_state TEXT NOT NULL CHECK (status_state IN ('submitted', 'working', 'input-required', 'completed', 'canceled', 'failed', 'rejected', 'auth-required', 'unknown')), 22 | message JSONB, 23 | FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE 24 | ); 25 | 26 | -- Push notification configurations 27 | CREATE TABLE IF NOT EXISTS push_notification_configs ( 28 | task_id TEXT PRIMARY KEY, 29 | webhook_url TEXT NOT NULL, 30 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (datetime('now')), 31 | FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE 32 | ); 33 | 34 | -- Indexes for better query performance 35 | CREATE INDEX IF NOT EXISTS idx_tasks_context_id ON tasks(context_id); 36 | CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at); 37 | CREATE INDEX IF NOT EXISTS idx_tasks_status_state ON tasks(status_state); 38 | CREATE INDEX IF NOT EXISTS idx_task_history_task_id ON task_history(task_id); 39 | CREATE INDEX IF NOT EXISTS idx_task_history_timestamp ON task_history(timestamp); 40 | 41 | -- Trigger to automatically update the updated_at timestamp 42 | CREATE TRIGGER IF NOT EXISTS update_tasks_updated_at 43 | AFTER UPDATE ON tasks 44 | FOR EACH ROW 45 | BEGIN 46 | UPDATE tasks SET updated_at = datetime('now') WHERE id = NEW.id; 47 | END; -------------------------------------------------------------------------------- /a2a-rs/migrations/001_initial_schema_postgres.sql: -------------------------------------------------------------------------------- 1 | -- Initial schema for A2A task storage (PostgreSQL version) 2 | -- This creates the core tables needed for persistent task storage 3 | 4 | -- Tasks table - stores main task information 5 | CREATE TABLE IF NOT EXISTS tasks ( 6 | id TEXT PRIMARY KEY, 7 | context_id TEXT NOT NULL, 8 | status_state TEXT NOT NULL CHECK (status_state IN ('submitted', 'working', 'input-required', 'completed', 'canceled', 'failed', 'rejected', 'auth-required', 'unknown')), 9 | status_message TEXT, 10 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 11 | updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 12 | metadata JSONB, 13 | artifacts JSONB 14 | ); 15 | 16 | -- Task history table - stores chronological task updates 17 | CREATE TABLE IF NOT EXISTS task_history ( 18 | id SERIAL PRIMARY KEY, 19 | task_id TEXT NOT NULL, 20 | timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), 21 | status_state TEXT NOT NULL CHECK (status_state IN ('submitted', 'working', 'input-required', 'completed', 'canceled', 'failed', 'rejected', 'auth-required', 'unknown')), 22 | message JSONB, 23 | FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE 24 | ); 25 | 26 | -- Push notification configurations 27 | CREATE TABLE IF NOT EXISTS push_notification_configs ( 28 | task_id TEXT PRIMARY KEY, 29 | webhook_url TEXT NOT NULL, 30 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 31 | FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE 32 | ); 33 | 34 | -- Indexes for better query performance 35 | CREATE INDEX IF NOT EXISTS idx_tasks_context_id ON tasks(context_id); 36 | CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at); 37 | CREATE INDEX IF NOT EXISTS idx_tasks_status_state ON tasks(status_state); 38 | CREATE INDEX IF NOT EXISTS idx_task_history_task_id ON task_history(task_id); 39 | CREATE INDEX IF NOT EXISTS idx_task_history_timestamp ON task_history(timestamp); 40 | 41 | -- Function to automatically update the updated_at timestamp 42 | CREATE OR REPLACE FUNCTION update_updated_at_column() 43 | RETURNS TRIGGER AS $$ 44 | BEGIN 45 | NEW.updated_at = NOW(); 46 | RETURN NEW; 47 | END; 48 | $$ language 'plpgsql'; 49 | 50 | -- Trigger to automatically update the updated_at timestamp 51 | CREATE TRIGGER IF NOT EXISTS update_tasks_updated_at 52 | BEFORE UPDATE ON tasks 53 | FOR EACH ROW 54 | EXECUTE FUNCTION update_updated_at_column(); -------------------------------------------------------------------------------- /a2a-rs/src/adapter/auth/mod.rs: -------------------------------------------------------------------------------- 1 | //! Authentication adapter implementations 2 | 3 | #[cfg(any(feature = "http-server", feature = "ws-server"))] 4 | pub mod authenticator; 5 | 6 | #[cfg(feature = "auth")] 7 | pub mod jwt; 8 | 9 | #[cfg(feature = "auth")] 10 | pub mod oauth2; 11 | 12 | // Re-export authentication types 13 | #[cfg(any(feature = "http-server", feature = "ws-server"))] 14 | pub use authenticator::{ 15 | ApiKeyAuthenticator, ApiKeyExtractor, BearerTokenAuthenticator, BearerTokenExtractor, 16 | NoopAuthenticator, 17 | }; 18 | 19 | #[cfg(feature = "auth")] 20 | pub use jwt::{JwtAuthenticator, JwtExtractor}; 21 | 22 | #[cfg(feature = "auth")] 23 | pub use oauth2::{OAuth2Authenticator, OAuth2Extractor, OpenIdConnectAuthenticator}; 24 | 25 | #[cfg(feature = "http-server")] 26 | pub use authenticator::with_auth; 27 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/business/message_handler.rs: -------------------------------------------------------------------------------- 1 | //! Default message handler implementation 2 | 3 | use std::sync::Arc; 4 | 5 | use async_trait::async_trait; 6 | 7 | use crate::{ 8 | domain::{A2AError, Message, Task, TaskState}, 9 | port::{AsyncMessageHandler, AsyncTaskManager}, 10 | }; 11 | 12 | /// Default message handler that processes messages and delegates to task manager 13 | #[derive(Clone)] 14 | pub struct DefaultMessageHandler 15 | where 16 | T: AsyncTaskManager + Send + Sync + 'static, 17 | { 18 | /// Task manager for handling task operations 19 | task_manager: Arc, 20 | } 21 | 22 | impl DefaultMessageHandler 23 | where 24 | T: AsyncTaskManager + Send + Sync + 'static, 25 | { 26 | /// Create a new message handler with the given task manager 27 | pub fn new(task_manager: T) -> Self { 28 | Self { 29 | task_manager: Arc::new(task_manager), 30 | } 31 | } 32 | } 33 | 34 | #[async_trait] 35 | impl AsyncMessageHandler for DefaultMessageHandler 36 | where 37 | T: AsyncTaskManager + Send + Sync + 'static, 38 | { 39 | async fn process_message<'a>( 40 | &self, 41 | task_id: &'a str, 42 | message: &'a Message, 43 | session_id: Option<&'a str>, 44 | ) -> Result { 45 | // Check if task exists 46 | let task_exists = self.task_manager.task_exists(task_id).await?; 47 | 48 | if !task_exists { 49 | // Create a new task 50 | let context_id = session_id.unwrap_or("default"); 51 | self.task_manager.create_task(task_id, context_id).await?; 52 | } 53 | 54 | // First, update the task with the incoming message to add it to history 55 | self.task_manager 56 | .update_task_status(task_id, TaskState::Working, Some(message.clone())) 57 | .await?; 58 | 59 | // Create a simple echo response 60 | let response_message = Message::builder() 61 | .role(crate::domain::Role::Agent) 62 | .parts(vec![crate::domain::Part::text(format!( 63 | "Echo: {}", 64 | message 65 | .parts 66 | .iter() 67 | .filter_map(|p| match p { 68 | crate::domain::Part::Text { text, .. } => Some(text.as_str()), 69 | _ => None, 70 | }) 71 | .collect::>() 72 | .join(" ") 73 | ))]) 74 | .message_id(uuid::Uuid::new_v4().to_string()) 75 | .task_id(task_id.to_string()) 76 | .context_id(message.context_id.clone().unwrap_or_default()) 77 | .build(); 78 | 79 | // For the default handler, we'll add the response message to history but keep the task in Working state 80 | // Real agents would process the message and determine the appropriate final state 81 | let final_task = self 82 | .task_manager 83 | .update_task_status(task_id, TaskState::Working, Some(response_message)) 84 | .await?; 85 | 86 | Ok(final_task) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/business/mod.rs: -------------------------------------------------------------------------------- 1 | //! Business logic adapter implementations 2 | 3 | #[cfg(feature = "server")] 4 | pub mod agent_info; 5 | #[cfg(feature = "server")] 6 | pub mod message_handler; 7 | #[cfg(feature = "server")] 8 | pub mod push_notification; 9 | #[cfg(feature = "server")] 10 | pub mod request_processor; 11 | 12 | // Re-export business implementations 13 | #[cfg(feature = "server")] 14 | pub use agent_info::SimpleAgentInfo; 15 | #[cfg(feature = "server")] 16 | pub use message_handler::DefaultMessageHandler; 17 | #[cfg(all(feature = "server", feature = "http-client"))] 18 | pub use push_notification::HttpPushNotificationSender; 19 | #[cfg(feature = "server")] 20 | pub use push_notification::{ 21 | NoopPushNotificationSender, PushNotificationRegistry, PushNotificationSender, 22 | }; 23 | #[cfg(feature = "server")] 24 | pub use request_processor::DefaultRequestProcessor; 25 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/error/client.rs: -------------------------------------------------------------------------------- 1 | //! Error types for client adapters 2 | 3 | use crate::domain::A2AError; 4 | use std::io; 5 | use thiserror::Error; 6 | 7 | /// Error type for HTTP client adapter 8 | #[derive(Error, Debug)] 9 | #[cfg(feature = "http-client")] 10 | pub enum HttpClientError { 11 | /// Reqwest client error 12 | #[error("HTTP client error: {0}")] 13 | Reqwest(#[from] reqwest::Error), 14 | 15 | /// IO error during HTTP operations 16 | #[error("IO error: {0}")] 17 | Io(#[from] io::Error), 18 | 19 | /// Error during request processing 20 | #[error("Request error: {0}")] 21 | Request(String), 22 | 23 | /// Error with HTTP response 24 | #[error("Response error: {status} - {message}")] 25 | Response { status: u16, message: String }, 26 | 27 | /// Connection timeout 28 | #[error("Connection timeout")] 29 | Timeout, 30 | } 31 | 32 | /// Error type for WebSocket client adapter 33 | #[derive(Error, Debug)] 34 | #[cfg(feature = "ws-client")] 35 | pub enum WebSocketClientError { 36 | /// WebSocket connection error 37 | #[error("WebSocket connection error: {0}")] 38 | Connection(String), 39 | 40 | /// WebSocket message error 41 | #[error("WebSocket message error: {0}")] 42 | Message(String), 43 | 44 | /// IO error during WebSocket operations 45 | #[error("IO error: {0}")] 46 | Io(#[from] io::Error), 47 | 48 | /// WebSocket protocol error 49 | #[error("WebSocket protocol error: {0}")] 50 | Protocol(String), 51 | 52 | /// Connection timeout 53 | #[error("Connection timeout")] 54 | Timeout, 55 | 56 | /// Connection closed 57 | #[error("Connection closed")] 58 | Closed, 59 | } 60 | 61 | // Conversion from adapter errors to domain errors 62 | #[cfg(feature = "http-client")] 63 | impl From for A2AError { 64 | fn from(error: HttpClientError) -> Self { 65 | match error { 66 | HttpClientError::Reqwest(e) => A2AError::Internal(format!("HTTP client error: {}", e)), 67 | HttpClientError::Io(e) => A2AError::Io(e), 68 | HttpClientError::Request(msg) => { 69 | A2AError::Internal(format!("HTTP request error: {}", msg)) 70 | } 71 | HttpClientError::Response { status, message } => { 72 | A2AError::Internal(format!("HTTP response error: {} - {}", status, message)) 73 | } 74 | HttpClientError::Timeout => A2AError::Internal("HTTP request timeout".to_string()), 75 | } 76 | } 77 | } 78 | 79 | #[cfg(feature = "ws-client")] 80 | impl From for A2AError { 81 | fn from(error: WebSocketClientError) -> Self { 82 | match error { 83 | WebSocketClientError::Connection(msg) => { 84 | A2AError::Internal(format!("WebSocket connection error: {}", msg)) 85 | } 86 | WebSocketClientError::Message(msg) => { 87 | A2AError::Internal(format!("WebSocket message error: {}", msg)) 88 | } 89 | WebSocketClientError::Io(e) => A2AError::Io(e), 90 | WebSocketClientError::Protocol(msg) => { 91 | A2AError::Internal(format!("WebSocket protocol error: {}", msg)) 92 | } 93 | WebSocketClientError::Timeout => A2AError::Internal("WebSocket timeout".to_string()), 94 | WebSocketClientError::Closed => { 95 | A2AError::Internal("WebSocket connection closed".to_string()) 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/error/mod.rs: -------------------------------------------------------------------------------- 1 | //! Error types for adapter implementations 2 | 3 | #[cfg(feature = "client")] 4 | pub mod client; 5 | 6 | #[cfg(feature = "server")] 7 | pub mod server; 8 | 9 | // Re-export client error types 10 | #[cfg(feature = "http-client")] 11 | pub use client::HttpClientError; 12 | #[cfg(feature = "ws-client")] 13 | pub use client::WebSocketClientError; 14 | 15 | // Re-export server error types 16 | #[cfg(feature = "http-server")] 17 | pub use server::HttpServerError; 18 | #[cfg(feature = "ws-server")] 19 | pub use server::WebSocketServerError; 20 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/error/server.rs: -------------------------------------------------------------------------------- 1 | //! Error types for server adapters 2 | 3 | #[cfg(any(feature = "http-server", feature = "ws-server"))] 4 | use std::io; 5 | 6 | #[cfg(any(feature = "http-server", feature = "ws-server"))] 7 | use thiserror::Error; 8 | 9 | /// Error type for HTTP server adapter 10 | #[derive(Error, Debug)] 11 | #[cfg(feature = "http-server")] 12 | pub enum HttpServerError { 13 | /// HTTP server error 14 | #[error("HTTP server error: {0}")] 15 | Server(String), 16 | 17 | /// IO error during HTTP operations 18 | #[error("IO error: {0}")] 19 | Io(#[from] io::Error), 20 | 21 | /// JSON serialization error 22 | #[error("JSON serialization error: {0}")] 23 | Json(#[from] serde_json::Error), 24 | 25 | /// Invalid request format 26 | #[error("Invalid request format: {0}")] 27 | InvalidRequest(String), 28 | } 29 | 30 | /// Error type for WebSocket server adapter 31 | #[derive(Error, Debug)] 32 | #[cfg(feature = "ws-server")] 33 | pub enum WebSocketServerError { 34 | /// WebSocket server error 35 | #[error("WebSocket server error: {0}")] 36 | Server(String), 37 | 38 | /// IO error during WebSocket operations 39 | #[error("IO error: {0}")] 40 | Io(#[from] io::Error), 41 | 42 | /// WebSocket connection error 43 | #[error("WebSocket connection error: {0}")] 44 | Connection(String), 45 | 46 | /// WebSocket message error 47 | #[error("WebSocket message error: {0}")] 48 | Message(String), 49 | 50 | /// JSON serialization error 51 | #[error("JSON serialization error: {0}")] 52 | Json(#[from] serde_json::Error), 53 | } 54 | 55 | // Conversion from adapter errors to domain errors 56 | #[cfg(feature = "http-server")] 57 | impl From for crate::domain::A2AError { 58 | fn from(error: HttpServerError) -> Self { 59 | match error { 60 | HttpServerError::Server(msg) => { 61 | crate::domain::A2AError::Internal(format!("HTTP server error: {}", msg)) 62 | } 63 | HttpServerError::Io(e) => crate::domain::A2AError::Io(e), 64 | HttpServerError::Json(e) => crate::domain::A2AError::JsonParse(e), 65 | HttpServerError::InvalidRequest(msg) => crate::domain::A2AError::InvalidRequest(msg), 66 | } 67 | } 68 | } 69 | 70 | #[cfg(feature = "ws-server")] 71 | impl From for crate::domain::A2AError { 72 | fn from(error: WebSocketServerError) -> Self { 73 | match error { 74 | WebSocketServerError::Server(msg) => { 75 | crate::domain::A2AError::Internal(format!("WebSocket server error: {}", msg)) 76 | } 77 | WebSocketServerError::Io(e) => crate::domain::A2AError::Io(e), 78 | WebSocketServerError::Connection(msg) => { 79 | crate::domain::A2AError::Internal(format!("WebSocket connection error: {}", msg)) 80 | } 81 | WebSocketServerError::Message(msg) => { 82 | crate::domain::A2AError::Internal(format!("WebSocket message error: {}", msg)) 83 | } 84 | WebSocketServerError::Json(e) => crate::domain::A2AError::JsonParse(e), 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/mod.rs: -------------------------------------------------------------------------------- 1 | //! Adapters for the A2A protocol 2 | //! 3 | //! This module provides concrete implementations of the port interfaces, 4 | //! organized by concern: 5 | //! 6 | //! - `transport`: Protocol-specific implementations (HTTP, WebSocket) 7 | //! - `business`: Business logic implementations 8 | //! - `storage`: Data persistence implementations 9 | //! - `auth`: Authentication and authorization implementations 10 | //! - `error`: Error types for all adapters 11 | 12 | pub mod auth; 13 | pub mod business; 14 | pub mod error; 15 | pub mod storage; 16 | pub mod transport; 17 | 18 | // Legacy re-exports for backward compatibility 19 | // TODO: Remove these in a future version 20 | 21 | // Client re-exports (from transport) 22 | #[cfg(feature = "http-client")] 23 | pub use transport::http::HttpClient; 24 | #[cfg(feature = "ws-client")] 25 | pub use transport::websocket::WebSocketClient; 26 | 27 | // Server re-exports (from various modules) 28 | #[cfg(feature = "http-server")] 29 | pub use auth::with_auth; 30 | #[cfg(any(feature = "http-server", feature = "ws-server"))] 31 | pub use auth::{ApiKeyAuthenticator, BearerTokenAuthenticator, NoopAuthenticator}; 32 | #[cfg(feature = "auth")] 33 | pub use auth::{JwtAuthenticator, OAuth2Authenticator, OpenIdConnectAuthenticator}; 34 | #[cfg(all(feature = "server", feature = "http-client"))] 35 | pub use business::HttpPushNotificationSender; 36 | #[cfg(feature = "server")] 37 | pub use business::{DefaultRequestProcessor, SimpleAgentInfo}; 38 | #[cfg(feature = "server")] 39 | pub use business::{NoopPushNotificationSender, PushNotificationRegistry, PushNotificationSender}; 40 | #[cfg(feature = "server")] 41 | pub use storage::InMemoryTaskStorage; 42 | #[cfg(feature = "http-server")] 43 | pub use transport::http::HttpServer; 44 | #[cfg(feature = "ws-server")] 45 | pub use transport::websocket::WebSocketServer; 46 | 47 | // Error re-exports 48 | #[cfg(feature = "http-client")] 49 | pub use error::HttpClientError; 50 | #[cfg(feature = "http-server")] 51 | pub use error::HttpServerError; 52 | #[cfg(feature = "ws-client")] 53 | pub use error::WebSocketClientError; 54 | #[cfg(feature = "ws-server")] 55 | pub use error::WebSocketServerError; 56 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/storage/mod.rs: -------------------------------------------------------------------------------- 1 | //! Storage adapter implementations 2 | 3 | #[cfg(feature = "server")] 4 | pub mod task_storage; 5 | 6 | #[cfg(feature = "sqlx-storage")] 7 | pub mod sqlx_storage; 8 | 9 | #[cfg(feature = "sqlx-storage")] 10 | pub mod database_config; 11 | 12 | #[cfg(feature = "server")] 13 | pub use task_storage::InMemoryTaskStorage; 14 | 15 | #[cfg(feature = "sqlx-storage")] 16 | pub use sqlx_storage::SqlxTaskStorage; 17 | 18 | #[cfg(feature = "sqlx-storage")] 19 | pub use database_config::DatabaseConfig; 20 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/transport/http/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP transport implementations 2 | 3 | #[cfg(feature = "http-client")] 4 | pub mod client; 5 | 6 | #[cfg(feature = "http-server")] 7 | pub mod server; 8 | 9 | // Re-export HTTP implementations 10 | #[cfg(feature = "http-client")] 11 | pub use client::HttpClient; 12 | 13 | #[cfg(feature = "http-server")] 14 | pub use server::HttpServer; 15 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/transport/mod.rs: -------------------------------------------------------------------------------- 1 | //! Transport protocol adapter implementations 2 | 3 | #[cfg(any(feature = "http-client", feature = "http-server"))] 4 | pub mod http; 5 | 6 | #[cfg(any(feature = "ws-client", feature = "ws-server"))] 7 | pub mod websocket; 8 | -------------------------------------------------------------------------------- /a2a-rs/src/adapter/transport/websocket/mod.rs: -------------------------------------------------------------------------------- 1 | //! WebSocket transport implementations 2 | 3 | #[cfg(feature = "ws-client")] 4 | pub mod client; 5 | 6 | #[cfg(feature = "ws-server")] 7 | pub mod server; 8 | 9 | // Re-export WebSocket implementations 10 | #[cfg(feature = "ws-client")] 11 | pub use client::WebSocketClient; 12 | 13 | #[cfg(feature = "ws-server")] 14 | pub use server::WebSocketServer; 15 | -------------------------------------------------------------------------------- /a2a-rs/src/application/handlers/agent.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | use crate::domain::AgentCard; 5 | 6 | /// Empty params type for agent/getExtendedCard 7 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 8 | pub struct GetExtendedCardParams {} 9 | 10 | /// Request to get an extended agent card (v0.3.0) 11 | /// 12 | /// This method returns an extended version of the agent card that may include 13 | /// sensitive information only available to authenticated clients. The response 14 | /// is identical to the regular agent card structure but may contain additional 15 | /// fields or data that are not exposed in the public agent card endpoint. 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct GetExtendedCardRequest { 18 | pub jsonrpc: String, 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub id: Option, 21 | pub method: String, 22 | #[serde(default)] 23 | pub params: GetExtendedCardParams, 24 | } 25 | 26 | impl GetExtendedCardRequest { 27 | pub fn new() -> Self { 28 | Self { 29 | jsonrpc: "2.0".to_string(), 30 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 31 | method: "agent/getExtendedCard".to_string(), 32 | params: GetExtendedCardParams::default(), 33 | } 34 | } 35 | 36 | pub fn with_id(mut self, id: Value) -> Self { 37 | self.id = Some(id); 38 | self 39 | } 40 | } 41 | 42 | impl Default for GetExtendedCardRequest { 43 | fn default() -> Self { 44 | Self::new() 45 | } 46 | } 47 | 48 | /// Response to a get extended card request (v0.3.0) 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | pub struct GetExtendedCardResponse { 51 | pub jsonrpc: String, 52 | #[serde(skip_serializing_if = "Option::is_none")] 53 | pub id: Option, 54 | #[serde(skip_serializing_if = "Option::is_none")] 55 | pub result: Option, 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | pub error: Option, 58 | } 59 | 60 | impl GetExtendedCardResponse { 61 | pub fn success(id: Option, card: AgentCard) -> Self { 62 | Self { 63 | jsonrpc: "2.0".to_string(), 64 | id, 65 | result: Some(card), 66 | error: None, 67 | } 68 | } 69 | 70 | pub fn error(id: Option, error: crate::domain::protocols::JSONRPCError) -> Self { 71 | Self { 72 | jsonrpc: "2.0".to_string(), 73 | id, 74 | result: None, 75 | error: Some(error), 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /a2a-rs/src/application/handlers/message.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | use crate::domain::{MessageSendParams, Task, TaskSendParams}; 5 | 6 | /// Request to send a message 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct SendMessageRequest { 9 | pub jsonrpc: String, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub id: Option, 12 | pub method: String, 13 | pub params: MessageSendParams, 14 | } 15 | 16 | impl SendMessageRequest { 17 | pub fn new(params: MessageSendParams) -> Self { 18 | Self { 19 | jsonrpc: "2.0".to_string(), 20 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 21 | method: "message/send".to_string(), 22 | params, 23 | } 24 | } 25 | } 26 | 27 | /// Request to send a task (legacy) 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct SendTaskRequest { 30 | pub jsonrpc: String, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub id: Option, 33 | pub method: String, 34 | pub params: TaskSendParams, 35 | } 36 | 37 | impl SendTaskRequest { 38 | pub fn new(params: TaskSendParams) -> Self { 39 | Self { 40 | jsonrpc: "2.0".to_string(), 41 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 42 | method: "tasks/send".to_string(), 43 | params, 44 | } 45 | } 46 | } 47 | 48 | /// Response to a send message request 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | pub struct SendMessageResponse { 51 | pub jsonrpc: String, 52 | #[serde(skip_serializing_if = "Option::is_none")] 53 | pub id: Option, 54 | #[serde(skip_serializing_if = "Option::is_none")] 55 | pub result: Option, 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | pub error: Option, 58 | } 59 | 60 | /// Response to a send task request (legacy) 61 | #[derive(Debug, Clone, Serialize, Deserialize)] 62 | pub struct SendTaskResponse { 63 | pub jsonrpc: String, 64 | #[serde(skip_serializing_if = "Option::is_none")] 65 | pub id: Option, 66 | #[serde(skip_serializing_if = "Option::is_none")] 67 | pub result: Option, 68 | #[serde(skip_serializing_if = "Option::is_none")] 69 | pub error: Option, 70 | } 71 | 72 | /// Request to send a message with streaming updates 73 | #[derive(Debug, Clone, Serialize, Deserialize)] 74 | pub struct SendMessageStreamingRequest { 75 | pub jsonrpc: String, 76 | #[serde(skip_serializing_if = "Option::is_none")] 77 | pub id: Option, 78 | pub method: String, 79 | pub params: MessageSendParams, 80 | } 81 | 82 | impl SendMessageStreamingRequest { 83 | pub fn new(params: MessageSendParams) -> Self { 84 | Self { 85 | jsonrpc: "2.0".to_string(), 86 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 87 | method: "message/stream".to_string(), 88 | params, 89 | } 90 | } 91 | } 92 | 93 | /// Request to send a task with streaming updates (legacy) 94 | #[derive(Debug, Clone, Serialize, Deserialize)] 95 | pub struct SendTaskStreamingRequest { 96 | pub jsonrpc: String, 97 | #[serde(skip_serializing_if = "Option::is_none")] 98 | pub id: Option, 99 | pub method: String, 100 | pub params: TaskSendParams, 101 | } 102 | 103 | impl SendTaskStreamingRequest { 104 | pub fn new(params: TaskSendParams) -> Self { 105 | Self { 106 | jsonrpc: "2.0".to_string(), 107 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 108 | method: "tasks/sendSubscribe".to_string(), 109 | params, 110 | } 111 | } 112 | } 113 | 114 | /// Response to a send message streaming request 115 | #[derive(Debug, Clone, Serialize, Deserialize)] 116 | #[serde(untagged)] 117 | pub enum SendMessageStreamingResponse { 118 | Initial { 119 | jsonrpc: String, 120 | #[serde(skip_serializing_if = "Option::is_none")] 121 | id: Option, 122 | result: Box, 123 | }, 124 | Update { 125 | jsonrpc: String, 126 | method: String, 127 | params: serde_json::Value, 128 | }, 129 | Error { 130 | jsonrpc: String, 131 | #[serde(skip_serializing_if = "Option::is_none")] 132 | id: Option, 133 | error: crate::domain::protocols::JSONRPCError, 134 | }, 135 | } 136 | 137 | /// Response to a send task streaming request (legacy) 138 | #[derive(Debug, Clone, Serialize, Deserialize)] 139 | #[serde(untagged)] 140 | pub enum SendTaskStreamingResponse { 141 | Initial { 142 | jsonrpc: String, 143 | #[serde(skip_serializing_if = "Option::is_none")] 144 | id: Option, 145 | result: Box, 146 | }, 147 | Update { 148 | jsonrpc: String, 149 | method: String, 150 | params: serde_json::Value, 151 | }, 152 | Error { 153 | jsonrpc: String, 154 | #[serde(skip_serializing_if = "Option::is_none")] 155 | id: Option, 156 | error: crate::domain::protocols::JSONRPCError, 157 | }, 158 | } 159 | -------------------------------------------------------------------------------- /a2a-rs/src/application/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Request and response handlers for the A2A protocol 2 | 3 | pub mod agent; 4 | pub mod message; 5 | pub mod notification; 6 | pub mod task; 7 | 8 | pub use agent::{GetExtendedCardRequest, GetExtendedCardResponse}; 9 | pub use message::{ 10 | SendMessageRequest, SendMessageResponse, SendMessageStreamingRequest, 11 | SendMessageStreamingResponse, SendTaskRequest, SendTaskResponse, SendTaskStreamingRequest, 12 | SendTaskStreamingResponse, 13 | }; 14 | pub use notification::{ 15 | GetTaskPushNotificationRequest, GetTaskPushNotificationResponse, 16 | SetTaskPushNotificationRequest, SetTaskPushNotificationResponse, 17 | }; 18 | pub use task::{ 19 | CancelTaskRequest, CancelTaskResponse, GetTaskRequest, GetTaskResponse, 20 | TaskResubscriptionRequest, 21 | }; 22 | -------------------------------------------------------------------------------- /a2a-rs/src/application/handlers/notification.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | use crate::domain::{TaskIdParams, TaskPushNotificationConfig}; 5 | 6 | /// Request to set task push notification 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct SetTaskPushNotificationRequest { 9 | pub jsonrpc: String, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub id: Option, 12 | pub method: String, 13 | pub params: TaskPushNotificationConfig, 14 | } 15 | 16 | impl SetTaskPushNotificationRequest { 17 | pub fn new(params: TaskPushNotificationConfig) -> Self { 18 | Self { 19 | jsonrpc: "2.0".to_string(), 20 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 21 | method: "tasks/pushNotificationConfig/set".to_string(), 22 | params, 23 | } 24 | } 25 | } 26 | 27 | /// Response to a set task push notification request 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct SetTaskPushNotificationResponse { 30 | pub jsonrpc: String, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub id: Option, 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub result: Option, 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub error: Option, 37 | } 38 | 39 | /// Request to get task push notification 40 | #[derive(Debug, Clone, Serialize, Deserialize)] 41 | pub struct GetTaskPushNotificationRequest { 42 | pub jsonrpc: String, 43 | #[serde(skip_serializing_if = "Option::is_none")] 44 | pub id: Option, 45 | pub method: String, 46 | pub params: TaskIdParams, 47 | } 48 | 49 | impl GetTaskPushNotificationRequest { 50 | pub fn new(params: TaskIdParams) -> Self { 51 | Self { 52 | jsonrpc: "2.0".to_string(), 53 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 54 | method: "tasks/pushNotificationConfig/get".to_string(), 55 | params, 56 | } 57 | } 58 | } 59 | 60 | /// Response to a get task push notification request 61 | #[derive(Debug, Clone, Serialize, Deserialize)] 62 | pub struct GetTaskPushNotificationResponse { 63 | pub jsonrpc: String, 64 | #[serde(skip_serializing_if = "Option::is_none")] 65 | pub id: Option, 66 | #[serde(skip_serializing_if = "Option::is_none")] 67 | pub result: Option, 68 | #[serde(skip_serializing_if = "Option::is_none")] 69 | pub error: Option, 70 | } 71 | -------------------------------------------------------------------------------- /a2a-rs/src/application/handlers/task.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | use crate::domain::{Task, TaskIdParams, TaskQueryParams}; 5 | 6 | /// Request to get a task 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct GetTaskRequest { 9 | pub jsonrpc: String, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub id: Option, 12 | pub method: String, 13 | pub params: TaskQueryParams, 14 | } 15 | 16 | impl GetTaskRequest { 17 | pub fn new(params: TaskQueryParams) -> Self { 18 | Self { 19 | jsonrpc: "2.0".to_string(), 20 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 21 | method: "tasks/get".to_string(), 22 | params, 23 | } 24 | } 25 | } 26 | 27 | /// Response to a get task request 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct GetTaskResponse { 30 | pub jsonrpc: String, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub id: Option, 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub result: Option, 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub error: Option, 37 | } 38 | 39 | /// Request to cancel a task 40 | #[derive(Debug, Clone, Serialize, Deserialize)] 41 | pub struct CancelTaskRequest { 42 | pub jsonrpc: String, 43 | #[serde(skip_serializing_if = "Option::is_none")] 44 | pub id: Option, 45 | pub method: String, 46 | pub params: TaskIdParams, 47 | } 48 | 49 | impl CancelTaskRequest { 50 | pub fn new(params: TaskIdParams) -> Self { 51 | Self { 52 | jsonrpc: "2.0".to_string(), 53 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 54 | method: "tasks/cancel".to_string(), 55 | params, 56 | } 57 | } 58 | } 59 | 60 | /// Response to a cancel task request 61 | #[derive(Debug, Clone, Serialize, Deserialize)] 62 | pub struct CancelTaskResponse { 63 | pub jsonrpc: String, 64 | #[serde(skip_serializing_if = "Option::is_none")] 65 | pub id: Option, 66 | #[serde(skip_serializing_if = "Option::is_none")] 67 | pub result: Option, 68 | #[serde(skip_serializing_if = "Option::is_none")] 69 | pub error: Option, 70 | } 71 | 72 | /// Request for task resubscription 73 | #[derive(Debug, Clone, Serialize, Deserialize)] 74 | pub struct TaskResubscriptionRequest { 75 | pub jsonrpc: String, 76 | #[serde(skip_serializing_if = "Option::is_none")] 77 | pub id: Option, 78 | pub method: String, 79 | pub params: TaskQueryParams, 80 | } 81 | 82 | impl TaskResubscriptionRequest { 83 | pub fn new(params: TaskQueryParams) -> Self { 84 | Self { 85 | jsonrpc: "2.0".to_string(), 86 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 87 | method: "tasks/resubscribe".to_string(), 88 | params, 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /a2a-rs/src/application/mod.rs: -------------------------------------------------------------------------------- 1 | //! Application services for the A2A protocol 2 | 3 | pub mod handlers; 4 | pub mod json_rpc; 5 | 6 | // Re-export key types for convenience 7 | pub use json_rpc::{ 8 | parse_request, serialize_request, A2ARequest, CancelTaskRequest, CancelTaskResponse, 9 | GetTaskPushNotificationRequest, GetTaskPushNotificationResponse, GetTaskRequest, 10 | GetTaskResponse, SendMessageRequest, SendMessageResponse, SendMessageStreamingRequest, 11 | SendMessageStreamingResponse, SendTaskRequest, SendTaskResponse, SendTaskStreamingRequest, 12 | SendTaskStreamingResponse, SetTaskPushNotificationRequest, SetTaskPushNotificationResponse, 13 | TaskResubscriptionRequest, 14 | }; 15 | 16 | // Re-export JSON-RPC protocol types from domain for backward compatibility 17 | pub use crate::domain::{JSONRPCError, JSONRPCMessage, JSONRPCRequest, JSONRPCResponse}; 18 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/core/mod.rs: -------------------------------------------------------------------------------- 1 | //! Core domain types for the A2A protocol 2 | 3 | pub mod agent; 4 | pub mod message; 5 | pub mod task; 6 | 7 | pub use agent::{ 8 | AgentCapabilities, AgentCard, AgentCardSignature, AgentProvider, AgentSkill, 9 | AuthorizationCodeOAuthFlow, ClientCredentialsOAuthFlow, ImplicitOAuthFlow, OAuthFlows, 10 | PasswordOAuthFlow, PushNotificationAuthenticationInfo, PushNotificationConfig, SecurityScheme, 11 | }; 12 | pub use message::{Artifact, FileContent, Message, Part, Role}; 13 | pub use task::{ 14 | MessageSendConfiguration, MessageSendParams, Task, TaskIdParams, TaskPushNotificationConfig, 15 | TaskQueryParams, TaskSendParams, TaskState, TaskStatus, 16 | }; 17 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Standard JSON-RPC error codes 4 | pub const PARSE_ERROR: i32 = -32700; 5 | pub const INVALID_REQUEST: i32 = -32600; 6 | pub const METHOD_NOT_FOUND: i32 = -32601; 7 | pub const INVALID_PARAMS: i32 = -32602; 8 | pub const INTERNAL_ERROR: i32 = -32603; 9 | 10 | /// A2A specific error codes 11 | pub const TASK_NOT_FOUND: i32 = -32001; 12 | pub const TASK_NOT_CANCELABLE: i32 = -32002; 13 | pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003; 14 | pub const UNSUPPORTED_OPERATION: i32 = -32004; 15 | pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005; 16 | pub const INVALID_AGENT_RESPONSE: i32 = -32006; 17 | pub const DATABASE_ERROR: i32 = -32007; 18 | 19 | /// Error type for the A2A protocol operations 20 | #[derive(Error, Debug)] 21 | pub enum A2AError { 22 | #[error("JSON-RPC error: {code} - {message}")] 23 | JsonRpc { 24 | code: i32, 25 | message: String, 26 | data: Option, 27 | }, 28 | 29 | #[error("JSON parse error: {0}")] 30 | JsonParse(#[from] serde_json::Error), 31 | 32 | #[error("Invalid request: {0}")] 33 | InvalidRequest(String), 34 | 35 | #[error("Invalid parameters: {0}")] 36 | InvalidParams(String), 37 | 38 | #[error("Method not found: {0}")] 39 | MethodNotFound(String), 40 | 41 | #[error("Task not found: {0}")] 42 | TaskNotFound(String), 43 | 44 | #[error("Task not cancelable: {0}")] 45 | TaskNotCancelable(String), 46 | 47 | #[error("Push notification not supported")] 48 | PushNotificationNotSupported, 49 | 50 | #[error("Unsupported operation: {0}")] 51 | UnsupportedOperation(String), 52 | 53 | #[error("Content type not supported: {0}")] 54 | ContentTypeNotSupported(String), 55 | 56 | #[error("Invalid agent response: {0}")] 57 | InvalidAgentResponse(String), 58 | 59 | #[error("Internal error: {0}")] 60 | Internal(String), 61 | 62 | #[error("Validation error in {field}: {message}")] 63 | ValidationError { field: String, message: String }, 64 | 65 | #[error("Database error: {0}")] 66 | DatabaseError(String), 67 | 68 | #[error("IO error: {0}")] 69 | Io(#[from] std::io::Error), 70 | } 71 | 72 | impl A2AError { 73 | /// Convert an A2AError to a JSON-RPC error value 74 | pub fn to_jsonrpc_error(&self) -> serde_json::Value { 75 | let (code, message) = match self { 76 | A2AError::JsonParse(_) => (PARSE_ERROR, "Invalid JSON payload"), 77 | A2AError::InvalidRequest(_) => (INVALID_REQUEST, "Request payload validation error"), 78 | A2AError::MethodNotFound(_) => (METHOD_NOT_FOUND, "Method not found"), 79 | A2AError::InvalidParams(_) => (INVALID_PARAMS, "Invalid parameters"), 80 | A2AError::TaskNotFound(_) => (TASK_NOT_FOUND, "Task not found"), 81 | A2AError::TaskNotCancelable(_) => (TASK_NOT_CANCELABLE, "Task cannot be canceled"), 82 | A2AError::PushNotificationNotSupported => ( 83 | PUSH_NOTIFICATION_NOT_SUPPORTED, 84 | "Push Notification is not supported", 85 | ), 86 | A2AError::UnsupportedOperation(_) => { 87 | (UNSUPPORTED_OPERATION, "This operation is not supported") 88 | } 89 | A2AError::ContentTypeNotSupported(_) => { 90 | (CONTENT_TYPE_NOT_SUPPORTED, "Incompatible content types") 91 | } 92 | A2AError::InvalidAgentResponse(_) => (INVALID_AGENT_RESPONSE, "Invalid agent response"), 93 | A2AError::ValidationError { .. } => (INVALID_PARAMS, "Validation error"), 94 | A2AError::DatabaseError(_) => (DATABASE_ERROR, "Database error"), 95 | A2AError::Internal(_) => (INTERNAL_ERROR, "Internal error"), 96 | _ => (INTERNAL_ERROR, "Internal error"), 97 | }; 98 | 99 | serde_json::json!({ 100 | "code": code, 101 | "message": message, 102 | "data": null, 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/events/mod.rs: -------------------------------------------------------------------------------- 1 | //! Event types for streaming and notifications 2 | 3 | pub mod task_events; 4 | 5 | pub use task_events::{TaskArtifactUpdateEvent, TaskStatusUpdateEvent}; 6 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/events/task_events.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{Map, Value}; 3 | 4 | use crate::domain::core::{message::Artifact, task::TaskStatus}; 5 | 6 | /// Event for task status updates 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct TaskStatusUpdateEvent { 9 | #[serde(rename = "taskId")] 10 | pub task_id: String, 11 | #[serde(rename = "contextId")] 12 | pub context_id: String, 13 | pub kind: String, // Always "status-update" 14 | pub status: TaskStatus, 15 | #[serde(rename = "final")] 16 | pub final_: bool, 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub metadata: Option>, 19 | } 20 | 21 | /// Event for task artifact updates 22 | #[derive(Debug, Clone, Serialize, Deserialize)] 23 | pub struct TaskArtifactUpdateEvent { 24 | #[serde(rename = "taskId")] 25 | pub task_id: String, 26 | #[serde(rename = "contextId")] 27 | pub context_id: String, 28 | pub kind: String, // Always "artifact-update" 29 | pub artifact: Artifact, 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub append: Option, 32 | #[serde(skip_serializing_if = "Option::is_none", rename = "lastChunk")] 33 | pub last_chunk: Option, 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub metadata: Option>, 36 | } 37 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/mod.rs: -------------------------------------------------------------------------------- 1 | //! Domain models for the A2A protocol 2 | 3 | pub mod core; 4 | pub mod error; 5 | pub mod events; 6 | pub mod protocols; 7 | #[cfg(test)] 8 | mod tests; 9 | pub mod validation; 10 | 11 | // Re-export key types for convenience 12 | pub use core::{ 13 | AgentCapabilities, AgentCard, AgentCardSignature, AgentProvider, AgentSkill, Artifact, 14 | AuthorizationCodeOAuthFlow, ClientCredentialsOAuthFlow, FileContent, ImplicitOAuthFlow, 15 | Message, MessageSendConfiguration, MessageSendParams, OAuthFlows, Part, PasswordOAuthFlow, 16 | PushNotificationAuthenticationInfo, PushNotificationConfig, Role, SecurityScheme, Task, 17 | TaskIdParams, TaskPushNotificationConfig, TaskQueryParams, TaskSendParams, TaskState, 18 | TaskStatus, 19 | }; 20 | pub use error::A2AError; 21 | pub use events::{TaskArtifactUpdateEvent, TaskStatusUpdateEvent}; 22 | pub use protocols::{ 23 | JSONRPCError, JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, 24 | }; 25 | pub use validation::{Validate, ValidationResult}; 26 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/protocols/json_rpc.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | use crate::domain::error::A2AError; 5 | 6 | /// Standard JSON-RPC 2.0 message 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct JSONRPCMessage { 9 | pub jsonrpc: String, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub id: Option, 12 | } 13 | 14 | impl Default for JSONRPCMessage { 15 | fn default() -> Self { 16 | Self { 17 | jsonrpc: "2.0".to_string(), 18 | id: None, 19 | } 20 | } 21 | } 22 | 23 | /// JSON-RPC 2.0 error object 24 | #[derive(Debug, Clone, Serialize, Deserialize)] 25 | pub struct JSONRPCError { 26 | pub code: i32, 27 | pub message: String, 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub data: Option, 30 | } 31 | 32 | impl From for JSONRPCError { 33 | fn from(error: A2AError) -> Self { 34 | let value = error.to_jsonrpc_error(); 35 | 36 | // Extract the fields from the JSON value 37 | if let Value::Object(map) = value { 38 | let code = map 39 | .get("code") 40 | .and_then(|v| v.as_i64()) 41 | .map(|v| v as i32) 42 | .unwrap_or(-32603); // Internal error code as fallback 43 | 44 | let message = map 45 | .get("message") 46 | .and_then(|v| v.as_str()) 47 | .unwrap_or("Internal error") 48 | .to_string(); 49 | 50 | let data = map.get("data").cloned(); 51 | 52 | Self { 53 | code, 54 | message, 55 | data, 56 | } 57 | } else { 58 | // Fallback to internal error if the JSON structure is unexpected 59 | Self { 60 | code: -32603, 61 | message: "Internal error".to_string(), 62 | data: None, 63 | } 64 | } 65 | } 66 | } 67 | 68 | /// JSON-RPC 2.0 request 69 | #[derive(Debug, Clone, Serialize, Deserialize)] 70 | pub struct JSONRPCRequest { 71 | pub jsonrpc: String, 72 | #[serde(skip_serializing_if = "Option::is_none")] 73 | pub id: Option, 74 | pub method: String, 75 | #[serde(skip_serializing_if = "Option::is_none")] 76 | pub params: Option, 77 | } 78 | 79 | impl JSONRPCRequest { 80 | /// Create a new JSON-RPC request with the given method and parameters 81 | pub fn new(method: String, params: Option) -> Self { 82 | Self { 83 | jsonrpc: "2.0".to_string(), 84 | id: Some(Value::String(uuid::Uuid::new_v4().to_string())), 85 | method, 86 | params, 87 | } 88 | } 89 | 90 | /// Create a new JSON-RPC request with the given method, parameters, and ID 91 | pub fn with_id(method: String, params: Option, id: Value) -> Self { 92 | Self { 93 | jsonrpc: "2.0".to_string(), 94 | id: Some(id), 95 | method, 96 | params, 97 | } 98 | } 99 | } 100 | 101 | /// JSON-RPC 2.0 response 102 | #[derive(Debug, Clone, Serialize, Deserialize)] 103 | pub struct JSONRPCResponse { 104 | pub jsonrpc: String, 105 | #[serde(skip_serializing_if = "Option::is_none")] 106 | pub id: Option, 107 | #[serde(skip_serializing_if = "Option::is_none")] 108 | pub result: Option, 109 | #[serde(skip_serializing_if = "Option::is_none")] 110 | pub error: Option, 111 | } 112 | 113 | impl JSONRPCResponse { 114 | /// Create a successful JSON-RPC response 115 | pub fn success(id: Option, result: Value) -> Self { 116 | Self { 117 | jsonrpc: "2.0".to_string(), 118 | id, 119 | result: Some(result), 120 | error: None, 121 | } 122 | } 123 | 124 | /// Create an error JSON-RPC response 125 | pub fn error(id: Option, error: JSONRPCError) -> Self { 126 | Self { 127 | jsonrpc: "2.0".to_string(), 128 | id, 129 | result: None, 130 | error: Some(error), 131 | } 132 | } 133 | } 134 | 135 | /// JSON-RPC 2.0 notification (request without id) 136 | #[derive(Debug, Clone, Serialize, Deserialize)] 137 | pub struct JSONRPCNotification { 138 | pub jsonrpc: String, 139 | pub method: String, 140 | #[serde(skip_serializing_if = "Option::is_none")] 141 | pub params: Option, 142 | } 143 | 144 | impl JSONRPCNotification { 145 | /// Create a new JSON-RPC notification 146 | pub fn new(method: String, params: Option) -> Self { 147 | Self { 148 | jsonrpc: "2.0".to_string(), 149 | method, 150 | params, 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | //! Protocol-specific types and implementations 2 | 3 | pub mod json_rpc; 4 | 5 | pub use json_rpc::{ 6 | JSONRPCError, JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, 7 | }; 8 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod task_tests { 3 | use crate::domain::{Message, Task, TaskState}; 4 | 5 | #[test] 6 | fn test_task_history_tracking() { 7 | // Create a new task 8 | let mut task = Task::new("test-task-1".to_string(), "test-context-1".to_string()); 9 | 10 | // Create messages using helper methods 11 | let message1 = Message::user_text("Message 1".to_string(), "msg1".to_string()); 12 | let message2 = Message::agent_text("Message 2".to_string(), "msg2".to_string()); 13 | let message3 = Message::user_text("Message 3".to_string(), "msg3".to_string()); 14 | 15 | // Update the task with messages 16 | task.update_status(TaskState::Working, Some(message1.clone())); 17 | task.update_status(TaskState::Working, Some(message2.clone())); 18 | task.update_status(TaskState::Working, Some(message3.clone())); 19 | 20 | // Verify history has all messages 21 | assert!(task.history.is_some()); 22 | let history = task.history.as_ref().unwrap(); 23 | assert_eq!(history.len(), 3); 24 | assert_eq!(history[0].parts[0].get_text().unwrap(), "Message 1"); 25 | assert_eq!(history[1].parts[0].get_text().unwrap(), "Message 2"); 26 | assert_eq!(history[2].parts[0].get_text().unwrap(), "Message 3"); 27 | 28 | // Test history truncation with with_limited_history 29 | let task_limited = task.with_limited_history(Some(2)); 30 | assert!(task_limited.history.is_some()); 31 | let history_limited = task_limited.history.unwrap(); 32 | assert_eq!(history_limited.len(), 2); 33 | 34 | // Should have the most recent 2 messages (message2, message3) 35 | assert_eq!(history_limited[0].parts[0].get_text().unwrap(), "Message 2"); 36 | assert_eq!(history_limited[1].parts[0].get_text().unwrap(), "Message 3"); 37 | 38 | // Test removing history entirely 39 | let task_no_history = task.with_limited_history(Some(0)); 40 | assert!(task_no_history.history.is_none()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /a2a-rs/src/domain/validation/mod.rs: -------------------------------------------------------------------------------- 1 | //! Cross-cutting validation logic for the A2A protocol 2 | 3 | use crate::domain::error::A2AError; 4 | 5 | /// Validation result type 6 | pub type ValidationResult = Result; 7 | 8 | /// Trait for validating domain objects 9 | pub trait Validate { 10 | /// Validate the object and return a result 11 | fn validate(&self) -> ValidationResult<()>; 12 | } 13 | 14 | /// Utility functions for common validations 15 | pub mod validators { 16 | use super::*; 17 | 18 | /// Validate that a string is not empty 19 | pub fn not_empty(value: &str, field_name: &str) -> ValidationResult<()> { 20 | if value.trim().is_empty() { 21 | Err(A2AError::ValidationError { 22 | field: field_name.to_string(), 23 | message: format!("{} cannot be empty", field_name), 24 | }) 25 | } else { 26 | Ok(()) 27 | } 28 | } 29 | 30 | /// Validate that an optional string, if present, is not empty 31 | pub fn optional_not_empty(value: &Option, field_name: &str) -> ValidationResult<()> { 32 | if let Some(ref val) = value { 33 | not_empty(val, field_name) 34 | } else { 35 | Ok(()) 36 | } 37 | } 38 | 39 | /// Validate UUID format 40 | pub fn valid_uuid(value: &str, field_name: &str) -> ValidationResult<()> { 41 | if uuid::Uuid::parse_str(value).is_err() { 42 | Err(A2AError::ValidationError { 43 | field: field_name.to_string(), 44 | message: format!("{} must be a valid UUID", field_name), 45 | }) 46 | } else { 47 | Ok(()) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /a2a-rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Rust implementation of the Agent-to-Agent (A2A) Protocol 2 | //! 3 | //! This library provides a type-safe, idiomatic Rust implementation of the A2A protocol, 4 | //! with support for both client and server roles. The implementation follows a hexagonal 5 | //! architecture with clear separation between domains, ports, and adapters. 6 | //! 7 | //! # Features 8 | //! 9 | //! - Complete implementation of the A2A protocol 10 | //! - Support for HTTP and WebSocket transport 11 | //! - Support for streaming updates 12 | //! - Async and sync interfaces 13 | //! - Feature flags for optional dependencies 14 | //! 15 | //! # Examples 16 | //! 17 | //! ## Creating a client 18 | //! 19 | //! ```rust,no_run 20 | //! # #[cfg(feature = "http-client")] 21 | //! # { 22 | //! use a2a_rs::{HttpClient, Message}; 23 | //! use a2a_rs::port::AsyncA2AClient; 24 | //! 25 | //! #[tokio::main] 26 | //! async fn main() -> Result<(), Box> { 27 | //! // Create a client 28 | //! let client = HttpClient::new("https://example.com/api".to_string()); 29 | //! 30 | //! // Send a task message 31 | //! let message = Message::user_text("Hello, world!".to_string()); 32 | //! let task = client.send_task_message("task-123", &message, None, None).await?; 33 | //! 34 | //! println!("Task: {:?}", task); 35 | //! Ok(()) 36 | //! } 37 | //! # } 38 | //! ``` 39 | //! 40 | //! ## Creating a server 41 | //! 42 | //! ```rust,no_run 43 | //! # #[cfg(feature = "http-server")] 44 | //! # { 45 | //! use a2a_rs::{HttpServer, SimpleAgentInfo, DefaultRequestProcessor}; 46 | //! 47 | //! #[tokio::main] 48 | //! async fn main() -> Result<(), Box> { 49 | //! // Create a server with default implementations 50 | //! let server = HttpServer::new( 51 | //! DefaultRequestProcessor::new(), 52 | //! SimpleAgentInfo::new("my-agent".to_string(), "1.0.0".to_string()), 53 | //! "127.0.0.1:8080".to_string(), 54 | //! ); 55 | //! 56 | //! // Start the server 57 | //! server.start().await?; 58 | //! Ok(()) 59 | //! } 60 | //! # } 61 | //! ``` 62 | 63 | // Re-export key modules and types 64 | pub mod adapter; 65 | pub mod application; 66 | pub mod domain; 67 | pub mod port; 68 | pub mod services; 69 | 70 | #[cfg(feature = "tracing")] 71 | pub mod observability; 72 | 73 | // Public API exports 74 | pub use domain::{ 75 | A2AError, AgentCapabilities, AgentCard, AgentCardSignature, AgentProvider, AgentSkill, 76 | Artifact, AuthorizationCodeOAuthFlow, ClientCredentialsOAuthFlow, FileContent, 77 | ImplicitOAuthFlow, Message, MessageSendConfiguration, MessageSendParams, OAuthFlows, Part, 78 | PasswordOAuthFlow, PushNotificationAuthenticationInfo, PushNotificationConfig, Role, 79 | SecurityScheme, Task, TaskArtifactUpdateEvent, TaskIdParams, TaskPushNotificationConfig, 80 | TaskQueryParams, TaskSendParams, TaskState, TaskStatus, TaskStatusUpdateEvent, 81 | }; 82 | 83 | // Port traits for better separation of concerns 84 | pub use port::{ 85 | AsyncMessageHandler, AsyncNotificationManager, AsyncStreamingHandler, AsyncTaskManager, 86 | MessageHandler, NotificationManager, StreamingHandler, StreamingSubscriber, TaskManager, 87 | UpdateEvent, 88 | }; 89 | 90 | #[cfg(feature = "http-client")] 91 | pub use adapter::HttpClient; 92 | 93 | #[cfg(feature = "ws-client")] 94 | pub use adapter::WebSocketClient; 95 | 96 | #[cfg(feature = "http-server")] 97 | pub use adapter::HttpServer; 98 | 99 | #[cfg(feature = "ws-server")] 100 | pub use adapter::WebSocketServer; 101 | 102 | #[cfg(feature = "server")] 103 | pub use adapter::{ 104 | DefaultRequestProcessor, InMemoryTaskStorage, NoopPushNotificationSender, 105 | PushNotificationRegistry, PushNotificationSender, SimpleAgentInfo, 106 | }; 107 | 108 | #[cfg(all(feature = "server", feature = "http-client"))] 109 | pub use adapter::HttpPushNotificationSender; 110 | 111 | #[cfg(any(feature = "http-server", feature = "ws-server"))] 112 | pub use adapter::{ApiKeyAuthenticator, BearerTokenAuthenticator, NoopAuthenticator}; 113 | #[cfg(feature = "auth")] 114 | pub use adapter::{JwtAuthenticator, OAuth2Authenticator, OpenIdConnectAuthenticator}; 115 | #[cfg(any(feature = "http-server", feature = "ws-server"))] 116 | pub use port::Authenticator; 117 | -------------------------------------------------------------------------------- /a2a-rs/src/port/authenticator.rs: -------------------------------------------------------------------------------- 1 | //! Authentication port - defines the interface for authentication in the domain 2 | 3 | use async_trait::async_trait; 4 | use std::collections::HashMap; 5 | 6 | use crate::domain::{core::agent::SecurityScheme, A2AError}; 7 | 8 | /// Authentication context containing credentials and metadata 9 | #[derive(Debug, Clone)] 10 | pub struct AuthContext { 11 | /// The authentication scheme type (e.g., "bearer", "apikey", "oauth2") 12 | pub scheme_type: String, 13 | /// The credential value (token, api key, etc) 14 | pub credential: String, 15 | /// Additional context (e.g., location for API key, scopes for OAuth2) 16 | pub metadata: HashMap, 17 | } 18 | 19 | impl AuthContext { 20 | /// Create a new authentication context 21 | pub fn new(scheme_type: String, credential: String) -> Self { 22 | Self { 23 | scheme_type, 24 | credential, 25 | metadata: HashMap::new(), 26 | } 27 | } 28 | 29 | /// Add metadata to the context 30 | pub fn with_metadata(mut self, key: String, value: String) -> Self { 31 | self.metadata.insert(key, value); 32 | self 33 | } 34 | 35 | /// Get a metadata value 36 | pub fn get_metadata(&self, key: &str) -> Option<&String> { 37 | self.metadata.get(key) 38 | } 39 | } 40 | 41 | /// Port interface for authentication handlers 42 | #[async_trait] 43 | pub trait Authenticator: Send + Sync { 44 | /// Authenticate a request based on the provided context 45 | async fn authenticate(&self, context: &AuthContext) -> Result; 46 | 47 | /// Get the security scheme configuration 48 | fn security_scheme(&self) -> &SecurityScheme; 49 | 50 | /// Validate that the context matches this authenticator's scheme 51 | fn validate_context(&self, context: &AuthContext) -> Result<(), A2AError>; 52 | } 53 | 54 | /// Represents an authenticated principal 55 | #[derive(Debug, Clone)] 56 | pub struct AuthPrincipal { 57 | /// Unique identifier for the authenticated entity 58 | pub id: String, 59 | /// The authentication scheme used 60 | pub scheme: String, 61 | /// Additional claims or attributes 62 | pub attributes: HashMap, 63 | } 64 | 65 | impl AuthPrincipal { 66 | /// Create a new authenticated principal 67 | pub fn new(id: String, scheme: String) -> Self { 68 | Self { 69 | id, 70 | scheme, 71 | attributes: HashMap::new(), 72 | } 73 | } 74 | 75 | /// Add an attribute to the principal 76 | pub fn with_attribute(mut self, key: String, value: String) -> Self { 77 | self.attributes.insert(key, value); 78 | self 79 | } 80 | } 81 | 82 | /// Port interface for authentication context extraction 83 | #[async_trait] 84 | pub trait AuthContextExtractor: Send + Sync { 85 | /// Extract authentication context from HTTP headers 86 | #[cfg(feature = "http-server")] 87 | async fn extract_from_headers(&self, headers: &axum::http::HeaderMap) -> Option; 88 | 89 | /// Extract authentication context from headers (generic version) 90 | #[cfg(not(feature = "http-server"))] 91 | async fn extract_from_headers( 92 | &self, 93 | headers: &std::collections::HashMap, 94 | ) -> Option; 95 | 96 | /// Extract authentication context from query parameters 97 | async fn extract_from_query(&self, params: &HashMap) -> Option; 98 | 99 | /// Extract authentication context from cookies 100 | async fn extract_from_cookies(&self, cookies: &str) -> Option; 101 | } 102 | 103 | /// Composite authenticator that tries multiple authentication methods 104 | #[async_trait] 105 | pub trait CompositeAuthenticator: Send + Sync { 106 | /// Try to authenticate using any available scheme 107 | async fn authenticate_any(&self, contexts: Vec) 108 | -> Result; 109 | 110 | /// Get all supported security schemes 111 | fn supported_schemes(&self) -> Vec<&SecurityScheme>; 112 | } 113 | -------------------------------------------------------------------------------- /a2a-rs/src/port/message_handler.rs: -------------------------------------------------------------------------------- 1 | //! Message handling port definitions 2 | 3 | #[cfg(feature = "server")] 4 | use async_trait::async_trait; 5 | 6 | use crate::domain::{A2AError, Message, Task}; 7 | 8 | /// A trait for handling message processing operations 9 | pub trait MessageHandler { 10 | /// Process a message for a specific task 11 | fn process_message( 12 | &self, 13 | task_id: &str, 14 | message: &Message, 15 | session_id: Option<&str>, 16 | ) -> Result; 17 | 18 | /// Validate a message before processing 19 | fn validate_message(&self, message: &Message) -> Result<(), A2AError> { 20 | // Default implementation - can be overridden 21 | if message.parts.is_empty() { 22 | return Err(A2AError::ValidationError { 23 | field: "message.parts".to_string(), 24 | message: "Message must contain at least one part".to_string(), 25 | }); 26 | } 27 | Ok(()) 28 | } 29 | 30 | /// Transform a message before processing (e.g., for content filtering) 31 | fn transform_message(&self, message: Message) -> Result { 32 | // Default implementation - pass through unchanged 33 | Ok(message) 34 | } 35 | } 36 | 37 | #[cfg(feature = "server")] 38 | #[async_trait] 39 | /// An async trait for handling message processing operations 40 | pub trait AsyncMessageHandler: Send + Sync { 41 | /// Process a message for a specific task 42 | async fn process_message<'a>( 43 | &self, 44 | task_id: &'a str, 45 | message: &'a Message, 46 | session_id: Option<&'a str>, 47 | ) -> Result; 48 | 49 | /// Validate a message before processing 50 | async fn validate_message<'a>(&self, message: &'a Message) -> Result<(), A2AError> { 51 | // Default implementation - can be overridden 52 | if message.parts.is_empty() { 53 | return Err(A2AError::ValidationError { 54 | field: "message.parts".to_string(), 55 | message: "Message must contain at least one part".to_string(), 56 | }); 57 | } 58 | Ok(()) 59 | } 60 | 61 | /// Transform a message before processing (e.g., for content filtering) 62 | async fn transform_message(&self, message: Message) -> Result { 63 | // Default implementation - pass through unchanged 64 | Ok(message) 65 | } 66 | 67 | /// Handle message processing with validation and transformation 68 | async fn handle_message_flow<'a>( 69 | &self, 70 | task_id: &'a str, 71 | message: Message, 72 | session_id: Option<&'a str>, 73 | ) -> Result { 74 | // Validate the message 75 | self.validate_message(&message).await?; 76 | 77 | // Transform the message if needed 78 | let transformed_message = self.transform_message(message).await?; 79 | 80 | // Process the message 81 | self.process_message(task_id, &transformed_message, session_id) 82 | .await 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /a2a-rs/src/port/mod.rs: -------------------------------------------------------------------------------- 1 | //! Ports (interfaces) for the A2A protocol 2 | //! 3 | //! Ports define the interfaces that our application needs, independent of implementation details. 4 | //! They represent the "what" - what operations our application needs to perform. 5 | //! 6 | //! ## Organization 7 | //! 8 | //! - **Business capability ports**: Focused interfaces for specific business capabilities 9 | //! - `authenticator`: Authentication and authorization 10 | //! - `message_handler`: Message processing 11 | //! - `task_manager`: Task lifecycle management 12 | //! - `notification_manager`: Push notifications 13 | //! - `streaming_handler`: Real-time updates 14 | 15 | // Business capability ports (focused domain interfaces) 16 | pub mod authenticator; 17 | pub mod message_handler; 18 | pub mod notification_manager; 19 | pub mod streaming_handler; 20 | pub mod task_manager; 21 | 22 | // Re-export business capability interfaces 23 | pub use authenticator::{ 24 | AuthContext, AuthContextExtractor, AuthPrincipal, Authenticator, CompositeAuthenticator, 25 | }; 26 | pub use message_handler::{AsyncMessageHandler, MessageHandler}; 27 | pub use notification_manager::{AsyncNotificationManager, NotificationManager}; 28 | pub use streaming_handler::{ 29 | AsyncStreamingHandler, StreamingHandler, Subscriber as StreamingSubscriber, UpdateEvent, 30 | }; 31 | pub use task_manager::{AsyncTaskManager, TaskManager}; 32 | -------------------------------------------------------------------------------- /a2a-rs/src/port/task_manager.rs: -------------------------------------------------------------------------------- 1 | //! Task management port definitions 2 | 3 | #[cfg(feature = "server")] 4 | use async_trait::async_trait; 5 | 6 | use crate::{ 7 | domain::{A2AError, Task, TaskIdParams, TaskQueryParams, TaskState}, 8 | Message, 9 | }; 10 | 11 | /// A trait for managing task lifecycle and operations 12 | pub trait TaskManager { 13 | /// Create a new task 14 | fn create_task(&self, task_id: &str, context_id: &str) -> Result; 15 | 16 | /// Get a task by ID with optional history 17 | fn get_task(&self, task_id: &str, history_length: Option) -> Result; 18 | 19 | /// Update task status with an optional message to add to history 20 | fn update_task_status( 21 | &self, 22 | task_id: &str, 23 | state: TaskState, 24 | message: Option, 25 | ) -> Result; 26 | 27 | /// Cancel a task 28 | fn cancel_task(&self, task_id: &str) -> Result; 29 | 30 | /// Check if a task exists 31 | fn task_exists(&self, task_id: &str) -> Result; 32 | 33 | /// List tasks with optional filtering 34 | fn list_tasks( 35 | &self, 36 | _context_id: Option<&str>, 37 | _limit: Option, 38 | ) -> Result, A2AError> { 39 | // Default implementation - can be overridden 40 | // Basic implementation that doesn't support filtering 41 | Err(A2AError::UnsupportedOperation( 42 | "Task listing not implemented".to_string(), 43 | )) 44 | } 45 | 46 | /// Get task metadata 47 | fn get_task_metadata( 48 | &self, 49 | task_id: &str, 50 | ) -> Result, A2AError> { 51 | let task = self.get_task(task_id, None)?; 52 | Ok(task.metadata.unwrap_or_default()) 53 | } 54 | 55 | /// Validate task parameters 56 | fn validate_task_params(&self, params: &TaskQueryParams) -> Result<(), A2AError> { 57 | if params.id.trim().is_empty() { 58 | return Err(A2AError::ValidationError { 59 | field: "task_id".to_string(), 60 | message: "Task ID cannot be empty".to_string(), 61 | }); 62 | } 63 | 64 | if let Some(history_length) = params.history_length { 65 | if history_length > 1000 { 66 | return Err(A2AError::ValidationError { 67 | field: "history_length".to_string(), 68 | message: "History length cannot exceed 1000".to_string(), 69 | }); 70 | } 71 | } 72 | 73 | Ok(()) 74 | } 75 | } 76 | 77 | #[cfg(feature = "server")] 78 | #[async_trait] 79 | /// An async trait for managing task lifecycle and operations 80 | pub trait AsyncTaskManager: Send + Sync { 81 | /// Create a new task 82 | async fn create_task<'a>( 83 | &self, 84 | task_id: &'a str, 85 | context_id: &'a str, 86 | ) -> Result; 87 | 88 | /// Get a task by ID with optional history 89 | async fn get_task<'a>( 90 | &self, 91 | task_id: &'a str, 92 | history_length: Option, 93 | ) -> Result; 94 | 95 | /// Update task status with an optional message to add to history 96 | async fn update_task_status<'a>( 97 | &self, 98 | task_id: &'a str, 99 | state: TaskState, 100 | message: Option, 101 | ) -> Result; 102 | 103 | /// Cancel a task 104 | async fn cancel_task<'a>(&self, task_id: &'a str) -> Result; 105 | 106 | /// Check if a task exists 107 | async fn task_exists<'a>(&self, task_id: &'a str) -> Result; 108 | 109 | /// List tasks with optional filtering 110 | async fn list_tasks<'a>( 111 | &self, 112 | _context_id: Option<&'a str>, 113 | _limit: Option, 114 | ) -> Result, A2AError> { 115 | // Default implementation - can be overridden 116 | // Basic implementation that doesn't support filtering 117 | Err(A2AError::UnsupportedOperation( 118 | "Task listing not implemented".to_string(), 119 | )) 120 | } 121 | 122 | /// Get task metadata 123 | async fn get_task_metadata<'a>( 124 | &self, 125 | task_id: &'a str, 126 | ) -> Result, A2AError> { 127 | let task = self.get_task(task_id, None).await?; 128 | Ok(task.metadata.unwrap_or_default()) 129 | } 130 | 131 | /// Validate task parameters 132 | async fn validate_task_params<'a>(&self, params: &'a TaskQueryParams) -> Result<(), A2AError> { 133 | if params.id.trim().is_empty() { 134 | return Err(A2AError::ValidationError { 135 | field: "task_id".to_string(), 136 | message: "Task ID cannot be empty".to_string(), 137 | }); 138 | } 139 | 140 | if let Some(history_length) = params.history_length { 141 | if history_length > 1000 { 142 | return Err(A2AError::ValidationError { 143 | field: "history_length".to_string(), 144 | message: "History length cannot exceed 1000".to_string(), 145 | }); 146 | } 147 | } 148 | 149 | Ok(()) 150 | } 151 | 152 | /// Get task with validation 153 | async fn get_task_validated<'a>(&self, params: &'a TaskQueryParams) -> Result { 154 | self.validate_task_params(params).await?; 155 | self.get_task(¶ms.id, params.history_length).await 156 | } 157 | 158 | /// Cancel task with validation 159 | async fn cancel_task_validated<'a>(&self, params: &'a TaskIdParams) -> Result { 160 | if params.id.trim().is_empty() { 161 | return Err(A2AError::ValidationError { 162 | field: "task_id".to_string(), 163 | message: "Task ID cannot be empty".to_string(), 164 | }); 165 | } 166 | 167 | self.cancel_task(¶ms.id).await 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /a2a-rs/src/services/client.rs: -------------------------------------------------------------------------------- 1 | //! Client interface traits 2 | 3 | use async_trait::async_trait; 4 | use futures::Stream; 5 | use std::pin::Pin; 6 | 7 | use crate::{ 8 | application::{json_rpc::A2ARequest, JSONRPCResponse}, 9 | domain::{ 10 | A2AError, Message, Task, TaskArtifactUpdateEvent, TaskPushNotificationConfig, 11 | TaskStatusUpdateEvent, 12 | }, 13 | }; 14 | 15 | #[async_trait] 16 | /// An async trait defining the methods an async client should implement 17 | pub trait AsyncA2AClient: Send + Sync { 18 | /// Send a raw request to the server and get a response 19 | async fn send_raw_request<'a>(&self, request: &'a str) -> Result; 20 | 21 | /// Send a structured request to the server and get a response 22 | async fn send_request<'a>(&self, request: &'a A2ARequest) -> Result; 23 | 24 | /// Send a message to a task 25 | async fn send_task_message<'a>( 26 | &self, 27 | task_id: &'a str, 28 | message: &'a Message, 29 | session_id: Option<&'a str>, 30 | history_length: Option, 31 | ) -> Result; 32 | 33 | /// Get a task by ID 34 | async fn get_task<'a>( 35 | &self, 36 | task_id: &'a str, 37 | history_length: Option, 38 | ) -> Result; 39 | 40 | /// Cancel a task 41 | async fn cancel_task<'a>(&self, task_id: &'a str) -> Result; 42 | 43 | /// Set up push notifications for a task 44 | async fn set_task_push_notification<'a>( 45 | &self, 46 | config: &'a TaskPushNotificationConfig, 47 | ) -> Result; 48 | 49 | /// Get push notification configuration for a task 50 | async fn get_task_push_notification<'a>( 51 | &self, 52 | task_id: &'a str, 53 | ) -> Result; 54 | 55 | /// Subscribe to task updates (for streaming) 56 | async fn subscribe_to_task<'a>( 57 | &self, 58 | task_id: &'a str, 59 | history_length: Option, 60 | ) -> Result> + Send>>, A2AError>; 61 | } 62 | 63 | /// Items that can be streamed from the server during task subscriptions.\n///\n/// When subscribing to streaming updates for a task, the server can send\n/// different types of items:\n/// - `Task`: The complete initial task state when subscription starts\n/// - `StatusUpdate`: Updates to the task's status (state changes, progress)\n/// - `ArtifactUpdate`: Notifications about new or updated artifacts\n///\n/// This allows clients to receive real-time updates about task progress\n/// and results as they become available. 64 | #[derive(Debug, Clone)] 65 | pub enum StreamItem { 66 | /// The initial task state 67 | Task(Task), 68 | /// A task status update 69 | StatusUpdate(TaskStatusUpdateEvent), 70 | /// A task artifact update 71 | ArtifactUpdate(TaskArtifactUpdateEvent), 72 | } 73 | -------------------------------------------------------------------------------- /a2a-rs/src/services/mod.rs: -------------------------------------------------------------------------------- 1 | //! Service layer for the A2A protocol 2 | //! 3 | //! Services provide application-level abstractions that orchestrate 4 | //! between ports and adapters. 5 | 6 | #[cfg(feature = "client")] 7 | pub mod client; 8 | 9 | #[cfg(feature = "server")] 10 | pub mod server; 11 | 12 | #[cfg(feature = "client")] 13 | pub use client::{AsyncA2AClient, StreamItem}; 14 | 15 | #[cfg(feature = "server")] 16 | pub use server::{AgentInfoProvider, AsyncA2ARequestProcessor}; 17 | -------------------------------------------------------------------------------- /a2a-rs/src/services/server.rs: -------------------------------------------------------------------------------- 1 | //! Server service traits 2 | 3 | use async_trait::async_trait; 4 | 5 | use crate::{ 6 | application::{json_rpc::A2ARequest, JSONRPCResponse}, 7 | domain::{A2AError, AgentCard, AgentSkill}, 8 | }; 9 | 10 | /// A trait for providing agent information 11 | #[async_trait] 12 | pub trait AgentInfoProvider: Send + Sync { 13 | /// Get the agent card 14 | async fn get_agent_card(&self) -> Result; 15 | 16 | /// Get the list of skills 17 | async fn get_skills(&self) -> Result, A2AError> { 18 | let card = self.get_agent_card().await?; 19 | Ok(card.skills) 20 | } 21 | 22 | /// Get a specific skill by ID 23 | async fn get_skill_by_id(&self, id: &str) -> Result, A2AError> { 24 | let skills = self.get_skills().await?; 25 | Ok(skills.into_iter().find(|s| s.id == id)) 26 | } 27 | 28 | /// Check if a skill exists 29 | async fn has_skill(&self, id: &str) -> Result { 30 | let skill = self.get_skill_by_id(id).await?; 31 | Ok(skill.is_some()) 32 | } 33 | } 34 | 35 | /// An async trait for processing A2A protocol requests 36 | #[async_trait] 37 | pub trait AsyncA2ARequestProcessor: Send + Sync { 38 | /// Process a raw JSON-RPC request string 39 | async fn process_raw_request<'a>(&self, request: &'a str) -> Result; 40 | 41 | /// Process a parsed A2A request 42 | async fn process_request<'a>( 43 | &self, 44 | request: &'a A2ARequest, 45 | ) -> Result; 46 | } 47 | -------------------------------------------------------------------------------- /a2a-rs/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common test utilities 2 | 3 | pub mod test_handler; 4 | 5 | pub use test_handler::TestBusinessHandler; 6 | -------------------------------------------------------------------------------- /a2a-rs/tests/property_based_test.proptest-regressions: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc ce4d749c2667c10f423ff1d3977b3ad14b2edc0891de8320890dd52759e1c3bf # shrinks to task_id = " ", context_id = "¡", states = [Submitted, Submitted, Submitted, Submitted, Submitted, Submitted], messages = [] 8 | -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | # A2A Protocol Specification 2 | 3 | This directory contains the Agent-to-Agent (A2A) Protocol specification split into focused, domain-specific files for better comprehension and compliance tracking. 4 | 5 | ## File Organization 6 | 7 | The specification has been organized into the following files: 8 | 9 | ### Core Domain Models 10 | - **[agent.json](./agent.json)** - Agent cards, capabilities, skills, and provider information 11 | - **[message.json](./message.json)** - Message structures, parts (text, file, data), and content types 12 | - **[task.json](./task.json)** - Task lifecycle, states, status, and artifacts 13 | 14 | ### Protocol Infrastructure 15 | - **[jsonrpc.json](./jsonrpc.json)** - JSON-RPC 2.0 base types and message structures 16 | - **[requests.json](./requests.json)** - Method-specific requests, responses, and parameters 17 | - **[errors.json](./errors.json)** - Error codes and types (both JSON-RPC and A2A-specific) 18 | 19 | ### Specialized Features 20 | - **[security.json](./security.json)** - Authentication schemes (API key, HTTP, OAuth2, OpenID Connect) 21 | - **[notifications.json](./notifications.json)** - Push notification configuration and authentication 22 | - **[events.json](./events.json)** - Streaming events for status and artifact updates 23 | 24 | ### Extensions 25 | - **[ap2.json](./ap2.json)** - AP2 (Agent Payments Protocol) extension for commerce capabilities 26 | 27 | ## Key A2A Protocol Methods 28 | 29 | The specification defines the following core methods: 30 | 31 | ### Message Methods 32 | 1. **`message/send`** - Send a message to an agent (blocking) 33 | 2. **`message/stream`** - Send a message with streaming response 34 | 35 | ### Task Management Methods 36 | 3. **`tasks/get`** - Retrieve task information and history 37 | 4. **`tasks/list`** - List tasks with filtering and pagination 38 | 5. **`tasks/cancel`** - Cancel an active task 39 | 6. **`tasks/resubscribe`** - Resubscribe to task updates 40 | 41 | ### Push Notification Methods 42 | 7. **`tasks/pushNotificationConfig/set`** - Configure push notifications for a task 43 | 8. **`tasks/pushNotificationConfig/get`** - Retrieve push notification configuration 44 | 9. **`tasks/pushNotificationConfig/list`** - List all push notification configurations for a task 45 | 10. **`tasks/pushNotificationConfig/delete`** - Delete a push notification configuration 46 | 47 | ### Agent Discovery Methods 48 | 11. **`agent/getAuthenticatedExtendedCard`** - Retrieve extended agent card for authenticated users 49 | 50 | ## Task States 51 | 52 | Tasks progress through these states: 53 | - `submitted` - Task received by agent 54 | - `working` - Agent is processing the task 55 | - `input-required` - Agent needs additional input 56 | - `completed` - Task finished successfully 57 | - `canceled` - Task was canceled 58 | - `failed` - Task failed due to an error 59 | - `rejected` - Task was rejected by agent 60 | - `auth-required` - Authentication needed 61 | - `unknown` - State unknown 62 | 63 | ## Error Codes 64 | 65 | The protocol defines specific error codes: 66 | - `-32700` to `-32603` - Standard JSON-RPC errors 67 | - `-32001` - Task not found 68 | - `-32002` - Task not cancelable 69 | - `-32003` - Push notifications not supported 70 | - `-32004` - Operation not supported 71 | - `-32005` - Content type not supported 72 | - `-32006` - Invalid agent response 73 | - `-32007` - Authenticated extended card not configured 74 | 75 | ## Usage for Implementation 76 | 77 | When implementing the A2A protocol: 78 | 79 | 1. Start with **agent.json** to understand agent capabilities and discovery 80 | 2. Reference **message.json** and **task.json** for core data structures 81 | 3. Use **requests.json** for method implementations 82 | 4. Handle errors according to **errors.json** 83 | 5. Implement security per **security.json** requirements 84 | 6. Add streaming support using **events.json** 85 | 7. Configure notifications via **notifications.json** 86 | 87 | Each file is self-contained with proper JSON Schema references to related files, making it easy to validate specific aspects of your implementation against the protocol specification. 88 | 89 | ## AP2 (Agent Payments Protocol) Extension 90 | 91 | The AP2 extension enables commerce and payment capabilities between agents. See **[ap2.json](./ap2.json)** for the complete schema. 92 | 93 | ### Key Concepts 94 | 95 | **Agent Roles:** 96 | - `merchant` - Handles payments and checkout 97 | - `shopper` - Makes purchases on behalf of users 98 | - `credentials-provider` - Supplies payment credentials 99 | - `payment-processor` - Processes transactions 100 | 101 | **Mandate Types:** 102 | 1. **IntentMandate** (`ap2.mandates.IntentMandate`) - User's purchase intent with constraints 103 | 2. **CartMandate** (`ap2.mandates.CartMandate`) - Merchant's cart and payment request 104 | 3. **PaymentMandate** (`ap2.mandates.PaymentMandate`) - Authorized payment details 105 | 106 | ### Extension URI 107 | 108 | `https://github.com/google-agentic-commerce/ap2/tree/v0.1` 109 | 110 | ### Usage 111 | 112 | To declare AP2 support in your AgentCard: 113 | 114 | 1. Add the extension URI to the `extensions` array 115 | 2. Specify at least one role in the `roles` array 116 | 3. Include mandates as Data parts in messages using the appropriate keys 117 | 118 | See the AP2 implementation plan and examples in the main repository for integration details. -------------------------------------------------------------------------------- /spec/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "A2A Event Definitions", 4 | "description": "Streaming events and updates for the A2A protocol", 5 | "definitions": { 6 | "TaskStatusUpdateEvent": { 7 | "description": "Sent by server during sendStream or subscribe requests", 8 | "properties": { 9 | "contextId": { 10 | "description": "The context the task is associated with", 11 | "type": "string" 12 | }, 13 | "final": { 14 | "description": "Indicates the end of the event stream", 15 | "type": "boolean" 16 | }, 17 | "kind": { 18 | "const": "status-update", 19 | "description": "Event type", 20 | "type": "string" 21 | }, 22 | "metadata": { 23 | "additionalProperties": {}, 24 | "description": "Extension metadata.", 25 | "type": "object" 26 | }, 27 | "status": { 28 | "$ref": "task.json#/definitions/TaskStatus", 29 | "description": "Current status of the task" 30 | }, 31 | "taskId": { 32 | "description": "Task id", 33 | "type": "string" 34 | } 35 | }, 36 | "required": [ 37 | "contextId", 38 | "final", 39 | "kind", 40 | "status", 41 | "taskId" 42 | ], 43 | "type": "object" 44 | }, 45 | "TaskArtifactUpdateEvent": { 46 | "description": "Sent by server during sendStream or subscribe requests", 47 | "properties": { 48 | "append": { 49 | "description": "Indicates if this artifact appends to a previous one", 50 | "type": "boolean" 51 | }, 52 | "artifact": { 53 | "$ref": "task.json#/definitions/Artifact", 54 | "description": "Generated artifact" 55 | }, 56 | "contextId": { 57 | "description": "The context the task is associated with", 58 | "type": "string" 59 | }, 60 | "kind": { 61 | "const": "artifact-update", 62 | "description": "Event type", 63 | "type": "string" 64 | }, 65 | "lastChunk": { 66 | "description": "Indicates if this is the last chunk of the artifact", 67 | "type": "boolean" 68 | }, 69 | "metadata": { 70 | "additionalProperties": {}, 71 | "description": "Extension metadata.", 72 | "type": "object" 73 | }, 74 | "taskId": { 75 | "description": "Task id", 76 | "type": "string" 77 | } 78 | }, 79 | "required": [ 80 | "artifact", 81 | "contextId", 82 | "kind", 83 | "taskId" 84 | ], 85 | "type": "object" 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /spec/notifications.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "A2A Push Notification Definitions", 4 | "description": "Push notification configuration and authentication for the A2A protocol", 5 | "definitions": { 6 | "PushNotificationConfig": { 7 | "description": "Defines the configuration for setting up push notifications for task updates.", 8 | "properties": { 9 | "authentication": { 10 | "$ref": "#/definitions/PushNotificationAuthenticationInfo", 11 | "description": "Optional authentication details for the agent to use when calling the notification URL." 12 | }, 13 | "id": { 14 | "description": "A unique identifier (e.g. UUID) for the push notification configuration, set by the client\nto support multiple notification callbacks.", 15 | "type": "string" 16 | }, 17 | "token": { 18 | "description": "A unique token for this task or session to validate incoming push notifications.", 19 | "type": "string" 20 | }, 21 | "url": { 22 | "description": "The callback URL where the agent should send push notifications.", 23 | "type": "string" 24 | } 25 | }, 26 | "required": [ 27 | "url" 28 | ], 29 | "type": "object" 30 | }, 31 | "PushNotificationAuthenticationInfo": { 32 | "description": "Defines authentication details for a push notification endpoint.", 33 | "properties": { 34 | "credentials": { 35 | "description": "Optional credentials required by the push notification endpoint.", 36 | "type": "string" 37 | }, 38 | "schemes": { 39 | "description": "A list of supported authentication schemes (e.g., 'Basic', 'Bearer').", 40 | "items": { 41 | "type": "string" 42 | }, 43 | "type": "array" 44 | } 45 | }, 46 | "required": [ 47 | "schemes" 48 | ], 49 | "type": "object" 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /spec/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "A2A Task Definitions", 4 | "description": "Task, artifact, and state management definitions for the A2A protocol", 5 | "definitions": { 6 | "Task": { 7 | "description": "Represents a single, stateful operation or conversation between a client and an agent.", 8 | "properties": { 9 | "artifacts": { 10 | "description": "A collection of artifacts generated by the agent during the execution of the task.", 11 | "items": { 12 | "$ref": "#/definitions/Artifact" 13 | }, 14 | "type": "array" 15 | }, 16 | "contextId": { 17 | "description": "A server-generated unique identifier (e.g. UUID) for maintaining context across multiple related tasks or interactions.", 18 | "type": "string" 19 | }, 20 | "history": { 21 | "description": "An array of messages exchanged during the task, representing the conversation history.", 22 | "items": { 23 | "$ref": "message.json#/definitions/Message" 24 | }, 25 | "type": "array" 26 | }, 27 | "id": { 28 | "description": "A unique identifier (e.g. UUID) for the task, generated by the server for a new task.", 29 | "type": "string" 30 | }, 31 | "kind": { 32 | "const": "task", 33 | "description": "The type of this object, used as a discriminator. Always 'task' for a Task.", 34 | "type": "string" 35 | }, 36 | "metadata": { 37 | "additionalProperties": {}, 38 | "description": "Optional metadata for extensions. The key is an extension-specific identifier.", 39 | "type": "object" 40 | }, 41 | "status": { 42 | "$ref": "#/definitions/TaskStatus", 43 | "description": "The current status of the task, including its state and a descriptive message." 44 | } 45 | }, 46 | "required": [ 47 | "contextId", 48 | "id", 49 | "kind", 50 | "status" 51 | ], 52 | "type": "object" 53 | }, 54 | "TaskState": { 55 | "description": "Represents the possible states of a Task.", 56 | "enum": [ 57 | "submitted", 58 | "working", 59 | "input-required", 60 | "completed", 61 | "canceled", 62 | "failed", 63 | "rejected", 64 | "auth-required", 65 | "unknown" 66 | ], 67 | "type": "string" 68 | }, 69 | "TaskStatus": { 70 | "description": "Represents the status of a task at a specific point in time.", 71 | "properties": { 72 | "message": { 73 | "$ref": "message.json#/definitions/Message", 74 | "description": "An optional, human-readable message providing more details about the current status." 75 | }, 76 | "state": { 77 | "$ref": "#/definitions/TaskState", 78 | "description": "The current state of the task's lifecycle." 79 | }, 80 | "timestamp": { 81 | "description": "An ISO 8601 datetime string indicating when this status was recorded.", 82 | "examples": [ 83 | "2023-10-27T10:00:00Z" 84 | ], 85 | "type": "string" 86 | } 87 | }, 88 | "required": [ 89 | "state" 90 | ], 91 | "type": "object" 92 | }, 93 | "Artifact": { 94 | "description": "Represents a file, data structure, or other resource generated by an agent during a task.", 95 | "properties": { 96 | "artifactId": { 97 | "description": "A unique identifier (e.g. UUID) for the artifact within the scope of the task.", 98 | "type": "string" 99 | }, 100 | "description": { 101 | "description": "An optional, human-readable description of the artifact.", 102 | "type": "string" 103 | }, 104 | "extensions": { 105 | "description": "The URIs of extensions that are relevant to this artifact.", 106 | "items": { 107 | "type": "string" 108 | }, 109 | "type": "array" 110 | }, 111 | "metadata": { 112 | "additionalProperties": {}, 113 | "description": "Optional metadata for extensions. The key is an extension-specific identifier.", 114 | "type": "object" 115 | }, 116 | "name": { 117 | "description": "An optional, human-readable name for the artifact.", 118 | "type": "string" 119 | }, 120 | "parts": { 121 | "description": "An array of content parts that make up the artifact.", 122 | "items": { 123 | "$ref": "message.json#/definitions/Part" 124 | }, 125 | "type": "array" 126 | } 127 | }, 128 | "required": [ 129 | "artifactId", 130 | "parts" 131 | ], 132 | "type": "object" 133 | } 134 | } 135 | } --------------------------------------------------------------------------------