Files
cleveragents-core/features/steps/tool_runtime_steps.py
T
Jeff (CTO) 18482b938f
CI / typecheck (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / quality (push) Waiting to run
CI / unit_tests (push) Waiting to run
CI / integration_tests (push) Waiting to run
CI / coverage (push) Blocked by required conditions
CI / build (push) Waiting to run
CI / docker (push) Blocked by required conditions
CI / lint (pull_request) Successful in 14s
CI / typecheck (pull_request) Successful in 27s
CI / security (pull_request) Successful in 22s
CI / quality (pull_request) Successful in 15s
CI / integration_tests (pull_request) Successful in 4m32s
CI / build (pull_request) Successful in 16s
CI / unit_tests (pull_request) Successful in 9m45s
CI / coverage (pull_request) Successful in 6m53s
CI / docker (pull_request) Successful in 39s
feat(tool): add tool runtime core
2026-02-14 13:46:10 -05:00

484 lines
15 KiB
Python

"""Step definitions for the Tool Runtime Core feature."""
import json
import threading
from typing import Any
from behave import given, then, when
from cleveragents.domain.models.core.tool import ToolCapability
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runner import ToolRunner
from cleveragents.tool.runtime import ToolError, ToolResult, ToolSpec
__all__: list[str] = []
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _echo_handler(inputs: dict[str, Any]) -> dict[str, Any]:
return dict(inputs)
def _adder_handler(inputs: dict[str, Any]) -> dict[str, Any]:
return {"sum": inputs.get("a", 0) + inputs.get("b", 0)}
def _failing_handler(inputs: dict[str, Any]) -> dict[str, Any]:
raise RuntimeError("handler exploded")
def _non_serialisable_handler(inputs: dict[str, Any]) -> dict[str, Any]:
return {"bad": object()}
def _scalar_handler(inputs: dict[str, Any]) -> int:
return 42
# ---------------------------------------------------------------------------
# Givens
# ---------------------------------------------------------------------------
@given("a tool registry")
def step_given_tool_registry(context: Any) -> None:
context.registry = ToolRegistry()
@given('a tool spec named "{name}" with a handler')
def step_given_tool_spec_with_handler(context: Any, name: str) -> None:
context.tool_spec = ToolSpec(
name=name,
description=f"Test tool {name}",
handler=_echo_handler,
)
@given('a registered tool spec named "{name}"')
def step_given_registered_tool_spec(context: Any, name: str) -> None:
spec = ToolSpec(
name=name,
description=f"Test tool {name}",
handler=_echo_handler,
)
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(
name=name,
description=f"Adder tool {name}",
handler=_adder_handler,
)
context.registry.register(spec)
@given('a registered tool spec named "{name}" with a failing handler')
def step_given_registered_failing(context: Any, name: str) -> None:
spec = ToolSpec(
name=name,
description=f"Failing tool {name}",
handler=_failing_handler,
)
context.registry.register(spec)
@given('a registered tool spec named "{name}" with a non-serialisable handler')
def step_given_registered_non_serialisable(context: Any, name: str) -> None:
spec = ToolSpec(
name=name,
description=f"Bad output tool {name}",
handler=_non_serialisable_handler,
)
context.registry.register(spec)
@given('a registered tool spec named "{name}" with a scalar handler')
def step_given_registered_scalar(context: Any, name: str) -> None:
spec = ToolSpec(
name=name,
description=f"Scalar tool {name}",
handler=_scalar_handler,
)
context.registry.register(spec)
@given('a tool spec named "{name}" with read_only capability')
def step_given_spec_read_only(context: Any, name: str) -> None:
context.tool_spec = ToolSpec(
name=name,
description=f"Read-only tool {name}",
capabilities=ToolCapability(read_only=True),
handler=_echo_handler,
)
@given('a tool spec named "{name}" with writes capability')
def step_given_spec_writes(context: Any, name: str) -> None:
context.tool_spec = ToolSpec(
name=name,
description=f"Writer tool {name}",
capabilities=ToolCapability(writes=True),
handler=_echo_handler,
)
@given("a tool runner for the registry")
def step_given_tool_runner(context: Any) -> None:
context.runner = ToolRunner(context.registry)
context.lifecycle_events = []
@given("an alternate registry with {count:d} tools")
def step_given_alternate_registry(context: Any, count: int) -> None:
context.alt_registry = ToolRegistry()
for i in range(count):
spec = ToolSpec(
name=f"alt/tool-{i}",
description=f"Alt tool {i}",
handler=_echo_handler,
)
context.alt_registry.register(spec)
# ---------------------------------------------------------------------------
# Whens
# ---------------------------------------------------------------------------
@when("I register the tool spec")
def step_when_register(context: Any) -> None:
context.registry.register(context.tool_spec)
@when('I get the tool by name "{name}"')
def step_when_get_tool(context: Any, name: str) -> None:
context.retrieved_spec = context.registry.get(name)
@when("I list all tools")
def step_when_list_all(context: Any) -> None:
context.tool_list = context.registry.list_tools()
@when('I list tools with namespace "{ns}"')
def step_when_list_namespace(context: Any, ns: str) -> None:
context.tool_list = context.registry.list_tools(namespace=ns)
@when('I list tools with tool_type "{tt}"')
def step_when_list_tool_type(context: Any, tt: str) -> None:
context.tool_list = context.registry.list_tools(tool_type=tt)
@when('I remove the tool "{name}"')
def step_when_remove(context: Any, name: str) -> None:
context.removal_result = context.registry.remove(name)
@when('I try to register a duplicate tool "{name}"')
def step_when_duplicate_register(context: Any, name: str) -> None:
dup_spec = ToolSpec(
name=name,
description=f"Duplicate {name}",
handler=_echo_handler,
)
try:
context.registry.register(dup_spec)
context.tool_error = None
except ToolError as exc:
context.tool_error = exc
@when("I discover tools from the runner")
def step_when_discover(context: Any) -> None:
context.discovered = context.runner.discover()
context.lifecycle_events.append("discover")
@when('I activate tool "{name}"')
def step_when_activate(context: Any, name: str) -> None:
context.activated_spec = context.runner.activate(name)
if hasattr(context, "lifecycle_events"):
context.lifecycle_events.append("activate")
@when('I execute tool "{name}" with inputs {inputs_json}')
def step_when_execute(context: Any, name: str, inputs_json: str) -> None:
inputs = json.loads(inputs_json)
context.tool_result = context.runner.execute(name, inputs)
if hasattr(context, "lifecycle_events"):
context.lifecycle_events.append("execute")
@when('I deactivate tool "{name}"')
def step_when_deactivate(context: Any, name: str) -> None:
context.deactivation_result = context.runner.deactivate(name)
if hasattr(context, "lifecycle_events"):
context.lifecycle_events.append("deactivate")
@when("I concurrently register {count:d} tools")
def step_when_concurrent_register(context: Any, count: int) -> None:
errors: list[Exception] = []
barrier = threading.Barrier(count)
def _register(idx: int) -> None:
try:
barrier.wait(timeout=5)
spec = ToolSpec(
name=f"conc/tool-{idx}",
description=f"Concurrent tool {idx}",
handler=_echo_handler,
)
context.registry.register(spec)
except Exception as exc:
errors.append(exc)
threads = [threading.Thread(target=_register, args=(i,)) for i in range(count)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=10)
context.concurrent_errors = errors
@when('I execute tool "{name}" with non-serialisable inputs')
def step_when_execute_bad_inputs(context: Any, name: str) -> None:
bad_inputs: dict[str, Any] = {"bad": object()}
context.tool_result = context.runner.execute(name, bad_inputs)
@when(
'I create a ToolError with name "{name}" type "{err_type}" and details "{details}"'
)
def step_when_create_tool_error(
context: Any, name: str, err_type: str, details: str
) -> None:
context.tool_error_obj = ToolError(
tool_name=name,
error_type=err_type,
details=details,
)
@when("I create a ToolResult with success True and metadata")
def step_when_create_result_success(context: Any) -> None:
context.tool_result_obj = ToolResult(
success=True,
output={"value": 42},
metadata={"trace_id": "abc123"},
duration_ms=1.5,
)
@when('I create a ToolResult with success False and error "{error}"')
def step_when_create_result_failure(context: Any, error: str) -> None:
context.tool_result_obj = ToolResult(
success=False,
output={},
error=error,
duration_ms=0.0,
)
@when('I try to activate a missing tool "{name}"')
def step_when_activate_missing(context: Any, name: str) -> None:
try:
context.runner.activate(name)
context.tool_error = None
except ToolError as exc:
context.tool_error = exc
@when('I try to execute a missing tool "{name}" with inputs {inputs_json}')
def step_when_execute_missing(context: Any, name: str, inputs_json: str) -> None:
try:
inputs = json.loads(inputs_json)
context.runner.execute(name, inputs)
context.tool_error = None
except ToolError as exc:
context.tool_error = exc
@when("I discover tools from the alternate registry")
def step_when_discover_alt(context: Any) -> None:
context.discovered = context.runner.discover(registry=context.alt_registry)
# ---------------------------------------------------------------------------
# Thens
# ---------------------------------------------------------------------------
@then("the tool should be in the registry")
def step_then_tool_in_registry(context: Any) -> None:
found = context.registry.get(context.tool_spec.name)
assert found is not None, "Tool not found in registry"
assert found.name == context.tool_spec.name
@then('the retrieved tool spec name should be "{name}"')
def step_then_retrieved_name(context: Any, name: str) -> None:
assert context.retrieved_spec is not None
assert context.retrieved_spec.name == name
@then("the retrieved tool spec should be None")
def step_then_retrieved_none(context: Any) -> None:
assert context.retrieved_spec is None
@then("the tool list should contain {count:d} tools")
def step_then_list_count(context: Any, count: int) -> None:
assert len(context.tool_list) == count, (
f"Expected {count} tools, got {len(context.tool_list)}"
)
@then("the removal should succeed")
def step_then_removal_success(context: Any) -> None:
assert context.removal_result is True
@then("the removal should fail")
def step_then_removal_fail(context: Any) -> None:
assert context.removal_result is False
@then('getting "{name}" should return None')
def step_then_get_returns_none(context: Any, name: str) -> None:
assert context.registry.get(name) is None
@then('a ToolError should be raised with type "{err_type}"')
def step_then_tool_error_raised(context: Any, err_type: str) -> None:
assert context.tool_error is not None, "Expected ToolError but none was raised"
assert context.tool_error.error_type == err_type
@then("the tool spec capabilities read_only should be True")
def step_then_cap_read_only(context: Any) -> None:
assert context.tool_spec.capabilities.read_only is True
@then("the tool spec capabilities writes should be False")
def step_then_cap_no_writes(context: Any) -> None:
assert context.tool_spec.capabilities.writes is False
@then("the tool spec capabilities writes should be True")
def step_then_cap_writes(context: Any) -> None:
assert context.tool_spec.capabilities.writes is True
@then("the lifecycle should complete in order")
def step_then_lifecycle_order(context: Any) -> None:
expected = ["discover", "activate", "execute", "deactivate"]
assert context.lifecycle_events == expected, (
f"Expected {expected}, got {context.lifecycle_events}"
)
@then("the tool result should be successful")
def step_then_result_success(context: Any) -> None:
assert context.tool_result.success is True
@then("the tool result should not be successful")
def step_then_result_failure(context: Any) -> None:
assert context.tool_result.success is False
@then('the tool result output should contain key "{key}"')
def step_then_result_has_key(context: Any, key: str) -> None:
assert key in context.tool_result.output, (
f"Key '{key}' not in output: {context.tool_result.output}"
)
@then("the tool result duration_ms should be non-negative")
def step_then_duration_non_negative(context: Any) -> None:
assert context.tool_result.duration_ms >= 0.0
@then("the tool result error should not be empty")
def step_then_result_error_present(context: Any) -> None:
assert context.tool_result.error is not None
assert len(context.tool_result.error) > 0
@then('the tool result error should mention "{text}"')
def step_then_result_error_mentions(context: Any, text: str) -> None:
assert context.tool_result.error is not None
assert text in context.tool_result.error, (
f"Expected '{text}' in error: {context.tool_result.error}"
)
@then("all {count:d} tools should be in the registry")
def step_then_all_concurrent_registered(context: Any, count: int) -> None:
assert len(context.concurrent_errors) == 0, (
f"Errors during concurrent registration: {context.concurrent_errors}"
)
tools = context.registry.list_tools()
assert len(tools) == count, f"Expected {count} tools, got {len(tools)}"
@then('the ToolError tool_name should be "{name}"')
def step_then_error_tool_name(context: Any, name: str) -> None:
assert context.tool_error_obj.tool_name == name
@then('the ToolError error_type should be "{err_type}"')
def step_then_error_type(context: Any, err_type: str) -> None:
assert context.tool_error_obj.error_type == err_type
@then('the ToolError details should be "{details}"')
def step_then_error_details(context: Any, details: str) -> None:
assert context.tool_error_obj.details == details
@then('the ToolError message should contain "{text}"')
def step_then_error_message_contains(context: Any, text: str) -> None:
assert text in str(context.tool_error_obj)
@then("the ToolResult success should be True")
def step_then_result_obj_success_true(context: Any) -> None:
assert context.tool_result_obj.success is True
@then("the ToolResult success should be False")
def step_then_result_obj_success_false(context: Any) -> None:
assert context.tool_result_obj.success is False
@then("the ToolResult metadata should not be empty")
def step_then_result_metadata_not_empty(context: Any) -> None:
assert len(context.tool_result_obj.metadata) > 0
@then('the ToolResult error should be "{error}"')
def step_then_result_obj_error(context: Any, error: str) -> None:
assert context.tool_result_obj.error == error
@then("the deactivation should return False")
def step_then_deactivation_false(context: Any) -> None:
assert context.deactivation_result is False
@then("the discovered list should contain {count:d} tools")
def step_then_discovered_count(context: Any, count: int) -> None:
assert len(context.discovered) == count, (
f"Expected {count} discovered, got {len(context.discovered)}"
)