Skip to main content

plexus_substrate/
builder.rs

1//! Plexus RPC builder - constructs a fully configured DynamicHub instance
2//!
3//! This module is used by both the main binary and examples.
4
5use std::sync::{Arc, Weak};
6
7use crate::activations::arbor::{Arbor, ArborConfig};
8use crate::activations::bash::Bash;
9use crate::activations::chaos::Chaos;
10use crate::activations::claudecode::{ClaudeCode, ClaudeCodeStorage, ClaudeCodeStorageConfig};
11use crate::activations::claudecode_loopback::{ClaudeCodeLoopback, LoopbackStorageConfig};
12use crate::activations::cone::{Cone, ConeStorageConfig};
13use crate::activations::echo::Echo;
14use crate::activations::health::Health;
15use crate::activations::interactive::Interactive;
16use crate::activations::lattice::{Lattice, LatticeStorageConfig};
17use crate::activations::changelog::{Changelog, ChangelogStorageConfig};
18use crate::activations::mustache::{Mustache, MustacheStorageConfig};
19use crate::activations::orcha::pm::{Pm, PmStorage, PmStorageConfig};
20use crate::activations::orcha::{GraphRuntime, Orcha, OrchaStorage, OrchaStorageConfig};
21use crate::activations::solar::Solar;
22use crate::plexus::DynamicHub;
23// use plexus_jsexec::{JsExec, JsExecConfig};  // temporarily disabled - needs API updates
24use registry::Registry;
25
26/// Build the Plexus RPC hub with registered activations
27///
28/// The hub implements the Plexus RPC protocol and provides introspection methods:
29/// - substrate.call: Route calls to registered activations
30/// - substrate.hash: Get configuration hash for cache invalidation
31/// - substrate.list_activations: Enumerate registered activations
32/// - substrate.schema: Get full Plexus RPC schema
33///
34/// Hub activations (with nested children) are registered with `register_hub`
35/// to enable direct nested routing like `substrate.solar.mercury.info`.
36///
37/// This function uses `Arc::new_cyclic` to inject a weak reference to the hub
38/// into Cone and ClaudeCode, enabling them to resolve foreign handles through
39/// the hub without creating reference cycles.
40///
41/// This function is async because Arbor, Cone, and ClaudeCode require
42/// async database initialization.
43pub async fn build_plexus_rpc() -> Arc<DynamicHub> {
44    // Initialize Arbor first (other activations depend on its storage)
45    // Use explicit type annotation for Weak<DynamicHub> parent context
46    let arbor: Arbor<Weak<DynamicHub>> = Arbor::with_context_type(ArborConfig::default())
47        .await
48        .expect("Failed to initialize Arbor");
49    let arbor_storage = arbor.storage();
50
51    // Initialize Cone with shared Arbor storage
52    // Use explicit type annotation for Weak<DynamicHub> parent context
53    let cone: Cone<Weak<DynamicHub>> = Cone::with_context_type(ConeStorageConfig::default(), arbor_storage.clone())
54        .await
55        .expect("Failed to initialize Cone");
56
57    // Initialize ClaudeCode with shared Arbor storage
58    // Use explicit type annotation for Weak<DynamicHub> parent context
59    let claudecode_storage = ClaudeCodeStorage::new(
60        ClaudeCodeStorageConfig::default(),
61        arbor_storage,
62    )
63    .await
64    .expect("Failed to initialize ClaudeCode storage");
65    let claudecode: ClaudeCode<Weak<DynamicHub>> = ClaudeCode::with_context_type(Arc::new(claudecode_storage));
66
67    // Initialize Mustache for template rendering
68    let mustache = Mustache::new(MustacheStorageConfig::default())
69        .await
70        .expect("Failed to initialize Mustache");
71
72    // Initialize ClaudeCode Loopback for tool permission routing
73    let loopback = Arc::new(
74        ClaudeCodeLoopback::new(LoopbackStorageConfig::default())
75            .await
76            .expect("Failed to initialize ClaudeCodeLoopback")
77    );
78
79    // Initialize Orcha storage for multi-agent orchestration
80    let orcha_storage = Arc::new(
81        OrchaStorage::new(OrchaStorageConfig::default())
82            .await
83            .expect("Failed to initialize Orcha storage")
84    );
85
86    // Initialize PM storage for ticket→node mapping
87    let pm_storage = Arc::new(
88        PmStorage::new(PmStorageConfig::default())
89            .await
90            .expect("Failed to initialize PM storage")
91    );
92
93    // Initialize Changelog for tracking plexus hash transitions
94    let changelog = Changelog::new(ChangelogStorageConfig::default())
95        .await
96        .expect("Failed to initialize Changelog");
97
98    // Clone arbor_storage for Orcha (needs separate reference)
99    let arbor_storage_for_orcha = arbor.storage();
100
101    // Initialize JsExec for JavaScript execution in V8 isolates
102    // let jsexec = JsExec::new(JsExecConfig::default());  // temporarily disabled
103
104    // Initialize Lattice DAG execution engine
105    let lattice = Lattice::new(LatticeStorageConfig::default())
106        .await
107        .expect("Failed to initialize Lattice storage");
108
109    // Initialize Registry for backend discovery
110    let registry = Registry::with_defaults()
111        .await
112        .expect("Failed to initialize Registry");
113
114    // Use Arc::new_cyclic to get a Weak<DynamicHub> during construction
115    // This allows us to inject the parent context into Cone and ClaudeCode
116    // before the hub is fully constructed, avoiding reference cycles
117    //
118    // We keep a clone of `orcha` outside the closure so we can call
119    // `recover_running_graphs` after the hub is fully assembled.
120    let orcha_for_recovery: std::cell::OnceCell<Orcha<Weak<DynamicHub>>> = std::cell::OnceCell::new();
121    let hub = Arc::new_cyclic(|weak_hub: &Weak<DynamicHub>| {
122        // Inject parent context into activations that need it
123        arbor.inject_parent(weak_hub.clone());
124        cone.inject_parent(weak_hub.clone());
125        claudecode.inject_parent(weak_hub.clone());
126
127        // Initialize Orcha with dependencies (needs to be inside closure to access claudecode)
128        let graph_runtime = Arc::new(GraphRuntime::new(lattice.storage()));
129        let pm = Arc::new(Pm::new(pm_storage.clone(), lattice.storage()));
130        let orcha: Orcha<Weak<DynamicHub>> = Orcha::new(
131            orcha_storage.clone(),
132            Arc::new(claudecode.clone()),
133            loopback.clone(),
134            arbor_storage_for_orcha,
135            graph_runtime,
136            pm,
137        );
138
139        // Store a clone for the post-construction recovery pass.
140        let _ = orcha_for_recovery.set(orcha.clone());
141
142        // Build and return the DynamicHub with "substrate" namespace
143        DynamicHub::new("substrate")
144            .register(Health::new())
145            .register(Echo::new())
146            .register(Bash::new())
147            .register(Chaos::new(lattice.storage()))
148            .register(arbor)
149            .register(cone)
150            .register(claudecode)
151            .register(mustache)
152            .register(changelog.clone())
153            .register((*loopback).clone())
154            .register_hub(orcha)
155            // .register(jsexec)  // temporarily disabled
156            .register(registry)
157            .register(lattice)
158            .register(Interactive::new())  // Bidirectional demo activation
159            .register_hub(Solar::new())
160    });
161
162    // Run changelog startup check
163    let plexus_hash = hub.compute_hash();
164    match changelog.startup_check(&plexus_hash).await {
165        Ok((hash_changed, is_documented, message)) => {
166            if hash_changed && !is_documented {
167                tracing::error!("{}", message);
168            } else if hash_changed {
169                tracing::info!("{}", message);
170            } else {
171                tracing::debug!("{}", message);
172            }
173        }
174        Err(e) => {
175            tracing::error!("Changelog startup check failed: {}", e);
176        }
177    }
178
179    // Run startup recovery for any Orcha graphs that were mid-execution when the
180    // substrate last shut down.  This is best-effort: failures are logged, never fatal.
181    if let Some(orcha) = orcha_for_recovery.into_inner() {
182        orcha.recover_running_graphs().await;
183    }
184
185    hub
186}