Skip to main content

monoloop_loop/transaction/
loop_adapters.rs

1//! ToolRegistry / ToolRuntime adapters that delegate to TransactionToolDispatcher.
2
3use super::dispatcher::{DispatchOutcome, DispatchRequest, TransactionToolDispatcher};
4use crate::registry::{
5    ResolveToolRequest, ToolDescriptorRef, ToolRegistry, ToolRegistryError, ToolResolution,
6};
7use crate::tools::{
8    StartToolExecution, ToolExecutionHandle, ToolRuntime, ToolRuntimeError, ToolRuntimeTerminal,
9};
10use monoloop_contracts::{
11    ExchangeId, OutboundToolOutcome, ToolActionId, ToolName, ToolUnavailableReason,
12};
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16use tokio::sync::oneshot;
17
18type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
19
20/// Registry backed by the transaction-resolved allowlist.
21pub struct ResolvedToolRegistry {
22    tools: super::resolved_tools::ResolvedToolSet,
23}
24
25impl ResolvedToolRegistry {
26    /// Construct from the admitted resolved set.
27    pub fn new(tools: super::resolved_tools::ResolvedToolSet) -> Self {
28        Self { tools }
29    }
30}
31
32impl ToolRegistry for ResolvedToolRegistry {
33    fn resolve<'a>(
34        &'a self,
35        request: ResolveToolRequest,
36    ) -> BoxFuture<'a, Result<ToolResolution, ToolRegistryError>> {
37        Box::pin(async move {
38            let name = match ToolName::try_new(&request.tool_name) {
39                Ok(n) => n,
40                Err(_) => {
41                    return Ok(ToolResolution::Unavailable(ToolUnavailableReason::NotFound));
42                }
43            };
44            if self.tools.contains_name(&name) {
45                Ok(ToolResolution::Available(ToolDescriptorRef {
46                    name: request.tool_name,
47                }))
48            } else if self.tools.is_empty() {
49                Ok(ToolResolution::Unavailable(
50                    ToolUnavailableReason::NoRegisteredTool,
51                ))
52            } else {
53                Ok(ToolResolution::Unavailable(ToolUnavailableReason::NotFound))
54            }
55        })
56    }
57}
58
59/// Runtime that starts linked tools through the shared dispatcher.
60pub struct HostToolRuntime {
61    dispatcher: Arc<TransactionToolDispatcher>,
62    exchange_id: ExchangeId,
63}
64
65impl HostToolRuntime {
66    /// Construct for one transaction / exchange scope.
67    pub fn new(dispatcher: Arc<TransactionToolDispatcher>, exchange_id: ExchangeId) -> Self {
68        Self {
69            dispatcher,
70            exchange_id,
71        }
72    }
73}
74
75impl ToolRuntime for HostToolRuntime {
76    fn start(&self, request: StartToolExecution) -> Result<ToolExecutionHandle, ToolRuntimeError> {
77        let name = ToolName::try_new(&request.tool_name)
78            .map_err(|_| ToolRuntimeError("invalid tool name".into()))?;
79        let action_id = ToolActionId::new(request.tool_action_id.clone());
80        let dispatcher = Arc::clone(&self.dispatcher);
81        let exchange_id = self.exchange_id;
82        let payload = request.request_payload;
83        let provider_id = request.execution_id.as_str().to_string();
84        let ordinal = request.request_generation as u32;
85        let (tx, rx) = oneshot::channel();
86        tokio::spawn(async move {
87            let outcome = dispatcher
88                .dispatch(DispatchRequest {
89                    exchange_id,
90                    tool_action_id: action_id,
91                    tool_name: name,
92                    provider_tool_call_id: provider_id,
93                    request_ordinal: ordinal,
94                    arguments_json: payload,
95                })
96                .await;
97            let _ = tx.send(map_outcome(outcome));
98        });
99
100        Ok(ToolExecutionHandle {
101            execution_id: request.execution_id,
102            completion: Some(rx),
103        })
104    }
105}
106
107fn map_outcome(outcome: DispatchOutcome) -> ToolRuntimeTerminal {
108    match outcome {
109        DispatchOutcome::Canonical { result, .. } => {
110            let payload = serde_json::to_string(&result.outcome)
111                .unwrap_or_else(|_| "{\"error\":\"encode\"}".into());
112            // Success and domain failure are both valid tool results for the model.
113            ToolRuntimeTerminal {
114                outcome: OutboundToolOutcome::Success,
115                payload,
116            }
117        }
118        DispatchOutcome::Rejected { code, message, .. } => ToolRuntimeTerminal {
119            outcome: OutboundToolOutcome::DispatchRejected,
120            payload: format!("{code}:{message}"),
121        },
122        DispatchOutcome::RuntimeFailed { code, .. } => ToolRuntimeTerminal {
123            outcome: OutboundToolOutcome::ExecutionFailed,
124            payload: code,
125        },
126    }
127}
128
129/// Dispatch one ready tool directly through the transaction dispatcher.
130pub async fn dispatch_ready_tool(
131    dispatcher: &Arc<TransactionToolDispatcher>,
132    exchange_id: ExchangeId,
133    tool_action_id: ToolActionId,
134    tool_name: &str,
135    provider_tool_call_id: &str,
136    request_ordinal: u32,
137    arguments_json: &str,
138) -> DispatchOutcome {
139    dispatch_ready_tool_cancellable(
140        dispatcher,
141        exchange_id,
142        tool_action_id,
143        tool_name,
144        provider_tool_call_id,
145        request_ordinal,
146        arguments_json,
147        None,
148    )
149    .await
150}
151
152/// Dispatch with an actor-owned cancel signal so mid-dispatch cancel joins the worker (D-028).
153#[allow(clippy::too_many_arguments)]
154pub async fn dispatch_ready_tool_cancellable(
155    dispatcher: &Arc<TransactionToolDispatcher>,
156    exchange_id: ExchangeId,
157    tool_action_id: ToolActionId,
158    tool_name: &str,
159    provider_tool_call_id: &str,
160    request_ordinal: u32,
161    arguments_json: &str,
162    cancel: Option<std::sync::Arc<tokio::sync::Notify>>,
163) -> DispatchOutcome {
164    let name = match ToolName::try_new(tool_name) {
165        Ok(n) => n,
166        Err(_) => {
167            return DispatchOutcome::Rejected {
168                tool_action_id,
169                code: "invalid_tool_name",
170                message: "tool name failed identity validation".into(),
171                lifecycle: vec![],
172            };
173        }
174    };
175    dispatcher
176        .dispatch_with_cancel(
177            DispatchRequest {
178                exchange_id,
179                tool_action_id,
180                tool_name: name,
181                provider_tool_call_id: provider_tool_call_id.into(),
182                request_ordinal,
183                arguments_json: arguments_json.into(),
184            },
185            cancel,
186        )
187        .await
188}