Skip to main content

funera_core/
env_actor.rs

1#[cfg(feature = "tool")]
2use std::sync::Arc;
3
4use async_openai::config::OpenAIConfig;
5use tokio::sync::{broadcast, mpsc, oneshot};
6
7#[cfg(feature = "tool")]
8use crate::event_bus::tool_bus::ToolBus;
9#[cfg(feature = "skill")]
10use crate::re_act::skills::Skill;
11#[cfg(feature = "tool")]
12use crate::re_act::tool::Tool;
13#[cfg(feature = "tool")]
14use crate::re_act::tool_executor::ToolExecutor;
15#[cfg(feature = "security")]
16use crate::security::audit::{AuditBus, AuditEvent};
17#[cfg(feature = "security")]
18use crate::security::policy::ToolPolicy;
19#[cfg(feature = "sandbox")]
20use crate::security::sandbox::SandboxPolicy;
21
22use crate::env::{FuneraEnv, FuneraEnvWatcher};
23use crate::event_bus::env_state_bus::EnvStateEvent;
24
25// ═══════════════════════════════════════════════════════════════
26// Config structs — bundle params to keep fn arg count ≤ 7
27// ═══════════════════════════════════════════════════════════════
28
29/// Bundle of tool-system resources for the EnvActor.
30///
31/// Fields are feature-gated internally; the struct always exists so call
32/// sites can pass `Option<EnvToolConfig>` without any `#[cfg]` on the
33/// function signature.
34pub struct EnvToolConfig {
35    #[cfg(feature = "tool")]
36    pub tool_bus: ToolBus,
37    #[cfg(feature = "tool")]
38    pub exec_rx: mpsc::Receiver<crate::event_bus::tool_bus::ToolExecCommand>,
39}
40
41/// Bundle of security resources for the EnvActor.
42pub struct EnvSecurityConfig {
43    #[cfg(feature = "sandbox")]
44    pub sandbox_policy: SandboxPolicy,
45    #[cfg(feature = "security")]
46    pub tool_policy: ToolPolicy,
47    #[cfg(feature = "security")]
48    pub audit_bus: AuditBus,
49}
50
51// ═══════════════════════════════════════════════════════════════
52// ReActConfig — bundled handles the ReAct loop needs each call
53// ═══════════════════════════════════════════════════════════════
54
55pub struct ReActConfig {
56    pub env_watcher: FuneraEnvWatcher,
57    #[cfg(feature = "tool")]
58    pub tool_bus: ToolBus,
59    pub max_iterations: usize,
60    pub channel_buffer: usize,
61}
62
63// ═══════════════════════════════════════════════════════════════
64// EnvCmd — commands sent to the EnvActor via mpsc
65// ═══════════════════════════════════════════════════════════════
66
67pub enum EnvCmd {
68    // ── Mutation (fire-and-forget) ────────────────────────────
69    SetModel(String),
70    SetClient(async_openai::Client<OpenAIConfig>),
71    #[cfg(feature = "tool")]
72    AddTool(Arc<dyn Tool>),
73    #[cfg(feature = "tool")]
74    RemoveTool(String),
75    #[cfg(feature = "tool")]
76    SetToolAvailability {
77        name: String,
78        available: bool,
79    },
80    #[cfg(feature = "skill")]
81    AddSkill(Skill),
82    #[cfg(feature = "skill")]
83    RemoveSkill(String),
84    #[cfg(feature = "skill")]
85    ActivateSkill {
86        name: String,
87        respond: oneshot::Sender<bool>,
88    },
89    #[cfg(feature = "skill")]
90    DeactivateSkill {
91        name: String,
92        respond: oneshot::Sender<bool>,
93    },
94    #[cfg(feature = "skill")]
95    SetSkillPrompt(String),
96
97    // ── Query (oneshot response) ──────────────────────────────
98    #[cfg(feature = "skill")]
99    GetSkillPrompt {
100        respond: oneshot::Sender<String>,
101    },
102    SubscribeEnvState {
103        respond: oneshot::Sender<broadcast::Receiver<EnvStateEvent>>,
104    },
105    GetReActConfig {
106        respond: oneshot::Sender<ReActConfig>,
107    },
108    GetModel {
109        respond: oneshot::Sender<String>,
110    },
111    #[cfg(feature = "tool")]
112    GetToolNames {
113        respond: oneshot::Sender<Vec<String>>,
114    },
115    #[cfg(feature = "sandbox")]
116    GetSandboxPolicy {
117        respond: oneshot::Sender<SandboxPolicy>,
118    },
119    #[cfg(all(feature = "tool", feature = "security"))]
120    ApproveToolCall {
121        call_id: String,
122        approved: bool,
123        respond: oneshot::Sender<Result<(), String>>,
124    },
125    #[cfg(feature = "security")]
126    SubscribeAudit {
127        respond: oneshot::Sender<broadcast::Receiver<AuditEvent>>,
128    },
129}
130
131/// Spawn a long-running EnvActor that owns all environment state.
132///
133/// The actor is the **single source of truth** for all environment
134/// configuration and mutation. It:
135/// - Owns [`FuneraEnv`] (model, client, watch senders, registries)
136/// - Spawns and manages the [`ToolExecutor`] internally
137/// - Atomically broadcasts [`EnvStateEvent`] on every mutation
138/// - Provides read-snapshot queries via oneshot channels
139///
140/// When all [`EnvCmd`] senders are dropped, the actor and its
141/// ToolExecutor exit cleanly.
142#[cfg_attr(not(feature = "tool"), allow(unused_variables))]
143pub fn spawn_env_actor(
144    env: FuneraEnv,
145    env_watcher: FuneraEnvWatcher,
146    max_iterations: usize,
147    channel_buffer: usize,
148    tool: Option<EnvToolConfig>,
149    security: Option<EnvSecurityConfig>,
150) -> mpsc::UnboundedSender<EnvCmd> {
151    let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::<EnvCmd>();
152    let (state_tx, _) = broadcast::channel::<EnvStateEvent>(32);
153
154    let mut env = env;
155
156    // ── Spawn ToolExecutor internally ─────────────────────────
157    #[cfg(feature = "tool")]
158    let tool_bus_for_react = {
159        if let Some(tc) = tool {
160            let reg = env.tool_registry.clone();
161            let tb = tc.tool_bus.clone();
162            tokio::spawn(async move {
163                ToolExecutor::new(reg, tc.exec_rx).run().await;
164            });
165            Some(tb)
166        } else {
167            None
168        }
169    };
170
171    // ── Extract security resources ────────────────────────────
172    #[cfg(feature = "sandbox")]
173    let sandbox_policy = {
174        let _s = &security;
175        _s.as_ref()
176            .map(|s| s.sandbox_policy.clone())
177            .unwrap_or_default()
178    };
179    #[cfg(feature = "security")]
180    let audit_bus = {
181        let _s = &security;
182        _s.as_ref().map(|s| s.audit_bus.clone()).unwrap_or_default()
183    };
184    #[cfg(not(any(feature = "sandbox", feature = "security")))]
185    let _ = &security;
186
187    tokio::spawn(async move {
188        // ── Broadcast initial state ─────────────────────────────
189        #[cfg(feature = "tool")]
190        if let Ok(guard) = env.tool_registry.try_read() {
191            for name in guard.get_all_tools().keys() {
192                let _ = state_tx.send(EnvStateEvent::ToolAdded(name.clone()));
193            }
194        }
195        #[cfg(feature = "skill")]
196        if let Ok(guard) = env.skill_registry.try_read() {
197            for name in guard.all_skills().keys() {
198                let _ = state_tx.send(EnvStateEvent::SkillAdded(name.clone()));
199            }
200        }
201
202        // ── Command loop ───────────────────────────────────────
203        while let Some(cmd) = cmd_rx.recv().await {
204            match cmd {
205                // ── Mutation ──────────────────────────────────
206                EnvCmd::SetModel(model) => {
207                    env.set_model(&model);
208                    let _ = state_tx.send(EnvStateEvent::LlmChanged(model));
209                }
210                EnvCmd::SetClient(client) => {
211                    env.set_client(client);
212                }
213                #[cfg(feature = "tool")]
214                EnvCmd::AddTool(tool) => {
215                    let name = tool.name().to_string();
216                    env.add_tool(tool).await;
217                    let _ = state_tx.send(EnvStateEvent::ToolAdded(name));
218                }
219                #[cfg(feature = "tool")]
220                EnvCmd::RemoveTool(name) => {
221                    env.remove_tool(&name).await;
222                    let _ = state_tx.send(EnvStateEvent::ToolRemoved(name));
223                }
224                #[cfg(feature = "tool")]
225                EnvCmd::SetToolAvailability { name, available } => {
226                    env.set_tool_availability(&name, available).await;
227                    let _ = state_tx.send(EnvStateEvent::ToolAvailability(name, available));
228                }
229                #[cfg(feature = "skill")]
230                EnvCmd::AddSkill(skill) => {
231                    let name = skill.name.clone();
232                    env.add_skill(skill).await;
233                    let _ = state_tx.send(EnvStateEvent::SkillAdded(name));
234                }
235                #[cfg(feature = "skill")]
236                EnvCmd::RemoveSkill(name) => {
237                    env.remove_skill(&name).await;
238                    let _ = state_tx.send(EnvStateEvent::SkillRemoved(name.clone()));
239                }
240                #[cfg(feature = "skill")]
241                EnvCmd::ActivateSkill { name, respond } => {
242                    let ok = env.activate_skill(&name).await;
243                    let _ = respond.send(ok);
244                    if ok {
245                        let _ = state_tx.send(EnvStateEvent::SkillActivated(name));
246                    }
247                }
248                #[cfg(feature = "skill")]
249                EnvCmd::DeactivateSkill { name, respond } => {
250                    let ok = env.deactivate_skill(&name).await;
251                    let _ = respond.send(ok);
252                    if ok {
253                        let _ = state_tx.send(EnvStateEvent::SkillDeactivated(name));
254                    }
255                }
256                #[cfg(feature = "skill")]
257                EnvCmd::SetSkillPrompt(prompt) => {
258                    env.set_skill_prompt(prompt);
259                }
260
261                // ── Query ─────────────────────────────────────
262                #[cfg(feature = "skill")]
263                EnvCmd::GetSkillPrompt { respond } => {
264                    let _ = respond.send(env.skill_prompt_now());
265                }
266                EnvCmd::SubscribeEnvState { respond } => {
267                    let _ = respond.send(state_tx.subscribe());
268                }
269                EnvCmd::GetReActConfig { respond } => {
270                    let _ = respond.send(ReActConfig {
271                        env_watcher: env_watcher.clone(),
272                        #[cfg(feature = "tool")]
273                        tool_bus: tool_bus_for_react
274                            .clone()
275                            .expect("tool_bus must be Some when tool feature is enabled"),
276                        max_iterations,
277                        channel_buffer,
278                    });
279                }
280                EnvCmd::GetModel { respond } => {
281                    let _ = respond.send(env.model().to_string());
282                }
283                #[cfg(feature = "tool")]
284                EnvCmd::GetToolNames { respond } => {
285                    let names = if let Ok(guard) = env.tool_registry.try_read() {
286                        guard.get_all_tools().keys().cloned().collect()
287                    } else {
288                        Vec::new()
289                    };
290                    let _ = respond.send(names);
291                }
292                #[cfg(feature = "sandbox")]
293                EnvCmd::GetSandboxPolicy { respond } => {
294                    let _ = respond.send(sandbox_policy.clone());
295                }
296                #[cfg(all(feature = "tool", feature = "security"))]
297                EnvCmd::ApproveToolCall {
298                    call_id,
299                    approved,
300                    respond,
301                } => {
302                    let result = env
303                        .tool_registry
304                        .read()
305                        .await
306                        .approve_tool_call(&call_id, approved);
307                    let _ = respond.send(result);
308                }
309                #[cfg(feature = "security")]
310                EnvCmd::SubscribeAudit { respond } => {
311                    let _ = respond.send(audit_bus.subscribe());
312                }
313            }
314        }
315
316        // ── Teardown ────────────────────────────────────────────
317        // All EnvCmd senders are gone: the runtime is being dropped. Run every
318        // registered disposer in reverse (LIFO) order so no effect (tool
319        // registration, listener, connection) outlives the env.
320        env.dispose();
321    });
322
323    cmd_tx
324}