Skip to main content

soma_codemode/runner/
runtime.rs

1use serde_json::Value;
2
3use crate::protocol::{
4    CodeModeRunnerInput, CodeModeRunnerOutput, CodeModeRunnerResult, CODE_MODE_STACK_SIZE_LIMIT,
5    RUNNER_STATE,
6};
7use crate::runner::jail::reset_execution_jail;
8use crate::runner::js_args::{javy_type_error, json_arg, optional_string_arg, required_string_arg};
9use crate::runner::limits::MEMORY_LIMIT_BYTES;
10use crate::wrapper::code_mode_runner_script;
11
12pub enum RunnerLoopOutcome {
13    Completed,
14    InputClosed,
15}
16
17pub struct RunnerError {
18    pub kind: String,
19    pub message: String,
20}
21
22impl From<String> for RunnerError {
23    fn from(message: String) -> Self {
24        Self {
25            kind: "server_error".to_string(),
26            message,
27        }
28    }
29}
30
31pub fn run_next() -> Result<RunnerLoopOutcome, RunnerError> {
32    let input = match runner_read_input() {
33        Ok(input) => input,
34        Err(RunnerReadError::InputClosed) => return Ok(RunnerLoopOutcome::InputClosed),
35        Err(RunnerReadError::Other(message)) => return Err(message.into()),
36    };
37    let CodeModeRunnerInput::Start { code, proxy } = input else {
38        return Err("runner expected start message".to_string().into());
39    };
40    reset_execution_jail();
41    let runtime = build_runtime()?;
42    install_host_bridge(&runtime).map_err(|message| RunnerError {
43        kind: "server_error".to_string(),
44        message,
45    })?;
46    run_wrapped(runtime, &code, &proxy)?;
47    Ok(RunnerLoopOutcome::Completed)
48}
49
50pub fn run_start_without_io(input: CodeModeRunnerInput) -> Result<CodeModeRunnerOutput, String> {
51    let CodeModeRunnerInput::Start { code, proxy } = input else {
52        return Err("runner expected start message".to_string());
53    };
54    let runtime = build_runtime().map_err(|err| err.message)?;
55    install_no_host_bridge(&runtime)?;
56    match run_wrapped_to_result(runtime, &code, &proxy) {
57        Ok(result) => Ok(CodeModeRunnerOutput::Done {
58            result: CodeModeRunnerResult::from_response_result(result),
59            logs: Vec::new(),
60        }),
61        Err(err) => Ok(CodeModeRunnerOutput::Error {
62            kind: err.kind,
63            message: err.message,
64        }),
65    }
66}
67
68pub fn emit_error(error: RunnerError) {
69    let _ = runner_emit(CodeModeRunnerOutput::Error {
70        kind: error.kind,
71        message: error.message,
72    });
73}
74
75pub fn reset_sequence() {
76    RUNNER_STATE.with(|state| {
77        if let Some(state) = state.borrow_mut().as_mut() {
78            state.next_seq = 0;
79        }
80    });
81}
82
83pub fn cleanup_execution_jail(drop_base: bool) {
84    crate::runner::jail::cleanup_execution_jail(drop_base);
85}
86
87fn build_runtime() -> Result<crate::javy::Runtime, RunnerError> {
88    let mut config = crate::javy::Config::default();
89    config
90        .redirect_stdout_to_stderr(true)
91        .memory_limit(MEMORY_LIMIT_BYTES)
92        .max_stack_size(CODE_MODE_STACK_SIZE_LIMIT);
93    crate::javy::Runtime::new(config).map_err(|err| RunnerError {
94        kind: "runtime_error".to_string(),
95        message: err.to_string(),
96    })
97}
98
99fn install_host_bridge(runtime: &crate::javy::Runtime) -> Result<(), String> {
100    runtime
101        .context()
102        .with(|cx| -> crate::javy::quickjs::Result<()> {
103            let globals = cx.globals();
104            globals.set(
105                "__somaEmitToolCall",
106                crate::javy::quickjs::Function::new(
107                    cx.clone(),
108                    crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
109                        javy_emit_tool_call(crate::javy::Args::hold(cx, args))
110                    }),
111                )?,
112            )?;
113            globals.set(
114                "__somaEmitArtifactWrite",
115                crate::javy::quickjs::Function::new(
116                    cx.clone(),
117                    crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
118                        javy_emit_artifact_write(crate::javy::Args::hold(cx, args))
119                    }),
120                )?,
121            )?;
122            globals.set(
123                "__somaEmitSnippetResolve",
124                crate::javy::quickjs::Function::new(
125                    cx.clone(),
126                    crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
127                        javy_emit_snippet_resolve(crate::javy::Args::hold(cx, args))
128                    }),
129                )?,
130            )?;
131            globals.set(
132                "__somaEmitStepBegin",
133                crate::javy::quickjs::Function::new(
134                    cx.clone(),
135                    crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
136                        javy_emit_step_begin(crate::javy::Args::hold(cx, args))
137                    }),
138                )?,
139            )?;
140            globals.set(
141                "__somaEmitStepResult",
142                crate::javy::quickjs::Function::new(
143                    cx.clone(),
144                    crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
145                        javy_emit_step_result(crate::javy::Args::hold(cx, args))
146                    }),
147                )?,
148            )?;
149            Ok(())
150        })
151        .map_err(javy_error_message)
152}
153
154fn install_no_host_bridge(runtime: &crate::javy::Runtime) -> Result<(), String> {
155    runtime
156        .context()
157        .with(|cx| -> crate::javy::quickjs::Result<()> {
158            let globals = cx.globals();
159            for name in [
160                "__somaEmitToolCall",
161                "__somaEmitArtifactWrite",
162                "__somaEmitSnippetResolve",
163                "__somaEmitStepBegin",
164                "__somaEmitStepResult",
165            ] {
166                globals.set(
167                    name,
168                    crate::javy::quickjs::Function::new(
169                        cx.clone(),
170                        crate::javy::quickjs::prelude::MutFn::new(
171                            |cx: crate::javy::quickjs::Ctx<'_>| {
172                                Err::<u64, _>(javy_type_error(cx, "host bridge unavailable"))
173                            },
174                        ),
175                    )?,
176                )?;
177            }
178            Ok(())
179        })
180        .map_err(javy_error_message)
181}
182
183fn run_wrapped(runtime: crate::javy::Runtime, code: &str, proxy: &str) -> Result<(), RunnerError> {
184    let result = run_wrapped_to_result(runtime, code, proxy)?;
185    runner_emit(CodeModeRunnerOutput::Done {
186        result: CodeModeRunnerResult::from_response_result(result),
187        logs: Vec::new(),
188    })
189    .map_err(RunnerError::from)
190}
191
192fn run_wrapped_to_result(
193    runtime: crate::javy::Runtime,
194    code: &str,
195    proxy: &str,
196) -> Result<Option<Value>, RunnerError> {
197    runtime
198        .context()
199        .with(|cx| cx.eval::<(), _>(code_mode_runner_script(code, proxy)))
200        .map_err(|err| RunnerError {
201            kind: "invalid_param".to_string(),
202            message: javy_error_message(err),
203        })?;
204    loop {
205        runtime
206            .resolve_pending_jobs()
207            .map_err(|err| err.to_string())?;
208        match javy_main_promise_state(&runtime)? {
209            JavyMainPromiseState::Resolved(result) => return Ok(result),
210            JavyMainPromiseState::Rejected(message) => return Err(classify_rejection(message)),
211            JavyMainPromiseState::Pending => {
212                let input = runner_read_input().map_err(RunnerReadError::into_runner_error)?;
213                javy_settle_pending_operation(&runtime, &input)?;
214            }
215        }
216    }
217}
218
219enum JavyMainPromiseState {
220    Pending,
221    Resolved(Option<Value>),
222    Rejected(String),
223}
224
225fn javy_main_promise_state(runtime: &crate::javy::Runtime) -> Result<JavyMainPromiseState, String> {
226    runtime
227        .context()
228        .with(|cx| -> crate::javy::quickjs::Result<JavyMainPromiseState> {
229            let promise: crate::javy::quickjs::Promise<'_> =
230                cx.globals().get("__somaMainPromise")?;
231            match promise.result::<crate::javy::quickjs::Value<'_>>() {
232                None => Ok(JavyMainPromiseState::Pending),
233                Some(Ok(val)) if val.is_undefined() => Ok(JavyMainPromiseState::Resolved(None)),
234                Some(Ok(val)) => match cx.json_stringify(val) {
235                    Ok(Some(json_str)) => {
236                        let text = json_str.to_string()?;
237                        serde_json::from_str(&text)
238                            .map(Some)
239                            .map(JavyMainPromiseState::Resolved)
240                            .or_else(|err| {
241                                Ok(JavyMainPromiseState::Rejected(format!(
242                                    "Code Mode result must be JSON-serializable: {err}"
243                                )))
244                            })
245                    }
246                    Ok(None) => Ok(JavyMainPromiseState::Rejected(
247                        "Code Mode result must be JSON-serializable".to_string(),
248                    )),
249                    Err(err) => Ok(JavyMainPromiseState::Rejected(javy_caught_error_message(
250                        &cx, err,
251                    ))),
252                },
253                Some(Err(err)) => Ok(JavyMainPromiseState::Rejected(javy_caught_error_message(
254                    &cx, err,
255                ))),
256            }
257        })
258        .map_err(javy_error_message)
259}
260
261fn javy_settle_pending_operation(
262    runtime: &crate::javy::Runtime,
263    input: &CodeModeRunnerInput,
264) -> Result<(), String> {
265    let message = serde_json::to_string(input).map_err(|err| err.to_string())?;
266    runtime
267        .context()
268        .with(|cx| -> crate::javy::quickjs::Result<()> {
269            let settle: crate::javy::quickjs::Function<'_> =
270                cx.globals().get("__somaSettlePendingOperation")?;
271            settle.call::<_, ()>((message,))?;
272            Ok(())
273        })
274        .map_err(javy_error_message)?;
275    runtime
276        .resolve_pending_jobs()
277        .map_err(|err| err.to_string())
278}
279
280fn javy_emit_tool_call(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
281    let (cx, args) = args.release();
282    let id = required_string_arg(&cx, &args.0, 0, "callTool id must be a non-empty string")?;
283    let params = json_arg(&cx, &args.0, 1, "{}")?;
284    if !params.is_object() {
285        return Err(javy_type_error(cx, "callTool params must be a JSON object"));
286    }
287    let seq = next_runner_seq(&cx)?;
288    runner_emit(CodeModeRunnerOutput::ToolCall { seq, id, params })
289        .map_err(|err| javy_type_error(cx, err))?;
290    Ok(seq)
291}
292
293fn javy_emit_artifact_write(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
294    let (cx, args) = args.release();
295    let path = required_string_arg(&cx, &args.0, 0, "writeArtifact path must be a string")?;
296    let content = required_string_arg(&cx, &args.0, 1, "writeArtifact content must be a string")?;
297    let content_type = optional_string_arg(&cx, &args.0, 2)?;
298    let seq = next_runner_seq(&cx)?;
299    runner_emit(CodeModeRunnerOutput::ArtifactWrite {
300        seq,
301        path,
302        content,
303        content_type,
304    })
305    .map_err(|err| javy_type_error(cx, err))?;
306    Ok(seq)
307}
308
309fn javy_emit_snippet_resolve(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
310    let (cx, args) = args.release();
311    let name = required_string_arg(&cx, &args.0, 0, "snippet name must be a string")?;
312    let input = json_arg(&cx, &args.0, 1, "{}")?;
313    let seq = next_runner_seq(&cx)?;
314    runner_emit(CodeModeRunnerOutput::SnippetResolve { seq, name, input })
315        .map_err(|err| javy_type_error(cx, err))?;
316    Ok(seq)
317}
318
319fn javy_emit_step_begin(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
320    let (cx, args) = args.release();
321    let name = required_string_arg(&cx, &args.0, 0, "codemode.step name must be a string")?;
322    let seq = next_runner_seq(&cx)?;
323    runner_emit(CodeModeRunnerOutput::StepBegin { seq, name })
324        .map_err(|err| javy_type_error(cx, err))?;
325    Ok(seq)
326}
327
328fn javy_emit_step_result(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
329    let (cx, args) = args.release();
330    let seq = args
331        .0
332        .first()
333        .and_then(crate::javy::quickjs::Value::as_number)
334        .ok_or_else(|| javy_type_error(cx.clone(), "codemode.step result seq must be a number"))?
335        as u64;
336    let value = json_arg(&cx, &args.0, 1, "null")?;
337    runner_emit(CodeModeRunnerOutput::StepResult { seq, value })
338        .map_err(|err| javy_type_error(cx, err))?;
339    Ok(seq)
340}
341
342fn classify_rejection(message: String) -> RunnerError {
343    if let Some(kind) = extract_structured_kind(&message) {
344        return RunnerError { kind, message };
345    }
346    if message.contains("JSON-serializable") {
347        return RunnerError {
348            kind: "invalid_param".to_string(),
349            message,
350        };
351    }
352    RunnerError {
353        kind: "server_error".to_string(),
354        message,
355    }
356}
357
358fn extract_structured_kind(message: &str) -> Option<String> {
359    let start = message.find('{')?;
360    let end = message.rfind('}')?;
361    let Value::Object(map) = serde_json::from_str::<Value>(&message[start..=end]).ok()? else {
362        return None;
363    };
364    map.get("kind").and_then(Value::as_str).map(str::to_string)
365}
366
367fn next_runner_seq(cx: &crate::javy::quickjs::Ctx<'_>) -> crate::javy::quickjs::Result<u64> {
368    RUNNER_STATE
369        .with(|state| {
370            let mut state = state.borrow_mut();
371            let state = state
372                .as_mut()
373                .ok_or_else(|| "runner state is not initialized".to_string())?;
374            let seq = state.next_seq;
375            state.next_seq = state.next_seq.saturating_add(1);
376            Ok::<_, String>(seq)
377        })
378        .map_err(|err| javy_type_error(cx.clone(), err))
379}
380
381fn runner_emit(output: CodeModeRunnerOutput) -> Result<(), String> {
382    use std::io::Write;
383    RUNNER_STATE.with(|state| {
384        let mut state = state.borrow_mut();
385        let state = state
386            .as_mut()
387            .ok_or_else(|| "runner state is not initialized".to_string())?;
388        serde_json::to_writer(&mut state.writer, &output).map_err(|err| err.to_string())?;
389        state
390            .writer
391            .write_all(b"\n")
392            .map_err(|err| err.to_string())?;
393        state.writer.flush().map_err(|err| err.to_string())
394    })
395}
396
397enum RunnerReadError {
398    InputClosed,
399    Other(String),
400}
401
402impl RunnerReadError {
403    fn into_runner_error(self) -> RunnerError {
404        match self {
405            Self::InputClosed => "runner input closed".to_string().into(),
406            Self::Other(message) => message.into(),
407        }
408    }
409}
410
411fn runner_read_input() -> Result<CodeModeRunnerInput, RunnerReadError> {
412    use std::io::BufRead;
413    RUNNER_STATE.with(|state| {
414        let mut state = state.borrow_mut();
415        let state = state
416            .as_mut()
417            .ok_or_else(|| RunnerReadError::Other("runner state is not initialized".to_string()))?;
418        let mut line = String::new();
419        let read = state
420            .reader
421            .read_line(&mut line)
422            .map_err(|err| RunnerReadError::Other(err.to_string()))?;
423        if read == 0 {
424            return Err(RunnerReadError::InputClosed);
425        }
426        serde_json::from_str(&line).map_err(|err| RunnerReadError::Other(err.to_string()))
427    })
428}
429
430fn javy_error_message(error: crate::javy::quickjs::Error) -> String {
431    error.to_string()
432}
433
434fn javy_caught_error_message(
435    cx: &crate::javy::quickjs::Ctx<'_>,
436    error: crate::javy::quickjs::Error,
437) -> String {
438    match crate::javy::quickjs::CaughtError::from_error(cx, error) {
439        crate::javy::quickjs::CaughtError::Exception(exception) => {
440            exception.message().unwrap_or_else(|| exception.to_string())
441        }
442        crate::javy::quickjs::CaughtError::Value(value) => {
443            crate::javy::val_to_string(cx, value).unwrap_or_else(|err| err.to_string())
444        }
445        crate::javy::quickjs::CaughtError::Error(error) => error.to_string(),
446    }
447}