Skip to main content

oino_tools/
config.rs

1//! Tool configuration and the per-call context core supplies (the `ToolsOptions` +
2//! `ExtensionContext` fields the built-ins read).
3
4use 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/// What the `read` tool needs to know about the current model.
14#[derive(Clone, Debug, Default, PartialEq, Eq)]
15pub struct ModelImageInfo {
16    /// The model accepts image input.
17    pub accepts_images: bool,
18    /// The model's image resize policy.
19    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/// Session values exported to shell tools (runtime §9.5, D12).
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct SessionEnv {
38    /// Session id.
39    pub session_id: String,
40    /// Session file, when persisted.
41    pub session_file: Option<String>,
42    /// Provider of the selected model.
43    pub provider: Option<String>,
44    /// Selected model id (the virtual selection when one is shown).
45    pub model: Option<String>,
46    /// Reasoning level.
47    pub reasoning_level: Option<String>,
48}
49
50/// Every variable [`SessionEnv`] may set; inherited copies are removed before spawning.
51pub 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    /// `OINO_*` variables followed by their identical `PI_*` aliases.
66    #[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
87/// Live values read at call time (from the `ExtensionContext`).
88pub trait ToolContext: Send + Sync {
89    /// Current working directory; `None` uses [`ToolConfig::cwd`].
90    fn cwd(&self) -> Option<String> {
91        None
92    }
93
94    /// Image capabilities of the current model; `None` when no model is selected.
95    fn model_images(&self) -> Option<ModelImageInfo> {
96        None
97    }
98
99    /// Session values for shell tools; `None` exports nothing.
100    fn session_env(&self) -> Option<SessionEnv> {
101        None
102    }
103}
104
105/// A context without a session or model.
106#[derive(Clone, Copy, Debug, Default)]
107pub struct NoContext;
108
109impl ToolContext for NoContext {}
110
111/// Image handling of `read` (settings `images.autoResize` + fallback resize profile).
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct ImageOptions {
114    /// Resize images to inline limits (`autoResizeImages`, default `true`).
115    pub auto_resize: bool,
116    /// Resize profile when the model has none (`resizeOptions`; `None` = 2000×2000, 4.5MB).
117    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/// Shell tool options.
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct ShellOptions {
132    /// Explicit shell (setting `shellPath`).
133    pub shell_path: Option<String>,
134    /// Prepended to every command as `{prefix}\n{command}` (setting `shellCommandPrefix`).
135    pub command_prefix: Option<String>,
136    /// Export [`SessionEnv`] and advertise it in the prompt guideline (default `true`).
137    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/// Everything the built-in tools are constructed with.
151#[derive(Clone)]
152pub struct ToolConfig {
153    /// Default working directory (absolute).
154    pub cwd: String,
155    /// Agent directory (`~/.oino`): temp files go to `<agent_dir>/tmp`, `<agent_dir>/bin` is
156    /// prepended to `PATH`.
157    pub agent_dir: String,
158    /// File system and process host.
159    pub env: Arc<dyn ExecutionEnv>,
160    /// Live per-call values.
161    pub context: Arc<dyn ToolContext>,
162    /// `read` image handling.
163    pub images: ImageOptions,
164    /// `bash` / `powershell` options.
165    pub shell: ShellOptions,
166    /// Per-file serialisation of `write` / `edit`.
167    pub mutation_queue: FileMutationQueue,
168    /// Registry through which core detaches running `bash` calls.
169    pub bash_detach: BashDetachRegistry,
170    /// Base environment of spawned shells; `None` uses this process's environment.
171    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    /// Defaults: [`LocalEnv`], [`NoContext`], the global mutation queue, a fresh detach
187    /// registry, the process environment.
188    #[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    /// The call's working directory.
204    #[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}