Skip to main content

sim_lib_agent_runner_process/
lib.rs

1//! Local subprocess-backed model runners for SIM.
2
3#![forbid(unsafe_code)]
4#![deny(missing_docs)]
5#![allow(deprecated)]
6
7mod effects;
8mod json_stdio;
9mod line_text;
10mod process;
11
12use sim_codec_chat::model_error_expr;
13use sim_kernel::{Cx, Error, Expr, Result, Symbol};
14use sim_lib_agent_runner_core::{
15    ModelCard, ModelEvent, ModelEventSink, ModelRequest, ModelResponse, ModelRunner,
16};
17use std::time::Duration;
18
19pub use effects::host_process_capability;
20pub use process::{ProcessCommandSpec, run_process_command, stream_process_command_lines};
21
22/// Wire protocol a [`ProcessRunner`] speaks with its subprocess.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum ProcessProtocol {
25    /// One JSON request/response exchange over stdin/stdout.
26    JsonStdio,
27    /// Plain text prompt in, line-delimited text out.
28    LineText,
29}
30
31/// Local subprocess-backed [`ModelRunner`].
32#[derive(Clone, Debug)]
33pub struct ProcessRunner {
34    runner: Symbol,
35    model: String,
36    command: String,
37    protocol: ProcessProtocol,
38    timeout: Duration,
39    max_output_bytes: usize,
40}
41
42impl ProcessRunner {
43    /// Builds a runner that invokes `command` using `protocol`, bounded by
44    /// `timeout` and `max_output_bytes`.
45    pub fn new(
46        runner: Symbol,
47        model: impl Into<String>,
48        command: impl Into<String>,
49        protocol: ProcessProtocol,
50        timeout: Duration,
51        max_output_bytes: usize,
52    ) -> Self {
53        Self {
54            runner,
55            model: model.into(),
56            command: command.into(),
57            protocol,
58            timeout,
59            max_output_bytes,
60        }
61    }
62
63    fn infer_inner(&self, request: ModelRequest) -> Result<ModelResponse> {
64        match self.protocol {
65            ProcessProtocol::JsonStdio => json_stdio::infer(self, request),
66            ProcessProtocol::LineText => line_text::infer(self, request),
67        }
68    }
69
70    fn error_response(&self, message: impl Into<String>) -> ModelResponse {
71        ModelResponse::try_from(model_error_expr(
72            self.runner.clone(),
73            self.model.clone(),
74            message,
75        ))
76        .expect("model_error_expr should always produce a valid response transcript")
77    }
78}
79
80impl ModelRunner for ProcessRunner {
81    fn card(&self) -> ModelCard {
82        ModelCard::new(
83            self.runner.clone(),
84            self.model.clone(),
85            Symbol::new("process"),
86            Symbol::new("local"),
87        )
88    }
89
90    fn infer(&self, cx: &mut Cx, request: ModelRequest) -> Result<ModelResponse> {
91        match effects::resolve_process_effect(self, cx, request, |runner, request| {
92            runner.infer_inner(request)
93        }) {
94            Ok(response) => Ok(response),
95            Err(error) => Ok(self.error_response(render_error(error))),
96        }
97    }
98
99    fn infer_stream(
100        &self,
101        cx: &mut Cx,
102        request: ModelRequest,
103        sink: &mut dyn ModelEventSink,
104    ) -> Result<ModelResponse> {
105        let streamed = match self.protocol {
106            ProcessProtocol::JsonStdio => None,
107            ProcessProtocol::LineText => Some(effects::resolve_process_effect(
108                self,
109                cx,
110                request.clone(),
111                {
112                    let sink = &mut *sink;
113                    |runner, request| line_text::infer_stream(runner, request, sink)
114                },
115            )),
116        };
117        match streamed.unwrap_or_else(|| {
118            effects::resolve_process_effect(self, cx, request, |runner, request| {
119                runner.infer_inner(request)
120            })
121        }) {
122            Ok(response) => Ok(response),
123            Err(error) => {
124                let message = render_error(error);
125                sink.emit(ModelEvent::error_text(
126                    self.runner.clone(),
127                    self.model.clone(),
128                    Expr::String("process-error".to_owned()),
129                    message.clone(),
130                ))?;
131                let response = self.error_response(message);
132                sink.emit(ModelEvent::final_of(&response))?;
133                Ok(response)
134            }
135        }
136    }
137}
138
139fn render_error(error: Error) -> String {
140    match error {
141        Error::Eval(message) | Error::HostError(message) => message,
142        other => other.to_string(),
143    }
144}
145
146/// Cookbook recipes for this lib, embedded at build time.
147pub static RECIPES: sim_cookbook::EmbeddedDir =
148    include!(concat!(env!("OUT_DIR"), "/cookbook_recipes.rs"));