Files
zed/crates/agent/src/tools.rs
T
Eric Holk 59f32d2135 Add create_thread and list_agents_and_models agent tools (#57987)
Adds two built-in agent tools, behind the staff-only
`create-thread-tool` feature flag, that let the agent spawn independent
sibling threads:

- **`create_thread`** creates a new thread with a title and initial
prompt. It runs on the user's behalf and shows up in the agent sidebar
like a thread the user started themselves, but it runs in the background
— the active thread is never interrupted — and it does **not** report
results back to the caller. (Use `spawn_agent` when you want the
results.)
- **`list_agents_and_models`** enumerates the configured agents and, for
the native agent, the available models, so the agent can pick a
cheaper/faster model for bulk work. The motivating stress test was
"spawn 100 threads that each write a haiku, using Haiku instead of
Opus."

By default the sibling shares the parent's project and worktree. If
`use_new_worktree` is set, `create_thread` instead opens a new
background workspace backed by fresh linked git worktrees of the
project's repos — the same flow you get from the worktree picker's
"Create worktree" — and starts the thread there. The new workspace opens
as a background tab so it doesn't pull you away from what you're doing.

A few notes on the design:

- The `agent` crate can't depend on the UI, so there's a
`SiblingThreadHost` trait that `agent_ui::AgentPanel` implements and
installs on the `NativeAgent`. Tool calls in a native-agent thread route
through it.
- Sibling threads go through the same `create_agent_thread` →
`ConversationView` path as the user-facing "new thread" action, so
eviction, persistence, and sidebar rendering match user-created threads.
- An explicit `agent`/`model` override doesn't clobber the panel's
selected agent or the last-used-agent preference — those are snapshotted
and restored around the call.
- The worktree path reuses the existing "Create worktree" machinery
rather than reimplementing it; the agent flow just opens the new
workspace in the background instead of activating it.

A couple of things I left out or want to call out:

- There's no tool to operate on an already-created sibling by session
id. The success payload deliberately omits it, since the session is
populated asynchronously and nothing consumes it yet.
- When a project has multiple worktrees that are linked worktrees of the
*same* underlying repo, they collapse into a single new worktree (they
resolve to the same target path). The tool returns a warning in that
case rather than failing — and this also fixes a latent collision the
manual "Create worktree" flow would hit in the same situation.

Release Notes:

- N/A
2026-06-01 23:44:18 +00:00

199 lines
5.8 KiB
Rust

mod apply_code_action_tool;
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;
mod edit_session;
#[cfg(all(test, feature = "unit-eval"))]
mod evals;
mod fetch_tool;
mod find_path_tool;
mod find_references_tool;
mod get_code_actions_tool;
mod go_to_definition_tool;
mod grep_tool;
mod list_agents_and_models_tool;
mod list_directory_tool;
mod move_path_tool;
mod read_file_tool;
mod rename_tool;
mod skill_tool;
mod spawn_agent_tool;
mod symbol_locator;
mod terminal_tool;
mod tool_permissions;
mod update_plan_tool;
mod update_title_tool;
mod web_search_tool;
mod write_file_tool;
use crate::AgentTool;
use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat};
use serde::{
Deserialize, Deserializer,
de::{DeserializeOwned, Error as _},
};
/// Deserialize a value that may have been provided as a JSON-encoded string
/// instead of the structured value. Some models occasionally stringify nested
/// arguments, so we accept either form.
pub(crate) fn deserialize_maybe_stringified<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: DeserializeOwned,
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum ValueOrJsonString<T> {
Value(T),
String(String),
}
match ValueOrJsonString::<T>::deserialize(deserializer)? {
ValueOrJsonString::Value(value) => Ok(value),
ValueOrJsonString::String(string) => serde_json::from_str::<T>(&string).map_err(|error| {
D::Error::custom(format!("failed to parse stringified value: {error}"))
}),
}
}
pub use apply_code_action_tool::*;
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 find_references_tool::*;
pub use get_code_actions_tool::*;
pub use go_to_definition_tool::*;
pub use grep_tool::*;
pub use list_agents_and_models_tool::*;
pub use list_directory_tool::*;
pub use move_path_tool::*;
pub use read_file_tool::*;
pub use rename_tool::*;
pub use skill_tool::*;
pub use spawn_agent_tool::*;
pub use symbol_locator::*;
pub use terminal_tool::*;
pub use tool_permissions::*;
pub use update_plan_tool::*;
pub use update_title_tool::*;
pub use web_search_tool::*;
pub use write_file_tool::*;
macro_rules! tools {
($($tool:ty),* $(,)?) => {
/// Every built-in tool name, determined at compile time.
pub const ALL_TOOL_NAMES: &[&str] = &[
$(<$tool>::NAME,)*
];
const _: () = {
const fn str_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
const NAMES: &[&str] = ALL_TOOL_NAMES;
let mut i = 0;
while i < NAMES.len() {
let mut j = i + 1;
while j < NAMES.len() {
if str_eq(NAMES[i], NAMES[j]) {
panic!("Duplicate tool name in tools! macro");
}
j += 1;
}
i += 1;
}
};
/// Returns whether the tool with the given name supports the given provider.
pub fn tool_supports_provider(name: &str, provider: &language_model::LanguageModelProviderId) -> bool {
$(
if name == <$tool>::NAME {
return <$tool>::supports_provider(provider);
}
)*
false
}
/// A list of all built-in tools
pub fn built_in_tools() -> impl Iterator<Item = LanguageModelRequestTool> {
fn language_model_tool<T: AgentTool>() -> LanguageModelRequestTool {
LanguageModelRequestTool {
name: T::NAME.to_string(),
description: T::description().to_string(),
input_schema: T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(),
use_input_streaming: T::supports_input_streaming(),
}
}
[
$(
language_model_tool::<$tool>(),
)*
]
.into_iter()
}
};
}
// Adding a tool here (and constructing it in `Thread::add_default_tools`) is
// not enough to make the model actually receive it. Two further gates will
// silently drop the tool rather than fail to compile:
//
// 1. `assets/settings/default.json`: the `write` and `ask` agent profiles each
// carry an explicit `tools` allowlist. `Thread::enabled_tools` filters out
// any tool not present there with value `true`, so it never reaches the
// model.
// 2. `test_all_tools_are_in_tool_info_or_excluded` in
// `crates/settings_ui/src/pages/tool_permissions_setup.rs`: every tool must
// be in the permission-UI `TOOLS` list (if it calls
// `decide_permission_from_settings`) or in `EXCLUDED_TOOLS`.
tools! {
ApplyCodeActionTool,
CopyPathTool,
CreateDirectoryTool,
CreateThreadTool,
DeletePathTool,
DiagnosticsTool,
EditFileTool,
FetchTool,
FindPathTool,
FindReferencesTool,
GetCodeActionsTool,
GoToDefinitionTool,
GrepTool,
ListAgentsAndModelsTool,
ListDirectoryTool,
MovePathTool,
ReadFileTool,
RenameTool,
SkillTool,
SpawnAgentTool,
TerminalTool,
UpdatePlanTool,
UpdateTitleTool,
WebSearchTool,
WriteFileTool,
}