Skip to main content

tirea_contract/runtime/state/
scope_context.rs

1use super::spec::StateScope;
2
3/// Routing context that resolves storage paths based on scope and call identity.
4///
5/// `ScopeContext` is threaded through the state reduction pipeline so that
6/// `ToolCall`-scoped actions are transparently routed to a per-call namespace
7/// (`__tool_call_scope.<call_id>.<base_path>`) without plugins needing to know
8/// the call id.
9#[derive(Debug, Clone)]
10pub struct ScopeContext {
11    call_id: Option<String>,
12}
13
14impl ScopeContext {
15    /// Create a scope context without a call id (used for Thread and Run scopes).
16    pub fn run() -> Self {
17        Self { call_id: None }
18    }
19
20    /// Create a scope context for a specific tool call.
21    pub fn for_call(call_id: impl Into<String>) -> Self {
22        Self {
23            call_id: Some(call_id.into()),
24        }
25    }
26
27    /// The call id, if this is a tool-call-scoped context.
28    pub fn call_id(&self) -> Option<&str> {
29        self.call_id.as_deref()
30    }
31
32    /// Resolve a (scope, base_path) pair to the actual storage path.
33    ///
34    /// - `Thread` / `Run` scope: returns `base_path` unchanged.
35    /// - `ToolCall` scope with a call id: returns `__tool_call_scope.<id>.<base_path>`.
36    /// - `ToolCall` scope without a call id: falls back to `base_path`.
37    pub fn resolve_path(&self, scope: StateScope, base_path: &str) -> String {
38        match (scope, &self.call_id) {
39            (StateScope::ToolCall, Some(id)) => {
40                format!("__tool_call_scope.{}.{}", id, base_path)
41            }
42            _ => base_path.to_string(),
43        }
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn run_scope_returns_base_path() {
53        let ctx = ScopeContext::run();
54        assert_eq!(ctx.resolve_path(StateScope::Run, "my_state"), "my_state");
55    }
56
57    #[test]
58    fn run_scope_with_tool_call_scoped_state_falls_back() {
59        let ctx = ScopeContext::run();
60        assert_eq!(
61            ctx.resolve_path(StateScope::ToolCall, "my_state"),
62            "my_state"
63        );
64    }
65
66    #[test]
67    fn for_call_routes_tool_call_scope() {
68        let ctx = ScopeContext::for_call("call_42");
69        assert_eq!(
70            ctx.resolve_path(StateScope::ToolCall, "my_plugin.tool_ctx"),
71            "__tool_call_scope.call_42.my_plugin.tool_ctx"
72        );
73    }
74
75    #[test]
76    fn for_call_leaves_run_scope_unchanged() {
77        let ctx = ScopeContext::for_call("call_42");
78        assert_eq!(ctx.resolve_path(StateScope::Run, "my_state"), "my_state");
79    }
80
81    #[test]
82    fn thread_scope_returns_base_path() {
83        let ctx = ScopeContext::run();
84        assert_eq!(
85            ctx.resolve_path(StateScope::Thread, "reminders"),
86            "reminders"
87        );
88    }
89
90    #[test]
91    fn for_call_leaves_thread_scope_unchanged() {
92        let ctx = ScopeContext::for_call("call_42");
93        assert_eq!(
94            ctx.resolve_path(StateScope::Thread, "reminders"),
95            "reminders"
96        );
97    }
98
99    #[test]
100    fn call_id_accessor() {
101        assert_eq!(ScopeContext::run().call_id(), None);
102        assert_eq!(ScopeContext::for_call("x").call_id(), Some("x"));
103    }
104}