gemini_adk_fluent_rs/
policy.rs

1//! Policy aspects — reusable, cross-cutting governance attached to a whole
2//! conversation rather than scattered across stages.
3//!
4//! Compliance is where regulated voice flows live, and it should *feel* like
5//! attaching an aspect, not hand-wiring guards everywhere. A [`Policy`] is a
6//! serializable aspect applied with
7//! [`Conversation::policy`](crate::conversation::Conversation::policy); the
8//! compiler lowers it into concrete machinery (a safety digression, a redaction
9//! set, commit governance), always through the validated IR.
10//!
11//! ```ignore
12//! Conversation::new("payment")
13//!     .policy(Policy::redact(["card_number", "cvv"]))
14//!     .policy(Policy::commit("charge_card").idempotency_key("{user_id}:{amount}").compensate_with("refund"))
15//!     .policy(Policy::safety_handoff(["self_harm", "abuse"]))
16//!     /* … stages … */
17//!     .compile()?;
18//! ```
19
20use serde::{Deserialize, Serialize};
21
22/// A reusable, cross-cutting policy aspect.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case", tag = "kind")]
25pub enum Policy {
26    /// Hand off (terminate the conversation) when any of these intents is
27    /// detected (the `intent:{name}` flag becomes true). Lowered to a `safety`
28    /// digression with `Resume::Terminate`.
29    SafetyHandoff {
30        /// Intent names that trigger handoff.
31        intents: Vec<String>,
32    },
33    /// Redact these state keys in logs/transcripts. Recorded for the runtime's
34    /// logging layer; pairs with `#[slot(pii)]`.
35    Redact {
36        /// State keys to redact.
37        keys: Vec<String>,
38    },
39    /// Commit-tool governance: idempotency and compensation metadata for a
40    /// confirm-before-act tool.
41    Commit {
42        /// The committing tool.
43        tool: String,
44        /// Idempotency key template (`{key}` interpolated from `State`).
45        #[serde(default, skip_serializing_if = "Option::is_none")]
46        idempotency_key: Option<String>,
47        /// Tool that compensates (undoes) this commit on failure.
48        #[serde(default, skip_serializing_if = "Option::is_none")]
49        compensate_with: Option<String>,
50    },
51}
52
53impl Policy {
54    /// Terminate/hand off when any of `intents` is detected.
55    pub fn safety_handoff<I, S>(intents: I) -> Self
56    where
57        I: IntoIterator<Item = S>,
58        S: Into<String>,
59    {
60        Policy::SafetyHandoff {
61            intents: intents.into_iter().map(Into::into).collect(),
62        }
63    }
64
65    /// Redact these state keys in logs/transcripts.
66    pub fn redact<I, S>(keys: I) -> Self
67    where
68        I: IntoIterator<Item = S>,
69        S: Into<String>,
70    {
71        Policy::Redact {
72            keys: keys.into_iter().map(Into::into).collect(),
73        }
74    }
75
76    /// Begin a commit-governance policy for `tool`.
77    pub fn commit(tool: impl Into<String>) -> CommitPolicy {
78        CommitPolicy {
79            tool: tool.into(),
80            idempotency_key: None,
81            compensate_with: None,
82        }
83    }
84
85    /// The state keys this policy marks for redaction (empty for non-redact).
86    pub fn redacted_keys(&self) -> &[String] {
87        match self {
88            Policy::Redact { keys } => keys,
89            _ => &[],
90        }
91    }
92}
93
94/// Builder for a [`Policy::Commit`] governance aspect.
95#[derive(Debug, Clone)]
96pub struct CommitPolicy {
97    tool: String,
98    idempotency_key: Option<String>,
99    compensate_with: Option<String>,
100}
101
102impl CommitPolicy {
103    /// Set the idempotency key template (`{key}` interpolated from `State`).
104    pub fn idempotency_key(mut self, template: impl Into<String>) -> Self {
105        self.idempotency_key = Some(template.into());
106        self
107    }
108
109    /// Set the compensating tool (undoes the commit on failure).
110    pub fn compensate_with(mut self, tool: impl Into<String>) -> Self {
111        self.compensate_with = Some(tool.into());
112        self
113    }
114}
115
116impl From<CommitPolicy> for Policy {
117    fn from(c: CommitPolicy) -> Self {
118        Policy::Commit {
119            tool: c.tool,
120            idempotency_key: c.idempotency_key,
121            compensate_with: c.compensate_with,
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn commit_builder_into_policy() {
132        let p: Policy = Policy::commit("charge_card")
133            .idempotency_key("{user_id}:{amount}")
134            .compensate_with("refund")
135            .into();
136        assert_eq!(
137            p,
138            Policy::Commit {
139                tool: "charge_card".into(),
140                idempotency_key: Some("{user_id}:{amount}".into()),
141                compensate_with: Some("refund".into()),
142            }
143        );
144    }
145
146    #[test]
147    fn policies_round_trip_through_json() {
148        let policies = vec![
149            Policy::redact(["card_number", "cvv"]),
150            Policy::safety_handoff(["self_harm"]),
151            Policy::commit("book").idempotency_key("{id}").into(),
152        ];
153        let json = serde_json::to_string(&policies).unwrap();
154        let back: Vec<Policy> = serde_json::from_str(&json).unwrap();
155        assert_eq!(policies, back);
156        assert_eq!(back[0].redacted_keys(), &["card_number", "cvv"]);
157    }
158}