Skip to main content

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: &params,
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};