Skip to content

Commit e801a04

Browse files
committed
Python SDK: add ToolError exception to surface tool failures to the LLM
Tool handlers can now raise ToolError("message") to return a structured failure result whose message is intentionally forwarded to the LLM as text_result_for_llm. This simplifies handler code: a tool that wants to signal a recoverable failure to the LLM can simply 'raise ToolError("...")' instead of having to construct and return a full ToolResult(result_type="failure", ...) manually. The raise-and-return paths now look symmetric for success and failure. Other exception types continue to be caught and hidden behind the generic 'Invoking this tool produced an error' message for security.
1 parent e89a891 commit e801a04

3 files changed

Lines changed: 42 additions & 0 deletions

File tree

python/copilot/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
from .tools import (
118118
Tool,
119119
ToolBinaryResult,
120+
ToolError,
120121
ToolInvocation,
121122
ToolResult,
122123
ToolResultType,
@@ -223,6 +224,7 @@
223224
"TelemetryConfig",
224225
"Tool",
225226
"ToolBinaryResult",
227+
"ToolError",
226228
"ToolInvocation",
227229
"ToolResult",
228230
"ToolResultType",

python/copilot/tools.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
ToolResultType = Literal["success", "failure", "rejected", "denied", "timeout"]
1919

2020

21+
class ToolError(Exception):
22+
"""
23+
Exception raised by tool handlers to return a failure result to the LLM.
24+
Unlike other exceptions, the message is intentionally surfaced to the LLM.
25+
"""
26+
27+
2128
@dataclass
2229
class ToolBinaryResult:
2330
"""Binary content returned by a tool."""
@@ -215,6 +222,14 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult:
215222

216223
return _normalize_result(result)
217224

225+
except ToolError as exc:
226+
msg = str(exc)
227+
return ToolResult(
228+
text_result_for_llm=msg,
229+
result_type="failure",
230+
error=msg,
231+
)
232+
218233
except Exception as exc:
219234
# Don't expose detailed error information to the LLM for security reasons.
220235
# The actual error is stored in the 'error' field for debugging.

python/test_tools.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from copilot import define_tool
99
from copilot.tools import (
10+
ToolError,
1011
ToolInvocation,
1112
ToolResult,
1213
_normalize_result,
@@ -197,6 +198,30 @@ def failing_tool(params: Params, invocation: ToolInvocation) -> str:
197198
# But the actual error is stored internally
198199
assert result.error == "secret error message"
199200

201+
async def test_tool_error_is_surfaced_to_llm(self):
202+
class Params(BaseModel):
203+
pass
204+
205+
@define_tool("failing", description="A failing tool")
206+
def failing_tool(params: Params, invocation: ToolInvocation) -> str:
207+
raise ToolError("public error message")
208+
209+
invocation = ToolInvocation(
210+
session_id="s1",
211+
tool_call_id="c1",
212+
tool_name="failing",
213+
arguments={},
214+
)
215+
216+
result = await failing_tool.handler(invocation)
217+
218+
assert result.result_type == "failure"
219+
assert result.text_result_for_llm == "public error message"
220+
assert result.error == "public error message"
221+
# ToolError must take the deliberate-failure path so the structured
222+
# result reaches the LLM verbatim.
223+
assert result._from_exception is False
224+
200225
async def test_function_style_api(self):
201226
class Params(BaseModel):
202227
value: str

0 commit comments

Comments
 (0)