use std::collections::VecDeque;
use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use crate::guard::AgentSnapshot;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PolicyId(pub(crate) u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyScope {
NextTurn,
EveryTurnUntilRemoved,
Persistent,
}
pub trait IntentMatcher: Send + Sync {
fn matches(&self, snapshot: &AgentSnapshot) -> bool;
}
impl<F> IntentMatcher for F
where
F: Fn(&AgentSnapshot) -> bool + Send + Sync,
{
fn matches(&self, snapshot: &AgentSnapshot) -> bool {
self(snapshot)
}
}
pub enum PolicyTrigger {
Always,
OnIntentMatch(Box<dyn IntentMatcher>),
}
pub struct PromptPolicy {
pub name: String,
pub scope: PolicyScope,
pub content: String,
pub precedence: u8,
pub trigger: PolicyTrigger,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyMetadata {
pub name: String,
pub scope: PolicyScope,
pub precedence: u8,
pub trigger: PolicyTriggerMetadata,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyTriggerMetadata {
Always,
IntentMatcher,
}
#[derive(Debug, thiserror::Error)]
pub enum PolicyError {
#[error("prompt policy name is empty")]
EmptyName,
#[error("prompt policy content is empty")]
EmptyContent,
#[error("prompt policy precedence {precedence} is already used by policy {existing}")]
DuplicatePrecedence { precedence: u8, existing: String },
#[error("prompt policy not found")]
NotFound,
}
#[derive(Clone)]
enum StoredTrigger {
Always,
OnIntentMatch(Arc<dyn IntentMatcher>),
}
struct PolicyEntry {
id: PolicyId,
name: String,
scope: PolicyScope,
content: String,
precedence: u8,
trigger: StoredTrigger,
}
#[derive(Default)]
pub(crate) struct PromptPolicySet {
next_id: u64,
policies: VecDeque<PolicyEntry>,
}
#[derive(Clone)]
pub(crate) struct PromptPolicyCandidate {
id: PolicyId,
trigger: StoredTrigger,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AppliedPromptPolicy {
pub id: PolicyId,
pub name: String,
pub content: String,
pub precedence: u8,
}
impl PromptPolicyCandidate {
pub(crate) fn id(&self) -> PolicyId {
self.id
}
pub(crate) fn matches(&self, snapshot: &AgentSnapshot) -> bool {
std::panic::catch_unwind(AssertUnwindSafe(|| match &self.trigger {
StoredTrigger::Always => true,
StoredTrigger::OnIntentMatch(matcher) => matcher.matches(snapshot),
}))
.unwrap_or(false)
}
}
impl PromptPolicySet {
pub(crate) fn install(&mut self, policy: PromptPolicy) -> Result<PolicyId, PolicyError> {
if policy.name.trim().is_empty() {
return Err(PolicyError::EmptyName);
}
if policy.content.trim().is_empty() {
return Err(PolicyError::EmptyContent);
}
if let Some(existing) = self
.policies
.iter()
.find(|entry| entry.precedence == policy.precedence)
{
return Err(PolicyError::DuplicatePrecedence {
precedence: policy.precedence,
existing: existing.name.clone(),
});
}
let trigger = match policy.trigger {
PolicyTrigger::Always => StoredTrigger::Always,
PolicyTrigger::OnIntentMatch(matcher) => {
StoredTrigger::OnIntentMatch(Arc::from(matcher))
}
};
let id = PolicyId(self.next_id);
self.next_id += 1;
self.policies.push_back(PolicyEntry {
id,
name: policy.name,
scope: policy.scope,
content: policy.content,
precedence: policy.precedence,
trigger,
});
Ok(id)
}
pub(crate) fn remove(&mut self, id: PolicyId) -> Result<(), PolicyError> {
let Some(idx) = self.policies.iter().position(|entry| entry.id == id) else {
return Err(PolicyError::NotFound);
};
self.policies.remove(idx);
Ok(())
}
pub(crate) fn list(&self) -> Vec<(PolicyId, PolicyMetadata)> {
let mut policies: Vec<_> = self
.policies
.iter()
.map(|entry| (entry.id, metadata(entry)))
.collect();
policies.sort_by_key(|(_, metadata)| metadata.precedence);
policies
}
pub(crate) fn candidates(&self) -> Vec<PromptPolicyCandidate> {
self.policies
.iter()
.map(|entry| PromptPolicyCandidate {
id: entry.id,
trigger: entry.trigger.clone(),
})
.collect()
}
pub(crate) fn apply_matches(&mut self, matched_ids: &[PolicyId]) -> Vec<AppliedPromptPolicy> {
let mut applied = Vec::new();
let mut idx = 0;
while idx < self.policies.len() {
let is_match = matched_ids.contains(&self.policies[idx].id);
if is_match {
let entry = &self.policies[idx];
applied.push(AppliedPromptPolicy {
id: entry.id,
name: entry.name.clone(),
content: entry.content.clone(),
precedence: entry.precedence,
});
}
if is_match && self.policies[idx].scope == PolicyScope::NextTurn {
self.policies.remove(idx);
} else {
idx += 1;
}
}
applied.sort_by_key(|policy| policy.precedence);
applied
}
}
fn metadata(policy: &PolicyEntry) -> PolicyMetadata {
PolicyMetadata {
name: policy.name.clone(),
scope: policy.scope,
precedence: policy.precedence,
trigger: match policy.trigger {
StoredTrigger::Always => PolicyTriggerMetadata::Always,
StoredTrigger::OnIntentMatch(_) => PolicyTriggerMetadata::IntentMatcher,
},
}
}