fix: Tool call improvements for Windows systems

This commit is contained in:
2026-04-08 12:49:43 -06:00
parent e98bf56a2b
commit ed59051f3d
8 changed files with 57 additions and 22 deletions
Generated
+1
View File
@@ -3137,6 +3137,7 @@ dependencies = [
"crossterm 0.28.1", "crossterm 0.28.1",
"dirs", "dirs",
"duct", "duct",
"dunce",
"fancy-regex", "fancy-regex",
"futures-util", "futures-util",
"fuzzy-matcher", "fuzzy-matcher",
+1
View File
@@ -18,6 +18,7 @@ anyhow = "1.0.69"
bytes = "1.4.0" bytes = "1.4.0"
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] } clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
dirs = "6.0.0" dirs = "6.0.0"
dunce = "1.0.5"
futures-util = "0.3.29" futures-util = "0.3.29"
inquire = "0.9.4" inquire = "0.9.4"
is-terminal = "0.4.9" is-terminal = "0.4.9"
+6
View File
@@ -50,6 +50,12 @@ def parse_raw_data(data):
def parse_argv(): def parse_argv():
agent_func = sys.argv[1] agent_func = sys.argv[1]
tool_data_file = os.environ.get("LLM_TOOL_DATA_FILE")
if tool_data_file and os.path.isfile(tool_data_file):
with open(tool_data_file, "r", encoding="utf-8") as f:
agent_data = f.read()
else:
agent_data = sys.argv[2] agent_data = sys.argv[2]
if (not agent_data) or (not agent_func): if (not agent_data) or (not agent_func):
+4 -1
View File
@@ -14,7 +14,11 @@ main() {
parse_argv() { parse_argv() {
agent_func="$1" agent_func="$1"
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
agent_data="$(cat "$LLM_TOOL_DATA_FILE")"
else
agent_data="$2" agent_data="$2"
fi
if [[ -z "$agent_data" ]] || [[ -z "$agent_func" ]]; then if [[ -z "$agent_data" ]] || [[ -z "$agent_func" ]]; then
die "usage: ./{agent_name}.sh <agent-func> <agent-data>" die "usage: ./{agent_name}.sh <agent-func> <agent-data>"
fi fi
@@ -57,7 +61,6 @@ run() {
if [[ "$OS" == "Windows_NT" ]]; then if [[ "$OS" == "Windows_NT" ]]; then
set -o igncr set -o igncr
tools_path="$(cygpath -w "$tools_path")" tools_path="$(cygpath -w "$tools_path")"
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
fi fi
jq_script="$(cat <<-'EOF' jq_script="$(cat <<-'EOF'
+5
View File
@@ -49,6 +49,11 @@ def parse_raw_data(data):
def parse_argv(): def parse_argv():
tool_data_file = os.environ.get("LLM_TOOL_DATA_FILE")
if tool_data_file and os.path.isfile(tool_data_file):
with open(tool_data_file, "r", encoding="utf-8") as f:
return f.read()
argv = sys.argv[:] + [None] * max(0, 2 - len(sys.argv)) argv = sys.argv[:] + [None] * max(0, 2 - len(sys.argv))
tool_data = argv[1] tool_data = argv[1]
+4 -1
View File
@@ -13,7 +13,11 @@ main() {
} }
parse_argv() { parse_argv() {
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
tool_data="$(cat "$LLM_TOOL_DATA_FILE")"
else
tool_data="$1" tool_data="$1"
fi
if [[ -z "$tool_data" ]]; then if [[ -z "$tool_data" ]]; then
die "usage: ./{function_name}.sh <tool-data>" die "usage: ./{function_name}.sh <tool-data>"
fi fi
@@ -54,7 +58,6 @@ run() {
if [[ "$OS" == "Windows_NT" ]]; then if [[ "$OS" == "Windows_NT" ]]; then
set -o igncr set -o igncr
tool_path="$(cygpath -w "$tool_path")" tool_path="$(cygpath -w "$tool_path")"
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
fi fi
jq_script="$(cat <<-'EOF' jq_script="$(cat <<-'EOF'
+2 -1
View File
@@ -108,5 +108,6 @@ can also pass the `--disable-log-colors` flag as well.
## Miscellaneous Variables ## Miscellaneous Variables
| Environment Variable | Description | Default Value | | Environment Variable | Description | Default Value |
|----------------------|--------------------------------------------------------------------------------------------------|---------------| |----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `AUTO_CONFIRM` | Bypass all `guard_*` checks in the bash prompt helpers; useful for agent composition and routing | | | `AUTO_CONFIRM` | Bypass all `guard_*` checks in the bash prompt helpers; useful for agent composition and routing | |
| `LLM_TOOL_DATA_FILE` | Set automatically by Loki on Windows. Points to a temporary file containing the JSON tool call data. <br>Tool scripts (`run-tool.sh`, `run-agent.sh`, etc.) read from this file instead of command-line args <br>to avoid JSON escaping issues when data passes through `cmd.exe` → bash. **Not intended to be set by users.** | |
+29 -14
View File
@@ -613,6 +613,7 @@ impl Functions {
) )
})?; })?;
let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) }; let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
let to_script_path = |p: &str| -> String { p.replace('\\', "/") };
let content = match binary_type { let content = match binary_type {
BinaryType::Tool(None) => { BinaryType::Tool(None) => {
let root_dir = Config::functions_dir(); let root_dir = Config::functions_dir();
@@ -622,8 +623,8 @@ impl Functions {
); );
content_template content_template
.replace("{function_name}", binary_name) .replace("{function_name}", binary_name)
.replace("{root_dir}", &root_dir.to_string_lossy()) .replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy()))
.replace("{tool_path}", &tool_path) .replace("{tool_path}", &to_script_path(&tool_path))
} }
BinaryType::Tool(Some(agent_name)) => { BinaryType::Tool(Some(agent_name)) => {
let root_dir = Config::agent_data_dir(agent_name); let root_dir = Config::agent_data_dir(agent_name);
@@ -633,16 +634,19 @@ impl Functions {
); );
content_template content_template
.replace("{function_name}", binary_name) .replace("{function_name}", binary_name)
.replace("{root_dir}", &root_dir.to_string_lossy()) .replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy()))
.replace("{tool_path}", &tool_path) .replace("{tool_path}", &to_script_path(&tool_path))
} }
BinaryType::Agent => content_template BinaryType::Agent => content_template
.replace("{agent_name}", binary_name) .replace("{agent_name}", binary_name)
.replace("{config_dir}", &Config::config_dir().to_string_lossy()), .replace(
"{config_dir}",
&to_script_path(&Config::config_dir().to_string_lossy()),
),
} }
.replace( .replace(
"{prompt_utils_file}", "{prompt_utils_file}",
&Config::bash_prompt_utils_file().to_string_lossy(), &to_script_path(&Config::bash_prompt_utils_file().to_string_lossy()),
); );
if binary_script_file.exists() { if binary_script_file.exists() {
fs::remove_file(&binary_script_file)?; fs::remove_file(&binary_script_file)?;
@@ -666,7 +670,7 @@ impl Functions {
.join(".venv") .join(".venv")
.join("Scripts") .join("Scripts")
.join("activate.bat"); .join("activate.bat");
let canonicalized_path = fs::canonicalize(&executable_path)?; let canonicalized_path = dunce::canonicalize(&executable_path)?;
format!( format!(
"call \"{}\" && {}", "call \"{}\" && {}",
canonicalized_path.to_string_lossy(), canonicalized_path.to_string_lossy(),
@@ -677,19 +681,16 @@ impl Functions {
let executable_path = which::which("python") let executable_path = which::which("python")
.or_else(|_| which::which("python3")) .or_else(|_| which::which("python3"))
.map_err(|_| anyhow!("Python executable not found in PATH"))?; .map_err(|_| anyhow!("Python executable not found in PATH"))?;
let canonicalized_path = fs::canonicalize(&executable_path)?; let canonicalized_path = dunce::canonicalize(&executable_path)?;
canonicalized_path.to_string_lossy().into_owned() canonicalized_path.to_string_lossy().into_owned()
} }
_ => bail!("Unsupported language: {}", language.as_ref()), _ => bail!("Unsupported language: {}", language.as_ref()),
}; };
let bin_dir = binary_file let bin_dir = binary_file
.parent() .parent()
.expect("Failed to get parent directory of binary file") .expect("Failed to get parent directory of binary file");
.canonicalize()? let bin_dir = dunce::canonicalize(bin_dir)?.to_string_lossy().into_owned();
.to_string_lossy() let wrapper_binary = dunce::canonicalize(&binary_script_file)?
.into_owned();
let wrapper_binary = binary_script_file
.canonicalize()?
.to_string_lossy() .to_string_lossy()
.into_owned(); .into_owned();
let content = formatdoc!( let content = formatdoc!(
@@ -1117,6 +1118,20 @@ pub fn run_llm_function(
#[cfg(windows)] #[cfg(windows)]
let cmd_name = polyfill_cmd_name(&cmd_name, &bin_dirs); let cmd_name = polyfill_cmd_name(&cmd_name, &bin_dirs);
#[cfg(windows)]
let cmd_args = {
let mut args = cmd_args;
if let Some(json_data) = args.pop() {
let tool_data_file = temp_file("-tool-data-", ".json");
fs::write(&tool_data_file, &json_data)?;
envs.insert(
"LLM_TOOL_DATA_FILE".into(),
tool_data_file.display().to_string(),
);
}
args
};
envs.insert("CLICOLOR_FORCE".into(), "1".into()); envs.insert("CLICOLOR_FORCE".into(), "1".into());
envs.insert("FORCE_COLOR".into(), "1".into()); envs.insert("FORCE_COLOR".into(), "1".into());