Skip to main content

ai_agents_runtime/
turn_context.rs

1use serde::{Deserialize, Serialize};
2
3use tokio::task_local;
4
5task_local! {
6    static TURN_ACTOR_CONTEXT: TurnActorContext;
7}
8
9/// Turn-scoped actor identity forwarded through direct chats, registry sends, and orchestration calls.
10#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
11pub struct TurnActorContext {
12    /// Original user, customer, player, or other top-level actor for the turn.
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub origin_actor_id: Option<String>,
15    /// Immediate agent sender for inter-agent hops within the same turn.
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub sender_agent_id: Option<String>,
18}
19
20impl TurnActorContext {
21    /// Create an empty turn actor context.
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Set the original actor ID for the turn.
27    pub fn with_origin_actor(mut self, actor_id: impl Into<String>) -> Self {
28        self.origin_actor_id = Some(actor_id.into());
29        self
30    }
31
32    /// Set the immediate sender agent ID for the turn.
33    pub fn with_sender_agent(mut self, agent_id: impl Into<String>) -> Self {
34        self.sender_agent_id = Some(agent_id.into());
35        self
36    }
37
38    /// Clone this context while overriding the immediate sender agent ID.
39    pub fn for_sender(&self, agent_id: impl Into<String>) -> Self {
40        let mut next = self.clone();
41        next.sender_agent_id = Some(agent_id.into());
42        next
43    }
44
45    /// Return the effective actor ID for memory features, preferring the original
46    /// actor and falling back to the immediate sender agent.
47    pub fn effective_actor_id(&self) -> Option<&str> {
48        self.origin_actor_id
49            .as_deref()
50            .or(self.sender_agent_id.as_deref())
51    }
52
53    /// Returns `true` when no actor identity is attached to the turn.
54    pub fn is_empty(&self) -> bool {
55        self.origin_actor_id.is_none() && self.sender_agent_id.is_none()
56    }
57}
58
59pub(crate) async fn scope_actor_context<F, T>(context: TurnActorContext, future: F) -> T
60where
61    F: std::future::Future<Output = T> + Send,
62    T: Send,
63{
64    TURN_ACTOR_CONTEXT.scope(context, future).await
65}
66
67pub(crate) fn current_turn_actor_context() -> Option<TurnActorContext> {
68    TURN_ACTOR_CONTEXT.try_with(Clone::clone).ok()
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_effective_actor_prefers_origin() {
77        let ctx = TurnActorContext::new()
78            .with_origin_actor("user_1")
79            .with_sender_agent("agent_a");
80        assert_eq!(ctx.effective_actor_id(), Some("user_1"));
81    }
82
83    #[test]
84    fn test_effective_actor_falls_back_to_sender() {
85        let ctx = TurnActorContext::new().with_sender_agent("agent_a");
86        assert_eq!(ctx.effective_actor_id(), Some("agent_a"));
87    }
88
89    #[test]
90    fn test_for_sender_preserves_origin() {
91        let ctx = TurnActorContext::new().with_origin_actor("user_1");
92        let next = ctx.for_sender("agent_b");
93        assert_eq!(next.origin_actor_id.as_deref(), Some("user_1"));
94        assert_eq!(next.sender_agent_id.as_deref(), Some("agent_b"));
95    }
96}