Skip to main content

atman_runtime/tools/
memory_stubs.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use tokio::sync::RwLock;
5
6use crate::error::RuntimeError;
7use crate::tool::{BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
8use crate::value::Value;
9
10use crate::migration::MigratedRule;
11
12#[derive(Default, Clone)]
13pub struct FetchRule {
14    entries: Arc<RwLock<HashMap<String, String>>>,
15    migrated: Arc<RwLock<Vec<MigratedRule>>>,
16}
17
18impl FetchRule {
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    pub async fn insert(&self, name: impl Into<String>, content: impl Into<String>) {
24        self.entries
25            .write()
26            .await
27            .insert(name.into(), content.into());
28    }
29
30    pub async fn set_migrated(&self, rules: Vec<MigratedRule>) {
31        *self.migrated.write().await = rules;
32    }
33
34    pub async fn migrated_count(&self) -> usize {
35        self.migrated.read().await.len()
36    }
37}
38
39impl Tool for FetchRule {
40    fn name(&self) -> &str {
41        "fetch_rule"
42    }
43
44    fn tier(&self) -> Tier {
45        Tier::Zero
46    }
47
48    fn call<'a>(&'a self, args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
49        Box::pin(async move {
50            let name = extract_string(&args, "name", 0)?;
51            let entries = self.entries.read().await;
52            if let Some(content) = entries.get(&name) {
53                return Ok(Value::Str(content.clone()));
54            }
55            drop(entries);
56            let migrated = self.migrated.read().await;
57            if let Some(rule) = crate::migration::resolve_by_name(&migrated, &name) {
58                return Ok(Value::Str(rule.content.clone()));
59            }
60            Ok(Value::Str(String::new()))
61        })
62    }
63}
64
65pub struct FetchConfessions;
66
67impl Tool for FetchConfessions {
68    fn name(&self) -> &str {
69        "fetch_confessions"
70    }
71
72    fn tier(&self) -> Tier {
73        Tier::Zero
74    }
75
76    fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
77        Box::pin(async move { Ok(Value::List(Vec::new())) })
78    }
79}
80
81fn extract_string(args: &ToolArgs, name: &str, pos: usize) -> Result<String, RuntimeError> {
82    let value = match args.named(name) {
83        Some(v) => v,
84        None => args.positional(pos)?,
85    };
86    match value {
87        Value::Str(s) => Ok(s.clone()),
88        other => Err(RuntimeError::TypeMismatch {
89            expected: "string".into(),
90            actual: other.kind_name().into(),
91        }),
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[tokio::test]
100    async fn fetch_rule_returns_stored_content() {
101        let tool = FetchRule::new();
102        tool.insert("code-review", "review carefully").await;
103        let out = tool
104            .call(
105                ToolArgs {
106                    positional: vec![Value::Str("code-review".into())],
107                    named: vec![],
108                },
109                &ToolCtx::new(),
110            )
111            .await
112            .unwrap();
113        assert!(matches!(out, Value::Str(s) if s == "review carefully"));
114    }
115
116    #[tokio::test]
117    async fn fetch_rule_missing_returns_empty_string() {
118        let tool = FetchRule::new();
119        let out = tool
120            .call(
121                ToolArgs {
122                    positional: vec![Value::Str("missing".into())],
123                    named: vec![],
124                },
125                &ToolCtx::new(),
126            )
127            .await
128            .unwrap();
129        assert!(matches!(out, Value::Str(s) if s.is_empty()));
130    }
131
132    #[tokio::test]
133    async fn fetch_confessions_returns_empty_list() {
134        let out = FetchConfessions
135            .call(ToolArgs::default(), &ToolCtx::new())
136            .await
137            .unwrap();
138        assert!(matches!(out, Value::List(items) if items.is_empty()));
139    }
140}