atman_runtime/tools/
memory_stubs.rs1use 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}