Skip to main content

made_core/entities/
task_constraints.rs

1use serde::{Deserialize, Serialize};
2
3use crate::value_objects::{DurationMs, NumAgents, OutputContract, Rounds, Rubric};
4
5/// Domain configuration that shapes a task's deliberation.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(default)]
8pub struct TaskConstraints {
9    rubric: Rubric,
10    rounds: Rounds,
11    num_agents: Option<NumAgents>,
12    deadline: Option<DurationMs>,
13    output_contract: Option<OutputContract>,
14}
15
16impl TaskConstraints {
17    #[must_use]
18    pub fn new(
19        rubric: Rubric,
20        rounds: Rounds,
21        num_agents: Option<NumAgents>,
22        deadline: Option<DurationMs>,
23    ) -> Self {
24        Self {
25            rubric,
26            rounds,
27            num_agents,
28            deadline,
29            output_contract: None,
30        }
31    }
32
33    #[must_use]
34    pub fn rubric(&self) -> &Rubric {
35        &self.rubric
36    }
37
38    #[must_use]
39    pub fn rounds(&self) -> Rounds {
40        self.rounds
41    }
42
43    #[must_use]
44    pub fn num_agents(&self) -> Option<NumAgents> {
45        self.num_agents
46    }
47
48    #[must_use]
49    pub fn deadline(&self) -> Option<DurationMs> {
50        self.deadline
51    }
52
53    #[must_use]
54    pub fn output_contract(&self) -> Option<&OutputContract> {
55        self.output_contract.as_ref()
56    }
57
58    #[must_use]
59    pub fn with_output_contract(mut self, output_contract: OutputContract) -> Self {
60        self.output_contract = Some(output_contract);
61        self
62    }
63}
64
65impl Default for TaskConstraints {
66    fn default() -> Self {
67        Self {
68            rubric: Rubric::empty(),
69            rounds: Rounds::default(),
70            num_agents: None,
71            deadline: None,
72            output_contract: None,
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use std::collections::BTreeMap;
80
81    use super::*;
82    use crate::value_objects::{OutputFieldRule, OutputFormat};
83
84    #[test]
85    fn defaults_are_sane() {
86        let constraints = TaskConstraints::default();
87        assert_eq!(constraints.rounds(), Rounds::default());
88        assert!(constraints.num_agents().is_none());
89        assert!(constraints.deadline().is_none());
90        assert!(constraints.rubric().is_empty());
91    }
92
93    #[test]
94    fn accepts_optional_bounds() {
95        let constraints = TaskConstraints::new(
96            Rubric::empty(),
97            Rounds::new(3).unwrap(),
98            Some(NumAgents::new(4).unwrap()),
99            Some(DurationMs::from_millis(1500)),
100        );
101        assert_eq!(constraints.rounds().get(), 3);
102        assert_eq!(constraints.num_agents().unwrap().get(), 4);
103        assert_eq!(constraints.deadline().unwrap().get(), 1500);
104    }
105
106    #[test]
107    fn supports_structured_output_contract() {
108        let contract = OutputContract::new(
109            "decision-contract",
110            OutputFormat::JsonObject,
111            BTreeMap::from([(
112                "decision".to_owned(),
113                OutputFieldRule::new(true, ["emit_event", "escalate"]).unwrap(),
114            )]),
115        )
116        .unwrap();
117
118        let constraints = TaskConstraints::default().with_output_contract(contract.clone());
119        assert_eq!(constraints.output_contract(), Some(&contract));
120    }
121
122    #[test]
123    fn empty_json_object_deserializes_to_defaults() {
124        let constraints: TaskConstraints = serde_json::from_str("{}").unwrap();
125        assert_eq!(constraints, TaskConstraints::default());
126    }
127}