testing
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use crate::client::{ModelType, list_models};
|
||||
use crate::config::paths;
|
||||
use crate::config::{Config, list_agents};
|
||||
use clap_complete::{CompletionCandidate, Shell, generate};
|
||||
use clap_complete_nushell::Nushell;
|
||||
@@ -33,7 +34,7 @@ impl ShellCompletion {
|
||||
pub(super) fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
match Config::init_bare() {
|
||||
Ok(config) => list_models(&config, ModelType::Chat)
|
||||
Ok(config) => list_models(&config.to_app_config(), ModelType::Chat)
|
||||
.into_iter()
|
||||
.filter(|&m| m.id().starts_with(&*cur))
|
||||
.map(|m| CompletionCandidate::new(m.id()))
|
||||
@@ -44,7 +45,7 @@ pub(super) fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
|
||||
pub(super) fn role_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
Config::list_roles(true)
|
||||
paths::list_roles(true)
|
||||
.into_iter()
|
||||
.filter(|r| r.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
@@ -62,7 +63,7 @@ pub(super) fn agent_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
|
||||
pub(super) fn rag_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
Config::list_rags()
|
||||
paths::list_rags()
|
||||
.into_iter()
|
||||
.filter(|r| r.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
@@ -71,7 +72,7 @@ pub(super) fn rag_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
|
||||
pub(super) fn macro_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||
let cur = current.to_string_lossy();
|
||||
Config::list_macros()
|
||||
paths::list_macros()
|
||||
.into_iter()
|
||||
.filter(|m| m.starts_with(&*cur))
|
||||
.map(CompletionCandidate::new)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::*;
|
||||
|
||||
use crate::config::paths;
|
||||
use crate::{
|
||||
config::{Config, GlobalConfig, Input},
|
||||
config::{GlobalConfig, Input},
|
||||
function::{FunctionDeclaration, ToolCall, ToolResult, eval_tool_calls},
|
||||
render::render_stream,
|
||||
utils::*,
|
||||
@@ -24,7 +25,7 @@ use tokio::sync::mpsc::unbounded_channel;
|
||||
pub const MODELS_YAML: &str = include_str!("../../models.yaml");
|
||||
|
||||
pub static ALL_PROVIDER_MODELS: LazyLock<Vec<ProviderModels>> = LazyLock::new(|| {
|
||||
Config::local_models_override()
|
||||
paths::local_models_override()
|
||||
.ok()
|
||||
.unwrap_or_else(|| serde_yaml::from_str(MODELS_YAML).unwrap())
|
||||
});
|
||||
|
||||
@@ -101,7 +101,7 @@ macro_rules! register_client {
|
||||
|
||||
static ALL_CLIENT_NAMES: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn list_client_names(config: &$crate::config::Config) -> Vec<&'static String> {
|
||||
pub fn list_client_names(config: &$crate::config::AppConfig) -> Vec<&'static String> {
|
||||
let names = ALL_CLIENT_NAMES.get_or_init(|| {
|
||||
config
|
||||
.clients
|
||||
@@ -117,7 +117,7 @@ macro_rules! register_client {
|
||||
|
||||
static ALL_MODELS: std::sync::OnceLock<Vec<$crate::client::Model>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn list_all_models(config: &$crate::config::Config) -> Vec<&'static $crate::client::Model> {
|
||||
pub fn list_all_models(config: &$crate::config::AppConfig) -> Vec<&'static $crate::client::Model> {
|
||||
let models = ALL_MODELS.get_or_init(|| {
|
||||
config
|
||||
.clients
|
||||
@@ -131,7 +131,7 @@ macro_rules! register_client {
|
||||
models.iter().collect()
|
||||
}
|
||||
|
||||
pub fn list_models(config: &$crate::config::Config, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
|
||||
pub fn list_models(config: &$crate::config::AppConfig, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
|
||||
list_all_models(config).into_iter().filter(|v| v.model_type() == model_type).collect()
|
||||
}
|
||||
};
|
||||
|
||||
+6
-2
@@ -3,7 +3,7 @@ use super::{
|
||||
message::{Message, MessageContent, MessageContentPart},
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::AppConfig;
|
||||
use crate::utils::{estimate_token_length, strip_think_tag};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
@@ -44,7 +44,11 @@ impl Model {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn retrieve_model(config: &Config, model_id: &str, model_type: ModelType) -> Result<Self> {
|
||||
pub fn retrieve_model(
|
||||
config: &AppConfig,
|
||||
model_id: &str,
|
||||
model_type: ModelType,
|
||||
) -> Result<Self> {
|
||||
let models = list_all_models(config);
|
||||
let (client_name, model_name) = match model_id.split_once(':') {
|
||||
Some((client_name, model_name)) => {
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use super::ClientConfig;
|
||||
use super::access_token::{is_valid_access_token, set_access_token};
|
||||
use crate::config::Config;
|
||||
use crate::config::paths;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
@@ -178,13 +178,13 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
|
||||
}
|
||||
|
||||
pub fn load_oauth_tokens(client_name: &str) -> Option<OAuthTokens> {
|
||||
let path = Config::token_file(client_name);
|
||||
let path = paths::token_file(client_name);
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
serde_json::from_str(&content).ok()
|
||||
}
|
||||
|
||||
fn save_oauth_tokens(client_name: &str, tokens: &OAuthTokens) -> Result<()> {
|
||||
let path = Config::token_file(client_name);
|
||||
let path = paths::token_file(client_name);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
+13
-10
@@ -6,6 +6,7 @@ use crate::{
|
||||
function::{Functions, run_llm_function},
|
||||
};
|
||||
|
||||
use crate::config::paths;
|
||||
use crate::config::prompts::{
|
||||
DEFAULT_SPAWN_INSTRUCTIONS, DEFAULT_TEAMMATE_INSTRUCTIONS, DEFAULT_TODO_INSTRUCTIONS,
|
||||
DEFAULT_USER_INTERACTION_INSTRUCTIONS,
|
||||
@@ -47,7 +48,7 @@ impl Agent {
|
||||
pub fn install_builtin_agents() -> Result<()> {
|
||||
info!(
|
||||
"Installing built-in agents in {}",
|
||||
Config::agents_data_dir().display()
|
||||
paths::agents_data_dir().display()
|
||||
);
|
||||
|
||||
for file in AgentAssets::iter() {
|
||||
@@ -56,7 +57,7 @@ impl Agent {
|
||||
let embedded_file = AgentAssets::get(&file)
|
||||
.ok_or_else(|| anyhow!("Failed to load embedded agent file: {}", file.as_ref()))?;
|
||||
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||
let file_path = Config::agents_data_dir().join(file.as_ref());
|
||||
let file_path = paths::agents_data_dir().join(file.as_ref());
|
||||
let file_extension = file_path
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
@@ -92,10 +93,10 @@ impl Agent {
|
||||
name: &str,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<Self> {
|
||||
let agent_data_dir = Config::agent_data_dir(name);
|
||||
let agent_data_dir = paths::agent_data_dir(name);
|
||||
let loaders = config.read().document_loaders.clone();
|
||||
let rag_path = Config::agent_rag_file(name, DEFAULT_AGENT_NAME);
|
||||
let config_path = Config::agent_config_file(name);
|
||||
let rag_path = paths::agent_rag_file(name, DEFAULT_AGENT_NAME);
|
||||
let config_path = paths::agent_config_file(name);
|
||||
let mut agent_config = if config_path.exists() {
|
||||
AgentConfig::load(&config_path)?
|
||||
} else {
|
||||
@@ -138,7 +139,9 @@ impl Agent {
|
||||
let model = {
|
||||
let config = config.read();
|
||||
match agent_config.model_id.as_ref() {
|
||||
Some(model_id) => Model::retrieve_model(&config, model_id, ModelType::Chat)?,
|
||||
Some(model_id) => {
|
||||
Model::retrieve_model(&config.to_app_config(), model_id, ModelType::Chat)?
|
||||
}
|
||||
None => {
|
||||
if agent_config.temperature.is_none() {
|
||||
agent_config.temperature = config.temperature;
|
||||
@@ -295,11 +298,11 @@ impl Agent {
|
||||
let mut config = self.config.clone();
|
||||
config.instructions = self.interpolated_instructions();
|
||||
value["definition"] = json!(config);
|
||||
value["data_dir"] = Config::agent_data_dir(&self.name)
|
||||
value["data_dir"] = paths::agent_data_dir(&self.name)
|
||||
.display()
|
||||
.to_string()
|
||||
.into();
|
||||
value["config_file"] = Config::agent_config_file(&self.name)
|
||||
value["config_file"] = paths::agent_config_file(&self.name)
|
||||
.display()
|
||||
.to_string()
|
||||
.into();
|
||||
@@ -793,7 +796,7 @@ pub struct AgentVariable {
|
||||
}
|
||||
|
||||
pub fn list_agents() -> Vec<String> {
|
||||
let agents_data_dir = Config::agents_data_dir();
|
||||
let agents_data_dir = paths::agents_data_dir();
|
||||
if !agents_data_dir.exists() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -813,7 +816,7 @@ pub fn list_agents() -> Vec<String> {
|
||||
}
|
||||
|
||||
pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option<String>)> {
|
||||
let config_path = Config::agent_config_file(agent_name);
|
||||
let config_path = paths::agent_config_file(agent_name);
|
||||
if !config_path.exists() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Per-request agent runtime: supervisor, inbox, escalation queue,
|
||||
//! optional todo list, shared RAG, and sub-agent wiring.
|
||||
//!
|
||||
//! `AgentRuntime` is present on [`RequestContext`](super::RequestContext)
|
||||
//! only when an agent is active. It holds the agent-specific state
|
||||
//! that today lives as flat fields on `Config` (`supervisor`, `inbox`,
|
||||
//! `root_escalation_queue`, `self_agent_id`, `current_depth`,
|
||||
//! `parent_supervisor`) plus the shared agent RAG (served from
|
||||
//! [`RagCache`](super::rag_cache::RagCache) via `RagKey::Agent`).
|
||||
//!
|
||||
//! # Phase 1 Step 6.5 scope
|
||||
//!
|
||||
//! This file introduces the type scaffolding. Agent activation
|
||||
//! (`Config::use_agent`) still populates the flat fields on
|
||||
//! `Config` directly; the new `AgentRuntime` field on
|
||||
//! `RequestContext` stays empty during the bridge window and gets
|
||||
//! wired up in Step 8 when entry points migrate.
|
||||
//!
|
||||
//! The `todo_list: Option<TodoList>` field is `Option` (not just
|
||||
//! `TodoList::default()`) as an opportunistic tightening during the
|
||||
//! Step 6.5 scaffolding: today's code always allocates a default
|
||||
//! `TodoList` regardless of whether `auto_continue` is enabled. When
|
||||
//! callers migrate to build `AgentRuntime` instances in Step 8, they
|
||||
//! will set `todo_list = Some(...)` only when `spec.auto_continue`
|
||||
//! is true. See `docs/PHASE-1-IMPLEMENTATION-PLAN.md` Step 6.5 for
|
||||
//! the rationale.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::todo::TodoList;
|
||||
use crate::rag::Rag;
|
||||
use crate::supervisor::Supervisor;
|
||||
use crate::supervisor::escalation::EscalationQueue;
|
||||
use crate::supervisor::mailbox::Inbox;
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AgentRuntime {
|
||||
pub rag: Option<Arc<Rag>>,
|
||||
pub supervisor: Arc<RwLock<Supervisor>>,
|
||||
pub inbox: Arc<Inbox>,
|
||||
pub escalation_queue: Arc<EscalationQueue>,
|
||||
pub todo_list: Option<TodoList>,
|
||||
pub self_agent_id: String,
|
||||
pub parent_supervisor: Option<Arc<RwLock<Supervisor>>>,
|
||||
pub current_depth: usize,
|
||||
pub auto_continue_count: usize,
|
||||
}
|
||||
|
||||
impl AgentRuntime {
|
||||
pub fn new(
|
||||
self_agent_id: String,
|
||||
supervisor: Arc<RwLock<Supervisor>>,
|
||||
inbox: Arc<Inbox>,
|
||||
escalation_queue: Arc<EscalationQueue>,
|
||||
) -> Self {
|
||||
Self {
|
||||
rag: None,
|
||||
supervisor,
|
||||
inbox,
|
||||
escalation_queue,
|
||||
todo_list: None,
|
||||
self_agent_id,
|
||||
parent_supervisor: None,
|
||||
current_depth: 0,
|
||||
auto_continue_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_rag(mut self, rag: Option<Arc<Rag>>) -> Self {
|
||||
self.rag = rag;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_todo_list(mut self, todo_list: Option<TodoList>) -> Self {
|
||||
self.todo_list = todo_list;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_parent_supervisor(
|
||||
mut self,
|
||||
parent_supervisor: Option<Arc<RwLock<Supervisor>>>,
|
||||
) -> Self {
|
||||
self.parent_supervisor = parent_supervisor;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_depth(mut self, depth: usize) -> Self {
|
||||
self.current_depth = depth;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
//! Immutable, server-wide application configuration.
|
||||
//!
|
||||
//! `AppConfig` contains the settings loaded from `config.yaml` that are
|
||||
//! global to the Loki process: LLM provider configs, UI preferences, tool
|
||||
//! and MCP settings, RAG defaults, etc.
|
||||
//!
|
||||
//! This is Phase 1, Step 0 of the REST API refactor: the struct is
|
||||
//! introduced alongside the existing [`Config`](super::Config) and is not
|
||||
//! yet wired into the runtime. See `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
//! for the full migration plan.
|
||||
//!
|
||||
//! # Relationship to `Config`
|
||||
//!
|
||||
//! `AppConfig` mirrors the **serialized** fields of [`Config`] — that is,
|
||||
//! every field that is NOT marked `#[serde(skip)]`. The deserialization
|
||||
//! shape is identical so an existing `config.yaml` can be loaded into
|
||||
//! either type without modification.
|
||||
//!
|
||||
//! Runtime-only state (current role, session, agent, supervisor, etc.)
|
||||
//! lives on [`RequestContext`](super::request_context::RequestContext).
|
||||
|
||||
use crate::client::ClientConfig;
|
||||
use crate::render::{MarkdownRender, RenderOptions};
|
||||
use crate::utils::{IS_STDOUT_TERMINAL, NO_COLOR, decode_bin, get_env_name};
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use terminal_colorsaurus::{ColorScheme, QueryOptions, color_scheme};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AppConfig {
|
||||
#[serde(rename(serialize = "model", deserialize = "model"))]
|
||||
#[serde(default)]
|
||||
pub model_id: String,
|
||||
pub temperature: Option<f64>,
|
||||
pub top_p: Option<f64>,
|
||||
|
||||
pub dry_run: bool,
|
||||
pub stream: bool,
|
||||
pub save: bool,
|
||||
pub keybindings: String,
|
||||
pub editor: Option<String>,
|
||||
pub wrap: Option<String>,
|
||||
pub wrap_code: bool,
|
||||
pub(crate) vault_password_file: Option<PathBuf>,
|
||||
|
||||
pub function_calling_support: bool,
|
||||
pub mapping_tools: IndexMap<String, String>,
|
||||
pub enabled_tools: Option<String>,
|
||||
pub visible_tools: Option<Vec<String>>,
|
||||
|
||||
pub mcp_server_support: bool,
|
||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||
pub enabled_mcp_servers: Option<String>,
|
||||
|
||||
pub repl_prelude: Option<String>,
|
||||
pub cmd_prelude: Option<String>,
|
||||
pub agent_session: Option<String>,
|
||||
|
||||
pub save_session: Option<bool>,
|
||||
pub compression_threshold: usize,
|
||||
pub summarization_prompt: Option<String>,
|
||||
pub summary_context_prompt: Option<String>,
|
||||
|
||||
pub rag_embedding_model: Option<String>,
|
||||
pub rag_reranker_model: Option<String>,
|
||||
pub rag_top_k: usize,
|
||||
pub rag_chunk_size: Option<usize>,
|
||||
pub rag_chunk_overlap: Option<usize>,
|
||||
pub rag_template: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub document_loaders: HashMap<String, String>,
|
||||
|
||||
pub highlight: bool,
|
||||
pub theme: Option<String>,
|
||||
pub left_prompt: Option<String>,
|
||||
pub right_prompt: Option<String>,
|
||||
|
||||
pub user_agent: Option<String>,
|
||||
pub save_shell_history: bool,
|
||||
pub sync_models_url: Option<String>,
|
||||
|
||||
pub clients: Vec<ClientConfig>,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model_id: Default::default(),
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
|
||||
dry_run: false,
|
||||
stream: true,
|
||||
save: false,
|
||||
keybindings: "emacs".into(),
|
||||
editor: None,
|
||||
wrap: None,
|
||||
wrap_code: false,
|
||||
vault_password_file: None,
|
||||
|
||||
function_calling_support: true,
|
||||
mapping_tools: Default::default(),
|
||||
enabled_tools: None,
|
||||
visible_tools: None,
|
||||
|
||||
mcp_server_support: true,
|
||||
mapping_mcp_servers: Default::default(),
|
||||
enabled_mcp_servers: None,
|
||||
|
||||
repl_prelude: None,
|
||||
cmd_prelude: None,
|
||||
agent_session: None,
|
||||
|
||||
save_session: None,
|
||||
compression_threshold: 4000,
|
||||
summarization_prompt: None,
|
||||
summary_context_prompt: None,
|
||||
|
||||
rag_embedding_model: None,
|
||||
rag_reranker_model: None,
|
||||
rag_top_k: 5,
|
||||
rag_chunk_size: None,
|
||||
rag_chunk_overlap: None,
|
||||
rag_template: None,
|
||||
|
||||
document_loaders: Default::default(),
|
||||
|
||||
highlight: true,
|
||||
theme: None,
|
||||
left_prompt: None,
|
||||
right_prompt: None,
|
||||
|
||||
user_agent: None,
|
||||
save_shell_history: true,
|
||||
sync_models_url: None,
|
||||
|
||||
clients: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AppConfig {
|
||||
pub fn vault_password_file(&self) -> PathBuf {
|
||||
match &self.vault_password_file {
|
||||
Some(path) => match path.exists() {
|
||||
true => path.clone(),
|
||||
false => gman::config::Config::local_provider_password_file(),
|
||||
},
|
||||
None => gman::config::Config::local_provider_password_file(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor(&self) -> Result<String> {
|
||||
super::EDITOR.get_or_init(move || {
|
||||
let editor = self.editor.clone()
|
||||
.or_else(|| env::var("VISUAL").ok().or_else(|| env::var("EDITOR").ok()))
|
||||
.unwrap_or_else(|| {
|
||||
if cfg!(windows) {
|
||||
"notepad".to_string()
|
||||
} else {
|
||||
"nano".to_string()
|
||||
}
|
||||
});
|
||||
which::which(&editor).ok().map(|_| editor)
|
||||
})
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Editor not found. Please add the `editor` configuration or set the $EDITOR or $VISUAL environment variable."))
|
||||
}
|
||||
|
||||
pub fn sync_models_url(&self) -> String {
|
||||
self.sync_models_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| super::SYNC_MODELS_URL.into())
|
||||
}
|
||||
|
||||
pub fn light_theme(&self) -> bool {
|
||||
matches!(self.theme.as_deref(), Some("light"))
|
||||
}
|
||||
|
||||
pub fn render_options(&self) -> Result<RenderOptions> {
|
||||
let theme = if self.highlight {
|
||||
let theme_mode = if self.light_theme() { "light" } else { "dark" };
|
||||
let theme_filename = format!("{theme_mode}.tmTheme");
|
||||
let theme_path = super::paths::local_path(&theme_filename);
|
||||
if theme_path.exists() {
|
||||
let theme = ThemeSet::get_theme(&theme_path)
|
||||
.with_context(|| format!("Invalid theme at '{}'", theme_path.display()))?;
|
||||
Some(theme)
|
||||
} else {
|
||||
let theme = if self.light_theme() {
|
||||
decode_bin(super::LIGHT_THEME).context("Invalid builtin light theme")?
|
||||
} else {
|
||||
decode_bin(super::DARK_THEME).context("Invalid builtin dark theme")?
|
||||
};
|
||||
Some(theme)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let wrap = if *IS_STDOUT_TERMINAL {
|
||||
self.wrap.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let truecolor = matches!(
|
||||
env::var("COLORTERM").as_ref().map(|v| v.as_str()),
|
||||
Ok("truecolor")
|
||||
);
|
||||
Ok(RenderOptions::new(theme, wrap, self.wrap_code, truecolor))
|
||||
}
|
||||
|
||||
pub fn print_markdown(&self, text: &str) -> Result<()> {
|
||||
if *IS_STDOUT_TERMINAL {
|
||||
let render_options = self.render_options()?;
|
||||
let mut markdown_render = MarkdownRender::init(render_options)?;
|
||||
println!("{}", markdown_render.render(text));
|
||||
} else {
|
||||
println!("{text}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rag_template(&self, embeddings: &str, sources: &str, text: &str) -> String {
|
||||
if embeddings.is_empty() {
|
||||
return text.to_string();
|
||||
}
|
||||
self.rag_template
|
||||
.as_deref()
|
||||
.unwrap_or(super::RAG_TEMPLATE)
|
||||
.replace("__CONTEXT__", embeddings)
|
||||
.replace("__SOURCES__", sources)
|
||||
.replace("__INPUT__", text)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AppConfig {
|
||||
pub fn set_wrap(&mut self, value: &str) -> Result<()> {
|
||||
if value == "no" {
|
||||
self.wrap = None;
|
||||
} else if value == "auto" {
|
||||
self.wrap = Some(value.into());
|
||||
} else {
|
||||
value
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid wrap value"))?;
|
||||
self.wrap = Some(value.into())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup_document_loaders(&mut self) {
|
||||
[("pdf", "pdftotext $1 -"), ("docx", "pandoc --to plain $1")]
|
||||
.into_iter()
|
||||
.for_each(|(k, v)| {
|
||||
let (k, v) = (k.to_string(), v.to_string());
|
||||
self.document_loaders.entry(k).or_insert(v);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn setup_user_agent(&mut self) {
|
||||
if let Some("auto") = self.user_agent.as_deref() {
|
||||
self.user_agent = Some(format!(
|
||||
"{}/{}",
|
||||
env!("CARGO_CRATE_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_envs(&mut self) {
|
||||
if let Ok(v) = env::var(get_env_name("model")) {
|
||||
self.model_id = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<f64>(&get_env_name("temperature")) {
|
||||
self.temperature = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<f64>(&get_env_name("top_p")) {
|
||||
self.top_p = v;
|
||||
}
|
||||
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("dry_run")) {
|
||||
self.dry_run = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("stream")) {
|
||||
self.stream = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("save")) {
|
||||
self.save = v;
|
||||
}
|
||||
if let Ok(v) = env::var(get_env_name("keybindings"))
|
||||
&& v == "vi"
|
||||
{
|
||||
self.keybindings = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("editor")) {
|
||||
self.editor = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("wrap")) {
|
||||
self.wrap = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("wrap_code")) {
|
||||
self.wrap_code = v;
|
||||
}
|
||||
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("function_calling_support")) {
|
||||
self.function_calling_support = v;
|
||||
}
|
||||
if let Ok(v) = env::var(get_env_name("mapping_tools"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
self.mapping_tools = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_tools")) {
|
||||
self.enabled_tools = v;
|
||||
}
|
||||
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
||||
self.mcp_server_support = v;
|
||||
}
|
||||
if let Ok(v) = env::var(get_env_name("mapping_mcp_servers"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
self.mapping_mcp_servers = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_mcp_servers")) {
|
||||
self.enabled_mcp_servers = v;
|
||||
}
|
||||
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("repl_prelude")) {
|
||||
self.repl_prelude = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("cmd_prelude")) {
|
||||
self.cmd_prelude = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("agent_session")) {
|
||||
self.agent_session = v;
|
||||
}
|
||||
|
||||
if let Some(v) = super::read_env_bool(&get_env_name("save_session")) {
|
||||
self.save_session = v;
|
||||
}
|
||||
if let Some(Some(v)) =
|
||||
super::read_env_value::<usize>(&get_env_name("compression_threshold"))
|
||||
{
|
||||
self.compression_threshold = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("summarization_prompt")) {
|
||||
self.summarization_prompt = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("summary_context_prompt")) {
|
||||
self.summary_context_prompt = v;
|
||||
}
|
||||
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_embedding_model")) {
|
||||
self.rag_embedding_model = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_reranker_model")) {
|
||||
self.rag_reranker_model = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_value::<usize>(&get_env_name("rag_top_k")) {
|
||||
self.rag_top_k = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<usize>(&get_env_name("rag_chunk_size")) {
|
||||
self.rag_chunk_size = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<usize>(&get_env_name("rag_chunk_overlap")) {
|
||||
self.rag_chunk_overlap = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("rag_template")) {
|
||||
self.rag_template = v;
|
||||
}
|
||||
|
||||
if let Ok(v) = env::var(get_env_name("document_loaders"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
self.document_loaders = v;
|
||||
}
|
||||
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("highlight")) {
|
||||
self.highlight = v;
|
||||
}
|
||||
if *NO_COLOR {
|
||||
self.highlight = false;
|
||||
}
|
||||
if self.highlight && self.theme.is_none() {
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("theme")) {
|
||||
self.theme = v;
|
||||
} else if *IS_STDOUT_TERMINAL
|
||||
&& let Ok(color_scheme) = color_scheme(QueryOptions::default())
|
||||
{
|
||||
let theme = match color_scheme {
|
||||
ColorScheme::Dark => "dark",
|
||||
ColorScheme::Light => "light",
|
||||
};
|
||||
self.theme = Some(theme.into());
|
||||
}
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("left_prompt")) {
|
||||
self.left_prompt = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("right_prompt")) {
|
||||
self.right_prompt = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("user_agent")) {
|
||||
self.user_agent = v;
|
||||
}
|
||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("save_shell_history")) {
|
||||
self.save_shell_history = v;
|
||||
}
|
||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("sync_models_url")) {
|
||||
self.sync_models_url = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AppConfig {
|
||||
pub fn set_temperature_default(&mut self, value: Option<f64>) {
|
||||
self.temperature = value;
|
||||
}
|
||||
|
||||
pub fn set_top_p_default(&mut self, value: Option<f64>) {
|
||||
self.top_p = value;
|
||||
}
|
||||
|
||||
pub fn set_enabled_tools_default(&mut self, value: Option<String>) {
|
||||
self.enabled_tools = value;
|
||||
}
|
||||
|
||||
pub fn set_enabled_mcp_servers_default(&mut self, value: Option<String>) {
|
||||
self.enabled_mcp_servers = value;
|
||||
}
|
||||
|
||||
pub fn set_save_session_default(&mut self, value: Option<bool>) {
|
||||
self.save_session = value;
|
||||
}
|
||||
|
||||
pub fn set_compression_threshold_default(&mut self, value: Option<usize>) {
|
||||
self.compression_threshold = value.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//! Shared global services for a running Loki process.
|
||||
//!
|
||||
//! `AppState` holds the services that are genuinely process-wide and
|
||||
//! immutable during request handling: the frozen [`AppConfig`], the
|
||||
//! credential [`Vault`](GlobalVault), the [`McpFactory`](super::mcp_factory::McpFactory)
|
||||
//! for MCP subprocess sharing, and the [`RagCache`](super::rag_cache::RagCache)
|
||||
//! for shared RAG instances. It is intended to be wrapped in `Arc`
|
||||
//! and shared across every [`RequestContext`] that a frontend (CLI,
|
||||
//! REPL, API) creates.
|
||||
//!
|
||||
//! This struct deliberately does **not** hold a live `McpRegistry`.
|
||||
//! MCP server processes are scoped to whichever `RoleLike`
|
||||
//! (role/session/agent) is currently active, because each scope may
|
||||
//! demand a different enabled server set. Live MCP processes are
|
||||
//! owned by per-scope
|
||||
//! [`ToolScope`](super::tool_scope::ToolScope)s on the
|
||||
//! [`RequestContext`] and acquired through `McpFactory`.
|
||||
//!
|
||||
//! # Phase 1 scope
|
||||
//!
|
||||
//! This is Phase 1 of the REST API refactor:
|
||||
//!
|
||||
//! * **Step 0** introduced this struct alongside the existing
|
||||
//! [`Config`](super::Config)
|
||||
//! * **Step 6.5** added the `mcp_factory` and `rag_cache` fields
|
||||
//!
|
||||
//! Neither field is wired into the runtime yet — they exist as
|
||||
//! additive scaffolding that Step 8+ will connect when the entry
|
||||
//! points migrate. See `docs/PHASE-1-IMPLEMENTATION-PLAN.md`.
|
||||
|
||||
use super::mcp_factory::McpFactory;
|
||||
use super::rag_cache::RagCache;
|
||||
use crate::config::AppConfig;
|
||||
use crate::vault::GlobalVault;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub config: Arc<AppConfig>,
|
||||
pub vault: GlobalVault,
|
||||
pub mcp_factory: Arc<McpFactory>,
|
||||
pub rag_cache: Arc<RagCache>,
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
//! Transitional conversions between the legacy [`Config`] struct and the
|
||||
//! new [`AppConfig`] + [`RequestContext`] split.
|
||||
//!
|
||||
//! These methods are the bridge that lets Phase 1 migrate incrementally:
|
||||
//! the old `Config` stays intact while new code starts threading
|
||||
//! `AppState` + `RequestContext` through specific callsites. Each
|
||||
//! migrated callsite can go between the two representations using
|
||||
//! [`Config::to_app_config`], [`Config::to_request_context`], and
|
||||
//! [`Config::from_parts`] without the rest of the codebase noticing.
|
||||
//!
|
||||
//! This entire module is scheduled for deletion in Phase 1 Step 10,
|
||||
//! once every callsite has been migrated and the legacy `Config` is
|
||||
//! removed. Keeping the bridge isolated in one file makes that deletion
|
||||
//! a single `rm` + one `mod bridge;` removal.
|
||||
//!
|
||||
//! # Lossy fields
|
||||
//!
|
||||
//! The round-trip `Config → AppConfig + RequestContext → Config` is
|
||||
//! lossy for fields that the architecture plan is deliberately removing
|
||||
//! during the refactor. Specifically:
|
||||
//!
|
||||
//! * **`mcp_registry`** — today's process-wide registry is being replaced
|
||||
//! by per-`ToolScope` `McpRuntime`s managed by a new `McpFactory` on
|
||||
//! `AppState`. Neither `AppConfig` nor `RequestContext` holds an
|
||||
//! `McpRegistry` field, so [`Config::from_parts`] reconstructs it as
|
||||
//! `None`. Callers that need the registry during the migration window
|
||||
//! must keep a reference to the original `Config`.
|
||||
//!
|
||||
//! All other runtime fields (including `model`, `functions`,
|
||||
//! `tool_call_tracker`) round-trip correctly.
|
||||
|
||||
use super::{AppConfig, AppState, Config, RequestContext};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Config {
|
||||
/// Extract the serialized half of the `Config` into a fresh
|
||||
/// [`AppConfig`]. The original `Config` is unchanged.
|
||||
pub fn to_app_config(&self) -> AppConfig {
|
||||
AppConfig {
|
||||
model_id: self.model_id.clone(),
|
||||
temperature: self.temperature,
|
||||
top_p: self.top_p,
|
||||
|
||||
dry_run: self.dry_run,
|
||||
stream: self.stream,
|
||||
save: self.save,
|
||||
keybindings: self.keybindings.clone(),
|
||||
editor: self.editor.clone(),
|
||||
wrap: self.wrap.clone(),
|
||||
wrap_code: self.wrap_code,
|
||||
vault_password_file: self.vault_password_file.clone(),
|
||||
|
||||
function_calling_support: self.function_calling_support,
|
||||
mapping_tools: self.mapping_tools.clone(),
|
||||
enabled_tools: self.enabled_tools.clone(),
|
||||
visible_tools: self.visible_tools.clone(),
|
||||
|
||||
mcp_server_support: self.mcp_server_support,
|
||||
mapping_mcp_servers: self.mapping_mcp_servers.clone(),
|
||||
enabled_mcp_servers: self.enabled_mcp_servers.clone(),
|
||||
|
||||
repl_prelude: self.repl_prelude.clone(),
|
||||
cmd_prelude: self.cmd_prelude.clone(),
|
||||
agent_session: self.agent_session.clone(),
|
||||
|
||||
save_session: self.save_session,
|
||||
compression_threshold: self.compression_threshold,
|
||||
summarization_prompt: self.summarization_prompt.clone(),
|
||||
summary_context_prompt: self.summary_context_prompt.clone(),
|
||||
|
||||
rag_embedding_model: self.rag_embedding_model.clone(),
|
||||
rag_reranker_model: self.rag_reranker_model.clone(),
|
||||
rag_top_k: self.rag_top_k,
|
||||
rag_chunk_size: self.rag_chunk_size,
|
||||
rag_chunk_overlap: self.rag_chunk_overlap,
|
||||
rag_template: self.rag_template.clone(),
|
||||
|
||||
document_loaders: self.document_loaders.clone(),
|
||||
|
||||
highlight: self.highlight,
|
||||
theme: self.theme.clone(),
|
||||
left_prompt: self.left_prompt.clone(),
|
||||
right_prompt: self.right_prompt.clone(),
|
||||
|
||||
user_agent: self.user_agent.clone(),
|
||||
save_shell_history: self.save_shell_history,
|
||||
sync_models_url: self.sync_models_url.clone(),
|
||||
|
||||
clients: self.clients.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the runtime half of the `Config` into a fresh
|
||||
/// [`RequestContext`] that shares the provided [`AppState`].
|
||||
/// The original `Config` is unchanged. See the module docs for
|
||||
/// the single lossy field (`mcp_registry`).
|
||||
pub fn to_request_context(&self, app: Arc<AppState>) -> RequestContext {
|
||||
RequestContext {
|
||||
app,
|
||||
macro_flag: self.macro_flag,
|
||||
info_flag: self.info_flag,
|
||||
working_mode: self.working_mode,
|
||||
model: self.model.clone(),
|
||||
functions: self.functions.clone(),
|
||||
agent_variables: self.agent_variables.clone(),
|
||||
role: self.role.clone(),
|
||||
session: self.session.clone(),
|
||||
rag: self.rag.clone(),
|
||||
agent: self.agent.clone(),
|
||||
last_message: self.last_message.clone(),
|
||||
tool_call_tracker: self.tool_call_tracker.clone(),
|
||||
supervisor: self.supervisor.clone(),
|
||||
parent_supervisor: self.parent_supervisor.clone(),
|
||||
self_agent_id: self.self_agent_id.clone(),
|
||||
current_depth: self.current_depth,
|
||||
inbox: self.inbox.clone(),
|
||||
root_escalation_queue: self.root_escalation_queue.clone(),
|
||||
tool_scope: super::tool_scope::ToolScope::default(),
|
||||
agent_runtime: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct a `Config` by merging the serialized half from an
|
||||
/// [`AppState`] with the runtime half from a [`RequestContext`].
|
||||
///
|
||||
/// The resulting `Config` is a new owned value; neither input is
|
||||
/// consumed. The `mcp_registry` field is reconstructed as `None`
|
||||
/// because no split type owns it — see module docs.
|
||||
pub fn from_parts(app: &AppState, ctx: &RequestContext) -> Config {
|
||||
let cfg = &*app.config;
|
||||
Config {
|
||||
model_id: cfg.model_id.clone(),
|
||||
temperature: cfg.temperature,
|
||||
top_p: cfg.top_p,
|
||||
|
||||
dry_run: cfg.dry_run,
|
||||
stream: cfg.stream,
|
||||
save: cfg.save,
|
||||
keybindings: cfg.keybindings.clone(),
|
||||
editor: cfg.editor.clone(),
|
||||
wrap: cfg.wrap.clone(),
|
||||
wrap_code: cfg.wrap_code,
|
||||
vault_password_file: cfg.vault_password_file.clone(),
|
||||
|
||||
function_calling_support: cfg.function_calling_support,
|
||||
mapping_tools: cfg.mapping_tools.clone(),
|
||||
enabled_tools: cfg.enabled_tools.clone(),
|
||||
visible_tools: cfg.visible_tools.clone(),
|
||||
|
||||
mcp_server_support: cfg.mcp_server_support,
|
||||
mapping_mcp_servers: cfg.mapping_mcp_servers.clone(),
|
||||
enabled_mcp_servers: cfg.enabled_mcp_servers.clone(),
|
||||
|
||||
repl_prelude: cfg.repl_prelude.clone(),
|
||||
cmd_prelude: cfg.cmd_prelude.clone(),
|
||||
agent_session: cfg.agent_session.clone(),
|
||||
|
||||
save_session: cfg.save_session,
|
||||
compression_threshold: cfg.compression_threshold,
|
||||
summarization_prompt: cfg.summarization_prompt.clone(),
|
||||
summary_context_prompt: cfg.summary_context_prompt.clone(),
|
||||
|
||||
rag_embedding_model: cfg.rag_embedding_model.clone(),
|
||||
rag_reranker_model: cfg.rag_reranker_model.clone(),
|
||||
rag_top_k: cfg.rag_top_k,
|
||||
rag_chunk_size: cfg.rag_chunk_size,
|
||||
rag_chunk_overlap: cfg.rag_chunk_overlap,
|
||||
rag_template: cfg.rag_template.clone(),
|
||||
|
||||
document_loaders: cfg.document_loaders.clone(),
|
||||
|
||||
highlight: cfg.highlight,
|
||||
theme: cfg.theme.clone(),
|
||||
left_prompt: cfg.left_prompt.clone(),
|
||||
right_prompt: cfg.right_prompt.clone(),
|
||||
|
||||
user_agent: cfg.user_agent.clone(),
|
||||
save_shell_history: cfg.save_shell_history,
|
||||
sync_models_url: cfg.sync_models_url.clone(),
|
||||
|
||||
clients: cfg.clients.clone(),
|
||||
|
||||
vault: app.vault.clone(),
|
||||
|
||||
macro_flag: ctx.macro_flag,
|
||||
info_flag: ctx.info_flag,
|
||||
agent_variables: ctx.agent_variables.clone(),
|
||||
|
||||
model: ctx.model.clone(),
|
||||
functions: ctx.functions.clone(),
|
||||
mcp_registry: None,
|
||||
working_mode: ctx.working_mode,
|
||||
last_message: ctx.last_message.clone(),
|
||||
|
||||
role: ctx.role.clone(),
|
||||
session: ctx.session.clone(),
|
||||
rag: ctx.rag.clone(),
|
||||
agent: ctx.agent.clone(),
|
||||
tool_call_tracker: ctx.tool_call_tracker.clone(),
|
||||
supervisor: ctx.supervisor.clone(),
|
||||
parent_supervisor: ctx.parent_supervisor.clone(),
|
||||
self_agent_id: ctx.self_agent_id.clone(),
|
||||
current_depth: ctx.current_depth,
|
||||
inbox: ctx.inbox.clone(),
|
||||
root_escalation_queue: ctx.root_escalation_queue.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::mcp_factory::McpFactory;
|
||||
use super::super::rag_cache::RagCache;
|
||||
use super::*;
|
||||
use crate::config::WorkingMode;
|
||||
|
||||
fn build_populated_config() -> Config {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
cfg.model_id = "openai:gpt-4o".into();
|
||||
cfg.temperature = Some(0.42);
|
||||
cfg.top_p = Some(0.9);
|
||||
cfg.dry_run = true;
|
||||
cfg.stream = false;
|
||||
cfg.save = true;
|
||||
cfg.keybindings = "vi".into();
|
||||
cfg.editor = Some("nvim".into());
|
||||
cfg.wrap = Some("80".into());
|
||||
cfg.wrap_code = true;
|
||||
|
||||
cfg.function_calling_support = false;
|
||||
cfg.enabled_tools = Some("fs,web".into());
|
||||
cfg.visible_tools = Some(vec!["fs".into(), "web".into()]);
|
||||
|
||||
cfg.mcp_server_support = false;
|
||||
cfg.enabled_mcp_servers = Some("github,jira".into());
|
||||
|
||||
cfg.repl_prelude = Some("role:explain".into());
|
||||
cfg.cmd_prelude = Some("session:temp".into());
|
||||
cfg.agent_session = Some("shared".into());
|
||||
|
||||
cfg.save_session = Some(true);
|
||||
cfg.compression_threshold = 8000;
|
||||
cfg.summarization_prompt = Some("be terse".into());
|
||||
cfg.summary_context_prompt = Some("recap:".into());
|
||||
|
||||
cfg.rag_embedding_model = Some("openai:text-embedding-3-small".into());
|
||||
cfg.rag_reranker_model = Some("voyage:rerank-2".into());
|
||||
cfg.rag_top_k = 12;
|
||||
cfg.rag_chunk_size = Some(1024);
|
||||
cfg.rag_chunk_overlap = Some(128);
|
||||
cfg.rag_template = Some("custom template".into());
|
||||
|
||||
cfg.highlight = false;
|
||||
cfg.theme = Some("light".into());
|
||||
cfg.left_prompt = Some("> ".into());
|
||||
cfg.right_prompt = Some(" <".into());
|
||||
|
||||
cfg.user_agent = Some("loki-test/1.0".into());
|
||||
cfg.save_shell_history = false;
|
||||
cfg.sync_models_url = Some("https://example.com/models.yaml".into());
|
||||
|
||||
cfg.macro_flag = true;
|
||||
cfg.info_flag = true;
|
||||
cfg.working_mode = WorkingMode::Repl;
|
||||
cfg.current_depth = 3;
|
||||
cfg.self_agent_id = Some("agent_42".into());
|
||||
|
||||
cfg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_app_config_copies_every_serialized_field() {
|
||||
let cfg = build_populated_config();
|
||||
let app = cfg.to_app_config();
|
||||
|
||||
assert_eq!(app.model_id, cfg.model_id);
|
||||
assert_eq!(app.temperature, cfg.temperature);
|
||||
assert_eq!(app.top_p, cfg.top_p);
|
||||
assert_eq!(app.dry_run, cfg.dry_run);
|
||||
assert_eq!(app.stream, cfg.stream);
|
||||
assert_eq!(app.save, cfg.save);
|
||||
assert_eq!(app.keybindings, cfg.keybindings);
|
||||
assert_eq!(app.editor, cfg.editor);
|
||||
assert_eq!(app.wrap, cfg.wrap);
|
||||
assert_eq!(app.wrap_code, cfg.wrap_code);
|
||||
assert_eq!(app.function_calling_support, cfg.function_calling_support);
|
||||
assert_eq!(app.enabled_tools, cfg.enabled_tools);
|
||||
assert_eq!(app.visible_tools, cfg.visible_tools);
|
||||
assert_eq!(app.mcp_server_support, cfg.mcp_server_support);
|
||||
assert_eq!(app.enabled_mcp_servers, cfg.enabled_mcp_servers);
|
||||
assert_eq!(app.repl_prelude, cfg.repl_prelude);
|
||||
assert_eq!(app.cmd_prelude, cfg.cmd_prelude);
|
||||
assert_eq!(app.agent_session, cfg.agent_session);
|
||||
assert_eq!(app.save_session, cfg.save_session);
|
||||
assert_eq!(app.compression_threshold, cfg.compression_threshold);
|
||||
assert_eq!(app.summarization_prompt, cfg.summarization_prompt);
|
||||
assert_eq!(app.summary_context_prompt, cfg.summary_context_prompt);
|
||||
assert_eq!(app.rag_embedding_model, cfg.rag_embedding_model);
|
||||
assert_eq!(app.rag_reranker_model, cfg.rag_reranker_model);
|
||||
assert_eq!(app.rag_top_k, cfg.rag_top_k);
|
||||
assert_eq!(app.rag_chunk_size, cfg.rag_chunk_size);
|
||||
assert_eq!(app.rag_chunk_overlap, cfg.rag_chunk_overlap);
|
||||
assert_eq!(app.rag_template, cfg.rag_template);
|
||||
assert_eq!(app.highlight, cfg.highlight);
|
||||
assert_eq!(app.theme, cfg.theme);
|
||||
assert_eq!(app.left_prompt, cfg.left_prompt);
|
||||
assert_eq!(app.right_prompt, cfg.right_prompt);
|
||||
assert_eq!(app.user_agent, cfg.user_agent);
|
||||
assert_eq!(app.save_shell_history, cfg.save_shell_history);
|
||||
assert_eq!(app.sync_models_url, cfg.sync_models_url);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_request_context_copies_every_runtime_field() {
|
||||
let cfg = build_populated_config();
|
||||
let app_config = Arc::new(cfg.to_app_config());
|
||||
let app_state = Arc::new(AppState {
|
||||
config: app_config,
|
||||
vault: cfg.vault.clone(),
|
||||
mcp_factory: Arc::new(McpFactory::new()),
|
||||
rag_cache: Arc::new(RagCache::new()),
|
||||
});
|
||||
|
||||
let ctx = cfg.to_request_context(app_state);
|
||||
|
||||
assert_eq!(ctx.macro_flag, cfg.macro_flag);
|
||||
assert_eq!(ctx.info_flag, cfg.info_flag);
|
||||
assert_eq!(ctx.working_mode, cfg.working_mode);
|
||||
assert_eq!(ctx.current_depth, cfg.current_depth);
|
||||
assert_eq!(ctx.self_agent_id, cfg.self_agent_id);
|
||||
assert!(ctx.role.is_none());
|
||||
assert!(ctx.session.is_none());
|
||||
assert!(ctx.rag.is_none());
|
||||
assert!(ctx.agent.is_none());
|
||||
assert!(ctx.last_message.is_none());
|
||||
assert!(ctx.supervisor.is_none());
|
||||
assert!(ctx.parent_supervisor.is_none());
|
||||
assert!(ctx.inbox.is_none());
|
||||
assert!(ctx.root_escalation_queue.is_none());
|
||||
assert!(ctx.agent_variables.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_preserves_all_non_lossy_fields() {
|
||||
let original = build_populated_config();
|
||||
|
||||
let app_config = Arc::new(original.to_app_config());
|
||||
let app_state = Arc::new(AppState {
|
||||
config: app_config,
|
||||
vault: original.vault.clone(),
|
||||
mcp_factory: Arc::new(McpFactory::new()),
|
||||
rag_cache: Arc::new(RagCache::new()),
|
||||
});
|
||||
let ctx = original.to_request_context(Arc::clone(&app_state));
|
||||
let rebuilt = Config::from_parts(&app_state, &ctx);
|
||||
|
||||
assert_eq!(rebuilt.model_id, original.model_id);
|
||||
assert_eq!(rebuilt.temperature, original.temperature);
|
||||
assert_eq!(rebuilt.top_p, original.top_p);
|
||||
assert_eq!(rebuilt.dry_run, original.dry_run);
|
||||
assert_eq!(rebuilt.stream, original.stream);
|
||||
assert_eq!(rebuilt.save, original.save);
|
||||
assert_eq!(rebuilt.keybindings, original.keybindings);
|
||||
assert_eq!(rebuilt.editor, original.editor);
|
||||
assert_eq!(rebuilt.wrap, original.wrap);
|
||||
assert_eq!(rebuilt.wrap_code, original.wrap_code);
|
||||
assert_eq!(
|
||||
rebuilt.function_calling_support,
|
||||
original.function_calling_support
|
||||
);
|
||||
assert_eq!(rebuilt.enabled_tools, original.enabled_tools);
|
||||
assert_eq!(rebuilt.visible_tools, original.visible_tools);
|
||||
assert_eq!(rebuilt.mcp_server_support, original.mcp_server_support);
|
||||
assert_eq!(rebuilt.enabled_mcp_servers, original.enabled_mcp_servers);
|
||||
assert_eq!(rebuilt.repl_prelude, original.repl_prelude);
|
||||
assert_eq!(rebuilt.cmd_prelude, original.cmd_prelude);
|
||||
assert_eq!(rebuilt.agent_session, original.agent_session);
|
||||
assert_eq!(rebuilt.save_session, original.save_session);
|
||||
assert_eq!(
|
||||
rebuilt.compression_threshold,
|
||||
original.compression_threshold
|
||||
);
|
||||
assert_eq!(rebuilt.summarization_prompt, original.summarization_prompt);
|
||||
assert_eq!(
|
||||
rebuilt.summary_context_prompt,
|
||||
original.summary_context_prompt
|
||||
);
|
||||
assert_eq!(rebuilt.rag_embedding_model, original.rag_embedding_model);
|
||||
assert_eq!(rebuilt.rag_reranker_model, original.rag_reranker_model);
|
||||
assert_eq!(rebuilt.rag_top_k, original.rag_top_k);
|
||||
assert_eq!(rebuilt.rag_chunk_size, original.rag_chunk_size);
|
||||
assert_eq!(rebuilt.rag_chunk_overlap, original.rag_chunk_overlap);
|
||||
assert_eq!(rebuilt.rag_template, original.rag_template);
|
||||
assert_eq!(rebuilt.highlight, original.highlight);
|
||||
assert_eq!(rebuilt.theme, original.theme);
|
||||
assert_eq!(rebuilt.left_prompt, original.left_prompt);
|
||||
assert_eq!(rebuilt.right_prompt, original.right_prompt);
|
||||
assert_eq!(rebuilt.user_agent, original.user_agent);
|
||||
assert_eq!(rebuilt.save_shell_history, original.save_shell_history);
|
||||
assert_eq!(rebuilt.sync_models_url, original.sync_models_url);
|
||||
|
||||
assert_eq!(rebuilt.macro_flag, original.macro_flag);
|
||||
assert_eq!(rebuilt.info_flag, original.info_flag);
|
||||
assert_eq!(rebuilt.working_mode, original.working_mode);
|
||||
assert_eq!(rebuilt.current_depth, original.current_depth);
|
||||
assert_eq!(rebuilt.self_agent_id, original.self_agent_id);
|
||||
|
||||
// Lossy field: mcp_registry is always reconstructed as None
|
||||
assert!(rebuilt.mcp_registry.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_default_config() {
|
||||
let original = Config::default();
|
||||
|
||||
let app_config = Arc::new(original.to_app_config());
|
||||
let app_state = Arc::new(AppState {
|
||||
config: app_config,
|
||||
vault: original.vault.clone(),
|
||||
mcp_factory: Arc::new(McpFactory::new()),
|
||||
rag_cache: Arc::new(RagCache::new()),
|
||||
});
|
||||
let ctx = original.to_request_context(Arc::clone(&app_state));
|
||||
let rebuilt = Config::from_parts(&app_state, &ctx);
|
||||
|
||||
assert_eq!(rebuilt.model_id, original.model_id);
|
||||
assert_eq!(
|
||||
rebuilt.compression_threshold,
|
||||
original.compression_threshold
|
||||
);
|
||||
assert_eq!(rebuilt.rag_top_k, original.rag_top_k);
|
||||
assert_eq!(rebuilt.highlight, original.highlight);
|
||||
assert_eq!(rebuilt.stream, original.stream);
|
||||
assert_eq!(rebuilt.working_mode, original.working_mode);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::paths;
|
||||
use crate::config::{Config, GlobalConfig, RoleLike, ensure_parent_exists};
|
||||
use crate::repl::{run_repl_command, split_args_text};
|
||||
use crate::utils::{AbortSignal, multiline_text};
|
||||
@@ -63,7 +64,7 @@ impl Macro {
|
||||
pub fn install_macros() -> Result<()> {
|
||||
info!(
|
||||
"Installing built-in macros in {}",
|
||||
Config::macros_dir().display()
|
||||
paths::macros_dir().display()
|
||||
);
|
||||
|
||||
for file in MacroAssets::iter() {
|
||||
@@ -71,7 +72,7 @@ impl Macro {
|
||||
let embedded_file = MacroAssets::get(&file)
|
||||
.ok_or_else(|| anyhow!("Failed to load embedded macro file: {}", file.as_ref()))?;
|
||||
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||
let file_path = Config::macros_dir().join(file.as_ref());
|
||||
let file_path = paths::macros_dir().join(file.as_ref());
|
||||
|
||||
if file_path.exists() {
|
||||
debug!(
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Per-process factory for MCP subprocess handles.
|
||||
//!
|
||||
//! `McpFactory` lives on [`AppState`](super::AppState) and is the
|
||||
//! single entrypoint that scopes use to obtain `Arc<ConnectedServer>`
|
||||
//! handles for MCP tool servers. Multiple scopes requesting the same
|
||||
//! server can (eventually) share a single subprocess via `Arc`
|
||||
//! reference counting.
|
||||
//!
|
||||
//! # Phase 1 Step 6.5 scope
|
||||
//!
|
||||
//! This file introduces the factory scaffolding with a trivial
|
||||
//! implementation:
|
||||
//!
|
||||
//! * `active` — `Mutex<HashMap<McpServerKey, Weak<ConnectedServer>>>`
|
||||
//! for future Arc-based sharing across scopes
|
||||
//! * `acquire` — unimplemented stub for now; will be filled in when
|
||||
//! Step 8 rewrites `use_role` / `use_session` / `use_agent` to
|
||||
//! actually build `ToolScope`s
|
||||
//!
|
||||
//! The full design (idle pool, reaper task, per-server TTL, health
|
||||
//! checks, graceful shutdown) lands in **Phase 5** per
|
||||
//! `docs/PHASE-5-IMPLEMENTATION-PLAN.md`. Phase 1 Step 6.5 ships just
|
||||
//! enough for the type to exist on `AppState` and participate in
|
||||
//! construction / test round-trips.
|
||||
//!
|
||||
//! The key type `McpServerKey` hashes the server name plus its full
|
||||
//! command/args/env so that two scopes requesting an identically-
|
||||
//! configured server share an `Arc`, while two scopes requesting
|
||||
//! differently-configured servers (e.g., different API tokens) get
|
||||
//! independent subprocesses. This is the sharing-vs-isolation property
|
||||
//! described in `docs/REST-API-ARCHITECTURE.md` section 5.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::mcp::ConnectedServer;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct McpServerKey {
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub env: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl McpServerKey {
|
||||
pub fn new(
|
||||
name: impl Into<String>,
|
||||
command: impl Into<String>,
|
||||
args: impl IntoIterator<Item = String>,
|
||||
env: impl IntoIterator<Item = (String, String)>,
|
||||
) -> Self {
|
||||
let mut args: Vec<String> = args.into_iter().collect();
|
||||
args.sort();
|
||||
let mut env: Vec<(String, String)> = env.into_iter().collect();
|
||||
env.sort();
|
||||
Self {
|
||||
name: name.into(),
|
||||
command: command.into(),
|
||||
args,
|
||||
env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct McpFactory {
|
||||
active: Mutex<HashMap<McpServerKey, Weak<ConnectedServer>>>,
|
||||
}
|
||||
|
||||
impl McpFactory {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn active_count(&self) -> usize {
|
||||
let map = self.active.lock();
|
||||
map.values().filter(|w| w.strong_count() > 0).count()
|
||||
}
|
||||
|
||||
pub fn try_get_active(&self, key: &McpServerKey) -> Option<Arc<ConnectedServer>> {
|
||||
let map = self.active.lock();
|
||||
map.get(key).and_then(|weak| weak.upgrade())
|
||||
}
|
||||
|
||||
pub fn insert_active(&self, key: McpServerKey, handle: &Arc<ConnectedServer>) {
|
||||
let mut map = self.active.lock();
|
||||
map.insert(key, Arc::downgrade(handle));
|
||||
}
|
||||
}
|
||||
+57
-275
@@ -1,13 +1,28 @@
|
||||
mod agent;
|
||||
mod agent_runtime;
|
||||
mod app_config;
|
||||
mod app_state;
|
||||
mod bridge;
|
||||
mod input;
|
||||
mod macros;
|
||||
mod mcp_factory;
|
||||
pub(crate) mod paths;
|
||||
mod prompts;
|
||||
mod rag_cache;
|
||||
mod request_context;
|
||||
mod role;
|
||||
mod session;
|
||||
pub(crate) mod todo;
|
||||
mod tool_scope;
|
||||
|
||||
pub use self::agent::{Agent, AgentVariables, complete_agent_variables, list_agents};
|
||||
#[allow(unused_imports)]
|
||||
pub use self::app_config::AppConfig;
|
||||
#[allow(unused_imports)]
|
||||
pub use self::app_state::AppState;
|
||||
pub use self::input::Input;
|
||||
#[allow(unused_imports)]
|
||||
pub use self::request_context::RequestContext;
|
||||
pub use self::role::{
|
||||
CODE_ROLE, CREATE_TITLE_ROLE, EXPLAIN_SHELL_ROLE, Role, RoleLike, SHELL_ROLE,
|
||||
};
|
||||
@@ -39,7 +54,6 @@ use fancy_regex::Regex;
|
||||
use indexmap::IndexMap;
|
||||
use indoc::formatdoc;
|
||||
use inquire::{Confirm, MultiSelect, Select, Text, list_option::ListOption, validator::Validation};
|
||||
use log::LevelFilter;
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@@ -330,7 +344,7 @@ impl Config {
|
||||
log_path: Option<PathBuf>,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<Self> {
|
||||
let config_path = Self::config_file();
|
||||
let config_path = paths::config_file();
|
||||
let (mut config, content) = if !config_path.exists() {
|
||||
match env::var(get_env_name("provider"))
|
||||
.ok()
|
||||
@@ -407,46 +421,6 @@ impl Config {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn config_dir() -> PathBuf {
|
||||
if let Ok(v) = env::var(get_env_name("config_dir")) {
|
||||
PathBuf::from(v)
|
||||
} else if let Ok(v) = env::var("XDG_CONFIG_HOME") {
|
||||
PathBuf::from(v).join(env!("CARGO_CRATE_NAME"))
|
||||
} else {
|
||||
let dir = dirs::config_dir().expect("No user's config directory");
|
||||
dir.join(env!("CARGO_CRATE_NAME"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_path(name: &str) -> PathBuf {
|
||||
Self::config_dir().join(name)
|
||||
}
|
||||
|
||||
pub fn cache_path() -> PathBuf {
|
||||
let base_dir = dirs::cache_dir().unwrap_or_else(env::temp_dir);
|
||||
|
||||
base_dir.join(env!("CARGO_CRATE_NAME"))
|
||||
}
|
||||
|
||||
pub fn oauth_tokens_path() -> PathBuf {
|
||||
Self::cache_path().join("oauth")
|
||||
}
|
||||
|
||||
pub fn token_file(client_name: &str) -> PathBuf {
|
||||
Self::oauth_tokens_path().join(format!("{client_name}_oauth_tokens.json"))
|
||||
}
|
||||
|
||||
pub fn log_path() -> PathBuf {
|
||||
Config::cache_path().join(format!("{}.log", env!("CARGO_CRATE_NAME")))
|
||||
}
|
||||
|
||||
pub fn config_file() -> PathBuf {
|
||||
match env::var(get_env_name("config_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::local_path(CONFIG_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vault_password_file(&self) -> PathBuf {
|
||||
match &self.vault_password_file {
|
||||
Some(path) => match path.exists() {
|
||||
@@ -457,42 +431,13 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn roles_dir() -> PathBuf {
|
||||
match env::var(get_env_name("roles_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::local_path(ROLES_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn role_file(name: &str) -> PathBuf {
|
||||
Self::roles_dir().join(format!("{name}.md"))
|
||||
}
|
||||
|
||||
pub fn macros_dir() -> PathBuf {
|
||||
match env::var(get_env_name("macros_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::local_path(MACROS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn macro_file(name: &str) -> PathBuf {
|
||||
Self::macros_dir().join(format!("{name}.yaml"))
|
||||
}
|
||||
|
||||
pub fn env_file() -> PathBuf {
|
||||
match env::var(get_env_name("env_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::local_path(ENV_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn messages_file(&self) -> PathBuf {
|
||||
match &self.agent {
|
||||
None => match env::var(get_env_name("messages_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::cache_path().join(MESSAGES_FILE_NAME),
|
||||
Err(_) => paths::cache_path().join(MESSAGES_FILE_NAME),
|
||||
},
|
||||
Some(agent) => Self::cache_path()
|
||||
Some(agent) => paths::cache_path()
|
||||
.join(AGENTS_DIR_NAME)
|
||||
.join(agent.name())
|
||||
.join(MESSAGES_FILE_NAME),
|
||||
@@ -503,46 +448,12 @@ impl Config {
|
||||
match &self.agent {
|
||||
None => match env::var(get_env_name("sessions_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::local_path(SESSIONS_DIR_NAME),
|
||||
Err(_) => paths::local_path(SESSIONS_DIR_NAME),
|
||||
},
|
||||
Some(agent) => Self::agent_data_dir(agent.name()).join(SESSIONS_DIR_NAME),
|
||||
Some(agent) => paths::agent_data_dir(agent.name()).join(SESSIONS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rags_dir() -> PathBuf {
|
||||
match env::var(get_env_name("rags_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::local_path(RAGS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn functions_dir() -> PathBuf {
|
||||
match env::var(get_env_name("functions_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::local_path(FUNCTIONS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn functions_bin_dir() -> PathBuf {
|
||||
Self::functions_dir().join(FUNCTIONS_BIN_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn mcp_config_file() -> PathBuf {
|
||||
Self::functions_dir().join(MCP_FILE_NAME)
|
||||
}
|
||||
|
||||
pub fn global_tools_dir() -> PathBuf {
|
||||
Self::functions_dir().join(GLOBAL_TOOLS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn global_utils_dir() -> PathBuf {
|
||||
Self::functions_dir().join(GLOBAL_TOOLS_UTILS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn bash_prompt_utils_file() -> PathBuf {
|
||||
Self::global_utils_dir().join(BASH_PROMPT_UTILS_FILE_NAME)
|
||||
}
|
||||
|
||||
pub fn session_file(&self, name: &str) -> PathBuf {
|
||||
match name.split_once("/") {
|
||||
Some((dir, name)) => self.sessions_dir().join(dir).join(format!("{name}.yaml")),
|
||||
@@ -552,58 +463,11 @@ impl Config {
|
||||
|
||||
pub fn rag_file(&self, name: &str) -> PathBuf {
|
||||
match &self.agent {
|
||||
Some(agent) => Self::agent_rag_file(agent.name(), name),
|
||||
None => Self::rags_dir().join(format!("{name}.yaml")),
|
||||
Some(agent) => paths::agent_rag_file(agent.name(), name),
|
||||
None => paths::rags_dir().join(format!("{name}.yaml")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agents_data_dir() -> PathBuf {
|
||||
Self::local_path(AGENTS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn agent_data_dir(name: &str) -> PathBuf {
|
||||
match env::var(format!("{}_DATA_DIR", normalize_env_name(name))) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::agents_data_dir().join(name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_config_file(name: &str) -> PathBuf {
|
||||
match env::var(format!("{}_CONFIG_FILE", normalize_env_name(name))) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => Self::agent_data_dir(name).join(CONFIG_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_bin_dir(name: &str) -> PathBuf {
|
||||
Self::agent_data_dir(name).join(FUNCTIONS_BIN_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn agent_rag_file(agent_name: &str, rag_name: &str) -> PathBuf {
|
||||
Self::agent_data_dir(agent_name).join(format!("{rag_name}.yaml"))
|
||||
}
|
||||
|
||||
pub fn agent_functions_file(name: &str) -> Result<PathBuf> {
|
||||
let allowed = ["tools.sh", "tools.py", "tools.ts", "tools.js"];
|
||||
|
||||
for entry in read_dir(Self::agent_data_dir(name))? {
|
||||
let entry = entry?;
|
||||
if let Some(file) = entry.file_name().to_str()
|
||||
&& allowed.contains(&file)
|
||||
{
|
||||
return Ok(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"No tools script found in agent functions directory"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn models_override_file() -> PathBuf {
|
||||
Self::local_path("models-override.yaml")
|
||||
}
|
||||
|
||||
pub fn state(&self) -> StateFlags {
|
||||
let mut flags = StateFlags::empty();
|
||||
if let Some(session) = &self.session {
|
||||
@@ -627,23 +491,8 @@ impl Config {
|
||||
flags
|
||||
}
|
||||
|
||||
pub fn log_config() -> Result<(LevelFilter, Option<PathBuf>)> {
|
||||
let log_level = env::var(get_env_name("log_level"))
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(match cfg!(debug_assertions) {
|
||||
true => LevelFilter::Debug,
|
||||
false => LevelFilter::Info,
|
||||
});
|
||||
let log_path = match env::var(get_env_name("log_path")) {
|
||||
Ok(v) => Some(PathBuf::from(v)),
|
||||
Err(_) => Some(Config::log_path()),
|
||||
};
|
||||
Ok((log_level, log_path))
|
||||
}
|
||||
|
||||
pub fn edit_config(&self) -> Result<()> {
|
||||
let config_path = Self::config_file();
|
||||
let config_path = paths::config_file();
|
||||
let editor = self.editor()?;
|
||||
edit_file(&editor, &config_path)?;
|
||||
println!(
|
||||
@@ -773,21 +622,21 @@ impl Config {
|
||||
("wrap_code", self.wrap_code.to_string()),
|
||||
("highlight", self.highlight.to_string()),
|
||||
("theme", format_option_value(&self.theme)),
|
||||
("config_file", display_path(&Self::config_file())),
|
||||
("env_file", display_path(&Self::env_file())),
|
||||
("agents_dir", display_path(&Self::agents_data_dir())),
|
||||
("roles_dir", display_path(&Self::roles_dir())),
|
||||
("config_file", display_path(&paths::config_file())),
|
||||
("env_file", display_path(&paths::env_file())),
|
||||
("agents_dir", display_path(&paths::agents_data_dir())),
|
||||
("roles_dir", display_path(&paths::roles_dir())),
|
||||
("sessions_dir", display_path(&self.sessions_dir())),
|
||||
("rags_dir", display_path(&Self::rags_dir())),
|
||||
("macros_dir", display_path(&Self::macros_dir())),
|
||||
("functions_dir", display_path(&Self::functions_dir())),
|
||||
("rags_dir", display_path(&paths::rags_dir())),
|
||||
("macros_dir", display_path(&paths::macros_dir())),
|
||||
("functions_dir", display_path(&paths::functions_dir())),
|
||||
("messages_file", display_path(&self.messages_file())),
|
||||
(
|
||||
"vault_password_file",
|
||||
display_path(&self.vault_password_file()),
|
||||
),
|
||||
];
|
||||
if let Ok((_, Some(log_path))) = Self::log_config() {
|
||||
if let Ok((_, Some(log_path))) = paths::log_config() {
|
||||
items.push(("log_path", display_path(&log_path)));
|
||||
}
|
||||
let output = items
|
||||
@@ -941,11 +790,11 @@ impl Config {
|
||||
|
||||
pub fn delete(config: &GlobalConfig, kind: &str) -> Result<()> {
|
||||
let (dir, file_ext) = match kind {
|
||||
"role" => (Self::roles_dir(), Some(".md")),
|
||||
"role" => (paths::roles_dir(), Some(".md")),
|
||||
"session" => (config.read().sessions_dir(), Some(".yaml")),
|
||||
"rag" => (Self::rags_dir(), Some(".yaml")),
|
||||
"macro" => (Self::macros_dir(), Some(".yaml")),
|
||||
"agent-data" => (Self::agents_data_dir(), None),
|
||||
"rag" => (paths::rags_dir(), Some(".yaml")),
|
||||
"macro" => (paths::macros_dir(), Some(".yaml")),
|
||||
"agent-data" => (paths::agents_data_dir(), None),
|
||||
_ => bail!("Unknown kind '{kind}'"),
|
||||
};
|
||||
let names = match read_dir(&dir) {
|
||||
@@ -1054,7 +903,7 @@ impl Config {
|
||||
|
||||
pub fn set_rag_reranker_model(config: &GlobalConfig, value: Option<String>) -> Result<()> {
|
||||
if let Some(id) = &value {
|
||||
Model::retrieve_model(&config.read(), id, ModelType::Reranker)?;
|
||||
Model::retrieve_model(&config.read().to_app_config(), id, ModelType::Reranker)?;
|
||||
}
|
||||
let has_rag = config.read().rag.is_some();
|
||||
match has_rag {
|
||||
@@ -1107,7 +956,7 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, model_id: &str) -> Result<()> {
|
||||
let model = Model::retrieve_model(self, model_id, ModelType::Chat)?;
|
||||
let model = Model::retrieve_model(&self.to_app_config(), model_id, ModelType::Chat)?;
|
||||
match self.role_like_mut() {
|
||||
Some(role_like) => role_like.set_model(model),
|
||||
None => {
|
||||
@@ -1215,9 +1064,9 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn retrieve_role(&self, name: &str) -> Result<Role> {
|
||||
let names = Self::list_roles(false);
|
||||
let names = paths::list_roles(false);
|
||||
let mut role = if names.contains(&name.to_string()) {
|
||||
let path = Self::role_file(name);
|
||||
let path = paths::role_file(name);
|
||||
let content = read_to_string(&path)?;
|
||||
Role::new(name, &content)
|
||||
} else {
|
||||
@@ -1227,7 +1076,8 @@ impl Config {
|
||||
match role.model_id() {
|
||||
Some(model_id) => {
|
||||
if current_model.id() != model_id {
|
||||
let model = Model::retrieve_model(self, model_id, ModelType::Chat)?;
|
||||
let model =
|
||||
Model::retrieve_model(&self.to_app_config(), model_id, ModelType::Chat)?;
|
||||
role.set_model(model);
|
||||
} else {
|
||||
role.set_model(current_model);
|
||||
@@ -1282,7 +1132,7 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn upsert_role(&mut self, name: &str) -> Result<()> {
|
||||
let role_path = Self::role_file(name);
|
||||
let role_path = paths::role_file(name);
|
||||
ensure_parent_exists(&role_path)?;
|
||||
let editor = self.editor()?;
|
||||
edit_file(&editor, &role_path)?;
|
||||
@@ -1319,7 +1169,7 @@ impl Config {
|
||||
})
|
||||
.prompt()?;
|
||||
}
|
||||
let role_path = Self::role_file(&role_name);
|
||||
let role_path = paths::role_file(&role_name);
|
||||
if let Some(role) = self.role.as_mut() {
|
||||
role.save(&role_name, &role_path, self.working_mode.is_repl())?;
|
||||
}
|
||||
@@ -1327,32 +1177,6 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_roles(with_builtin: bool) -> Vec<String> {
|
||||
let mut names = HashSet::new();
|
||||
if let Ok(rd) = read_dir(Self::roles_dir()) {
|
||||
for entry in rd.flatten() {
|
||||
if let Some(name) = entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.and_then(|v| v.strip_suffix(".md"))
|
||||
{
|
||||
names.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if with_builtin {
|
||||
names.extend(Role::list_builtin_role_names());
|
||||
}
|
||||
let mut names: Vec<_> = names.into_iter().collect();
|
||||
names.sort_unstable();
|
||||
names
|
||||
}
|
||||
|
||||
pub fn has_role(name: &str) -> bool {
|
||||
let names = Self::list_roles(true);
|
||||
names.contains(&name.to_string())
|
||||
}
|
||||
|
||||
pub async fn use_session_safely(
|
||||
config: &GlobalConfig,
|
||||
session_name: Option<&str>,
|
||||
@@ -1800,23 +1624,6 @@ impl Config {
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
pub fn list_rags() -> Vec<String> {
|
||||
match read_dir(Self::rags_dir()) {
|
||||
Ok(rd) => {
|
||||
let mut names = vec![];
|
||||
for entry in rd.flatten() {
|
||||
let name = entry.file_name();
|
||||
if let Some(name) = name.to_string_lossy().strip_suffix(".yaml") {
|
||||
names.push(name.to_string());
|
||||
}
|
||||
}
|
||||
names.sort_unstable();
|
||||
names
|
||||
}
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rag_template(&self, embeddings: &str, sources: &str, text: &str) -> String {
|
||||
if embeddings.is_empty() {
|
||||
return text.to_string();
|
||||
@@ -1895,7 +1702,7 @@ impl Config {
|
||||
Some(agent) => agent.name(),
|
||||
None => bail!("No agent"),
|
||||
};
|
||||
let agent_config_path = Config::agent_config_file(agent_name);
|
||||
let agent_config_path = paths::agent_config_file(agent_name);
|
||||
ensure_parent_exists(&agent_config_path)?;
|
||||
if !agent_config_path.exists() {
|
||||
std::fs::write(
|
||||
@@ -1938,23 +1745,14 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_macros() -> Vec<String> {
|
||||
list_file_names(Self::macros_dir(), ".yaml")
|
||||
}
|
||||
|
||||
pub fn load_macro(name: &str) -> Result<Macro> {
|
||||
let path = Self::macro_file(name);
|
||||
let path = paths::macro_file(name);
|
||||
let err = || format!("Failed to load macro '{name}' at '{}'", path.display());
|
||||
let content = read_to_string(&path).with_context(err)?;
|
||||
let value: Macro = serde_yaml::from_str(&content).with_context(err)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn has_macro(name: &str) -> bool {
|
||||
let names = Self::list_macros();
|
||||
names.contains(&name.to_string())
|
||||
}
|
||||
|
||||
pub fn new_macro(&mut self, name: &str) -> Result<()> {
|
||||
if self.macro_flag {
|
||||
bail!("No macro");
|
||||
@@ -1963,7 +1761,7 @@ impl Config {
|
||||
.with_default(true)
|
||||
.prompt()?;
|
||||
if ans {
|
||||
let macro_path = Self::macro_file(name);
|
||||
let macro_path = paths::macro_file(name);
|
||||
ensure_parent_exists(¯o_path)?;
|
||||
let editor = self.editor()?;
|
||||
edit_file(&editor, ¯o_path)?;
|
||||
@@ -2261,8 +2059,8 @@ impl Config {
|
||||
let filter = args.last().unwrap_or(&"");
|
||||
if args.len() == 1 {
|
||||
values = match cmd {
|
||||
".role" => map_completion_values(Self::list_roles(true)),
|
||||
".model" => list_models(self, ModelType::Chat)
|
||||
".role" => map_completion_values(paths::list_roles(true)),
|
||||
".model" => list_models(&self.to_app_config(), ModelType::Chat)
|
||||
.into_iter()
|
||||
.map(|v| (v.id(), Some(v.description())))
|
||||
.collect(),
|
||||
@@ -2279,9 +2077,9 @@ impl Config {
|
||||
map_completion_values(self.list_sessions())
|
||||
}
|
||||
}
|
||||
".rag" => map_completion_values(Self::list_rags()),
|
||||
".rag" => map_completion_values(paths::list_rags()),
|
||||
".agent" => map_completion_values(list_agents()),
|
||||
".macro" => map_completion_values(Self::list_macros()),
|
||||
".macro" => map_completion_values(paths::list_macros()),
|
||||
".starter" => match &self.agent {
|
||||
Some(agent) => agent
|
||||
.conversation_starters()
|
||||
@@ -2389,7 +2187,7 @@ impl Config {
|
||||
};
|
||||
complete_option_bool(save_session)
|
||||
}
|
||||
"rag_reranker_model" => list_models(self, ModelType::Reranker)
|
||||
"rag_reranker_model" => list_models(&self.to_app_config(), ModelType::Reranker)
|
||||
.iter()
|
||||
.map(|v| v.id())
|
||||
.collect(),
|
||||
@@ -2407,7 +2205,7 @@ impl Config {
|
||||
.collect();
|
||||
} else if cmd == ".agent" {
|
||||
if args.len() == 2 {
|
||||
let dir = Self::agent_data_dir(args[0]).join(SESSIONS_DIR_NAME);
|
||||
let dir = paths::agent_data_dir(args[0]).join(SESSIONS_DIR_NAME);
|
||||
values = list_file_names(dir, ".yaml")
|
||||
.into_iter()
|
||||
.map(|v| (v, None))
|
||||
@@ -2438,7 +2236,7 @@ impl Config {
|
||||
let models_override_data =
|
||||
serde_yaml::to_string(&models_override).with_context(|| "Failed to serde {}")?;
|
||||
|
||||
let model_override_path = Self::models_override_file();
|
||||
let model_override_path = paths::models_override_file();
|
||||
ensure_parent_exists(&model_override_path)?;
|
||||
std::fs::write(&model_override_path, models_override_data)
|
||||
.with_context(|| format!("Failed to write to '{}'", model_override_path.display()))?;
|
||||
@@ -2446,22 +2244,6 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn local_models_override() -> Result<Vec<ProviderModels>> {
|
||||
let model_override_path = Self::models_override_file();
|
||||
let err = || {
|
||||
format!(
|
||||
"Failed to load models at '{}'",
|
||||
model_override_path.display()
|
||||
)
|
||||
};
|
||||
let content = read_to_string(&model_override_path).with_context(err)?;
|
||||
let models_override: ModelsOverride = serde_yaml::from_str(&content).with_context(err)?;
|
||||
if models_override.version != env!("CARGO_PKG_VERSION") {
|
||||
bail!("Incompatible version")
|
||||
}
|
||||
Ok(models_override.list)
|
||||
}
|
||||
|
||||
pub fn light_theme(&self) -> bool {
|
||||
matches!(self.theme.as_deref(), Some("light"))
|
||||
}
|
||||
@@ -2470,7 +2252,7 @@ impl Config {
|
||||
let theme = if self.highlight {
|
||||
let theme_mode = if self.light_theme() { "light" } else { "dark" };
|
||||
let theme_filename = format!("{theme_mode}.tmTheme");
|
||||
let theme_path = Self::local_path(&theme_filename);
|
||||
let theme_path = paths::local_path(&theme_filename);
|
||||
if theme_path.exists() {
|
||||
let theme = ThemeSet::get_theme(&theme_path)
|
||||
.with_context(|| format!("Invalid theme at '{}'", theme_path.display()))?;
|
||||
@@ -2993,7 +2775,7 @@ impl Config {
|
||||
fn setup_model(&mut self) -> Result<()> {
|
||||
let mut model_id = self.model_id.clone();
|
||||
if model_id.is_empty() {
|
||||
let models = list_models(self, ModelType::Chat);
|
||||
let models = list_models(&self.to_app_config(), ModelType::Chat);
|
||||
if models.is_empty() {
|
||||
bail!("No available model");
|
||||
}
|
||||
@@ -3026,7 +2808,7 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn load_env_file() -> Result<()> {
|
||||
let env_file_path = Config::env_file();
|
||||
let env_file_path = paths::env_file();
|
||||
let contents = match read_to_string(&env_file_path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Ok(()),
|
||||
@@ -3239,7 +3021,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_option_value<T>(value: &Option<T>) -> String
|
||||
pub(super) fn format_option_value<T>(value: &Option<T>) -> String
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
//! Static path and filesystem-lookup helpers that used to live as
|
||||
//! associated functions on [`Config`](super::Config).
|
||||
//!
|
||||
//! None of these functions depend on any `Config` instance data — they
|
||||
//! compute paths from environment variables, XDG directories, or the
|
||||
//! crate constant for the config root. Moving them here is Phase 1
|
||||
//! Step 2 of the REST API refactor: the `Config` struct is shedding
|
||||
//! anything that doesn't actually need per-instance state so the
|
||||
//! eventual split into `AppConfig` + `RequestContext` has a clean
|
||||
//! division line.
|
||||
//!
|
||||
//! # Compatibility shim during migration
|
||||
//!
|
||||
//! The existing associated functions on `Config` (e.g.,
|
||||
//! `Config::config_dir()`) are kept as `#[deprecated]` forwarders that
|
||||
//! call into this module. Callers are migrated module-by-module; when
|
||||
//! the last caller is updated, the forwarders are deleted in a later
|
||||
//! sub-step of Step 2.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::role::Role;
|
||||
use super::{
|
||||
AGENTS_DIR_NAME, BASH_PROMPT_UTILS_FILE_NAME, CONFIG_FILE_NAME, ENV_FILE_NAME,
|
||||
FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME, GLOBAL_TOOLS_UTILS_DIR_NAME,
|
||||
MACROS_DIR_NAME, MCP_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME,
|
||||
};
|
||||
use crate::client::ProviderModels;
|
||||
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use log::LevelFilter;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{read_dir, read_to_string};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn config_dir() -> PathBuf {
|
||||
if let Ok(v) = env::var(get_env_name("config_dir")) {
|
||||
PathBuf::from(v)
|
||||
} else if let Ok(v) = env::var("XDG_CONFIG_HOME") {
|
||||
PathBuf::from(v).join(env!("CARGO_CRATE_NAME"))
|
||||
} else {
|
||||
let dir = dirs::config_dir().expect("No user's config directory");
|
||||
dir.join(env!("CARGO_CRATE_NAME"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_path(name: &str) -> PathBuf {
|
||||
config_dir().join(name)
|
||||
}
|
||||
|
||||
pub fn cache_path() -> PathBuf {
|
||||
let base_dir = dirs::cache_dir().unwrap_or_else(env::temp_dir);
|
||||
base_dir.join(env!("CARGO_CRATE_NAME"))
|
||||
}
|
||||
|
||||
pub fn oauth_tokens_path() -> PathBuf {
|
||||
cache_path().join("oauth")
|
||||
}
|
||||
|
||||
pub fn token_file(client_name: &str) -> PathBuf {
|
||||
oauth_tokens_path().join(format!("{client_name}_oauth_tokens.json"))
|
||||
}
|
||||
|
||||
pub fn log_path() -> PathBuf {
|
||||
cache_path().join(format!("{}.log", env!("CARGO_CRATE_NAME")))
|
||||
}
|
||||
|
||||
pub fn config_file() -> PathBuf {
|
||||
match env::var(get_env_name("config_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(CONFIG_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn roles_dir() -> PathBuf {
|
||||
match env::var(get_env_name("roles_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(ROLES_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn role_file(name: &str) -> PathBuf {
|
||||
roles_dir().join(format!("{name}.md"))
|
||||
}
|
||||
|
||||
pub fn macros_dir() -> PathBuf {
|
||||
match env::var(get_env_name("macros_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(MACROS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn macro_file(name: &str) -> PathBuf {
|
||||
macros_dir().join(format!("{name}.yaml"))
|
||||
}
|
||||
|
||||
pub fn env_file() -> PathBuf {
|
||||
match env::var(get_env_name("env_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(ENV_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rags_dir() -> PathBuf {
|
||||
match env::var(get_env_name("rags_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(RAGS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn functions_dir() -> PathBuf {
|
||||
match env::var(get_env_name("functions_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(FUNCTIONS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn functions_bin_dir() -> PathBuf {
|
||||
functions_dir().join(FUNCTIONS_BIN_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn mcp_config_file() -> PathBuf {
|
||||
functions_dir().join(MCP_FILE_NAME)
|
||||
}
|
||||
|
||||
pub fn global_tools_dir() -> PathBuf {
|
||||
functions_dir().join(GLOBAL_TOOLS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn global_utils_dir() -> PathBuf {
|
||||
functions_dir().join(GLOBAL_TOOLS_UTILS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn bash_prompt_utils_file() -> PathBuf {
|
||||
global_utils_dir().join(BASH_PROMPT_UTILS_FILE_NAME)
|
||||
}
|
||||
|
||||
pub fn agents_data_dir() -> PathBuf {
|
||||
local_path(AGENTS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn agent_data_dir(name: &str) -> PathBuf {
|
||||
match env::var(format!("{}_DATA_DIR", normalize_env_name(name))) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => agents_data_dir().join(name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_config_file(name: &str) -> PathBuf {
|
||||
match env::var(format!("{}_CONFIG_FILE", normalize_env_name(name))) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => agent_data_dir(name).join(CONFIG_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_bin_dir(name: &str) -> PathBuf {
|
||||
agent_data_dir(name).join(FUNCTIONS_BIN_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn agent_rag_file(agent_name: &str, rag_name: &str) -> PathBuf {
|
||||
agent_data_dir(agent_name).join(format!("{rag_name}.yaml"))
|
||||
}
|
||||
|
||||
pub fn agent_functions_file(name: &str) -> Result<PathBuf> {
|
||||
let allowed = ["tools.sh", "tools.py", "tools.ts", "tools.js"];
|
||||
|
||||
for entry in read_dir(agent_data_dir(name))? {
|
||||
let entry = entry?;
|
||||
if let Some(file) = entry.file_name().to_str()
|
||||
&& allowed.contains(&file)
|
||||
{
|
||||
return Ok(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"No tools script found in agent functions directory"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn models_override_file() -> PathBuf {
|
||||
local_path("models-override.yaml")
|
||||
}
|
||||
|
||||
pub fn log_config() -> Result<(LevelFilter, Option<PathBuf>)> {
|
||||
let log_level = env::var(get_env_name("log_level"))
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(match cfg!(debug_assertions) {
|
||||
true => LevelFilter::Debug,
|
||||
false => LevelFilter::Info,
|
||||
});
|
||||
let resolved_log_path = match env::var(get_env_name("log_path")) {
|
||||
Ok(v) => Some(PathBuf::from(v)),
|
||||
Err(_) => Some(log_path()),
|
||||
};
|
||||
Ok((log_level, resolved_log_path))
|
||||
}
|
||||
|
||||
pub fn list_roles(with_builtin: bool) -> Vec<String> {
|
||||
let mut names = HashSet::new();
|
||||
if let Ok(rd) = read_dir(roles_dir()) {
|
||||
for entry in rd.flatten() {
|
||||
if let Some(name) = entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.and_then(|v| v.strip_suffix(".md"))
|
||||
{
|
||||
names.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if with_builtin {
|
||||
names.extend(Role::list_builtin_role_names());
|
||||
}
|
||||
let mut names: Vec<_> = names.into_iter().collect();
|
||||
names.sort_unstable();
|
||||
names
|
||||
}
|
||||
|
||||
pub fn has_role(name: &str) -> bool {
|
||||
let names = list_roles(true);
|
||||
names.contains(&name.to_string())
|
||||
}
|
||||
|
||||
pub fn list_rags() -> Vec<String> {
|
||||
match read_dir(rags_dir()) {
|
||||
Ok(rd) => {
|
||||
let mut names = vec![];
|
||||
for entry in rd.flatten() {
|
||||
let name = entry.file_name();
|
||||
if let Some(name) = name.to_string_lossy().strip_suffix(".yaml") {
|
||||
names.push(name.to_string());
|
||||
}
|
||||
}
|
||||
names.sort_unstable();
|
||||
names
|
||||
}
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_macros() -> Vec<String> {
|
||||
list_file_names(macros_dir(), ".yaml")
|
||||
}
|
||||
|
||||
pub fn has_macro(name: &str) -> bool {
|
||||
let names = list_macros();
|
||||
names.contains(&name.to_string())
|
||||
}
|
||||
|
||||
pub fn local_models_override() -> Result<Vec<ProviderModels>> {
|
||||
let model_override_path = models_override_file();
|
||||
let err = || {
|
||||
format!(
|
||||
"Failed to load models at '{}'",
|
||||
model_override_path.display()
|
||||
)
|
||||
};
|
||||
let content = read_to_string(&model_override_path).with_context(err)?;
|
||||
let models_override: ModelsOverride = serde_yaml::from_str(&content).with_context(err)?;
|
||||
if models_override.version != env!("CARGO_PKG_VERSION") {
|
||||
bail!("Incompatible version")
|
||||
}
|
||||
Ok(models_override.list)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Per-process RAG instance cache with weak-reference sharing.
|
||||
//!
|
||||
//! `RagCache` lives on [`AppState`](super::AppState) and serves both
|
||||
//! standalone RAGs (attached via `.rag <name>`) and agent-owned RAGs
|
||||
//! (loaded from an agent's `documents:` field). The cache keys with
|
||||
//! [`RagKey`] so that agent RAGs and standalone RAGs occupy distinct
|
||||
//! namespaces even if they share a name.
|
||||
//!
|
||||
//! Entries are held as `Weak<Rag>` so the cache never keeps a RAG
|
||||
//! alive on its own — once all active scopes drop their `Arc<Rag>`,
|
||||
//! the cache entry becomes unupgradable and the next `load()` falls
|
||||
//! through to a fresh disk read.
|
||||
//!
|
||||
//! # Phase 1 Step 6.5 scope
|
||||
//!
|
||||
//! This file introduces the type scaffolding. Actual cache population
|
||||
//! (i.e., routing `use_rag`, `use_agent`, and sub-agent spawning
|
||||
//! through the cache) is deferred to Step 8 when the entry points get
|
||||
//! rewritten. During the bridge window, `Config.rag` keeps serving
|
||||
//! today's callers via direct `Rag::load` / `Rag::init` calls and
|
||||
//! `RagCache` sits on `AppState` as an unused-but-ready service.
|
||||
//!
|
||||
//! See `docs/REST-API-ARCHITECTURE.md` section 5 ("RAG Cache") for
|
||||
//! the full design including concurrent first-load serialization and
|
||||
//! invalidation semantics.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::rag::Rag;
|
||||
|
||||
use anyhow::Result;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RagKey {
|
||||
Named(String),
|
||||
Agent(String),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RagCache {
|
||||
entries: RwLock<HashMap<RagKey, Weak<Rag>>>,
|
||||
}
|
||||
|
||||
impl RagCache {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn try_get(&self, key: &RagKey) -> Option<Arc<Rag>> {
|
||||
let map = self.entries.read();
|
||||
map.get(key).and_then(|weak| weak.upgrade())
|
||||
}
|
||||
|
||||
pub fn insert(&self, key: RagKey, rag: &Arc<Rag>) {
|
||||
let mut map = self.entries.write();
|
||||
map.insert(key, Arc::downgrade(rag));
|
||||
}
|
||||
|
||||
pub fn invalidate(&self, key: &RagKey) {
|
||||
let mut map = self.entries.write();
|
||||
map.remove(key);
|
||||
}
|
||||
|
||||
pub fn entry_count(&self) -> usize {
|
||||
let map = self.entries.read();
|
||||
map.values().filter(|w| w.strong_count() > 0).count()
|
||||
}
|
||||
|
||||
pub async fn load_with<F, Fut>(&self, key: RagKey, loader: F) -> Result<Arc<Rag>>
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: std::future::Future<Output = Result<Rag>>,
|
||||
{
|
||||
if let Some(existing) = self.try_get(&key) {
|
||||
return Ok(existing);
|
||||
}
|
||||
let rag = loader().await?;
|
||||
let arc = Arc::new(rag);
|
||||
self.insert(key, &arc);
|
||||
Ok(arc)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,7 +85,8 @@ impl Session {
|
||||
let mut session: Self =
|
||||
serde_yaml::from_str(&content).with_context(|| format!("Invalid session {name}"))?;
|
||||
|
||||
session.model = Model::retrieve_model(config, &session.model_id, ModelType::Chat)?;
|
||||
session.model =
|
||||
Model::retrieve_model(&config.to_app_config(), &session.model_id, ModelType::Chat)?;
|
||||
|
||||
if let Some(autoname) = name.strip_prefix("_/") {
|
||||
session.name = TEMP_SESSION_NAME.to_string();
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Per-scope tool runtime: resolved functions + live MCP handles +
|
||||
//! call tracker.
|
||||
//!
|
||||
//! `ToolScope` is the unit of tool availability for a single request.
|
||||
//! Every active `RoleLike` (role, session, agent) conceptually owns one.
|
||||
//! The contents are:
|
||||
//!
|
||||
//! * `functions` — the `Functions` declarations visible to the LLM for
|
||||
//! this scope (global tools + role/session/agent filters applied)
|
||||
//! * `mcp_runtime` — live MCP subprocess handles for the servers this
|
||||
//! scope has enabled, keyed by server name
|
||||
//! * `tool_tracker` — per-scope tool call history for auto-continuation
|
||||
//! and looping detection
|
||||
//!
|
||||
//! # Phase 1 Step 6.5 scope
|
||||
//!
|
||||
//! This file introduces the type scaffolding. Scope transitions
|
||||
//! (`use_role`, `use_session`, `use_agent`, `exit_*`) that actually
|
||||
//! build and swap `ToolScope` instances are deferred to Step 8 when
|
||||
//! the entry points (`main.rs`, `repl/mod.rs`) get rewritten to thread
|
||||
//! `RequestContext` through the pipeline. During the bridge window,
|
||||
//! `Config.functions` / `Config.mcp_registry` keep serving today's
|
||||
//! callers and `ToolScope` sits alongside them on `RequestContext` as
|
||||
//! an unused (but compiling and tested) parallel structure.
|
||||
//!
|
||||
//! The fields mirror the plan in `docs/REST-API-ARCHITECTURE.md`
|
||||
//! section 5 and `docs/PHASE-1-IMPLEMENTATION-PLAN.md` Step 6.5.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::function::{Functions, ToolCallTracker};
|
||||
use crate::mcp::ConnectedServer;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ToolScope {
|
||||
pub functions: Functions,
|
||||
pub mcp_runtime: McpRuntime,
|
||||
pub tool_tracker: ToolCallTracker,
|
||||
}
|
||||
|
||||
impl Default for ToolScope {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
functions: Functions::default(),
|
||||
mcp_runtime: McpRuntime::default(),
|
||||
tool_tracker: ToolCallTracker::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct McpRuntime {
|
||||
pub servers: HashMap<String, Arc<ConnectedServer>>,
|
||||
}
|
||||
|
||||
impl McpRuntime {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.servers.is_empty()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, name: String, handle: Arc<ConnectedServer>) {
|
||||
self.servers.insert(name, handle);
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&Arc<ConnectedServer>> {
|
||||
self.servers.get(name)
|
||||
}
|
||||
|
||||
pub fn server_names(&self) -> Vec<String> {
|
||||
self.servers.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
+34
-33
@@ -3,11 +3,12 @@ pub(crate) mod todo;
|
||||
pub(crate) mod user_interaction;
|
||||
|
||||
use crate::{
|
||||
config::{Agent, Config, GlobalConfig},
|
||||
config::{Agent, GlobalConfig},
|
||||
utils::*,
|
||||
};
|
||||
|
||||
use crate::config::ensure_parent_exists;
|
||||
use crate::config::paths;
|
||||
use crate::mcp::{
|
||||
MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX,
|
||||
MCP_SEARCH_META_FUNCTION_NAME_PREFIX,
|
||||
@@ -199,7 +200,7 @@ impl Functions {
|
||||
fn install_global_tools() -> Result<()> {
|
||||
info!(
|
||||
"Installing global built-in functions in {}",
|
||||
Config::functions_dir().display()
|
||||
paths::functions_dir().display()
|
||||
);
|
||||
|
||||
for file in FunctionAssets::iter() {
|
||||
@@ -213,7 +214,7 @@ impl Functions {
|
||||
anyhow!("Failed to load embedded function file: {}", file.as_ref())
|
||||
})?;
|
||||
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||
let file_path = Config::functions_dir().join(file.as_ref());
|
||||
let file_path = paths::functions_dir().join(file.as_ref());
|
||||
let file_extension = file_path
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
@@ -254,7 +255,7 @@ impl Functions {
|
||||
|
||||
info!(
|
||||
"Building global function binaries in {}",
|
||||
Config::functions_bin_dir().display()
|
||||
paths::functions_bin_dir().display()
|
||||
);
|
||||
Self::build_global_function_binaries(visible_tools, None)?;
|
||||
|
||||
@@ -271,7 +272,7 @@ impl Functions {
|
||||
|
||||
info!(
|
||||
"Building global function binaries required by agent: {name} in {}",
|
||||
Config::functions_bin_dir().display()
|
||||
paths::functions_bin_dir().display()
|
||||
);
|
||||
Self::build_global_function_binaries(global_tools, Some(name))?;
|
||||
tools_declarations
|
||||
@@ -279,7 +280,7 @@ impl Functions {
|
||||
debug!("No global tools found for agent: {}", name);
|
||||
Vec::new()
|
||||
};
|
||||
let agent_script_declarations = match Config::agent_functions_file(name) {
|
||||
let agent_script_declarations = match paths::agent_functions_file(name) {
|
||||
Ok(path) if path.exists() => {
|
||||
info!(
|
||||
"Loading functions script for agent: {name} from {}",
|
||||
@@ -290,7 +291,7 @@ impl Functions {
|
||||
|
||||
info!(
|
||||
"Building function binary for agent: {name} in {}",
|
||||
Config::agent_bin_dir(name).display()
|
||||
paths::agent_bin_dir(name).display()
|
||||
);
|
||||
Self::build_agent_tool_binaries(name)?;
|
||||
script_declarations
|
||||
@@ -453,7 +454,7 @@ impl Functions {
|
||||
fn build_global_tool_declarations(
|
||||
enabled_tools: &[String],
|
||||
) -> Result<Vec<FunctionDeclaration>> {
|
||||
let global_tools_directory = Config::global_tools_dir();
|
||||
let global_tools_directory = paths::global_tools_dir();
|
||||
let mut function_declarations = Vec::new();
|
||||
|
||||
for tool in enabled_tools {
|
||||
@@ -542,7 +543,7 @@ impl Functions {
|
||||
bail!("Unsupported tool file extension: {}", language.as_ref());
|
||||
}
|
||||
|
||||
let tool_path = Config::global_tools_dir().join(tool);
|
||||
let tool_path = paths::global_tools_dir().join(tool);
|
||||
let custom_runtime = extract_shebang_runtime(&tool_path);
|
||||
Self::build_binaries(
|
||||
binary_name,
|
||||
@@ -556,7 +557,7 @@ impl Functions {
|
||||
}
|
||||
|
||||
fn clear_agent_bin_dir(name: &str) -> Result<()> {
|
||||
let agent_bin_directory = Config::agent_bin_dir(name);
|
||||
let agent_bin_directory = paths::agent_bin_dir(name);
|
||||
if !agent_bin_directory.exists() {
|
||||
debug!(
|
||||
"Creating agent bin directory: {}",
|
||||
@@ -575,7 +576,7 @@ impl Functions {
|
||||
}
|
||||
|
||||
fn clear_global_functions_bin_dir() -> Result<()> {
|
||||
let bin_dir = Config::functions_bin_dir();
|
||||
let bin_dir = paths::functions_bin_dir();
|
||||
if !bin_dir.exists() {
|
||||
fs::create_dir_all(&bin_dir)?;
|
||||
}
|
||||
@@ -590,7 +591,7 @@ impl Functions {
|
||||
}
|
||||
|
||||
fn build_agent_tool_binaries(name: &str) -> Result<()> {
|
||||
let tools_file = Config::agent_functions_file(name)?;
|
||||
let tools_file = paths::agent_functions_file(name)?;
|
||||
let language = Language::from(
|
||||
&tools_file
|
||||
.extension()
|
||||
@@ -619,18 +620,18 @@ impl Functions {
|
||||
use native::runtime;
|
||||
let (binary_file, binary_script_file) = match binary_type {
|
||||
BinaryType::Tool(None) => (
|
||||
Config::functions_bin_dir().join(format!("{binary_name}.cmd")),
|
||||
Config::functions_bin_dir()
|
||||
paths::functions_bin_dir().join(format!("{binary_name}.cmd")),
|
||||
paths::functions_bin_dir()
|
||||
.join(format!("run-{binary_name}.{}", language.to_extension())),
|
||||
),
|
||||
BinaryType::Tool(Some(agent_name)) => (
|
||||
Config::agent_bin_dir(agent_name).join(format!("{binary_name}.cmd")),
|
||||
Config::agent_bin_dir(agent_name)
|
||||
paths::agent_bin_dir(agent_name).join(format!("{binary_name}.cmd")),
|
||||
paths::agent_bin_dir(agent_name)
|
||||
.join(format!("run-{binary_name}.{}", language.to_extension())),
|
||||
),
|
||||
BinaryType::Agent => (
|
||||
Config::agent_bin_dir(binary_name).join(format!("{binary_name}.cmd")),
|
||||
Config::agent_bin_dir(binary_name)
|
||||
paths::agent_bin_dir(binary_name).join(format!("{binary_name}.cmd")),
|
||||
paths::agent_bin_dir(binary_name)
|
||||
.join(format!("run-{binary_name}.{}", language.to_extension())),
|
||||
),
|
||||
};
|
||||
@@ -655,7 +656,7 @@ impl Functions {
|
||||
let to_script_path = |p: &str| -> String { p.replace('\\', "/") };
|
||||
let content = match binary_type {
|
||||
BinaryType::Tool(None) => {
|
||||
let root_dir = Config::functions_dir();
|
||||
let root_dir = paths::functions_dir();
|
||||
let tool_path = format!(
|
||||
"{}/{binary_name}",
|
||||
&Config::global_tools_dir().to_string_lossy()
|
||||
@@ -666,7 +667,7 @@ impl Functions {
|
||||
.replace("{tool_path}", &to_script_path(&tool_path))
|
||||
}
|
||||
BinaryType::Tool(Some(agent_name)) => {
|
||||
let root_dir = Config::agent_data_dir(agent_name);
|
||||
let root_dir = paths::agent_data_dir(agent_name);
|
||||
let tool_path = format!(
|
||||
"{}/{binary_name}",
|
||||
&Config::global_tools_dir().to_string_lossy()
|
||||
@@ -680,12 +681,12 @@ impl Functions {
|
||||
.replace("{agent_name}", binary_name)
|
||||
.replace(
|
||||
"{config_dir}",
|
||||
&to_script_path(&Config::config_dir().to_string_lossy()),
|
||||
&to_script_path(&paths::config_dir().to_string_lossy()),
|
||||
),
|
||||
}
|
||||
.replace(
|
||||
"{prompt_utils_file}",
|
||||
&to_script_path(&Config::bash_prompt_utils_file().to_string_lossy()),
|
||||
&to_script_path(&paths::bash_prompt_utils_file().to_string_lossy()),
|
||||
);
|
||||
if binary_script_file.exists() {
|
||||
fs::remove_file(&binary_script_file)?;
|
||||
@@ -769,11 +770,11 @@ impl Functions {
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
|
||||
let binary_file = match binary_type {
|
||||
BinaryType::Tool(None) => Config::functions_bin_dir().join(binary_name),
|
||||
BinaryType::Tool(None) => paths::functions_bin_dir().join(binary_name),
|
||||
BinaryType::Tool(Some(agent_name)) => {
|
||||
Config::agent_bin_dir(agent_name).join(binary_name)
|
||||
paths::agent_bin_dir(agent_name).join(binary_name)
|
||||
}
|
||||
BinaryType::Agent => Config::agent_bin_dir(binary_name).join(binary_name),
|
||||
BinaryType::Agent => paths::agent_bin_dir(binary_name).join(binary_name),
|
||||
};
|
||||
info!(
|
||||
"Building binary for function: {} ({})",
|
||||
@@ -795,10 +796,10 @@ impl Functions {
|
||||
let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||
let mut content = match binary_type {
|
||||
BinaryType::Tool(None) => {
|
||||
let root_dir = Config::functions_dir();
|
||||
let root_dir = paths::functions_dir();
|
||||
let tool_path = format!(
|
||||
"{}/{binary_name}",
|
||||
&Config::global_tools_dir().to_string_lossy()
|
||||
&paths::global_tools_dir().to_string_lossy()
|
||||
);
|
||||
content_template
|
||||
.replace("{function_name}", binary_name)
|
||||
@@ -806,10 +807,10 @@ impl Functions {
|
||||
.replace("{tool_path}", &tool_path)
|
||||
}
|
||||
BinaryType::Tool(Some(agent_name)) => {
|
||||
let root_dir = Config::agent_data_dir(agent_name);
|
||||
let root_dir = paths::agent_data_dir(agent_name);
|
||||
let tool_path = format!(
|
||||
"{}/{binary_name}",
|
||||
&Config::global_tools_dir().to_string_lossy()
|
||||
&paths::global_tools_dir().to_string_lossy()
|
||||
);
|
||||
content_template
|
||||
.replace("{function_name}", binary_name)
|
||||
@@ -818,11 +819,11 @@ impl Functions {
|
||||
}
|
||||
BinaryType::Agent => content_template
|
||||
.replace("{agent_name}", binary_name)
|
||||
.replace("{config_dir}", &Config::config_dir().to_string_lossy()),
|
||||
.replace("{config_dir}", &paths::config_dir().to_string_lossy()),
|
||||
}
|
||||
.replace(
|
||||
"{prompt_utils_file}",
|
||||
&Config::bash_prompt_utils_file().to_string_lossy(),
|
||||
&paths::bash_prompt_utils_file().to_string_lossy(),
|
||||
);
|
||||
|
||||
if let Some(rt) = custom_runtime
|
||||
@@ -1179,12 +1180,12 @@ pub fn run_llm_function(
|
||||
let mut command_name = cmd_name.clone();
|
||||
if let Some(agent_name) = agent_name {
|
||||
command_name = cmd_args[0].clone();
|
||||
let dir = Config::agent_bin_dir(&agent_name);
|
||||
let dir = paths::agent_bin_dir(&agent_name);
|
||||
if dir.exists() {
|
||||
bin_dirs.push(dir);
|
||||
}
|
||||
} else {
|
||||
bin_dirs.push(Config::functions_bin_dir());
|
||||
bin_dirs.push(paths::functions_bin_dir());
|
||||
}
|
||||
let current_path = env::var("PATH").context("No PATH environment variable")?;
|
||||
let prepend_path = bin_dirs
|
||||
|
||||
@@ -990,7 +990,9 @@ async fn summarize_output(config: &GlobalConfig, agent_name: &str, output: &str)
|
||||
let model = {
|
||||
let cfg = config.read();
|
||||
match summarization_model_id {
|
||||
Some(ref model_id) => Model::retrieve_model(&cfg, model_id, ModelType::Chat)?,
|
||||
Some(ref model_id) => {
|
||||
Model::retrieve_model(&cfg.to_app_config(), model_id, ModelType::Chat)?
|
||||
}
|
||||
None => cfg.current_model().clone(),
|
||||
}
|
||||
};
|
||||
|
||||
+6
-5
@@ -18,6 +18,7 @@ extern crate log;
|
||||
use crate::client::{
|
||||
ModelType, call_chat_completions, call_chat_completions_streaming, list_models, oauth,
|
||||
};
|
||||
use crate::config::paths;
|
||||
use crate::config::{
|
||||
Agent, CODE_ROLE, Config, EXPLAIN_SHELL_ROLE, GlobalConfig, Input, SHELL_ROLE,
|
||||
TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, load_env_file,
|
||||
@@ -133,13 +134,13 @@ async fn run(
|
||||
}
|
||||
|
||||
if cli.list_models {
|
||||
for model in list_models(&config.read(), ModelType::Chat) {
|
||||
for model in list_models(&config.read().to_app_config(), ModelType::Chat) {
|
||||
println!("{}", model.id());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if cli.list_roles {
|
||||
let roles = Config::list_roles(true).join("\n");
|
||||
let roles = paths::list_roles(true).join("\n");
|
||||
println!("{roles}");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -149,12 +150,12 @@ async fn run(
|
||||
return Ok(());
|
||||
}
|
||||
if cli.list_rags {
|
||||
let rags = Config::list_rags().join("\n");
|
||||
let rags = paths::list_rags().join("\n");
|
||||
println!("{rags}");
|
||||
return Ok(());
|
||||
}
|
||||
if cli.list_macros {
|
||||
let macros = Config::list_macros().join("\n");
|
||||
let macros = paths::list_macros().join("\n");
|
||||
println!("{macros}");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -443,7 +444,7 @@ async fn create_input(
|
||||
}
|
||||
|
||||
fn setup_logger() -> Result<Option<PathBuf>> {
|
||||
let (log_level, log_path) = Config::log_config()?;
|
||||
let (log_level, log_path) = paths::log_config()?;
|
||||
if log_level == LevelFilter::Off {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,4 +1,5 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::paths;
|
||||
use crate::utils::{AbortSignal, abortable_run_with_spinner};
|
||||
use crate::vault::interpolate_secrets;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
@@ -24,7 +25,7 @@ pub const MCP_INVOKE_META_FUNCTION_NAME_PREFIX: &str = "mcp_invoke";
|
||||
pub const MCP_SEARCH_META_FUNCTION_NAME_PREFIX: &str = "mcp_search";
|
||||
pub const MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX: &str = "mcp_describe";
|
||||
|
||||
type ConnectedServer = RunningService<RoleClient, ()>;
|
||||
pub type ConnectedServer = RunningService<RoleClient, ()>;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct CatalogItem {
|
||||
@@ -103,25 +104,25 @@ impl McpRegistry {
|
||||
log_path,
|
||||
..Default::default()
|
||||
};
|
||||
if !Config::mcp_config_file().try_exists().with_context(|| {
|
||||
if !paths::mcp_config_file().try_exists().with_context(|| {
|
||||
format!(
|
||||
"Failed to check MCP config file at {}",
|
||||
Config::mcp_config_file().display()
|
||||
paths::mcp_config_file().display()
|
||||
)
|
||||
})? {
|
||||
debug!(
|
||||
"MCP config file does not exist at {}, skipping MCP initialization",
|
||||
Config::mcp_config_file().display()
|
||||
paths::mcp_config_file().display()
|
||||
);
|
||||
return Ok(registry);
|
||||
}
|
||||
let err = || {
|
||||
format!(
|
||||
"Failed to load MCP config file at {}",
|
||||
Config::mcp_config_file().display()
|
||||
paths::mcp_config_file().display()
|
||||
)
|
||||
};
|
||||
let content = tokio::fs::read_to_string(Config::mcp_config_file())
|
||||
let content = tokio::fs::read_to_string(paths::mcp_config_file())
|
||||
.await
|
||||
.with_context(err)?;
|
||||
|
||||
|
||||
+16
-7
@@ -109,8 +109,11 @@ impl Rag {
|
||||
pub fn create(config: &GlobalConfig, name: &str, path: &Path, data: RagData) -> Result<Self> {
|
||||
let hnsw = data.build_hnsw();
|
||||
let bm25 = data.build_bm25();
|
||||
let embedding_model =
|
||||
Model::retrieve_model(&config.read(), &data.embedding_model, ModelType::Embedding)?;
|
||||
let embedding_model = Model::retrieve_model(
|
||||
&config.read().to_app_config(),
|
||||
&data.embedding_model,
|
||||
ModelType::Embedding,
|
||||
)?;
|
||||
let rag = Rag {
|
||||
config: config.clone(),
|
||||
name: name.to_string(),
|
||||
@@ -164,15 +167,18 @@ impl Rag {
|
||||
value
|
||||
}
|
||||
None => {
|
||||
let models = list_models(&config.read(), ModelType::Embedding);
|
||||
let models = list_models(&config.read().to_app_config(), ModelType::Embedding);
|
||||
if models.is_empty() {
|
||||
bail!("No available embedding model");
|
||||
}
|
||||
select_embedding_model(&models)?
|
||||
}
|
||||
};
|
||||
let embedding_model =
|
||||
Model::retrieve_model(&config.read(), &embedding_model_id, ModelType::Embedding)?;
|
||||
let embedding_model = Model::retrieve_model(
|
||||
&config.read().to_app_config(),
|
||||
&embedding_model_id,
|
||||
ModelType::Embedding,
|
||||
)?;
|
||||
|
||||
let chunk_size = match chunk_size {
|
||||
Some(value) => {
|
||||
@@ -560,8 +566,11 @@ impl Rag {
|
||||
|
||||
let ids = match rerank_model {
|
||||
Some(model_id) => {
|
||||
let model =
|
||||
Model::retrieve_model(&self.config.read(), model_id, ModelType::Reranker)?;
|
||||
let model = Model::retrieve_model(
|
||||
&self.config.read().to_app_config(),
|
||||
model_id,
|
||||
ModelType::Reranker,
|
||||
)?;
|
||||
let client = init_client(&self.config, Some(model))?;
|
||||
let ids: IndexSet<DocumentId> = [vector_search_ids, keyword_search_ids]
|
||||
.concat()
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ use self::highlighter::ReplHighlighter;
|
||||
use self::prompt::ReplPrompt;
|
||||
|
||||
use crate::client::{call_chat_completions, call_chat_completions_streaming, init_client, oauth};
|
||||
use crate::config::paths;
|
||||
use crate::config::{
|
||||
AgentVariables, AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags,
|
||||
macro_execute,
|
||||
@@ -460,7 +461,7 @@ pub async fn run_repl_command(
|
||||
}
|
||||
None => {
|
||||
let name = args;
|
||||
if !Config::has_role(name) {
|
||||
if !paths::has_role(name) {
|
||||
config.write().new_role(name)?;
|
||||
}
|
||||
|
||||
@@ -622,7 +623,7 @@ pub async fn run_repl_command(
|
||||
},
|
||||
".macro" => match split_first_arg(args) {
|
||||
Some((name, extra)) => {
|
||||
if !Config::has_macro(name) && extra.is_none() {
|
||||
if !paths::has_macro(name) && extra.is_none() {
|
||||
config.write().new_macro(name)?;
|
||||
} else {
|
||||
macro_execute(config, name, extra, abort_signal.clone()).await?;
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::paths;
|
||||
use colored::Colorize;
|
||||
use fancy_regex::Regex;
|
||||
use std::fs::File;
|
||||
@@ -7,7 +7,7 @@ use std::process;
|
||||
|
||||
pub async fn tail_logs(no_color: bool) {
|
||||
let re = Regex::new(r"^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+<(?P<opid>[^\s>]+)>\s+\[(?P<level>[A-Z]+)\]\s+(?P<logger>[^:]+):(?P<line>\d+)\s+-\s+(?P<message>.*)$").unwrap();
|
||||
let file_path = Config::log_path();
|
||||
let file_path = paths::log_path();
|
||||
let file = File::open(&file_path).expect("Cannot open file");
|
||||
let mut reader = BufReader::new(file);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user