Skip to main content

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}