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}