wm_tools/expansion/
tasks.rs1#![forbid(unsafe_code)]
4
5use async_trait::async_trait;
6
7use serde_json::{Value, json};
8use std::sync::Arc;
9use wm_core::{Context, EffectRow, Galaxy, Gana, Resource, Tool, ToolStats};
10use wm_memory::{Memory, MemoryStore};
11
12pub struct TaskDistributeTool {
13 store: Arc<MemoryStore>,
14 stats: ToolStats,
15 effects: EffectRow,
16}
17
18impl TaskDistributeTool {
19 pub fn new(store: Arc<MemoryStore>) -> Self {
20 Self {
21 store,
22 stats: ToolStats::default(),
23 effects: EffectRow {
24 writes: vec![Resource::Galaxy("substrate".into())],
25 ..Default::default()
26 },
27 }
28 }
29}
30
31#[async_trait]
32impl Tool for TaskDistributeTool {
33 fn name(&self) -> &str {
34 "task.distribute"
35 }
36 fn gana(&self) -> Gana {
37 Gana::TurtleBeak
38 }
39 fn effects(&self) -> &EffectRow {
40 &self.effects
41 }
42 fn input_schema(&self) -> Value {
43 super::common::schema(
44 &json!({
45 "task": super::common::str_prop("Non-empty task description to record"),
46 "agent_id": super::common::str_prop("Requested assignee label; not a verified or contacted agent"),
47 }),
48 &["task"],
49 )
50 }
51 fn description(&self) -> &str {
52 "Record a task assignment memo; does not contact, verify, or execute an agent"
53 }
54 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
55 let task = args
56 .get("task")
57 .and_then(|v| v.as_str())
58 .map(str::trim)
59 .filter(|value| !value.is_empty())
60 .ok_or_else(|| {
61 wm_core::CoreError::InvalidArgs("Non-empty 'task' parameter required".into())
62 })?;
63 let agent_id = args
64 .get("agent_id")
65 .and_then(|v| v.as_str())
66 .map(str::trim)
67 .filter(|value| !value.is_empty())
68 .unwrap_or("any");
69 let mut mem = Memory::new(
70 Galaxy::Substrate,
71 json!({
72 "type": "task",
73 "task": task,
74 "agent_id": agent_id,
75 "status": "recorded",
76 })
77 .to_string(),
78 );
79 mem.metadata.tags = vec!["task".into(), "recorded".into()];
80 mem.metadata.importance = 0.7;
81 self.store.put(Galaxy::Substrate, &mem)?;
82 Ok(json!({
83 "status": "success",
84 "task_id": mem.metadata.id,
85 "task": task,
86 "agent_id": agent_id,
87 "assignment_state": "recorded_only",
88 "agent_verified": false,
89 "agent_contacted": false,
90 "execution_started": false,
91 }))
92 }
93 fn stats(&self) -> &ToolStats {
94 &self.stats
95 }
96}
97
98pub struct TaskStatusTool {
100 store: Arc<MemoryStore>,
101 stats: ToolStats,
102 effects: EffectRow,
103}
104
105impl TaskStatusTool {
106 pub fn new(store: Arc<MemoryStore>) -> Self {
107 Self {
108 store,
109 stats: ToolStats::default(),
110 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
111 }
112 }
113}
114
115#[async_trait]
116impl Tool for TaskStatusTool {
117 fn name(&self) -> &str {
118 "task.status"
119 }
120 fn gana(&self) -> Gana {
121 Gana::TurtleBeak
122 }
123 fn effects(&self) -> &EffectRow {
124 &self.effects
125 }
126 fn input_schema(&self) -> Value {
127 super::common::schema(
128 &json!({
129 "task_id": super::common::str_prop("Optional exact task memo UUID"),
130 }),
131 &[],
132 )
133 }
134 fn description(&self) -> &str {
135 "List recorded task assignment memos; does not report agent execution state"
136 }
137 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
138 let task_id = args
139 .get("task_id")
140 .and_then(|v| v.as_str())
141 .map(uuid::Uuid::parse_str)
142 .transpose()
143 .map_err(|error| {
144 wm_core::CoreError::InvalidArgs(format!("Invalid task_id UUID: {error}"))
145 })?;
146 let memories = self.store.scan(Galaxy::Substrate, 500)?;
147 let tasks: Vec<Value> = memories
148 .iter()
149 .filter(|m| m.metadata.tags.contains(&"task".to_string()))
150 .filter(|m| task_id.is_none_or(|id| m.metadata.id == id))
151 .map(|m| {
152 json!({
153 "id": m.metadata.id,
154 "content": m.content,
155 "tags": m.metadata.tags,
156 })
157 })
158 .collect();
159 Ok(json!({
160 "status": "success",
161 "count": tasks.len(),
162 "tasks": tasks,
163 }))
164 }
165 fn stats(&self) -> &ToolStats {
166 &self.stats
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn open_store() -> (tempfile::TempDir, Arc<MemoryStore>) {
175 let tmp = tempfile::tempdir().unwrap();
176 let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
177 (tmp, store)
178 }
179
180 #[tokio::test]
181 async fn distribute_records_memo_without_claiming_agent_execution() {
182 let (_tmp, store) = open_store();
183 let result = TaskDistributeTool::new(store.clone())
184 .call(
185 &mut Context::default(),
186 json!({"task": "inspect invented fixture", "agent_id": "agent-example"}),
187 )
188 .await
189 .unwrap();
190 assert_eq!(result["assignment_state"], "recorded_only");
191 assert_eq!(result["agent_verified"], false);
192 assert_eq!(result["agent_contacted"], false);
193 assert_eq!(result["execution_started"], false);
194
195 let saved = store.scan(Galaxy::Substrate, 10).unwrap();
196 assert_eq!(saved.len(), 1);
197 let body: Value = serde_json::from_str(&saved[0].content).unwrap();
198 assert_eq!(body["status"], "recorded");
199 }
200
201 #[tokio::test]
202 async fn distribute_rejects_missing_or_blank_task() {
203 let (_tmp, store) = open_store();
204 let tool = TaskDistributeTool::new(store);
205 for args in [json!({}), json!({"task": " "})] {
206 let error = tool.call(&mut Context::default(), args).await.unwrap_err();
207 assert!(error.to_string().contains("Non-empty 'task'"));
208 }
209 }
210
211 #[tokio::test]
212 async fn status_reads_recorded_task_memos() {
213 let (_tmp, store) = open_store();
214 let created = TaskDistributeTool::new(store.clone())
215 .call(&mut Context::default(), json!({"task": "bounded audit"}))
216 .await
217 .unwrap();
218 let result = TaskStatusTool::new(store)
219 .call(
220 &mut Context::default(),
221 json!({"task_id": created["task_id"]}),
222 )
223 .await
224 .unwrap();
225 assert_eq!(result["count"], 1);
226 assert!(
227 result["tasks"][0]["content"]
228 .as_str()
229 .unwrap()
230 .contains("bounded audit")
231 );
232 }
233}