Skip to main content

tea_context/providers/
skills.rs

1use std::str::FromStr;
2
3use crate::{
4    BudgetBehavior, CacheScope, ContextError, ContextErrorCode, ContextProvider,
5    ContextProviderFuture, ContextProviderId, ContextRequest, PromptAuthority, PromptModule,
6    PromptModuleId, PromptPriority, PromptProvenance, PromptSegment, PromptSegmentId,
7    SkillMetadata, TrustLevel,
8};
9
10/// Maximum active skill metadata entries.
11pub const MAX_ACTIVE_SKILLS: usize = 128;
12
13/// Deterministic model-visible active skill metadata provider.
14#[derive(Debug, Clone)]
15pub struct SkillMetadataProvider {
16    id: ContextProviderId,
17    skills: Vec<SkillMetadata>,
18}
19
20impl SkillMetadataProvider {
21    /// Creates a canonical active skill provider.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error for too many or duplicate skill IDs.
26    pub fn new(mut skills: Vec<SkillMetadata>) -> Result<Self, ContextError> {
27        if skills.len() > MAX_ACTIVE_SKILLS {
28            return Err(ContextError::new(
29                ContextErrorCode::BoundsExceeded,
30                "active skill collection is too large",
31            ));
32        }
33        skills.sort_by(|left, right| left.id().cmp(right.id()));
34        if skills
35            .windows(2)
36            .any(|items| items[0].id() == items[1].id())
37        {
38            return Err(ContextError::new(
39                ContextErrorCode::DuplicateIdentity,
40                "active skill ID is duplicated",
41            ));
42        }
43        Ok(Self {
44            id: ContextProviderId::from_str("builtin.skill_metadata").map_err(value_error)?,
45            skills,
46        })
47    }
48}
49
50impl ContextProvider for SkillMetadataProvider {
51    fn id(&self) -> &ContextProviderId {
52        &self.id
53    }
54
55    fn provide(&self, _request: ContextRequest) -> ContextProviderFuture<'_> {
56        let id = self.id.clone();
57        let skills = self.skills.clone();
58        Box::pin(async move {
59            if skills.is_empty() {
60                return Ok(Vec::new());
61            }
62            let segments = skills
63                .into_iter()
64                .map(|skill| {
65                    let segment_id = format!("skill.{}.metadata", skill.id().as_str());
66                    PromptSegment::new(
67                        PromptSegmentId::from_str(&segment_id).map_err(value_error)?,
68                        format!(
69                            "Skill `{}`: {} Invoke explicitly with `{}`.",
70                            skill.id(),
71                            skill.description(),
72                            skill.invocation()
73                        ),
74                        PromptProvenance::new(
75                            id.clone(),
76                            "skill_metadata",
77                            Some(skill.id().to_string()),
78                        )
79                        .map_err(value_error)?,
80                        TrustLevel::Delegated,
81                        CacheScope::Profile,
82                        BudgetBehavior::Omit,
83                    )
84                    .map_err(value_error)
85                })
86                .collect::<Result<Vec<_>, ContextError>>()?;
87            Ok(vec![
88                PromptModule::new(
89                    PromptModuleId::from_str("skill.active_metadata").map_err(value_error)?,
90                    PromptAuthority::Skill,
91                    PromptPriority::new(0),
92                    segments,
93                )
94                .map_err(value_error)?,
95            ])
96        })
97    }
98}
99
100fn value_error(error: impl std::fmt::Display) -> ContextError {
101    ContextError::new(ContextErrorCode::InvalidValue, error.to_string())
102}