Skip to content

Commit 55d743e

Browse files
committed
fix(agents): write custom agents to loader paths
1 parent 7954d02 commit 55d743e

4 files changed

Lines changed: 140 additions & 86 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cortex-app-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,4 @@ gethostname = "0.5"
7373

7474
[dev-dependencies]
7575
tokio-test = { workspace = true }
76+
tempfile = { workspace = true }

src/cortex-app-server/src/api/agents.rs

Lines changed: 120 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,54 @@ use super::types::{
1515
ImportAgentRequest, UpdateAgentRequest,
1616
};
1717

18+
const PROJECT_AGENTS_DIR: &str = ".cortex/agents";
19+
const LEGACY_PROJECT_AGENTS_DIR: &str = ".factory/agents";
20+
21+
fn project_agents_dir() -> std::path::PathBuf {
22+
std::path::PathBuf::from(PROJECT_AGENTS_DIR)
23+
}
24+
25+
fn legacy_project_agents_dir() -> std::path::PathBuf {
26+
std::path::PathBuf::from(LEGACY_PROJECT_AGENTS_DIR)
27+
}
28+
29+
fn user_agents_dir() -> Option<std::path::PathBuf> {
30+
dirs::home_dir().map(|home| home.join(".cortex/agents"))
31+
}
32+
33+
fn legacy_user_agents_dir() -> Option<std::path::PathBuf> {
34+
dirs::home_dir().map(|home| home.join(".factory/agents"))
35+
}
36+
37+
fn writable_agents_dir(scope: &str) -> AppResult<std::path::PathBuf> {
38+
if scope == "project" {
39+
Ok(project_agents_dir())
40+
} else {
41+
user_agents_dir()
42+
.ok_or_else(|| AppError::Internal("Cannot find home directory".to_string()))
43+
}
44+
}
45+
46+
fn search_agent_dirs() -> Vec<(std::path::PathBuf, &'static str)> {
47+
let mut dirs = vec![
48+
(project_agents_dir(), "project"),
49+
(legacy_project_agents_dir(), "project"),
50+
];
51+
52+
if let Some(user_dir) = user_agents_dir() {
53+
dirs.push((user_dir, "user"));
54+
}
55+
if let Some(user_dir) = legacy_user_agents_dir() {
56+
dirs.push((user_dir, "user"));
57+
}
58+
59+
dirs
60+
}
61+
62+
fn agent_path_in(dir: &std::path::Path, name: &str) -> std::path::PathBuf {
63+
dir.join(format!("{}.md", name))
64+
}
65+
1866
/// Read agent file from disk.
1967
fn read_agent_file(path: &std::path::Path, scope: &str) -> Option<AgentDefinition> {
2068
if path.extension().and_then(|e| e.to_str()) != Some("md") {
@@ -73,28 +121,17 @@ fn read_agent_file(path: &std::path::Path, scope: &str) -> Option<AgentDefinitio
73121
/// List all agents.
74122
pub async fn list_agents() -> AppResult<Json<Vec<AgentDefinition>>> {
75123
let mut agents = Vec::new();
124+
let mut seen_names = std::collections::HashSet::new();
76125

77-
// Project agents (.factory/agents/)
78-
let project_dir = std::path::Path::new(".factory/agents");
79-
if project_dir.exists()
80-
&& let Ok(entries) = std::fs::read_dir(project_dir)
81-
{
82-
for entry in entries.flatten() {
83-
if let Some(agent) = read_agent_file(&entry.path(), "project") {
84-
agents.push(agent);
85-
}
86-
}
87-
}
88-
89-
// User agents (~/.factory/agents/)
90-
if let Some(home) = dirs::home_dir() {
91-
let user_dir = home.join(".factory/agents");
92-
if user_dir.exists()
93-
&& let Ok(entries) = std::fs::read_dir(&user_dir)
126+
for (dir, scope) in search_agent_dirs() {
127+
if dir.exists()
128+
&& let Ok(entries) = std::fs::read_dir(&dir)
94129
{
95130
for entry in entries.flatten() {
96-
if let Some(agent) = read_agent_file(&entry.path(), "user") {
97-
agents.push(agent);
131+
if let Some(agent) = read_agent_file(&entry.path(), scope) {
132+
if seen_names.insert(agent.name.clone()) {
133+
agents.push(agent);
134+
}
98135
}
99136
}
100137
}
@@ -105,16 +142,9 @@ pub async fn list_agents() -> AppResult<Json<Vec<AgentDefinition>>> {
105142

106143
/// Get a specific agent.
107144
pub async fn get_agent(Path(name): Path<String>) -> AppResult<Json<AgentDefinition>> {
108-
// Check project first
109-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
110-
if let Some(agent) = read_agent_file(&project_path, "project") {
111-
return Ok(Json(agent));
112-
}
113-
114-
// Check user
115-
if let Some(home) = dirs::home_dir() {
116-
let user_path = home.join(".factory/agents").join(format!("{}.md", name));
117-
if let Some(agent) = read_agent_file(&user_path, "user") {
145+
for (dir, scope) in search_agent_dirs() {
146+
let path = agent_path_in(&dir, &name);
147+
if let Some(agent) = read_agent_file(&path, scope) {
118148
return Ok(Json(agent));
119149
}
120150
}
@@ -124,13 +154,7 @@ pub async fn get_agent(Path(name): Path<String>) -> AppResult<Json<AgentDefiniti
124154

125155
/// Create or update an agent.
126156
pub async fn create_agent(Json(req): Json<CreateAgentRequest>) -> AppResult<Json<AgentDefinition>> {
127-
let dir = if req.scope == "project" {
128-
std::path::PathBuf::from(".factory/agents")
129-
} else {
130-
dirs::home_dir()
131-
.ok_or_else(|| AppError::Internal("Cannot find home directory".to_string()))?
132-
.join(".factory/agents")
133-
};
157+
let dir = writable_agents_dir(&req.scope)?;
134158

135159
std::fs::create_dir_all(&dir)
136160
.map_err(|e| AppError::Internal(format!("Failed to create directory: {}", e)))?;
@@ -169,19 +193,10 @@ pub async fn create_agent(Json(req): Json<CreateAgentRequest>) -> AppResult<Json
169193

170194
/// Delete an agent.
171195
pub async fn delete_agent(Path(name): Path<String>) -> AppResult<Json<serde_json::Value>> {
172-
// Try project first
173-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
174-
if project_path.exists() {
175-
std::fs::remove_file(&project_path)
176-
.map_err(|e| AppError::Internal(format!("Failed to delete: {}", e)))?;
177-
return Ok(Json(serde_json::json!({"deleted": true})));
178-
}
179-
180-
// Try user
181-
if let Some(home) = dirs::home_dir() {
182-
let user_path = home.join(".factory/agents").join(format!("{}.md", name));
183-
if user_path.exists() {
184-
std::fs::remove_file(&user_path)
196+
for (dir, _scope) in search_agent_dirs() {
197+
let path = agent_path_in(&dir, &name);
198+
if path.exists() {
199+
std::fs::remove_file(&path)
185200
.map_err(|e| AppError::Internal(format!("Failed to delete: {}", e)))?;
186201
return Ok(Json(serde_json::json!({"deleted": true})));
187202
}
@@ -229,21 +244,16 @@ pub async fn update_agent(
229244
Json(req): Json<UpdateAgentRequest>,
230245
) -> AppResult<Json<AgentDefinition>> {
231246
// Find existing agent
232-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
233-
let user_path =
234-
dirs::home_dir().map(|h| h.join(".factory/agents").join(format!("{}.md", name)));
235-
236-
let (existing, path) = if let Some(agent) = read_agent_file(&project_path, "project") {
237-
(agent, project_path)
238-
} else if let Some(ref user_path) = user_path {
239-
if let Some(agent) = read_agent_file(user_path, "user") {
240-
(agent, user_path.clone())
241-
} else {
242-
return Err(AppError::NotFound(format!("Agent not found: {}", name)));
247+
let mut found = None;
248+
for (dir, scope) in search_agent_dirs() {
249+
let path = agent_path_in(&dir, &name);
250+
if let Some(agent) = read_agent_file(&path, scope) {
251+
found = Some((agent, path));
252+
break;
243253
}
244-
} else {
245-
return Err(AppError::NotFound(format!("Agent not found: {}", name)));
246-
};
254+
}
255+
let (existing, path) =
256+
found.ok_or_else(|| AppError::NotFound(format!("Agent not found: {}", name)))?;
247257

248258
// Merge updates
249259
let updated = AgentDefinition {
@@ -352,13 +362,7 @@ pub async fn import_agent(Json(req): Json<ImportAgentRequest>) -> AppResult<Json
352362
let (name, agent) = parse_agent_content(&req.content, &req.format)?;
353363

354364
// Determine directory
355-
let dir = if req.scope == "project" {
356-
std::path::PathBuf::from(".factory/agents")
357-
} else {
358-
dirs::home_dir()
359-
.ok_or_else(|| AppError::Internal("Cannot find home directory".to_string()))?
360-
.join(".factory/agents")
361-
};
365+
let dir = writable_agents_dir(&req.scope)?;
362366

363367
std::fs::create_dir_all(&dir)
364368
.map_err(|e| AppError::Internal(format!("Failed to create directory: {}", e)))?;
@@ -409,3 +413,49 @@ pub async fn generate_agent_prompt(
409413
permission_mode: "default".to_string(),
410414
}))
411415
}
416+
417+
#[cfg(test)]
418+
mod tests {
419+
use super::*;
420+
421+
#[test]
422+
fn test_writable_project_agents_dir_uses_standard_loader_path() {
423+
assert_eq!(
424+
writable_agents_dir("project").unwrap(),
425+
std::path::PathBuf::from(".cortex/agents")
426+
);
427+
}
428+
429+
#[test]
430+
fn test_search_agent_dirs_include_standard_before_legacy_project_path() {
431+
let dirs = search_agent_dirs();
432+
433+
assert_eq!(
434+
dirs[0],
435+
(std::path::PathBuf::from(".cortex/agents"), "project")
436+
);
437+
assert_eq!(
438+
dirs[1],
439+
(std::path::PathBuf::from(".factory/agents"), "project")
440+
);
441+
}
442+
443+
#[test]
444+
fn test_read_agent_file_reads_standard_markdown_agent() {
445+
let dir = tempfile::tempdir().unwrap();
446+
let path = dir.path().join("reviewer.md");
447+
std::fs::write(
448+
&path,
449+
"---\ndescription: Code reviewer\ntools: [\"Read\", \"Grep\"]\nmodel: inherit\npermissionMode: default\n---\n\nReview code carefully.",
450+
)
451+
.unwrap();
452+
453+
let agent = read_agent_file(&path, "project").expect("agent should parse");
454+
455+
assert_eq!(agent.name, "reviewer");
456+
assert_eq!(agent.description, "Code reviewer");
457+
assert_eq!(agent.tools, vec!["Read", "Grep"]);
458+
assert_eq!(agent.scope, "project");
459+
assert_eq!(agent.prompt, "Review code carefully.");
460+
}
461+
}

src/cortex-engine/src/tools/handlers/create_agent.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ impl ToolHandler for CreateAgentHandler {
128128

129129
// Use context.cwd for project location to avoid relying on process cwd
130130
let agents_dir = Self::get_agents_dir_with_cwd(location, &context.cwd)?;
131-
let agent_path = agents_dir.join(format!("{}.toml", agent_name));
131+
let agent_path = agents_dir.join(format!("{}.md", agent_name));
132132

133133
if agent_path.exists() {
134134
return Err(CortexError::InvalidInput(format!(
@@ -139,18 +139,10 @@ impl ToolHandler for CreateAgentHandler {
139139

140140
std::fs::create_dir_all(&agents_dir)?;
141141

142+
let description_yaml =
143+
serde_json::to_string(description).unwrap_or_else(|_| format!("{description:?}"));
142144
let agent_content = format!(
143-
r#"[agent]
144-
name = "{}"
145-
description = """
146-
{}
147-
"""
148-
149-
# Add custom configuration here
150-
# [config]
151-
# key = "value"
152-
"#,
153-
agent_name, description
145+
"---\nname: {agent_name}\ndescription: {description_yaml}\nmodel: inherit\n---\n\n{description}\n"
154146
);
155147

156148
std::fs::write(&agent_path, agent_content)?;
@@ -211,16 +203,26 @@ mod tests {
211203
let expected_name = CreateAgentHandler::sanitize_filename(description);
212204
let agent_path = temp_dir
213205
.path()
214-
.join(format!(".cortex/agents/{}.toml", expected_name));
206+
.join(format!(".cortex/agents/{}.md", expected_name));
215207
assert!(
216208
agent_path.exists(),
217209
"Agent file should exist at {:?}",
218210
agent_path
219211
);
220212

221213
let content = std::fs::read_to_string(&agent_path).unwrap();
222-
assert!(content.contains("name = \"process-and-transform-data-files\""));
214+
assert!(content.contains("name: process-and-transform-data-files"));
215+
assert!(content.contains("model: inherit"));
223216
assert!(content.contains("Process and transform data files"));
217+
218+
let loaded = cortex_agents_ext::custom::loader::sync::load_from_dir(
219+
&temp_dir.path().join(".cortex/agents"),
220+
)
221+
.expect("standard loader should read created agent");
222+
assert_eq!(loaded.len(), 1);
223+
assert_eq!(loaded[0].name, "process-and-transform-data-files");
224+
assert_eq!(loaded[0].description, "Process and transform data files");
225+
assert_eq!(loaded[0].model, "inherit");
224226
}
225227

226228
#[tokio::test]
@@ -325,7 +327,7 @@ mod tests {
325327
let expected_name = CreateAgentHandler::sanitize_filename(description);
326328
let agent_path = temp_dir
327329
.path()
328-
.join(format!(".cortex/agents/{}.toml", expected_name));
330+
.join(format!(".cortex/agents/{}.md", expected_name));
329331
assert!(
330332
agent_path.exists(),
331333
"Agent file should exist at {:?}",
@@ -351,7 +353,7 @@ mod tests {
351353

352354
let agent_path = temp_dir
353355
.path()
354-
.join(".cortex/agents/duplicate-agent-test-case.toml");
356+
.join(".cortex/agents/duplicate-agent-test-case.md");
355357
assert!(
356358
agent_path.exists(),
357359
"First agent file should exist at {:?}",

0 commit comments

Comments
 (0)