This commit is contained in:
2026-04-10 15:45:51 -06:00
parent ff3419a714
commit e9e6b82e24
42 changed files with 11578 additions and 358 deletions
+5 -4
View File
@@ -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)
+3 -2
View File
@@ -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())
});
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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![];
}
+93
View File
@@ -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
}
}
+452
View File
@@ -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();
}
}
+45
View File
@@ -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>,
}
+439
View File
@@ -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);
}
}
+3 -2
View File
@@ -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!(
+93
View File
@@ -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
View File
@@ -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(&macro_path)?;
let editor = self.editor()?;
edit_file(&editor, &macro_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,
{
+268
View File
@@ -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)
}
+85
View File
@@ -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
+2 -1
View File
@@ -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();
+78
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);