返回 CodeWhale
tool_setup.rs
根目录 / crates / tui / src / core / engine / tool_setup.rs
1 //! Per-turn tool registry setup.
2 //!
3 //! This keeps mode/feature-specific registry construction out of the send path.
4
5 use super::*;
6 use crate::core::authority::shell_policy_for_mode;
7 use crate::tools::AgentToolSurfaceOptions;
8 use crate::worker_profile::ShellPolicy;
9
10 fn should_register_remember_tool(memory_enabled: bool) -> bool {
11 memory_enabled
12 }
13
14 impl Engine {
15 pub(super) fn agent_tool_surface_options(
16 &self,
17 shell_policy: ShellPolicy,
18 ) -> AgentToolSurfaceOptions {
19 let mut options = AgentToolSurfaceOptions::new(shell_policy);
20 options.apply_patch_enabled = self.config.features.enabled(Feature::ApplyPatch);
21 options.web_search_enabled = self.config.features.enabled(Feature::WebSearch);
22 options.memory_tool_enabled = should_register_remember_tool(self.config.memory_enabled);
23 options.vision_config = if self.config.features.enabled(Feature::VisionModel) {
24 self.config.vision_config.clone()
25 } else {
26 None
27 };
28 options.speech_output_dir = self.config.speech_output_dir.clone();
29 options.goal_state = Some(self.config.goal_state.clone());
30 options.verify_tool_enabled = self.config.features.enabled(Feature::Verify);
31 options.user_input_limits = self.config.user_input_limits;
32 options.request_plugin_install_enabled = self.request_plugin_install_allowed();
33 options
34 }
35
36 /// `request_plugin_install` returns a TUI slash command and is a
37 /// proactive offer, so it exists only in the interactive TUI with
38 /// contextual tips on (0.10.1 plugin offering policy, rules 3 and 11).
39 /// Exec, ACP, and runtime-API hosts run with terminal chrome off. Applies
40 /// to every mode's surface and is inherited by child agents.
41 fn request_plugin_install_allowed(&self) -> bool {
42 self.config.terminal_chrome_enabled
43 && crate::settings::Settings::load_read_only()
44 .map(|settings| settings.contextual_tips)
45 .unwrap_or(true)
46 }
47
48 #[cfg(test)]
49 pub(super) fn build_turn_tool_registry_builder(
50 &self,
51 mode: AppMode,
52 todo_list: SharedTodoList,
53 plan_state: SharedPlanState,
54 ) -> ToolRegistryBuilder {
55 self.build_turn_tool_registry_builder_for_route(
56 mode,
57 self.session.allow_shell,
58 self.codewhale_client.clone(),
59 &self.session.model,
60 todo_list,
61 plan_state,
62 )
63 }
64
65 /// Build the registry from the route and authority already resolved for
66 /// this turn. Preview calls this before either is installed on the engine,
67 /// so reading `self.session` here would describe the previous turn's shell
68 /// posture, client, and model.
69 #[allow(clippy::too_many_arguments)]
70 pub(super) fn build_turn_tool_registry_builder_for_route(
71 &self,
72 mode: AppMode,
73 allow_shell: bool,
74 client: Option<CodewhaleClient>,
75 model: &str,
76 todo_list: SharedTodoList,
77 plan_state: SharedPlanState,
78 ) -> ToolRegistryBuilder {
79 let shell_policy = shell_policy_for_mode(mode, allow_shell);
80 if mode != AppMode::Plan {
81 let mut builder = ToolRegistryBuilder::new().with_agent_runtime_surface(
82 client.clone(),
83 model.to_string(),
84 self.agent_tool_surface_options(shell_policy),
85 todo_list,
86 plan_state,
87 );
88 if self.config.features.enabled(Feature::Mcp) {
89 builder = builder.with_registry_mcp_sync_tool();
90 }
91 // `start_mcp_server` belongs to every executable mode. Keep its
92 // handler aligned with the model catalog, which always loads the
93 // tool while MCP is enabled. The former early return registered
94 // it only in Plan mode, so Agent/Full Access advertised a tool
95 // that could never cross the execution boundary.
96 if let Some(ref pool) = self.mcp_pool {
97 builder = builder
98 .with_runtime_mcp_tool(Arc::clone(pool))
99 .with_registry_mcp_start_tool(Arc::clone(pool));
100 }
101 return builder;
102 }
103
104 let mut builder = ToolRegistryBuilder::new()
105 // Modes change authority, not the primitive tool identity.
106 // Plan advertises the same file names as Work; the turn-loop
107 // mode gate below the schema boundary blocks write/edit, and
108 // the ToolContext carries ShellPolicy::None.
109 .with_file_tools()
110 // Foreground-only shell registration: never add terminal/*
111 // lifecycle tools to Plan merely to keep the `bash` identity
112 // stable across modes.
113 .with_foreground_shell_tools()
114 .with_search_tools()
115 .with_git_tools()
116 .with_git_history_tools()
117 .with_diagnostics_tool()
118 .with_read_media_tool()
119 .with_skill_tools()
120 .with_validation_tools()
121 .with_handle_tools()
122 .with_runtime_read_only_task_tools()
123 .with_todo_tool(todo_list)
124 .with_plan_tool(plan_state)
125 .with_goal_tools(self.config.goal_state.clone());
126
127 builder = builder
128 .with_review_tool(client.clone(), model.to_string())
129 .with_user_input_tool(self.config.user_input_limits);
130
131 if self.config.features.enabled(Feature::WebSearch) {
132 builder = builder.with_web_tools();
133 }
134
135 // Register the `remember` tool only when the user has opted in to
136 // user-memory (#489). Without that opt-in the tool would always
137 // fail; surfacing it would just waste catalog slots.
138 if should_register_remember_tool(self.config.memory_enabled) {
139 builder = builder.with_remember_tool();
140 }
141
142 // Register image_analyze tool when vision_model is configured and feature enabled.
143 if self.config.features.enabled(Feature::VisionModel)
144 && let Some(ref vision_config) = self.config.vision_config
145 {
146 builder = builder.with_vision_tools(vision_config.clone(), client.clone());
147 }
148
149 // Register the `notify` tool unconditionally (#1322). Interactive and
150 // headless entry points install the merged notification policy before
151 // tool setup, including method=off, quiet/category, and attention.
152 // The tool returns a truthful suppressed/delivered receipt.
153 builder = builder.with_notify_tool();
154
155 if self.request_plugin_install_allowed() {
156 builder = builder.with_request_plugin_install_tool();
157 }
158
159 // Register the `registry_sync` tool for fetching and caching
160 // MCP Registry server metadata. Rides on `Feature::Mcp` — the same
161 // flag that gates the rest of the MCP system (defaults to enabled;
162 // opt out via `[features]` in config.toml).
163 if self.config.features.enabled(Feature::Mcp) {
164 builder = builder.with_registry_mcp_sync_tool();
165 }
166
167 // Register the start_mcp_server tool so LLM can dynamically start
168 // MCP servers from conversation context. Only when the pool has been
169 // initialized (lazy via ensure_mcp_pool).
170 if let Some(ref pool) = self.mcp_pool {
171 builder = builder
172 .with_runtime_mcp_tool(Arc::clone(pool))
173 .with_registry_mcp_start_tool(Arc::clone(pool));
174 }
175
176 builder
177 }
178 }
179
180 #[cfg(test)]
181 mod tests {
182 use super::should_register_remember_tool;
183
184 #[test]
185 fn remember_tool_registration_requires_memory_opt_in() {
186 assert!(should_register_remember_tool(true));
187 assert!(!should_register_remember_tool(false));
188 }
189 }
190
190 lines RUST