kiln_ai.sandbox.tools_surface
Shared definition of the synthetic kiln.tools module surface.
Stdlib only — no Pydantic / Kiln-model / DB / UI imports.
This is the single source of truth for the kiln / kiln.tools /
kiln.async_tools surface that user code (from kiln import tools) sees.
Both the sandbox runtime (sandbox/tools_api.py) and the test shim
(tool_testing/) build the exact same module objects from here, so runtime
and tests can never drift — a test that catches kiln.tools.ToolCallError
catches the same class the runtime raises.
The surface is parameterized over a bridge (see ToolBridge): the
sandbox injects its real IPC bridge, the shim injects an in-process fake. This
module contains no execution, IPC, or subprocess behavior of its own.
1"""Shared definition of the synthetic ``kiln.tools`` module surface. 2 3Stdlib only — no Pydantic / Kiln-model / DB / UI imports. 4 5This is the single source of truth for the ``kiln`` / ``kiln.tools`` / 6``kiln.async_tools`` surface that user code (``from kiln import tools``) sees. 7Both the sandbox runtime (``sandbox/tools_api.py``) and the test shim 8(``tool_testing/``) build the exact same module objects from here, so runtime 9and tests can never drift — a test that catches ``kiln.tools.ToolCallError`` 10catches the same class the runtime raises. 11 12The surface is parameterized over a *bridge* (see :class:`ToolBridge`): the 13sandbox injects its real IPC bridge, the shim injects an in-process fake. This 14module contains no execution, IPC, or subprocess behavior of its own. 15""" 16 17from __future__ import annotations 18 19import asyncio 20import sys 21import types 22from typing import Any, Protocol 23 24# --------------------------------------------------------------------------- 25# Typed exceptions 26# --------------------------------------------------------------------------- 27 28 29class ToolNotAllowed(Exception): 30 """The requested tool is not in this code tool's allowlist.""" 31 32 def __init__(self, tool: str, message: str) -> None: 33 self.tool = tool 34 self.message = message 35 self.raw: str | None = None 36 super().__init__(message) 37 38 39class ToolTimeout(Exception): 40 """A nested tool call timed out.""" 41 42 def __init__(self, tool: str, message: str) -> None: 43 self.tool = tool 44 self.message = message 45 self.raw: str | None = None 46 super().__init__(message) 47 48 49class ToolCallError(Exception): 50 """Catch-all for nested tool-call failures.""" 51 52 def __init__(self, tool: str, message: str, raw: str | None = None) -> None: 53 self.tool = tool 54 self.message = message 55 self.raw = raw 56 super().__init__(message) 57 58 59# --------------------------------------------------------------------------- 60# Bridge protocol 61# --------------------------------------------------------------------------- 62 63 64class ToolBridge(Protocol): 65 """Behavior the synthetic modules require of a bridge. 66 67 ``call`` receives the tool name plus the positional/keyword arguments the 68 user passed to the proxy and returns the tool's raw output string (or raises 69 one of the typed exceptions). ``list_tools`` returns the tool declarations. 70 """ 71 72 def call(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> str: ... 73 74 def list_tools(self) -> list[dict[str, Any]]: ... 75 76 77# --------------------------------------------------------------------------- 78# Synthetic module objects 79# --------------------------------------------------------------------------- 80 81 82class _SyncToolsModule(types.ModuleType): 83 """``kiln.tools`` — sync callable proxies for tool calls.""" 84 85 def __getattr__(self, name: str) -> Any: 86 if name.startswith("_"): 87 raise AttributeError(name) 88 bridge: ToolBridge = self._bridge # type: ignore[attr-defined] 89 return lambda *args, **kw: bridge.call(name, args, kw) 90 91 92class _AsyncToolsModule(types.ModuleType): 93 """``kiln.async_tools`` — awaitable proxies for tool calls.""" 94 95 def __getattr__(self, name: str) -> Any: 96 if name.startswith("_"): 97 raise AttributeError(name) 98 bridge: ToolBridge = self._bridge # type: ignore[attr-defined] 99 100 async def _async_proxy(*args: Any, **kw: Any) -> str: 101 return await asyncio.to_thread(bridge.call, name, args, kw) 102 103 return _async_proxy 104 105 106def build_tools_modules( 107 bridge: ToolBridge, 108) -> tuple[types.ModuleType, _SyncToolsModule, _AsyncToolsModule]: 109 """Build the ``kiln`` / ``kiln.tools`` / ``kiln.async_tools`` modules. 110 111 Wires the modules to *bridge* and returns them WITHOUT installing them into 112 ``sys.modules`` (see :func:`install_tools_modules_for_bridge`). 113 """ 114 # -- kiln -- 115 kiln_mod = types.ModuleType("kiln") 116 kiln_mod.__path__ = [] # type: ignore[attr-defined] # make it a package so `from kiln import ...` works 117 118 # -- kiln.tools (sync) -- 119 tools_mod = _SyncToolsModule("kiln.tools") 120 tools_mod._bridge = bridge # type: ignore[attr-defined] 121 tools_mod.ToolNotAllowed = ToolNotAllowed # type: ignore[attr-defined] 122 tools_mod.ToolTimeout = ToolTimeout # type: ignore[attr-defined] 123 tools_mod.ToolCallError = ToolCallError # type: ignore[attr-defined] 124 tools_mod.list_tools = bridge.list_tools # type: ignore[attr-defined] 125 126 # -- kiln.async_tools (async) -- 127 async_tools_mod = _AsyncToolsModule("kiln.async_tools") 128 async_tools_mod._bridge = bridge # type: ignore[attr-defined] 129 async_tools_mod.ToolNotAllowed = ToolNotAllowed # type: ignore[attr-defined] 130 async_tools_mod.ToolTimeout = ToolTimeout # type: ignore[attr-defined] 131 async_tools_mod.ToolCallError = ToolCallError # type: ignore[attr-defined] 132 133 async def _async_list_tools() -> list[dict[str, Any]]: 134 return await asyncio.to_thread(bridge.list_tools) 135 136 async_tools_mod.list_tools = _async_list_tools # type: ignore[attr-defined] 137 138 kiln_mod.tools = tools_mod # type: ignore[attr-defined] 139 kiln_mod.async_tools = async_tools_mod # type: ignore[attr-defined] 140 141 return kiln_mod, tools_mod, async_tools_mod 142 143 144def install_tools_modules_for_bridge( 145 bridge: ToolBridge, 146) -> tuple[types.ModuleType, _SyncToolsModule, _AsyncToolsModule]: 147 """Build and install the synthetic ``kiln`` package into ``sys.modules``. 148 149 Returns the built modules. After this call ``from kiln import tools`` (and 150 ``kiln.async_tools``) resolves against these objects. 151 """ 152 kiln_mod, tools_mod, async_tools_mod = build_tools_modules(bridge) 153 154 sys.modules["kiln"] = kiln_mod 155 sys.modules["kiln.tools"] = tools_mod 156 sys.modules["kiln.async_tools"] = async_tools_mod 157 158 return kiln_mod, tools_mod, async_tools_mod
30class ToolNotAllowed(Exception): 31 """The requested tool is not in this code tool's allowlist.""" 32 33 def __init__(self, tool: str, message: str) -> None: 34 self.tool = tool 35 self.message = message 36 self.raw: str | None = None 37 super().__init__(message)
The requested tool is not in this code tool's allowlist.
40class ToolTimeout(Exception): 41 """A nested tool call timed out.""" 42 43 def __init__(self, tool: str, message: str) -> None: 44 self.tool = tool 45 self.message = message 46 self.raw: str | None = None 47 super().__init__(message)
A nested tool call timed out.
50class ToolCallError(Exception): 51 """Catch-all for nested tool-call failures.""" 52 53 def __init__(self, tool: str, message: str, raw: str | None = None) -> None: 54 self.tool = tool 55 self.message = message 56 self.raw = raw 57 super().__init__(message)
Catch-all for nested tool-call failures.
65class ToolBridge(Protocol): 66 """Behavior the synthetic modules require of a bridge. 67 68 ``call`` receives the tool name plus the positional/keyword arguments the 69 user passed to the proxy and returns the tool's raw output string (or raises 70 one of the typed exceptions). ``list_tools`` returns the tool declarations. 71 """ 72 73 def call(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> str: ... 74 75 def list_tools(self) -> list[dict[str, Any]]: ...
Behavior the synthetic modules require of a bridge.
call receives the tool name plus the positional/keyword arguments the
user passed to the proxy and returns the tool's raw output string (or raises
one of the typed exceptions). list_tools returns the tool declarations.
1968def _no_init_or_replace_init(self, *args, **kwargs): 1969 cls = type(self) 1970 1971 if cls._is_protocol: 1972 raise TypeError('Protocols cannot be instantiated') 1973 1974 # Already using a custom `__init__`. No need to calculate correct 1975 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1976 if cls.__init__ is not _no_init_or_replace_init: 1977 return 1978 1979 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1980 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1981 # searches for a proper new `__init__` in the MRO. The new `__init__` 1982 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1983 # instantiation of the protocol subclass will thus use the new 1984 # `__init__` and no longer call `_no_init_or_replace_init`. 1985 for base in cls.__mro__: 1986 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1987 if init is not _no_init_or_replace_init: 1988 cls.__init__ = init 1989 break 1990 else: 1991 # should not happen 1992 cls.__init__ = object.__init__ 1993 1994 cls.__init__(self, *args, **kwargs)
73 def call(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> str: ...
75 def list_tools(self) -> list[dict[str, Any]]: ...
107def build_tools_modules( 108 bridge: ToolBridge, 109) -> tuple[types.ModuleType, _SyncToolsModule, _AsyncToolsModule]: 110 """Build the ``kiln`` / ``kiln.tools`` / ``kiln.async_tools`` modules. 111 112 Wires the modules to *bridge* and returns them WITHOUT installing them into 113 ``sys.modules`` (see :func:`install_tools_modules_for_bridge`). 114 """ 115 # -- kiln -- 116 kiln_mod = types.ModuleType("kiln") 117 kiln_mod.__path__ = [] # type: ignore[attr-defined] # make it a package so `from kiln import ...` works 118 119 # -- kiln.tools (sync) -- 120 tools_mod = _SyncToolsModule("kiln.tools") 121 tools_mod._bridge = bridge # type: ignore[attr-defined] 122 tools_mod.ToolNotAllowed = ToolNotAllowed # type: ignore[attr-defined] 123 tools_mod.ToolTimeout = ToolTimeout # type: ignore[attr-defined] 124 tools_mod.ToolCallError = ToolCallError # type: ignore[attr-defined] 125 tools_mod.list_tools = bridge.list_tools # type: ignore[attr-defined] 126 127 # -- kiln.async_tools (async) -- 128 async_tools_mod = _AsyncToolsModule("kiln.async_tools") 129 async_tools_mod._bridge = bridge # type: ignore[attr-defined] 130 async_tools_mod.ToolNotAllowed = ToolNotAllowed # type: ignore[attr-defined] 131 async_tools_mod.ToolTimeout = ToolTimeout # type: ignore[attr-defined] 132 async_tools_mod.ToolCallError = ToolCallError # type: ignore[attr-defined] 133 134 async def _async_list_tools() -> list[dict[str, Any]]: 135 return await asyncio.to_thread(bridge.list_tools) 136 137 async_tools_mod.list_tools = _async_list_tools # type: ignore[attr-defined] 138 139 kiln_mod.tools = tools_mod # type: ignore[attr-defined] 140 kiln_mod.async_tools = async_tools_mod # type: ignore[attr-defined] 141 142 return kiln_mod, tools_mod, async_tools_mod
Build the kiln / kiln.tools / kiln.async_tools modules.
Wires the modules to bridge and returns them WITHOUT installing them into
sys.modules (see install_tools_modules_for_bridge()).
145def install_tools_modules_for_bridge( 146 bridge: ToolBridge, 147) -> tuple[types.ModuleType, _SyncToolsModule, _AsyncToolsModule]: 148 """Build and install the synthetic ``kiln`` package into ``sys.modules``. 149 150 Returns the built modules. After this call ``from kiln import tools`` (and 151 ``kiln.async_tools``) resolves against these objects. 152 """ 153 kiln_mod, tools_mod, async_tools_mod = build_tools_modules(bridge) 154 155 sys.modules["kiln"] = kiln_mod 156 sys.modules["kiln.tools"] = tools_mod 157 sys.modules["kiln.async_tools"] = async_tools_mod 158 159 return kiln_mod, tools_mod, async_tools_mod
Build and install the synthetic kiln package into sys.modules.
Returns the built modules. After this call from kiln import tools (and
kiln.async_tools) resolves against these objects.