Skip to main content

agent_line/
ctx.rs

1use std::collections::HashMap;
2
3/// Execution context shared across all agents in a workflow.
4///
5/// `Ctx` carries two things:
6/// - a string key-value store for cross-agent data,
7/// - an append-only event log for diagnostic messages.
8///
9/// State is not reset between [`crate::Runner::run`] calls, so the store and
10/// log accumulate across runs unless cleared explicitly with
11/// [`clear_logs`](Ctx::clear_logs) or [`clear`](Ctx::clear).
12///
13/// LLM access lives on [`crate::LlmConfig`], not on `Ctx`. Agents that need
14/// an LLM hold their own [`crate::LlmConfig`] and call
15/// [`crate::LlmConfig::request`] to start a chat request.
16pub struct Ctx {
17    store: HashMap<String, String>,
18    log: Vec<String>,
19}
20
21impl Ctx {
22    /// Create a new empty context (KV store and event log).
23    pub fn new() -> Self {
24        Self {
25            store: HashMap::new(),
26            log: vec![],
27        }
28    }
29
30    /// Insert or overwrite a key in the KV store.
31    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
32        self.store.insert(key.into(), value.into());
33    }
34
35    /// Look up a key in the KV store.
36    pub fn get(&self, key: &str) -> Option<&str> {
37        self.store.get(key).map(|s| s.as_str())
38    }
39
40    /// Remove a key from the KV store, returning its value if it existed.
41    pub fn remove(&mut self, key: &str) -> Option<String> {
42        self.store.remove(key)
43    }
44
45    /// Append a message to the event log.
46    pub fn log(&mut self, msg: impl Into<String>) {
47        self.log.push(msg.into());
48    }
49
50    /// Return all log messages in order.
51    pub fn logs(&self) -> &[String] {
52        &self.log
53    }
54
55    /// Clear the event log, leaving the KV store intact.
56    pub fn clear_logs(&mut self) {
57        self.log.clear();
58    }
59
60    /// Clear both the KV store and the event log.
61    pub fn clear(&mut self) {
62        self.store.clear();
63        self.log.clear();
64    }
65}
66
67impl Default for Ctx {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    // --- KV store ---
78
79    #[test]
80    fn set_then_get() {
81        let mut ctx = Ctx::new();
82        ctx.set("key", "value");
83        assert_eq!(ctx.get("key"), Some("value"));
84    }
85
86    #[test]
87    fn get_missing_key() {
88        let ctx = Ctx::new();
89        assert_eq!(ctx.get("nope"), None);
90    }
91
92    #[test]
93    fn set_overwrites() {
94        let mut ctx = Ctx::new();
95        ctx.set("key", "first");
96        ctx.set("key", "second");
97        assert_eq!(ctx.get("key"), Some("second"));
98    }
99
100    #[test]
101    fn remove_returns_value() {
102        let mut ctx = Ctx::new();
103        ctx.set("key", "value");
104        assert_eq!(ctx.remove("key"), Some("value".to_string()));
105        assert_eq!(ctx.get("key"), None);
106    }
107
108    #[test]
109    fn remove_missing_key() {
110        let mut ctx = Ctx::new();
111        assert_eq!(ctx.remove("nope"), None);
112    }
113
114    // --- Logging ---
115
116    #[test]
117    fn log_appends_and_logs_returns_in_order() {
118        let mut ctx = Ctx::new();
119        ctx.log("first");
120        ctx.log("second");
121        ctx.log("third");
122        assert_eq!(ctx.logs(), &["first", "second", "third"]);
123    }
124
125    #[test]
126    fn clear_logs_preserves_store() {
127        let mut ctx = Ctx::new();
128        ctx.set("key", "value");
129        ctx.log("msg");
130        ctx.clear_logs();
131        assert!(ctx.logs().is_empty());
132        assert_eq!(ctx.get("key"), Some("value"));
133    }
134
135    #[test]
136    fn clear_empties_both() {
137        let mut ctx = Ctx::new();
138        ctx.set("key", "value");
139        ctx.log("msg");
140        ctx.clear();
141        assert!(ctx.logs().is_empty());
142        assert_eq!(ctx.get("key"), None);
143    }
144
145    #[test]
146    fn separate_contexts_have_independent_state() {
147        let mut a = Ctx::new();
148        let b = Ctx::new();
149
150        a.set("topic", "rust");
151        a.log("phase 1 done");
152
153        assert_eq!(a.get("topic"), Some("rust"));
154        assert_eq!(b.get("topic"), None);
155        assert_eq!(a.logs(), &["phase 1 done"]);
156        assert!(b.logs().is_empty());
157    }
158}