Skip to main content

turul_a2a/
executor.rs

1//! AgentExecutor trait — the user-facing contract for agent behavior.
2
3use async_trait::async_trait;
4use tokio_util::sync::CancellationToken;
5use turul_a2a_types::{Message, Task};
6
7use crate::error::A2aError;
8use crate::event_sink::EventSink;
9
10/// Context available to the executor during message processing.
11///
12/// Provides auth identity, tenant/task metadata, cooperative cancellation,
13/// and an [`EventSink`] for durable event emission.
14/// `#[non_exhaustive]` allows adding fields in future versions.
15#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub struct ExecutionContext {
18    /// The authenticated owner (from middleware). Empty string for anonymous.
19    pub owner: String,
20    /// Tenant scope. `None` on default (un-tenanted) routes.
21    pub tenant: Option<String>,
22    /// The task being processed.
23    pub task_id: String,
24    /// Context ID for this conversation. `None` if not yet assigned.
25    pub context_id: Option<String>,
26    /// Auth claims from middleware (JWT claims, API key metadata, etc.).
27    pub claims: Option<serde_json::Value>,
28    /// Cooperative cancellation token. Check `cancellation.is_cancelled()`
29    /// in long-running loops to respect client cancellation.
30    pub cancellation: CancellationToken,
31    /// Durable event sink. Executors that want to drive task
32    /// lifecycle themselves — progress artifacts, interrupted states,
33    /// explicit terminal — call the methods on this sink.
34    ///
35    /// Production send paths install a live sink that writes through the
36    /// atomic store and fires the framework's blocking-send awaiter on
37    /// terminal or interrupted commit.
38    /// [`ExecutionContext::anonymous`] returns a detached sink — emits
39    /// return `A2aError::Internal`. Executor unit tests that don't stand
40    /// up a server can use it as-is; they simply cannot drive durable
41    /// lifecycle events without real storage.
42    pub events: EventSink,
43}
44
45impl ExecutionContext {
46    /// Create a context for anonymous/unauthenticated execution.
47    ///
48    /// The [`events`] sink is [`EventSink::detached`] — emits return
49    /// `A2aError::Internal`. Use this for executor unit tests that don't
50    /// need durable event persistence. Production code constructs
51    /// `ExecutionContext` with a live sink wired to the framework's
52    /// atomic store.
53    ///
54    /// [`events`]: Self#structfield.events
55    pub fn anonymous(task_id: &str) -> Self {
56        Self {
57            owner: "anonymous".to_string(),
58            tenant: None,
59            task_id: task_id.to_string(),
60            context_id: None,
61            claims: None,
62            cancellation: CancellationToken::new(),
63            events: EventSink::detached(),
64        }
65    }
66}
67
68/// Trait that users implement to define agent behavior.
69///
70/// The server calls `execute` when a message arrives. The executor
71/// updates the task's status, appends artifacts, etc.
72#[async_trait]
73pub trait AgentExecutor: Send + Sync {
74    /// Process a message against a task.
75    ///
76    /// The task is mutable — update its status, append messages/artifacts.
77    /// Return `Ok(())` on success. The server persists the updated task.
78    ///
79    /// `ctx` provides auth identity, tenant, and cancellation support.
80    /// Executors that don't need context can ignore it with `_ctx`.
81    async fn execute(
82        &self,
83        task: &mut Task,
84        message: &Message,
85        ctx: &ExecutionContext,
86    ) -> Result<(), A2aError>;
87
88    /// Return the public agent card (unauthenticated discovery).
89    fn agent_card(&self) -> turul_a2a_proto::AgentCard;
90
91    /// Return the extended agent card for authenticated callers.
92    /// Return `None` if extended card is not supported.
93    fn extended_agent_card(
94        &self,
95        _claims: Option<&serde_json::Value>,
96    ) -> Option<turul_a2a_proto::AgentCard> {
97        None
98    }
99}