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    /// Cross-border transfer legal basis under Indonesia UU PDP Pasal 56:
165    /// `adequacy`, `safeguards`, `pasal_56b_dpa`, or `consent`. Surfaced
166    /// verbatim — never auto-translated. See the [`transfer_basis`] module
167    /// constants for the recognized set. The field stays an `Option<String>`
168    /// so the SDK never rejects a value a newer platform may add.
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub transfer_basis: Option<String>,
171}
172
173/// Cross-border transfer-basis values recognized under Indonesia UU PDP Pasal 56,
174/// for [`AuditLogEntry::transfer_basis`]:
175///
176/// * [`ADEQUACY`](transfer_basis::ADEQUACY) — Pasal 56(a): destination with adequate protection
177/// * [`SAFEGUARDS`](transfer_basis::SAFEGUARDS) — Pasal 56(b): binding legal instrument (generic label)
178/// * [`PASAL_56B_DPA`](transfer_basis::PASAL_56B_DPA) — Pasal 56(b): binding legal instrument, explicit DPA tag
179/// * [`CONSENT`](transfer_basis::CONSENT) — Pasal 56(c): explicit data-subject consent
180///
181/// `safeguards` and `pasal_56b_dpa` are semantic equivalents; the platform
182/// surfaces whichever was recorded at decision time. (platform #2513 / epic #2508)
183pub mod transfer_basis {
184    pub const ADEQUACY: &str = "adequacy";
185    pub const SAFEGUARDS: &str = "safeguards";
186    pub const PASAL_56B_DPA: &str = "pasal_56b_dpa";
187    pub const CONSENT: &str = "consent";
188}
189
190#[derive(Debug, Serialize, Deserialize, Clone, Default)]
191#[serde(default)]
192pub struct ConnectorMetadata {
193    pub id: String,
194    pub name: String,
195    #[serde(rename = "type")]
196    pub r#type: String,
197    pub version: String,
198    pub description: String,
199    pub category: String,
200    pub icon: String,
201    #[serde(deserialize_with = "null_to_default")]
202    pub tags: Vec<String>,
203    #[serde(deserialize_with = "null_to_default")]
204    pub capabilities: Vec<String>,
205    #[serde(deserialize_with = "null_to_default")]
206    pub config_schema: HashMap<String, serde_json::Value>,
207    pub installed: bool,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub instance_name: Option<String>,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub healthy: Option<bool>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub last_check: Option<String>,
214}
215
216#[derive(Debug, Serialize, Deserialize, Clone, Default)]
217#[serde(default)]
218pub struct ConnectorHealthStatus {
219    pub healthy: bool,
220    pub latency: i64,
221    #[serde(deserialize_with = "null_to_default")]
222    pub details: HashMap<String, String>,
223    pub timestamp: String,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub error: Option<String>,
226}
227
228#[derive(Debug, Serialize, Deserialize, Clone)]
229pub struct ConnectorInstallRequest {
230    pub connector_id: String,
231    pub name: String,
232    pub tenant_id: String,
233    pub options: HashMap<String, serde_json::Value>,
234    pub credentials: HashMap<String, String>,
235}
236
237#[must_use]
238#[derive(Debug, Serialize, Deserialize, Clone)]
239pub struct ConnectorResponse {
240    pub success: bool,
241    pub data: serde_json::Value,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub error: Option<String>,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub meta: Option<HashMap<String, serde_json::Value>>,
246    #[serde(default)]
247    pub redacted: bool,
248    #[serde(default, deserialize_with = "null_to_default")]
249    pub redacted_fields: Vec<String>,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub policy_info: Option<PolicyInfo>,
252}
253
254#[derive(Debug, Serialize, Deserialize, Clone)]
255pub struct PolicyInfo {
256    pub policies_evaluated: usize,
257    pub blocked: bool,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub block_reason: Option<String>,
260    pub redactions_applied: usize,
261    pub processing_time_ms: i64,
262    #[serde(default, deserialize_with = "null_to_default")]
263    pub matched_policies: Vec<PolicyMatchInfo>,
264}
265
266#[derive(Debug, Serialize, Deserialize, Clone)]
267pub struct PolicyMatchInfo {
268    pub policy_id: String,
269    pub policy_name: String,
270    pub category: String,
271    pub severity: String,
272    pub action: String,
273}
274
275#[derive(Debug, Serialize, Deserialize, Clone, Default)]
276pub struct PlanStep {
277    #[serde(default)]
278    pub id: String,
279    #[serde(default)]
280    pub name: String,
281    #[serde(default, rename = "type")]
282    pub r#type: String,
283    #[serde(default)]
284    pub description: String,
285    #[serde(default, deserialize_with = "null_to_default")]
286    pub dependencies: Vec<String>,
287    #[serde(default)]
288    pub agent: String,
289    #[serde(default, deserialize_with = "null_to_default")]
290    pub parameters: HashMap<String, serde_json::Value>,
291    #[serde(default)]
292    pub estimated_time: String,
293}
294
295#[must_use]
296#[derive(Debug, Serialize, Deserialize, Clone, Default)]
297#[serde(default)]
298pub struct PlanResponse {
299    pub plan_id: String,
300    pub status: String,
301    #[serde(deserialize_with = "null_to_default")]
302    pub steps: Vec<PlanStep>,
303    pub domain: String,
304    pub complexity: i32,
305    pub parallel: bool,
306    pub estimated_duration: String,
307    #[serde(deserialize_with = "null_to_default")]
308    pub metadata: HashMap<String, serde_json::Value>,
309    pub success: bool,
310    pub version: i32,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub result: Option<serde_json::Value>,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub error: Option<String>,
315}
316
317#[derive(Debug, Serialize, Deserialize, Clone, Default)]
318#[serde(default)]
319pub struct StepResult {
320    pub step_id: String,
321    pub step_name: String,
322    pub status: String,
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub result: Option<serde_json::Value>,
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub error: Option<String>,
327    pub duration: String,
328}
329
330#[must_use]
331#[derive(Debug, Serialize, Deserialize, Clone, Default)]
332#[serde(default)]
333pub struct PlanExecutionResponse {
334    pub plan_id: String,
335    pub status: String,
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub workflow_id: Option<String>,
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub result: Option<String>,
340    #[serde(deserialize_with = "null_to_default")]
341    pub step_results: Vec<StepResult>,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub error: Option<String>,
344    pub duration: String,
345    pub completed_steps: i32,
346    pub total_steps: i32,
347}
348
349#[derive(Debug, Serialize, Deserialize, Clone, Default)]
350#[serde(default)]
351pub struct CancelPlanResponse {
352    pub plan_id: String,
353    pub status: String,
354    pub success: bool,
355}
356
357#[derive(Debug, Serialize, Deserialize, Clone)]
358pub struct CodeArtifact {
359    pub is_code_output: bool,
360    pub language: String,
361    pub code_type: String,
362    pub size_bytes: usize,
363    pub line_count: usize,
364    pub secrets_detected: usize,
365    pub unsafe_patterns: usize,
366    #[serde(default, deserialize_with = "null_to_default")]
367    pub policies_checked: Vec<String>,
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn audit_log_entry_with_residency_fields_deserializes() {
376        let json = r#"{
377            "id": "audit-001",
378            "request_id": "req-123",
379            "timestamp": "2026-05-26T00:00:00Z",
380            "user_email": "user@example.com",
381            "client_id": "client-1",
382            "tenant_id": "tenant-1",
383            "request_type": "llm_query",
384            "query_summary": "test query",
385            "success": true,
386            "blocked": false,
387            "risk_score": 0.15,
388            "provider": "openai",
389            "model": "gpt-4",
390            "tokens_used": 500,
391            "latency_ms": 120,
392            "policy_violations": [],
393            "data_residency": "id-jakarta",
394            "transfer_basis": "pdp-consent"
395        }"#;
396
397        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
398        assert_eq!(entry.id, "audit-001");
399        assert_eq!(entry.data_residency.as_deref(), Some("id-jakarta"));
400        assert_eq!(entry.transfer_basis.as_deref(), Some("pdp-consent"));
401        assert!(entry.success);
402        assert!(!entry.blocked);
403        assert_eq!(entry.tokens_used, 500);
404    }
405
406    #[test]
407    fn audit_log_entry_without_new_fields_deserializes_backward_compat() {
408        let json = r#"{
409            "id": "audit-002",
410            "request_id": "req-456",
411            "timestamp": "2026-05-26T00:00:00Z",
412            "user_email": "user@example.com",
413            "client_id": "client-1",
414            "tenant_id": "tenant-1",
415            "request_type": "llm_query",
416            "query_summary": "test query",
417            "success": true,
418            "blocked": false,
419            "risk_score": 0.0,
420            "provider": "anthropic",
421            "model": "claude-3",
422            "tokens_used": 100,
423            "latency_ms": 50
424        }"#;
425
426        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
427        assert_eq!(entry.id, "audit-002");
428        assert!(entry.data_residency.is_none());
429        assert!(entry.transfer_basis.is_none());
430        assert!(entry.policy_violations.is_empty());
431        assert!(entry.metadata.is_none());
432    }
433
434    #[test]
435    fn audit_log_entry_empty_optional_fields_omitted_in_serialization() {
436        let entry = AuditLogEntry {
437            id: "audit-003".to_string(),
438            request_id: "req-789".to_string(),
439            success: true,
440            ..Default::default()
441        };
442
443        let json = serde_json::to_string(&entry).unwrap();
444        // Option<String> fields set to None should be absent
445        assert!(!json.contains("data_residency"));
446        assert!(!json.contains("transfer_basis"));
447        assert!(!json.contains("metadata"));
448        // Empty Vec should be absent (skip_serializing_if = "Vec::is_empty")
449        assert!(!json.contains("policy_violations"));
450    }
451
452    #[test]
453    fn audit_log_entry_with_metadata_round_trips() {
454        let mut meta = HashMap::new();
455        meta.insert(
456            "region".to_string(),
457            serde_json::Value::String("ap-southeast-3".to_string()),
458        );
459
460        let entry = AuditLogEntry {
461            id: "audit-004".to_string(),
462            data_residency: Some("id-jakarta".to_string()),
463            transfer_basis: Some("pdp-consent".to_string()),
464            metadata: Some(meta),
465            ..Default::default()
466        };
467
468        let json = serde_json::to_string(&entry).unwrap();
469        let back: AuditLogEntry = serde_json::from_str(&json).unwrap();
470        assert_eq!(back.data_residency.as_deref(), Some("id-jakarta"));
471        assert_eq!(back.transfer_basis.as_deref(), Some("pdp-consent"));
472        assert!(back.metadata.is_some());
473        assert_eq!(
474            back.metadata.unwrap().get("region").unwrap(),
475            &serde_json::Value::String("ap-southeast-3".to_string())
476        );
477    }
478
479    // v0.6.0 (platform #2513): pasal_56b_dpa accepted; existing values kept.
480
481    #[test]
482    fn transfer_basis_constants_wire_values() {
483        assert_eq!(transfer_basis::ADEQUACY, "adequacy");
484        assert_eq!(transfer_basis::SAFEGUARDS, "safeguards");
485        assert_eq!(transfer_basis::PASAL_56B_DPA, "pasal_56b_dpa");
486        assert_eq!(transfer_basis::CONSENT, "consent");
487    }
488
489    #[test]
490    fn audit_log_entry_pasal_56b_dpa_round_trips_verbatim() {
491        let json = r#"{
492            "id": "aud-56b",
493            "timestamp": "2026-05-30T10:00:00Z",
494            "data_residency": "ID",
495            "transfer_basis": "pasal_56b_dpa"
496        }"#;
497        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
498        assert_eq!(
499            entry.transfer_basis.as_deref(),
500            Some(transfer_basis::PASAL_56B_DPA)
501        );
502
503        // never auto-translated to "safeguards"
504        let back: AuditLogEntry =
505            serde_json::from_str(&serde_json::to_string(&entry).unwrap()).unwrap();
506        assert_eq!(back.transfer_basis.as_deref(), Some("pasal_56b_dpa"));
507    }
508
509    #[test]
510    fn audit_log_entry_safeguards_backward_compat() {
511        // Existing v0.5.0-shaped rows using "safeguards" are unaffected by the widening.
512        let json =
513            r#"{"id":"aud-sg","timestamp":"2026-05-26T10:00:00Z","transfer_basis":"safeguards"}"#;
514        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
515        assert_eq!(
516            entry.transfer_basis.as_deref(),
517            Some(transfer_basis::SAFEGUARDS)
518        );
519    }
520}