Skip to main content

organism_intent/
resolution.rs

1//! Intent resolution — maps intents to the packs, capabilities, and invariants
2//! needed for convergence.
3//!
4//! Four resolution levels:
5//!
6//! 1. **Declarative** — intent explicitly declares which packs it needs
7//! 2. **Structural** — resolver matches fact prefixes to pack metadata
8//! 3. **Semantic** — huddle matches outcome description to pack capabilities
9//! 4. **Learned** — prior calibration from execution history predicts pack needs
10//!
11//! Resolution runs after admission, before planning. The output is an
12//! `IntentBinding` that tells the runtime which agents to register
13//! with the Converge engine.
14
15use serde::{Deserialize, Serialize};
16
17// ── Intent Binding ─────────────────────────────────────────────────
18
19/// The output of intent resolution. Tells the runtime what to wire up.
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct IntentBinding {
22    /// Which domain packs to register with the engine.
23    pub packs: Vec<PackRequirement>,
24    /// Which capabilities the intent needs (OCR, web, vision, etc.).
25    pub capabilities: Vec<CapabilityRequirement>,
26    /// Additional invariants to enforce beyond pack defaults.
27    pub invariants: Vec<String>,
28    /// How the binding was resolved.
29    pub resolution: ResolutionTrace,
30}
31
32/// A domain pack needed by the intent.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct PackRequirement {
35    pub pack_name: String,
36    pub reason: String,
37    pub confidence: f64,
38    pub source: ResolutionLevel,
39}
40
41/// A capability needed by the intent.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct CapabilityRequirement {
44    pub capability: String,
45    pub reason: String,
46    pub confidence: f64,
47    pub source: ResolutionLevel,
48}
49
50/// Which resolution level produced the binding.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ResolutionLevel {
54    /// Intent explicitly declared its packs.
55    Declarative,
56    /// Resolver matched fact prefixes to pack metadata.
57    Structural,
58    /// Huddle matched outcome to pack descriptions.
59    Semantic,
60    /// Prior calibration predicted from execution history.
61    Learned,
62}
63
64/// How the resolution was performed — for traceability.
65#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66pub struct ResolutionTrace {
67    pub levels_attempted: Vec<ResolutionLevel>,
68    pub levels_contributed: Vec<ResolutionLevel>,
69    /// Number of prior episodes consulted (level 4).
70    pub prior_episodes_consulted: usize,
71    /// Confidence that the binding is complete.
72    pub completeness_confidence: f64,
73}
74
75// ── Declarative Binding (Level 1) ──────────────────────────────────
76
77/// Builder for declaring an intent's resource needs explicitly.
78/// This is what apps use today.
79///
80/// ```rust,ignore
81/// let binding = DeclarativeBinding::new()
82///     .pack("customers", "lead qualification workflow")
83///     .pack("linkedin_research", "enrich with LinkedIn data")
84///     .capability("web", "capture company website")
85///     .invariant("lead_has_source")
86///     .build();
87/// ```
88#[derive(Debug, Clone, Default)]
89pub struct DeclarativeBinding {
90    packs: Vec<PackRequirement>,
91    capabilities: Vec<CapabilityRequirement>,
92    invariants: Vec<String>,
93}
94
95impl DeclarativeBinding {
96    #[must_use]
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    #[must_use]
102    pub fn pack(mut self, name: impl Into<String>, reason: impl Into<String>) -> Self {
103        self.packs.push(PackRequirement {
104            pack_name: name.into(),
105            reason: reason.into(),
106            confidence: 1.0,
107            source: ResolutionLevel::Declarative,
108        });
109        self
110    }
111
112    #[must_use]
113    pub fn capability(mut self, name: impl Into<String>, reason: impl Into<String>) -> Self {
114        self.capabilities.push(CapabilityRequirement {
115            capability: name.into(),
116            reason: reason.into(),
117            confidence: 1.0,
118            source: ResolutionLevel::Declarative,
119        });
120        self
121    }
122
123    #[must_use]
124    pub fn invariant(mut self, name: impl Into<String>) -> Self {
125        self.invariants.push(name.into());
126        self
127    }
128
129    #[must_use]
130    pub fn build(self) -> IntentBinding {
131        IntentBinding {
132            packs: self.packs,
133            capabilities: self.capabilities,
134            invariants: self.invariants,
135            resolution: ResolutionTrace {
136                levels_attempted: vec![ResolutionLevel::Declarative],
137                levels_contributed: vec![ResolutionLevel::Declarative],
138                prior_episodes_consulted: 0,
139                completeness_confidence: 1.0,
140            },
141        }
142    }
143}
144
145// ── Resolution Trait ───────────────────────────────────────────────
146
147/// Resolves an intent to its resource binding.
148///
149/// Implementations exist for each level. The runtime chains them:
150/// declarative first, then structural fills gaps, semantic adds
151/// uncertain matches, learned adjusts confidences from history.
152pub trait IntentResolver: Send + Sync {
153    fn level(&self) -> ResolutionLevel;
154    fn resolve(&self, intent: &super::IntentPacket, current: &IntentBinding) -> IntentBinding;
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn declarative_binding_builds_correctly() {
163        let binding = DeclarativeBinding::new()
164            .pack("customers", "lead qualification")
165            .pack("linkedin_research", "enrich leads")
166            .capability("web", "capture company page")
167            .invariant("lead_has_source")
168            .build();
169
170        assert_eq!(binding.packs.len(), 2);
171        assert_eq!(binding.capabilities.len(), 1);
172        assert_eq!(binding.invariants.len(), 1);
173        assert_eq!(binding.packs[0].pack_name, "customers");
174        assert_eq!(binding.packs[0].source, ResolutionLevel::Declarative);
175        assert!((binding.resolution.completeness_confidence - 1.0).abs() < f64::EPSILON);
176    }
177
178    #[test]
179    fn declarative_binding_empty() {
180        let binding = DeclarativeBinding::new().build();
181        assert!(binding.packs.is_empty());
182        assert!(binding.capabilities.is_empty());
183        assert!(binding.invariants.is_empty());
184        assert_eq!(
185            binding.resolution.levels_attempted,
186            vec![ResolutionLevel::Declarative]
187        );
188        assert_eq!(
189            binding.resolution.levels_contributed,
190            vec![ResolutionLevel::Declarative]
191        );
192        assert_eq!(binding.resolution.prior_episodes_consulted, 0);
193    }
194
195    #[test]
196    fn declarative_binding_pack_confidence_is_one() {
197        let binding = DeclarativeBinding::new().pack("test", "reason").build();
198        assert!((binding.packs[0].confidence - 1.0).abs() < f64::EPSILON);
199    }
200
201    #[test]
202    fn declarative_binding_capability_confidence_is_one() {
203        let binding = DeclarativeBinding::new()
204            .capability("ocr", "doc processing")
205            .build();
206        assert!((binding.capabilities[0].confidence - 1.0).abs() < f64::EPSILON);
207    }
208
209    #[test]
210    fn declarative_binding_multiple_invariants() {
211        let binding = DeclarativeBinding::new()
212            .invariant("inv_a")
213            .invariant("inv_b")
214            .invariant("inv_c")
215            .build();
216        assert_eq!(binding.invariants, vec!["inv_a", "inv_b", "inv_c"]);
217    }
218
219    #[test]
220    fn declarative_binding_default() {
221        let binding = DeclarativeBinding::default();
222        assert!(binding.packs.is_empty());
223        assert!(binding.capabilities.is_empty());
224        assert!(binding.invariants.is_empty());
225    }
226
227    #[test]
228    fn intent_binding_default() {
229        let binding = IntentBinding::default();
230        assert!(binding.packs.is_empty());
231        assert!(binding.capabilities.is_empty());
232        assert!(binding.invariants.is_empty());
233        assert!(binding.resolution.levels_attempted.is_empty());
234        assert!(binding.resolution.levels_contributed.is_empty());
235        assert_eq!(binding.resolution.prior_episodes_consulted, 0);
236        assert!((binding.resolution.completeness_confidence - 0.0).abs() < f64::EPSILON);
237    }
238
239    #[test]
240    fn resolution_trace_default() {
241        let trace = ResolutionTrace::default();
242        assert!(trace.levels_attempted.is_empty());
243        assert!(trace.levels_contributed.is_empty());
244        assert_eq!(trace.prior_episodes_consulted, 0);
245        assert!((trace.completeness_confidence - 0.0).abs() < f64::EPSILON);
246    }
247
248    #[test]
249    fn resolution_level_all_variants_distinct() {
250        let variants = [
251            ResolutionLevel::Declarative,
252            ResolutionLevel::Structural,
253            ResolutionLevel::Semantic,
254            ResolutionLevel::Learned,
255        ];
256        for (i, a) in variants.iter().enumerate() {
257            for (j, b) in variants.iter().enumerate() {
258                assert_eq!(i == j, a == b);
259            }
260        }
261    }
262
263    #[test]
264    fn resolution_level_serde_roundtrip() {
265        for level in [
266            ResolutionLevel::Declarative,
267            ResolutionLevel::Structural,
268            ResolutionLevel::Semantic,
269            ResolutionLevel::Learned,
270        ] {
271            let json = serde_json::to_string(&level).unwrap();
272            let back: ResolutionLevel = serde_json::from_str(&json).unwrap();
273            assert_eq!(level, back);
274        }
275    }
276
277    #[test]
278    fn resolution_level_snake_case() {
279        assert_eq!(
280            serde_json::to_string(&ResolutionLevel::Declarative).unwrap(),
281            "\"declarative\""
282        );
283        assert_eq!(
284            serde_json::to_string(&ResolutionLevel::Structural).unwrap(),
285            "\"structural\""
286        );
287        assert_eq!(
288            serde_json::to_string(&ResolutionLevel::Semantic).unwrap(),
289            "\"semantic\""
290        );
291        assert_eq!(
292            serde_json::to_string(&ResolutionLevel::Learned).unwrap(),
293            "\"learned\""
294        );
295    }
296
297    #[test]
298    fn pack_requirement_serde_roundtrip() {
299        let req = PackRequirement {
300            pack_name: "customers".into(),
301            reason: "lead workflow".into(),
302            confidence: 0.85,
303            source: ResolutionLevel::Structural,
304        };
305        let json = serde_json::to_string(&req).unwrap();
306        let back: PackRequirement = serde_json::from_str(&json).unwrap();
307        assert_eq!(back.pack_name, "customers");
308        assert_eq!(back.reason, "lead workflow");
309        assert!((back.confidence - 0.85).abs() < f64::EPSILON);
310        assert_eq!(back.source, ResolutionLevel::Structural);
311    }
312
313    #[test]
314    fn capability_requirement_serde_roundtrip() {
315        let req = CapabilityRequirement {
316            capability: "vision".into(),
317            reason: "document scanning".into(),
318            confidence: 0.7,
319            source: ResolutionLevel::Semantic,
320        };
321        let json = serde_json::to_string(&req).unwrap();
322        let back: CapabilityRequirement = serde_json::from_str(&json).unwrap();
323        assert_eq!(back.capability, "vision");
324        assert_eq!(back.source, ResolutionLevel::Semantic);
325    }
326
327    #[test]
328    fn intent_binding_serde_roundtrip() {
329        let binding = DeclarativeBinding::new()
330            .pack("dd", "due diligence")
331            .capability("web", "scraping")
332            .invariant("hypothesis_has_source")
333            .build();
334
335        let json = serde_json::to_string(&binding).unwrap();
336        let back: IntentBinding = serde_json::from_str(&json).unwrap();
337        assert_eq!(back.packs.len(), 1);
338        assert_eq!(back.capabilities.len(), 1);
339        assert_eq!(back.invariants, vec!["hypothesis_has_source"]);
340        assert_eq!(
341            back.resolution.levels_attempted,
342            vec![ResolutionLevel::Declarative]
343        );
344    }
345
346    #[test]
347    fn resolution_trace_serde_roundtrip() {
348        let trace = ResolutionTrace {
349            levels_attempted: vec![ResolutionLevel::Declarative, ResolutionLevel::Structural],
350            levels_contributed: vec![ResolutionLevel::Declarative],
351            prior_episodes_consulted: 42,
352            completeness_confidence: 0.95,
353        };
354        let json = serde_json::to_string(&trace).unwrap();
355        let back: ResolutionTrace = serde_json::from_str(&json).unwrap();
356        assert_eq!(back.levels_attempted.len(), 2);
357        assert_eq!(back.levels_contributed.len(), 1);
358        assert_eq!(back.prior_episodes_consulted, 42);
359        assert!((back.completeness_confidence - 0.95).abs() < f64::EPSILON);
360    }
361}