Skip to main content

incurs_codemode/
dispatch.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::{
9    CodeModeRuntime, Connector, ConnectorDescription, ReplayPolicy, RuntimeError, ToolContext,
10    ToolDecision, describe, search,
11};
12
13/// Wire-safe result of one connector dispatch.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DispatchResponse {
16    /// Successful connector or replay result.
17    pub result: Option<Value>,
18    /// Control signal consumed by the sandbox proxy.
19    #[serde(rename = "__codemode_control__")]
20    pub control: Option<String>,
21    /// Host-side connector or runtime error.
22    pub message: Option<String>,
23}
24
25/// Wire-safe decision for a local `codemode.step` callback.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct StepResponse {
28    /// Replay, execute, or pause.
29    pub kind: String,
30    /// Host-assigned sequence.
31    pub seq: u64,
32    /// Previously recorded step result.
33    pub result: Option<Value>,
34}
35
36/// One replay pass's sequenced bridge between a sandbox and Rust connectors.
37pub struct DispatchSession {
38    runtime: Arc<CodeModeRuntime>,
39    execution_id: String,
40    context: ToolContext,
41    connectors: BTreeMap<String, Arc<dyn Connector>>,
42    descriptions: BTreeMap<String, ConnectorDescription>,
43    /// Descriptions resolved so far, filled in as namespaces are first used.
44    described: tokio::sync::Mutex<BTreeMap<String, ConnectorDescription>>,
45    sequence: AtomicU64,
46}
47
48impl DispatchSession {
49    /// Resolves connector descriptions and creates a fresh replay cursor.
50    pub async fn new(
51        runtime: Arc<CodeModeRuntime>,
52        execution_id: impl Into<String>,
53        connectors: Vec<Arc<dyn Connector>>,
54    ) -> Result<Self, String> {
55        let execution_id = execution_id.into();
56        let mut descriptions = Vec::new();
57        for connector in &connectors {
58            descriptions.push(connector.describe().await?);
59        }
60        Self::new_with_descriptions(runtime, execution_id, connectors, descriptions).await
61    }
62
63    /// Creates a replay cursor using a previously captured capability snapshot.
64    pub async fn new_with_descriptions(
65        runtime: Arc<CodeModeRuntime>,
66        execution_id: impl Into<String>,
67        connectors: Vec<Arc<dyn Connector>>,
68        snapshot: Vec<ConnectorDescription>,
69    ) -> Result<Self, String> {
70        let execution_id = execution_id.into();
71        Self::new_with_descriptions_and_context(
72            runtime,
73            ToolContext {
74                execution_id,
75                control: Default::default(),
76                request: None,
77            },
78            connectors,
79            snapshot,
80        )
81        .await
82    }
83
84    /// Creates a replay cursor with an execution-scoped connector context.
85    pub async fn new_with_descriptions_and_context(
86        runtime: Arc<CodeModeRuntime>,
87        context: ToolContext,
88        connectors: Vec<Arc<dyn Connector>>,
89        snapshot: Vec<ConnectorDescription>,
90    ) -> Result<Self, String> {
91        // Namespaces are registered by name alone. Describing every connector here
92        // meant contacting every configured server before any program ran, so one
93        // slow or dead server delayed or failed executions that never mentioned it.
94        let mut resolved = BTreeMap::new();
95        for connector in connectors {
96            let name = connector.name().to_string();
97            if resolved.insert(name.clone(), connector).is_some() {
98                return Err(format!("Duplicate connector name \"{name}\""));
99            }
100        }
101        let descriptions = snapshot
102            .into_iter()
103            .map(|description| (description.name.clone(), description))
104            .collect();
105        Ok(Self {
106            runtime,
107            execution_id: context.execution_id.clone(),
108            context,
109            connectors: resolved,
110            descriptions,
111            described: tokio::sync::Mutex::new(BTreeMap::new()),
112            sequence: AtomicU64::new(0),
113        })
114    }
115
116    /// Returns descriptions used to generate the child Worker bindings.
117    pub fn descriptions(&self) -> Vec<ConnectorDescription> {
118        self.descriptions.values().cloned().collect()
119    }
120
121    /// Returns one connector's description, contacting it at most once per pass.
122    ///
123    /// This is the whole of the laziness. A namespace the program never calls is
124    /// never described, so a server that is slow, unreachable or removed costs
125    /// nothing until something actually uses it, and cannot fail an execution that
126    /// does not touch it.
127    async fn describe_once(&self, name: &str) -> Result<ConnectorDescription, String> {
128        // The cache lock is scoped so it is never held across `describe`. A
129        // connector may reach back into this session -- a nested apoc invocation
130        // does exactly that -- and holding the lock across that await would
131        // deadlock the outer program against its own child.
132        {
133            let cache = self.described.lock().await;
134            if let Some(description) = cache.get(name) {
135                return Ok(description.clone());
136            }
137        }
138        let connector = self
139            .connectors
140            .get(name)
141            .ok_or_else(|| capability_unavailable(name, ""))?;
142        let description = connector.describe().await?;
143        self.described
144            .lock()
145            .await
146            .insert(name.to_string(), description.clone());
147        Ok(description)
148    }
149
150    /// Describes every connector, for the callers that genuinely need all of them.
151    ///
152    /// Only the in-sandbox `codemode.search` and `codemode.describe` use this: a
153    /// program asking what exists is asking to enumerate, so it pays for the
154    /// enumeration. Nothing on the ordinary dispatch path calls it.
155    async fn described_all(&self) -> Result<Vec<ConnectorDescription>, String> {
156        let names: Vec<String> = self.connectors.keys().cloned().collect();
157        let mut descriptions = Vec::with_capacity(names.len());
158        for name in names {
159            descriptions.push(self.describe_once(&name).await?);
160        }
161        Ok(descriptions)
162    }
163
164    /// Executes or replays one connector call under the runtime policy.
165    pub async fn call(
166        &self,
167        connector: &str,
168        method: &str,
169        arguments: Value,
170        now: u64,
171    ) -> DispatchResponse {
172        let seq = self.sequence.fetch_add(1, Ordering::Relaxed);
173        self.call_at(seq, connector, method, arguments, now).await
174    }
175
176    /// Executes or replays one connector call at an explicit sandbox sequence.
177    pub async fn call_at(
178        &self,
179        seq: u64,
180        connector: &str,
181        method: &str,
182        arguments: Value,
183        now: u64,
184    ) -> DispatchResponse {
185        match self
186            .call_inner(seq, connector, method, arguments, now)
187            .await
188        {
189            Ok(response) => response,
190            Err(error) => DispatchResponse {
191                result: None,
192                control: Some("error".to_string()),
193                message: Some(error),
194            },
195        }
196    }
197
198    /// Begins a deterministic local step.
199    pub async fn begin_step(&self, name: &str, now: u64) -> Result<StepResponse, RuntimeError> {
200        let seq = self.sequence.fetch_add(1, Ordering::Relaxed);
201        self.begin_step_at(seq, name, now).await
202    }
203
204    /// Begins a deterministic local step at an explicit sandbox sequence.
205    pub async fn begin_step_at(
206        &self,
207        seq: u64,
208        name: &str,
209        now: u64,
210    ) -> Result<StepResponse, RuntimeError> {
211        Ok(
212            match self
213                .runtime
214                .decide(
215                    &self.execution_id,
216                    seq,
217                    "__step",
218                    name,
219                    Value::Null,
220                    false,
221                    false,
222                    now,
223                )
224                .await?
225            {
226                ToolDecision::Replay(result) => StepResponse {
227                    kind: "replay".to_string(),
228                    seq,
229                    result: Some(result),
230                },
231                ToolDecision::Execute(seq) => StepResponse {
232                    kind: "execute".to_string(),
233                    seq,
234                    result: None,
235                },
236                ToolDecision::Pause(seq) => StepResponse {
237                    kind: "pause".to_string(),
238                    seq,
239                    result: None,
240                },
241            },
242        )
243    }
244
245    /// Records a local step result for replay.
246    pub async fn record_step(&self, seq: u64, result: Value, now: u64) -> Result<(), RuntimeError> {
247        self.runtime
248            .record_result(&self.execution_id, seq, result, now)
249            .await
250    }
251
252    /// Notifies connectors that the current sandbox pass ended.
253    pub async fn pass_ended(&self, status: &str) {
254        for connector in self.connectors.values() {
255            connector.pass_ended(&self.execution_id, status).await;
256        }
257    }
258
259    /// Notifies connectors that the execution reached a terminal state.
260    pub async fn execution_ended(&self, status: &str) {
261        for connector in self.connectors.values() {
262            connector.execution_ended(&self.execution_id, status).await;
263        }
264    }
265
266    async fn call_inner(
267        &self,
268        seq: u64,
269        connector_name: &str,
270        method: &str,
271        arguments: Value,
272        now: u64,
273    ) -> Result<DispatchResponse, String> {
274        if connector_name == "codemode" {
275            return self.call_builtin(seq, method, arguments, now).await;
276        }
277        let connector = self
278            .connectors
279            .get(connector_name)
280            .ok_or_else(|| capability_unavailable(connector_name, method))?;
281        let description = self.describe_once(connector_name).await?;
282        let tool = description
283            .tools
284            .iter()
285            .find(|tool| tool.name == method)
286            .ok_or_else(|| format!("Tool \"{method}\" not found on {connector_name}"))?;
287        match self
288            .runtime
289            .decide(
290                &self.execution_id,
291                seq,
292                connector_name,
293                method,
294                arguments.clone(),
295                tool.policy.requires_approval,
296                tool.policy.replay == ReplayPolicy::Reexecute,
297                now,
298            )
299            .await
300            .map_err(|error| error.to_string())?
301        {
302            ToolDecision::Replay(result) => Ok(DispatchResponse {
303                result: Some(result),
304                control: None,
305                message: None,
306            }),
307            ToolDecision::Pause(_) => Ok(DispatchResponse {
308                result: None,
309                control: Some("pause".to_string()),
310                message: None,
311            }),
312            ToolDecision::Execute(seq) => {
313                let result = connector.execute(method, arguments, &self.context).await?;
314                self.runtime
315                    .record_result(&self.execution_id, seq, result.clone(), now)
316                    .await
317                    .map_err(|error| error.to_string())?;
318                Ok(DispatchResponse {
319                    result: Some(result),
320                    control: None,
321                    message: None,
322                })
323            }
324        }
325    }
326
327    async fn call_builtin(
328        &self,
329        seq: u64,
330        method: &str,
331        arguments: Value,
332        now: u64,
333    ) -> Result<DispatchResponse, String> {
334        match self
335            .runtime
336            .decide(
337                &self.execution_id,
338                seq,
339                "codemode",
340                method,
341                arguments.clone(),
342                false,
343                true,
344                now,
345            )
346            .await
347            .map_err(|error| error.to_string())?
348        {
349            ToolDecision::Pause(_) => Ok(DispatchResponse {
350                result: None,
351                control: Some("pause".to_string()),
352                message: None,
353            }),
354            ToolDecision::Replay(result) => Ok(DispatchResponse {
355                result: Some(result),
356                control: None,
357                message: None,
358            }),
359            ToolDecision::Execute(seq) => {
360                let connectors = self.described_all().await?;
361                let snippets = self
362                    .runtime
363                    .snippets()
364                    .await
365                    .map_err(|error| error.to_string())?;
366                let value = match method {
367                    "search" => serde_json::to_value(search(
368                        arguments
369                            .get("query")
370                            .and_then(Value::as_str)
371                            .unwrap_or_default(),
372                        &connectors,
373                        &snippets,
374                    )),
375                    "describe" => serde_json::to_value(describe(
376                        arguments
377                            .get("target")
378                            .and_then(Value::as_str)
379                            .unwrap_or_default(),
380                        &connectors,
381                        &snippets,
382                    )),
383                    _ => return Err(format!("Tool \"{method}\" not found on codemode")),
384                }
385                .map_err(|error| error.to_string())?;
386                self.runtime
387                    .record_result(&self.execution_id, seq, value.clone(), now)
388                    .await
389                    .map_err(|error| error.to_string())?;
390                Ok(DispatchResponse {
391                    result: Some(value),
392                    control: None,
393                    message: None,
394                })
395            }
396        }
397    }
398}
399
400fn capability_unavailable(connector: &str, method: &str) -> String {
401    serde_json::json!({
402        "code": "CAPABILITY_UNAVAILABLE",
403        "message": format!(
404            "Capability {connector}.{method} existed when the execution began but is unavailable"
405        ),
406        "connector": connector,
407        "method": method,
408    })
409    .to_string()
410}