Skip to main content

task_graph_mcp/
config.rs

1//! Configuration loading and management.
2
3use crate::format::OutputFormat;
4use anyhow::{Result, anyhow};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9/// Auto-advance configuration for automatically transitioning tasks when dependencies are satisfied.
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct AutoAdvanceConfig {
12    /// Enable auto-advance when dependencies are satisfied (default: false).
13    #[serde(default)]
14    pub enabled: bool,
15
16    /// Target state for auto-advanced tasks (e.g., "ready").
17    /// If None, tasks remain in their current state even when unblocked.
18    #[serde(default)]
19    pub target_state: Option<String>,
20}
21
22/// Behavior for unknown attachment keys.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
24#[serde(rename_all = "snake_case")]
25pub enum UnknownKeyBehavior {
26    /// Silently use default mime/mode.
27    Allow,
28    /// Use defaults but return a warning in the response (default).
29    #[default]
30    Warn,
31    /// Reject unknown keys with an error.
32    Reject,
33}
34
35/// Definition of a preconfigured attachment key.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AttachmentKeyDefinition {
38    /// Default MIME type for this key.
39    pub mime: String,
40    /// Default mode: "append" or "replace".
41    #[serde(default = "default_append_mode")]
42    pub mode: String,
43}
44
45fn default_append_mode() -> String {
46    "append".to_string()
47}
48
49/// Attachments configuration with preconfigured key definitions.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct AttachmentsConfig {
52    /// Behavior for unknown attachment keys (allow, warn, reject).
53    #[serde(default)]
54    pub unknown_key: UnknownKeyBehavior,
55    /// Preconfigured attachment key definitions.
56    #[serde(default = "AttachmentsConfig::default_definitions")]
57    pub definitions: HashMap<String, AttachmentKeyDefinition>,
58}
59
60impl Default for AttachmentsConfig {
61    fn default() -> Self {
62        Self {
63            unknown_key: UnknownKeyBehavior::default(),
64            definitions: Self::default_definitions(),
65        }
66    }
67}
68
69impl AttachmentsConfig {
70    /// Default attachment key definitions.
71    pub fn default_definitions() -> HashMap<String, AttachmentKeyDefinition> {
72        let mut defs = HashMap::new();
73
74        defs.insert(
75            "commit".to_string(),
76            AttachmentKeyDefinition {
77                mime: "text/git.hash".to_string(),
78                mode: "append".to_string(),
79            },
80        );
81
82        defs.insert(
83            "checkin".to_string(),
84            AttachmentKeyDefinition {
85                mime: "text/p4.changelist".to_string(),
86                mode: "append".to_string(),
87            },
88        );
89
90        defs.insert(
91            "meta".to_string(),
92            AttachmentKeyDefinition {
93                mime: "application/json".to_string(),
94                mode: "replace".to_string(),
95            },
96        );
97
98        defs.insert(
99            "note".to_string(),
100            AttachmentKeyDefinition {
101                mime: "text/plain".to_string(),
102                mode: "append".to_string(),
103            },
104        );
105
106        defs.insert(
107            "log".to_string(),
108            AttachmentKeyDefinition {
109                mime: "text/plain".to_string(),
110                mode: "append".to_string(),
111            },
112        );
113
114        defs.insert(
115            "error".to_string(),
116            AttachmentKeyDefinition {
117                mime: "text/plain".to_string(),
118                mode: "append".to_string(),
119            },
120        );
121
122        defs.insert(
123            "output".to_string(),
124            AttachmentKeyDefinition {
125                mime: "text/plain".to_string(),
126                mode: "append".to_string(),
127            },
128        );
129
130        defs.insert(
131            "diff".to_string(),
132            AttachmentKeyDefinition {
133                mime: "text/x-diff".to_string(),
134                mode: "append".to_string(),
135            },
136        );
137
138        defs.insert(
139            "changelist".to_string(),
140            AttachmentKeyDefinition {
141                mime: "text/plain".to_string(),
142                mode: "append".to_string(),
143            },
144        );
145
146        defs.insert(
147            "plan".to_string(),
148            AttachmentKeyDefinition {
149                mime: "text/markdown".to_string(),
150                mode: "replace".to_string(),
151            },
152        );
153
154        defs.insert(
155            "result".to_string(),
156            AttachmentKeyDefinition {
157                mime: "application/json".to_string(),
158                mode: "replace".to_string(),
159            },
160        );
161
162        defs.insert(
163            "context".to_string(),
164            AttachmentKeyDefinition {
165                mime: "text/plain".to_string(),
166                mode: "replace".to_string(),
167            },
168        );
169
170        defs
171    }
172
173    /// Get the definition for a key, if it exists.
174    pub fn get_definition(&self, key: &str) -> Option<&AttachmentKeyDefinition> {
175        self.definitions.get(key)
176    }
177
178    /// Check if a key is a known/configured key.
179    pub fn is_known_key(&self, key: &str) -> bool {
180        self.definitions.contains_key(key)
181    }
182
183    /// Get the default MIME type for a key, or fallback to text/plain.
184    pub fn get_mime_default(&self, key: &str) -> &str {
185        self.definitions
186            .get(key)
187            .map(|d| d.mime.as_str())
188            .unwrap_or("text/plain")
189    }
190
191    /// Get the default mode for a key, or fallback to "append".
192    pub fn get_mode_default(&self, key: &str) -> &str {
193        self.definitions
194            .get(key)
195            .map(|d| d.mode.as_str())
196            .unwrap_or("append")
197    }
198}
199
200/// Server configuration.
201#[derive(Debug, Clone, Serialize, Deserialize)]
202#[derive(Default)]
203pub struct Config {
204    #[serde(default)]
205    pub server: ServerConfig,
206
207    #[serde(default)]
208    pub paths: PathsConfig,
209
210    #[serde(default)]
211    pub states: StatesConfig,
212
213    #[serde(default)]
214    pub dependencies: DependenciesConfig,
215
216    #[serde(default)]
217    pub auto_advance: AutoAdvanceConfig,
218
219    #[serde(default)]
220    pub attachments: AttachmentsConfig,
221}
222
223
224/// Paths configured for the server, returned by connect.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ServerPaths {
227    /// Path to the SQLite database file.
228    pub db_path: PathBuf,
229    /// Path to the media directory for file attachments.
230    pub media_dir: PathBuf,
231    /// Path to the log directory.
232    pub log_dir: PathBuf,
233    /// Path to the configuration file (if one was loaded).
234    pub config_path: Option<PathBuf>,
235}
236
237/// Server-specific configuration.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct ServerConfig {
240    /// Path to the SQLite database file.
241    #[serde(default = "default_db_path")]
242    pub db_path: PathBuf,
243
244    /// Path to the media directory for file attachments.
245    #[serde(default = "default_media_dir")]
246    pub media_dir: PathBuf,
247
248    /// Maximum claims per agent.
249    #[serde(default = "default_claim_limit")]
250    pub claim_limit: i32,
251
252    /// Timeout for stale claims in seconds.
253    #[serde(default = "default_stale_timeout")]
254    pub stale_timeout_seconds: i64,
255
256    /// Default output format for query results (json or markdown).
257    #[serde(default)]
258    pub default_format: OutputFormat,
259
260    /// Path to the skills directory for skill overrides.
261    #[serde(default = "default_skills_dir")]
262    pub skills_dir: PathBuf,
263
264    /// Path to the log directory.
265    #[serde(default = "default_log_dir")]
266    pub log_dir: PathBuf,
267}
268
269impl Default for ServerConfig {
270    fn default() -> Self {
271        Self {
272            db_path: default_db_path(),
273            media_dir: default_media_dir(),
274            claim_limit: default_claim_limit(),
275            stale_timeout_seconds: default_stale_timeout(),
276            default_format: OutputFormat::default(),
277            skills_dir: default_skills_dir(),
278            log_dir: default_log_dir(),
279        }
280    }
281}
282
283fn default_db_path() -> PathBuf {
284    PathBuf::from(".task-graph/tasks.db")
285}
286
287fn default_media_dir() -> PathBuf {
288    PathBuf::from(".task-graph/media")
289}
290
291fn default_skills_dir() -> PathBuf {
292    PathBuf::from(".task-graph/skills")
293}
294
295fn default_log_dir() -> PathBuf {
296    PathBuf::from(".task-graph/logs")
297}
298
299fn default_claim_limit() -> i32 {
300    5
301}
302
303fn default_stale_timeout() -> i64 {
304    900 // 15 minutes
305}
306
307/// Path handling configuration.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct PathsConfig {
310    /// Style for representing file paths.
311    #[serde(default)]
312    pub style: PathStyle,
313}
314
315impl Default for PathsConfig {
316    fn default() -> Self {
317        Self {
318            style: PathStyle::Relative,
319        }
320    }
321}
322
323/// Path style for file locks.
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
325#[serde(rename_all = "snake_case")]
326#[derive(Default)]
327pub enum PathStyle {
328    /// Relative paths (e.g., src/main.rs)
329    #[default]
330    Relative,
331    /// Project-prefixed paths (e.g., ${project}/src/main.rs)
332    ProjectPrefixed,
333}
334
335
336/// Task state configuration.
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct StatesConfig {
339    /// Default state for new tasks.
340    #[serde(default = "default_initial_state")]
341    pub initial: String,
342
343    /// Default state for tasks when their owner disconnects (must be untimed).
344    #[serde(default = "default_disconnect_state")]
345    pub disconnect_state: String,
346
347    /// States that block dependent tasks (tasks in these states count as "not done").
348    #[serde(default = "default_blocking_states")]
349    pub blocking_states: Vec<String>,
350
351    /// State definitions with allowed transitions and timing behavior.
352    #[serde(default = "default_state_definitions")]
353    pub definitions: HashMap<String, StateDefinition>,
354}
355
356impl Default for StatesConfig {
357    fn default() -> Self {
358        Self {
359            initial: default_initial_state(),
360            disconnect_state: default_disconnect_state(),
361            blocking_states: default_blocking_states(),
362            definitions: default_state_definitions(),
363        }
364    }
365}
366
367fn default_initial_state() -> String {
368    "pending".to_string()
369}
370
371fn default_disconnect_state() -> String {
372    "pending".to_string()
373}
374
375fn default_blocking_states() -> Vec<String> {
376    vec![
377        "pending".to_string(),
378        "assigned".to_string(),
379        "in_progress".to_string(),
380    ]
381}
382
383fn default_state_definitions() -> HashMap<String, StateDefinition> {
384    let mut defs = HashMap::new();
385
386    defs.insert(
387        "pending".to_string(),
388        StateDefinition {
389            exits: vec![
390                "assigned".to_string(),
391                "in_progress".to_string(),
392                "cancelled".to_string(),
393            ],
394            timed: false,
395        },
396    );
397
398    defs.insert(
399        "assigned".to_string(),
400        StateDefinition {
401            exits: vec![
402                "in_progress".to_string(),
403                "pending".to_string(),
404                "cancelled".to_string(),
405            ],
406            timed: false,
407        },
408    );
409
410    defs.insert(
411        "in_progress".to_string(),
412        StateDefinition {
413            exits: vec![
414                "completed".to_string(),
415                "failed".to_string(),
416                "pending".to_string(),
417            ],
418            timed: true,
419        },
420    );
421
422    defs.insert(
423        "completed".to_string(),
424        StateDefinition {
425            exits: vec![],
426            timed: false,
427        },
428    );
429
430    defs.insert(
431        "failed".to_string(),
432        StateDefinition {
433            exits: vec!["pending".to_string()],
434            timed: false,
435        },
436    );
437
438    defs.insert(
439        "cancelled".to_string(),
440        StateDefinition {
441            exits: vec![],
442            timed: false,
443        },
444    );
445
446    defs
447}
448
449/// Definition of a single task state.
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct StateDefinition {
452    /// Allowed states to transition to from this state.
453    #[serde(default)]
454    pub exits: Vec<String>,
455
456    /// Whether time spent in this state should be tracked (accumulated to time_actual_ms).
457    #[serde(default)]
458    pub timed: bool,
459}
460
461/// Dependency type configuration.
462#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct DependenciesConfig {
464    /// Dependency type definitions.
465    #[serde(default = "default_dependency_definitions")]
466    pub definitions: HashMap<String, DependencyDefinition>,
467}
468
469impl Default for DependenciesConfig {
470    fn default() -> Self {
471        Self {
472            definitions: default_dependency_definitions(),
473        }
474    }
475}
476
477/// Definition of a dependency type.
478#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct DependencyDefinition {
480    /// Display orientation: "horizontal" (same level) or "vertical" (parent-child).
481    pub display: DependencyDisplay,
482
483    /// What this dependency blocks: "start" (blocks claiming) or "completion" (blocks completing).
484    pub blocks: BlockTarget,
485}
486
487/// Display orientation for dependency visualization.
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
489#[serde(rename_all = "snake_case")]
490pub enum DependencyDisplay {
491    /// Same level dependencies (blocks, follows).
492    Horizontal,
493    /// Parent-child relationships (contains).
494    Vertical,
495}
496
497/// What a dependency blocks.
498#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
499#[serde(rename_all = "snake_case")]
500pub enum BlockTarget {
501    /// Does not block - informational link only.
502    None,
503    /// Blocks the task from being started/claimed.
504    Start,
505    /// Blocks the task from being completed.
506    Completion,
507}
508
509fn default_dependency_definitions() -> HashMap<String, DependencyDefinition> {
510    let mut defs = HashMap::new();
511
512    // Primary workflow types (blocking)
513    defs.insert(
514        "blocks".to_string(),
515        DependencyDefinition {
516            display: DependencyDisplay::Horizontal,
517            blocks: BlockTarget::Start,
518        },
519    );
520
521    defs.insert(
522        "follows".to_string(),
523        DependencyDefinition {
524            display: DependencyDisplay::Horizontal,
525            blocks: BlockTarget::Start,
526        },
527    );
528
529    defs.insert(
530        "contains".to_string(),
531        DependencyDefinition {
532            display: DependencyDisplay::Vertical,
533            blocks: BlockTarget::Completion,
534        },
535    );
536
537    // Non-blocking relationship types
538    defs.insert(
539        "duplicate".to_string(),
540        DependencyDefinition {
541            display: DependencyDisplay::Horizontal,
542            blocks: BlockTarget::None,
543        },
544    );
545
546    defs.insert(
547        "see-also".to_string(),
548        DependencyDefinition {
549            display: DependencyDisplay::Horizontal,
550            blocks: BlockTarget::None,
551        },
552    );
553
554    defs.insert(
555        "relates-to".to_string(),
556        DependencyDefinition {
557            display: DependencyDisplay::Horizontal,
558            blocks: BlockTarget::None,
559        },
560    );
561
562    defs
563}
564
565impl DependenciesConfig {
566    /// Check if a dependency type is valid.
567    pub fn is_valid_dep_type(&self, dep_type: &str) -> bool {
568        self.definitions.contains_key(dep_type)
569    }
570
571    /// Get the definition for a dependency type.
572    pub fn get_definition(&self, dep_type: &str) -> Option<&DependencyDefinition> {
573        self.definitions.get(dep_type)
574    }
575
576    /// Get all dependency types that block start.
577    pub fn start_blocking_types(&self) -> Vec<&str> {
578        self.definitions
579            .iter()
580            .filter(|(_, def)| def.blocks == BlockTarget::Start)
581            .map(|(name, _)| name.as_str())
582            .collect()
583    }
584
585    /// Get all dependency types that block completion.
586    pub fn completion_blocking_types(&self) -> Vec<&str> {
587        self.definitions
588            .iter()
589            .filter(|(_, def)| def.blocks == BlockTarget::Completion)
590            .map(|(name, _)| name.as_str())
591            .collect()
592    }
593
594    /// Get all vertical (parent-child) dependency types.
595    pub fn vertical_types(&self) -> Vec<&str> {
596        self.definitions
597            .iter()
598            .filter(|(_, def)| def.display == DependencyDisplay::Vertical)
599            .map(|(name, _)| name.as_str())
600            .collect()
601    }
602
603    /// Get all dependency type names.
604    pub fn dep_type_names(&self) -> Vec<&str> {
605        self.definitions.keys().map(|s| s.as_str()).collect()
606    }
607
608    /// Validate the dependencies configuration.
609    pub fn validate(&self) -> anyhow::Result<()> {
610        if self.definitions.is_empty() {
611            return Err(anyhow::anyhow!(
612                "At least one dependency type must be defined"
613            ));
614        }
615
616        // Check for at least one start-blocking type (for task sequencing)
617        let has_start_blocking = self
618            .definitions
619            .values()
620            .any(|d| d.blocks == BlockTarget::Start);
621        if !has_start_blocking {
622            return Err(anyhow::anyhow!(
623                "At least one dependency type with blocks: start must be defined"
624            ));
625        }
626
627        Ok(())
628    }
629}
630
631impl StatesConfig {
632    /// Check if a state is a valid defined state.
633    pub fn is_valid_state(&self, state: &str) -> bool {
634        self.definitions.contains_key(state)
635    }
636
637    /// Check if a transition from one state to another is allowed.
638    pub fn is_valid_transition(&self, from: &str, to: &str) -> bool {
639        if let Some(def) = self.definitions.get(from) {
640            def.exits.contains(&to.to_string())
641        } else {
642            false
643        }
644    }
645
646    /// Check if a state is timed (accumulates duration).
647    pub fn is_timed_state(&self, state: &str) -> bool {
648        self.definitions
649            .get(state)
650            .map(|d| d.timed)
651            .unwrap_or(false)
652    }
653
654    /// Check if a state is terminal (has no exits).
655    pub fn is_terminal_state(&self, state: &str) -> bool {
656        self.definitions
657            .get(state)
658            .map(|d| d.exits.is_empty())
659            .unwrap_or(false)
660    }
661
662    /// Check if a state is a blocking state (blocks dependents).
663    pub fn is_blocking_state(&self, state: &str) -> bool {
664        self.blocking_states.contains(&state.to_string())
665    }
666
667    /// Get all defined state names.
668    pub fn state_names(&self) -> Vec<&str> {
669        self.definitions.keys().map(|s| s.as_str()).collect()
670    }
671
672    /// Get allowed exit states for a given state.
673    pub fn get_exits(&self, state: &str) -> Vec<&str> {
674        self.definitions
675            .get(state)
676            .map(|d| d.exits.iter().map(|s| s.as_str()).collect())
677            .unwrap_or_default()
678    }
679
680    /// Get all untimed state names (valid for disconnect final_state).
681    pub fn untimed_state_names(&self) -> Vec<&str> {
682        self.definitions
683            .iter()
684            .filter(|(_, def)| !def.timed)
685            .map(|(name, _)| name.as_str())
686            .collect()
687    }
688
689    /// Validate the states configuration.
690    pub fn validate(&self) -> Result<()> {
691        // Check initial state exists
692        if !self.definitions.contains_key(&self.initial) {
693            return Err(anyhow!(
694                "Initial state '{}' is not defined in state definitions",
695                self.initial
696            ));
697        }
698
699        // Check disconnect_state exists and is not timed
700        if !self.definitions.contains_key(&self.disconnect_state) {
701            return Err(anyhow!(
702                "Disconnect state '{}' is not defined in state definitions",
703                self.disconnect_state
704            ));
705        }
706        if self.is_timed_state(&self.disconnect_state) {
707            return Err(anyhow!(
708                "Disconnect state '{}' must not be a timed state",
709                self.disconnect_state
710            ));
711        }
712
713        // Check all blocking_states exist
714        for state in &self.blocking_states {
715            if !self.definitions.contains_key(state) {
716                return Err(anyhow!(
717                    "Blocking state '{}' is not defined in state definitions",
718                    state
719                ));
720            }
721        }
722
723        // Check all exit targets exist
724        for (state_name, def) in &self.definitions {
725            for exit in &def.exits {
726                if !self.definitions.contains_key(exit) {
727                    return Err(anyhow!(
728                        "State '{}' has exit '{}' which is not defined",
729                        state_name,
730                        exit
731                    ));
732                }
733            }
734        }
735
736        // Check at least one terminal state exists
737        let has_terminal = self.definitions.values().any(|d| d.exits.is_empty());
738        if !has_terminal {
739            return Err(anyhow!(
740                "At least one terminal state (with empty exits) must be defined"
741            ));
742        }
743
744        Ok(())
745    }
746}
747
748impl Config {
749    /// Load configuration from file.
750    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
751        let content = std::fs::read_to_string(path)?;
752        let config: Config = serde_yaml::from_str(&content)?;
753        Ok(config)
754    }
755
756    /// Load configuration from default locations or return defaults.
757    pub fn load_or_default() -> Self {
758        // Try TASK_GRAPH_CONFIG_PATH environment variable first
759        if let Ok(config_path) = std::env::var("TASK_GRAPH_CONFIG_PATH")
760            && let Ok(config) = Self::load(&config_path) {
761                return config;
762            }
763
764        // Try .task-graph/config.yaml
765        if let Ok(config) = Self::load(".task-graph/config.yaml") {
766            return config;
767        }
768
769        // Fall back to defaults with environment variable overrides
770        let mut config = Self::default();
771
772        if let Ok(db_path) = std::env::var("TASK_GRAPH_DB_PATH") {
773            config.server.db_path = PathBuf::from(db_path);
774        }
775
776        if let Ok(media_dir) = std::env::var("TASK_GRAPH_MEDIA_DIR") {
777            config.server.media_dir = PathBuf::from(media_dir);
778        }
779
780        if let Ok(log_dir) = std::env::var("TASK_GRAPH_LOG_DIR") {
781            config.server.log_dir = PathBuf::from(log_dir);
782        }
783
784        config
785    }
786
787    /// Ensure the database directory exists.
788    pub fn ensure_db_dir(&self) -> Result<()> {
789        if let Some(parent) = self.server.db_path.parent() {
790            std::fs::create_dir_all(parent)?;
791        }
792        Ok(())
793    }
794
795    /// Ensure the media directory exists.
796    pub fn ensure_media_dir(&self) -> Result<()> {
797        std::fs::create_dir_all(&self.server.media_dir)?;
798        Ok(())
799    }
800
801    /// Ensure the log directory exists.
802    pub fn ensure_log_dir(&self) -> Result<()> {
803        std::fs::create_dir_all(&self.server.log_dir)?;
804        Ok(())
805    }
806
807    /// Get the media directory path.
808    pub fn media_dir(&self) -> &Path {
809        &self.server.media_dir
810    }
811
812    /// Get the log directory path.
813    pub fn log_dir(&self) -> &Path {
814        &self.server.log_dir
815    }
816}
817
818/// Tool description override.
819#[derive(Debug, Clone, Serialize, Deserialize)]
820pub struct ToolPrompt {
821    pub description: String,
822}
823
824/// LLM-facing prompts configuration.
825#[derive(Debug, Clone, Serialize, Deserialize, Default)]
826pub struct Prompts {
827    /// Server instructions shown to the LLM.
828    pub instructions: Option<String>,
829
830    /// Tool description overrides by tool name.
831    #[serde(default)]
832    pub tools: HashMap<String, ToolPrompt>,
833}
834
835impl Prompts {
836    /// Load prompts from file.
837    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
838        let content = std::fs::read_to_string(path)?;
839        // Handle empty or comment-only YAML files (which parse as null)
840        let prompts: Option<Prompts> = serde_yaml::from_str(&content)?;
841        Ok(prompts.unwrap_or_default())
842    }
843
844    /// Load prompts from default location or return defaults.
845    pub fn load_or_default() -> Self {
846        // Try .task-graph/prompts.yaml
847        if let Ok(prompts) = Self::load(".task-graph/prompts.yaml") {
848            return prompts;
849        }
850
851        Self::default()
852    }
853
854    /// Get a tool description override if available.
855    pub fn get_tool_description(&self, name: &str) -> Option<&str> {
856        self.tools.get(name).map(|t| t.description.as_str())
857    }
858}