tirea_contract/runtime/run/
options.rs

1use crate::storage::RunOrigin;
2
3/// Strongly typed scope and execution policy carried with a resolved run.
4#[derive(Debug, Clone, Default, PartialEq, Eq)]
5pub struct RunPolicy {
6    allowed_tools: Option<Vec<String>>,
7    excluded_tools: Option<Vec<String>>,
8    allowed_skills: Option<Vec<String>>,
9    excluded_skills: Option<Vec<String>>,
10    allowed_agents: Option<Vec<String>>,
11    excluded_agents: Option<Vec<String>>,
12}
13
14impl RunPolicy {
15    #[must_use]
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    pub fn allowed_tools(&self) -> Option<&[String]> {
21        self.allowed_tools.as_deref()
22    }
23
24    pub fn excluded_tools(&self) -> Option<&[String]> {
25        self.excluded_tools.as_deref()
26    }
27
28    pub fn allowed_skills(&self) -> Option<&[String]> {
29        self.allowed_skills.as_deref()
30    }
31
32    pub fn excluded_skills(&self) -> Option<&[String]> {
33        self.excluded_skills.as_deref()
34    }
35
36    pub fn allowed_agents(&self) -> Option<&[String]> {
37        self.allowed_agents.as_deref()
38    }
39
40    pub fn excluded_agents(&self) -> Option<&[String]> {
41        self.excluded_agents.as_deref()
42    }
43
44    pub fn set_allowed_tools_if_absent(&mut self, values: Option<&[String]>) {
45        if self.allowed_tools.is_none() {
46            self.allowed_tools = normalize_scope_values(values);
47        }
48    }
49
50    pub fn set_excluded_tools_if_absent(&mut self, values: Option<&[String]>) {
51        if self.excluded_tools.is_none() {
52            self.excluded_tools = normalize_scope_values(values);
53        }
54    }
55
56    pub fn set_allowed_skills_if_absent(&mut self, values: Option<&[String]>) {
57        if self.allowed_skills.is_none() {
58            self.allowed_skills = normalize_scope_values(values);
59        }
60    }
61
62    pub fn set_excluded_skills_if_absent(&mut self, values: Option<&[String]>) {
63        if self.excluded_skills.is_none() {
64            self.excluded_skills = normalize_scope_values(values);
65        }
66    }
67
68    pub fn set_allowed_agents_if_absent(&mut self, values: Option<&[String]>) {
69        if self.allowed_agents.is_none() {
70            self.allowed_agents = normalize_scope_values(values);
71        }
72    }
73
74    pub fn set_excluded_agents_if_absent(&mut self, values: Option<&[String]>) {
75        if self.excluded_agents.is_none() {
76            self.excluded_agents = normalize_scope_values(values);
77        }
78    }
79}
80
81fn normalize_scope_values(values: Option<&[String]>) -> Option<Vec<String>> {
82    let parsed: Vec<String> = values
83        .into_iter()
84        .flatten()
85        .map(|value| value.trim())
86        .filter(|value| !value.is_empty())
87        .map(ToOwned::to_owned)
88        .collect();
89    if parsed.is_empty() {
90        None
91    } else {
92        Some(parsed)
93    }
94}
95
96/// Strongly typed identity for the active run.
97#[derive(Debug, Clone, Default, PartialEq, Eq)]
98pub struct RunIdentity {
99    pub thread_id: String,
100    pub parent_thread_id: Option<String>,
101    pub run_id: String,
102    pub parent_run_id: Option<String>,
103    pub agent_id: String,
104    pub origin: RunOrigin,
105    pub parent_tool_call_id: Option<String>,
106}
107
108impl RunIdentity {
109    #[must_use]
110    pub fn for_thread(thread_id: impl Into<String>) -> Self {
111        Self {
112            thread_id: thread_id.into(),
113            ..Self::default()
114        }
115    }
116
117    #[must_use]
118    pub fn new(
119        thread_id: String,
120        parent_thread_id: Option<String>,
121        run_id: String,
122        parent_run_id: Option<String>,
123        agent_id: String,
124        origin: RunOrigin,
125    ) -> Self {
126        Self {
127            thread_id,
128            parent_thread_id,
129            run_id,
130            parent_run_id,
131            agent_id,
132            origin,
133            parent_tool_call_id: None,
134        }
135    }
136
137    #[must_use]
138    pub fn with_parent_tool_call_id(mut self, parent_tool_call_id: impl Into<String>) -> Self {
139        let value = parent_tool_call_id.into();
140        if !value.trim().is_empty() {
141            self.parent_tool_call_id = Some(value);
142        }
143        self
144    }
145
146    pub fn thread_id_opt(&self) -> Option<&str> {
147        let thread_id = self.thread_id.trim();
148        if thread_id.is_empty() {
149            None
150        } else {
151            Some(thread_id)
152        }
153    }
154
155    pub fn parent_thread_id_opt(&self) -> Option<&str> {
156        self.parent_thread_id
157            .as_deref()
158            .map(str::trim)
159            .filter(|value| !value.is_empty())
160    }
161
162    pub fn run_id_opt(&self) -> Option<&str> {
163        let run_id = self.run_id.trim();
164        if run_id.is_empty() {
165            None
166        } else {
167            Some(run_id)
168        }
169    }
170
171    pub fn parent_run_id_opt(&self) -> Option<&str> {
172        self.parent_run_id
173            .as_deref()
174            .map(str::trim)
175            .filter(|value| !value.is_empty())
176    }
177
178    pub fn agent_id_opt(&self) -> Option<&str> {
179        let agent_id = self.agent_id.trim();
180        if agent_id.is_empty() {
181            None
182        } else {
183            Some(agent_id)
184        }
185    }
186
187    pub fn parent_tool_call_id_opt(&self) -> Option<&str> {
188        self.parent_tool_call_id
189            .as_deref()
190            .map(str::trim)
191            .filter(|value| !value.is_empty())
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn run_policy_normalizes_values() {
201        let mut policy = RunPolicy::new();
202        policy.set_allowed_tools_if_absent(Some(&[" a ".to_string(), "".to_string()]));
203        assert_eq!(policy.allowed_tools(), Some(&["a".to_string()][..]));
204    }
205
206    #[test]
207    fn run_identity_ignores_blank_parent_tool_call_id() {
208        let identity = RunIdentity::new(
209            "thread-1".to_string(),
210            None,
211            "run-1".to_string(),
212            None,
213            "agent-1".to_string(),
214            RunOrigin::Internal,
215        )
216        .with_parent_tool_call_id("   ");
217        assert!(identity.parent_tool_call_id_opt().is_none());
218    }
219}