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
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,12 @@ def _partition_tools(
user_config_tools: dict[str, Any] = {}

for ld_name, stub in tool_handlers.items():
if not config_tools or ld_name not in config_tools:
continue
native = getattr(stub, NATIVE_TOOL_KEY, None)
if isinstance(native, NativeTool):
native_tool_map[native.tool_name] = stub
elif config_tools and ld_name in config_tools:
else:
user_config_tools[ld_name] = config_tools[ld_name]

return native_tool_map, user_config_tools, list(native_tool_map.keys())
Expand Down
49 changes: 47 additions & 2 deletions packages/claude-agents/tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,20 @@ def test_native_tool_goes_to_native_bucket(self) -> None:
assert native_names == ["WebSearch"]
assert user_tools == {}

def test_native_tool_absent_from_the_variation_is_not_enabled(self) -> None:
from launchdarkly_ai_claude_agents.builtins import ClaudeWebSearch
from launchdarkly_ai_server import NATIVE_TOOL_KEY

stub = lambda: None # noqa: E731
setattr(stub, NATIVE_TOOL_KEY, ClaudeWebSearch)
for config_tools in (None, {}, {"other": {"name": "other"}}):
native_map, user_tools, native_names = _partition_tools(
config_tools, {"webSearch": stub}
)
assert native_map == {}
assert user_tools == {}
assert native_names == []

def test_native_tool_aliases_map_ld_key_to_provider_name(self) -> None:
from launchdarkly_ai_claude_agents.builtins import ClaudeWebSearch
from launchdarkly_ai_server import NATIVE_TOOL_KEY
Expand Down Expand Up @@ -2496,15 +2510,46 @@ async def test_sends_exactly_the_native_tool_provider_names(
read, bash = MagicMock(), MagicMock()
setattr(read, NATIVE_TOOL_KEY, ClaudeRead)
setattr(bash, NATIVE_TOOL_KEY, ClaudeBash)
handlers = {"read": read, "bash": bash}
listed = {
**BASE_CONFIG,
"tools": {
"read": {"name": "read", "type": "function", "parameters": {}},
"bash": {"name": "bash", "type": "function", "parameters": {}},
},
}
opts = await _query_options(
monkeypatch,
create_claude_agents_handler(),
path,
BASE_CONFIG,
{"read": read, "bash": bash},
listed,
handlers,
)
assert sorted(opts.tools) == ["Bash", "Read"]

for config in (
BASE_CONFIG,
{**BASE_CONFIG, "tools": {}},
{
**BASE_CONFIG,
"tools": {
"search": {
"name": "search",
"type": "function",
"parameters": {},
}
},
},
):
omitted = await _query_options(
monkeypatch,
create_claude_agents_handler(),
path,
config,
handlers,
)
assert omitted.tools == []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: these cases cover all-attached and none-attached, but not the one this fix is for: some registered built-ins attached and others not. Please add a variation that attaches only read, keep both read and bash in handlers, and assert opts.tools == ["Read"]. That's the test that catches a regression back to enabling everything registered.



class TestAbandonedToolSpansAreNotErrors:
"""An abandoned stream leaves an open tool span UNSET, not ERROR.
Expand Down
32 changes: 30 additions & 2 deletions packages/claude-agents/tests/test_native_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ class TestToClaudeAgentsHostIsolation:
@staticmethod
async def _root_options(
tool_handlers: dict[str, Any] | None = None,
graph_def: GraphDefinition | None = None,
) -> dict[str, Any]:
mock_sdk = _make_sdk_mock("done")
captured: list[dict[str, Any]] = []
Expand All @@ -509,7 +510,7 @@ async def _root_options(
),
):
await to_claude_agents(
_make_def_promise(_make_graph_def()),
_make_def_promise(graph_def or _make_graph_def()),
opts={"tool_handlers": tool_handlers or {}},
).invoke("hi")
assert len(captured) == 1
Expand Down Expand Up @@ -581,8 +582,35 @@ async def test_sends_an_empty_tool_list_with_no_native_tools(self) -> None:
assert opts["tools"] == []

@pytest.mark.asyncio
async def test_sends_the_native_tool_names(self) -> None:
async def test_does_not_enable_a_native_tool_the_variation_omits(self) -> None:
opts = await self._root_options({"run-bash": NativeTool("Bash")})
assert opts["tools"] == []

async def test_sends_the_native_tool_names(self) -> None:
graph_def = _make_graph_def(
nodes={
"root": {
"key": "root",
"config": {
"model": {"name": "claude-3"},
"instructions": "be helpful",
"tools": {
"run-bash": {
"name": "run-bash",
"type": "function",
"parameters": {},
}
},
},
"meta": {"variationKey": "v1", "version": 1},
"edges": [],
"is_terminal": True,
}
}
)
opts = await self._root_options(
{"run-bash": NativeTool("Bash")}, graph_def=graph_def
)
assert opts["tools"] == ["Bash"]

@pytest.mark.asyncio
Expand Down
15 changes: 14 additions & 1 deletion packages/claude-agents/tests/test_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,22 @@ async def test_skill_joins_the_builtin_set_once_last(
assert fake.options.tools == ["Skill"]
assert "Skill" not in fake.options.allowed_tools

unlisted = _FakeQuery()
monkeypatch.setattr(handler_mod, "query", unlisted)
await _run(handler, path, _config(("a", 1)), {"read": _read_stub()})
assert unlisted.options.tools == ["Skill"]

with_read = _FakeQuery()
monkeypatch.setattr(handler_mod, "query", with_read)
await _run(handler, path, _config(("a", 1)), {"read": _read_stub()})
await _run(
handler,
path,
_config(
("a", 1),
tools={"read": {"name": "read", "type": "function", "parameters": {}}},
),
{"read": _read_stub()},
)
assert with_read.options.tools == ["Read", "Skill"]
assert "Skill" not in with_read.options.allowed_tools

Expand Down
Loading