1use crate::config::constants::tools;
11use crate::types::CompactStr;
12use hashbrown::HashMap;
13use std::fmt::Write as _;
14use std::path::PathBuf;
15use std::sync::Arc;
16
17use super::sandboxing::{ExecToolCallOutput, ToolError};
18use super::tool_handler::{DiffTracker, FileChange, ToolCallError, TurnContext};
19
20pub struct ToolEventCtx<'a> {
22 pub turn: &'a TurnContext,
23 pub call_id: &'a str,
24 pub turn_diff_tracker: Option<&'a Arc<tokio::sync::Mutex<DiffTracker>>>,
25}
26
27impl<'a> ToolEventCtx<'a> {
28 pub fn new(
29 turn: &'a TurnContext,
30 call_id: &'a str,
31 tracker: Option<&'a Arc<tokio::sync::Mutex<DiffTracker>>>,
32 ) -> Self {
33 Self { turn, call_id, turn_diff_tracker: tracker }
34 }
35}
36
37#[derive(Clone, Debug)]
39pub enum ToolEventStage {
40 Begin,
41 Success(ExecToolCallOutput),
42 Failure(ToolEventFailureKind),
43}
44
45#[derive(Clone, Debug)]
47pub enum ToolEventFailureKind {
48 Output(ExecToolCallOutput),
49 Message(String),
50 Error(String),
51}
52
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum ExecCommandSource {
56 #[default]
57 Agent,
58 User,
59 CommandSessionStartup,
60 CommandSessionWriteStdin,
61}
62
63#[derive(Clone, Debug)]
65pub struct ParsedCommand {
66 pub program: String,
67 pub args: Vec<String>,
68}
69
70pub fn parse_command(command: &[String]) -> ParsedCommand {
72 let program = command.first().cloned().unwrap_or_default();
73 let args = command.get(1..).map(|s| s.to_vec()).unwrap_or_default();
74 ParsedCommand { program, args }
75}
76
77#[derive(Clone, Debug)]
79pub enum ToolEmitter {
80 Shell {
82 command: Vec<String>,
83 cwd: PathBuf,
84 source: ExecCommandSource,
85 parsed_cmd: ParsedCommand,
86 freeform: bool,
87 },
88 ApplyPatch {
90 changes: HashMap<PathBuf, FileChange>,
91 auto_approved: bool,
92 },
93 CommandSession {
95 command: Vec<String>,
96 cwd: PathBuf,
97 source: ExecCommandSource,
98 interaction_input: Option<String>,
99 parsed_cmd: ParsedCommand,
100 process_id: Option<String>,
101 },
102 Generic { tool_name: CompactStr },
104}
105
106impl ToolEmitter {
107 pub fn shell(command: Vec<String>, cwd: PathBuf, source: ExecCommandSource, freeform: bool) -> Self {
109 let parsed_cmd = parse_command(&command);
110 Self::Shell { command, cwd, source, parsed_cmd, freeform }
111 }
112
113 pub fn apply_patch(changes: HashMap<PathBuf, FileChange>, auto_approved: bool) -> Self {
115 Self::ApplyPatch { changes, auto_approved }
116 }
117
118 pub fn command_session(
120 command: &[String],
121 cwd: PathBuf,
122 source: ExecCommandSource,
123 process_id: Option<String>,
124 ) -> Self {
125 let parsed_cmd = parse_command(command);
126 Self::CommandSession {
127 command: command.to_vec(),
128 cwd,
129 source,
130 interaction_input: None,
131 parsed_cmd,
132 process_id,
133 }
134 }
135
136 pub fn generic(tool_name: impl Into<CompactStr>) -> Self {
138 Self::Generic { tool_name: tool_name.into() }
139 }
140
141 pub async fn emit(&self, ctx: ToolEventCtx<'_>, stage: ToolEventStage) {
143 match (self, &stage) {
144 (Self::ApplyPatch { changes, auto_approved }, ToolEventStage::Begin) => {
146 if let Some(tracker) = ctx.turn_diff_tracker {
148 let mut guard = tracker.lock().await;
149 guard.on_patch_begin(changes);
150 }
151
152 tracing::debug!(
153 call_id = %ctx.call_id,
154 turn_id = %ctx.turn.turn_id,
155 changes = changes.len(),
156 auto_approved = *auto_approved,
157 "patch apply began"
158 );
159 }
160
161 (Self::ApplyPatch { .. }, ToolEventStage::Success(output)) => {
163 self.emit_patch_end(ctx, output.stdout.clone(), output.stderr.clone(), true)
164 .await;
165 }
166
167 (Self::ApplyPatch { .. }, ToolEventStage::Failure(ToolEventFailureKind::Output(output))) => {
169 self.emit_patch_end(ctx, output.stdout.clone(), output.stderr.clone(), false)
170 .await;
171 }
172
173 (Self::ApplyPatch { .. }, ToolEventStage::Failure(ToolEventFailureKind::Message(msg))) => {
174 self.emit_patch_end(ctx, String::new(), msg.clone(), false).await;
175 }
176
177 (Self::Shell { .. } | Self::CommandSession { .. }, ToolEventStage::Begin) => {
179 tracing::debug!(
180 tool = %self.tool_name(),
181 call_id = %ctx.call_id,
182 turn_id = %ctx.turn.turn_id,
183 "Tool execution started"
184 );
185 }
186
187 (Self::Shell { .. } | Self::CommandSession { .. }, ToolEventStage::Success(_)) => {
189 tracing::debug!(call_id = %ctx.call_id, "Tool execution succeeded");
190 }
191
192 (Self::Shell { .. } | Self::CommandSession { .. }, ToolEventStage::Failure(kind)) => {
194 let error = match kind {
195 ToolEventFailureKind::Output(output) => output.combined_output(),
196 ToolEventFailureKind::Message(msg) => msg.clone(),
197 ToolEventFailureKind::Error(err) => err.clone(),
198 };
199 tracing::warn!(call_id = %ctx.call_id, error = %error, "Tool execution failed");
200 }
201
202 (Self::Generic { tool_name }, ToolEventStage::Begin) => {
204 tracing::debug!(
205 tool = %tool_name,
206 call_id = %ctx.call_id,
207 turn_id = %ctx.turn.turn_id,
208 "Tool execution started"
209 );
210 }
211
212 (Self::Generic { .. }, ToolEventStage::Success(_)) => {
213 tracing::debug!(call_id = %ctx.call_id, "Tool execution succeeded");
214 }
215
216 (Self::Generic { .. }, ToolEventStage::Failure(kind)) => {
217 let error = match kind {
218 ToolEventFailureKind::Output(output) => output.combined_output(),
219 ToolEventFailureKind::Message(msg) => msg.clone(),
220 ToolEventFailureKind::Error(err) => err.clone(),
221 };
222 tracing::warn!(call_id = %ctx.call_id, error = %error, "Tool execution failed");
223 }
224
225 _ => {}
226 }
227 }
228
229 pub async fn begin(&self, ctx: ToolEventCtx<'_>) {
231 self.emit(ctx, ToolEventStage::Begin).await;
232 }
233
234 pub async fn finish(
236 &self,
237 ctx: ToolEventCtx<'_>,
238 result: Result<ExecToolCallOutput, ToolError>,
239 ) -> Result<String, ToolCallError> {
240 match result {
241 Ok(output) => {
242 self.emit(ctx, ToolEventStage::Success(output.clone())).await;
243 Ok(self.format_output_for_model(&output))
244 }
245 Err(ToolError::Rejected(msg)) => {
246 self.emit(ctx, ToolEventStage::Failure(ToolEventFailureKind::Message(msg.clone())))
247 .await;
248 Err(ToolCallError::Rejected(msg))
249 }
250 Err(ToolError::Timeout(ms)) => {
251 let msg = format!("Command timed out after {ms}ms");
252 self.emit(ctx, ToolEventStage::Failure(ToolEventFailureKind::Message(msg.clone())))
253 .await;
254 Err(ToolCallError::Timeout(ms))
255 }
256 Err(e) => {
257 let msg = e.to_string();
258 self.emit(ctx, ToolEventStage::Failure(ToolEventFailureKind::Error(msg.clone())))
259 .await;
260 Err(ToolCallError::Internal(e.into()))
261 }
262 }
263 }
264
265 fn format_output_for_model(&self, output: &ExecToolCallOutput) -> String {
267 let mut result = String::with_capacity(output.stdout.len() + output.stderr.len() + 32);
268
269 if !output.stdout.is_empty() {
270 result.push_str(&output.stdout);
271 }
272
273 if !output.stderr.is_empty() {
274 if !result.is_empty() {
275 result.push_str("\n\n[stderr]\n");
276 }
277 result.push_str(&output.stderr);
278 }
279
280 if output.exit_code != 0 {
281 if !result.is_empty() {
282 result.push('\n');
283 }
284 let _ = write!(result, "[exit code: {}]", output.exit_code);
285 }
286
287 if result.is_empty() {
288 result.push_str("[no output]");
289 }
290
291 result
292 }
293
294 fn tool_name(&self) -> CompactStr {
296 match self {
297 Self::Shell { .. } => CompactStr::from(tools::SHELL),
298 Self::ApplyPatch { .. } => CompactStr::from(tools::APPLY_PATCH),
299 Self::CommandSession { .. } => CompactStr::from(tools::EXEC_COMMAND),
300 Self::Generic { tool_name } => tool_name.clone(),
301 }
302 }
303
304 async fn emit_patch_end(&self, ctx: ToolEventCtx<'_>, _stdout: String, _stderr: String, success: bool) {
306 if let Some(tracker) = ctx.turn_diff_tracker {
308 let mut guard = tracker.lock().await;
309 guard.on_patch_end(success);
310 }
311
312 tracing::debug!(call_id = %ctx.call_id, success, "patch apply finished");
313 }
314}
315
316#[derive(Clone, Debug)]
318pub struct ExecCommandInput<'a> {
319 pub command: &'a [String],
320 pub cwd: &'a std::path::Path,
321 pub parsed_cmd: &'a ParsedCommand,
322 pub source: ExecCommandSource,
323 pub timeout_ms: Option<u64>,
324 pub justification: Option<&'a str>,
325}
326
327impl<'a> ExecCommandInput<'a> {
328 pub fn new(
329 command: &'a [String],
330 cwd: &'a std::path::Path,
331 parsed_cmd: &'a ParsedCommand,
332 source: ExecCommandSource,
333 timeout_ms: Option<u64>,
334 justification: Option<&'a str>,
335 ) -> Self {
336 Self {
337 command,
338 cwd,
339 parsed_cmd,
340 source,
341 timeout_ms,
342 justification,
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn test_parse_command() {
353 let cmd = vec!["ls".to_string(), "-la".to_string(), "/tmp".to_string()];
354 let parsed = parse_command(&cmd);
355
356 assert_eq!(parsed.program, "ls");
357 assert_eq!(parsed.args, vec!["-la", "/tmp"]);
358 }
359
360 #[test]
361 fn test_parse_command_empty() {
362 let cmd: Vec<String> = vec![];
363 let parsed = parse_command(&cmd);
364
365 assert_eq!(parsed.program, "");
366 assert!(parsed.args.is_empty());
367 }
368
369 #[test]
370 fn test_emitter_tool_names() {
371 let shell = ToolEmitter::shell(vec!["ls".to_string()], PathBuf::new(), ExecCommandSource::Agent, false);
372 assert_eq!(shell.tool_name(), "shell");
373
374 let patch = ToolEmitter::apply_patch(HashMap::new(), true);
375 assert_eq!(patch.tool_name(), "apply_patch");
376
377 let exec = ToolEmitter::command_session(&["echo".to_string()], PathBuf::new(), ExecCommandSource::Agent, None);
378 assert_eq!(exec.tool_name(), "exec_command");
379
380 let generic = ToolEmitter::generic("custom_tool");
381 assert_eq!(generic.tool_name(), "custom_tool");
382 }
383
384 #[test]
385 fn test_format_output_for_model() {
386 let emitter = ToolEmitter::generic("test");
387
388 let output = ExecToolCallOutput {
390 stdout: "Hello, world!".to_string(),
391 stderr: String::new(),
392 exit_code: 0,
393 };
394 assert_eq!(emitter.format_output_for_model(&output), "Hello, world!");
395
396 let output = ExecToolCallOutput {
398 stdout: String::new(),
399 stderr: "Error!".to_string(),
400 exit_code: 1,
401 };
402 assert_eq!(emitter.format_output_for_model(&output), "Error!\n[exit code: 1]");
403
404 let output = ExecToolCallOutput::default();
406 assert_eq!(emitter.format_output_for_model(&output), "[no output]");
407 }
408}