{"id":33922510,"url":"https://github.com/agents-sh/radkit","last_synced_at":"2026-04-04T13:00:50.502Z","repository":{"id":311765703,"uuid":"1044984453","full_name":"agents-sh/radkit","owner":"agents-sh","description":"Rust Agent Development Kit","archived":false,"fork":false,"pushed_at":"2026-03-27T23:12:26.000Z","size":1670,"stargazers_count":56,"open_issues_count":2,"forks_count":5,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-28T01:16:12.358Z","etag":null,"topics":["agents","ai","ai-agents","ai-agents-framework"],"latest_commit_sha":null,"homepage":"https://radkit.rs/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/agents-sh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-08-26T13:42:07.000Z","updated_at":"2026-03-27T23:12:30.000Z","dependencies_parsed_at":"2025-08-26T16:07:48.960Z","dependency_job_id":"66f2fb44-aa1a-470c-aed0-14630ec80817","html_url":"https://github.com/agents-sh/radkit","commit_stats":null,"previous_names":["microagents/radkit","agents-sh/radkit"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/agents-sh/radkit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agents-sh%2Fradkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agents-sh%2Fradkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agents-sh%2Fradkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agents-sh%2Fradkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/agents-sh","download_url":"https://codeload.github.com/agents-sh/radkit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agents-sh%2Fradkit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31400460,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agents","ai","ai-agents","ai-agents-framework"],"created_at":"2025-12-12T09:04:01.476Z","updated_at":"2026-04-04T13:00:50.488Z","avatar_url":"https://github.com/agents-sh.png","language":"Rust","funding_links":[],"categories":["\u003ca name=\"Rust\"\u003e\u003c/a\u003eRust"],"sub_categories":[],"readme":"\u003cdiv style=\"text-align: center;\"\u003e\n  \u003cdiv class=\"centered-logo-text-group\"\u003e\n    \u003cimg src=\"docs/src/assets/logo.svg\" alt=\"RadKit Logo\" width=\"100\"\u003e\n    \u003ch1\u003eRadkit - Rust Agent Development Kit\u003c/h1\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n**A Rust SDK for building reliable AI agent systems with first-class [A2A protocol](https://a2a-protocol.org) support.**\n\nRadkit prioritizes developer experience and control above all else. \nDevelopers maintain complete control over agent behavior, execution flow, context management, and state. \n\nWhile the library provides abstractions, developers can always drop down to lower-level APIs when needed.\n\n\n[![Crates.io](https://img.shields.io/crates/v/radkit.svg)](https://crates.io/crates/radkit)\n[![Documentation](https://docs.rs/radkit/badge.svg)](https://docs.rs/radkit)\n[![License](https://img.shields.io/crates/l/radkit.svg)](LICENSE)\n\n---\n\n## Features\n\n- **A2A Protocol First** - Native support for Agent-to-Agent communication standard\n- **Unified LLM Interface** - Single API for Anthropic, OpenAI, Gemini, Grok, DeepSeek\n- **Tool Execution** - Automatic tool calling with multi-turn loops and state management\n- **Structured Outputs** - Type-safe response deserialization with JSON Schema\n- **Type Safety** - Leverage Rust's type system for reliability and correctness\n\n---\n\n## Installation\n\nAdd `radkit` to your `Cargo.toml`.\n\n#### Default (Minimal)\n\nFor using core types and helpers like `LlmFunction` and `LlmWorker` without the agent server runtime:\n\n```toml\n[dependencies]\nradkit = \"0.0.4\"\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"sync\", \"net\", \"process\", \"macros\"] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nschemars = \"1\"\n```\n\n#### With Agent Server Runtime\n\nTo include the runtime server handle and enable the full A2A agent server capabilities (on native targets), enable the `runtime` feature:\n\n```toml\n[dependencies]\nradkit = { version = \"0.0.4\", features = [\"runtime\"] }\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"sync\", \"net\", \"process\", \"macros\"] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nschemars = \"1\"\n```\n\n## Feature Flags\n\nRadkit ships optional capabilities that you can opt into per target:\n\n- `runtime`: Enables the native runtime handle, HTTP server, tracing, and other dependencies required to run A2A-compliant agents locally.\n- `agentskill`: Enables `AgentSkillDef`, `include_skill!`, and `with_skill_dir` for loading skills from `SKILL.md` files. Included in the `macros` feature by default.\n- `dev-ui`: Builds on top of `runtime` and serves an interactive UI (native-only) where you can trigger tasks, and inspect streaming output.\n- `task-store-sqlite`: Enables a native SQLite-backed `TaskStore` for persistent task, event, and state storage.\n\n## Core Concepts\n\n### Thread - Conversation Context\n\nA `Thread` represents the complete conversation history with the LLM, including system prompts and message exchanges.\n\n```rust\nuse radkit::models::{Thread, Event};\n\n// Simple thread from user message\nlet thread = Thread::from_user(\"Hello, world!\");\n\n// Thread with system prompt\nlet thread = Thread::from_system(\"You are a helpful coding assistant\")\n    .add_event(Event::user(\"Explain Rust ownership\"));\n\n// Multi-turn conversation\nlet thread = Thread::new(vec![\n    Event::user(\"What is 2+2?\"),\n    Event::assistant(\"2+2 equals 4.\"),\n    Event::user(\"What about 3+3?\"),\n]);\n\n// Builder pattern\nlet thread = Thread::new(vec![])\n    .with_system(\"You are an expert in mathematics\")\n    .add_event(Event::user(\"Calculate the area of a circle with radius 5\"));\n```\n\n**Type Conversions:**\n\n```rust\n// From string slice\nlet thread: Thread = \"Hello\".into();\n\n// From String\nlet thread: Thread = String::from(\"World\").into();\n\n// From Event\nlet thread: Thread = Event::user(\"Question\").into();\n\n// From Vec\u003cEvent\u003e\nlet thread: Thread = vec![\n    Event::user(\"First\"),\n    Event::assistant(\"Response\"),\n].into();\n```\n\n---\n\n### Content - Multi-Modal Messages\n\n`Content` represents the payload of a message, supporting text, images, documents, tool calls, and tool responses.\n\n```rust\nuse radkit::models::{Content, ContentPart};\nuse serde_json::json;\n\n// Simple text content\nlet content = Content::from_text(\"Hello!\");\n\n// Multi-part content\nlet content = Content::from_parts(vec![\n    ContentPart::Text(\"Check this image:\".to_string()),\n    ContentPart::from_data(\n        \"image/png\",\n        \"base64_encoded_image_data_here\",\n        Some(\"photo.png\".to_string())\n    )?,\n]);\n\n// Access text parts\nfor text in content.texts() {\n    println!(\"{}\", text);\n}\n\n// Query content\nif content.has_text() {\n    println!(\"First text: {}\", content.first_text().unwrap());\n}\n\nif content.has_tool_calls() {\n    println!(\"Tool calls: {}\", content.tool_calls().len());\n}\n\n// Join all text parts\nif let Some(combined) = content.joined_texts() {\n    println!(\"Combined: {}\", combined);\n}\n```\n\n---\n\n### Event - Conversation Messages\n\n`Event` represents a single message in a conversation with an associated role.\n\n```rust\nuse radkit::models::{Event, Role};\n\n// Create events with different roles\nlet system_event = Event::system(\"You are a helpful assistant\");\nlet user_event = Event::user(\"What is Rust?\");\nlet assistant_event = Event::assistant(\"Rust is a systems programming language...\");\n\n// Access event properties\nmatch event.role() {\n    Role::System =\u003e println!(\"System message\"),\n    Role::User =\u003e println!(\"User message\"),\n    Role::Assistant =\u003e println!(\"Assistant message\"),\n    Role::Tool =\u003e println!(\"Tool response\"),\n}\n\nlet content = event.content();\nprintln!(\"Message: {}\", content.first_text().unwrap_or(\"\"));\n```\n\n---\n\n## LLM Providers\n\nRadkit supports multiple LLM providers with a unified interface.\n\n### Anthropic (Claude)\n\n```rust\nuse radkit::models::providers::AnthropicLlm;\nuse radkit::models::{BaseLlm, Thread};\n\n// From environment variable (ANTHROPIC_API_KEY)\nlet llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\n\n// With explicit API key\nlet llm = AnthropicLlm::new(\"claude-sonnet-4-5-20250929\", \"sk-ant-...\");\n\n// With configuration\nlet llm = AnthropicLlm::from_env(\"claude-opus-4-1-20250805\")?\n    .with_max_tokens(8192)\n    .with_temperature(0.7);\n\n// Generate content\nlet thread = Thread::from_user(\"Explain quantum computing\");\nlet response = llm.generate_content(thread, None).await?;\n\nprintln!(\"Response: {}\", response.content().first_text().unwrap());\nprintln!(\"Tokens used: {}\", response.usage().total_tokens());\n```\n\n### OpenAI (GPT)\n\n```rust\nuse radkit::models::providers::OpenAILlm;\n\n// From environment variable (OPENAI_API_KEY)\nlet llm = OpenAILlm::from_env(\"gpt-4o\")?;\n\n// With configuration\nlet llm = OpenAILlm::from_env(\"gpt-4o-mini\")?\n    .with_max_tokens(2000)\n    .with_temperature(0.5);\n\nlet response = llm.generate(\"What is machine learning?\", None).await?;\n```\n\n### OpenRouter\n\nOpenRouter exposes an OpenAI-compatible endpoint that can route calls to hosted Anthropic, Google, Cohere, and other marketplace models behind a single API key.\n\n```rust\nuse radkit::models::providers::OpenRouterLlm;\n\n// From environment variable (OPENROUTER_API_KEY)\nlet llm = OpenRouterLlm::from_env(\"anthropic/claude-3.5-sonnet\")?\n    .with_site_url(\"https://example.com\") // optional attribution headers\n    .with_app_name(\"My Radkit Agent\");\n\nlet response = llm.generate(\"Summarize the latest release notes\", None).await?;\n```\n\n### Google Gemini\n\n```rust\nuse radkit::models::providers::GeminiLlm;\n\n// From environment variable (GEMINI_API_KEY)\nlet llm = GeminiLlm::from_env(\"gemini-2.0-flash-exp\")?;\n\nlet response = llm.generate(\"Explain neural networks\", None).await?;\n```\n\n### Grok (xAI)\n\n```rust\nuse radkit::models::providers::GrokLlm;\n\n// From environment variable (XAI_API_KEY)\nlet llm = GrokLlm::from_env(\"grok-2-latest\")?;\n\nlet response = llm.generate(\"What is the meaning of life?\", None).await?;\n```\n\n### DeepSeek\n\n```rust\nuse radkit::models::providers::DeepSeekLlm;\n\n// From environment variable (DEEPSEEK_API_KEY)\nlet llm = DeepSeekLlm::from_env(\"deepseek-chat\")?;\n\nlet response = llm.generate(\"Code review best practices\", None).await?;\n```\n\n---\n\n## LlmFunction - Simple Structured Outputs\n\n`LlmFunction\u003cT\u003e` is perfect for when you want structured, typed responses without tool execution.\n\n### Basic Usage\n\n```rust\nuse radkit::agent::LlmFunction;\nuse radkit::models::providers::AnthropicLlm;\nuse schemars::JsonSchema;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Serialize, Deserialize, JsonSchema)]\nstruct MovieRecommendation {\n    title: String,\n    year: u16,\n    genre: String,\n    rating: f32,\n    reason: String,\n}\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\n    let movie_fn = LlmFunction::\u003cMovieRecommendation\u003e::new(llm);\n\n    let recommendation = movie_fn\n        .run(\"Recommend a sci-fi movie for someone who loves The Matrix\")\n        .await?;\n\n    println!(\"🎬 {}\", recommendation.title);\n    println!(\"📅 Year: {}\", recommendation.year);\n    println!(\"🎭 Genre: {}\", recommendation.genre);\n    println!(\"⭐ Rating: {}/10\", recommendation.rating);\n    println!(\"💡 {}\", recommendation.reason);\n\n    Ok(())\n}\n```\n\n### With System Instructions\n\n```rust\n#[derive(Debug, Serialize, Deserialize, JsonSchema)]\nstruct CodeReview {\n    issues: Vec\u003cString\u003e,\n    suggestions: Vec\u003cString\u003e,\n    severity: String,\n}\n\nlet llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\n\nlet review_fn = LlmFunction::\u003cCodeReview\u003e::new_with_system_instructions(\n    llm,\n    \"You are a senior code reviewer. Be thorough but constructive.\"\n);\n\nlet code = r#\"\n    fn divide(a: i32, b: i32) -\u003e i32 {\n        a / b\n    }\n\"#;\n\nlet review = review_fn.run(format!(\"Review this code:\\n{}\", code)).await?;\n\nprintln!(\"Severity: {}\", review.severity);\nprintln!(\"\\nIssues:\");\nfor issue in review.issues {\n    println!(\"  - {}\", issue);\n}\nprintln!(\"\\nSuggestions:\");\nfor suggestion in review.suggestions {\n    println!(\"  - {}\", suggestion);\n}\n```\n\n### Multi-Turn Conversations\n\n```rust\n#[derive(Debug, Serialize, Deserialize, JsonSchema)]\nstruct Answer {\n    response: String,\n    confidence: f32,\n}\n\nlet llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\nlet qa_fn = LlmFunction::\u003cAnswer\u003e::new(llm);\n\n// First question\nlet (answer1, thread) = qa_fn\n    .run_and_continue(\"What is Rust?\")\n    .await?;\n\nprintln!(\"Q1: {}\", answer1.response);\n\n// Follow-up question (continues conversation)\nlet (answer2, thread) = qa_fn\n    .run_and_continue(\n        thread.add_event(Event::user(\"What are its main benefits?\"))\n    )\n    .await?;\n\nprintln!(\"Q2: {}\", answer2.response);\n\n// Another follow-up\nlet (answer3, _) = qa_fn\n    .run_and_continue(\n        thread.add_event(Event::user(\"Give me a code example\"))\n    )\n    .await?;\n\nprintln!(\"Q3: {}\", answer3.response);\n```\n\n### Complex Data Structures\n\n```rust\n#[derive(Debug, Serialize, Deserialize, JsonSchema)]\nstruct Recipe {\n    name: String,\n    prep_time_minutes: u32,\n    cook_time_minutes: u32,\n    servings: u8,\n    ingredients: Vec\u003cIngredient\u003e,\n    instructions: Vec\u003cString\u003e,\n    tags: Vec\u003cString\u003e,\n}\n\n#[derive(Debug, Serialize, Deserialize, JsonSchema)]\nstruct Ingredient {\n    name: String,\n    amount: String,\n    unit: String,\n}\n\nlet llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\nlet recipe_fn = LlmFunction::\u003cRecipe\u003e::new_with_system_instructions(\n    llm,\n    \"You are a professional chef. Provide detailed, accurate recipes.\"\n);\n\nlet recipe = recipe_fn\n    .run(\"Create a recipe for chocolate chip cookies\")\n    .await?;\n\nprintln!(\"🍪 {}\", recipe.name);\nprintln!(\"⏱️  Prep: {}min, Cook: {}min\", recipe.prep_time_minutes, recipe.cook_time_minutes);\nprintln!(\"👥 Servings: {}\", recipe.servings);\nprintln!(\"\\n📋 Ingredients:\");\nfor ingredient in recipe.ingredients {\n    println!(\"  - {} {} {}\", ingredient.amount, ingredient.unit, ingredient.name);\n}\nprintln!(\"\\n👨‍🍳 Instructions:\");\nfor (i, instruction) in recipe.instructions.iter().enumerate() {\n    println!(\"  {}. {}\", i + 1, instruction);\n}\n```\n\n---\n\n## LlmWorker - Tool Execution\n\n`LlmWorker\u003cT\u003e` adds automatic tool calling and multi-turn execution loops to `LlmFunction`.\n\n```rust\nuse radkit::agent::LlmWorker;\nuse radkit::models::providers::AnthropicLlm;\nuse radkit::tools::{tool, ToolResult};\nuse schemars::JsonSchema;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\n\n#[derive(Debug, Serialize, Deserialize, JsonSchema)]\nstruct WeatherReport {\n    location: String,\n    temperature: f64,\n    condition: String,\n    forecast: String,\n}\n\n// Define tool arguments\n#[derive(Deserialize, JsonSchema)]\nstruct GetWeatherArgs {\n    /// City name or location\n    location: String,\n}\n\n// Define the weather tool using the #[tool] macro\n#[tool(description = \"Get current weather for a location\")]\nasync fn get_weather(args: GetWeatherArgs) -\u003e ToolResult {\n    // In real app, call weather API here\n    let weather_data = json!({\n        \"temperature\": 72.5,\n        \"condition\": \"Sunny\",\n        \"humidity\": 65,\n        \"location\": args.location,\n    });\n\n    ToolResult::success(weather_data)\n}\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    // Create worker with tool\n    let llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\n    let worker = LlmWorker::\u003cWeatherReport\u003e::builder(llm)\n        .with_system_instructions(\"You are a weather assistant\")\n        .with_tool(get_weather)  // Pass the tool directly\n        .build();\n\n    // Run - LLM will automatically call the weather tool\n    let report = worker.run(\"What's the weather in San Francisco?\").await?;\n\n    println!(\"📍 Location: {}\", report.location);\n    println!(\"🌡️  Temperature: {}°F\", report.temperature);\n    println!(\"☀️  Condition: {}\", report.condition);\n    println!(\"📅 {}\", report.forecast);\n\n    Ok(())\n}\n```\n\n### Multiple Tools\n\n```rust\nuse radkit::tools::tool;\n\n#[derive(Debug, Serialize, Deserialize, JsonSchema)]\nstruct TravelPlan {\n    destination: String,\n    weather_summary: String,\n    hotel_recommendation: String,\n    estimated_cost: f64,\n}\n\n// Define tool argument structs\n#[derive(Deserialize, JsonSchema)]\nstruct WeatherArgs {\n    /// Location to get weather for\n    location: String,\n}\n\n#[derive(Deserialize, JsonSchema)]\nstruct HotelArgs {\n    /// Location to search hotels in\n    location: String,\n}\n\n#[derive(Deserialize, JsonSchema)]\nstruct TripCostArgs {\n    /// Hotel price per night\n    hotel_price: f64,\n    /// Number of nights\n    nights: i64,\n}\n\n// Define tools using the #[tool] macro\n#[tool(description = \"Get weather forecast\")]\nasync fn get_weather(args: WeatherArgs) -\u003e ToolResult {\n    ToolResult::success(json!({\n        \"forecast\": format!(\"Sunny and 75°F in {}\", args.location)\n    }))\n}\n\n#[tool(description = \"Search for hotels in a location\")]\nasync fn search_hotels(args: HotelArgs) -\u003e ToolResult {\n    ToolResult::success(json!({\n        \"hotels\": [{\n            \"name\": \"Grand Hotel\",\n            \"price\": 150,\n            \"rating\": 4.5,\n            \"location\": args.location\n        }]\n    }))\n}\n\n#[tool(description = \"Calculate estimated trip cost\")]\nasync fn calculate_trip_cost(args: TripCostArgs) -\u003e ToolResult {\n    let total = args.hotel_price * args.nights as f64 + 500.0; // +flight estimate\n    ToolResult::success(json!({\"total\": total}))\n}\n\nlet llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\nlet worker = LlmWorker::\u003cTravelPlan\u003e::builder(llm)\n    .with_system_instructions(\"You are a travel planning assistant\")\n    .with_tools(vec![get_weather, search_hotels, calculate_trip_cost])\n    .build();\n\nlet plan = worker.run(\"Plan a 3-day trip to Tokyo\").await?;\n\nprintln!(\"🗺️  {}\", plan.destination);\nprintln!(\"🌤️  {}\", plan.weather_summary);\nprintln!(\"🏨 {}\", plan.hotel_recommendation);\nprintln!(\"💰 Estimated cost: ${:.2}\", plan.estimated_cost);\n```\n\n### Stateful Tools\n\nTools can maintain state across calls using `ToolContext`.\n\n```rust\nuse radkit::tools::{tool, ToolContext};\n\n#[derive(Debug, Serialize, Deserialize, JsonSchema)]\nstruct ShoppingCart {\n    items: Vec\u003cString\u003e,\n    total_items: u32,\n    estimated_total: f64,\n}\n\n// Define tool arguments\n#[derive(Deserialize, JsonSchema)]\nstruct AddToCartArgs {\n    /// Item name to add\n    item: String,\n    /// Price of the item\n    price: f64,\n}\n\n// Add to cart tool with state management\n#[tool(description = \"Add an item to the shopping cart\")]\nasync fn add_to_cart(args: AddToCartArgs, ctx: ToolContext) -\u003e ToolResult {\n    // Get current cart\n    let mut items: Vec\u003cString\u003e = ctx\n        .state()\n        .get_state(\"items\")\n        .and_then(|v| serde_json::from_value(v).ok())\n        .unwrap_or_default();\n\n    let total_price: f64 = ctx\n        .state()\n        .get_state(\"total_price\")\n        .and_then(|v| v.as_f64())\n        .unwrap_or(0.0);\n\n    // Add item\n    items.push(args.item.clone());\n    let new_total = total_price + args.price;\n\n    // Update state\n    ctx.state().set_state(\"items\", json!(items));\n    ctx.state().set_state(\"total_price\", json!(new_total));\n\n    ToolResult::success(json!({\n        \"item_added\": args.item,\n        \"cart_size\": items.len(),\n        \"total\": new_total\n    }))\n}\n\nlet llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\nlet worker = LlmWorker::\u003cShoppingCart\u003e::builder(llm)\n    .with_tool(add_to_cart)\n    .build();\n\n// The worker can call add_to_cart multiple times, maintaining state\nlet cart = worker.run(\"Add a laptop for $999 and a mouse for $25\").await?;\n\nprintln!(\"🛒 Cart:\");\nfor item in cart.items {\n    println!(\"  - {}\", item);\n}\nprintln!(\"📦 Total items: {}\", cart.total_items);\nprintln!(\"💵 Total: ${:.2}\", cart.estimated_total);\n```\n\n---\n\n## A2A Agents\n\nRadkit provides first-class support for building [Agent-to-Agent (A2A) protocol](https://a2a-protocol.org) compliant agents. The framework ensures that if your code compiles, it's automatically A2A compliant.\n\n### What is A2A?\n\nThe A2A protocol is an open standard that enables seamless communication and collaboration between AI agents. It provides:\n- Standardized agent discovery via Agent Cards\n- Task lifecycle management (submitted, working, completed, etc.)\n- Multi-turn conversations with input-required states\n- Streaming support for long-running operations\n- Artifact generation for tangible outputs\n\n### Building A2A Agents\n\nAgents in radkit are composed of **skills**. Each skill handles a specific capability and is annotated with the `#[skill]` macro to provide A2A metadata.\n\n#### Defining a Skill\n\n```rust\nuse radkit::agent::{Artifact, LlmFunction, OnRequestResult, SkillHandler};\nuse radkit::errors::AgentError;\nuse radkit::macros::skill;\nuse radkit::models::{BaseLlm, Content};\nuse radkit::runtime::context::{ProgressSender, State};\nuse radkit::runtime::Runtime;\nuse schemars::JsonSchema;\nuse serde::{Deserialize, Serialize};\n\n// Define your output types\n#[derive(Serialize, Deserialize, JsonSchema)]\nstruct UserProfile {\n    name: String,\n    email: String,\n    role: String,\n}\n\n// Annotate with A2A metadata\n#[skill(\n    id = \"extract_profile\",\n    name = \"Profile Extractor\",\n    description = \"Extracts structured user profiles from text\",\n    tags = [\"extraction\", \"profiles\"],\n    examples = [\n        \"Extract profile: John Doe, john@example.com, Software Engineer\",\n        \"Parse this resume into a profile\"\n    ],\n    input_modes = [\"text/plain\", \"application/pdf\"],\n    output_modes = [\"application/json\"]\n)]\npub struct ProfileExtractorSkill;\n\n// Implement the SkillHandler trait\n#[cfg_attr(all(target_os = \"wasi\", target_env = \"p1\"), async_trait::async_trait(?Send))]\n#[cfg_attr(\n    not(all(target_os = \"wasi\", target_env = \"p1\")),\n    async_trait::async_trait\n)]\nimpl SkillHandler for ProfileExtractorSkill {\n    async fn on_request(\n        \u0026self,\n        state: \u0026mut State,\n        progress: \u0026ProgressSender,\n        runtime: \u0026dyn Runtime,\n        content: Content,\n    ) -\u003e Result\u003cOnRequestResult, AgentError\u003e {\n        // Get LLM from runtime\n        let llm = runtime.llm_provider().default_llm()?;\n\n        // Send intermediate update (A2A TaskState::Working)\n        progress.send_update(\"Analyzing text...\").await?;\n\n        // Use LLM function for extraction\n        let profile = extract_profile_data(llm)\n            .run(content.first_text().unwrap())\n            .await?;\n\n        // Create artifact (automatically becomes A2A Artifact)\n        let artifact = Artifact::from_json(\"user_profile.json\", \u0026profile)?;\n\n        // Return completion (A2A TaskState::Completed)\n        Ok(OnRequestResult::Completed {\n            message: Some(Content::from_text(\"Profile extracted successfully\")),\n            artifacts: vec![artifact],\n        })\n    }\n}\n\nfn extract_profile_data(llm: impl BaseLlm + 'static) -\u003e LlmFunction\u003cUserProfile\u003e {\n    LlmFunction::new_with_system_instructions(\n        llm,\n        \"Extract name, email, and role from the provided text.\"\n    )\n}\n```\n\n#### Multi-Turn Conversations\n\nSkills can request additional input from users when needed. Use **slot enums** to track different input states:\n\n```rust\nuse serde::{Deserialize, Serialize};\n\n// Define slot enum to track different input requirements\n#[derive(Serialize, Deserialize)]\nenum ProfileSlot {\n    Email,\n    PhoneNumber,\n    Department,\n}\n\n#[cfg_attr(all(target_os = \"wasi\", target_env = \"p1\"), async_trait::async_trait(?Send))]\n#[cfg_attr(\n    not(all(target_os = \"wasi\", target_env = \"p1\")),\n    async_trait::async_trait\n)]\nimpl SkillHandler for ProfileExtractorSkill {\n    async fn on_request(\n        \u0026self,\n        state: \u0026mut State,\n        progress: \u0026ProgressSender,\n        runtime: \u0026dyn Runtime,\n        content: Content,\n    ) -\u003e Result\u003cOnRequestResult, AgentError\u003e {\n        let llm = runtime.llm_provider().default_llm()?;\n        let profile = extract_profile_data(llm)\n            .run(content.first_text().unwrap())\n            .await?;\n\n        // Check what information is missing\n        if profile.email.is_empty() {\n            state.task().save(\"partial_profile\", \u0026profile)?;\n\n            // Request email - track with slot\n            state.set_slot(ProfileSlot::Email)?;\n            return Ok(OnRequestResult::InputRequired {\n                message: Content::from_text(\"Please provide the user's email address\"),\n            });\n        }\n\n        if profile.phone.is_empty() {\n            state.task().save(\"partial_profile\", \u0026profile)?;\n\n            // Request phone - different slot\n            state.set_slot(ProfileSlot::PhoneNumber)?;\n            return Ok(OnRequestResult::InputRequired {\n                message: Content::from_text(\"Please provide the user's phone number\"),\n            });\n        }\n\n        let artifact = Artifact::from_json(\"user_profile.json\", \u0026profile)?;\n        Ok(OnRequestResult::Completed {\n            message: Some(Content::from_text(\"Profile complete!\")),\n            artifacts: vec![artifact],\n        })\n    }\n\n    // Handle the follow-up input based on which slot was requested\n    async fn on_input_received(\n        \u0026self,\n        state: \u0026mut State,\n        progress: \u0026ProgressSender,\n        runtime: \u0026dyn Runtime,\n        content: Content,\n    ) -\u003e Result\u003cOnInputResult, AgentError\u003e {\n        // Get the slot to know which input we're continuing from\n        let slot: ProfileSlot = state.slot()?.unwrap();\n\n        // Load saved state\n        let mut profile: UserProfile = state.task()\n            .load(\"partial_profile\")?\n            .ok_or_else(|| anyhow!(\"No partial profile found\"))?;\n\n        // Handle different continuation paths based on slot\n        match slot {\n            ProfileSlot::Email =\u003e {\n                profile.email = content.first_text().unwrap().to_string();\n\n                // Check if we need phone number next\n                if profile.phone.is_empty() {\n                    state.task().save(\"partial_profile\", \u0026profile)?;\n                    state.set_slot(ProfileSlot::PhoneNumber)?;\n                    return Ok(OnInputResult::InputRequired {\n                        message: Content::from_text(\"Please provide your phone number\"),\n                    });\n                }\n            }\n            ProfileSlot::PhoneNumber =\u003e {\n                profile.phone = content.first_text().unwrap().to_string();\n\n                // Validate phone format\n                if !is_valid_phone(\u0026profile.phone) {\n                    return Ok(OnInputResult::Failed {\n                        error: \"Invalid phone number format\".to_string(),\n                    });\n                }\n            }\n            ProfileSlot::Department =\u003e {\n                profile.department = content.first_text().unwrap().to_string();\n            }\n        }\n\n        // Profile is complete\n        state.clear_slot();\n        let artifact = Artifact::from_json(\"user_profile.json\", \u0026profile)?;\n        Ok(OnInputResult::Completed {\n            message: Some(Content::from_text(\"Profile completed!\")),\n            artifacts: vec![artifact],\n        })\n    }\n}\n```\n\n#### Intermediate Updates and Partial Artifacts\n\nFor long-running operations, send progress updates and partial results:\n\n```rust\n#[cfg_attr(all(target_os = \"wasi\", target_env = \"p1\"), async_trait::async_trait(?Send))]\n#[cfg_attr(\n    not(all(target_os = \"wasi\", target_env = \"p1\")),\n    async_trait::async_trait\n)]\nimpl SkillHandler for ReportGeneratorSkill {\n    async fn on_request(\n        \u0026self,\n        state: \u0026mut State,\n        progress: \u0026ProgressSender,\n        runtime: \u0026dyn Runtime,\n        content: Content,\n    ) -\u003e Result\u003cOnRequestResult, AgentError\u003e {\n        let llm = runtime.llm_provider().default_llm()?;\n\n        // Send intermediate status (A2A TaskStatusUpdateEvent with state=working)\n        progress.send_update(\"Analyzing data...\").await?;\n\n        let analysis = analyze_data(llm.clone())\n            .run(content.first_text().unwrap())\n            .await?;\n\n        // Send partial artifact (A2A TaskArtifactUpdateEvent)\n        let partial = Artifact::from_json(\"analysis.json\", \u0026analysis)?;\n        progress.send_artifact(partial).await?;\n\n        // Another update\n        progress.send_update(\"Generating visualizations...\").await?;\n\n        let charts = generate_charts(llm.clone())\n            .run(\u0026analysis)\n            .await?;\n\n        // Another partial artifact\n        let charts_artifact = Artifact::from_json(\"charts.json\", \u0026charts)?;\n        progress.send_artifact(charts_artifact).await?;\n\n        // Final compilation\n        progress.send_update(\"Compiling final report...\").await?;\n\n        let report = compile_report(llm)\n            .run(\u0026analysis, \u0026charts)\n            .await?;\n\n        // Return final state with final artifact\n        let final_artifact = Artifact::from_json(\"report.json\", \u0026report)?;\n        Ok(OnRequestResult::Completed {\n            message: Some(Content::from_text(\"Report complete!\")),\n            artifacts: vec![final_artifact],\n        })\n    }\n}\n```\n\n#### Composing an Agent\n\n```rust\nuse radkit::agent::{Agent, AgentDefinition};\nuse radkit::models::providers::AnthropicLlm;\nuse radkit::runtime::Runtime;\n\npub fn configure_agent() -\u003e AgentDefinition {\n    Agent::builder()\n        .with_name(\"My A2A Agent\")\n        .with_description(\"An intelligent agent with multiple skills\")\n        // Skills automatically provide metadata from #[skill] macro\n        .with_skill(ProfileExtractorSkill)\n        .with_skill(ReportGeneratorSkill)\n        .with_skill(DataAnalysisSkill)\n        .build()\n}\n\n// Local development\n#[cfg(not(all(target_os = \"wasi\", target_env = \"p1\")))]\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let llm = AnthropicLlm::from_env(\"claude-sonnet-4-5-20250929\")?;\n    Runtime::builder(configure_agent(), llm)\n        .build()\n        .serve(\"127.0.0.1:8080\")\n        .await?;\n\n    Ok(())\n}\n```\n\n### How Radkit Guarantees A2A Compliance\n\nRadkit ensures A2A compliance through **compile-time guarantees** and automatic protocol mapping:\n\n#### 1. Typed State Management\n\n```rust\npub enum OnRequestResult {\n    InputRequired { message: Content, slot: SkillSlot },    // → A2A: state=input-required\n    Completed { message: Option\u003cContent\u003e, artifacts: Vec\u003cArtifact\u003e }, // → A2A: state=completed\n    Failed { error: String },                                // → A2A: state=failed\n    Rejected { reason: String },                             // → A2A: state=rejected\n}\n```\n\n**Guarantee:** You can only return valid A2A task states. Invalid states won't compile.\n\n#### 2. Intermediate Updates\n\n```rust\n// Always maps to A2A TaskState::Working with final=false\nprogress.send_update(\"Processing...\").await?;\n\n// Always creates A2A TaskArtifactUpdateEvent\nprogress.send_artifact(artifact).await?;\n```\n\n**Guarantee:** You cannot accidentally send terminal states or mark intermediate updates as final.\n\n#### 3. Automatic Metadata Generation\n\nThe `#[skill]` macro automatically generates:\n- A2A `AgentSkill` entries for the Agent Card\n- MIME type validation based on `input_modes`/`output_modes`\n- Proper skill discovery metadata\n\n**Guarantee:** Your Agent Card is always consistent with your skill implementations.\n\n#### 4. Protocol Type Mapping\n\nThe framework automatically converts between radkit types and A2A protocol types:\n\n| Radkit Type | A2A Protocol Type |\n|---|---|\n| `Content` | `Message` with `Part[]` |\n| `Artifact::from_json()` | `Artifact` with `DataPart` |\n| `Artifact::from_text()` | `Artifact` with `TextPart` |\n| `OnRequestResult::Completed` | `Task` with `state=TASK_STATE_COMPLETED` |\n| `OnRequestResult::InputRequired` | `Task` with `state=TASK_STATE_INPUT_REQUIRED` |\n\n**Guarantee:** You never handle A2A protocol types directly. The framework ensures correct serialization.\n\n#### 5. Lifecycle Enforcement\n\n```rust\n// ✅ Allowed: Send intermediate updates during execution\nprogress.send_update(\"Working...\").await?;\n\n// ✅ Allowed: Send partial artifacts any time\nprogress.send_artifact(artifact).await?;\n\n// ✅ Allowed: Return terminal state with final artifacts\nOk(OnRequestResult::Completed {\n    artifacts: vec![final_artifact],\n    ..\n})\n\n// ❌ Not possible: Can't send \"completed\" state during execution\n// ❌ Not possible: Can't mark intermediate update as final\n// ❌ Not possible: Can't send invalid task states\n```\n\n**Guarantee:** The type system prevents protocol violations at compile time.\n\n#### How These Guarantees Work\n\nRadkit enforces A2A compliance through several type-level mechanisms:\n\n**1. Unrepresentable Invalid States**\n\nThe `OnRequestResult` and `OnInputResult` enums only expose valid A2A states as variants. There's no way to construct an invalid state because the type system doesn't allow it:\n\n```rust\n// ✅ This compiles - valid A2A state\nOk(OnRequestResult::Completed { message: None, artifacts: vec![] })\n\n// ❌ This doesn't compile - InvalidState doesn't exist\nOk(OnRequestResult::InvalidState { ... })  // Compilation error!\n```\n\n**2. Restricted Method APIs**\n\nMethods like `progress.send_update()` are internally hardcoded to use `TASK_STATE_WORKING` with no final flag. The API doesn't expose parameters that would allow setting invalid combinations:\n\n```rust\n// Implementation detail (in radkit internals):\npub async fn send_update(\u0026self, message: impl Into\u003cContent\u003e) -\u003e Result\u003c()\u003e {\n    // Always sends TASK_STATE_WORKING\n    // No way for developers to override this behaviour\n}\n```\n\n**3. Separation of Concerns via Return Types**\n\nIntermediate updates go through `ProgressSender` methods, while final states are only set via return values from `on_request()` and `on_input_received()`. This architectural separation, enforced by Rust's type system, makes it impossible to accidentally mark an intermediate update as final or send a terminal state mid-execution:\n\n```rust\n// During execution: Only intermediate methods available via ProgressSender\nprogress.send_update(\"Working...\").await?;  // Always non-final\n\n// At completion: Only way to set final state is via return\nOk(OnRequestResult::Completed { ... })  // Compiler ensures this ends execution\n```\n\n**4. Compile-Time WASM Compatibility**\n\nThe library uses conditional compilation and the `compat` module to ensure WASM portability while maintaining the same API surface. The `?Send` trait bound is conditionally applied based on target:\n\n```rust\n#[cfg_attr(all(target_os = \"wasi\", target_env = \"p1\"), async_trait(?Send))]\n#[cfg_attr(not(all(target_os = \"wasi\", target_env = \"p1\")), async_trait)]\n```\n\nThis means WASM compatibility is verified at compile time — if your agent compiles for native targets, it will compile for WASM without code changes.\n\n---\n\n### AgentSkills — File-Based LLM Skills\n\nIn addition to programmatic Rust skills, radkit supports **AgentSkills** — skills defined entirely in a `SKILL.md` file, with no Rust code required. The LLM reads the instructions and drives the task.\n\nAgentSkills follow the [AgentSkills specification](https://agentskills.io/specification). A skill is a directory containing a `SKILL.md` file:\n\n```\nskills/\n└── text-summariser/\n    └── SKILL.md\n```\n\nThe `SKILL.md` file has YAML frontmatter followed by Markdown instructions:\n\n```markdown\n---\nname: text-summariser\ndescription: Summarises text. Use when the user asks to summarise or condense text.\nlicense: MIT\n---\n\nYou are a precise text summariser.\n\n## Instructions\n\n1. Read the provided text carefully.\n2. Write a concise summary capturing the essential information.\n\n## Output format\n\nRespond with a JSON object:\n{ \"status\": \"complete\", \"message\": \"Your summary here.\" }\n\nIf no text has been provided yet:\n{ \"status\": \"needs_input\", \"message\": \"Please provide the text to summarise.\" }\n```\n\n#### Registering AgentSkills\n\nThere are two ways to register an AgentSkill:\n\n**Compile-time embedding** — `SKILL.md` is baked into the binary (like `include_str!`). No filesystem I/O at startup. Works on WASM.\n\n```rust\nuse radkit::{agent::Agent, include_skill};\n\nlet agent = Agent::builder()\n    .with_name(\"Text Agent\")\n    .with_skill_def(include_skill!(\"./skills/text-summariser\"))\n    .build();\n```\n\n**Runtime loading** — `SKILL.md` is read from disk at startup. Useful when you want to update skills without recompiling.\n\n```rust\nlet agent = Agent::builder()\n    .with_name(\"Text Agent\")\n    .with_skill_dir(\"./skills/text-summariser\")?\n    .build();\n```\n\nBoth produce identical `SkillRegistration`s at runtime. You can mix programmatic Rust skills and AgentSkills freely:\n\n```rust\nAgent::builder()\n    .with_name(\"My Agent\")\n    .with_skill(MyRustSkill)                                        // Rust skill\n    .with_skill_def(include_skill!(\"./skills/text-summariser\"))     // compile-time AgentSkill\n    .with_skill_dir(\"./skills/translate\")?                          // runtime AgentSkill\n    .build()\n```\n\n#### Multi-turn AgentSkills\n\nAgentSkills support multi-turn conversations out of the box. If the LLM responds with `\"status\": \"needs_input\"`, the task enters `InputRequired` state and the full conversation thread is preserved in the slot. When the user replies, the thread is replayed and the LLM continues from where it left off.\n\nThe `WorkStatus` enum drives this:\n\n| LLM responds with | Task state |\n|---|---|\n| `{ \"status\": \"complete\", \"message\": \"...\" }` | `Completed` |\n| `{ \"status\": \"needs_input\", \"message\": \"...\" }` | `InputRequired` (multi-turn continues) |\n| `{ \"status\": \"failed\", \"reason\": \"...\" }` | `Failed` |\n\n#### Feature flag\n\nAgentSkill support requires the `agentskill` feature (included in `macros` by default):\n\n```toml\n[dependencies]\nradkit = { version = \"0.0.4\", features = [\"runtime\", \"agentskill\"] }\n```\n\n---\n\n### Example: Complete A2A Agent\n\nSee the [hr_agent example](examples/hr_agent/) for a complete multi-skill A2A agent with:\n- Resume processing with multi-turn input handling\n- Onboarding plan generation with intermediate updates\n- IT account creation via remote agent delegation\n- Full A2A protocol compliance\n\n---\n\n## Contributing\n\nContributions welcome!\n\n\u003e We love agentic coding. We use Claude-Code, Gemini, Codex.\n\u003e That doesn't mean this is a random vibe-coded project. Everything in this project is carefully crafted.\n\u003e And we expect your contributions to be well-thought-out and have reasons for the changes you submit.\n\n1. Follow the [AGENTS.md](radkit/AGENTS.md)\n2. Add tests for new features\n3. Update documentation\n4. Ensure `cargo fmt` and `cargo clippy` pass\n\n---\n\n## License\n\nMIT\n\n---\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagents-sh%2Fradkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fagents-sh%2Fradkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagents-sh%2Fradkit/lists"}