1use std::path::PathBuf;
2
3use serde_json::{json, Value};
4
5use crate::context::AppContext;
6use crate::protocol::{RawRequest, Response};
7
8pub type DispatchFn<'a> = dyn Fn(RawRequest, &AppContext) -> Response + 'a;
9
10#[derive(Debug)]
15pub struct ToolCallResult {
16 pub text: String,
17 pub response: crate::protocol::Response,
18}
19
20#[derive(Debug)]
23pub enum ToolCallOutcome {
24 Unary(ToolCallResult),
25}
26
27#[derive(Debug, Clone)]
30pub struct ToolCallContext {
31 pub project_root: PathBuf,
32 pub session_id: Option<String>,
33 pub request_id: String,
34 pub diagnostics_on_edit: bool,
35 pub preview: bool,
36}
37
38pub fn run_tool_call(
39 bare_name: &str,
40 args: &Value,
41 ctx: &ToolCallContext,
42 app_ctx: &AppContext,
43 dispatch: &DispatchFn<'_>,
44) -> ToolCallOutcome {
45 let sanitized_args = strip_agent_preview_arg(args);
46 let format_context = crate::subc_format::FormatContext::from_tool_call(
47 bare_name,
48 &sanitized_args,
49 ctx.project_root.as_path(),
50 );
51 let translate_context = crate::subc_translate::TranslateContext {
52 diagnostics_on_edit: ctx.diagnostics_on_edit,
53 preview: ctx.preview,
54 };
55 let (command, translated_args) = match crate::subc_translate::subc_translate_with_context(
56 bare_name,
57 &sanitized_args,
58 ctx.project_root.as_path(),
59 translate_context,
60 ) {
61 Ok(translated) => (translated.command, translated.args),
62 Err(err) if err.code != "unsupported_tool" => {
66 let response = Response::error(ctx.request_id.clone(), err.code, err.message);
67 return ToolCallOutcome::Unary(tool_call_result_from_response(
68 bare_name,
69 &format_context,
70 response,
71 ));
72 }
73 Err(_) => {
74 let map = sanitized_args.as_object().cloned().unwrap_or_default();
75 (bare_name.to_string(), map)
76 }
77 };
78
79 let mut map = translated_args;
80 if ctx.preview {
81 map.insert("preview".to_string(), json!(true));
82 }
83 map.insert("id".to_string(), json!(ctx.request_id.clone()));
84 map.insert("command".to_string(), json!(command));
85 map.insert("session_id".to_string(), json!(ctx.session_id.clone()));
86
87 let raw_req = match serde_json::from_value::<RawRequest>(Value::Object(map)) {
88 Ok(req) => req,
89 Err(error) => {
90 let response = Response::error(
91 ctx.request_id.clone(),
92 "invalid_request",
93 format!("failed to build request from tool call: {error}"),
94 );
95 return ToolCallOutcome::Unary(tool_call_result_from_response(
96 bare_name,
97 &format_context,
98 response,
99 ));
100 }
101 };
102
103 let response = dispatch(raw_req, app_ctx);
104 ToolCallOutcome::Unary(tool_call_result_from_response(
105 bare_name,
106 &format_context,
107 response,
108 ))
109}
110
111fn strip_agent_preview_arg(args: &Value) -> Value {
112 let Some(map) = args.as_object() else {
113 return args.clone();
114 };
115 if !map.contains_key("preview") {
116 return args.clone();
117 }
118
119 let mut sanitized = map.clone();
120 sanitized.remove("preview");
121 Value::Object(sanitized)
122}
123
124fn tool_call_result_from_response(
125 bare_name: &str,
126 format_context: &crate::subc_format::FormatContext,
127 response: Response,
128) -> ToolCallResult {
129 let text =
130 crate::subc_format::format_response_with_context(bare_name, &response, format_context);
131 ToolCallResult { text, response }
132}