Skip to main content

axonflow_sdk_rust/types/
agent.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use std::collections::HashMap;
3
4/// Deserialize helper: when the wire value is `null`, fall back to `T::default()`.
5///
6/// The platform sometimes serializes empty collections as `null` rather than
7/// `[]`. `#[serde(default)]` only fires for missing fields, so without this
8/// helper a payload containing `"policies_evaluated": null` would fail with
9/// "invalid type: null, expected a sequence". Combine with `#[serde(default)]`
10/// so both null AND missing are tolerated.
11fn null_to_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
12where
13    D: Deserializer<'de>,
14    T: Default + Deserialize<'de>,
15{
16    let opt = Option::<T>::deserialize(deserializer)?;
17    Ok(opt.unwrap_or_default())
18}
19
20#[derive(Debug, Serialize, Deserialize, Clone)]
21pub struct MediaContent {
22    pub id: String,
23    pub r#type: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub url: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub base64: Option<String>,
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone)]
31pub struct ClientRequest {
32    pub query: String,
33    pub user_token: String,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub client_id: Option<String>,
36    pub request_type: String,
37    #[serde(default)]
38    pub context: HashMap<String, serde_json::Value>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub media: Option<Vec<MediaContent>>,
41}
42
43#[must_use]
44#[derive(Debug, Serialize, Deserialize, Clone)]
45pub struct ClientResponse {
46    pub success: bool,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub data: Option<serde_json::Value>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub result: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub plan_id: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub request_id: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub metadata: Option<HashMap<String, serde_json::Value>>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub error: Option<String>,
59    #[serde(default)]
60    pub blocked: bool,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub block_reason: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub policy_info: Option<PolicyEvaluationInfo>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub budget_info: Option<BudgetInfo>,
67}
68
69impl ClientResponse {
70    pub fn fail_open(error: crate::error::AxonFlowError) -> Self {
71        Self {
72            success: true,
73            data: None,
74            result: None,
75            plan_id: None,
76            request_id: None,
77            metadata: None,
78            error: Some(format!("AxonFlow unavailable (fail-open): {}", error)),
79            blocked: false,
80            block_reason: None,
81            policy_info: None,
82            budget_info: None,
83        }
84    }
85}
86
87#[derive(Debug, Serialize, Deserialize, Clone)]
88pub struct BudgetInfo {
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub budget_id: Option<String>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub budget_name: Option<String>,
93    pub used_usd: f64,
94    pub limit_usd: f64,
95    pub percentage: f64,
96    pub exceeded: bool,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub action: Option<String>,
99}
100
101#[derive(Debug, Serialize, Deserialize, Clone, Default)]
102#[serde(default)]
103pub struct PolicyEvaluationInfo {
104    #[serde(deserialize_with = "null_to_default")]
105    pub policies_evaluated: Vec<String>,
106    #[serde(deserialize_with = "null_to_default")]
107    pub static_checks: Vec<String>,
108    pub processing_time: String,
109    pub tenant_id: String,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub code_artifact: Option<CodeArtifact>,
112}
113
114#[derive(Debug, Serialize, Deserialize, Clone, Default)]
115pub struct TokenUsage {
116    pub prompt_tokens: usize,
117    pub completion_tokens: usize,
118    pub total_tokens: usize,
119}
120
121#[derive(Debug, Clone)]
122pub struct AuditRequest {
123    pub context_id: String,
124    pub response_summary: String,
125    pub provider: String,
126    pub model: String,
127    pub token_usage: TokenUsage,
128    pub latency_ms: i64,
129    pub metadata: Option<HashMap<String, serde_json::Value>>,
130}
131
132#[derive(Debug, Serialize, Deserialize, Clone, Default)]
133#[serde(default)]
134pub struct AuditResult {
135    pub success: bool,
136    pub audit_id: String,
137}
138
139/// A single audit log entry from the platform.
140#[derive(Debug, Serialize, Deserialize, Clone, Default)]
141#[serde(default)]
142pub struct AuditLogEntry {
143    pub id: String,
144    pub request_id: String,
145    pub timestamp: String,
146    pub user_email: String,
147    pub client_id: String,
148    pub tenant_id: String,
149    pub request_type: String,
150    pub query_summary: String,
151    pub success: bool,
152    pub blocked: bool,
153    pub risk_score: f64,
154    pub provider: String,
155    pub model: String,
156    pub tokens_used: i64,
157    pub latency_ms: i64,
158    #[serde(default, skip_serializing_if = "Vec::is_empty")]
159    pub policy_violations: Vec<String>,
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub metadata: Option<HashMap<String, serde_json::Value>>,
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub data_residency: Option<String>,
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub transfer_basis: Option<String>,
166}
167
168#[derive(Debug, Serialize, Deserialize, Clone, Default)]
169#[serde(default)]
170pub struct ConnectorMetadata {
171    pub id: String,
172    pub name: String,
173    #[serde(rename = "type")]
174    pub r#type: String,
175    pub version: String,
176    pub description: String,
177    pub category: String,
178    pub icon: String,
179    #[serde(deserialize_with = "null_to_default")]
180    pub tags: Vec<String>,
181    #[serde(deserialize_with = "null_to_default")]
182    pub capabilities: Vec<String>,
183    #[serde(deserialize_with = "null_to_default")]
184    pub config_schema: HashMap<String, serde_json::Value>,
185    pub installed: bool,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub instance_name: Option<String>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub healthy: Option<bool>,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub last_check: Option<String>,
192}
193
194#[derive(Debug, Serialize, Deserialize, Clone, Default)]
195#[serde(default)]
196pub struct ConnectorHealthStatus {
197    pub healthy: bool,
198    pub latency: i64,
199    #[serde(deserialize_with = "null_to_default")]
200    pub details: HashMap<String, String>,
201    pub timestamp: String,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub error: Option<String>,
204}
205
206#[derive(Debug, Serialize, Deserialize, Clone)]
207pub struct ConnectorInstallRequest {
208    pub connector_id: String,
209    pub name: String,
210    pub tenant_id: String,
211    pub options: HashMap<String, serde_json::Value>,
212    pub credentials: HashMap<String, String>,
213}
214
215#[must_use]
216#[derive(Debug, Serialize, Deserialize, Clone)]
217pub struct ConnectorResponse {
218    pub success: bool,
219    pub data: serde_json::Value,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub error: Option<String>,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub meta: Option<HashMap<String, serde_json::Value>>,
224    #[serde(default)]
225    pub redacted: bool,
226    #[serde(default, deserialize_with = "null_to_default")]
227    pub redacted_fields: Vec<String>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub policy_info: Option<PolicyInfo>,
230}
231
232#[derive(Debug, Serialize, Deserialize, Clone)]
233pub struct PolicyInfo {
234    pub policies_evaluated: usize,
235    pub blocked: bool,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub block_reason: Option<String>,
238    pub redactions_applied: usize,
239    pub processing_time_ms: i64,
240    #[serde(default, deserialize_with = "null_to_default")]
241    pub matched_policies: Vec<PolicyMatchInfo>,
242}
243
244#[derive(Debug, Serialize, Deserialize, Clone)]
245pub struct PolicyMatchInfo {
246    pub policy_id: String,
247    pub policy_name: String,
248    pub category: String,
249    pub severity: String,
250    pub action: String,
251}
252
253#[derive(Debug, Serialize, Deserialize, Clone, Default)]
254pub struct PlanStep {
255    #[serde(default)]
256    pub id: String,
257    #[serde(default)]
258    pub name: String,
259    #[serde(default, rename = "type")]
260    pub r#type: String,
261    #[serde(default)]
262    pub description: String,
263    #[serde(default, deserialize_with = "null_to_default")]
264    pub dependencies: Vec<String>,
265    #[serde(default)]
266    pub agent: String,
267    #[serde(default, deserialize_with = "null_to_default")]
268    pub parameters: HashMap<String, serde_json::Value>,
269    #[serde(default)]
270    pub estimated_time: String,
271}
272
273#[must_use]
274#[derive(Debug, Serialize, Deserialize, Clone, Default)]
275#[serde(default)]
276pub struct PlanResponse {
277    pub plan_id: String,
278    pub status: String,
279    #[serde(deserialize_with = "null_to_default")]
280    pub steps: Vec<PlanStep>,
281    pub domain: String,
282    pub complexity: i32,
283    pub parallel: bool,
284    pub estimated_duration: String,
285    #[serde(deserialize_with = "null_to_default")]
286    pub metadata: HashMap<String, serde_json::Value>,
287    pub success: bool,
288    pub version: i32,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub result: Option<serde_json::Value>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub error: Option<String>,
293}
294
295#[derive(Debug, Serialize, Deserialize, Clone, Default)]
296#[serde(default)]
297pub struct StepResult {
298    pub step_id: String,
299    pub step_name: String,
300    pub status: String,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub result: Option<serde_json::Value>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub error: Option<String>,
305    pub duration: String,
306}
307
308#[must_use]
309#[derive(Debug, Serialize, Deserialize, Clone, Default)]
310#[serde(default)]
311pub struct PlanExecutionResponse {
312    pub plan_id: String,
313    pub status: String,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub workflow_id: Option<String>,
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub result: Option<String>,
318    #[serde(deserialize_with = "null_to_default")]
319    pub step_results: Vec<StepResult>,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub error: Option<String>,
322    pub duration: String,
323    pub completed_steps: i32,
324    pub total_steps: i32,
325}
326
327#[derive(Debug, Serialize, Deserialize, Clone, Default)]
328#[serde(default)]
329pub struct CancelPlanResponse {
330    pub plan_id: String,
331    pub status: String,
332    pub success: bool,
333}
334
335#[derive(Debug, Serialize, Deserialize, Clone)]
336pub struct CodeArtifact {
337    pub is_code_output: bool,
338    pub language: String,
339    pub code_type: String,
340    pub size_bytes: usize,
341    pub line_count: usize,
342    pub secrets_detected: usize,
343    pub unsafe_patterns: usize,
344    #[serde(default, deserialize_with = "null_to_default")]
345    pub policies_checked: Vec<String>,
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn audit_log_entry_with_residency_fields_deserializes() {
354        let json = r#"{
355            "id": "audit-001",
356            "request_id": "req-123",
357            "timestamp": "2026-05-26T00:00:00Z",
358            "user_email": "user@example.com",
359            "client_id": "client-1",
360            "tenant_id": "tenant-1",
361            "request_type": "llm_query",
362            "query_summary": "test query",
363            "success": true,
364            "blocked": false,
365            "risk_score": 0.15,
366            "provider": "openai",
367            "model": "gpt-4",
368            "tokens_used": 500,
369            "latency_ms": 120,
370            "policy_violations": [],
371            "data_residency": "id-jakarta",
372            "transfer_basis": "pdp-consent"
373        }"#;
374
375        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
376        assert_eq!(entry.id, "audit-001");
377        assert_eq!(entry.data_residency.as_deref(), Some("id-jakarta"));
378        assert_eq!(entry.transfer_basis.as_deref(), Some("pdp-consent"));
379        assert!(entry.success);
380        assert!(!entry.blocked);
381        assert_eq!(entry.tokens_used, 500);
382    }
383
384    #[test]
385    fn audit_log_entry_without_new_fields_deserializes_backward_compat() {
386        let json = r#"{
387            "id": "audit-002",
388            "request_id": "req-456",
389            "timestamp": "2026-05-26T00:00:00Z",
390            "user_email": "user@example.com",
391            "client_id": "client-1",
392            "tenant_id": "tenant-1",
393            "request_type": "llm_query",
394            "query_summary": "test query",
395            "success": true,
396            "blocked": false,
397            "risk_score": 0.0,
398            "provider": "anthropic",
399            "model": "claude-3",
400            "tokens_used": 100,
401            "latency_ms": 50
402        }"#;
403
404        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
405        assert_eq!(entry.id, "audit-002");
406        assert!(entry.data_residency.is_none());
407        assert!(entry.transfer_basis.is_none());
408        assert!(entry.policy_violations.is_empty());
409        assert!(entry.metadata.is_none());
410    }
411
412    #[test]
413    fn audit_log_entry_empty_optional_fields_omitted_in_serialization() {
414        let entry = AuditLogEntry {
415            id: "audit-003".to_string(),
416            request_id: "req-789".to_string(),
417            success: true,
418            ..Default::default()
419        };
420
421        let json = serde_json::to_string(&entry).unwrap();
422        // Option<String> fields set to None should be absent
423        assert!(!json.contains("data_residency"));
424        assert!(!json.contains("transfer_basis"));
425        assert!(!json.contains("metadata"));
426        // Empty Vec should be absent (skip_serializing_if = "Vec::is_empty")
427        assert!(!json.contains("policy_violations"));
428    }
429
430    #[test]
431    fn audit_log_entry_with_metadata_round_trips() {
432        let mut meta = HashMap::new();
433        meta.insert(
434            "region".to_string(),
435            serde_json::Value::String("ap-southeast-3".to_string()),
436        );
437
438        let entry = AuditLogEntry {
439            id: "audit-004".to_string(),
440            data_residency: Some("id-jakarta".to_string()),
441            transfer_basis: Some("pdp-consent".to_string()),
442            metadata: Some(meta),
443            ..Default::default()
444        };
445
446        let json = serde_json::to_string(&entry).unwrap();
447        let back: AuditLogEntry = serde_json::from_str(&json).unwrap();
448        assert_eq!(back.data_residency.as_deref(), Some("id-jakarta"));
449        assert_eq!(back.transfer_basis.as_deref(), Some("pdp-consent"));
450        assert!(back.metadata.is_some());
451        assert_eq!(
452            back.metadata.unwrap().get("region").unwrap(),
453            &serde_json::Value::String("ap-southeast-3".to_string())
454        );
455    }
456}