diff --git a/features/consolidated_tool.feature b/features/consolidated_tool.feature index 356850273..220217a9e 100644 --- a/features/consolidated_tool.feature +++ b/features/consolidated_tool.feature @@ -966,9 +966,50 @@ Feature: Consolidated Tool # ---- List Tools With tool_type Filter ---- - Scenario: List tools with tool_type filter + Scenario: List tools with tool_type filter returns matching tools Given a tool registry - And a registered tool spec named "test/filtered" + And a registered tool spec named "test/filtered" with tool_type "tool" When I list tools with tool_type "tool" Then the tool list should contain 1 tools + Scenario: List tools with tool_type validation returns only validations + Given a tool registry + And a registered tool spec named "test/my-tool" with tool_type "tool" + And a registered tool spec named "test/my-validation" with tool_type "validation" + When I list tools with tool_type "validation" + Then the tool list should contain 1 tools + + Scenario: List tools with tool_type tool excludes validations + Given a tool registry + And a registered tool spec named "test/plain-tool" with tool_type "tool" + And a registered tool spec named "test/plain-validation" with tool_type "validation" + When I list tools with tool_type "tool" + Then the tool list should contain 1 tools + + Scenario: List tools without tool_type filter returns all tools + Given a tool registry + And a registered tool spec named "test/all-tool" with tool_type "tool" + And a registered tool spec named "test/all-validation" with tool_type "validation" + When I list all tools + Then the tool list should contain 2 tools + + Scenario: List tools with tool_type None returns all tools + Given a tool registry + And a registered tool spec named "test/none-tool" with tool_type "tool" + And a registered tool spec named "test/none-validation" with tool_type "validation" + When I list all tools + Then the tool list should contain 2 tools + + Scenario: ToolSpec default tool_type is tool + Given a tool registry + And a registered tool spec named "test/default-type" + When I list tools with tool_type "tool" + Then the tool list should contain 1 tools + + Scenario: ToolSpec with validation tool_type is excluded from tool filter + Given a tool registry + And a registered tool spec named "test/excl-validation" with tool_type "validation" + When I list tools with tool_type "tool" + Then the tool list should contain 0 tools + + diff --git a/features/steps/tool_runtime_steps.py b/features/steps/tool_runtime_steps.py index 519081c60..48d396c7d 100644 --- a/features/steps/tool_runtime_steps.py +++ b/features/steps/tool_runtime_steps.py @@ -68,6 +68,17 @@ def step_given_registered_tool_spec(context: Any, name: str) -> None: context.registry.register(spec) +@given('a registered tool spec named "{name}" with tool_type "{tt}"') +def step_given_registered_tool_spec_with_type(context: Any, name: str, tt: str) -> None: + spec = ToolSpec( + name=name, + description=f"Test tool {name}", + handler=_echo_handler, + tool_type=tt, + ) + context.registry.register(spec) + + @given('a registered tool spec named "{name}" with an adder handler') def step_given_registered_adder(context: Any, name: str) -> None: spec = ToolSpec( diff --git a/robot/helper_tool_cli.py b/robot/helper_tool_cli.py index aa266f49e..1bf083b47 100644 --- a/robot/helper_tool_cli.py +++ b/robot/helper_tool_cli.py @@ -193,9 +193,43 @@ def validation_detach() -> None: print("validation-cli-detach-ok") +def tool_list_type_tool() -> None: + """Verify tool list --type tool returns only tools.""" + svc = MagicMock() + svc.list_tools.return_value = [_mock_tool("local/smoke-tool", "tool")] + with patch( + "cleveragents.cli.commands.tool._get_tool_registry_service", + return_value=svc, + ): + result = runner.invoke(tool_app, ["list", "--type", "tool"]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + svc.list_tools.assert_called_once_with( + namespace=None, tool_type="tool", source=None + ) + print("tool-cli-list-type-tool-ok") + + +def tool_list_type_validation() -> None: + """Verify tool list --type validation returns only validations.""" + svc = MagicMock() + svc.list_tools.return_value = [_mock_tool("local/smoke-val", "validation")] + with patch( + "cleveragents.cli.commands.tool._get_tool_registry_service", + return_value=svc, + ): + result = runner.invoke(tool_app, ["list", "--type", "validation"]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + svc.list_tools.assert_called_once_with( + namespace=None, tool_type="validation", source=None + ) + print("tool-cli-list-type-validation-ok") + + _COMMANDS = { "tool-add-config": tool_add_config, "tool-list": tool_list_all, + "tool-list-type-tool": tool_list_type_tool, + "tool-list-type-validation": tool_list_type_validation, "tool-show": tool_show_name, "tool-remove": tool_remove_name, "validation-add-config": validation_add_config, diff --git a/robot/tool_cli.robot b/robot/tool_cli.robot index cca076684..006cec8d7 100644 --- a/robot/tool_cli.robot +++ b/robot/tool_cli.robot @@ -55,3 +55,19 @@ Validation Detach Removes Attachment ${result}= Run Process ${PYTHON} ${HELPER} validation-detach cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} validation-cli-detach-ok + +Tool List Type Tool Filter Works + [Documentation] Verify that ``tool list --type tool`` passes tool_type to service + ${result}= Run Process ${PYTHON} ${HELPER} tool-list-type-tool cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tool-cli-list-type-tool-ok + +Tool List Type Validation Filter Works + [Documentation] Verify that ``tool list --type validation`` passes tool_type to service + ${result}= Run Process ${PYTHON} ${HELPER} tool-list-type-validation cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tool-cli-list-type-validation-ok diff --git a/src/cleveragents/tool/registry.py b/src/cleveragents/tool/registry.py index f0154878a..a6c573548 100644 --- a/src/cleveragents/tool/registry.py +++ b/src/cleveragents/tool/registry.py @@ -75,8 +75,9 @@ class ToolRegistry: If provided, only return tools whose name starts with ``namespace/``. tool_type: - Reserved for future use (e.g. filtering by ``tool`` vs - ``validation``). Currently unused. + If provided, only return tools whose ``tool_type`` matches + (``"tool"`` or ``"validation"``). When ``None``, all tools + are returned regardless of type. source: If provided, only return tools whose ``source`` field matches. """ @@ -90,9 +91,8 @@ class ToolRegistry: if source is not None: specs = [s for s in specs if s.source == source] - # tool_type filtering is a no-op for now but the parameter is - # part of the public API to avoid a breaking change later. - _ = tool_type + if tool_type is not None: + specs = [s for s in specs if s.tool_type == tool_type] return specs diff --git a/src/cleveragents/tool/runtime.py b/src/cleveragents/tool/runtime.py index 5bc6de959..fdd8b6303 100644 --- a/src/cleveragents/tool/runtime.py +++ b/src/cleveragents/tool/runtime.py @@ -29,7 +29,7 @@ Provides the execution-layer data structures that sit on top of the domain from __future__ import annotations from collections.abc import Callable -from typing import Any +from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -82,6 +82,10 @@ class ToolSpec(BaseModel): ..., description="Callable that executes the tool logic", ) + tool_type: Literal["tool", "validation"] = Field( + default="tool", + description="Discriminator: tool or validation", + ) source: str = Field( default="builtin", description=("Origin of the tool: builtin, agent_skills, mcp, or custom"),