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, Serialize, Deserialize, 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#[must_use]
350#[derive(Debug, Serialize, Deserialize, Clone, Default)]
351#[serde(default)]
352pub struct CancelPlanResponse {
353    pub plan_id: String,
354    pub status: String,
355    pub success: bool,
356}
357
358#[derive(Debug, Serialize, Deserialize, Clone)]
359pub struct CodeArtifact {
360    pub is_code_output: bool,
361    pub language: String,
362    pub code_type: String,
363    pub size_bytes: usize,
364    pub line_count: usize,
365    pub secrets_detected: usize,
366    pub unsafe_patterns: usize,
367    #[serde(default, deserialize_with = "null_to_default")]
368    pub policies_checked: Vec<String>,
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn audit_log_entry_with_residency_fields_deserializes() {
377        let json = r#"{
378            "id": "audit-001",
379            "request_id": "req-123",
380            "timestamp": "2026-05-26T00:00:00Z",
381            "user_email": "user@example.com",
382            "client_id": "client-1",
383            "tenant_id": "tenant-1",
384            "request_type": "llm_query",
385            "query_summary": "test query",
386            "success": true,
387            "blocked": false,
388            "risk_score": 0.15,
389            "provider": "openai",
390            "model": "gpt-4",
391            "tokens_used": 500,
392            "latency_ms": 120,
393            "policy_violations": [],
394            "data_residency": "id-jakarta",
395            "transfer_basis": "pdp-consent"
396        }"#;
397
398        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
399        assert_eq!(entry.id, "audit-001");
400        assert_eq!(entry.data_residency.as_deref(), Some("id-jakarta"));
401        assert_eq!(entry.transfer_basis.as_deref(), Some("pdp-consent"));
402        assert!(entry.success);
403        assert!(!entry.blocked);
404        assert_eq!(entry.tokens_used, 500);
405    }
406
407    #[test]
408    fn audit_log_entry_without_new_fields_deserializes_backward_compat() {
409        let json = r#"{
410            "id": "audit-002",
411            "request_id": "req-456",
412            "timestamp": "2026-05-26T00:00:00Z",
413            "user_email": "user@example.com",
414            "client_id": "client-1",
415            "tenant_id": "tenant-1",
416            "request_type": "llm_query",
417            "query_summary": "test query",
418            "success": true,
419            "blocked": false,
420            "risk_score": 0.0,
421            "provider": "anthropic",
422            "model": "claude-3",
423            "tokens_used": 100,
424            "latency_ms": 50
425        }"#;
426
427        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
428        assert_eq!(entry.id, "audit-002");
429        assert!(entry.data_residency.is_none());
430        assert!(entry.transfer_basis.is_none());
431        assert!(entry.policy_violations.is_empty());
432        assert!(entry.metadata.is_none());
433    }
434
435    #[test]
436    fn audit_log_entry_empty_optional_fields_omitted_in_serialization() {
437        let entry = AuditLogEntry {
438            id: "audit-003".to_string(),
439            request_id: "req-789".to_string(),
440            success: true,
441            ..Default::default()
442        };
443
444        let json = serde_json::to_string(&entry).unwrap();
445        // Option<String> fields set to None should be absent
446        assert!(!json.contains("data_residency"));
447        assert!(!json.contains("transfer_basis"));
448        assert!(!json.contains("metadata"));
449        // Empty Vec should be absent (skip_serializing_if = "Vec::is_empty")
450        assert!(!json.contains("policy_violations"));
451    }
452
453    #[test]
454    fn audit_log_entry_with_metadata_round_trips() {
455        let mut meta = HashMap::new();
456        meta.insert(
457            "region".to_string(),
458            serde_json::Value::String("ap-southeast-3".to_string()),
459        );
460
461        let entry = AuditLogEntry {
462            id: "audit-004".to_string(),
463            data_residency: Some("id-jakarta".to_string()),
464            transfer_basis: Some("pdp-consent".to_string()),
465            metadata: Some(meta),
466            ..Default::default()
467        };
468
469        let json = serde_json::to_string(&entry).unwrap();
470        let back: AuditLogEntry = serde_json::from_str(&json).unwrap();
471        assert_eq!(back.data_residency.as_deref(), Some("id-jakarta"));
472        assert_eq!(back.transfer_basis.as_deref(), Some("pdp-consent"));
473        assert!(back.metadata.is_some());
474        assert_eq!(
475            back.metadata.unwrap().get("region").unwrap(),
476            &serde_json::Value::String("ap-southeast-3".to_string())
477        );
478    }
479
480    // v0.6.0 (platform #2513): pasal_56b_dpa accepted; existing values kept.
481
482    #[test]
483    fn transfer_basis_constants_wire_values() {
484        assert_eq!(transfer_basis::ADEQUACY, "adequacy");
485        assert_eq!(transfer_basis::SAFEGUARDS, "safeguards");
486        assert_eq!(transfer_basis::PASAL_56B_DPA, "pasal_56b_dpa");
487        assert_eq!(transfer_basis::CONSENT, "consent");
488    }
489
490    #[test]
491    fn audit_log_entry_pasal_56b_dpa_round_trips_verbatim() {
492        let json = r#"{
493            "id": "aud-56b",
494            "timestamp": "2026-05-30T10:00:00Z",
495            "data_residency": "ID",
496            "transfer_basis": "pasal_56b_dpa"
497        }"#;
498        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
499        assert_eq!(
500            entry.transfer_basis.as_deref(),
501            Some(transfer_basis::PASAL_56B_DPA)
502        );
503
504        // never auto-translated to "safeguards"
505        let back: AuditLogEntry =
506            serde_json::from_str(&serde_json::to_string(&entry).unwrap()).unwrap();
507        assert_eq!(back.transfer_basis.as_deref(), Some("pasal_56b_dpa"));
508    }
509
510    #[test]
511    fn audit_log_entry_safeguards_backward_compat() {
512        // Existing v0.5.0-shaped rows using "safeguards" are unaffected by the widening.
513        let json =
514            r#"{"id":"aud-sg","timestamp":"2026-05-26T10:00:00Z","transfer_basis":"safeguards"}"#;
515        let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
516        assert_eq!(
517            entry.transfer_basis.as_deref(),
518            Some(transfer_basis::SAFEGUARDS)
519        );
520    }
521}