Skip to content

Commit e478ba2

Browse files
authored
Revert "feat: Path based loading for agent (#2859)" (#3257)
This reverts commit 26caae9.
1 parent 26caae9 commit e478ba2

File tree

4 files changed

+83
-204
lines changed

4 files changed

+83
-204
lines changed

crates/chat-cli/src/cli/agent/mod.rs

Lines changed: 73 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use std::collections::{
1010
HashSet,
1111
};
1212
use std::ffi::OsStr;
13-
use std::io::Write;
13+
use std::io::{
14+
self,
15+
Write,
16+
};
1417
use std::path::{
1518
Path,
1619
PathBuf,
@@ -34,12 +37,12 @@ use serde::{
3437
Serialize,
3538
};
3639
use thiserror::Error;
40+
use tokio::fs::ReadDir;
3741
use tracing::{
3842
error,
3943
info,
4044
warn,
4145
};
42-
use walkdir;
4346
use wrapper_types::ResourcePath;
4447
pub use wrapper_types::{
4548
OriginalToolName,
@@ -209,38 +212,6 @@ impl Default for Agent {
209212
}
210213

211214
impl 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
}

crates/chat-cli/src/cli/agent/root_command_args.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ impl AgentArgs {
286286
},
287287
Some(AgentSubcommands::SetDefault { name }) => {
288288
let mut agents = Agents::load(os, None, true, &mut stderr, mcp_enabled).await.0;
289-
match agents.switch(&name, os) {
289+
match agents.switch(&name) {
290290
Ok(agent) => {
291291
os.database
292292
.settings
@@ -352,7 +352,7 @@ pub async fn create_agent(
352352
}
353353

354354
let prepopulated_content = if let Some(from) = from {
355-
let mut agent_to_copy = agents.switch(from.as_str(), os)?.clone();
355+
let mut agent_to_copy = agents.switch(from.as_str())?.clone();
356356
agent_to_copy.name = name.clone();
357357
agent_to_copy
358358
} else {

0 commit comments

Comments
 (0)