use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use agent_base::{
AgentResult, ApprovalDecision, ApprovalHandler, ApprovalRequest, ChatMessage, LlmCapabilities, LlmClient,
ReasoningConfig, ResponseFormat, RiskLevel, StreamChunk,
};
use async_trait::async_trait;
use futures_core::Stream;
struct QueueStream {
items: VecDeque<AgentResult<StreamChunk>>,
}
impl Stream for QueueStream {
type Item = AgentResult<StreamChunk>;
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Ready(self.items.pop_front())
}
}
struct MockLlmClient;
impl MockLlmClient {
fn new() -> Self {
Self
}
}
#[async_trait]
impl LlmClient for MockLlmClient {
async fn chat(
&self,
_messages: &[ChatMessage],
_tools: &[serde_json::Value],
_reasoning: Option<&ReasoningConfig>,
_response_format: Option<&ResponseFormat>,
) -> AgentResult<serde_json::Value> {
Ok(serde_json::json!({"choices":[{"message":{"content":"mock response"}}]}))
}
async fn chat_stream(
&self,
_messages: &[ChatMessage],
_tools: &[serde_json::Value],
_reasoning: Option<&ReasoningConfig>,
_response_format: Option<&ResponseFormat>,
) -> AgentResult<Pin<Box<dyn Stream<Item = AgentResult<StreamChunk>> + Send>>> {
let mut items = VecDeque::new();
items.push_back(Ok(StreamChunk::Text("Hello from mock!".into())));
items.push_back(Ok(StreamChunk::Stop { finish_reason: Some("stop".into()) }));
Ok(Box::pin(QueueStream { items }))
}
fn capabilities(&self) -> LlmCapabilities {
LlmCapabilities {
supports_streaming: true,
supports_tools: true,
supports_vision: false,
supports_thinking: false,
max_context_tokens: Some(128_000),
max_output_tokens: Some(16_384),
}
}
}
pub struct RiskBasedApprovalHandler;
impl RiskBasedApprovalHandler {
pub fn new() -> Self {
Self
}
}
impl Default for RiskBasedApprovalHandler {
fn default() -> Self {
Self
}
}
#[async_trait]
impl ApprovalHandler for RiskBasedApprovalHandler {
async fn approve(
&self,
request: ApprovalRequest,
_cancel_token: tokio_util::sync::CancellationToken,
) -> AgentResult<ApprovalDecision> {
let decision = match request.risk_level {
RiskLevel::Safe => {
eprintln!(" [APPROVE] '{}' is safe — AllowAlways", request.title);
ApprovalDecision::AllowAlways
},
RiskLevel::Sensitive => {
eprintln!(" [APPROVE] '{}' is sensitive — AllowOnce (will re-prompt next time)", request.title);
ApprovalDecision::AllowOnce
},
RiskLevel::Destructive => {
eprintln!(" [DENY] '{}' is destructive — Deny\n reason: {}", request.title, request.message);
ApprovalDecision::Deny
},
};
Ok(decision)
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("=== Custom ApprovalHandler Demo ===\n");
println!("This example demonstrates a risk-based approval handler.\n");
println!("Strategy:");
println!(" Safe → AllowAlways (cached — won't prompt again)");
println!(" Sensitive → AllowOnce (approve this once, re-prompt later)");
println!(" Destructive → Deny (always reject)\n");
let llm_client = agent_base::llm::adapt(Arc::new(MockLlmClient::new()));
let approval_handler = Arc::new(RiskBasedApprovalHandler::new());
let runtime = phi_agent::base_agent_builder(llm_client)
.system_prompt("You are a helpful assistant.".to_string())
.approval_handler(approval_handler)
.build()?;
let session = runtime.create_session().await;
println!("--- Running turn: 'hello' ---\n");
runtime
.run_turn(session, "hello", |event| {
if let agent_base::RuntimeEvent::TextDelta { text, .. } = &event {
eprintln!(" [LLM] {text}");
}
Ok(())
})
.await?;
println!("\n--- Done ---");
println!("\nTo use this handler in your own code:");
println!(" let handler = Arc::new(RiskBasedApprovalHandler::new());");
println!(" builder.approval_handler(handler);");
Ok(())
}