Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -1079,10 +1079,12 @@
"tools": {
"copy_path": true,
"create_directory": true,
"create_thread": true,
"delete_path": true,
"diagnostics": true,
"edit_file": true,
"fetch": true,
"list_agents_and_models": true,
"list_directory": true,
"project_notifications": false,
"move_path": true,
Expand All @@ -1105,8 +1107,10 @@
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
// "enable_all_context_servers": true,
"tools": {
"create_thread": true,
"diagnostics": true,
"fetch": true,
"list_agents_and_models": true,
"list_directory": true,
"project_notifications": false,
"now": true,
Expand Down
25 changes: 25 additions & 0 deletions crates/agent/.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Registering a new built-in agent tool

Registering a tool in `Thread::add_default_tools()` and the `tools!` macro in
`crates/agent/src/tools.rs` is **not enough** for the model to actually receive
it. There are two additional gates that must be updated or the tool will be
silently filtered out:

1. **Built-in profiles in `assets/settings/default.json`.** The `write` and
`ask` profiles under `agent.profiles` each contain an explicit `tools` map
that acts as a per-profile allowlist. `Thread::enabled_tools()` calls
`profile.is_tool_enabled(name)` and excludes any tool not present with value
`true`. Add the new tool name to both profiles (or whichever profiles it
should be available in).

2. **`test_all_tools_are_in_tool_info_or_excluded` in
`crates/settings_ui/src/pages/tool_permissions_setup.rs`.** This test walks
`agent::ALL_TOOL_NAMES` and asserts each entry is either in the `TOOLS`
permission-UI list or in `EXCLUDED_TOOLS`. Add a `ToolInfo` entry if the
tool has permission checks (i.e. calls `decide_permission_from_settings`);
otherwise add the tool name to `EXCLUDED_TOOLS` with a comment explaining
why it doesn't need one.

The symptom of skipping step 1 is that the tool appears nowhere in the LLM's
tool list even though it's registered and compiles fine. The symptom of
skipping step 2 is a failing unit test in `settings_ui`.
61 changes: 61 additions & 0 deletions crates/agent/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,22 @@ impl LanguageModels {
}
}

/// Implemented by the UI layer to provide the ability for agent tools to create
/// sibling threads that appear in the agent panel.
///
/// `agent_ui::AgentPanel` installs an implementation of this trait on the
/// `NativeAgent` when it sets up a connection. Tools in a native-agent thread
/// then discover and use the host via `NativeThreadEnvironment`.
pub trait SiblingThreadHost {
fn create_sibling_thread(
&self,
request: SiblingThreadRequest,
cx: &mut AsyncApp,
) -> Task<Result<SiblingThreadInfo>>;

fn list_available_agents(&self, cx: &mut App) -> Result<AvailableAgents>;
}

pub struct NativeAgent {
/// Session ID -> Session mapping
sessions: HashMap<acp::SessionId, Session>,
Expand All @@ -261,6 +277,8 @@ pub struct NativeAgent {
templates: Arc<Templates>,
/// Cached model information
models: LanguageModels,
/// Handler installed by the UI for `create_thread` / `list_agents_and_models` tools.
sibling_thread_host: Option<Rc<dyn SiblingThreadHost>>,
prompt_store: Option<Entity<PromptStore>>,
fs: Arc<dyn Fs>,
_subscriptions: Vec<Subscription>,
Expand Down Expand Up @@ -292,13 +310,22 @@ impl NativeAgent {
projects: HashMap::default(),
templates,
models: LanguageModels::new(cx),
sibling_thread_host: None,
prompt_store,
fs,
_subscriptions: subscriptions,
}
})
}

pub fn set_sibling_thread_host(&mut self, host: Rc<dyn SiblingThreadHost>) {
self.sibling_thread_host = Some(host);
}

pub fn sibling_thread_host(&self) -> Option<Rc<dyn SiblingThreadHost>> {
self.sibling_thread_host.clone()
}

fn new_session(
&mut self,
project: Entity<Project>,
Expand Down Expand Up @@ -1997,6 +2024,40 @@ impl ThreadEnvironment for NativeThreadEnvironment {
) -> Result<Rc<dyn SubagentHandle>> {
self.resume_subagent_thread(session_id, cx)
}

fn create_sibling_thread(
&self,
request: SiblingThreadRequest,
cx: &mut AsyncApp,
) -> Task<Result<SiblingThreadInfo>> {
let host = match self
.agent
.read_with(cx, |agent, _| agent.sibling_thread_host())
{
Ok(Some(host)) => host,
Ok(None) => {
return Task::ready(Err(anyhow!(
"No sibling-thread host is registered. This usually means the \
agent panel hasn't been initialized in this workspace."
)));
}
Err(err) => return Task::ready(Err(err)),
};
host.create_sibling_thread(request, cx)
}

fn list_available_agents(&self, cx: &mut App) -> Result<AvailableAgents> {
let host = self
.agent
.read_with(cx, |agent, _| agent.sibling_thread_host())?
.ok_or_else(|| {
anyhow!(
"No sibling-thread host is registered. This usually means the \
agent panel hasn't been initialized in this workspace."
)
})?;
host.list_available_agents(cx)
}
}

#[derive(Debug, Clone)]
Expand Down
105 changes: 99 additions & 6 deletions crates/agent/src/thread.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use crate::{
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool,
RestoreFileFromDiskTool, SaveFileTool, SpawnAgentTool, StreamingEditFileTool,
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, CreateThreadTool, DbLanguageModel,
DbThread, DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
ListAgentsAndModelsTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot,
ReadFileTool, RestoreFileFromDiskTool, SaveFileTool, SpawnAgentTool, StreamingEditFileTool,
SystemPromptTemplate, Template, Templates, TerminalTool, ToolPermissionDecision,
UpdatePlanTool, WebSearchTool, decide_permission_from_settings,
};
use acp_thread::{MentionUri, UserMessageId};
use action_log::ActionLog;
use feature_flags::{
FeatureFlagAppExt as _, StreamingEditFileToolFeatureFlag, UpdatePlanToolFeatureFlag,
CreateThreadToolFeatureFlag, FeatureFlagAppExt as _, StreamingEditFileToolFeatureFlag,
UpdatePlanToolFeatureFlag,
};

use agent_client_protocol as acp;
Expand Down Expand Up @@ -661,6 +662,90 @@ pub trait ThreadEnvironment {
"Resuming subagent sessions is not supported"
))
}

/// Creates an independent sibling thread visible in the agent sidebar.
/// Unlike subagents, sibling threads are first-class threads that persist
/// and run in parallel without reporting results back to the parent.
fn create_sibling_thread(
&self,
request: SiblingThreadRequest,
cx: &mut AsyncApp,
) -> Task<Result<SiblingThreadInfo>> {
let _ = request;
let _ = cx;
Task::ready(Err(anyhow::anyhow!(
"Creating sibling threads is not supported in this environment"
)))
}

/// Lists the agents and models available for use with `create_sibling_thread`.
fn list_available_agents(&self, cx: &mut App) -> Result<AvailableAgents> {
let _ = cx;
Err(anyhow::anyhow!(
"Listing available agents is not supported in this environment"
))
}
}

/// A request to create a new sibling thread.
#[derive(Debug, Clone)]
pub struct SiblingThreadRequest {
/// A short title for the new thread, shown in the sidebar.
pub title: SharedString,
/// The initial prompt to send to the new thread.
pub prompt: String,
/// Optional agent ID to use. Defaults to the native Zed agent.
pub agent_id: Option<String>,
/// Optional model override, as `provider/model-id`.
/// Defaults to the user's configured default model for the agent.
pub model: Option<String>,
/// Whether to create the thread in a new git worktree.
/// Not yet supported; passing `true` will return an error.
pub use_new_worktree: bool,
/// Git ref (branch, tag, or commit) to base the new worktree on.
/// Only relevant when `use_new_worktree` is true.
pub base_ref: Option<String>,
}

/// Information returned when a sibling thread is successfully created.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SiblingThreadInfo {
/// The title assigned to the thread.
pub title: SharedString,
/// The agent ID used for the thread.
pub agent_id: String,
/// The model ID used for the thread, if known.
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}

/// A list of agents and, for each, the models available for use.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvailableAgents {
pub agents: Vec<AvailableAgent>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvailableAgent {
/// Identifier used when creating a thread.
pub id: String,
/// Human-readable name shown in the UI.
pub name: String,
/// Whether this is Zed's built-in native agent.
pub is_native: bool,
/// Models available for this agent. May be empty if models are not
/// enumerated up front (e.g., external agents that choose their own).
pub models: Vec<AvailableModel>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvailableModel {
/// Identifier to pass as the `model` field when creating a thread.
pub id: String,
/// Human-readable name.
pub name: String,
/// Whether this is the default model for the agent.
pub is_default: bool,
}

#[derive(Debug)]
Expand Down Expand Up @@ -1574,7 +1659,15 @@ impl Thread {
self.add_tool(WebSearchTool);

if self.depth() < MAX_SUBAGENT_DEPTH {
self.add_tool(SpawnAgentTool::new(environment));
self.add_tool(SpawnAgentTool::new(environment.clone()));
}

// Sibling-thread tools are exposed at every depth: a subagent should
// still be able to kick off independent sibling work on behalf of the
// user, even when it can no longer nest further subagents.
if cx.has_flag::<CreateThreadToolFeatureFlag>() {
self.add_tool(CreateThreadTool::new(environment.clone()));
self.add_tool(ListAgentsAndModelsTool::new(environment));
}
}

Expand Down
6 changes: 6 additions & 0 deletions crates/agent/src/tools.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod context_server_registry;
mod copy_path_tool;
mod create_directory_tool;
mod create_thread_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
Expand All @@ -9,6 +10,7 @@ mod evals;
mod fetch_tool;
mod find_path_tool;
mod grep_tool;
mod list_agents_and_models_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
Expand All @@ -30,12 +32,14 @@ use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat};
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
pub use create_thread_tool::*;
pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*;
pub use grep_tool::*;
pub use list_agents_and_models_tool::*;
pub use list_directory_tool::*;
pub use move_path_tool::*;
pub use now_tool::*;
Expand Down Expand Up @@ -121,12 +125,14 @@ macro_rules! tools {
tools! {
CopyPathTool,
CreateDirectoryTool,
CreateThreadTool,
DeletePathTool,
DiagnosticsTool,
EditFileTool,
FetchTool,
FindPathTool,
GrepTool,
ListAgentsAndModelsTool,
ListDirectoryTool,
MovePathTool,
NowTool,
Expand Down
Loading
Loading