Skip to main content

aft/
run_tool_call.rs

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/// The full result of a tool call: the COMPLETE dispatch Response carried VERBATIM,
11/// plus the server-rendered agent-facing text (what the deleted TS formatters used to produce).
12/// Oracle #1: carry the WHOLE Response — promote nothing, drop nothing (preview_diff, attachments,
13/// status_bar, bg_completions, lsp_diagnostics, code, message, candidates, … all ride inside `response`).
14#[derive(Debug)]
15pub struct ToolCallResult {
16    pub text: String,
17    pub response: crate::protocol::Response,
18}
19
20/// Reserve a discriminated seam so bash/PTY/streaming (P3) doesn't force a signature rewrite.
21/// Only `Unary` is constructed today. Do NOT build `Stream`.
22#[derive(Debug)]
23pub enum ToolCallOutcome {
24    Unary(ToolCallResult),
25}
26
27/// Server-owned settings for a single `tool_call` request.
28/// These fields cannot be supplied through the agent's arguments object.
29#[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        // Return validation errors from the translation step immediately. Only
63        // the special unsupported_tool error can fall through, allowing native
64        // NDJSON commands such as configure/undo to reach dispatch unchanged.
65        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}