Skip to main content

incurs_codemode/
dispatch.rs

1use std::collections::{BTreeMap, BTreeSet};
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    available: BTreeMap<String, BTreeSet<String>>,
44    sequence: AtomicU64,
45}
46
47impl DispatchSession {
48    /// Resolves connector descriptions and creates a fresh replay cursor.
49    pub async fn new(
50        runtime: Arc<CodeModeRuntime>,
51        execution_id: impl Into<String>,
52        connectors: Vec<Arc<dyn Connector>>,
53    ) -> Result<Self, String> {
54        let execution_id = execution_id.into();
55        let mut descriptions = Vec::new();
56        for connector in &connectors {
57            descriptions.push(connector.describe().await?);
58        }
59        Self::new_with_descriptions(runtime, execution_id, connectors, descriptions).await
60    }
61
62    /// Creates a replay cursor using a previously captured capability snapshot.
63    pub async fn new_with_descriptions(
64        runtime: Arc<CodeModeRuntime>,
65        execution_id: impl Into<String>,
66        connectors: Vec<Arc<dyn Connector>>,
67        snapshot: Vec<ConnectorDescription>,
68    ) -> Result<Self, String> {
69        let execution_id = execution_id.into();
70        Self::new_with_descriptions_and_context(
71            runtime,
72            ToolContext {
73                execution_id,
74                control: Default::default(),
75                request: None,
76            },
77            connectors,
78            snapshot,
79        )
80        .await
81    }
82
83    /// Creates a replay cursor with an execution-scoped connector context.
84    pub async fn new_with_descriptions_and_context(
85        runtime: Arc<CodeModeRuntime>,
86        context: ToolContext,
87        connectors: Vec<Arc<dyn Connector>>,
88        snapshot: Vec<ConnectorDescription>,
89    ) -> Result<Self, String> {
90        let mut resolved = BTreeMap::new();
91        let mut available = BTreeMap::new();
92        for connector in connectors {
93            let description = connector.describe().await?;
94            available.insert(
95                description.name.clone(),
96                description
97                    .tools
98                    .iter()
99                    .map(|tool| tool.name.clone())
100                    .collect(),
101            );
102            if resolved
103                .insert(description.name.clone(), connector)
104                .is_some()
105            {
106                return Err(format!("Duplicate connector name \"{}\"", description.name));
107            }
108        }
109        let descriptions = snapshot
110            .into_iter()
111            .map(|description| (description.name.clone(), description))
112            .collect();
113        Ok(Self {
114            runtime,
115            execution_id: context.execution_id.clone(),
116            context,
117            connectors: resolved,
118            descriptions,
119            available,
120            sequence: AtomicU64::new(0),
121        })
122    }
123
124    /// Returns descriptions used to generate the child Worker bindings.
125    pub fn descriptions(&self) -> Vec<ConnectorDescription> {
126        self.descriptions.values().cloned().collect()
127    }
128
129    /// Executes or replays one connector call under the runtime policy.
130    pub async fn call(
131        &self,
132        connector: &str,
133        method: &str,
134        arguments: Value,
135        now: u64,
136    ) -> DispatchResponse {
137        let seq = self.sequence.fetch_add(1, Ordering::Relaxed);
138        self.call_at(seq, connector, method, arguments, now).await
139    }
140
141    /// Executes or replays one connector call at an explicit sandbox sequence.
142    pub async fn call_at(
143        &self,
144        seq: u64,
145        connector: &str,
146        method: &str,
147        arguments: Value,
148        now: u64,
149    ) -> DispatchResponse {
150        match self
151            .call_inner(seq, connector, method, arguments, now)
152            .await
153        {
154            Ok(response) => response,
155            Err(error) => DispatchResponse {
156                result: None,
157                control: Some("error".to_string()),
158                message: Some(error),
159            },
160        }
161    }
162
163    /// Begins a deterministic local step.
164    pub async fn begin_step(&self, name: &str, now: u64) -> Result<StepResponse, RuntimeError> {
165        let seq = self.sequence.fetch_add(1, Ordering::Relaxed);
166        self.begin_step_at(seq, name, now).await
167    }
168
169    /// Begins a deterministic local step at an explicit sandbox sequence.
170    pub async fn begin_step_at(
171        &self,
172        seq: u64,
173        name: &str,
174        now: u64,
175    ) -> Result<StepResponse, RuntimeError> {
176        Ok(
177            match self
178                .runtime
179                .decide(
180                    &self.execution_id,
181                    seq,
182                    "__step",
183                    name,
184                    Value::Null,
185                    false,
186                    false,
187                    now,
188                )
189                .await?
190            {
191                ToolDecision::Replay(result) => StepResponse {
192                    kind: "replay".to_string(),
193                    seq,
194                    result: Some(result),
195                },
196                ToolDecision::Execute(seq) => StepResponse {
197                    kind: "execute".to_string(),
198                    seq,
199                    result: None,
200                },
201                ToolDecision::Pause(seq) => StepResponse {
202                    kind: "pause".to_string(),
203                    seq,
204                    result: None,
205                },
206            },
207        )
208    }
209
210    /// Records a local step result for replay.
211    pub async fn record_step(&self, seq: u64, result: Value, now: u64) -> Result<(), RuntimeError> {
212        self.runtime
213            .record_result(&self.execution_id, seq, result, now)
214            .await
215    }
216
217    /// Notifies connectors that the current sandbox pass ended.
218    pub async fn pass_ended(&self, status: &str) {
219        for connector in self.connectors.values() {
220            connector.pass_ended(&self.execution_id, status).await;
221        }
222    }
223
224    /// Notifies connectors that the execution reached a terminal state.
225    pub async fn execution_ended(&self, status: &str) {
226        for connector in self.connectors.values() {
227            connector.execution_ended(&self.execution_id, status).await;
228        }
229    }
230
231    async fn call_inner(
232        &self,
233        seq: u64,
234        connector_name: &str,
235        method: &str,
236        arguments: Value,
237        now: u64,
238    ) -> Result<DispatchResponse, String> {
239        if connector_name == "codemode" {
240            return self.call_builtin(seq, method, arguments, now).await;
241        }
242        let connector = self
243            .connectors
244            .get(connector_name)
245            .ok_or_else(|| capability_unavailable(connector_name, method))?;
246        let tool = self
247            .descriptions
248            .get(connector_name)
249            .and_then(|description| description.tools.iter().find(|tool| tool.name == method))
250            .ok_or_else(|| format!("Tool \"{method}\" not found on {connector_name}"))?;
251        if !self
252            .available
253            .get(connector_name)
254            .is_some_and(|tools| tools.contains(method))
255        {
256            return Err(capability_unavailable(connector_name, method));
257        }
258        match self
259            .runtime
260            .decide(
261                &self.execution_id,
262                seq,
263                connector_name,
264                method,
265                arguments.clone(),
266                tool.policy.requires_approval,
267                tool.policy.replay == ReplayPolicy::Reexecute,
268                now,
269            )
270            .await
271            .map_err(|error| error.to_string())?
272        {
273            ToolDecision::Replay(result) => Ok(DispatchResponse {
274                result: Some(result),
275                control: None,
276                message: None,
277            }),
278            ToolDecision::Pause(_) => Ok(DispatchResponse {
279                result: None,
280                control: Some("pause".to_string()),
281                message: None,
282            }),
283            ToolDecision::Execute(seq) => {
284                let result = connector.execute(method, arguments, &self.context).await?;
285                self.runtime
286                    .record_result(&self.execution_id, seq, result.clone(), now)
287                    .await
288                    .map_err(|error| error.to_string())?;
289                Ok(DispatchResponse {
290                    result: Some(result),
291                    control: None,
292                    message: None,
293                })
294            }
295        }
296    }
297
298    async fn call_builtin(
299        &self,
300        seq: u64,
301        method: &str,
302        arguments: Value,
303        now: u64,
304    ) -> Result<DispatchResponse, String> {
305        match self
306            .runtime
307            .decide(
308                &self.execution_id,
309                seq,
310                "codemode",
311                method,
312                arguments.clone(),
313                false,
314                true,
315                now,
316            )
317            .await
318            .map_err(|error| error.to_string())?
319        {
320            ToolDecision::Pause(_) => Ok(DispatchResponse {
321                result: None,
322                control: Some("pause".to_string()),
323                message: None,
324            }),
325            ToolDecision::Replay(result) => Ok(DispatchResponse {
326                result: Some(result),
327                control: None,
328                message: None,
329            }),
330            ToolDecision::Execute(seq) => {
331                let connectors = self.descriptions();
332                let snippets = self
333                    .runtime
334                    .snippets()
335                    .await
336                    .map_err(|error| error.to_string())?;
337                let value = match method {
338                    "search" => serde_json::to_value(search(
339                        arguments
340                            .get("query")
341                            .and_then(Value::as_str)
342                            .unwrap_or_default(),
343                        &connectors,
344                        &snippets,
345                    )),
346                    "describe" => serde_json::to_value(describe(
347                        arguments
348                            .get("target")
349                            .and_then(Value::as_str)
350                            .unwrap_or_default(),
351                        &connectors,
352                        &snippets,
353                    )),
354                    _ => return Err(format!("Tool \"{method}\" not found on codemode")),
355                }
356                .map_err(|error| error.to_string())?;
357                self.runtime
358                    .record_result(&self.execution_id, seq, value.clone(), now)
359                    .await
360                    .map_err(|error| error.to_string())?;
361                Ok(DispatchResponse {
362                    result: Some(value),
363                    control: None,
364                    message: None,
365                })
366            }
367        }
368    }
369}
370
371fn capability_unavailable(connector: &str, method: &str) -> String {
372    serde_json::json!({
373        "code": "CAPABILITY_UNAVAILABLE",
374        "message": format!(
375            "Capability {connector}.{method} existed when the execution began but is unavailable"
376        ),
377        "connector": connector,
378        "method": method,
379    })
380    .to_string()
381}