Skip to content

Skills

In Radkit, the fundamental unit of capability for an A2A agent is the Skill. A skill handles a specific type of task. An agent is a collection of one or more skills.

There are two kinds of skills:

  • Programmatic Rust skills — logic implemented in Rust, annotated with #[skill].
  • AgentSkills — LLM-driven skills defined in a SKILL.md file, no Rust code required.

Both register with the same AgentBuilder API and are indistinguishable at runtime.


Annotate your struct with #[skill] to provide A2A metadata. This metadata populates the agent card and guides the negotiator LLM when routing incoming messages to the right skill.

use radkit::macros::skill;
#[skill(
id = "extract_profile",
name = "Profile Extractor",
description = "Extracts structured user profiles from text",
tags = ["extraction", "profiles"],
examples = [
"Extract a profile from: John Doe, john@example.com",
"Parse this resume into a profile"
],
input_modes = ["text/plain"],
output_modes = ["application/json"]
)]
pub struct ProfileExtractorSkill;

Implement SkillHandler to define the skill’s logic. The only required method is on_request, called for every new task assigned to the skill.

use radkit::agent::{Artifact, LlmFunction, OnRequestResult, SkillHandler};
use radkit::errors::AgentResult;
use radkit::models::Content;
use radkit::runtime::context::{ProgressSender, State};
use radkit::runtime::AgentRuntime;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, JsonSchema)]
struct UserProfile {
name: String,
email: String,
role: String,
}
#[async_trait::async_trait]
impl SkillHandler for ProfileExtractorSkill {
async fn on_request(
&self,
_state: &mut State,
_progress: &ProgressSender,
runtime: &dyn AgentRuntime,
content: Content,
) -> AgentResult<OnRequestResult> {
let llm = runtime.default_llm();
let profile = LlmFunction::<UserProfile>::new_with_shared_model(
llm,
Some("Extract the user's name, email, and role from the text.".into()),
)
.run(content)
.await?;
let artifact = Artifact::from_json("user_profile.json", &profile)?;
Ok(OnRequestResult::Completed {
message: Some(Content::from_text("Profile extracted successfully.")),
artifacts: vec![artifact],
})
}
}
VariantA2A task stateWhen to use
Completed { message, artifacts }completedTask finished successfully
InputRequired { message, slot }input-requiredNeed more information from the user
Failed { error }failedUnrecoverable error
Rejected { reason }rejectedSkill cannot handle this request

AgentSkills let you define a skill entirely in a SKILL.md file. The LLM reads the instructions and drives the task — no Rust implementation required. This follows the AgentSkills specification.

skills/
└── text-summariser/
└── SKILL.md

The directory name must match the name field in the frontmatter.

The file must start with YAML frontmatter followed by Markdown instructions:

---
name: text-summariser
description: Summarises text into a concise overview. Use when the user asks to summarise or condense text.
license: MIT
allowed-tools: Bash(python3:*)
---
You are a precise text summariser.
## Instructions
1. Read the provided text carefully.
2. Write a concise summary that captures the essential information.
## Output format
Respond with:
{ "status": "complete", "message": "Your summary here." }
If no text has been provided:
{ "status": "needs_input", "message": "Please provide the text to summarise." }
FieldRequiredDescription
nameYesLowercase letters, numbers, hyphens only. Must match directory name. Max 64 chars.
descriptionYesWhat the skill does and when to use it. Shown to the negotiator. Max 1024 chars.
licenseNoLicense name or path to a bundled license file.
compatibilityNoEnvironment requirements (packages, network access, etc.).
allowed-toolsNoSpace-delimited list of pre-approved tool names.
metadataNoArbitrary key-value pairs for custom use.

Compile-time embeddingSKILL.md is baked into the binary at compile time (like include_str!). No filesystem I/O at startup. Works on WASM.

use radkit::{agent::Agent, include_skill};
let agent = Agent::builder()
.with_name("My Agent")
.with_skill_def(include_skill!("./skills/text-summariser"))
.build();

Runtime loadingSKILL.md is read from disk at startup. Useful when you want to add or update skills without recompiling.

let agent = Agent::builder()
.with_name("My Agent")
.with_skill_dir("./skills/text-summariser")?
.build();

Mixing both kinds — programmatic and file-based skills can coexist freely:

Agent::builder()
.with_name("My Agent")
.with_skill(ProfileExtractorSkill) // Rust skill
.with_skill_def(include_skill!("./skills/summarise")) // compile-time AgentSkill
.with_skill_dir("./skills/translate")? // runtime AgentSkill
.build()

AgentSkills support multi-turn conversations automatically. The LLM signals intent through the status field in its JSON response:

LLM responds withWhat happens
{ "status": "complete", "message": "..." }Task completes, message returned to user
{ "status": "needs_input", "message": "..." }Task pauses, user is asked the question, conversation continues
{ "status": "failed", "reason": "..." }Task fails with the given reason

The full conversation thread is preserved between turns — the LLM always has complete context when resuming.

AgentSkill support requires the agentskill feature, which is included in the macros feature (enabled by default):

[dependencies]
# agentskill is included in macros (default)
radkit = { version = "0.0.4", features = ["runtime"] }
# Or enable explicitly
radkit = { version = "0.0.4", features = ["runtime", "agentskill"] }

Pass the agent builder directly to Runtime::builder — it injects the LLM into AgentSkill handlers automatically during build().

use radkit::runtime::Runtime;
Runtime::builder(
Agent::builder()
.with_name("My Agent")
.with_skill(MyRustSkill)
.with_skill_def(include_skill!("./skills/summarise")),
llm,
)
.build()
.serve("0.0.0.0:8080")
.await?;