Skip to content

Commit bb98cdb

Browse files
committed
fix(python): forward binary tool results in HandlePendingToolCall RPC
ToolResult.binary_results_for_llm and session_log were dropped when building ExternalToolTextResultForLlm in _execute_tool_and_respond, so image/binary tool outputs never reached the model. Fixes #1644
1 parent 8c3ecbd commit bb98cdb

3 files changed

Lines changed: 78 additions & 8 deletions

File tree

python/copilot/session.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
CanvasProviderOpenResult,
3737
ClientSessionApiHandlers,
3838
CommandsHandlePendingCommandRequest,
39-
ExternalToolTextResultForLlm,
4039
HandlePendingToolCallRequest,
4140
LogRequest,
4241
ModelSwitchToRequest,
@@ -79,7 +78,13 @@
7978
from .generated.session_events import (
8079
ReasoningSummary as _RpcReasoningSummary,
8180
)
82-
from .tools import Tool, ToolHandler, ToolInvocation, ToolResult
81+
from .tools import (
82+
Tool,
83+
ToolHandler,
84+
ToolInvocation,
85+
ToolResult,
86+
tool_result_to_external_tool_text_result_for_llm,
87+
)
8388

8489
logger = logging.getLogger(__name__)
8590

@@ -1862,12 +1867,7 @@ async def _execute_tool_and_respond(
18621867
await self.rpc.tools.handle_pending_tool_call(
18631868
HandlePendingToolCallRequest(
18641869
request_id=request_id,
1865-
result=ExternalToolTextResultForLlm(
1866-
text_result_for_llm=tool_result.text_result_for_llm,
1867-
error=tool_result.error,
1868-
result_type=tool_result.result_type,
1869-
tool_telemetry=tool_result.tool_telemetry,
1870-
),
1870+
result=tool_result_to_external_tool_text_result_for_llm(tool_result),
18711871
)
18721872
)
18731873
log_timing(

python/copilot/tools.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515

1616
from pydantic import BaseModel
1717

18+
from .generated.rpc import (
19+
ExternalToolTextResultForLlm,
20+
ExternalToolTextResultForLlmBinaryResultsForLlm,
21+
ExternalToolTextResultForLlmBinaryResultsForLlmType,
22+
)
23+
1824
ToolResultType = Literal["success", "failure", "rejected", "denied", "timeout"]
1925

2026

@@ -371,3 +377,29 @@ def convert_mcp_call_tool_result(call_result: dict[str, Any]) -> ToolResult:
371377
result_type="failure" if call_result.get("isError") is True else "success",
372378
binary_results_for_llm=binary_results if binary_results else None,
373379
)
380+
381+
382+
def tool_result_to_external_tool_text_result_for_llm(
383+
tool_result: ToolResult,
384+
) -> ExternalToolTextResultForLlm:
385+
"""Convert a ToolResult into the RPC payload sent to HandlePendingToolCall."""
386+
binary_results_for_llm = None
387+
if tool_result.binary_results_for_llm:
388+
binary_results_for_llm = [
389+
ExternalToolTextResultForLlmBinaryResultsForLlm(
390+
data=binary_result.data,
391+
mime_type=binary_result.mime_type,
392+
type=ExternalToolTextResultForLlmBinaryResultsForLlmType(binary_result.type),
393+
description=binary_result.description or None,
394+
)
395+
for binary_result in tool_result.binary_results_for_llm
396+
]
397+
398+
return ExternalToolTextResultForLlm(
399+
text_result_for_llm=tool_result.text_result_for_llm,
400+
binary_results_for_llm=binary_results_for_llm,
401+
error=tool_result.error,
402+
result_type=tool_result.result_type,
403+
session_log=tool_result.session_log,
404+
tool_telemetry=tool_result.tool_telemetry,
405+
)

python/test_tools.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
from copilot import define_tool
99
from copilot.tools import (
10+
ToolBinaryResult,
1011
ToolInvocation,
1112
ToolResult,
1213
_normalize_result,
1314
convert_mcp_call_tool_result,
15+
tool_result_to_external_tool_text_result_for_llm,
1416
)
1517

1618

@@ -427,3 +429,39 @@ def test_call_tool_result_dict_is_json_serialized_by_normalize(self):
427429
result = _normalize_result({"content": [{"type": "text", "text": "hello"}]})
428430
parsed = json.loads(result.text_result_for_llm)
429431
assert parsed == {"content": [{"type": "text", "text": "hello"}]}
432+
433+
434+
class TestToolResultToExternalToolTextResultForLlm:
435+
def test_forwards_binary_results_and_session_log(self):
436+
tool_result = ToolResult(
437+
text_result_for_llm="screenshot captured",
438+
binary_results_for_llm=[
439+
ToolBinaryResult(
440+
data="base64data",
441+
mime_type="image/png",
442+
type="image",
443+
description="screenshot.png",
444+
)
445+
],
446+
session_log="tool execution details",
447+
tool_telemetry={"duration_ms": 42},
448+
)
449+
450+
rpc_result = tool_result_to_external_tool_text_result_for_llm(tool_result)
451+
452+
assert rpc_result.text_result_for_llm == "screenshot captured"
453+
assert rpc_result.session_log == "tool execution details"
454+
assert rpc_result.tool_telemetry == {"duration_ms": 42}
455+
assert rpc_result.binary_results_for_llm is not None
456+
assert len(rpc_result.binary_results_for_llm) == 1
457+
assert rpc_result.binary_results_for_llm[0].data == "base64data"
458+
assert rpc_result.binary_results_for_llm[0].mime_type == "image/png"
459+
assert rpc_result.binary_results_for_llm[0].type.value == "image"
460+
assert rpc_result.binary_results_for_llm[0].description == "screenshot.png"
461+
462+
def test_omits_binary_results_when_none(self):
463+
tool_result = ToolResult(text_result_for_llm="done")
464+
rpc_result = tool_result_to_external_tool_text_result_for_llm(tool_result)
465+
assert rpc_result.binary_results_for_llm is None
466+
assert rpc_result.session_log is None
467+

0 commit comments

Comments
 (0)