crabtalk_runtime/env.rs
1//! Env — trait for node-specific capabilities and tool dispatch.
2//!
3//! The runtime engine talks to a single `Env` implementation. The node
4//! crate provides [`NodeEnv`] which bundles event broadcasting,
5//! instruction discovery, and a composite Hook. Tests use `()`.
6
7use crate::Hook;
8use std::path::{Path, PathBuf};
9use tokio::sync::broadcast;
10use wcore::{AgentEvent, ToolDispatch, ToolFuture, protocol::message};
11
12/// The runtime environment — combines server capabilities with tool dispatch.
13///
14/// Each node/binary defines one implementation that wires together
15/// the composite hook, event broadcasting, CWD management, and
16/// instruction discovery.
17pub trait Env: Send + Sync + 'static {
18 /// The composite hook providing tool schemas, dispatch, and lifecycle.
19 type Hook: Hook;
20
21 /// Access the composite hook.
22 fn hook(&self) -> &Self::Hook;
23
24 /// Called when an agent event occurs. Default: no-op.
25 fn on_agent_event(&self, _agent: &str, _conversation_id: u64, _event: &AgentEvent) {}
26
27 /// Subscribe to agent events. Returns `None` if event broadcasting
28 /// is not supported.
29 fn subscribe_events(&self) -> Option<broadcast::Receiver<message::AgentEventMsg>> {
30 None
31 }
32
33 /// Collect layered instructions (e.g. `Crab.md` files) for the
34 /// given working directory.
35 fn discover_instructions(&self, _cwd: &Path) -> Option<String> {
36 None
37 }
38
39 /// Effective working directory for a conversation. Defaults to the
40 /// process CWD.
41 fn effective_cwd(&self, _conversation_id: u64) -> PathBuf {
42 std::env::current_dir().unwrap_or_default()
43 }
44}
45
46/// Dispatch a tool call through an Env's hook. Utility for Env
47/// implementors building their ToolDispatcher impl.
48pub fn dispatch_tool<'a, E: Env>(
49 env: &'a E,
50 name: &'a str,
51 args: &'a str,
52 agent: &'a str,
53 sender: &'a str,
54 conversation_id: Option<u64>,
55) -> ToolFuture<'a> {
56 let call = ToolDispatch {
57 args: args.to_owned(),
58 agent: agent.to_owned(),
59 sender: sender.to_owned(),
60 conversation_id,
61 };
62
63 match env.hook().dispatch(name, call) {
64 Some(fut) => fut,
65 None => Box::pin(async move { Err(format!("tool not registered: {name}")) }),
66 }
67}
68
69impl Env for () {
70 type Hook = ();
71
72 fn hook(&self) -> &() {
73 &()
74 }
75}