Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions python/packages/anthropic/agent_framework_anthropic/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
FunctionTool,
Message,
ResponseStream,
ShellTool,
TextSpanRegion,
UsageDetails,
)
Expand Down Expand Up @@ -716,7 +717,16 @@ def _prepare_tools_for_anthropic(self, options: Mapping[str, Any]) -> dict[str,
tool_list: list[Any] = []
mcp_server_list: list[Any] = []
for tool in tools:
if isinstance(tool, FunctionTool):
if isinstance(tool, ShellTool):
api_type = (tool.additional_properties or {}).get("type", "bash_20250124")
# Anthropic requires name="bash" — align tool.name so
# the function invocation layer can match tool_use calls.
tool.name = "bash"
tool_list.append({
"type": api_type,
"name": "bash",
})
elif isinstance(tool, FunctionTool):
tool_list.append({
"type": "custom",
"name": tool.name,
Expand Down Expand Up @@ -1006,33 +1016,29 @@ def _parse_contents_from_anthropic(
)
)
case "bash_code_execution_tool_result":
bash_outputs: list[Content] = []
shell_outputs: list[Content] = []
if content_block.content:
if isinstance(
content_block.content,
BetaBashCodeExecutionToolResultError,
):
bash_outputs.append(
Content.from_error(
message=content_block.content.error_code,
shell_outputs.append(
Content.from_shell_command_output(
stderr=content_block.content.error_code,
timed_out=content_block.content.error_code == "execution_time_exceeded",
raw_representation=content_block.content,
)
)
else:
if content_block.content.stdout:
bash_outputs.append(
Content.from_text(
text=content_block.content.stdout,
raw_representation=content_block.content,
)
)
if content_block.content.stderr:
bash_outputs.append(
Content.from_error(
message=content_block.content.stderr,
raw_representation=content_block.content,
)
shell_outputs.append(
Content.from_shell_command_output(
stdout=content_block.content.stdout or None,
stderr=content_block.content.stderr or None,
exit_code=int(content_block.content.return_code),
timed_out=False,
raw_representation=content_block.content,
)
)
for bash_file_content in content_block.content.content:
contents.append(
Content.from_hosted_file(
Expand All @@ -1041,9 +1047,9 @@ def _parse_contents_from_anthropic(
)
)
contents.append(
Content.from_function_result(
Content.from_shell_tool_result(
call_id=content_block.tool_use_id,
result=bash_outputs,
outputs=shell_outputs,
raw_representation=content_block,
)
)
Expand Down
85 changes: 81 additions & 4 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,42 @@ def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: Mag
assert result["tools"][0]["name"] == "code_execution"


def _dummy_bash(command: str) -> str:
return f"executed: {command}"


def test_prepare_tools_for_anthropic_shell_tool(mock_anthropic_client: MagicMock) -> None:
"""Test converting ShellTool to Anthropic bash format."""
from agent_framework import ShellTool

client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[ShellTool(func=_dummy_bash)])

result = client._prepare_tools_for_anthropic(chat_options)

assert result is not None
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "bash_20250124"
assert result["tools"][0]["name"] == "bash"


def test_prepare_tools_for_anthropic_shell_tool_custom_type(mock_anthropic_client: MagicMock) -> None:
"""Test shell tool with custom type via additional_properties."""
from agent_framework import ShellTool

client = create_test_anthropic_client(mock_anthropic_client)
shell = ShellTool(func=_dummy_bash, additional_properties={"type": "bash_20241022"})
chat_options = ChatOptions(tools=[shell])

result = client._prepare_tools_for_anthropic(chat_options)

assert result is not None
assert "tools" in result
assert result["tools"][0]["type"] == "bash_20241022"
assert result["tools"][0]["name"] == "bash"


def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) -> None:
"""Test converting MCP dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
Expand Down Expand Up @@ -1733,14 +1769,15 @@ def test_parse_code_execution_result_with_files(mock_anthropic_client: MagicMock


def test_parse_bash_execution_result_with_stdout(mock_anthropic_client: MagicMock) -> None:
"""Test parsing bash execution result with stdout."""
"""Test parsing bash execution result with stdout produces shell_tool_result."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_bash2", "bash_code_execution")

# Create mock bash execution result with stdout
mock_content = MagicMock()
mock_content.stdout = "Output text"
mock_content.stderr = None
mock_content.return_code = 0
mock_content.content = []

mock_block = MagicMock()
Expand All @@ -1751,18 +1788,26 @@ def test_parse_bash_execution_result_with_stdout(mock_anthropic_client: MagicMoc
result = client._parse_contents_from_anthropic([mock_block])

assert len(result) == 1
assert result[0].type == "function_result"
assert result[0].type == "shell_tool_result"
assert result[0].call_id == "call_bash2"
assert result[0].outputs is not None
assert len(result[0].outputs) == 1
assert result[0].outputs[0].type == "shell_command_output"
assert result[0].outputs[0].stdout == "Output text"
assert result[0].outputs[0].exit_code == 0
assert result[0].outputs[0].timed_out is False


def test_parse_bash_execution_result_with_stderr(mock_anthropic_client: MagicMock) -> None:
"""Test parsing bash execution result with stderr."""
"""Test parsing bash execution result with stderr produces shell_tool_result."""
client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_bash3", "bash_code_execution")

# Create mock bash execution result with stderr
mock_content = MagicMock()
mock_content.stdout = None
mock_content.stderr = "Error output"
mock_content.return_code = 1
mock_content.content = []

mock_block = MagicMock()
Expand All @@ -1773,7 +1818,39 @@ def test_parse_bash_execution_result_with_stderr(mock_anthropic_client: MagicMoc
result = client._parse_contents_from_anthropic([mock_block])

assert len(result) == 1
assert result[0].type == "function_result"
assert result[0].type == "shell_tool_result"
assert result[0].call_id == "call_bash3"
assert result[0].outputs is not None
assert result[0].outputs[0].type == "shell_command_output"
assert result[0].outputs[0].stderr == "Error output"
assert result[0].outputs[0].exit_code == 1


def test_parse_bash_execution_result_with_error(mock_anthropic_client: MagicMock) -> None:
"""Test parsing bash execution error produces shell_tool_result with error info."""
from anthropic.types.beta.beta_bash_code_execution_tool_result_error import (
BetaBashCodeExecutionToolResultError,
)

client = create_test_anthropic_client(mock_anthropic_client)
client._last_call_id_name = ("call_bash_err", "bash_code_execution")

mock_error = MagicMock(spec=BetaBashCodeExecutionToolResultError)
mock_error.error_code = "execution_time_exceeded"

mock_block = MagicMock()
mock_block.type = "bash_code_execution_tool_result"
mock_block.tool_use_id = "call_bash_err"
mock_block.content = mock_error

result = client._parse_contents_from_anthropic([mock_block])

assert len(result) == 1
assert result[0].type == "shell_tool_result"
assert result[0].outputs is not None
assert result[0].outputs[0].type == "shell_command_output"
assert result[0].outputs[0].stderr == "execution_time_exceeded"
assert result[0].outputs[0].timed_out is True


# Text Editor Result Tests
Expand Down
4 changes: 4 additions & 0 deletions python/packages/core/agent_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
ShellTool,
ToolTypes,
normalize_function_invocation_configuration,
shell_tool,
tool,
)
from ._types import (
Expand Down Expand Up @@ -269,6 +271,7 @@
"RunnerContext",
"SecretString",
"SessionContext",
"ShellTool",
"SingleEdgeGroup",
"SubWorkflowRequestMessage",
"SubWorkflowResponseMessage",
Expand Down Expand Up @@ -329,6 +332,7 @@
"register_state_type",
"resolve_agent_id",
"response_handler",
"shell_tool",
"tool",
"validate_chat_options",
"validate_tool_mode",
Expand Down
6 changes: 5 additions & 1 deletion python/packages/core/agent_framework/_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,11 @@ def _propagate_conversation_id(update: AgentResponseUpdate) -> AgentResponseUpda

def _finalizer(updates: Sequence[AgentResponseUpdate]) -> AgentResponse[Any]:
ctx = ctx_holder["ctx"]
rf = ctx.get("chat_options", {}).get("response_format") if ctx else (options.get("response_format") if options else None)
rf = (
ctx.get("chat_options", {}).get("response_format")
if ctx
else (options.get("response_format") if options else None)
)
return self._finalize_response_updates(updates, response_format=rf)

return (
Expand Down
Loading