diff --git a/kcpp_agent.py b/kcpp_agent.py index 295c19cd0..8322c44e9 100644 --- a/kcpp_agent.py +++ b/kcpp_agent.py @@ -41,6 +41,12 @@ import urllib.request from pathlib import Path from typing import Any, Callable +if os.name != "nt": + try: + import readline # Enable standard line editing and in-memory history. + except ImportError: + pass # Optional in some Python builds; plain input() still works. + DEFAULT_MAX_TOOL_RESULT_CHARS = 20000 MAX_TOOL_RESULT_CHARS = DEFAULT_MAX_TOOL_RESULT_CHARS @@ -195,6 +201,14 @@ def color(text: str, code: str, *, stderr: bool = False) -> str: return f"{code}{text}{ANSI_RESET}" if enabled else text +def input_prompt(text: str) -> str: + label = color(text, ANSI_BOLD_CYAN) + if os.name != "nt" and "readline" in sys.modules: + # Readline must exclude ANSI color sequences when counting columns. + label = re.sub(r"\x1b\[[0-9;]*m", lambda match: "\001" + match[0] + "\002", label) + return label + " " + + def toggle_status(enabled: bool) -> str: return color("ON" if enabled else "OFF", ANSI_GREEN if enabled else ANSI_YELLOW) @@ -748,6 +762,16 @@ def tool_shell(args: dict[str, Any]) -> str: raise RuntimeError("No POSIX command shell was found") argv = [executable, "-c", command] + shell_env = None + if os.name == "posix" and getattr(sys, "frozen", False): + # System commands must not load the frozen agent's bundled libraries. + shell_env = os.environ.copy() + original_library_path = shell_env.get("LD_LIBRARY_PATH_ORIG") + if original_library_path is not None: + shell_env["LD_LIBRARY_PATH"] = original_library_path + else: + shell_env.pop("LD_LIBRARY_PATH", None) + completed = subprocess.run( argv, capture_output=True, @@ -755,6 +779,7 @@ def tool_shell(args: dict[str, Any]) -> str: encoding="utf-8", errors="replace", timeout=timeout, + env=shell_env, ) return limit_text( @@ -777,7 +802,7 @@ def tool_ask_user(args: dict[str, Any]) -> str: raise ValueError("question must be a non-empty string") print("\n" + color("Agent asks:", ANSI_CYAN) + f" {question.strip()}") try: - answer = input(color("Your answer>", ANSI_BOLD_CYAN) + " ") + answer = input(input_prompt("Your answer>")) except (EOFError, KeyboardInterrupt): print() return "The user declined to answer." @@ -1735,7 +1760,7 @@ def run_agent( while True: try: - user_text = input(color("User>", ANSI_BOLD_CYAN) + " ").strip() + user_text = input(input_prompt("User>")).strip() except (EOFError, KeyboardInterrupt): print("\nExiting.") return diff --git a/koboldcpp.py b/koboldcpp.py index c3e553f73..da790c1b6 100644 --- a/koboldcpp.py +++ b/koboldcpp.py @@ -11700,7 +11700,8 @@ def run_bundled_kobold_agent(base_url=None, api_key=None): if not os.path.isfile(agent_path): raise FileNotFoundError(f"Kobold Agent script not found: {agent_path}") - import runpy + # A normal import also lets PyInstaller collect the agent's stdlib imports. + import kcpp_agent old_argv = sys.argv try: sys.argv = [agent_path] @@ -11708,7 +11709,7 @@ def run_bundled_kobold_agent(base_url=None, api_key=None): sys.argv.extend(["--base-url", base_url]) if api_key: sys.argv.extend(["--api-key", api_key]) - runpy.run_path(agent_path, run_name="__main__") + kcpp_agent.main() finally: sys.argv = old_argv @@ -11754,6 +11755,23 @@ def launch_kobold_agent_terminal(base_url=None, api_key=None): apple_script = f'tell application "Terminal" to do script {json.dumps(shell_command)}' subprocess.Popen(["osascript", "-e", apple_script], start_new_session=True) else: + # Frozen Linux builds prepend their extraction directory to + # LD_LIBRARY_PATH. Do not leak those bundled libraries into a + # system terminal emulator: mixing them with the terminal's system + # libraries can produce loader errors (for example, Cairo failing + # to resolve FreeType symbols) before the agent is ever started. + terminal_env = os.environ.copy() + if is_frozen: + original_library_path = terminal_env.get("LD_LIBRARY_PATH_ORIG") + if original_library_path is not None: + terminal_env["LD_LIBRARY_PATH"] = original_library_path + else: + terminal_env.pop("LD_LIBRARY_PATH", None) + # Give the agent its own extracted files, even if this launcher + # exits. Set this on the command so terminal-server handoffs + # cannot lose it when forwarding to an existing terminal. + command = ["env", "PYINSTALLER_RESET_ENVIRONMENT=1", *command] + terminal_commands = [ ("x-terminal-emulator", ["-e"]), ("gnome-terminal", ["--"]), @@ -11774,13 +11792,13 @@ def launch_kobold_agent_terminal(base_url=None, api_key=None): if resolved_path in attempted: continue attempted.add(resolved_path) - # xterm's default bitmap font is often absent in minimal WSL + # xterm's default bitmap font is often absent in minimal Linux # installations; request a fontconfig-backed font instead. launch_args = (["-fa", "monospace"] if os.path.basename(resolved_path) == "xterm" else []) + terminal_args try: terminal_process = subprocess.Popen( [terminal_path, *launch_args, *command], - cwd=os.getcwd(), start_new_session=True) + cwd=os.getcwd(), env=terminal_env, start_new_session=True) try: exit_code = terminal_process.wait(timeout=1) except subprocess.TimeoutExpired: @@ -11794,7 +11812,9 @@ def launch_kobold_agent_terminal(base_url=None, api_key=None): print(f"Cannot launch Kobold Agent: {'; '.join(failures)}.") else: print("Cannot launch Kobold Agent: no supported terminal emulator was found.") - print("Try running kcpp_agent.py in another terminal window.") + manual_command = ([sys.executable, "--run-bundled-agent"] if is_frozen + else [sys.executable, agent_path]) + print(f"Try running this in an existing terminal: {shlex.join(manual_command)}") return False return True except Exception as e: