rab/agent/extension.rs
1/// Extension trait - all capability (built-in or user-provided) comes through this.
2use crate::agent::types::{ToolCall, ToolExecutionMode};
3use crate::tui::Theme;
4use async_trait::async_trait;
5use std::borrow::Cow;
6use std::sync::{
7 Arc,
8 atomic::{AtomicBool, Ordering},
9};
10use tokio::sync::mpsc::UnboundedSender;
11
12/// Reason a tool call was blocked.
13#[derive(Debug, Clone)]
14#[allow(dead_code)]
15pub enum BlockReason {
16 Security(String),
17 Policy(String),
18 Other(String),
19}
20
21/// An autocomplete item for slash command arguments.
22#[derive(Debug, Clone)]
23pub struct AutocompleteItem {
24 /// The value to insert when selected.
25 pub value: String,
26 /// Display label.
27 pub label: String,
28 /// Optional description.
29 pub description: Option<String>,
30}
31
32/// A slash command handler (built-in or extension-provided).
33/// Commands use the same Extension trait as tools - built-ins and
34/// user extensions register commands through a uniform interface.
35pub trait CommandHandler: Send + Sync {
36 /// Execute the command with the given arguments string.
37 fn execute(&self, args: &str) -> anyhow::Result<CommandResult>;
38
39 /// Get argument completions for autocomplete.
40 /// Called when user types `/cmd ` - returns matching autocomplete items.
41 fn argument_completions(&self, _prefix: &str) -> Vec<AutocompleteItem> {
42 vec![]
43 }
44}
45
46/// Result of executing a slash command.
47#[derive(Debug, Clone)]
48pub enum CommandResult {
49 /// Command handled, show this info message.
50 Info(String),
51 /// Command caused a quit request.
52 Quit,
53 /// Command switched the model (new model name).
54 ModelChanged(String),
55 /// Show keyboard shortcuts help overlay.
56 ShowHelp,
57 /// Reload settings and auth from disk.
58 Reloaded,
59 /// Start a new session (clear conversation).
60 NewSession,
61 /// Switch to a different session file.
62 SessionSwitched { path: std::path::PathBuf },
63 /// Show session info (ID, file, messages, tokens, cost).
64 SessionInfo {
65 session_id: String,
66 file_path: Option<std::path::PathBuf>,
67 name: Option<String>,
68 message_count: usize,
69 },
70 /// Open session selector UI.
71 OpenSessionSelector,
72 /// Name was set for the session.
73 SessionNamed { name: String },
74}
75
76/// A registered slash command.
77pub struct SlashCommand {
78 pub name: String,
79 pub description: String,
80 pub handler: Box<dyn CommandHandler>,
81}
82
83/// Simple cancellation token for tool execution.
84/// Shared between the agent loop and tool execution to signal cancellation.
85#[derive(Debug, Clone)]
86pub struct Cancel {
87 flag: Arc<AtomicBool>,
88}
89
90impl Cancel {
91 pub fn new() -> Self {
92 Self {
93 flag: Arc::new(AtomicBool::new(false)),
94 }
95 }
96
97 /// Check whether cancellation has been requested.
98 pub fn is_cancelled(&self) -> bool {
99 self.flag.load(Ordering::Relaxed)
100 }
101
102 /// Request cancellation.
103 pub fn cancel(&self) {
104 self.flag.store(true, Ordering::Relaxed);
105 }
106
107 /// Check if cancelled, returning an error if so.
108 pub fn check(&self) -> anyhow::Result<()> {
109 if self.is_cancelled() {
110 Err(anyhow::anyhow!("Operation cancelled"))
111 } else {
112 Ok(())
113 }
114 }
115}
116
117impl Default for Cancel {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123/// Output from a tool execution, carrying both the full content (shown in expanded
124/// mode / sent to the LLM) and an optional compact label for collapsed UI display.
125#[derive(Debug, Clone)]
126pub struct ToolOutput {
127 /// Full content sent to the LLM and shown when expanded.
128 pub content: String,
129 /// Compact label shown in collapsed mode (e.g. `read docs docs/README.md`).
130 /// When `None`, the full content is always shown.
131 pub compact: Option<String>,
132 /// Whether the result is an error.
133 pub is_error: bool,
134 /// When true, the agent loop stops after this batch of tool calls
135 /// (no more LLM calls). Pi-compatible: `terminate` on tool results.
136 pub terminate: bool,
137}
138
139impl ToolOutput {
140 pub fn ok(content: impl Into<String>) -> Self {
141 Self {
142 content: content.into(),
143 compact: None,
144 is_error: false,
145 terminate: false,
146 }
147 }
148
149 pub fn ok_with_compact(content: impl Into<String>, compact: impl Into<String>) -> Self {
150 Self {
151 content: content.into(),
152 compact: Some(compact.into()),
153 is_error: false,
154 terminate: false,
155 }
156 }
157
158 pub fn err(message: impl Into<String>) -> Self {
159 Self {
160 content: message.into(),
161 compact: None,
162 is_error: true,
163 terminate: false,
164 }
165 }
166
167 /// Mark this tool output as terminal — the agent loop will stop after
168 /// this batch of tool calls when ALL tools in the batch return terminate=true.
169 pub fn with_terminate(mut self, terminate: bool) -> Self {
170 self.terminate = terminate;
171 self
172 }
173}
174
175/// Context passed to ToolRenderer methods (matching pi's ToolRenderContext).
176/// Carries all metadata about the tool execution that renderers may need.
177#[derive(Debug, Clone)]
178pub struct ToolRenderContext {
179 pub expanded: bool,
180 pub args_complete: bool,
181 pub is_partial: bool,
182 pub is_error: bool,
183 /// Working directory for path resolution.
184 pub cwd: String,
185 /// Duration in seconds (bash).
186 pub duration_secs: Option<f64>,
187 /// Exit code (bash).
188 pub exit_code: Option<i32>,
189 /// Whether execution was cancelled (bash).
190 pub cancelled: bool,
191 /// Whether output was truncated (bash/read).
192 pub was_truncated: bool,
193 /// Path to full output file (bash).
194 pub full_output_path: Option<String>,
195 /// File path for syntax highlighting (read).
196 pub file_path: Option<String>,
197 /// Keybinding hint for the expand action, e.g. "C-O".
198 pub expand_key: String,
199}
200
201/// Tool-specific rendering interface (matching pi's renderCall/renderResult pattern).
202/// Each built-in tool implements this to provide its own visual representation.
203pub trait ToolRenderer: Send + Sync {
204 /// Render the tool call header/title.
205 /// Returns ANSI-styled lines for the call portion (inside the colored box shell).
206 fn render_call(
207 &self,
208 args: &serde_json::Value,
209 width: usize,
210 theme: &dyn Theme,
211 ctx: &ToolRenderContext,
212 ) -> Vec<String>;
213
214 /// Render the tool result body.
215 /// Returns lines to display as the result body, or empty vec for no result.
216 /// When empty, only the call portion is shown (e.g. write success).
217 fn render_result(
218 &self,
219 content: &str,
220 width: usize,
221 theme: &dyn Theme,
222 ctx: &ToolRenderContext,
223 ) -> Vec<String>;
224
225 /// Whether this tool uses `renderShell: "self"` (controls its own framing).
226 /// When true, ToolExecComponent does NOT wrap the tool in a colored background box.
227 fn render_self(&self) -> bool {
228 false
229 }
230}
231
232/// An LLM-callable tool.
233#[async_trait]
234pub trait AgentTool: Send + Sync {
235 fn name(&self) -> &str;
236 fn description(&self) -> &str;
237 fn parameters(&self) -> serde_json::Value;
238 #[allow(dead_code)]
239 fn label(&self) -> &str;
240
241 /// Execution mode for this tool. When set to `Sequential`, a batch of tool calls
242 /// containing this tool will execute sequentially (one-at-a-time) even when the
243 /// global config is `Parallel`. Defaults to `Parallel`.
244 fn execution_mode(&self) -> ToolExecutionMode {
245 ToolExecutionMode::Parallel
246 }
247
248 /// Optional argument pre-processing (pi-compatible: `prepareArguments`).
249 /// Called before execution, receives the raw LLM arguments and returns
250 /// (possibly modified) arguments. Default is identity (no transformation).
251 fn prepare_arguments(&self, args: serde_json::Value) -> serde_json::Value {
252 args
253 }
254
255 /// Provide a tool-specific renderer for the UI.
256 /// When None (the default), ToolExecComponent falls back to generic rendering.
257 fn renderer(&self) -> Option<Box<dyn ToolRenderer>> {
258 None
259 }
260
261 /// Guidelines for the system prompt specific to this tool.
262 fn prompt_guidelines(&self) -> Vec<String> {
263 vec![]
264 }
265
266 /// Execute the tool. Returns output carrying both the full content (sent to LLM)
267 /// and an optional compact label for collapsed UI display.
268 ///
269 /// If `on_update` is provided, the tool may send intermediate `ToolOutput` updates
270 /// during long-running operations (e.g. bash streaming).
271 async fn execute(
272 &self,
273 tool_call_id: String,
274 args: serde_json::Value,
275 cancel: Cancel,
276 on_update: Option<UnboundedSender<ToolOutput>>,
277 ) -> anyhow::Result<ToolOutput>;
278}
279
280#[async_trait]
281#[allow(dead_code)]
282pub trait Extension: Send + Sync {
283 fn name(&self) -> Cow<'static, str>;
284
285 /// Tools this extension provides (LLM-callable).
286 fn tools(&self) -> Vec<Box<dyn AgentTool>> {
287 vec![]
288 }
289
290 /// Slash commands this extension provides (e.g. `/quit`, `/model`).
291 /// Built-in commands and extension commands use the same interface.
292 fn commands(&self) -> Vec<SlashCommand> {
293 vec![]
294 }
295
296 /// Called before any tool executes. Return Some(reason) to block.
297 async fn before_tool_call(&self, _tc: &ToolCall) -> Option<BlockReason> {
298 None
299 }
300
301 /// Called after a tool executes. Return Some(text) to replace result.
302 async fn after_tool_call(&self, _tc: &ToolCall, _result: &str) -> Option<String> {
303 None
304 }
305}