Skip to main content

wm_tools/expansion/
imagination.rs

1//! Imagination engine tools — scenario generation, prediction, and reflection.
2//!
3//! Gana::ThreeStars — "Imagination, scenario planning, counterfactual reflection"
4//!
5//! Tools:
6//! - `imagine.scenario` — Generate scenarios for a given state + goal
7//! - `imagine.predict` — Predict outcome of a specific action
8//! - `imagine.reflect` — Counterfactual reflection on past decisions
9
10#![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
25// ── WorldModel factory ────────────────────────────────────────────────
26
27/// Build a WorldModel from env-configured LLM handlers, falling back to stubs.
28fn build_world_model() -> WorldModel {
29    world_model_from_env()
30}
31
32// ── imagine.scenario ──────────────────────────────────────────────────
33
34/// Imagination scenario tool — generates scenarios for a given state + goal.
35///
36/// Uses the bicameral world model to imagine multiple possible actions,
37/// predict their outcomes, and evaluate them. Optionally enriches with
38/// simulation data (MC rollout, forecasting, sensitivity analysis).
39pub struct ImagineScenarioTool {
40    store: Arc<MemoryStore>,
41    stats: ToolStats,
42    effects: EffectRow,
43}
44
45impl ImagineScenarioTool {
46    /// Create a new imagination scenario tool.
47    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                    // model_exclude memories never enter scenario context.
64                    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        // Gather memory context
122        let memory_context = self.gather_context(goal, scan_limit);
123
124        // Build scenario engine and generate scenarios
125        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        // Optionally enrich with simulation data
140        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        // Select best scenario
174        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
191// ── imagine.predict ───────────────────────────────────────────────────
192
193/// Imagination predict tool — predicts the outcome of a specific action.
194///
195/// Uses the bicameral world model to predict what would happen if a
196/// specific action is taken from a given state.
197pub struct ImaginePredictTool {
198    stats: ToolStats,
199    effects: EffectRow,
200}
201
202impl ImaginePredictTool {
203    /// Create a new imagination predict tool.
204    #[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
293// ── imagine.reflect ───────────────────────────────────────────────────
294
295/// Imagination reflect tool — counterfactual reflection on past decisions.
296///
297/// Given a past state, the action taken, and an alternative action,
298/// predicts what would have happened with the alternative.
299pub struct ImagineReflectTool {
300    stats: ToolStats,
301    effects: EffectRow,
302}
303
304impl ImagineReflectTool {
305    /// Create a new imagination reflect tool.
306    #[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
392// ── Registration ──────────────────────────────────────────────────────
393
394/// Register imagination tools into a registry.
395pub 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// ── Tests ─────────────────────────────────────────────────────────────
406
407#[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(&registry, &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}