1#![forbid(unsafe_code)]
11#![allow(clippy::significant_drop_tightening)]
12
13use async_trait::async_trait;
14
15use serde_json::{Value, json};
16use std::sync::Arc;
17use wm_bicameral::{
18 ScenarioEngine, ScenarioEvaluator, WorldModel,
19 simulation_bridge::{SimulationBridge, SimulationBridgeConfig},
20 world_model_from_env,
21};
22use wm_core::{Context, EffectRow, Gana, Tool, ToolStats};
23use wm_memory::MemoryStore;
24
25fn build_world_model() -> WorldModel {
29 world_model_from_env()
30}
31
32pub struct ImagineScenarioTool {
40 store: Arc<MemoryStore>,
41 stats: ToolStats,
42 effects: EffectRow,
43}
44
45impl ImagineScenarioTool {
46 pub fn new(store: Arc<MemoryStore>) -> Self {
48 Self {
49 store,
50 stats: ToolStats::default(),
51 effects: EffectRow::read_only(vec![wm_core::Resource::Galaxy("universal".into())]),
52 }
53 }
54
55 fn gather_context(&self, query: &str, limit: usize) -> String {
56 let topic_lower = query.to_lowercase();
57 let topic_words: Vec<&str> = topic_lower.split_whitespace().collect();
58
59 let mut context_parts: Vec<String> = Vec::new();
60 for galaxy in wm_core::Galaxy::memory_galaxies() {
61 if let Ok(mems) = self.store.scan(galaxy, limit) {
62 for mem in mems {
63 if mem.metadata.model_exclude {
65 continue;
66 }
67 let content_lower = mem.content.to_lowercase();
68 if topic_words.iter().any(|w| content_lower.contains(w)) {
69 context_parts.push(format!("- {}", mem.content));
70 if context_parts.len() >= 20 {
71 break;
72 }
73 }
74 }
75 }
76 if context_parts.len() >= 20 {
77 break;
78 }
79 }
80 context_parts.join("\n")
81 }
82}
83
84#[async_trait]
85impl Tool for ImagineScenarioTool {
86 fn name(&self) -> &str {
87 "imagine.scenario"
88 }
89 fn gana(&self) -> Gana {
90 Gana::ThreeStars
91 }
92 fn effects(&self) -> &EffectRow {
93 &self.effects
94 }
95 fn description(&self) -> &str {
96 "[Experimental] Generate and evaluate scenarios for a given state and goal using the bicameral imagination engine"
97 }
98 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
99 let state = args
100 .get("state")
101 .and_then(Value::as_str)
102 .ok_or_else(|| wm_core::CoreError::InvalidArgs("state (string) required".into()))?;
103
104 let goal = args
105 .get("goal")
106 .and_then(Value::as_str)
107 .ok_or_else(|| wm_core::CoreError::InvalidArgs("goal (string) required".into()))?;
108
109 let scan_limit = args
110 .get("scan_limit")
111 .and_then(Value::as_u64)
112 .unwrap_or(200) as usize;
113
114 let enrich_sim = args
115 .get("enrich_simulation")
116 .and_then(Value::as_bool)
117 .unwrap_or(false);
118
119 let mc_samples = args.get("mc_samples").and_then(Value::as_u64).unwrap_or(10) as usize;
120
121 let memory_context = self.gather_context(goal, scan_limit);
123
124 let world_model = build_world_model();
126 let scenario_engine =
127 ScenarioEngine::with_defaults(world_model, ScenarioEvaluator::with_defaults());
128 let scenarios = scenario_engine.imagine(state, goal, &memory_context);
129
130 if scenarios.is_empty() {
131 return Ok(json!({
132 "status": "no_scenarios",
133 "state": state,
134 "goal": goal,
135 "scenarios": [],
136 }));
137 }
138
139 let scenarios_json: Vec<Value> = if enrich_sim {
141 let world_model = build_world_model();
142 let bridge_config = SimulationBridgeConfig {
143 mc_samples,
144 sensitivity_samples: 50,
145 cf_bootstrap: 50,
146 ..Default::default()
147 };
148 let mut bridge = SimulationBridge::new(bridge_config);
149 scenarios
150 .iter()
151 .map(|s| {
152 let history: Vec<f64> = vec![f64::from(s.score), f64::from(s.score)];
153 let enriched = bridge.enrich_scenario(&world_model, s, &history);
154 enriched.to_json()
155 })
156 .collect()
157 } else {
158 scenarios
159 .iter()
160 .map(|s| {
161 json!({
162 "action": s.action,
163 "score": s.score,
164 "risk": s.risk,
165 "novelty": s.novelty,
166 "rationale": s.rationale,
167 "trajectory_steps": s.trajectory.len(),
168 })
169 })
170 .collect()
171 };
172
173 let best = scenario_engine.select_balanced(&scenarios, 0.05);
175
176 Ok(json!({
177 "status": "ok",
178 "state": state,
179 "goal": goal,
180 "scenario_count": scenarios.len(),
181 "best_action": best.map(|s| s.action.clone()),
182 "best_score": best.map(|s| s.score),
183 "scenarios": scenarios_json,
184 }))
185 }
186 fn stats(&self) -> &ToolStats {
187 &self.stats
188 }
189}
190
191pub struct ImaginePredictTool {
198 stats: ToolStats,
199 effects: EffectRow,
200}
201
202impl ImaginePredictTool {
203 #[must_use]
205 pub fn new() -> Self {
206 Self {
207 stats: ToolStats::default(),
208 effects: EffectRow::read_only(vec![wm_core::Resource::Galaxy("universal".into())]),
209 }
210 }
211}
212
213impl Default for ImaginePredictTool {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219#[async_trait]
220impl Tool for ImaginePredictTool {
221 fn name(&self) -> &str {
222 "imagine.predict"
223 }
224 fn gana(&self) -> Gana {
225 Gana::ThreeStars
226 }
227 fn effects(&self) -> &EffectRow {
228 &self.effects
229 }
230 fn description(&self) -> &str {
231 "[Experimental] Predict the outcome of a specific action from a given state using the bicameral world model"
232 }
233 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
234 let state = args
235 .get("state")
236 .and_then(Value::as_str)
237 .ok_or_else(|| wm_core::CoreError::InvalidArgs("state (string) required".into()))?;
238
239 let action = args
240 .get("action")
241 .and_then(Value::as_str)
242 .ok_or_else(|| wm_core::CoreError::InvalidArgs("action (string) required".into()))?;
243
244 let goal = args
245 .get("goal")
246 .and_then(Value::as_str)
247 .ok_or_else(|| wm_core::CoreError::InvalidArgs("goal (string) required".into()))?;
248
249 let world_model = build_world_model();
250 let prediction = world_model.predict(state, action, goal);
251
252 let best = prediction.best();
253
254 let mut alternatives: Vec<Value> = Vec::new();
255 if prediction.left.description != best.description {
256 alternatives.push(json!({
257 "description": prediction.left.description,
258 "confidence": prediction.left.confidence,
259 "source": "left",
260 }));
261 }
262 if let Some(ref right) = prediction.right {
263 if right.description != best.description {
264 alternatives.push(json!({
265 "description": right.description,
266 "confidence": right.confidence,
267 "source": "right",
268 }));
269 }
270 }
271
272 Ok(json!({
273 "status": "ok",
274 "state": state,
275 "action": action,
276 "goal": goal,
277 "best_prediction": {
278 "description": best.description,
279 "confidence": best.confidence,
280 "changes": best.changes,
281 "risks": best.risks,
282 "goal_progress": best.goal_progress,
283 },
284 "alternatives": alternatives,
285 "has_consensus": prediction.has_consensus(),
286 }))
287 }
288 fn stats(&self) -> &ToolStats {
289 &self.stats
290 }
291}
292
293pub struct ImagineReflectTool {
300 stats: ToolStats,
301 effects: EffectRow,
302}
303
304impl ImagineReflectTool {
305 #[must_use]
307 pub fn new() -> Self {
308 Self {
309 stats: ToolStats::default(),
310 effects: EffectRow::read_only(vec![wm_core::Resource::Galaxy("universal".into())]),
311 }
312 }
313}
314
315impl Default for ImagineReflectTool {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321#[async_trait]
322impl Tool for ImagineReflectTool {
323 fn name(&self) -> &str {
324 "imagine.reflect"
325 }
326 fn gana(&self) -> Gana {
327 Gana::ThreeStars
328 }
329 fn effects(&self) -> &EffectRow {
330 &self.effects
331 }
332 fn description(&self) -> &str {
333 "[Experimental] Counterfactual reflection: compare actual action outcome vs alternative action using the bicameral world model"
334 }
335 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
336 let past_state = args
337 .get("past_state")
338 .and_then(Value::as_str)
339 .ok_or_else(|| {
340 wm_core::CoreError::InvalidArgs("past_state (string) required".into())
341 })?;
342
343 let actual_action = args
344 .get("actual_action")
345 .and_then(Value::as_str)
346 .ok_or_else(|| {
347 wm_core::CoreError::InvalidArgs("actual_action (string) required".into())
348 })?;
349
350 let alternative_action = args
351 .get("alternative_action")
352 .and_then(Value::as_str)
353 .ok_or_else(|| {
354 wm_core::CoreError::InvalidArgs("alternative_action (string) required".into())
355 })?;
356
357 let goal = args
358 .get("goal")
359 .and_then(Value::as_str)
360 .unwrap_or("improve outcome");
361
362 let world_model = build_world_model();
363 let scenario_engine =
364 ScenarioEngine::with_defaults(world_model, ScenarioEvaluator::with_defaults());
365 let reflection =
366 scenario_engine.reflect(past_state, actual_action, alternative_action, goal);
367
368 Ok(json!({
369 "status": "ok",
370 "past_state": past_state,
371 "actual_action": actual_action,
372 "alternative_action": alternative_action,
373 "actual_outcome": {
374 "description": reflection.actual_prediction.description,
375 "confidence": reflection.actual_prediction.confidence,
376 "goal_progress": reflection.actual_prediction.goal_progress,
377 },
378 "counterfactual_outcome": {
379 "description": reflection.counterfactual_prediction.description,
380 "confidence": reflection.counterfactual_prediction.confidence,
381 "goal_progress": reflection.counterfactual_prediction.goal_progress,
382 },
383 "would_have_been_better": reflection.would_have_been_better,
384 "lesson": reflection.lesson,
385 }))
386 }
387 fn stats(&self) -> &ToolStats {
388 &self.stats
389 }
390}
391
392pub fn register_imagination(
396 registry: &wm_dispatch::ToolRegistry,
397 store: &Arc<MemoryStore>,
398) -> wm_dispatch::ToolRegistry {
399 registry
400 .register(Arc::new(ImagineScenarioTool::new(store.clone())))
401 .register(Arc::new(ImaginePredictTool::new()))
402 .register(Arc::new(ImagineReflectTool::new()))
403}
404
405#[cfg(test)]
408mod tests {
409 use super::*;
410
411 fn make_store() -> Arc<MemoryStore> {
412 let dir = tempfile::tempdir().unwrap();
413 Arc::new(MemoryStore::open(dir.path(), 1024 * 1024).unwrap())
414 }
415
416 #[tokio::test]
417 async fn imagine_scenario_tool_name() {
418 let store = make_store();
419 let tool = ImagineScenarioTool::new(store);
420 assert_eq!(tool.name(), "imagine.scenario");
421 assert_eq!(tool.gana(), Gana::ThreeStars);
422 }
423
424 #[tokio::test]
425 async fn imagine_predict_tool_name() {
426 let tool = ImaginePredictTool::new();
427 assert_eq!(tool.name(), "imagine.predict");
428 assert_eq!(tool.gana(), Gana::ThreeStars);
429 }
430
431 #[tokio::test]
432 async fn imagine_reflect_tool_name() {
433 let tool = ImagineReflectTool::new();
434 assert_eq!(tool.name(), "imagine.reflect");
435 assert_eq!(tool.gana(), Gana::ThreeStars);
436 }
437
438 #[tokio::test]
439 async fn imagine_scenario_generates_scenarios() {
440 let store = make_store();
441 let tool = ImagineScenarioTool::new(store);
442 let mut ctx = Context::default();
443 let result = tool
444 .call(
445 &mut ctx,
446 json!({
447 "state": "system is slow",
448 "goal": "improve performance",
449 }),
450 )
451 .await;
452 assert!(result.is_ok());
453 let val = result.unwrap();
454 assert_eq!(val["status"], "ok");
455 assert!(val["scenario_count"].as_u64().is_some());
456 }
457
458 #[tokio::test]
459 async fn imagine_scenario_missing_state() {
460 let store = make_store();
461 let tool = ImagineScenarioTool::new(store);
462 let mut ctx = Context::default();
463 let result = tool.call(&mut ctx, json!({"goal": "test"})).await;
464 assert!(result.is_err());
465 }
466
467 #[tokio::test]
468 async fn imagine_scenario_missing_goal() {
469 let store = make_store();
470 let tool = ImagineScenarioTool::new(store);
471 let mut ctx = Context::default();
472 let result = tool.call(&mut ctx, json!({"state": "test"})).await;
473 assert!(result.is_err());
474 }
475
476 #[tokio::test]
477 async fn imagine_predict_returns_prediction() {
478 let tool = ImaginePredictTool::new();
479 let mut ctx = Context::default();
480 let result = tool
481 .call(
482 &mut ctx,
483 json!({
484 "state": "idle system",
485 "action": "run optimization",
486 "goal": "improve speed",
487 }),
488 )
489 .await;
490 assert!(result.is_ok());
491 let val = result.unwrap();
492 assert_eq!(val["status"], "ok");
493 assert!(val["best_prediction"]["description"].as_str().is_some());
494 }
495
496 #[tokio::test]
497 async fn imagine_predict_missing_action() {
498 let tool = ImaginePredictTool::new();
499 let mut ctx = Context::default();
500 let result = tool
501 .call(&mut ctx, json!({"state": "test", "goal": "test"}))
502 .await;
503 assert!(result.is_err());
504 }
505
506 #[tokio::test]
507 async fn imagine_reflect_returns_reflection() {
508 let tool = ImagineReflectTool::new();
509 let mut ctx = Context::default();
510 let result = tool
511 .call(
512 &mut ctx,
513 json!({
514 "past_state": "system running",
515 "actual_action": "did nothing",
516 "alternative_action": "optimized cache",
517 "goal": "improve speed",
518 }),
519 )
520 .await;
521 assert!(result.is_ok());
522 let val = result.unwrap();
523 assert_eq!(val["status"], "ok");
524 assert!(val["actual_outcome"]["description"].as_str().is_some());
525 assert!(
526 val["counterfactual_outcome"]["description"]
527 .as_str()
528 .is_some()
529 );
530 }
531
532 #[tokio::test]
533 async fn imagine_reflect_missing_alternative() {
534 let tool = ImagineReflectTool::new();
535 let mut ctx = Context::default();
536 let result = tool
537 .call(
538 &mut ctx,
539 json!({
540 "past_state": "test",
541 "actual_action": "test",
542 }),
543 )
544 .await;
545 assert!(result.is_err());
546 }
547
548 #[tokio::test]
549 async fn register_imagination_registers_three_tools() {
550 let store = make_store();
551 let registry = wm_dispatch::ToolRegistry::new();
552 let registry = register_imagination(®istry, &store);
553 assert!(registry.get("imagine.scenario").is_some());
554 assert!(registry.get("imagine.predict").is_some());
555 assert!(registry.get("imagine.reflect").is_some());
556 }
557}