#![forbid(unsafe_code)]
use std::panic::AssertUnwindSafe;
use std::path::Path;
use std::sync::Arc;
use tracing::{debug, warn};
use crate::types::{
AgentMessage, AssistantMessage, Cost, ModelSpec, StopReason, ToolResultMessage, Usage,
};
#[non_exhaustive]
#[derive(Debug)]
pub enum PolicyVerdict {
Continue,
Stop(String),
Inject(Vec<AgentMessage>),
}
#[non_exhaustive]
#[derive(Debug)]
pub enum PreDispatchVerdict {
Continue,
Stop(String),
Inject(Vec<AgentMessage>),
Skip(String),
}
#[non_exhaustive]
#[derive(Debug)]
pub struct PolicyContext<'a> {
pub turn_index: usize,
pub accumulated_usage: &'a Usage,
pub accumulated_cost: &'a Cost,
pub message_count: usize,
pub overflow_signal: bool,
pub new_messages: &'a [AgentMessage],
pub state: &'a crate::SessionState,
}
impl<'a> PolicyContext<'a> {
#[must_use]
pub const fn new(
turn_index: usize,
accumulated_usage: &'a Usage,
accumulated_cost: &'a Cost,
message_count: usize,
overflow_signal: bool,
new_messages: &'a [AgentMessage],
state: &'a crate::SessionState,
) -> Self {
Self {
turn_index,
accumulated_usage,
accumulated_cost,
message_count,
overflow_signal,
new_messages,
state,
}
}
}
#[non_exhaustive]
pub struct ToolDispatchContext<'a> {
pub tool_name: &'a str,
pub tool_call_id: &'a str,
pub arguments: &'a mut serde_json::Value,
pub execution_root: Option<&'a Path>,
pub state: &'a crate::SessionState,
}
impl<'a> ToolDispatchContext<'a> {
#[must_use]
pub const fn new(
tool_name: &'a str,
tool_call_id: &'a str,
arguments: &'a mut serde_json::Value,
execution_root: Option<&'a Path>,
state: &'a crate::SessionState,
) -> Self {
Self {
tool_name,
tool_call_id,
arguments,
execution_root,
state,
}
}
}
impl std::fmt::Debug for ToolDispatchContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolDispatchContext")
.field("tool_name", &self.tool_name)
.field("tool_call_id", &self.tool_call_id)
.field("execution_root", &self.execution_root)
.field("arguments", &"<redacted>")
.finish()
}
}
#[non_exhaustive]
#[derive(Debug)]
pub struct TurnPolicyContext<'a> {
pub assistant_message: &'a AssistantMessage,
pub tool_results: &'a [ToolResultMessage],
pub stop_reason: StopReason,
pub system_prompt: &'a str,
pub model_spec: &'a ModelSpec,
pub context_messages: &'a [AgentMessage],
}
impl<'a> TurnPolicyContext<'a> {
#[must_use]
pub const fn new(
assistant_message: &'a AssistantMessage,
tool_results: &'a [ToolResultMessage],
stop_reason: StopReason,
system_prompt: &'a str,
model_spec: &'a ModelSpec,
context_messages: &'a [AgentMessage],
) -> Self {
Self {
assistant_message,
tool_results,
stop_reason,
system_prompt,
model_spec,
context_messages,
}
}
}
pub trait PreTurnPolicy: Send + Sync {
fn name(&self) -> &str;
fn evaluate(&self, ctx: &PolicyContext<'_>) -> PolicyVerdict;
}
pub trait PreDispatchPolicy: Send + Sync {
fn name(&self) -> &str;
fn evaluate(&self, ctx: &mut ToolDispatchContext<'_>) -> PreDispatchVerdict;
}
pub trait PostTurnPolicy: Send + Sync {
fn name(&self) -> &str;
fn evaluate(&self, ctx: &PolicyContext<'_>, turn: &TurnPolicyContext<'_>) -> PolicyVerdict;
}
pub trait PostLoopPolicy: Send + Sync {
fn name(&self) -> &str;
fn evaluate(&self, ctx: &PolicyContext<'_>) -> PolicyVerdict;
}
pub fn run_policies(policies: &[Arc<dyn PreTurnPolicy>], ctx: &PolicyContext<'_>) -> PolicyVerdict {
run_policies_inner(policies.iter().map(std::convert::AsRef::as_ref), ctx)
}
pub fn run_post_turn_policies(
policies: &[Arc<dyn PostTurnPolicy>],
ctx: &PolicyContext<'_>,
turn: &TurnPolicyContext<'_>,
) -> PolicyVerdict {
let mut injections: Vec<AgentMessage> = Vec::new();
for policy in policies {
let policy_name = policy.name().to_string();
let result = std::panic::catch_unwind(AssertUnwindSafe(|| policy.evaluate(ctx, turn)));
match result {
Ok(PolicyVerdict::Continue) => {}
Ok(PolicyVerdict::Stop(reason)) => {
debug!(policy = %policy_name, reason = %reason, "policy stopped loop");
return PolicyVerdict::Stop(reason);
}
Ok(PolicyVerdict::Inject(msgs)) => {
injections.extend(msgs);
}
Err(_) => {
warn!(policy = %policy_name, "policy panicked during evaluation, skipping");
}
}
}
if injections.is_empty() {
PolicyVerdict::Continue
} else {
PolicyVerdict::Inject(injections)
}
}
pub fn run_post_loop_policies(
policies: &[Arc<dyn PostLoopPolicy>],
ctx: &PolicyContext<'_>,
) -> PolicyVerdict {
let mut injections: Vec<AgentMessage> = Vec::new();
for policy in policies {
let policy_name = policy.name().to_string();
let result = std::panic::catch_unwind(AssertUnwindSafe(|| policy.evaluate(ctx)));
match result {
Ok(PolicyVerdict::Continue) => {}
Ok(PolicyVerdict::Stop(reason)) => {
debug!(policy = %policy_name, reason = %reason, "policy stopped loop");
return PolicyVerdict::Stop(reason);
}
Ok(PolicyVerdict::Inject(msgs)) => {
injections.extend(msgs);
}
Err(_) => {
warn!(policy = %policy_name, "policy panicked during evaluation, skipping");
}
}
}
if injections.is_empty() {
PolicyVerdict::Continue
} else {
PolicyVerdict::Inject(injections)
}
}
fn run_policies_inner<'a>(
policies: impl Iterator<Item = &'a dyn PreTurnPolicy>,
ctx: &PolicyContext<'_>,
) -> PolicyVerdict {
let mut injections: Vec<AgentMessage> = Vec::new();
for policy in policies {
let policy_name = policy.name().to_string();
let result = std::panic::catch_unwind(AssertUnwindSafe(|| policy.evaluate(ctx)));
match result {
Ok(PolicyVerdict::Continue) => {}
Ok(PolicyVerdict::Stop(reason)) => {
debug!(policy = %policy_name, reason = %reason, "policy stopped loop");
return PolicyVerdict::Stop(reason);
}
Ok(PolicyVerdict::Inject(msgs)) => {
injections.extend(msgs);
}
Err(_) => {
warn!(policy = %policy_name, "policy panicked during evaluation, skipping");
}
}
}
if injections.is_empty() {
PolicyVerdict::Continue
} else {
PolicyVerdict::Inject(injections)
}
}
pub fn run_pre_dispatch_policies(
policies: &[Arc<dyn PreDispatchPolicy>],
ctx: &mut ToolDispatchContext<'_>,
) -> PreDispatchVerdict {
let mut injections: Vec<AgentMessage> = Vec::new();
for policy in policies {
let policy_name = policy.name().to_string();
let argument_snapshot = ctx.arguments.clone();
let result = std::panic::catch_unwind(AssertUnwindSafe(|| policy.evaluate(ctx)));
match result {
Ok(PreDispatchVerdict::Continue) => {}
Ok(PreDispatchVerdict::Stop(reason)) => {
debug!(policy = %policy_name, reason = %reason, "policy stopped loop (pre-dispatch)");
return PreDispatchVerdict::Stop(reason);
}
Ok(PreDispatchVerdict::Skip(error_text)) => {
debug!(policy = %policy_name, "policy skipped tool call");
return PreDispatchVerdict::Skip(error_text);
}
Ok(PreDispatchVerdict::Inject(msgs)) => {
injections.extend(msgs);
}
Err(_) => {
*ctx.arguments = argument_snapshot;
warn!(policy = %policy_name, "policy panicked during evaluation, skipping");
}
}
}
if injections.is_empty() {
PreDispatchVerdict::Continue
} else {
PreDispatchVerdict::Inject(injections)
}
}
#[cfg(test)]
#[path = "policy_tests.rs"]
mod tests;