1use std::fmt;
5use std::sync::Arc;
6
7use oino_ai::{ImageResize, InputModality, Model};
8
9use crate::bash::BashDetachRegistry;
10use crate::env::{ExecutionEnv, LocalEnv};
11use crate::mutation_queue::FileMutationQueue;
12
13#[derive(Clone, Debug, Default, PartialEq, Eq)]
15pub struct ModelImageInfo {
16 pub accepts_images: bool,
18 pub resize: Option<ImageResize>,
20}
21
22impl From<&Model> for ModelImageInfo {
23 fn from(model: &Model) -> Self {
24 Self {
25 accepts_images: model.input.contains(&InputModality::Image),
26 resize: model
27 .input_limits
28 .as_ref()
29 .and_then(|limits| limits.images.as_ref())
30 .and_then(|images| images.resize.clone()),
31 }
32 }
33}
34
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct SessionEnv {
38 pub session_id: String,
40 pub session_file: Option<String>,
42 pub provider: Option<String>,
44 pub model: Option<String>,
46 pub reasoning_level: Option<String>,
48}
49
50pub const SESSION_ENV_NAMES: [&str; 10] = [
52 "OINO_SESSION_ID",
53 "OINO_SESSION_FILE",
54 "OINO_PROVIDER",
55 "OINO_MODEL",
56 "OINO_REASONING_LEVEL",
57 "PI_SESSION_ID",
58 "PI_SESSION_FILE",
59 "PI_PROVIDER",
60 "PI_MODEL",
61 "PI_REASONING_LEVEL",
62];
63
64impl SessionEnv {
65 #[must_use]
67 pub fn vars(&self) -> Vec<(String, String)> {
68 let values = [
69 ("SESSION_ID", Some(&self.session_id)),
70 ("SESSION_FILE", self.session_file.as_ref()),
71 ("PROVIDER", self.provider.as_ref()),
72 ("MODEL", self.model.as_ref()),
73 ("REASONING_LEVEL", self.reasoning_level.as_ref()),
74 ];
75 let mut vars = Vec::new();
76 for prefix in ["OINO_", "PI_"] {
77 for (name, value) in values {
78 if let Some(value) = value {
79 vars.push((format!("{prefix}{name}"), value.clone()));
80 }
81 }
82 }
83 vars
84 }
85}
86
87pub trait ToolContext: Send + Sync {
89 fn cwd(&self) -> Option<String> {
91 None
92 }
93
94 fn model_images(&self) -> Option<ModelImageInfo> {
96 None
97 }
98
99 fn session_env(&self) -> Option<SessionEnv> {
101 None
102 }
103}
104
105#[derive(Clone, Copy, Debug, Default)]
107pub struct NoContext;
108
109impl ToolContext for NoContext {}
110
111#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct ImageOptions {
114 pub auto_resize: bool,
116 pub fallback_resize: Option<ImageResize>,
118}
119
120impl Default for ImageOptions {
121 fn default() -> Self {
122 Self {
123 auto_resize: true,
124 fallback_resize: None,
125 }
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct ShellOptions {
132 pub shell_path: Option<String>,
134 pub command_prefix: Option<String>,
136 pub expose_session_environment: bool,
138}
139
140impl Default for ShellOptions {
141 fn default() -> Self {
142 Self {
143 shell_path: None,
144 command_prefix: None,
145 expose_session_environment: true,
146 }
147 }
148}
149
150#[derive(Clone)]
152pub struct ToolConfig {
153 pub cwd: String,
155 pub agent_dir: String,
158 pub env: Arc<dyn ExecutionEnv>,
160 pub context: Arc<dyn ToolContext>,
162 pub images: ImageOptions,
164 pub shell: ShellOptions,
166 pub mutation_queue: FileMutationQueue,
168 pub bash_detach: BashDetachRegistry,
170 pub process_env: Option<Vec<(String, String)>>,
172}
173
174impl fmt::Debug for ToolConfig {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 f.debug_struct("ToolConfig")
177 .field("cwd", &self.cwd)
178 .field("agent_dir", &self.agent_dir)
179 .field("images", &self.images)
180 .field("shell", &self.shell)
181 .finish_non_exhaustive()
182 }
183}
184
185impl ToolConfig {
186 #[must_use]
189 pub fn new(cwd: impl Into<String>, agent_dir: impl Into<String>) -> Self {
190 Self {
191 cwd: cwd.into(),
192 agent_dir: agent_dir.into(),
193 env: Arc::new(LocalEnv::new()),
194 context: Arc::new(NoContext),
195 images: ImageOptions::default(),
196 shell: ShellOptions::default(),
197 mutation_queue: FileMutationQueue::global(),
198 bash_detach: BashDetachRegistry::default(),
199 process_env: None,
200 }
201 }
202
203 #[must_use]
205 pub fn effective_cwd(&self) -> String {
206 self.context
207 .cwd()
208 .filter(|cwd| !cwd.is_empty())
209 .unwrap_or_else(|| self.cwd.clone())
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn session_env_exports_both_prefixes() {
219 let env = SessionEnv {
220 session_id: "s".into(),
221 model: Some("m".into()),
222 ..SessionEnv::default()
223 };
224 assert_eq!(
225 env.vars(),
226 vec![
227 ("OINO_SESSION_ID".into(), "s".into()),
228 ("OINO_MODEL".into(), "m".into()),
229 ("PI_SESSION_ID".into(), "s".into()),
230 ("PI_MODEL".into(), "m".into()),
231 ]
232 );
233 }
234}