use crate::agent::skill::{RegisteredSkill, SkillHandler, SkillMetadata};
use std::sync::Arc;
#[cfg(feature = "agentskill")]
use crate::agent::agentskill::AgentSkillDef;
const DEFAULT_AGENT_VERSION: &str = "0.0.1";
pub struct AgentDefinition {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) description: Option<String>,
pub(crate) dispatcher_prompt: Option<String>,
pub(crate) skills: Vec<SkillRegistration>,
}
pub struct AgentBuilder {
inner: AgentDefinition,
#[cfg(feature = "agentskill")]
pub(crate) pending_skill_defs: Vec<AgentSkillDef>,
}
pub struct Agent;
pub struct SkillRegistration {
pub(crate) metadata: Arc<SkillMetadata>,
pub(crate) handler: Arc<dyn SkillHandler>,
}
impl Agent {
#[must_use]
pub fn builder() -> AgentBuilder {
AgentBuilder {
inner: AgentDefinition {
name: String::new(),
version: DEFAULT_AGENT_VERSION.to_string(),
description: None,
dispatcher_prompt: None,
skills: Vec::new(),
},
#[cfg(feature = "agentskill")]
pending_skill_defs: Vec::new(),
}
}
}
impl AgentBuilder {
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.inner.name = name.into();
self
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.inner.version = version.into();
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.inner.description = Some(description.into());
self
}
#[must_use]
pub fn with_dispatcher_prompt(mut self, prompt: impl Into<String>) -> Self {
self.inner.dispatcher_prompt = Some(prompt.into());
self
}
#[must_use]
pub fn with_skill<T>(mut self, skill: T) -> Self
where
T: RegisteredSkill + 'static,
{
self.inner.skills.push(SkillRegistration {
metadata: T::metadata(),
handler: Arc::new(skill),
});
self
}
#[cfg(feature = "agentskill")]
#[must_use]
pub fn with_skill_def(mut self, def: AgentSkillDef) -> Self {
self.pending_skill_defs.push(def);
self
}
#[cfg(feature = "agentskill")]
pub fn with_skill_dir(
mut self,
path: impl AsRef<std::path::Path>,
) -> Result<Self, crate::errors::AgentError> {
let def = AgentSkillDef::from_dir(path)?;
self.pending_skill_defs.push(def);
Ok(self)
}
#[must_use]
pub fn build(mut self) -> AgentDefinition {
if self.inner.version.trim().is_empty() {
self.inner.version = DEFAULT_AGENT_VERSION.to_string();
}
self.inner
}
#[cfg(feature = "agentskill")]
#[allow(dead_code)]
pub(crate) fn into_parts(
mut self,
) -> (
AgentDefinition,
Vec<crate::agent::agentskill::AgentSkillDef>,
) {
if self.inner.version.trim().is_empty() {
self.inner.version = DEFAULT_AGENT_VERSION.to_string();
}
(self.inner, self.pending_skill_defs)
}
}
impl From<AgentDefinition> for AgentBuilder {
fn from(def: AgentDefinition) -> Self {
Self {
inner: def,
#[cfg(feature = "agentskill")]
pending_skill_defs: Vec::new(),
}
}
}
impl AgentDefinition {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn version(&self) -> &str {
&self.version
}
#[must_use]
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
#[must_use]
pub fn dispatcher_prompt(&self) -> Option<&str> {
self.dispatcher_prompt.as_deref()
}
#[must_use]
pub fn skills(&self) -> &[SkillRegistration] {
&self.skills
}
}
impl SkillRegistration {
#[must_use]
pub fn name(&self) -> &str {
&self.metadata.name
}
#[must_use]
pub fn id(&self) -> &str {
&self.metadata.id
}
#[must_use]
pub fn description(&self) -> &str {
&self.metadata.description
}
#[must_use]
pub fn handler(&self) -> &dyn SkillHandler {
&*self.handler
}
#[must_use]
pub fn handler_arc(&self) -> Arc<dyn SkillHandler> {
Arc::clone(&self.handler)
}
#[must_use]
pub fn metadata(&self) -> &SkillMetadata {
&self.metadata
}
#[must_use]
pub fn metadata_arc(&self) -> Arc<SkillMetadata> {
Arc::clone(&self.metadata)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::skill::{OnInputResult, OnRequestResult, SkillSlot};
use crate::errors::AgentError;
use crate::models::Content;
#[test]
fn build_applies_defaults() {
let agent = Agent::builder().with_name("Test").build();
assert_eq!(agent.name(), "Test");
assert_eq!(agent.version(), DEFAULT_AGENT_VERSION);
assert!(agent.description().is_none());
assert!(agent.dispatcher_prompt().is_none());
assert!(agent.skills().is_empty());
}
#[test]
fn build_preserves_all_fields() {
let agent = Agent::builder()
.with_name("Custom Agent")
.with_version("1.2.3")
.with_description("A helpful description")
.with_dispatcher_prompt("Route wisely")
.with_skill(DummySkill)
.build();
assert_eq!(agent.name(), "Custom Agent");
assert_eq!(agent.version(), "1.2.3");
assert_eq!(agent.description(), Some("A helpful description"));
assert_eq!(agent.dispatcher_prompt(), Some("Route wisely"));
let skills = agent.skills();
assert_eq!(skills.len(), 1);
assert_eq!(skills[0].id(), DummySkill::metadata().id);
assert_eq!(skills[0].name(), DummySkill::metadata().name);
}
#[test]
fn skills_maintain_registration_order() {
let agent = Agent::builder()
.with_skill(DummySkill)
.with_skill(SecondarySkill)
.build();
let skills = agent.skills();
assert_eq!(skills.len(), 2);
assert_eq!(skills[0].id(), DummySkill::metadata().id);
assert_eq!(skills[1].id(), SecondarySkill::metadata().id);
}
struct DummySkill;
#[cfg_attr(
all(target_os = "wasi", target_env = "p1"),
async_trait::async_trait(?Send)
)]
#[cfg_attr(
not(all(target_os = "wasi", target_env = "p1")),
async_trait::async_trait
)]
impl SkillHandler for DummySkill {
async fn on_request(
&self,
_state: &mut crate::runtime::context::State,
_progress: &crate::runtime::context::ProgressSender,
_runtime: &dyn crate::runtime::AgentRuntime,
_content: Content,
) -> Result<OnRequestResult, AgentError> {
Ok(OnRequestResult::Completed {
message: None,
artifacts: Vec::new(),
})
}
async fn on_input_received(
&self,
_state: &mut crate::runtime::context::State,
_progress: &crate::runtime::context::ProgressSender,
_runtime: &dyn crate::runtime::AgentRuntime,
_content: Content,
) -> Result<OnInputResult, AgentError> {
Ok(OnInputResult::InputRequired {
message: Content::from_text("Need more input"),
slot: SkillSlot::new("dummy"),
})
}
}
impl RegisteredSkill for DummySkill {
fn metadata() -> Arc<SkillMetadata> {
Arc::new(SkillMetadata::new(
"dummy_skill",
"Dummy Skill",
"A test skill used for verifying registration behaviour.",
&[],
&[],
&[],
&[],
))
}
}
struct SecondarySkill;
#[cfg_attr(
all(target_os = "wasi", target_env = "p1"),
async_trait::async_trait(?Send)
)]
#[cfg_attr(
not(all(target_os = "wasi", target_env = "p1")),
async_trait::async_trait
)]
impl SkillHandler for SecondarySkill {
async fn on_request(
&self,
_state: &mut crate::runtime::context::State,
_progress: &crate::runtime::context::ProgressSender,
_runtime: &dyn crate::runtime::AgentRuntime,
_content: Content,
) -> Result<OnRequestResult, AgentError> {
Ok(OnRequestResult::Rejected {
reason: Content::from_text("Not supported"),
})
}
}
impl RegisteredSkill for SecondarySkill {
fn metadata() -> Arc<SkillMetadata> {
Arc::new(SkillMetadata::new(
"secondary_skill",
"Secondary Skill",
"A second skill for ordering tests.",
&[],
&[],
&[],
&[],
))
}
}
#[cfg(feature = "agentskill")]
mod agentskill_builder_tests {
use super::*;
use crate::agent::agentskill::AgentSkillDef;
const VALID_SKILL_MD: &str = "\
---
name: test-skill
description: A skill used in builder tests.
---
## Instructions
Do the thing.
";
#[test]
fn with_skill_def_queues_pending_def() {
let def = AgentSkillDef::from_skill_md_str(VALID_SKILL_MD, "").expect("valid skill");
let builder = Agent::builder().with_name("Test").with_skill_def(def);
assert_eq!(builder.pending_skill_defs.len(), 1);
assert_eq!(builder.pending_skill_defs[0].id(), "test-skill");
}
#[test]
fn multiple_skill_defs_maintain_order() {
let def1 = AgentSkillDef::from_skill_md_str(VALID_SKILL_MD, "").expect("valid");
let def2 = AgentSkillDef::from_skill_md_str(
"---\nname: second-skill\ndescription: Another skill.\n---\nbody",
"",
)
.expect("valid second skill");
let builder = Agent::builder().with_skill_def(def1).with_skill_def(def2);
assert_eq!(builder.pending_skill_defs.len(), 2);
assert_eq!(builder.pending_skill_defs[0].id(), "test-skill");
assert_eq!(builder.pending_skill_defs[1].id(), "second-skill");
}
#[test]
fn build_splits_correctly_via_into_parts() {
let def = AgentSkillDef::from_skill_md_str(VALID_SKILL_MD, "").expect("valid skill");
let (agent_def, pending) = Agent::builder()
.with_name("Test")
.with_skill_def(def)
.into_parts();
assert_eq!(agent_def.skills().len(), 0);
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].id(), "test-skill");
}
#[test]
fn with_skill_dir_returns_error_for_nonexistent_path() {
let result = Agent::builder().with_skill_dir("/does/not/exist");
assert!(result.is_err());
}
#[test]
fn agentskill_and_rust_skill_can_coexist() {
let def = AgentSkillDef::from_skill_md_str(VALID_SKILL_MD, "").expect("valid skill");
let builder = Agent::builder().with_skill(DummySkill).with_skill_def(def);
assert_eq!(builder.inner.skills.len(), 1);
assert_eq!(builder.pending_skill_defs.len(), 1);
}
#[test]
fn from_agentdefinition_produces_empty_pending_defs() {
let def = Agent::builder().with_name("Test").build();
let builder: AgentBuilder = def.into();
assert_eq!(builder.pending_skill_defs.len(), 0);
}
}
}