1use crate::run::{Artifact, RunMetadata};
10use crate::tool::ToolMemoryPolicy;
11use serde::{Deserialize, Serialize};
12use std::sync::OnceLock;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct DisplayOutput {
24 pub format: DisplayFormat,
26 pub content: String,
28 pub metadata: RunMetadata,
30}
31
32impl DisplayOutput {
33 pub fn new(format: DisplayFormat, content: impl Into<String>) -> Self {
35 Self {
36 format,
37 content: content.into(),
38 metadata: RunMetadata::new(),
39 }
40 }
41
42 pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
44 self.metadata = metadata;
45 self
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[non_exhaustive]
52pub enum DisplayFormat {
53 PlainText,
55 Markdown,
57 Json,
59 Custom(String),
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[non_exhaustive]
66pub enum EffectKind {
67 ReadFile,
69 WriteFile,
71 ApplyPatch,
73 Search,
75 ExecuteCommand,
77 Git,
79 Network,
81 Browser,
83 Mcp,
85 Custom(String),
87}
88
89#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
94#[non_exhaustive]
95pub enum RiskLevel {
96 #[default]
98 Low,
99 Medium,
101 High,
103 Critical,
105}
106
107#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
109pub struct EffectSource {
110 pub tool_call_id: Option<String>,
112 pub tool_name: Option<String>,
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct EffectRequest {
119 pub id: String,
121 pub kind: EffectKind,
123 pub description: String,
125 pub payload: serde_json::Value,
127 pub source: EffectSource,
129 pub risk: RiskLevel,
131 pub timeout: Option<Duration>,
133 pub metadata: RunMetadata,
135}
136
137impl EffectRequest {
138 pub fn new(
140 kind: EffectKind,
141 description: impl Into<String>,
142 payload: serde_json::Value,
143 ) -> Self {
144 Self {
145 id: generated_effect_id(),
146 kind,
147 description: description.into(),
148 payload,
149 source: EffectSource::default(),
150 risk: RiskLevel::Low,
151 timeout: None,
152 metadata: RunMetadata::new(),
153 }
154 }
155
156 pub fn with_id(mut self, id: impl Into<String>) -> Self {
158 self.id = id.into();
159 self
160 }
161
162 pub fn with_source(
164 mut self,
165 tool_call_id: impl Into<String>,
166 tool_name: impl Into<String>,
167 ) -> Self {
168 self.source.tool_call_id = Some(tool_call_id.into());
169 self.source.tool_name = Some(tool_name.into());
170 self
171 }
172
173 pub fn with_source_if_missing(
176 mut self,
177 tool_call_id: impl Into<String>,
178 tool_name: impl Into<String>,
179 ) -> Self {
180 if self.source.tool_call_id.is_none() {
181 self.source.tool_call_id = Some(tool_call_id.into());
182 }
183 if self.source.tool_name.is_none() {
184 self.source.tool_name = Some(tool_name.into());
185 }
186 self
187 }
188
189 pub fn with_risk(mut self, risk: RiskLevel) -> Self {
191 self.risk = risk;
192 self
193 }
194
195 pub fn with_timeout(mut self, timeout: Duration) -> Self {
197 self.timeout = Some(timeout);
198 self
199 }
200
201 pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
203 self.metadata = metadata;
204 self
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210#[non_exhaustive]
211pub enum EffectStatus {
212 Succeeded,
214 Denied,
216 Failed,
218 Cancelled,
220 TimedOut,
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226pub struct EffectOutput {
227 pub observation_for_model: String,
229 pub display: Option<DisplayOutput>,
231 pub artifacts: Vec<Artifact>,
233 pub memory_policy: ToolMemoryPolicy,
235 pub metadata: RunMetadata,
237}
238
239impl EffectOutput {
240 pub fn text(observation_for_model: impl Into<String>) -> Self {
242 Self {
243 observation_for_model: observation_for_model.into(),
244 display: None,
245 artifacts: Vec::new(),
246 memory_policy: ToolMemoryPolicy::Normal,
247 metadata: RunMetadata::new(),
248 }
249 }
250
251 pub fn with_display(mut self, display: DisplayOutput) -> Self {
253 self.display = Some(display);
254 self
255 }
256
257 pub fn with_artifacts(mut self, artifacts: Vec<Artifact>) -> Self {
259 self.artifacts = artifacts;
260 self
261 }
262
263 pub fn with_memory_policy(mut self, policy: ToolMemoryPolicy) -> Self {
265 self.memory_policy = policy;
266 self
267 }
268
269 pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
271 self.metadata = metadata;
272 self
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct EffectObservation {
280 pub effect_id: String,
282 pub status: EffectStatus,
284 pub output: EffectOutput,
286 pub metadata: RunMetadata,
288}
289
290impl EffectObservation {
291 pub fn succeeded(effect_id: impl Into<String>, observation: impl Into<String>) -> Self {
293 Self {
294 effect_id: effect_id.into(),
295 status: EffectStatus::Succeeded,
296 output: EffectOutput::text(observation),
297 metadata: RunMetadata::new(),
298 }
299 }
300
301 pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
303 self.metadata = metadata;
304 self
305 }
306}
307
308fn generated_effect_id() -> String {
309 static START_NANOS: OnceLock<u128> = OnceLock::new();
310 static EFFECT_COUNTER: AtomicU64 = AtomicU64::new(0);
311
312 let start_nanos = *START_NANOS.get_or_init(|| {
313 SystemTime::now()
314 .duration_since(UNIX_EPOCH)
315 .unwrap_or_default()
316 .as_nanos()
317 });
318 let n = EFFECT_COUNTER.fetch_add(1, Ordering::Relaxed);
319 format!("effect-{start_nanos}-{n}")
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use serde_json::json;
326
327 #[test]
328 fn effect_request_builder_sets_fields() {
329 let request = EffectRequest::new(EffectKind::ReadFile, "read", json!({"path": "a.rs"}))
330 .with_id("effect-1")
331 .with_source("call-1", "read_file")
332 .with_risk(RiskLevel::Medium)
333 .with_timeout(Duration::from_secs(5));
334
335 assert_eq!(request.id, "effect-1");
336 assert_eq!(request.source.tool_call_id.as_deref(), Some("call-1"));
337 assert_eq!(request.source.tool_name.as_deref(), Some("read_file"));
338 assert_eq!(request.risk, RiskLevel::Medium);
339 assert_eq!(request.timeout, Some(Duration::from_secs(5)));
340 }
341
342 #[test]
343 fn effect_output_text_is_model_visible_only() {
344 let output = EffectOutput::text("observed")
345 .with_display(DisplayOutput::new(DisplayFormat::Markdown, "**observed**"));
346 assert_eq!(output.observation_for_model, "observed");
347 assert_eq!(output.display.unwrap().content, "**observed**");
348 }
349}