agent_first_http/sdk/fetch/
result.rs1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::shared::artifacts::Artifact;
8use crate::shared::ids::{RequestId, TabId};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FetchResult {
14 pub request_id: RequestId,
15 pub url: String,
16 pub final_url: String,
17 pub status: u16,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub tab_id: Option<TabId>,
20 pub trace: Trace,
21 #[serde(skip_serializing_if = "Vec::is_empty", default)]
22 pub warnings: Vec<Warning>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub body_file: Option<PathBuf>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub rendered_html_file: Option<PathBuf>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub text_file: Option<PathBuf>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub screenshot_file: Option<PathBuf>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub network_file: Option<PathBuf>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub console_file: Option<PathBuf>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub observation_file: Option<PathBuf>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub storage_file: Option<PathBuf>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub download_file: Option<PathBuf>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub download_bytes: Option<u64>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub download_filename: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub download_url: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub download_state: Option<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52pub struct Trace {
53 pub render_decision: RenderDecision,
54 #[serde(default)]
60 pub render_mode: TraceRenderMode,
61 #[serde(default)]
65 pub render_used: bool,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub escalation_reason: Option<String>,
68 pub main_request_observed: bool,
69 pub duration_ms: u64,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub navigation_duration_ms: Option<u64>,
72 #[serde(skip_serializing_if = "Option::is_none")]
77 pub cookie_jar_file: Option<std::path::PathBuf>,
78 #[serde(skip_serializing_if = "Option::is_none")]
81 pub cookie_jar_warning: Option<String>,
82 #[serde(skip_serializing_if = "Vec::is_empty", default)]
85 pub sensitive_capture: Vec<String>,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
92#[serde(rename_all = "snake_case")]
93pub enum TraceRenderMode {
94 None,
95 #[default]
96 Auto,
97 Always,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
101#[serde(rename_all = "snake_case")]
102pub enum RenderDecision {
103 #[default]
104 HttpOnly,
105 Browser,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Warning {
110 pub artifact: Artifact,
111 pub code: crate::shared::error::ErrorCode,
112 pub detail: String,
113}
114
115pub struct EscalationReason;
126
127impl EscalationReason {
128 pub const EMPTY_HTML_SHELL: &'static str = "empty_html_shell";
130
131 #[must_use]
133 pub fn http_status(status: u16) -> String {
134 format!("http_status_{status}")
135 }
136
137 #[must_use]
139 pub fn http_failed(error_code: &str) -> String {
140 format!("http_failed_{error_code}")
141 }
142}
143
144impl FetchResult {
145 pub fn set_artifact_file(&mut self, artifact: Artifact, path: PathBuf) {
148 match artifact {
149 Artifact::Body => self.body_file = Some(path),
150 Artifact::RenderedHtml => self.rendered_html_file = Some(path),
151 Artifact::Text => self.text_file = Some(path),
152 Artifact::Screenshot => self.screenshot_file = Some(path),
153 Artifact::Network => self.network_file = Some(path),
154 Artifact::Console => self.console_file = Some(path),
155 Artifact::Observation => self.observation_file = Some(path),
156 Artifact::Storage => self.storage_file = Some(path),
157 }
158 }
159
160 #[must_use]
161 pub fn artifact_file(&self, artifact: Artifact) -> Option<&PathBuf> {
162 match artifact {
163 Artifact::Body => self.body_file.as_ref(),
164 Artifact::RenderedHtml => self.rendered_html_file.as_ref(),
165 Artifact::Text => self.text_file.as_ref(),
166 Artifact::Screenshot => self.screenshot_file.as_ref(),
167 Artifact::Network => self.network_file.as_ref(),
168 Artifact::Console => self.console_file.as_ref(),
169 Artifact::Observation => self.observation_file.as_ref(),
170 Artifact::Storage => self.storage_file.as_ref(),
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn fetch_success_json_is_flat_golden() {
181 let mut result = FetchResult {
182 request_id: RequestId("req-1".into()),
183 url: "https://example.com/".into(),
184 final_url: "https://example.com/".into(),
185 status: 200,
186 tab_id: None,
187 trace: Trace {
188 render_decision: RenderDecision::Browser,
189 render_mode: TraceRenderMode::Always,
190 render_used: true,
191 escalation_reason: None,
192 main_request_observed: true,
193 duration_ms: 12,
194 navigation_duration_ms: Some(8),
195 cookie_jar_file: None,
196 cookie_jar_warning: None,
197 sensitive_capture: Vec::new(),
198 },
199 warnings: Vec::new(),
200 body_file: None,
201 rendered_html_file: None,
202 text_file: None,
203 screenshot_file: None,
204 network_file: None,
205 console_file: None,
206 observation_file: None,
207 storage_file: None,
208 download_file: None,
209 download_bytes: None,
210 download_filename: None,
211 download_url: None,
212 download_state: None,
213 };
214 for (artifact, file) in [
215 (Artifact::Body, "body.html"),
216 (Artifact::RenderedHtml, "rendered.html"),
217 (Artifact::Text, "text.txt"),
218 (Artifact::Screenshot, "page.png"),
219 (Artifact::Network, "network.json"),
220 (Artifact::Console, "console.json"),
221 (Artifact::Observation, "observation.json"),
222 (Artifact::Storage, "storage.json"),
223 ] {
224 result.set_artifact_file(artifact, PathBuf::from("/tmp/afhttp-out/req-1").join(file));
225 }
226 let mut buf = Vec::new();
227 crate::shared::envelope::emit(&mut buf, "fetch", &result).unwrap();
228 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
229 assert!(
230 json.get("artifacts").is_none(),
231 "artifacts map must be gone"
232 );
233 let canonical = serde_json::to_string(&json).unwrap();
234 let expected = include_str!("../../../tests/golden/fetch-success.json").trim();
235 assert_eq!(canonical, expected);
236 }
237}