use super::bundles::installed_bundle_names; use super::mcp_tool_policy::{McpToolPolicy, SkillMcpLayer, ToolFilter, expand_mcp_server_alias}; use super::rag_cache::{RagCache, RagKey}; use super::session::{INTERRUPTED_RESPONSE_TEXT, Session}; use super::skill::{SKILL_SCAFFOLD, Skill}; use super::skill_policy::SkillPolicy; use super::skill_registry::SkillRegistry; use super::todo::TodoList; use super::tool_scope::{McpPromptCompletion, McpRuntime, ToolScope, format_prompt_arguments}; use super::{ AGENTS_DIR_NAME, Agent, AgentVariables, AppConfig, AppState, AssetCategory, CREATE_TITLE_ROLE, Input, InstallFilter, LEFT_PROMPT, LastMessage, MESSAGES_FILE_NAME, MacroAllowlistLevel, MacroPolicy, MacroSource, MacroState, RESERVED_MACRO_NAMES, RIGHT_PROMPT, ResolvedMacro, Role, RoleLike, SESSIONS_DIR_NAME, SUMMARIZATION_PROMPT, SUMMARY_CONTEXT_PROMPT, StateFlags, TEMP_ROLE_NAME, TEMP_SESSION_NAME, WorkingMode, bundles, ensure_parent_exists, list_agents_with_descriptions, memory, paths, }; use super::{MessageContentToolCalls, prompts}; use crate::client::{Model, ModelType, list_models}; use crate::function::{ FunctionDeclaration, Functions, ToolCallTracker, ToolResult, agents::AGENT_FUNCTION_PREFIX, jobs::{DEFAULT_MAX_CONCURRENT_JOBS, JOB_FUNCTION_PREFIX, is_backgroundable_tool}, memory::MEMORY_FUNCTION_PREFIX, rag_query::RAG_FUNCTION_PREFIX, skill::SKILL_FUNCTION_PREFIX, todo::TODO_FUNCTION_PREFIX, user_interaction::USER_FUNCTION_PREFIX, }; use crate::mcp::{ CatalogItem, MCP_SEARCH_META_FUNCTION_NAME_PREFIX, McpAuthReason, McpAuthRequired, McpServerFeatures, McpServersConfig, McpTransportType, is_auth_required_error, is_mcp_meta_function, mcp_meta_function_names, }; use crate::rag::Rag; use crate::supervisor::Supervisor; use crate::supervisor::escalation::EscalationQueue; use crate::supervisor::mailbox::{Inbox, PeerRegistry}; use crate::supervisor::notification::NotificationQueue; use crate::utils::{ AbortSignal, abortable_run_with_spinner, edit_file, fuzzy_filter, get_env_name, list_file_names, now, render_prompt, temp_file, }; use comfy_table::{ContentArrangement, Table, presets::UTF8_FULL}; use super::install_remote::DEFAULT_GIT_HOST; use super::instructions; use super::macros::Macro; use super::memory::{ DEFAULT_MEMORY_CAP_WITH_TOOLS, DEFAULT_MEMORY_CAP_WITHOUT_TOOLS, MemoryStore, WorkspaceMemory, }; use crate::graph; use anyhow::{Context, Error, Result, bail}; use colored::Colorize; use gman::providers::SupportedProvider; use indexmap::IndexMap; use indoc::formatdoc; use inquire::{Confirm, MultiSelect, Text, list_option::ListOption, validator::Validation}; use log::warn; use parking_lot::RwLock; use prompts::DEFAULT_SKILL_INSTRUCTIONS; use std::collections::{BTreeSet, HashMap, HashSet}; use std::fs::{File, OpenOptions, read_dir, read_to_string, remove_dir_all, remove_file}; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use std::{env, fs}; pub(crate) fn expand_enabled_mcp_server_ids( app: &AppConfig, mcp_config: &McpServersConfig, enabled_mcp_servers: &[String], ) -> Vec { if enabled_mcp_servers.iter().any(|s| s.trim() == "all") { return mcp_config.mcp_servers.keys().cloned().collect(); } let mut ids = Vec::new(); for item in enabled_mcp_servers.iter().map(|s| s.trim()) { if mcp_config.mcp_servers.contains_key(item) { ids.push(item.to_string()); } else { for mapped_id in expand_mcp_server_alias(&app.mapping_mcp_servers, item) { if mcp_config.mcp_servers.contains_key(&mapped_id) { ids.push(mapped_id); } } } } ids } pub struct AutoContinueConfig { pub enabled: bool, pub max_continues: usize, pub inject_instructions: bool, pub continuation_prompt: Option, } pub struct SkillInstructionsConfig { pub inject: bool, pub instructions: Option, } #[derive(Debug, Clone)] pub struct MemoryConfig { pub enabled: bool, pub workspace: Option, } impl MemoryConfig { pub fn disabled() -> Self { Self { enabled: false, workspace: None, } } } /// Must stay in sync with the predicate that registers `skill__*` tools in `rebuild_tool_scope` /// (and in `graph::llm::run_llm_node`). Telling the model to call tools that are not exposed /// is a footgun. `compatible_enabled` is the post-filter universe that `skill__list` would /// actually return (cascade-allowed AND surviving `Skill::is_compatible` for current /// `mcp_server_support`), so an empty set means the hint has nothing to point at. pub fn should_inject_skill_instructions(app: &AppConfig, policy: &SkillPolicy) -> bool { app.function_calling_support && policy.skills_enabled && !policy.compatible_enabled.is_empty() } pub fn effective_max_concurrent_jobs(agent: Option<&Agent>, app: &AppConfig) -> usize { agent .and_then(|a| a.max_concurrent_jobs()) .or(app.max_concurrent_jobs) .unwrap_or(DEFAULT_MAX_CONCURRENT_JOBS) } pub fn jobs_enabled(agent: Option<&Agent>, app: &AppConfig) -> bool { app.function_calling_support && effective_max_concurrent_jobs(agent, app) > 0 } fn print_asset_names(kind: &str, names: &[String]) -> Result<()> { if names.is_empty() { println!("No {kind} found."); return Ok(()); } let mut header: Vec = kind.chars().collect(); header[0] = header[0].to_ascii_uppercase(); let header: String = header.into_iter().collect(); println!("{header}:"); for name in names { println!(" • {name}"); } Ok(()) } pub(crate) fn asset_table(header: &[&str]) -> Table { let mut table = Table::new(); table.load_preset(UTF8_FULL); table.set_content_arrangement(ContentArrangement::Dynamic); table.set_header(header.to_vec()); table } fn mcp_prompt_rows(items: &[CatalogItem]) -> Vec<[String; 4]> { items .iter() .map(|item| { [ item.server.clone(), item.name.clone(), item.description.clone(), format_prompt_arguments(item.arguments.as_deref().unwrap_or_default()), ] }) .collect() } fn complete_skills_with_descriptions(names: Vec) -> Vec<(String, Option)> { names .into_iter() .map(|name| { let description = Skill::load(&name) .ok() .map(|s| s.description().to_string()) .filter(|d| !d.is_empty()); (name, description) }) .collect() } const SET_COMPLETION_KEYS: [&str; 27] = [ "auto_continue", "continuation_prompt", "temperature", "top_p", "enabled_macros", "enabled_skills", "enabled_tools", "enabled_mcp_servers", "inject_todo_instructions", "inject_skill_instructions", "skill_instructions", "max_auto_continues", "mcp_tools", "memory", "save_session", "compression_threshold", "rag_reranker_model", "rag_top_k", "max_output_tokens", "dry_run", "function_calling_support", "mcp_server_support", "skills_enabled", "stream", "save", "highlight", "raw_markdown", ]; fn toggled_enabled_macros( current: Option<&[String]>, all_active: &[String], name: &str, enable: bool, ) -> Option> { match (current, enable) { (None, true) => None, (Some(list), true) => { if list.iter().any(|v| v == name) { None } else { let mut list = list.to_vec(); list.push(name.to_string()); Some(list) } } (None, false) => Some( all_active .iter() .filter(|v| v.as_str() != name) .cloned() .collect(), ), (Some(list), false) => { if list.iter().any(|v| v == name) { Some( list.iter() .filter(|v| v.as_str() != name) .cloned() .collect(), ) } else { None } } } } fn macro_state_display( row: &ResolvedMacro, lock_owner: impl Fn(MacroAllowlistLevel) -> String, ) -> String { match &row.state { MacroState::Enabled => "enabled".to_string(), MacroState::DisabledRuntime => "disabled (runtime)".to_string(), MacroState::Locked { level } => format!("locked ({} enabled_macros)", lock_owner(*level)), MacroState::Missing => "missing".to_string(), MacroState::ShadowedBuiltin => "shadowed (built-in)".to_string(), MacroState::Invalid { reason } => format!("invalid ({reason})"), } } fn macro_source_display(source: Option) -> String { match source { Some(source) => source.to_string(), None => "-".to_string(), } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum RenderMode { #[default] Streaming, Silent, } pub struct RequestContext { pub app: Arc, pub macro_flag: bool, pub macro_non_isolated: bool, pub info_flag: bool, pub working_mode: WorkingMode, pub model: Model, pub agent_variables: Option, pub role: Option, pub session: Option, pub rag: Option>, pub rag_key: Option, pub agent: Option, pub last_message: Option, pub tool_scope: ToolScope, pub declared_function_names: HashSet, /// Ids of jobs started by the currently executing graph LLM node. /// `Some` only while a node runs: `job__start` records into it, the /// turn-end guardrail scopes its nag to it, and the node executor reaps /// whatever is left in it on exit. `None` outside graph nodes — there the /// context owns every job in its supervisor. pub node_job_scope: Option>, /// Set while a graph LLM node with `mcp_tools` is executing; re-applied as /// the last filter layer by every `refresh_mcp_tool_filters` recompute. pub active_node_mcp_tools: Option<(String, IndexMap>)>, pub supervisor: Option>>, pub parent_supervisor: Option>>, pub self_agent_id: Option, pub inbox: Option>, pub parent_inbox: Option>, /// Directory of concurrent teammates for `teammates: true` graph fan-outs. /// Never inherited: fork sites and `run_agent_for_graph` propagate it /// explicitly so it cannot leak into nested fan-outs or spawned children. pub peer_registry: Option>, /// Pre-provisioned (agent id, inbox) for the sub-agent this branch context /// will run, consumed by `run_agent_for_graph`. pub peer_assignment: Option<(String, Arc)>, pub escalation_queue: Option>, pub notification_queue: Arc, pub current_depth: usize, pub auto_continue_count: usize, pub pending_tasks_guardrail_count: u32, pub todo_list: TodoList, pub skill_registry: SkillRegistry, pub last_continuation_response: Option, pub auto_continue_paused: Option, pub pending_prefill: Option, pub session_abort: Option, pub render_mode: RenderMode, } impl RequestContext { pub fn new(app: Arc, working_mode: WorkingMode) -> Self { Self { app, macro_flag: false, macro_non_isolated: false, info_flag: false, working_mode, model: Default::default(), agent_variables: None, role: None, session: None, rag: None, rag_key: None, agent: None, last_message: None, tool_scope: ToolScope::default(), declared_function_names: Default::default(), node_job_scope: None, active_node_mcp_tools: None, supervisor: None, parent_supervisor: None, self_agent_id: None, inbox: None, parent_inbox: None, peer_registry: None, peer_assignment: None, escalation_queue: None, notification_queue: Arc::new(NotificationQueue::new()), current_depth: 0, auto_continue_count: 0, pending_tasks_guardrail_count: 0, todo_list: TodoList::default(), skill_registry: SkillRegistry::default(), last_continuation_response: None, auto_continue_paused: None, pending_prefill: None, session_abort: None, render_mode: RenderMode::default(), } } pub fn bootstrap( app: Arc, working_mode: WorkingMode, info_flag: bool, ) -> Result { let model = Model::retrieve_model(&app.config, &app.config.model_id, ModelType::Chat)?; let mut functions = app.functions.clone(); if working_mode.is_repl() { functions.append_user_interaction_functions(); } if app.config.function_calling_support { let policy = SkillPolicy::effective(&app.config, None, None, None)?; if policy.skills_enabled { functions.append_skill_functions(); } } let mut mcp_runtime = McpRuntime::default(); if let Some(registry) = &app.mcp_registry { mcp_runtime.sync_from_registry(registry); } let mut ctx = Self { app, macro_flag: false, macro_non_isolated: false, info_flag, working_mode, model, agent_variables: None, role: None, session: None, rag: None, rag_key: None, agent: None, last_message: None, tool_scope: ToolScope { functions, mcp_runtime, tool_tracker: ToolCallTracker::default(), }, declared_function_names: Default::default(), node_job_scope: None, active_node_mcp_tools: None, supervisor: None, parent_supervisor: None, self_agent_id: None, inbox: None, parent_inbox: None, peer_registry: None, peer_assignment: None, escalation_queue: None, notification_queue: Arc::new(NotificationQueue::new()), current_depth: 0, auto_continue_count: 0, pending_tasks_guardrail_count: 0, todo_list: TodoList::default(), skill_registry: SkillRegistry::default(), last_continuation_response: None, auto_continue_paused: None, pending_prefill: None, session_abort: None, render_mode: RenderMode::default(), }; ctx.refresh_mcp_tool_filters(); Ok(ctx) } /// Forks the context for one parallel branch of a graph super-step. /// /// Each branch gets a fresh, owned clone. Mutations (role swap, /// `before/after_chat_completion`, tool tracker, last_message, etc.) are /// scoped to the branch and discarded when the branch finishes. The /// user-visible state communication happens through the graph's /// `StateManager` (via `fork_for_branch_state` + `diff_against` + /// `apply_branch_writes` reducers), and not through `RequestContext`. /// /// Distinction from `new_for_child`: `new_for_child` builds a fresh context /// for a spawned sub-agent (different agent identity, different supervisor /// hierarchy, depth+1, fresh tool tracker). `fork_for_branch` keeps the /// caller's identity and supervisor hierarchy; it's a sibling clone of the /// same logical agent, running one of N parallel work items. pub fn fork_for_branch(&self) -> Self { Self { app: Arc::clone(&self.app), macro_flag: self.macro_flag, macro_non_isolated: self.macro_non_isolated, info_flag: self.info_flag, working_mode: self.working_mode, model: self.model.clone(), agent_variables: self.agent_variables.clone(), role: self.role.clone(), session: self.session.clone(), rag: self.rag.clone(), rag_key: self.rag_key.clone(), agent: self.agent.clone(), last_message: self.last_message.clone(), tool_scope: self.tool_scope.clone(), declared_function_names: self.declared_function_names.clone(), node_job_scope: None, active_node_mcp_tools: self.active_node_mcp_tools.clone(), supervisor: self.supervisor.clone(), parent_supervisor: self.parent_supervisor.clone(), self_agent_id: self.self_agent_id.clone(), inbox: self.inbox.clone(), parent_inbox: self.parent_inbox.clone(), peer_registry: None, peer_assignment: None, escalation_queue: self.escalation_queue.clone(), notification_queue: self.notification_queue.clone(), current_depth: self.current_depth, auto_continue_count: 0, pending_tasks_guardrail_count: 0, todo_list: self.todo_list.clone(), skill_registry: self.skill_registry.clone(), last_continuation_response: None, auto_continue_paused: self.auto_continue_paused.clone(), pending_prefill: None, session_abort: self.session_abort.clone(), render_mode: self.render_mode, } } pub fn new_for_child( app: Arc, parent: &Self, current_depth: usize, inbox: Arc, self_agent_id: String, ) -> Self { let tool_call_tracker = ToolCallTracker::new(4, 10); Self { app, macro_flag: parent.macro_flag, macro_non_isolated: parent.macro_non_isolated, info_flag: parent.info_flag, working_mode: WorkingMode::Cmd, model: parent.model.clone(), agent_variables: parent.agent_variables.clone(), role: None, session: None, rag: None, rag_key: None, agent: None, last_message: None, tool_scope: ToolScope { functions: Functions::default(), mcp_runtime: McpRuntime::default(), tool_tracker: tool_call_tracker, }, declared_function_names: Default::default(), node_job_scope: None, active_node_mcp_tools: None, supervisor: None, parent_supervisor: parent.supervisor.clone(), self_agent_id: Some(self_agent_id), inbox: Some(inbox), parent_inbox: parent.inbox.clone(), peer_registry: None, peer_assignment: None, escalation_queue: parent.escalation_queue.clone(), notification_queue: Arc::new(NotificationQueue::new()), current_depth, auto_continue_count: 0, pending_tasks_guardrail_count: 0, todo_list: TodoList::default(), skill_registry: SkillRegistry::default(), last_continuation_response: None, auto_continue_paused: None, pending_prefill: None, session_abort: None, render_mode: parent.render_mode, } } fn update_app_config(&mut self, update: impl FnOnce(&mut AppConfig)) { let mut app_config = (*self.app.config).clone(); update(&mut app_config); let mut app_state = (*self.app).clone(); app_state.config = Arc::new(app_config); self.app = Arc::new(app_state); } pub fn root_escalation_queue(&self) -> Option<&Arc> { self.escalation_queue.as_ref() } pub fn ensure_root_escalation_queue(&mut self) -> Arc { self.escalation_queue .get_or_insert_with(|| Arc::new(EscalationQueue::new())) .clone() } pub fn ensure_inbox(&mut self) -> Arc { self.inbox .get_or_insert_with(|| Arc::new(Inbox::new())) .clone() } pub fn rag_cache(&self) -> &Arc { &self.app.rag_cache } pub fn init_todo_list(&mut self, goal: &str) { self.todo_list = TodoList::new(goal); self.auto_continue_paused = None; } pub fn add_todo(&mut self, task: &str) -> usize { self.todo_list.add(task) } pub fn mark_todo_done(&mut self, id: usize) -> bool { self.todo_list.mark_done(id) } pub fn clear_todo_list(&mut self) { self.todo_list.clear(); self.auto_continue_count = 0; self.auto_continue_paused = None; } pub fn increment_auto_continue_count(&mut self) { self.auto_continue_count += 1; } pub fn pause_auto_continue(&mut self, reason: &str) { self.auto_continue_paused = Some(reason.to_string()); } pub fn resume_auto_continue(&mut self) { self.auto_continue_paused = None; } pub fn reset_continuation_count(&mut self) { self.auto_continue_count = 0; self.last_continuation_response = None; } pub fn set_last_continuation_response(&mut self, response: String) { self.last_continuation_response = Some(response); } pub fn state(&self) -> StateFlags { let mut flags = StateFlags::empty(); if let Some(session) = &self.session { if session.is_empty() { flags |= StateFlags::SESSION_EMPTY; } else { flags |= StateFlags::SESSION; } if session.role_name().is_some() { flags |= StateFlags::ROLE; } } else if self.role.is_some() { flags |= StateFlags::ROLE; } if self.agent.is_some() { flags |= StateFlags::AGENT; } if self.rag.is_some() { flags |= StateFlags::RAG; } if self.app.config.function_calling_support { flags |= StateFlags::FUNCTION_CALLING; } if self.auto_continue_config().enabled { flags |= StateFlags::AUTO_CONTINUE; } if self.resolved_skills_enabled() { flags |= StateFlags::SKILLS_ENABLED; } flags } pub fn resolved_skills_enabled(&self) -> bool { if let Some(agent) = &self.agent && let Some(value) = agent.skills_enabled() { return value; } let app = &self.app.config; self.session .as_ref() .and_then(|s| s.skills_enabled()) .or_else(|| self.role.as_ref().and_then(|r| r.skills_enabled())) .unwrap_or(app.skills_enabled) } pub fn messages_file(&self) -> PathBuf { match &self.agent { None => match env::var(get_env_name("messages_file")) { Ok(value) => PathBuf::from(value), Err(_) => paths::cache_dir().join(MESSAGES_FILE_NAME), }, Some(agent) => paths::cache_dir() .join(AGENTS_DIR_NAME) .join(agent.name()) .join(MESSAGES_FILE_NAME), } } pub fn sessions_dir(&self) -> PathBuf { match &self.agent { None => match env::var(get_env_name("sessions_dir")) { Ok(value) => PathBuf::from(value), Err(_) => paths::local_dir(SESSIONS_DIR_NAME), }, Some(agent) => paths::agent_data_dir(agent.name()).join(SESSIONS_DIR_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")), None => self.sessions_dir().join(format!("{name}.yaml")), } } pub fn rag_file(&self, name: &str) -> PathBuf { match &self.agent { Some(agent) => paths::agent_rag_file(agent.name(), name), None => paths::rags_dir().join(format!("{name}.yaml")), } } pub fn role_info(&self) -> Result { if let Some(session) = &self.session { if session.role_name().is_some() { let role = session.to_role(); Ok(role.export()) } else { bail!("No session role") } } else if let Some(role) = &self.role { Ok(role.export()) } else { bail!("No role") } } pub fn agent_info(&self) -> Result { if let Some(agent) = &self.agent { agent.export() } else { bail!("No agent") } } pub fn agent_banner(&self) -> Result { if let Some(agent) = &self.agent { Ok(agent.banner()) } else { bail!("No agent") } } pub fn rag_info(&self) -> Result { if let Some(rag) = &self.rag { rag.export() } else { bail!("No RAG") } } pub fn todo_info(&self) -> Result { if !self.auto_continue_config().enabled { bail!( "Auto-continuation is disabled. Enable it by setting `auto_continue: true` in your config or running `.set auto_continue true`." ); } if self.todo_list.is_empty() { return Ok("No todos in the running list.\n".to_string()); } let mut out = self.todo_list.render_for_model(); out.push('\n'); Ok(out) } pub fn tools_info(&self) -> Result { if !self.app.config.function_calling_support { bail!( "Function calling is disabled. Enable it by setting `function_calling_support: true` in your config or running `.set function_calling_support true`." ); } let role = self.extract_role(&self.app.config)?; match self.select_functions(&role) { None => Ok("No tools enabled for the next request.\n".to_string()), Some(functions) => { let mut names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect(); names.sort_unstable(); let mut out = format!( "Tools enabled for the next request: {}\n\n", functions.len() ); for name in names { out.push_str(" "); out.push_str(name); out.push('\n'); } Ok(out) } } } pub async fn mcp_server_info(&self, name: &str) -> Result { let Some(spec) = self .app .mcp_config .as_ref() .and_then(|config| config.mcp_servers.get(name)) else { bail!( "MCP server '{name}' is not configured. Run `.list mcp-servers` to see what's available" ); }; let Some(handle) = self.tool_scope.mcp_runtime.servers.get(name).cloned() else { bail!("MCP server '{name}' is not running. Enable it with `.mcp enable {name}`."); }; let transport = match spec.transport_type { McpTransportType::Stdio => "stdio", McpTransportType::Http => "http", McpTransportType::Sse => "sse", }; let info = handle.peer_info(); let features = McpServerFeatures::from_capabilities(name, info.as_ref().map(|i| &i.capabilities)); let mut capabilities: Vec = vec![]; if features.tools { capabilities.push("tools".to_string()); } if features.resources { let resources = handle.list_all_resources().await; let templates = handle.list_all_resource_templates().await; let any_failed = resources.is_err() || templates.is_err(); let count = resources.map(|r| r.len()).unwrap_or_default() + templates.map(|t| t.len()).unwrap_or_default(); if count > 0 { capabilities.push(format!("resources ({count})")); } else if any_failed { capabilities.push("resources (declared, list failed)".to_string()); } } if features.prompts { match handle.list_all_prompts().await { Ok(prompts) if prompts.is_empty() => {} Ok(prompts) => capabilities.push(format!("prompts ({})", prompts.len())), Err(_) => { capabilities.push("prompts (declared, list failed)".to_string()); } } } const INFO_LABEL_WIDTH: usize = 15; let mut out = String::new(); out.push_str(&format!( "{: = filter .map(|f| { f.layers() .map(|(source, patterns)| (format!("{source}:"), patterns.join(" | "))) .collect() }) .unwrap_or_default(); if layers.is_empty() { out.push_str(&format!( "{: = tools.iter().map(|tool| tool.name.to_string()).collect(); names.sort_unstable(); let allowed = names .iter() .filter(|tool| filter.is_none_or(|f| f.allows(tool))) .count(); out.push_str(&format!( "\ntools ({allowed} allowed / {} total)\n", names.len() )); let name_width = names .iter() .map(|tool| tool.chars().count()) .max() .unwrap_or_default(); let allowed_marker = "✓".green().bold().to_string(); let hidden_marker = "✗".red().bold().to_string(); for tool in &names { let explained = filter.map(|f| f.allows_explain(tool)); match explained { None => out.push_str(&format!(" {allowed_marker} {tool}\n")), Some(Ok(matches)) => { let chain: Vec = matches .iter() .map(|(source, pattern)| format!("{pattern} ({})", source.short_label())) .collect(); if chain.is_empty() { out.push_str(&format!(" {allowed_marker} {tool}\n")); } else { out.push_str(&format!( " {allowed_marker} {tool: out.push_str(&format!( " {hidden_marker} {tool: