Skip to content

Commit bc0ddd2

Browse files
committed
fix(cortex-cli): deduplicate agents by name in JSON output
Fixes bounty issue #1203 When an agent is defined in multiple sources (builtin, personal, project), the agent list JSON output previously showed all duplicates. Now the load_all_agents() function uses a HashMap to deduplicate agents by name, with later sources taking precedence: project > personal > builtin. This ensures users see only one entry per agent name, with their custom definitions correctly overriding built-in agents.
1 parent 7a104aa commit bc0ddd2

File tree

1 file changed

+86
-10
lines changed

1 file changed

+86
-10
lines changed

cortex-cli/src/agent_cmd.rs

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -359,26 +359,34 @@ fn get_project_agents_dir() -> Option<PathBuf> {
359359
}
360360

361361
/// Load all agents from various sources.
362+
///
363+
/// Agents are loaded in order: builtin -> personal -> project.
364+
/// When an agent with the same name exists in multiple sources,
365+
/// the later source takes precedence (project > personal > builtin).
362366
fn load_all_agents() -> Result<Vec<AgentInfo>> {
363-
let mut agents = Vec::new();
367+
let mut agents_map: HashMap<String, AgentInfo> = HashMap::new();
364368

365-
// Load built-in agents
366-
agents.extend(load_builtin_agents());
369+
// Load built-in agents first (lowest priority)
370+
for agent in load_builtin_agents() {
371+
agents_map.insert(agent.name.clone(), agent);
372+
}
367373

368-
// Load personal agents from ~/.cortex/agents/
374+
// Load personal agents from ~/.cortex/agents/ (overrides builtin)
369375
let personal_dir = get_agents_dir()?;
370376
if personal_dir.exists() {
371-
let personal_agents = load_agents_from_dir(&personal_dir, AgentSource::Personal)?;
372-
agents.extend(personal_agents);
377+
for agent in load_agents_from_dir(&personal_dir, AgentSource::Personal)? {
378+
agents_map.insert(agent.name.clone(), agent);
379+
}
373380
}
374381

375-
// Load project agents from .cortex/agents/
382+
// Load project agents from .cortex/agents/ (highest priority, overrides all)
376383
if let Some(project_dir) = get_project_agents_dir() {
377-
let project_agents = load_agents_from_dir(&project_dir, AgentSource::Project)?;
378-
agents.extend(project_agents);
384+
for agent in load_agents_from_dir(&project_dir, AgentSource::Project)? {
385+
agents_map.insert(agent.name.clone(), agent);
386+
}
379387
}
380388

381-
Ok(agents)
389+
Ok(agents_map.into_values().collect())
382390
}
383391

384392
/// Load built-in agents.
@@ -1687,4 +1695,72 @@ This is the system prompt.
16871695
let result = format_color_with_preview("invalid");
16881696
assert_eq!(result, "invalid");
16891697
}
1698+
1699+
#[test]
1700+
fn test_agent_deduplication_by_name() {
1701+
// Simulate the deduplication logic used in load_all_agents()
1702+
let builtin_agent = AgentInfo {
1703+
name: "build".to_string(),
1704+
display_name: Some("Build".to_string()),
1705+
description: Some("Builtin build agent".to_string()),
1706+
mode: AgentMode::Primary,
1707+
native: true,
1708+
hidden: false,
1709+
prompt: None,
1710+
temperature: None,
1711+
top_p: None,
1712+
color: Some("#22c55e".to_string()),
1713+
model: None,
1714+
tools: HashMap::new(),
1715+
allowed_tools: None,
1716+
denied_tools: Vec::new(),
1717+
max_turns: None,
1718+
can_delegate: true,
1719+
tags: vec!["development".to_string()],
1720+
source: AgentSource::Builtin,
1721+
path: None,
1722+
};
1723+
1724+
let personal_agent = AgentInfo {
1725+
name: "build".to_string(), // Same name as builtin
1726+
display_name: Some("Custom Build".to_string()),
1727+
description: Some("Personal build agent override".to_string()),
1728+
mode: AgentMode::Primary,
1729+
native: false,
1730+
hidden: false,
1731+
prompt: Some("Custom prompt".to_string()),
1732+
temperature: Some(0.5),
1733+
top_p: None,
1734+
color: Some("#ff0000".to_string()),
1735+
model: None,
1736+
tools: HashMap::new(),
1737+
allowed_tools: None,
1738+
denied_tools: Vec::new(),
1739+
max_turns: None,
1740+
can_delegate: true,
1741+
tags: vec!["custom".to_string()],
1742+
source: AgentSource::Personal,
1743+
path: Some(PathBuf::from("/home/user/.cortex/agents/build.md")),
1744+
};
1745+
1746+
// Test deduplication: later source should override earlier
1747+
let mut agents_map: HashMap<String, AgentInfo> = HashMap::new();
1748+
agents_map.insert(builtin_agent.name.clone(), builtin_agent);
1749+
agents_map.insert(personal_agent.name.clone(), personal_agent);
1750+
1751+
let agents: Vec<_> = agents_map.into_values().collect();
1752+
1753+
// Should have only one agent named "build"
1754+
let build_agents: Vec<_> = agents.iter().filter(|a| a.name == "build").collect();
1755+
assert_eq!(build_agents.len(), 1);
1756+
1757+
// The personal agent should have overridden the builtin
1758+
let build = build_agents[0];
1759+
assert_eq!(build.source, AgentSource::Personal);
1760+
assert_eq!(
1761+
build.description,
1762+
Some("Personal build agent override".to_string())
1763+
);
1764+
assert!(!build.native);
1765+
}
16901766
}

0 commit comments

Comments
 (0)