Skip to content

Add multi-agent tool support and docs#2

Open
Grinsven wants to merge 22 commits intomainfrom
agents-multi-final
Open

Add multi-agent tool support and docs#2
Grinsven wants to merge 22 commits intomainfrom
agents-multi-final

Conversation

@Grinsven
Copy link
Copy Markdown
Owner

Summary

  • reintroduce agent protocol and handler wiring; pass registry through core
  • add TUI /agents list, @mentions with plan integration
  • document multi-agent usage and provide example agents config

Testing

  • cargo test -p codex-core --tests
  • cargo test -p codex-tui

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @Grinsven, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the system's capabilities by integrating a robust multi-agent orchestration framework. It allows users to define and interact with specialized AI agents through intuitive TUI commands and @mentions, streamlining complex task delegation. The changes encompass core logic for agent management, secure prompt handling, real-time feedback mechanisms, and comprehensive documentation, laying the groundwork for more sophisticated AI-driven workflows.

Highlights

  • Multi-Agent Tool Support: Reintroduced the agent protocol and handler wiring, enabling the core system to support multi-agent interactions and passing the agent registry through the core session flow.
  • TUI Integration for Agents: Added a new /agents command to list available agents and implemented @mention handling within the TUI, allowing users to delegate tasks to specific agents with automatic plan integration.
  • Comprehensive Documentation: Provided extensive documentation for multi-agent usage, including a new docs/subagents.md guide, a quickstart in docs/getting-started.md, and an example agents.toml configuration file.
  • Agent Registry and Configuration: Introduced an AgentRegistry to manage agent configurations, including loading custom agents from ~/.codex/agents.toml, validating prompt paths to prevent security vulnerabilities, and providing a default 'general' agent.
  • Agent Execution and Event Streaming: Implemented the AgentHandler to execute agent tools, stream real-time progress events (begin, progress, end, message deltas, reasoning deltas) back to the client, and prevent recursive agent spawning.
  • TurnDiffTracker Merge Functionality: Added a merge function to TurnDiffTracker to correctly combine changes from parallel tool executions, ensuring consistent state tracking across complex agent workflows.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant new functionality by adding multi-agent tool support. The changes are extensive, touching core logic, the TUI, and documentation. The implementation of the agent registry, tool handler, and TUI components appears robust and well-designed. The path validation for agent prompts is a good security measure. The documentation is comprehensive and will be very helpful for users. I've found a couple of areas for improvement: one related to improving platform compatibility for locating the home directory, and another to address a potential correctness issue with concurrent agent calls.

Comment on lines +162 to +164
if item_id.is_empty() {
item_id = "agent-call".to_string();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using a hardcoded string "agent-call" as a fallback for an empty item_id could lead to issues if multiple agent tool calls with empty call_ids are processed concurrently. This would result in multiple agent executions sharing the same item_id, which could cause confusion in clients trying to track them. A unique ID should be generated instead. You can use the rand crate for this, which is already a dependency. You'll need to add use rand::Rng; at the top of the file.

Suggested change
if item_id.is_empty() {
item_id = "agent-call".to_string();
}
if item_id.is_empty() {
let random_id: u64 = rand::thread_rng().gen();
item_id = format!("agent-call-{}", random_id);
}

Comment on lines +229 to +234
fn get_agents_directory() -> Option<PathBuf> {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()
.map(|home| PathBuf::from(home).join(".codex"))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation for finding the home directory is platform-specific and less robust than using the dirs crate, which is already a dependency in this file. Using dirs::home_dir() provides a more reliable and cross-platform way to locate the user's home directory.

    fn get_agents_directory() -> Option<PathBuf> {
        dirs::home_dir().map(|home| home.join(".codex"))
    }

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

@Grinsven
Copy link
Copy Markdown
Owner Author

@jules

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive support for a multi-agent system. The changes are well-structured, spanning from core logic for agent registration and execution to TUI enhancements for user interaction, including a new /agents command and @mention support. The new agent tool is cleanly integrated into the existing tool handling infrastructure. I'm particularly impressed with the thoroughness of the implementation, which includes security considerations like path traversal prevention for agent prompts and robust error handling. The accompanying documentation is extensive and provides clear guidance for users.

I have one minor suggestion to improve code consistency in the new agent.rs module. Overall, this is an excellent addition to the project.

Comment on lines +229 to +234
fn get_agents_directory() -> Option<PathBuf> {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()
.map(|home| PathBuf::from(home).join(".codex"))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency and robustness, consider using dirs::home_dir() to determine the home directory, as is done elsewhere in this file (e.g., in validate_prompt_path). The dirs crate is designed to handle cross-platform differences and edge cases for locating user directories.

    fn get_agents_directory() -> Option<PathBuf> {
        dirs::home_dir().map(|home| home.join(".codex"))
    }

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive support for a multi-agent system, including core logic for an agent registry, a tool handler for invoking agents, and TUI integration for listing agents and using @mentions. The changes are well-documented with a new subagents.md guide and example configurations. My review found a high-severity security vulnerability related to path traversal and a medium-severity bug in the TUI's agent mention parsing. I've provided suggestions to address both issues.


// Try to load user agents from ~/.codex/agents.toml
let agents_dir = Self::get_agents_directory();
if let Some(ref dir) = agents_dir {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a path traversal vulnerability here. If a user creates a symbolic link from ~/.codex to a directory like /, the validate_prompt_path function can be tricked into allowing access to any file on the system (e.g., /etc/passwd). This is because canonicalize() resolves the symlink, and the subsequent starts_with check will pass.

To mitigate this, I recommend disallowing the agents directory from being a symlink altogether. This is a safer approach for a security-sensitive operation like loading agent prompts.

        if let Some(ref dir) = agents_dir {
            if dir.symlink_metadata().map_or(false, |m| m.is_symlink()) {
                tracing::error!(
                    "Security: agents directory at '{}' is a symlink, which is not allowed. Aborting loading of user-defined agents.",
                    dir.display()
                );
                return Ok(Self { agents, agents_dir });
            }

Comment thread codex-rs/tui/src/agent_mention.rs Outdated
// Compile regex once at startup
#[allow(clippy::expect_used)]
static AGENT_MENTION_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"@(\w+):?\s+([^\n@]+)")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regex for parsing agent mentions uses \w+ for the agent name, which only matches alphanumeric characters and underscores. However, TOML keys (used for agent names in agents.toml) can contain hyphens. The documentation in docs/subagents.md even provides code-reviewer as an example agent name.

This regex will fail to correctly parse mentions for agents with hyphens in their names. To fix this, the character class for the agent name should be updated to include hyphens.

Suggested change
Regex::new(r"@(\w+):?\s+([^\n@]+)")
Regex::new(r"@([\w-]+):?\s+([^\n@]+)")

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant new functionality by adding multi-agent support. The changes include core logic for an agent registry, configuration handling, and a tool handler for invoking agents, complete with security measures to prevent path traversal and agent recursion. The TUI is also enhanced with support for agent mentions (@agent: task) and a new /agents command. The documentation has been extensively updated to reflect these new features. My review identifies a critical discrepancy where the implementation prevents sub-agents from using any tools, which contradicts the documentation. The provided feedback aims to rectify this by enabling tool inheritance for agents, which is crucial for the feature's usability.

Comment on lines +141 to +151
let prompt = Prompt {
input: vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text: task_text }],
}],
tools: Vec::new(),
parallel_tool_calls: false,
base_instructions_override: Some(instructions),
output_schema: None,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Sub-agents are currently unable to use any tools as an empty tool list is passed to their prompt. This contradicts the documentation which states that agents should inherit tools from the parent context.

To enable tool inheritance, you can reconstruct the toolset for the sub-agent, making sure to exclude the agent tool to prevent recursion.

        let mcp_tools = session
            .services
            .mcp_connection_manager
            .read()
            .await
            .list_all_tools()
            .await
            .map_err(|e| FunctionCallError::Fatal(e.to_string()))?
            .into_iter()
            .map(|(name, tool)| (name, tool.tool))
            .collect();

        let mut sub_tools_config = turn.tools_config.clone();
        sub_tools_config.include_agent_tool = false;

        let agent_tools = crate::tools::spec::build_specs(&sub_tools_config, Some(mcp_tools))
            .build()
            .specs()
            .to_vec();

        let prompt = Prompt {
            input: vec![ResponseItem::Message {
                id: None,
                role: "user".to_string(),
                content: vec![ContentItem::InputText { text: task_text }],
            }],
            tools: agent_tools,
            parallel_tool_calls: false,
            base_instructions_override: Some(instructions),
            output_schema: None,
        };

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant new functionality by adding multi-agent support. The changes are comprehensive, covering core logic, TUI integration, and extensive documentation. The implementation of the agent registry and tool handlers is well-structured. I've identified a critical compilation error and a couple of areas where the logic for security and recursion prevention could be clarified and strengthened. Overall, this is a solid contribution that greatly expands the capabilities of the system.

let mut message = String::new();
let mut item_id = call_id.clone();
if item_id.is_empty() {
let random_id: u64 = rand::rng().random();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This line will cause a compilation error because rand::rng() is not a valid function call. You likely intended to use rand::thread_rng() to get a thread-local random number generator.

Suggested change
let random_id: u64 = rand::rng().random();
let random_id: u64 = rand::thread_rng().random();

Comment thread codex-rs/core/src/agent.rs Outdated
Comment on lines +124 to +131
let canonical_home = home_codex.canonicalize().unwrap_or(home_codex);

// Security check: path must be within ~/.codex or the base directory
if !canonical.starts_with(&canonical_home) && !canonical.starts_with(&canonical_base) {
return Err(anyhow::anyhow!(
"Security error: Prompt file must be within ~/.codex or the configured agents directory"
));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of unwrap_or on the result of canonicalize() can be confusing and potentially less safe. If home_codex.canonicalize() fails (e.g., because ~/.codex doesn't exist), canonical_home will be a non-canonicalized path, which is then compared to a canonicalized path. This can lead to subtle bugs.

A clearer and safer approach is to handle the Result from canonicalize() explicitly. If it fails, we can safely assume the path is not within ~/.codex.

        let is_in_home_codex = if let Ok(canonical_home) = home_codex.canonicalize() {
            canonical.starts_with(&canonical_home)
        } else {
            false
        };
        // Security check: path must be within ~/.codex or the base directory
        if !is_in_home_codex && !canonical.starts_with(&canonical_base) {
            return Err(anyhow::anyhow!(
                "Security error: Prompt file must be within ~/.codex or the configured agents directory"
            ));
        }

Comment on lines +106 to +112
// Prevent recursion by refusing to run if the tool list already excludes agent tool.
if matches!(turn.client.get_session_source(), SessionSource::SubAgent(_)) {
warn!("agent tool recursion detected");
return Err(FunctionCallError::RespondToModel(
"Agents cannot spawn other agents".to_string(),
));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This recursion check is likely ineffective. The SessionSource is not updated for the sub-agent's ModelClient, so turn.client.get_session_source() will return the parent's source, and the SubAgent match arm will never be hit.

The effective recursion prevention is on line 153, where include_agent_tool is set to false for the sub-agent.

To avoid confusion, I recommend removing this check. If it's intended as a defense-in-depth measure, the ModelClient for the sub-agent would need to be created with SessionSource::SubAgent.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive multi-agent system, a significant and well-implemented new feature. The changes include the core agent registry, a tool handler for invoking agents, TUI integration for @mentions and the /agents command, and extensive documentation. The overall architecture is solid, with good security considerations like recursion prevention and path validation for prompt files.

My review includes a critical security fix for a potential path traversal vulnerability in AgentConfig::get_prompt. I've also included a couple of medium-severity suggestions to improve code clarity and adhere to best practices, such as renaming a misleadingly named function and replacing a deprecated RNG function.

The documentation additions are excellent and will be very helpful for users to understand and adopt this new feature. Great work on this complex addition!

Comment on lines +53 to +77
/// Get the effective prompt, loading from file if necessary
pub fn get_prompt(&mut self, agents_dir: Option<&Path>) -> anyhow::Result<String> {
if let Some(prompt) = &self.prompt {
return Ok(prompt.clone());
}

if let Some(prompt_file) = &self.prompt_file {
let full_path = if let Some(dir) = agents_dir {
dir.join(prompt_file)
} else {
PathBuf::from(prompt_file)
};

let prompt_content = std::fs::read_to_string(&full_path).map_err(|e| {
anyhow::anyhow!("Cannot read prompt file '{}': {}", full_path.display(), e)
})?;

// Cache the loaded prompt
self.prompt = Some(prompt_content.clone());
Ok(prompt_content)
} else {
Err(anyhow::anyhow!("No prompt or prompt_file specified"))
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The get_prompt function is vulnerable to a path traversal attack. It constructs a file path from prompt_file and reads it without validation. If an AgentConfig is constructed manually with a malicious prompt_file (e.g., ../../../etc/passwd), this method could be used to read arbitrary files from the filesystem. The validation performed in AgentRegistry::new does not protect against direct calls to this public method.

To fix this, get_prompt must validate the path before reading from it. You can reuse the existing validate_prompt_path logic for this, but you'll need to make it accessible from AgentConfig (e.g., by making it a pub(crate) or pub function). Additionally, agents_dir should be required when loading from a file to provide a base for validation.

    pub fn get_prompt(&mut self, agents_dir: &Path) -> anyhow::Result<String> {
        if let Some(prompt) = &self.prompt {
            return Ok(prompt.clone());
        }

        if let Some(prompt_file) = &self.prompt_file {
            let safe_path = AgentRegistry::validate_prompt_path(agents_dir, prompt_file)?;

            let prompt_content = std::fs::read_to_string(&safe_path).map_err(|e| {
                anyhow::anyhow!("Cannot read prompt file '{}': {}", safe_path.display(), e)
            })?;

            // Cache the loaded prompt
            self.prompt = Some(prompt_content.clone());
            Ok(prompt_content)
        } else {
            Err(anyhow::anyhow!("No prompt or prompt_file specified"))
        }
    }

Comment on lines +322 to +337
#[allow(dead_code)]
pub async fn execute_agent_task(
agent_name: &str,
task: String,
registry: &AgentRegistry,
) -> Result<String> {
// Get the agent's system prompt
let system_prompt = registry.get_system_prompt(agent_name);

// Build the specialized prompt for this agent
let full_prompt = format!("{system_prompt}\n\nTask: {task}");

// Note: The actual execution will be handled by the parent context
// using the existing conversation infrastructure
Ok(full_prompt)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The function execute_agent_task is misleadingly named, as it doesn't execute a task but rather prepares a prompt string for it. It's also marked as async but contains no asynchronous operations, which could confuse future maintainers.

#[allow(dead_code)]
pub fn prepare_agent_task_prompt(
    agent_name: &str,
    task: String,
    registry: &AgentRegistry,
) -> Result<String> {
    // Get the agent's system prompt
    let system_prompt = registry.get_system_prompt(agent_name);

    // Build the specialized prompt for this agent
    let full_prompt = format!("{system_prompt}\n\nTask: {task}");

    // Note: The actual execution will be handled by the parent context
    // using the existing conversation infrastructure
    Ok(full_prompt)
}

Comment on lines +182 to +183
let random_id: u64 = rand::rng().random();
item_id = format!("agent-call-{random_id}");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The rand::rng() function is deprecated. The documentation recommends using rand::thread_rng() for a thread-local random number generator. While not a security risk here, it's best practice to avoid deprecated functions.

            let random_id: u64 = rand::thread_rng().gen();
            item_id = format!("agent-call-{random_id}");

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-implemented multi-agent system. The changes include the core logic for an agent registry, a tool handler for invoking agents, and UI integrations for mentions and listing agents. The security aspects, such as preventing path traversal and agent recursion, are thoughtfully handled. The documentation is comprehensive and will be very helpful for users. My review includes a couple of suggestions: one to enable parallel agent execution to align with documented use cases, and a security hardening to prevent symlink abuse on the agent configuration file. Overall, this is a solid contribution.

return Ok(Self { agents, agents_dir });
}
let config_path = dir.join("agents.toml");
if config_path.exists() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's a potential security vulnerability here. The code checks if the ~/.codex directory is a symlink, but it doesn't check if ~/.codex/agents.toml itself is a symlink. std::fs::read_to_string follows symlinks, which could lead to reading sensitive files if agents.toml is a symlink pointing elsewhere (e.g., ~/.ssh/id_rsa). While parsing would likely fail, this could still leak information about file existence or content in error messages. It's best to explicitly check for and disallow symlinks for the configuration file itself.

            if config_path.symlink_metadata().map_or(false, |m| m.is_symlink()) {
                tracing::error!(
                    "Security: agents config file at '{}' is a symlink, which is not allowed. Aborting loading of user-defined agents.",
                    config_path.display()
                );
                return Ok(Self { agents, agents_dir });
            }
            if config_path.exists() {

Comment thread codex-rs/core/src/tools/spec.rs Outdated
}

if config.include_agent_tool {
builder.push_spec(create_agent_tool());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The agent tool is not configured to support parallel execution, but the documentation in AGENTS.md suggests that multiple agents (like @code_reviewer and @code_reviewer_b) could be run concurrently. Enabling parallel tool calls for the agent tool would align with this documented behavior and allow for more efficient multi-agent workflows. The AgentHandler appears to be stateless and safe for parallel execution.

Suggested change
builder.push_spec(create_agent_tool());
builder.push_spec_with_parallel_support(create_agent_tool(), true);

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant multi-agent support, including core logic, TUI integration, and comprehensive documentation. The implementation is well-structured, with good attention to security, particularly in loading agent prompts. The TUI changes also provide a solid user experience for agent interaction. I have identified a few areas for improvement, including a potential bug in event persistence and a suggestion for code simplification to enhance maintainability.

Comment on lines +81 to +83
| EventMsg::AgentBegin(_)
| EventMsg::AgentProgress(_)
| EventMsg::AgentEnd(_)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

AgentBegin and AgentEnd events are important for understanding the session flow during replay or debugging and should be persisted in the rollout file. ListAgentsResponse and AgentProgress are more transient and are fine to exclude.

Suggested change
| EventMsg::AgentBegin(_)
| EventMsg::AgentProgress(_)
| EventMsg::AgentEnd(_)
| EventMsg::AgentProgress(_)

Comment thread AGENTS.md
- If the user asks for "a debugger", run @debugger.
- If the user asks for "research", run @researcher.
- Default scope when not provided: latest two commits in the current repo.
- Always cite file:line in findings and keep summaries concise. No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's a common convention to have a newline at the end of text files. Please add one here for consistency.

Comment on lines +184 to +218
if let Some(ref prompt_file) = config.prompt_file {
// Validate the path to prevent traversal attacks
match Self::validate_prompt_path(dir, prompt_file) {
Ok(safe_path) => {
match std::fs::read_to_string(&safe_path) {
Ok(prompt_content) => {
config.prompt = Some(prompt_content);
tracing::debug!(
"Loaded prompt file for agent '{}'",
name
);
}
Err(e) => {
tracing::error!(
"Cannot read prompt file '{}' for agent '{}': {}",
prompt_file,
name,
e
);
// Skip this agent but continue loading others
continue;
}
}
}
Err(e) => {
tracing::error!(
"Agent '{}' configuration error: {}",
name,
e
);
// Skip this agent but continue loading others
continue;
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code for loading prompts from files is a bit deeply nested. It could be simplified by chaining Result operations, which would improve readability and maintainability.

                                    if let Some(ref prompt_file) = config.prompt_file {
                                        match Self::validate_prompt_path(dir, prompt_file).and_then(|path| {
                                            std::fs::read_to_string(&path).map_err(|e| {
                                                anyhow::anyhow!(
                                                    "Cannot read prompt file '{}': {}",
                                                    path.display(),
                                                    e
                                                )
                                            })
                                        }) {
                                            Ok(prompt_content) => {
                                                config.prompt = Some(prompt_content);
                                                tracing::debug!("Loaded prompt file for agent '{}'", name);
                                            }
                                            Err(e) => {
                                                tracing::error!("Agent '{}' configuration error: {}", name, e);
                                                continue;
                                            }
                                        }
                                    }

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

@Grinsven
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant multi-agent support, including a core agent registry, a tool handler for agent invocation, TUI integration for agent mentions and listing, and comprehensive documentation. The implementation is well-structured with strong attention to security aspects like path traversal and symlink protection, and robustness features such as agent recursion prevention. The new documentation is excellent and thorough. I have a couple of suggestions for minor refactoring to improve maintainability and correctness, but overall, this is a very solid and well-executed feature addition.

Comment on lines +190 to +225
// If prompt_file is specified, load the prompt from file
if let Some(ref prompt_file) = config.prompt_file {
// Validate the path to prevent traversal attacks
match Self::validate_prompt_path(dir, prompt_file).and_then(|path| {
std::fs::read_to_string(&path).map_err(|e| {
anyhow::anyhow!(
"Cannot read prompt file '{}': {}",
path.display(),
e
)
})
}) {
Ok(prompt_content) => {
config.prompt = Some(prompt_content);
tracing::debug!(
"Loaded prompt file for agent '{}'",
name
);
}
Err(e) => {
tracing::error!(
"Agent '{}' configuration error: {}",
name,
e
);
// Skip this agent but continue loading others
continue;
}
}
}

agents.insert(name, config);
}
tracing::info!("Loaded {} user-defined agents", agents.len() - 1);
}
Err(e) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for loading a prompt from prompt_file is duplicated in AgentRegistry::new and AgentConfig::get_prompt. The new function eagerly loads prompts, while get_prompt supports lazy loading. This duplication can lead to maintenance issues where one implementation is updated but the other is not.

To improve this, you can refactor AgentRegistry::new to call config.get_prompt() to load prompts, which will centralize the logic and remove the duplicated code. This would make the code more maintainable.

                                    // If prompt_file is specified, load the prompt from file
                                    if config.prompt_file.is_some() {
                                        if let Err(e) = config.get_prompt(Some(dir)) {
                                            tracing::error!(
                                                "Failed to load prompt for agent '{}': {}",
                                                name,
                                                e
                                            );
                                            continue;
                                        }
                                    }

Comment on lines +305 to +309
desc.trim_start_matches("You are a ")
.trim_start_matches("You are an ")
.trim_start_matches("You are ")
.trim()
.to_string()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current logic for extracting an agent's description from its prompt can prematurely truncate the sentence if it contains a period that is not at the end, such as in a version number (e.g., v1.2). This could result in confusing or incomplete agent descriptions in the UI. A more robust approach would be to split at the first occurrence of a period followed by a space, or to take the whole first line if that pattern isn't found.

        let desc = if let Some(pos) = first_line.find(". ") {
            &first_line[..=pos]
        } else {
            first_line
        };

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant new functionality by adding multi-agent tool support. This includes core logic for an agent registry, an agent tool handler, and updates to the protocol. The TUI is also updated to support listing agents and using @mentions to invoke them. The changes are accompanied by extensive documentation and examples, which is excellent.

My review found one critical issue that will prevent compilation and a couple of medium-severity suggestions regarding repository hygiene. Overall, this is a well-structured and comprehensive feature addition.

Comment on lines +285 to +288
match (a.is_builtin, b.is_builtin) {
(true, false) => std::cmp::Ordering::Less,\n (false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There appears to be a stray newline character \n within the match statement, which will cause a compilation error. Please remove it to ensure the code compiles correctly.

            match (a.is_builtin, b.is_builtin) {
                (true, false) => std::cmp::Ordering::Less,
                (false, true) => std::cmp::Ordering::Greater,
                _ => a.name.cmp(&b.name),
            }

Comment thread NEXT_SESSION_NOTES.md
@@ -0,0 +1,21 @@
# Next Session Notes
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file appears to contain personal development notes and a PR draft. Such files are generally not committed to the repository. Please consider removing it from this pull request to keep the project history clean.

Comment thread agent-merge-plan.md
@@ -0,0 +1,39 @@
# Multi-agent merge plan (local fork)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file seems to be a local merge plan. It's good practice to exclude such temporary or personal planning documents from the repository's history. Please consider removing this file from the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant