turul_a2a_patterns/gate.rs
1//! Pre-terminal gates: checks that run after a handler returns its output
2//! and before the executor makes the task terminal, and that can turn a
3//! successful output into a failure.
4
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use serde_json::Value;
9
10use crate::error::SkillError;
11use crate::handler::SkillHandler;
12use crate::sink::SkillProgressSink;
13
14/// What a gate sees: the skill id, the invocation params and the output
15/// the handler produced.
16#[non_exhaustive]
17#[derive(Debug, Clone, Copy)]
18pub struct GateContext<'a> {
19 pub skill_id: &'a str,
20 pub params: &'a Value,
21 pub output: &'a Value,
22}
23
24/// A check that can veto a successful skill output.
25///
26/// `Ok(())` lets the output through. `Err` replaces it: the executor
27/// records the error as it would any handler failure, and a
28/// `TerminalHook` observes it as `SkillOutcome::Failure`. A veto is
29/// `SkillError::Rejected` naming the gate; `Internal` is for the gate's
30/// own failure (for example an unreachable dependency), so a refusal is
31/// never mistaken for a fault.
32///
33/// A gate sees only the output the handler returns, not artifacts the
34/// handler streamed through its sink. A handler whose result must pass a
35/// gate returns what the gate checks. Gates receive no sink: they
36/// decide, they do not emit events.
37#[async_trait]
38pub trait PreTerminalGate: Send + Sync {
39 async fn check(&self, ctx: GateContext<'_>) -> Result<(), SkillError>;
40}
41
42/// A `SkillHandler` that runs its gates on the inner handler's output.
43///
44/// The inner handler runs first; its error is returned unchanged and no
45/// gate runs. On success the gates run in the order they were added, and
46/// the first error stops the rest and becomes the result. Register it
47/// like any other handler.
48pub struct GatedHandler {
49 skill_id: String,
50 inner: Arc<dyn SkillHandler>,
51 gates: Vec<Arc<dyn PreTerminalGate>>,
52}
53
54impl GatedHandler {
55 pub fn new(skill_id: impl Into<String>, inner: Arc<dyn SkillHandler>) -> Self {
56 Self {
57 skill_id: skill_id.into(),
58 inner,
59 gates: Vec::new(),
60 }
61 }
62
63 /// Adds a gate after those already added.
64 pub fn gate(mut self, gate: Arc<dyn PreTerminalGate>) -> Self {
65 self.gates.push(gate);
66 self
67 }
68}
69
70#[async_trait]
71impl SkillHandler for GatedHandler {
72 async fn run(&self, params: Value, sink: &dyn SkillProgressSink) -> Result<Value, SkillError> {
73 let output = self.inner.run(params.clone(), sink).await?;
74 for gate in &self.gates {
75 gate.check(GateContext {
76 skill_id: &self.skill_id,
77 params: ¶ms,
78 output: &output,
79 })
80 .await?;
81 }
82 Ok(output)
83 }
84}
85
86const _: fn() = || {
87 fn assert<T: ?Sized + PreTerminalGate>() {}
88 assert::<dyn PreTerminalGate>();
89};