#![allow(clippy::print_stdout, clippy::cast_precision_loss)]
use std::sync::Arc;
use async_trait::async_trait;
use entelix::tools::{Tool, ToolMetadata, ToolRegistry};
use entelix::{
AgentContext, ApprovalDecision, ApprovalLayer, ApprovalRequest, Approver, Error,
ExecutionContext, Result,
};
use serde_json::{Value, json};
struct SensitiveActionTool {
metadata: ToolMetadata,
}
impl SensitiveActionTool {
fn new() -> Self {
Self {
metadata: ToolMetadata::function(
"process_payment",
"Charge a customer's payment method.",
json!({
"type": "object",
"properties": {
"customer_id": { "type": "string" },
"amount_cents": { "type": "integer" },
},
"required": ["customer_id", "amount_cents"],
}),
),
}
}
}
#[async_trait]
impl Tool for SensitiveActionTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: Value, _ctx: &AgentContext<()>) -> Result<Value> {
let customer = input
.get("customer_id")
.and_then(Value::as_str)
.unwrap_or("(unspecified)");
let amount = input
.get("amount_cents")
.and_then(Value::as_i64)
.unwrap_or(0);
Ok(json!({
"status": "executed",
"customer_id": customer,
"amount_cents": amount,
"message": format!("would have charged customer {customer} ${:.2} (example only)", amount as f64 / 100.0),
}))
}
}
struct AlwaysAwaitExternal;
#[async_trait]
impl Approver for AlwaysAwaitExternal {
async fn decide(
&self,
_request: &ApprovalRequest,
_ctx: &ExecutionContext,
) -> Result<ApprovalDecision> {
Ok(ApprovalDecision::AwaitExternal)
}
}
#[tokio::main]
async fn main() -> Result<()> {
let approver: Arc<dyn Approver> = Arc::new(AlwaysAwaitExternal);
let registry = ToolRegistry::new()
.layer(ApprovalLayer::new(Arc::clone(&approver)))
.register(Arc::new(SensitiveActionTool::new()))?;
let tool_use_id = "charge-customer-acme-2026-05-03".to_owned();
let tool_input = json!({ "customer_id": "acme", "amount_cents": 4999 });
println!("=== first dispatch — should pause for out-of-band review ===");
match registry
.dispatch(
&tool_use_id,
"process_payment",
tool_input.clone(),
&ExecutionContext::new(),
)
.await
{
Err(Error::Interrupted { kind, payload }) => {
println!("agent paused for human review.");
println!("kind: {kind:?}");
println!("payload: {payload:#}");
}
Ok(_) => {
println!("WARNING: expected an interrupt, the dispatch ran without approval");
return Ok(());
}
Err(other) => {
println!("unexpected error: {other}");
return Ok(());
}
}
println!();
println!("=== operator reviews out-of-band and approves ===");
println!(
"(pretend we routed payload['tool_use_id'] to a Slack \
#ops-approvals channel and got a thumbs-up emoji back)"
);
println!();
println!("In a real agent built on `entelix::ReActAgentBuilder`, the");
println!("resume call uses the typed `Command::ApproveTool` primitive:");
println!();
println!(" let final_state = compiled_graph");
println!(" .resume_with(");
println!(" Command::ApproveTool {{");
println!(" tool_use_id: \"{tool_use_id}\".into(),");
println!(" decision: ApprovalDecision::Approve,");
println!(" }},");
println!(" &ctx,");
println!(" ).await?;");
println!();
println!("CompiledGraph::resume_with attaches the decision to the");
println!("ExecutionContext internally; the dispatch re-fires from the");
println!("checkpoint and the approval layer short-circuits the approver");
println!("for this tool_use_id.");
println!();
println!("Below we simulate the same effect by attaching the decision");
println!("directly (lower-level path — useful for non-graph dispatch).");
let mut pending = entelix::PendingApprovalDecisions::new();
pending.insert(&tool_use_id, ApprovalDecision::Approve);
let resume_ctx = ExecutionContext::new().add_extension(pending);
println!();
println!("=== second dispatch with the decision attached ===");
let result = registry
.dispatch(&tool_use_id, "process_payment", tool_input, &resume_ctx)
.await?;
println!("tool result: {result:#}");
println!();
println!("=== alternative: operator denies out-of-band ===");
let mut denied = entelix::PendingApprovalDecisions::new();
denied.insert(
&tool_use_id,
ApprovalDecision::Reject {
reason: "amount exceeds operator-approval ceiling".to_owned(),
},
);
let denied_ctx = ExecutionContext::new().add_extension(denied);
match registry
.dispatch(
&tool_use_id,
"process_payment",
json!({ "customer_id": "acme", "amount_cents": 4999 }),
&denied_ctx,
)
.await
{
Err(Error::InvalidRequest(msg)) => {
println!("denial surfaced as typed error:");
println!(" {msg}");
}
Ok(value) => println!("WARNING: expected denial, got {value:#}"),
Err(other) => println!("unexpected error: {other}"),
}
Ok(())
}