codex_skills_extension/
sources.rs1use std::fmt;
2use std::sync::Arc;
3
4use crate::catalog::SkillCatalog;
5use crate::catalog::SkillProviderError;
6use crate::catalog::SkillProviderResult;
7use crate::catalog::SkillReadResult;
8use crate::catalog::SkillSearchResult;
9use crate::catalog::SkillSourceKind;
10use crate::provider::SkillListQuery;
11use crate::provider::SkillProvider;
12use crate::provider::SkillReadRequest;
13use crate::provider::SkillSearchRequest;
14
15#[derive(Clone)]
16pub struct SkillProviderSource {
17 kind: SkillSourceKind,
18 label: String,
19 provider: Arc<dyn SkillProvider>,
20}
21
22impl SkillProviderSource {
23 pub fn new(
24 kind: SkillSourceKind,
25 label: impl Into<String>,
26 provider: Arc<dyn SkillProvider>,
27 ) -> Self {
28 Self {
29 kind,
30 label: label.into(),
31 provider,
32 }
33 }
34
35 pub fn host(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self {
36 Self::new(SkillSourceKind::Host, label, provider)
37 }
38
39 pub fn executor(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self {
40 Self::new(SkillSourceKind::Executor, label, provider)
41 }
42
43 pub fn orchestrator(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self {
44 Self::new(SkillSourceKind::Orchestrator, label, provider)
45 }
46
47 fn should_list(&self, query: &SkillListQuery) -> bool {
48 match &self.kind {
49 SkillSourceKind::Host => query.include_host_skills,
50 SkillSourceKind::Executor => !query.executor_roots.is_empty(),
51 SkillSourceKind::Orchestrator => query.include_orchestrator_skills,
52 SkillSourceKind::Custom(_) => true,
53 }
54 }
55
56 fn owns_kind(&self, kind: &SkillSourceKind) -> bool {
57 &self.kind == kind
58 }
59}
60
61impl fmt::Debug for SkillProviderSource {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 formatter
64 .debug_struct("SkillProviderSource")
65 .field("kind", &self.kind)
66 .field("label", &self.label)
67 .finish()
68 }
69}
70
71#[derive(Clone, Default, Debug)]
72pub struct SkillProviders {
73 sources: Vec<SkillProviderSource>,
74}
75
76impl SkillProviders {
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn with_provider(mut self, source: SkillProviderSource) -> Self {
82 self.sources.push(source);
83 self
84 }
85
86 pub fn with_host_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self {
87 self.sources
88 .push(SkillProviderSource::host("host", provider));
89 self
90 }
91
92 pub fn with_executor_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self {
93 self.sources
94 .push(SkillProviderSource::executor("executor", provider));
95 self
96 }
97
98 pub fn with_orchestrator_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self {
99 self.sources
100 .push(SkillProviderSource::orchestrator("orchestrator", provider));
101 self
102 }
103
104 pub(crate) fn has_orchestrator_provider(&self) -> bool {
105 self.sources
106 .iter()
107 .any(|source| source.kind == SkillSourceKind::Orchestrator)
108 }
109
110 pub(crate) fn has_host_provider(&self) -> bool {
111 self.sources
112 .iter()
113 .any(|source| source.kind == SkillSourceKind::Host)
114 }
115
116 pub(crate) async fn list_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
117 self.list_matching(&query, |source| source.should_list(&query))
118 .await
119 }
120
121 pub(crate) async fn list_orchestrator_for_turn(
122 &self,
123 query: SkillListQuery,
124 ) -> SkillProviderResult<SkillCatalog> {
125 let mut catalog = SkillCatalog::default();
126
127 for source in self
128 .sources
129 .iter()
130 .filter(|source| source.kind == SkillSourceKind::Orchestrator)
131 {
132 let source_catalog = source.provider.list(query.clone()).await.map_err(|err| {
133 SkillProviderError::new(format!(
134 "{} skills unavailable: {}",
135 source.label, err.message
136 ))
137 })?;
138 catalog.extend(source_catalog);
139 }
140
141 Ok(catalog)
142 }
143
144 pub(crate) async fn list_executor_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
145 self.list_matching(&query, |source| source.kind == SkillSourceKind::Executor)
146 .await
147 }
148
149 pub(crate) async fn list_host_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
150 self.list_matching(&query, |source| source.kind == SkillSourceKind::Host)
151 .await
152 }
153
154 async fn list_matching(
155 &self,
156 query: &SkillListQuery,
157 should_list: impl Fn(&SkillProviderSource) -> bool,
158 ) -> SkillCatalog {
159 let mut catalog = SkillCatalog::default();
160
161 for source in self.sources.iter().filter(|source| should_list(source)) {
162 extend_catalog(
163 &mut catalog,
164 source.provider.list(query.clone()).await,
165 source.label.as_str(),
166 );
167 }
168
169 catalog
170 }
171
172 pub(crate) async fn read(
173 &self,
174 request: SkillReadRequest,
175 ) -> Result<SkillReadResult, SkillProviderError> {
176 let mut last_error = None;
177 for source in self
178 .sources
179 .iter()
180 .filter(|source| source.owns_kind(&request.authority.kind))
181 {
182 match source.provider.read(request.clone()).await {
183 Ok(result) => return Ok(result),
184 Err(err) => last_error = Some(err),
185 }
186 }
187
188 match last_error {
189 Some(err) => Err(err),
190 None => Err(SkillProviderError::new(format!(
191 "{} skill provider is not configured",
192 request.authority.kind
193 ))),
194 }
195 }
196
197 pub async fn search(
198 &self,
199 request: SkillSearchRequest,
200 ) -> Result<SkillSearchResult, SkillProviderError> {
201 let mut last_error = None;
202 for source in self
203 .sources
204 .iter()
205 .filter(|source| source.owns_kind(&request.authority.kind))
206 {
207 match source.provider.search(request.clone()).await {
208 Ok(result) => return Ok(result),
209 Err(err) => last_error = Some(err),
210 }
211 }
212
213 match last_error {
214 Some(err) => Err(err),
215 None => Err(SkillProviderError::new(format!(
216 "{} skill provider is not configured",
217 request.authority.kind
218 ))),
219 }
220 }
221}
222
223fn extend_catalog(
224 catalog: &mut SkillCatalog,
225 result: Result<SkillCatalog, SkillProviderError>,
226 label: &str,
227) {
228 match result {
229 Ok(source_catalog) => catalog.extend(source_catalog),
230 Err(err) => catalog
231 .warnings
232 .push(format!("{label} skills unavailable: {}", err.message)),
233 }
234}