llm 1.3.8

A Rust library unifying multiple LLM backends.
Documentation
use std::io::IsTerminal;
use std::io::{self, Write};

use llm::{FunctionCall, ToolCall};

use crate::config::ToolExecutionMode;
use crate::tools::{ToolContext, ToolRegistry};

pub(super) struct ToolRunner {
    registry: ToolRegistry,
    context: ToolContext,
    execution_mode: ToolExecutionMode,
    stdin_is_tty: bool,
}

impl ToolRunner {
    pub(super) fn new(
        registry: ToolRegistry,
        context: ToolContext,
        execution_mode: ToolExecutionMode,
    ) -> Self {
        Self {
            registry,
            context,
            execution_mode,
            stdin_is_tty: io::stdin().is_terminal(),
        }
    }

    pub(super) fn execute(&self, calls: &[ToolCall]) -> anyhow::Result<Vec<ToolCall>> {
        let mut results = Vec::with_capacity(calls.len());
        for call in calls {
            results.push(self.execute_call(call)?);
        }
        Ok(results)
    }

    pub(super) fn print_calls(&self, calls: &[ToolCall]) {
        for call in calls {
            eprintln!("[tool] {} {}", call.function.name, call.id);
            eprintln!("{}", call.function.arguments);
        }
    }

    pub(super) fn print_results(&self, results: &[ToolCall]) {
        for result in results {
            eprintln!("[tool result] {} {}", result.function.name, result.id);
            eprintln!("{}", result.function.arguments);
        }
    }

    fn execute_call(&self, call: &ToolCall) -> anyhow::Result<ToolCall> {
        let decision = self.should_execute_tool(call)?;
        let (output, success) = match decision {
            ToolDecision::Execute => self.run_tool(call),
            ToolDecision::Decline(reason) => (reason, false),
        };
        Ok(tool_result_call(call, output, success))
    }

    fn should_execute_tool(&self, call: &ToolCall) -> anyhow::Result<ToolDecision> {
        match self.execution_mode {
            ToolExecutionMode::Always => Ok(ToolDecision::Execute),
            ToolExecutionMode::Never => Ok(ToolDecision::Decline("Tool execution disabled".into())),
            ToolExecutionMode::Ask => self.ask_for_approval(call),
        }
    }

    fn ask_for_approval(&self, call: &ToolCall) -> anyhow::Result<ToolDecision> {
        if !self.stdin_is_tty {
            return Ok(ToolDecision::Decline(
                "Tool execution requires approval".into(),
            ));
        }
        let approved = prompt_for_tool(call)?;
        if approved {
            Ok(ToolDecision::Execute)
        } else {
            Ok(ToolDecision::Decline("Tool execution declined".into()))
        }
    }

    fn run_tool(&self, call: &ToolCall) -> (String, bool) {
        match self
            .registry
            .execute(&call.function.name, &call.function.arguments, &self.context)
        {
            Ok(output) => (output, true),
            Err(err) => (format!("Tool error: {err}"), false),
        }
    }
}

enum ToolDecision {
    Execute,
    Decline(String),
}

fn tool_result_call(call: &ToolCall, output: String, success: bool) -> ToolCall {
    let call_type = if success {
        call.call_type.clone()
    } else {
        "function".to_string()
    };
    ToolCall {
        id: call.id.clone(),
        call_type,
        function: FunctionCall {
            name: call.function.name.clone(),
            arguments: output,
        },
    }
}

fn prompt_for_tool(call: &ToolCall) -> anyhow::Result<bool> {
    eprintln!("[tool approval] {} {}", call.function.name, call.id);
    eprintln!("{}", call.function.arguments);
    eprint!("Run tool? [y/N]: ");
    io::stderr().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    Ok(matches!(input.trim(), "y" | "Y"))
}