Skip to main content

lean_ctx/core/knowledge_router/
mod.rs

1//! Runtime routing from task references to bounded context bundles.
2pub mod context_bundle;
3pub mod gate_integration;
4pub mod manifest_builder;
5pub mod planner;
6pub mod provider_bridge;
7pub mod receipt;
8pub mod reference_resolver;
9pub mod source_manifest;
10
11#[cfg(test)]
12#[path = "knowledge_router_tests.rs"]
13mod knowledge_router_tests;
14
15use crate::core::task_spine::TaskProfileLocal;
16pub use context_bundle::{BundleStrategy, ContextBundle};
17pub use manifest_builder::{
18    ProviderConfig, build_manifests_for_provider_ids, build_manifests_from_config, detect_config,
19};
20pub use planner::{ContextCandidate, QueryPlanner};
21pub use provider_bridge::{ProviderBridge, ProviderResolution};
22pub use receipt::KnowledgeReceipt;
23pub use reference_resolver::{PatternReferenceResolver, ReferenceResolver, ResolvedReference};
24pub use source_manifest::{SourceManifestEntry, builtin_manifests};
25use std::sync::Arc;
26
27#[derive(Debug, Clone, Default)]
28/// Routes task references into bounded knowledge bundles.
29pub struct KnowledgeRouter {
30    pub manifests: Vec<SourceManifestEntry>,
31    pub resolvers: Vec<Arc<dyn ReferenceResolver>>,
32}
33
34#[derive(Debug, Clone, Default)]
35/// Contains candidates, bundle, and receipt from knowledge routing.
36pub struct RoutingResult {
37    pub candidates: Vec<ContextCandidate>,
38    pub bundle: ContextBundle,
39    pub receipt: KnowledgeReceipt,
40}
41
42/// Router-derived hints for request-local compression.
43///
44/// References selected by the router identify message content that should be
45/// preserved verbatim so a compressor does not discard the context required to
46/// resolve the referenced source.
47#[derive(Debug, Clone, Default, PartialEq, Eq)]
48pub struct ContextAdvice {
49    protected_references: Vec<String>,
50}
51
52impl ContextAdvice {
53    pub fn is_empty(&self) -> bool {
54        self.protected_references.is_empty()
55    }
56
57    pub fn protects(&self, content: &str) -> bool {
58        self.protected_references
59            .iter()
60            .any(|reference| content.contains(reference))
61    }
62}
63
64impl KnowledgeRouter {
65    pub fn route(
66        &self,
67        task_id: &str,
68        query: &str,
69        profile: &TaskProfileLocal,
70        providers: &[SourceManifestEntry],
71        bridge: Option<&ProviderBridge<'_>>,
72    ) -> RoutingResult {
73        let mut references = self
74            .resolvers
75            .iter()
76            .flat_map(|resolver| resolver.resolve(query))
77            .collect::<Vec<_>>();
78        let dynamic_manifests = bridge.map(|bridge| {
79            let config = detect_config();
80            let mut manifests = build_manifests_from_config(&config);
81            let configured = manifests
82                .iter()
83                .any(|manifest| manifest.source_id != "local_files");
84            if !configured {
85                manifests = builtin_manifests();
86            }
87            let available = bridge.available_provider_ids();
88            for manifest in build_manifests_for_provider_ids(&available) {
89                if !manifests
90                    .iter()
91                    .any(|existing| existing.source_id == manifest.source_id)
92                {
93                    manifests.push(manifest);
94                }
95            }
96            let ids = available.iter().map(String::as_str).collect::<Vec<_>>();
97            let resolutions = bridge.resolve_from_providers(&references, &ids);
98            for (reference, resolution) in references.iter_mut().zip(resolutions) {
99                if resolution.resolved {
100                    reference.source_id = resolution.provider_id;
101                } else {
102                    reference.source_id.clear();
103                }
104            }
105            manifests
106        });
107        let manifests = if let Some(manifests) = dynamic_manifests.as_deref() {
108            manifests
109        } else if providers.is_empty() {
110            &self.manifests
111        } else {
112            providers
113        };
114        let budget = u64::from(profile.context_need_milli).max(250) * 4;
115        let candidates = QueryPlanner::plan(&references, manifests, budget);
116        let strategy = if profile.risk_signal == lean_ctx_protocol::RiskClass::Low {
117            BundleStrategy::Enriched
118        } else {
119            BundleStrategy::Governed
120        };
121        let bundle = context_bundle::create_bundle(task_id, &candidates, strategy);
122        let receipt = receipt::create_receipt(task_id, &bundle, &candidates, budget);
123        RoutingResult {
124            candidates,
125            bundle,
126            receipt,
127        }
128    }
129
130    /// Convert routing candidates into bounded, request-local compression
131    /// hints.  This deliberately exposes no provider payload: callers only
132    /// receive the references whose surrounding message text must survive
133    /// compression.
134    pub fn context_advice(
135        &self,
136        task_id: &str,
137        query: &str,
138        profile: &TaskProfileLocal,
139        providers: &[SourceManifestEntry],
140        bridge: Option<&ProviderBridge<'_>>,
141    ) -> ContextAdvice {
142        let mut protected_references = self
143            .route(task_id, query, profile, providers, bridge)
144            .candidates
145            .into_iter()
146            .filter_map(|candidate| candidate.reference)
147            .filter(|reference| !reference.is_empty())
148            .collect::<Vec<_>>();
149        protected_references.sort();
150        protected_references.dedup();
151
152        ContextAdvice {
153            protected_references,
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::core::knowledge_router::reference_resolver::ReferenceType;
162    use crate::core::providers::registry::global_registry;
163    use crate::core::task_spine::TaskProfileLocal;
164
165    fn reference(ref_type: ReferenceType, identifier: &str) -> ResolvedReference {
166        ResolvedReference {
167            ref_type,
168            identifier: identifier.into(),
169            source_id: String::new(),
170            confidence_milli: 900,
171        }
172    }
173
174    #[test]
175    fn test_provider_bridge_jira() {
176        let result = ProviderBridge::new(global_registry())
177            .resolve_from_providers(&[reference(ReferenceType::JiraIssue, "LEAN-42")], &["jira"]);
178        assert_eq!(result[0].provider_id, "jira");
179    }
180
181    #[test]
182    fn test_provider_bridge_github() {
183        let result = ProviderBridge::new(global_registry())
184            .resolve_from_providers(&[reference(ReferenceType::GitHubPR, "#42")], &["github"]);
185        assert_eq!(result[0].estimated_tokens, 1_000);
186    }
187
188    #[test]
189    fn test_provider_bridge_no_provider() {
190        {
191            let found = &ProviderBridge::new(global_registry()).resolve_from_providers(
192                &[reference(ReferenceType::Url, "https://example.com")],
193                &[],
194            );
195            assert!(found.is_empty(), "{found:?}");
196        }
197    }
198
199    #[test]
200    fn test_manifest_builder_empty() {
201        assert_eq!(
202            build_manifests_from_config(&ProviderConfig::default()).len(),
203            1
204        );
205    }
206
207    #[test]
208    fn test_manifest_builder_full() {
209        let config = ProviderConfig {
210            jira_configured: true,
211            github_configured: true,
212            gitlab_configured: true,
213            postgres_configured: false,
214            custom_providers: vec![],
215        };
216        assert_eq!(build_manifests_from_config(&config).len(), 4);
217    }
218
219    #[test]
220    fn test_router_with_bridge() {
221        let router = KnowledgeRouter {
222            manifests: builtin_manifests(),
223            resolvers: vec![Arc::new(PatternReferenceResolver)],
224        };
225        let profile = TaskProfileLocal::default();
226        let bridge = ProviderBridge::new(global_registry());
227        let result = router.route(
228            "task",
229            "review src/core/knowledge_router/mod.rs",
230            &profile,
231            &[],
232            Some(&bridge),
233        );
234        assert!(
235            !result.candidates.is_empty(),
236            "`result.candidates` is empty"
237        );
238    }
239
240    #[test]
241    fn context_advice_preserves_router_reference() {
242        let router = KnowledgeRouter {
243            manifests: builtin_manifests(),
244            resolvers: vec![Arc::new(PatternReferenceResolver)],
245        };
246        let profile = TaskProfileLocal::default();
247        let advice = router.context_advice(
248            "task",
249            "review src/core/knowledge_router/mod.rs",
250            &profile,
251            &builtin_manifests(),
252            None,
253        );
254
255        assert!(advice.protects("Please review src/core/knowledge_router/mod.rs"));
256    }
257}