@@ -10,7 +10,10 @@ use std::collections::{
1010 HashSet ,
1111} ;
1212use std:: ffi:: OsStr ;
13- use std:: io:: Write ;
13+ use std:: io:: {
14+ self ,
15+ Write ,
16+ } ;
1417use std:: path:: {
1518 Path ,
1619 PathBuf ,
@@ -34,12 +37,12 @@ use serde::{
3437 Serialize ,
3538} ;
3639use thiserror:: Error ;
40+ use tokio:: fs:: ReadDir ;
3741use tracing:: {
3842 error,
3943 info,
4044 warn,
4145} ;
42- use walkdir;
4346use wrapper_types:: ResourcePath ;
4447pub use wrapper_types:: {
4548 OriginalToolName ,
@@ -209,38 +212,6 @@ impl Default for Agent {
209212}
210213
211214impl Agent {
212- /// Calculate the path-based identifier for this agent
213- /// Returns the relative path from agent directory to file (without .json extension)
214- /// Example: "team/assistant" for file at agents/team/assistant.json
215- pub fn path_identifier ( & self , os : & Os ) -> Option < String > {
216- let full_path = self . path . as_ref ( ) ?;
217-
218- // Extract just the filename without extension for fallback
219- let file_stem = full_path. file_stem ( ) ?. to_str ( ) ?;
220-
221- // Try to get the actual directory paths using the proper functions
222- // Check local workspace directory first
223- if let Ok ( local_dir) = directories:: chat_local_agent_dir ( os) {
224- if let Ok ( rel_path) = full_path. strip_prefix ( & local_dir) {
225- if let Some ( path_str) = rel_path. with_extension ( "" ) . to_str ( ) {
226- return Some ( path_str. to_string ( ) ) ;
227- }
228- }
229- }
230-
231- // Check global directory
232- if let Ok ( global_dir) = directories:: chat_global_agent_path ( os) {
233- if let Ok ( rel_path) = full_path. strip_prefix ( & global_dir) {
234- if let Some ( path_str) = rel_path. with_extension ( "" ) . to_str ( ) {
235- return Some ( path_str. to_string ( ) ) ;
236- }
237- }
238- }
239-
240- // Fallback to just filename
241- Some ( file_stem. to_string ( ) )
242- }
243-
244215 /// This function mutates the agent to a state that is writable.
245216 /// Practically this means reverting some fields back to their original values as they were
246217 /// written in the config.
@@ -368,23 +339,39 @@ impl Agent {
368339 }
369340 }
370341
371- /// Retrieves an agent by name or path identifier. It does so via first seeking the given agent
372- /// under local dir, and falling back to global dir if it does not exist in local.
373- /// Supports both JSON name field lookup and path-based lookup (e.g., "team/assistant").
374- /// Load all agents first and filter by both JSON name and path identifier
342+ /// Retrieves an agent by name. It does so via first seeking the given agent under local dir,
343+ /// and falling back to global dir if it does not exist in local.
375344 pub async fn get_agent_by_name ( os : & Os , agent_name : & str ) -> eyre:: Result < ( Agent , PathBuf ) > {
376- let mut stderr = std:: io:: stderr ( ) ;
377- let ( agents, _) = Agents :: load ( & mut os. clone ( ) , None , true , & mut stderr, true ) . await ;
345+ let config_path: Result < PathBuf , PathBuf > = ' config: {
346+ // local first, and then fall back to looking at global
347+ let local_config_dir = directories:: chat_local_agent_dir ( os) ?. join ( format ! ( "{agent_name}.json" ) ) ;
348+ if os. fs . exists ( & local_config_dir) {
349+ break ' config Ok ( local_config_dir) ;
350+ }
378351
379- for ( _, agent) in agents. agents {
380- if agent. name == agent_name || agent. path_identifier ( os) . as_deref ( ) == Some ( agent_name) {
381- if let Some ( path) = agent. path . clone ( ) {
382- return Ok ( ( agent, path) ) ;
383- }
352+ let global_config_dir = directories:: chat_global_agent_path ( os) ?. join ( format ! ( "{agent_name}.json" ) ) ;
353+ if os. fs . exists ( & global_config_dir) {
354+ break ' config Ok ( global_config_dir) ;
384355 }
385- }
386356
387- bail ! ( "Agent {agent_name} does not exist" )
357+ Err ( global_config_dir)
358+ } ;
359+
360+ match config_path {
361+ Ok ( config_path) => {
362+ let content = os. fs . read ( & config_path) . await ?;
363+ let mut agent = serde_json:: from_slice :: < Agent > ( & content) ?;
364+ let legacy_mcp_config = if agent. use_legacy_mcp_json {
365+ load_legacy_mcp_config ( os) . await . unwrap_or ( None )
366+ } else {
367+ None
368+ } ;
369+ let mut stderr = std:: io:: stderr ( ) ;
370+ agent. thaw ( & config_path, legacy_mcp_config. as_ref ( ) , & mut stderr) ?;
371+ Ok ( ( agent, config_path) )
372+ } ,
373+ _ => bail ! ( "Agent {agent_name} does not exist" ) ,
374+ }
388375 }
389376
390377 pub async fn load (
@@ -489,22 +476,14 @@ impl Agents {
489476 self . agents . get_mut ( & self . active_idx )
490477 }
491478
492- pub fn switch ( & mut self , name : & str , os : & Os ) -> eyre:: Result < & Agent > {
493- // Find agent by either JSON name or path identifier
494- let matching_key = self
495- . agents
496- . iter ( )
497- . find ( |( _, agent) | agent. name . as_str ( ) == name || agent. path_identifier ( os) . as_deref ( ) == Some ( name) )
498- . map ( |( key, _) | key. clone ( ) ) ;
499-
500- if let Some ( key) = matching_key {
501- self . active_idx = key;
502- self . agents
503- . get ( & self . active_idx )
504- . ok_or ( eyre:: eyre!( "No agent with name {name} found" ) )
505- } else {
479+ pub fn switch ( & mut self , name : & str ) -> eyre:: Result < & Agent > {
480+ if !self . agents . contains_key ( name) {
506481 eyre:: bail!( "No agent with name {name} found" ) ;
507482 }
483+ self . active_idx = name. to_string ( ) ;
484+ self . agents
485+ . get ( name)
486+ . ok_or ( eyre:: eyre!( "No agent with name {name} found" ) )
508487 }
509488
510489 /// This function does a number of things in the following order:
@@ -579,9 +558,12 @@ impl Agents {
579558 let Ok ( path) = directories:: chat_local_agent_dir ( os) else {
580559 break ' local Vec :: < Agent > :: new ( ) ;
581560 } ;
561+ let Ok ( files) = os. fs . read_dir ( path) . await else {
562+ break ' local Vec :: < Agent > :: new ( ) ;
563+ } ;
582564
583565 let mut agents = Vec :: < Agent > :: new ( ) ;
584- let results = load_agents_from_directory ( & path , os, & mut global_mcp_config, mcp_enabled, output) . await ;
566+ let results = load_agents_from_entries ( files , os, & mut global_mcp_config, mcp_enabled, output) . await ;
585567 for result in results {
586568 match result {
587569 Ok ( agent) => agents. push ( agent) ,
@@ -606,9 +588,20 @@ impl Agents {
606588 let Ok ( path) = directories:: chat_global_agent_path ( os) else {
607589 break ' global Vec :: < Agent > :: new ( ) ;
608590 } ;
591+ let files = match os. fs . read_dir ( & path) . await {
592+ Ok ( files) => files,
593+ Err ( e) => {
594+ if matches ! ( e. kind( ) , io:: ErrorKind :: NotFound ) {
595+ if let Err ( e) = os. fs . create_dir_all ( & path) . await {
596+ error ! ( "Error creating global agent dir: {:?}" , e) ;
597+ }
598+ }
599+ break ' global Vec :: < Agent > :: new ( ) ;
600+ } ,
601+ } ;
609602
610603 let mut agents = Vec :: < Agent > :: new ( ) ;
611- let results = load_agents_from_directory ( & path , os, & mut global_mcp_config, mcp_enabled, output) . await ;
604+ let results = load_agents_from_entries ( files , os, & mut global_mcp_config, mcp_enabled, output) . await ;
612605 for result in results {
613606 match result {
614607 Ok ( agent) => agents. push ( agent) ,
@@ -711,14 +704,8 @@ impl Agents {
711704 // 3. If the above is missing or invalid, assume the in-memory default
712705 let active_idx = ' active_idx: {
713706 if let Some ( name) = agent_name {
714- // Dual lookup: try both JSON name field and path identifier
715- if let Some ( matching_agent) = all_agents. iter ( ) . find ( |agent| {
716- // Current behavior: match against JSON name field
717- agent. name . as_str ( ) == name ||
718- // New behavior: match against file path identifier
719- agent. path_identifier ( os) . as_deref ( ) == Some ( name)
720- } ) {
721- break ' active_idx matching_agent. name . clone ( ) ;
707+ if all_agents. iter ( ) . any ( |a| a. name . as_str ( ) == name) {
708+ break ' active_idx name. to_string ( ) ;
722709 }
723710 let _ = queue ! (
724711 output,
@@ -893,41 +880,24 @@ pub struct AgentsLoadMetadata {
893880 pub launched_agent : String ,
894881}
895882
896- async fn load_agents_from_directory (
897- dir_path : & Path ,
883+ async fn load_agents_from_entries (
884+ mut files : ReadDir ,
898885 os : & Os ,
899886 global_mcp_config : & mut Option < McpServerConfig > ,
900887 mcp_enabled : bool ,
901888 output : & mut impl Write ,
902889) -> Vec < Result < Agent , AgentConfigError > > {
903890 let mut res = Vec :: < Result < Agent , AgentConfigError > > :: new ( ) ;
904891
905- // Check if directory exists before trying to walk it
906- if !os. fs . exists ( dir_path) {
907- // Directory doesn't exist - return empty list (this is normal)
908- return res;
909- }
910-
911- // Collect file paths in a blocking task to avoid blocking the async runtime
912- let dir_path = dir_path. to_path_buf ( ) ;
913- let file_paths = tokio:: task:: spawn_blocking ( move || {
914- walkdir:: WalkDir :: new ( & dir_path)
915- . follow_links ( false )
916- . into_iter ( )
917- . filter_map ( |e| e. ok ( ) )
918- . filter ( |entry| {
919- let path = entry. path ( ) ;
920- path. is_file ( ) && path. extension ( ) . and_then ( OsStr :: to_str) . is_some_and ( |s| s == "json" )
921- } )
922- . map ( |entry| entry. path ( ) . to_path_buf ( ) )
923- . collect :: < Vec < _ > > ( )
924- } )
925- . await
926- . unwrap_or_default ( ) ;
927-
928- // Load agents asynchronously
929- for file_path in file_paths {
930- res. push ( Agent :: load ( os, & file_path, global_mcp_config, mcp_enabled, output) . await ) ;
892+ while let Ok ( Some ( file) ) = files. next_entry ( ) . await {
893+ let file_path = & file. path ( ) ;
894+ if file_path
895+ . extension ( )
896+ . and_then ( OsStr :: to_str)
897+ . is_some_and ( |s| s == "json" )
898+ {
899+ res. push ( Agent :: load ( os, file_path, global_mcp_config, mcp_enabled, output) . await ) ;
900+ }
931901 }
932902
933903 res
@@ -1100,10 +1070,9 @@ mod tests {
11001070 ) ;
11011071 }
11021072
1103- #[ tokio :: test]
1104- async fn test_switch ( ) {
1073+ #[ test]
1074+ fn test_switch ( ) {
11051075 let mut collection = Agents :: default ( ) ;
1106- let os = crate :: os:: Os :: new ( ) . await . unwrap ( ) ;
11071076
11081077 let default_agent = Agent :: default ( ) ;
11091078 let dev_agent = Agent {
@@ -1117,12 +1086,12 @@ mod tests {
11171086 collection. active_idx = "default" . to_string ( ) ;
11181087
11191088 // Test successful switch
1120- let result = collection. switch ( "dev" , & os ) ;
1089+ let result = collection. switch ( "dev" ) ;
11211090 assert ! ( result. is_ok( ) ) ;
11221091 assert_eq ! ( result. unwrap( ) . name, "dev" ) ;
11231092
11241093 // Test switch to non-existent agent
1125- let result = collection. switch ( "nonexistent" , & os ) ;
1094+ let result = collection. switch ( "nonexistent" ) ;
11261095 assert ! ( result. is_err( ) ) ;
11271096 assert_eq ! ( result. unwrap_err( ) . to_string( ) , "No agent with name nonexistent found" ) ;
11281097 }
@@ -1610,94 +1579,4 @@ mod tests {
16101579 let result = agent. resolve_prompt ( ) ;
16111580 assert ! ( result. is_err( ) ) ;
16121581 }
1613-
1614- #[ tokio:: test]
1615- async fn test_path_identifier ( ) {
1616- use std:: path:: PathBuf ;
1617-
1618- // Create a mock Os for testing
1619- let os = crate :: os:: Os :: new ( ) . await . unwrap ( ) ;
1620-
1621- // Get the actual OS paths for testing
1622- let local_dir = directories:: chat_local_agent_dir ( & os) . unwrap ( ) ;
1623- let global_dir = directories:: chat_global_agent_path ( & os) . unwrap ( ) ;
1624-
1625- // Test workspace agent path using actual OS paths
1626- let mut agent = Agent :: default ( ) ;
1627- agent. path = Some ( local_dir. join ( "team/assistant.json" ) ) ;
1628- assert_eq ! ( agent. path_identifier( & os) , Some ( "team/assistant" . to_string( ) ) ) ;
1629-
1630- // Test global agent path using actual OS paths
1631- agent. path = Some ( global_dir. join ( "org/specialist.json" ) ) ;
1632- assert_eq ! ( agent. path_identifier( & os) , Some ( "org/specialist" . to_string( ) ) ) ;
1633-
1634- // Test nested path using actual OS paths
1635- agent. path = Some ( global_dir. join ( "company/team/expert.json" ) ) ;
1636- assert_eq ! ( agent. path_identifier( & os) , Some ( "company/team/expert" . to_string( ) ) ) ;
1637-
1638- // Test simple filename (fallback) - path that doesn't match agent directories
1639- agent. path = Some ( PathBuf :: from ( "/some/other/path/simple.json" ) ) ;
1640- assert_eq ! ( agent. path_identifier( & os) , Some ( "simple" . to_string( ) ) ) ;
1641-
1642- // Test no path
1643- agent. path = None ;
1644- assert_eq ! ( agent. path_identifier( & os) , None ) ;
1645-
1646- // Test cross-platform path normalization using actual OS paths
1647- agent. path = Some ( global_dir. join ( "dev" ) . join ( "helper.json" ) ) ;
1648- assert_eq ! ( agent. path_identifier( & os) , Some ( "dev/helper" . to_string( ) ) ) ;
1649- }
1650-
1651- #[ tokio:: test]
1652- async fn test_switch_with_path_identifier ( ) {
1653- let mut collection = Agents :: default ( ) ;
1654- let os = crate :: os:: Os :: new ( ) . await . unwrap ( ) ;
1655-
1656- // Get the actual OS paths for testing
1657- let global_dir = directories:: chat_global_agent_path ( & os) . unwrap ( ) ;
1658-
1659- // Create agents with different paths using actual OS paths
1660- let mut agent1 = Agent {
1661- name : "helper" . to_string ( ) ,
1662- ..Default :: default ( )
1663- } ;
1664- agent1. path = Some ( global_dir. join ( "dev/helper.json" ) ) ;
1665-
1666- let mut agent2 = Agent {
1667- name : "assistant" . to_string ( ) ,
1668- ..Default :: default ( )
1669- } ;
1670- agent2. path = Some ( global_dir. join ( "team/assistant.json" ) ) ;
1671-
1672- collection. agents . insert ( "helper" . to_string ( ) , agent1) ;
1673- collection. agents . insert ( "assistant" . to_string ( ) , agent2) ;
1674- collection. active_idx = "helper" . to_string ( ) ;
1675-
1676- // Test switch by JSON name (existing behavior)
1677- let result = collection. switch ( "assistant" , & os) ;
1678- assert ! ( result. is_ok( ) ) ;
1679- assert_eq ! ( result. unwrap( ) . name, "assistant" ) ;
1680-
1681- // Test switch by path identifier (new behavior)
1682- let result = collection. switch ( "dev/helper" , & os) ;
1683- assert ! ( result. is_ok( ) ) ;
1684- assert_eq ! ( result. unwrap( ) . name, "helper" ) ;
1685-
1686- // Test switch by nested path identifier
1687- let result = collection. switch ( "team/assistant" , & os) ;
1688- assert ! ( result. is_ok( ) ) ;
1689- assert_eq ! ( result. unwrap( ) . name, "assistant" ) ;
1690-
1691- // Test switch to non-existent agent (both name and path)
1692- let result = collection. switch ( "nonexistent" , & os) ;
1693- assert ! ( result. is_err( ) ) ;
1694- assert_eq ! ( result. unwrap_err( ) . to_string( ) , "No agent with name nonexistent found" ) ;
1695-
1696- let result = collection. switch ( "nonexistent/path" , & os) ;
1697- assert ! ( result. is_err( ) ) ;
1698- assert_eq ! (
1699- result. unwrap_err( ) . to_string( ) ,
1700- "No agent with name nonexistent/path found"
1701- ) ;
1702- }
17031582}
0 commit comments