Skip to main content

entelix_runnable/
adapter.rs

1//! `ToolToRunnableAdapter` — bridges the `Tool` trait into the `Runnable`
2//! composition contract.
3//!
4//! `Tool` lives in `entelix-core` (DAG root) and does not extend `Runnable`.
5//! When a user wants to drop a tool into a `.pipe()` chain, they wrap it
6//! here:
7//!
8//! ```ignore
9//! use entelix_runnable::{RunnableExt, ToolToRunnableAdapter};
10//! let chain = prompt.pipe(model).pipe(ToolToRunnableAdapter::new(my_tool));
11//! ```
12//!
13//! The adapter forwards `invoke` to `Tool::execute`; metadata
14//! (`name`, `description`, `input_schema`) stays accessible via
15//! `inner()` for callers that need it.
16
17use std::sync::Arc;
18
19use entelix_core::tools::Tool;
20use entelix_core::{AgentContext, ExecutionContext, Result};
21
22use crate::runnable::Runnable;
23
24/// Wraps any `Tool` as `Runnable<serde_json::Value, serde_json::Value>`.
25///
26/// Cheap to clone (internal `Arc`).
27pub struct ToolToRunnableAdapter {
28    inner: Arc<dyn Tool>,
29}
30
31impl ToolToRunnableAdapter {
32    /// Wrap a concrete `Tool`.
33    pub fn new<T: Tool>(tool: T) -> Self {
34        Self {
35            inner: Arc::new(tool),
36        }
37    }
38
39    /// Wrap an already-shared `Arc<dyn Tool>` (e.g. one stored in a
40    /// `ToolRegistry`). Avoids a needless `Arc::new`.
41    pub fn from_arc(tool: Arc<dyn Tool>) -> Self {
42        Self { inner: tool }
43    }
44
45    /// Borrow the wrapped tool — useful for inspecting metadata.
46    pub fn inner(&self) -> &Arc<dyn Tool> {
47        &self.inner
48    }
49}
50
51impl Clone for ToolToRunnableAdapter {
52    fn clone(&self) -> Self {
53        Self {
54            inner: Arc::clone(&self.inner),
55        }
56    }
57}
58
59#[async_trait::async_trait]
60impl Runnable<serde_json::Value, serde_json::Value> for ToolToRunnableAdapter {
61    async fn invoke(
62        &self,
63        input: serde_json::Value,
64        ctx: &ExecutionContext,
65    ) -> Result<serde_json::Value> {
66        // Bridge `Runnable` (D-free, ExecutionContext) → `Tool`
67        // (typed `D`). Adapter-wrapped tools are always `Tool<()>`
68        // because composition does not carry typed deps; operators
69        // threading deps reach for the typed registry path
70        // (slice 103).
71        let agent_ctx = AgentContext::<()>::from(ctx.clone());
72        self.inner.execute(input, &agent_ctx).await
73    }
74
75    fn name(&self) -> std::borrow::Cow<'_, str> {
76        std::borrow::Cow::Borrowed(&self.inner.metadata().name)
77    }
78}