use std::fmt;
use std::sync::Arc;
use oino_ai::{ImageResize, InputModality, Model};
use crate::bash::BashDetachRegistry;
use crate::env::{ExecutionEnv, LocalEnv};
use crate::mutation_queue::FileMutationQueue;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ModelImageInfo {
pub accepts_images: bool,
pub resize: Option<ImageResize>,
}
impl From<&Model> for ModelImageInfo {
fn from(model: &Model) -> Self {
Self {
accepts_images: model.input.contains(&InputModality::Image),
resize: model
.input_limits
.as_ref()
.and_then(|limits| limits.images.as_ref())
.and_then(|images| images.resize.clone()),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SessionEnv {
pub session_id: String,
pub session_file: Option<String>,
pub provider: Option<String>,
pub model: Option<String>,
pub reasoning_level: Option<String>,
}
pub const SESSION_ENV_NAMES: [&str; 10] = [
"OINO_SESSION_ID",
"OINO_SESSION_FILE",
"OINO_PROVIDER",
"OINO_MODEL",
"OINO_REASONING_LEVEL",
"PI_SESSION_ID",
"PI_SESSION_FILE",
"PI_PROVIDER",
"PI_MODEL",
"PI_REASONING_LEVEL",
];
impl SessionEnv {
#[must_use]
pub fn vars(&self) -> Vec<(String, String)> {
let values = [
("SESSION_ID", Some(&self.session_id)),
("SESSION_FILE", self.session_file.as_ref()),
("PROVIDER", self.provider.as_ref()),
("MODEL", self.model.as_ref()),
("REASONING_LEVEL", self.reasoning_level.as_ref()),
];
let mut vars = Vec::new();
for prefix in ["OINO_", "PI_"] {
for (name, value) in values {
if let Some(value) = value {
vars.push((format!("{prefix}{name}"), value.clone()));
}
}
}
vars
}
}
pub trait ToolContext: Send + Sync {
fn cwd(&self) -> Option<String> {
None
}
fn model_images(&self) -> Option<ModelImageInfo> {
None
}
fn session_env(&self) -> Option<SessionEnv> {
None
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoContext;
impl ToolContext for NoContext {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageOptions {
pub auto_resize: bool,
pub fallback_resize: Option<ImageResize>,
}
impl Default for ImageOptions {
fn default() -> Self {
Self {
auto_resize: true,
fallback_resize: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShellOptions {
pub shell_path: Option<String>,
pub command_prefix: Option<String>,
pub expose_session_environment: bool,
}
impl Default for ShellOptions {
fn default() -> Self {
Self {
shell_path: None,
command_prefix: None,
expose_session_environment: true,
}
}
}
#[derive(Clone)]
pub struct ToolConfig {
pub cwd: String,
pub agent_dir: String,
pub env: Arc<dyn ExecutionEnv>,
pub context: Arc<dyn ToolContext>,
pub images: ImageOptions,
pub shell: ShellOptions,
pub mutation_queue: FileMutationQueue,
pub bash_detach: BashDetachRegistry,
pub process_env: Option<Vec<(String, String)>>,
}
impl fmt::Debug for ToolConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ToolConfig")
.field("cwd", &self.cwd)
.field("agent_dir", &self.agent_dir)
.field("images", &self.images)
.field("shell", &self.shell)
.finish_non_exhaustive()
}
}
impl ToolConfig {
#[must_use]
pub fn new(cwd: impl Into<String>, agent_dir: impl Into<String>) -> Self {
Self {
cwd: cwd.into(),
agent_dir: agent_dir.into(),
env: Arc::new(LocalEnv::new()),
context: Arc::new(NoContext),
images: ImageOptions::default(),
shell: ShellOptions::default(),
mutation_queue: FileMutationQueue::global(),
bash_detach: BashDetachRegistry::default(),
process_env: None,
}
}
#[must_use]
pub fn effective_cwd(&self) -> String {
self.context
.cwd()
.filter(|cwd| !cwd.is_empty())
.unwrap_or_else(|| self.cwd.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_env_exports_both_prefixes() {
let env = SessionEnv {
session_id: "s".into(),
model: Some("m".into()),
..SessionEnv::default()
};
assert_eq!(
env.vars(),
vec![
("OINO_SESSION_ID".into(), "s".into()),
("OINO_MODEL".into(), "m".into()),
("PI_SESSION_ID".into(), "s".into()),
("PI_MODEL".into(), "m".into()),
]
);
}
}