monoloop_loop/transaction/
loop_adapters.rs1use super::dispatcher::{DispatchOutcome, DispatchRequest, TransactionToolDispatcher};
4use super::lifecycle::session_identity::provider_tool_call_id_from_action;
5use super::lifecycle::{TaskClass, TransactionTaskSpawner};
6use crate::registry::{
7 ResolveToolRequest, ToolDescriptorRef, ToolRegistry, ToolRegistryError, ToolResolution,
8};
9use crate::tools::{
10 StartToolExecution, ToolExecutionHandle, ToolRuntime, ToolRuntimeError, ToolRuntimeTerminal,
11};
12use monoloop_contracts::{
13 ExchangeId, OutboundToolOutcome, ToolActionId, ToolName, ToolUnavailableReason, TransactionId,
14};
15use std::future::Future;
16use std::pin::Pin;
17use std::sync::Arc;
18use tokio::sync::oneshot;
19
20type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
21
22pub struct ResolvedToolRegistry {
24 tools: super::resolved_tools::ResolvedToolSet,
25}
26
27impl ResolvedToolRegistry {
28 pub fn new(tools: super::resolved_tools::ResolvedToolSet) -> Self {
30 Self { tools }
31 }
32}
33
34impl ToolRegistry for ResolvedToolRegistry {
35 fn resolve<'a>(
36 &'a self,
37 request: ResolveToolRequest,
38 ) -> BoxFuture<'a, Result<ToolResolution, ToolRegistryError>> {
39 Box::pin(async move {
40 let name = match ToolName::try_new(&request.tool_name) {
41 Ok(n) => n,
42 Err(_) => {
43 return Ok(ToolResolution::Unavailable(ToolUnavailableReason::NotFound));
44 }
45 };
46 if self.tools.contains_name(&name) {
47 Ok(ToolResolution::Available(ToolDescriptorRef {
48 name: request.tool_name,
49 }))
50 } else if self.tools.is_empty() {
51 Ok(ToolResolution::Unavailable(
52 ToolUnavailableReason::NoRegisteredTool,
53 ))
54 } else {
55 Ok(ToolResolution::Unavailable(ToolUnavailableReason::NotFound))
56 }
57 })
58 }
59}
60
61pub struct HostToolRuntime {
63 dispatcher: Arc<TransactionToolDispatcher>,
64 exchange_id: ExchangeId,
65 spawner: TransactionTaskSpawner,
66 transaction_id: TransactionId,
67 transaction_deadline: std::time::Instant,
69}
70
71impl HostToolRuntime {
72 pub fn with_spawner(
74 dispatcher: Arc<TransactionToolDispatcher>,
75 exchange_id: ExchangeId,
76 transaction_id: TransactionId,
77 spawner: TransactionTaskSpawner,
78 transaction_deadline: std::time::Instant,
79 ) -> Self {
80 Self {
81 dispatcher,
82 exchange_id,
83 spawner,
84 transaction_id,
85 transaction_deadline,
86 }
87 }
88}
89
90impl ToolRuntime for HostToolRuntime {
91 fn start(&self, request: StartToolExecution) -> Result<ToolExecutionHandle, ToolRuntimeError> {
92 let name = ToolName::try_new(&request.tool_name)
93 .map_err(|_| ToolRuntimeError("invalid tool name".into()))?;
94 let action_id = ToolActionId::new(request.tool_action_id.clone());
95 let provider_id = provider_tool_call_id_from_action(self.exchange_id, &action_id)
97 .unwrap_or(action_id.as_str())
98 .to_string();
99 let dispatcher = Arc::clone(&self.dispatcher);
100 let exchange_id = self.exchange_id;
101 let payload = request.request_payload;
102 let ordinal = request.request_generation as u32;
103 let execution_id = request.execution_id.clone();
104 let transaction_deadline = self.transaction_deadline;
105 let (tx, rx) = oneshot::channel();
106 let work = async move {
107 let outcome = dispatcher
108 .dispatch(DispatchRequest {
109 exchange_id,
110 tool_action_id: action_id,
111 tool_name: name,
112 provider_tool_call_id: provider_id,
113 request_ordinal: ordinal,
114 arguments_json: payload,
115 transaction_deadline,
116 })
117 .await;
118 let _ = tx.send(map_outcome(outcome));
119 };
120
121 let class = TaskClass::ToolWorker(self.transaction_id, execution_id.clone());
122 self.spawner
123 .try_spawn_owned(class, work)
124 .map_err(|_| ToolRuntimeError("tool worker spawn capacity exceeded".into()))?;
125
126 Ok(ToolExecutionHandle {
127 execution_id: request.execution_id,
128 completion: Some(rx),
129 })
130 }
131}
132
133fn map_outcome(outcome: DispatchOutcome) -> ToolRuntimeTerminal {
134 match outcome {
135 DispatchOutcome::Canonical { result, .. } => {
136 let payload =
139 serde_json::to_string(&result).unwrap_or_else(|_| "{\"error\":\"encode\"}".into());
140 let outcome = match &result.outcome {
141 monoloop_contracts::CanonicalToolResultOutcome::Succeeded(_) => {
142 OutboundToolOutcome::Success
143 }
144 monoloop_contracts::CanonicalToolResultOutcome::DomainFailed(_) => {
145 OutboundToolOutcome::ExecutionFailed
146 }
147 };
148 ToolRuntimeTerminal { outcome, payload }
149 }
150 DispatchOutcome::Rejected { code, message, .. } => ToolRuntimeTerminal {
151 outcome: OutboundToolOutcome::DispatchRejected,
152 payload: format!("{code}:{message}"),
153 },
154 DispatchOutcome::RuntimeFailed { code, .. } => ToolRuntimeTerminal {
155 outcome: OutboundToolOutcome::ExecutionFailed,
156 payload: code,
157 },
158 }
159}
160
161pub async fn dispatch_ready_tool(
163 dispatcher: &Arc<TransactionToolDispatcher>,
164 exchange_id: ExchangeId,
165 tool_action_id: ToolActionId,
166 tool_name: &str,
167 provider_tool_call_id: &str,
168 request_ordinal: u32,
169 arguments_json: &str,
170) -> DispatchOutcome {
171 dispatch_ready_tool_cancellable(
172 dispatcher,
173 exchange_id,
174 tool_action_id,
175 tool_name,
176 provider_tool_call_id,
177 request_ordinal,
178 arguments_json,
179 None,
180 )
181 .await
182}
183
184#[allow(clippy::too_many_arguments)]
186pub async fn dispatch_ready_tool_cancellable(
187 dispatcher: &Arc<TransactionToolDispatcher>,
188 exchange_id: ExchangeId,
189 tool_action_id: ToolActionId,
190 tool_name: &str,
191 provider_tool_call_id: &str,
192 request_ordinal: u32,
193 arguments_json: &str,
194 cancel: Option<std::sync::Arc<super::sticky_cancel::StickyCancel>>,
195) -> DispatchOutcome {
196 let name = match ToolName::try_new(tool_name) {
197 Ok(n) => n,
198 Err(_) => {
199 return DispatchOutcome::Rejected {
200 tool_action_id,
201 code: "invalid_tool_name",
202 message: "tool name failed identity validation".into(),
203 lifecycle: vec![],
204 };
205 }
206 };
207 dispatcher
208 .dispatch_with_cancel(
209 DispatchRequest {
210 exchange_id,
211 tool_action_id,
212 tool_name: name,
213 provider_tool_call_id: provider_tool_call_id.into(),
214 request_ordinal,
215 arguments_json: arguments_json.into(),
216 transaction_deadline: std::time::Instant::now()
218 + std::time::Duration::from_secs(365 * 24 * 3600),
219 },
220 cancel,
221 )
222 .await
223}