use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use futures::future::BoxFuture;
use oino_ai::{Model, ThinkingLevel};
use oino_core::builtin::SessionToolContext;
use oino_core::extensions::api::ExtensionMode;
use oino_core::runtime::{AgentSessionRuntime, SessionOptions, SessionSource, ToolSelection};
use oino_core::services::{Services, ServicesOptions};
use oino_core::session::ToolEntry;
use oino_tools::env::ExecutionEnv;
use oino_tools::{ImageOptions, ShellOptions, ToolConfig, builtin_tool_infos, builtin_tools};
use tokio::sync::Mutex;
use crate::error::AcpError;
pub struct SessionRequest {
pub cwd: PathBuf,
pub source: SessionSource,
pub env: Arc<dyn ExecutionEnv>,
}
impl std::fmt::Debug for SessionRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionRequest")
.field("cwd", &self.cwd)
.field("source", &self.source)
.finish_non_exhaustive()
}
}
pub trait SessionFactory: Send + Sync {
fn create(
&self,
request: SessionRequest,
) -> BoxFuture<'_, Result<Arc<AgentSessionRuntime>, AcpError>>;
fn sessions_root<'a>(&'a self, cwd: &'a Path) -> BoxFuture<'a, Result<PathBuf, AcpError>>;
fn agent_dir<'a>(&'a self, cwd: &'a Path) -> BoxFuture<'a, Result<PathBuf, AcpError>>;
}
#[derive(Clone, Debug, Default)]
pub struct SessionDefaults {
pub session_dir: Option<PathBuf>,
pub model: Option<Model>,
pub thinking_level: Option<ThinkingLevel>,
pub tools: ToolSelection,
pub exclude_tools: Vec<String>,
pub model_resolver: Option<ModelResolver>,
}
#[derive(Clone, Debug, Default)]
pub struct ModelChoice {
pub model: Option<Model>,
pub thinking_level: Option<ThinkingLevel>,
pub scoped_models: Vec<oino_models::resolver::ScopedModel>,
}
#[derive(Clone)]
pub struct ModelResolver(pub Arc<dyn Fn(&Services) -> ModelChoice + Send + Sync>);
impl std::fmt::Debug for ModelResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ModelResolver(..)")
}
}
pub type ServicesOptionsFn = dyn Fn(&Path) -> ServicesOptions + Send + Sync;
pub struct CoreSessionFactory {
options: Box<ServicesOptionsFn>,
defaults: SessionDefaults,
services: Mutex<HashMap<PathBuf, Arc<Services>>>,
}
impl std::fmt::Debug for CoreSessionFactory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CoreSessionFactory")
.field("defaults", &self.defaults)
.finish_non_exhaustive()
}
}
impl CoreSessionFactory {
#[must_use]
pub fn new(
options: impl Fn(&Path) -> ServicesOptions + Send + Sync + 'static,
defaults: SessionDefaults,
) -> Self {
Self {
options: Box::new(options),
defaults,
services: Mutex::new(HashMap::new()),
}
}
pub async fn services(&self, cwd: &Path) -> Result<Arc<Services>, AcpError> {
let mut cache = self.services.lock().await;
if let Some(services) = cache.get(cwd) {
return Ok(Arc::clone(services));
}
let mut options = (self.options)(cwd);
options.cwd = cwd.to_path_buf();
options.mode = ExtensionMode::Acp;
options.has_ui = true;
let services = Services::create(options)
.await
.map_err(|e| AcpError::Factory(e.to_string()))?;
for diagnostic in &services.diagnostics {
tracing::warn!("{diagnostic}");
}
cache.insert(cwd.to_path_buf(), Arc::clone(&services));
Ok(services)
}
}
#[must_use]
pub fn builtin_tool_entries_with_env(
services: &Services,
context: Arc<SessionToolContext>,
env: Arc<dyn ExecutionEnv>,
) -> Vec<ToolEntry> {
let settings = services
.settings
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.snapshot();
let mut config = ToolConfig::new(
services.cwd.display().to_string(),
services.agent_dir.display().to_string(),
);
config.env = env;
config.context = context;
config.images = ImageOptions {
auto_resize: settings.image_settings().auto_resize,
fallback_resize: None,
};
config.shell = ShellOptions {
shell_path: settings.shell_path().map(|p| p.display().to_string()),
command_prefix: settings.shell_command_prefix().map(str::to_owned),
expose_session_environment: true,
};
let infos = builtin_tool_infos(&config);
builtin_tools(&config)
.into_iter()
.zip(infos)
.map(|(tool, info)| {
ToolEntry::builtin(
tool,
info.prompt_snippet.map(str::to_owned),
info.prompt_guidelines.unwrap_or_default(),
)
})
.collect()
}
impl SessionFactory for CoreSessionFactory {
fn create(
&self,
request: SessionRequest,
) -> BoxFuture<'_, Result<Arc<AgentSessionRuntime>, AcpError>> {
Box::pin(async move {
let services = self.services(&request.cwd).await?;
let context = Arc::new(SessionToolContext::default());
let builtin_tools =
builtin_tool_entries_with_env(&services, Arc::clone(&context), request.env);
let choice = match (&self.defaults.model, &self.defaults.model_resolver) {
(None, Some(resolver)) => (resolver.0)(&services),
(model, _) => ModelChoice {
model: model.clone(),
..ModelChoice::default()
},
};
let options = SessionOptions {
source: request.source,
session_dir: self.defaults.session_dir.clone(),
name: None,
model: choice.model,
thinking_level: self.defaults.thinking_level.or(choice.thinking_level),
scoped_models: choice.scoped_models,
tools: self.defaults.tools.clone(),
exclude_tools: self.defaults.exclude_tools.clone(),
builtin_tools,
custom_tools: Vec::new(),
};
let runtime = AgentSessionRuntime::create(services, options)
.await
.map_err(|e| AcpError::Factory(e.to_string()))?;
context.bind(&runtime.session());
Ok(Arc::new(runtime))
})
}
fn sessions_root<'a>(&'a self, cwd: &'a Path) -> BoxFuture<'a, Result<PathBuf, AcpError>> {
Box::pin(async move {
let services = self.services(cwd).await?;
Ok(services.sessions_root(self.defaults.session_dir.as_deref()))
})
}
fn agent_dir<'a>(&'a self, cwd: &'a Path) -> BoxFuture<'a, Result<PathBuf, AcpError>> {
Box::pin(async move { Ok(self.services(cwd).await?.agent_dir.clone()) })
}
}