sim_lib_agent_runner_process/
lib.rs1#![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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum ProcessProtocol {
25 JsonStdio,
27 LineText,
29}
30
31#[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 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
146pub static RECIPES: sim_cookbook::EmbeddedDir =
148 include!(concat!(env!("OUT_DIR"), "/cookbook_recipes.rs"));