feat(plugin): register spec-defined extension points in PluginManager #1217
@@ -0,0 +1,229 @@
|
||||
Feature: Plugin Extension Points Registration
|
||||
The plugin infrastructure must register all 30 spec-defined extension
|
||||
points with typed Protocol interfaces in the PluginManager.
|
||||
|
||||
Based on issue #939.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# All 30 extension points are registered
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: All 30 extension points are registered via register_all_extension_points
|
||||
Given a fresh PluginManager for extension point registration
|
||||
When I register all spec-defined extension points
|
||||
Then exactly 30 extension points should be registered
|
||||
|
||||
Scenario: Extension point definitions are available as a tuple
|
||||
When I retrieve the extension point definitions
|
||||
Then I should get exactly 30 extension point definitions
|
||||
And each definition should have a non-empty name
|
||||
And each definition should have a protocol_type
|
||||
And each definition should have a non-empty description
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Extension point lookup by name
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: Extension point lookup by name returns the correct point
|
||||
Given a PluginManager with all extension points registered
|
||||
When I look up extension point "context.strategy"
|
||||
Then the looked-up extension point name should be "context.strategy"
|
||||
|
||||
Scenario: Extension point lookup for pipeline component by name
|
||||
Given a PluginManager with all extension points registered
|
||||
When I look up extension point "context.pipeline_component.scorer"
|
||||
Then the looked-up extension point name should be "context.pipeline_component.scorer"
|
||||
|
||||
Scenario: Extension point lookup for safety guardrail by name
|
||||
Given a PluginManager with all extension points registered
|
||||
When I look up extension point "safety.guardrail"
|
||||
Then the looked-up extension point name should be "safety.guardrail"
|
||||
|
||||
Scenario: Lookup of non-existent extension point returns None
|
||||
Given a PluginManager with all extension points registered
|
||||
When I look up extension point "nonexistent.point"
|
||||
Then the looked-up extension point should be None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Extension point lookup by category
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: Extension points grouped by category have correct counts
|
||||
When I retrieve extension points grouped by category
|
||||
Then the "context" category should have 12 extension points
|
||||
And the "output" category should have 3 extension points
|
||||
And the "validation" category should have 2 extension points
|
||||
And the "tool" category should have 2 extension points
|
||||
And the "skill" category should have 2 extension points
|
||||
And the "resource" category should have 2 extension points
|
||||
And the "a2a" category should have 2 extension points
|
||||
And the "event" category should have 2 extension points
|
||||
And the "config" category should have 2 extension points
|
||||
And the "safety" category should have 1 extension points
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Protocol type correctness
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: Context strategy extension point has correct protocol type
|
||||
Given a PluginManager with all extension points registered
|
||||
When I look up extension point "context.strategy"
|
||||
Then the protocol type name should be "ContextStrategyExtension"
|
||||
|
||||
Scenario: Output renderer extension point has correct protocol type
|
||||
Given a PluginManager with all extension points registered
|
||||
When I look up extension point "output.renderer"
|
||||
Then the protocol type name should be "OutputRendererExtension"
|
||||
|
||||
Scenario: Safety guardrail extension point has correct protocol type
|
||||
Given a PluginManager with all extension points registered
|
||||
When I look up extension point "safety.guardrail"
|
||||
Then the protocol type name should be "SafetyGuardrailExtension"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Plugin registration via entry points
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: Plugin registration via entry points discovers no plugins by default
|
||||
Given a fresh PluginManager for extension point registration
|
||||
When I discover extension point plugins from group "cleveragents.ext.test"
|
||||
Then the entry point discovered list should be empty
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# TOTAL_EXTENSION_POINTS constant
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: TOTAL_EXTENSION_POINTS constant equals 30
|
||||
Then the TOTAL_EXTENSION_POINTS constant should equal 30
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Context extension points wired to ACMSPipeline
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: ACMSPipeline reports context extension points when plugin manager is wired
|
||||
Given a PluginManager with all extension points registered
|
||||
And an ACMSPipeline wired with the plugin manager
|
||||
Then the pipeline should report 12 context extension points
|
||||
|
||||
Scenario: ACMSPipeline reports no context extension points when no plugin manager
|
||||
Given an ACMSPipeline without a plugin manager
|
||||
Then the pipeline should report 0 context extension points
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# All extension protocols are runtime-checkable
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: All extension protocol types are runtime-checkable
|
||||
When I retrieve all extension protocol types
|
||||
Then every protocol should be runtime-checkable
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Idempotent registration
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: Registering extension points twice does not duplicate
|
||||
Given a fresh PluginManager for extension point registration
|
||||
When I register all spec-defined extension points
|
||||
And I register all spec-defined extension points again
|
||||
Then exactly 30 extension points should be registered
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Extension point registry_key matches category
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: Each extension point registry_key matches its category
|
||||
When I retrieve the extension point definitions
|
||||
Then each definition registry_key should match its name category prefix
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Protocol implementation conformance
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: A concrete context strategy satisfies ContextStrategyExtension
|
||||
Given a concrete ContextStrategyExtension implementation
|
||||
Then it should be recognized as a ContextStrategyExtension instance
|
||||
And calling its protocol methods should succeed
|
||||
|
||||
Scenario: A concrete pipeline component satisfies ContextPipelineComponentExtension
|
||||
Given a concrete ContextPipelineComponentExtension implementation
|
||||
Then it should be recognized as a ContextPipelineComponentExtension instance
|
||||
And calling its pipeline component methods should succeed
|
||||
|
||||
Scenario: A concrete storage backend satisfies ContextStorageBackendExtension
|
||||
Given a concrete ContextStorageBackendExtension implementation
|
||||
Then it should be recognized as a ContextStorageBackendExtension instance
|
||||
And calling its storage methods should succeed
|
||||
|
||||
Scenario: A concrete output renderer satisfies OutputRendererExtension
|
||||
Given a concrete OutputRendererExtension implementation
|
||||
Then it should be recognized as an OutputRendererExtension instance
|
||||
And calling its renderer methods should succeed
|
||||
|
||||
Scenario: A concrete output materializer satisfies OutputMaterializerExtension
|
||||
Given a concrete OutputMaterializerExtension implementation
|
||||
Then it should be recognized as an OutputMaterializerExtension instance
|
||||
|
||||
Scenario: A concrete output format satisfies OutputFormatExtension
|
||||
Given a concrete OutputFormatExtension implementation
|
||||
Then it should be recognized as an OutputFormatExtension instance
|
||||
|
||||
Scenario: A concrete validation runner satisfies ValidationRunnerExtension
|
||||
Given a concrete ValidationRunnerExtension implementation
|
||||
Then it should be recognized as a ValidationRunnerExtension instance
|
||||
|
||||
Scenario: A concrete validation rule provider satisfies ValidationRuleProviderExtension
|
||||
Given a concrete ValidationRuleProviderExtension implementation
|
||||
Then it should be recognized as a ValidationRuleProviderExtension instance
|
||||
|
||||
Scenario: A concrete tool provider satisfies ToolProviderExtension
|
||||
Given a concrete ToolProviderExtension implementation
|
||||
Then it should be recognized as a ToolProviderExtension instance
|
||||
|
||||
Scenario: A concrete tool middleware satisfies ToolMiddlewareExtension
|
||||
Given a concrete ToolMiddlewareExtension implementation
|
||||
Then it should be recognized as a ToolMiddlewareExtension instance
|
||||
|
||||
Scenario: A concrete skill provider satisfies SkillProviderExtension
|
||||
Given a concrete SkillProviderExtension implementation
|
||||
Then it should be recognized as a SkillProviderExtension instance
|
||||
|
||||
Scenario: A concrete skill template satisfies SkillTemplateExtension
|
||||
Given a concrete SkillTemplateExtension implementation
|
||||
Then it should be recognized as a SkillTemplateExtension instance
|
||||
|
||||
Scenario: A concrete resource resolver satisfies ResourceResolverExtension
|
||||
Given a concrete ResourceResolverExtension implementation
|
||||
Then it should be recognized as a ResourceResolverExtension instance
|
||||
|
||||
Scenario: A concrete resource type handler satisfies ResourceTypeHandlerExtension
|
||||
Given a concrete ResourceTypeHandlerExtension implementation
|
||||
Then it should be recognized as a ResourceTypeHandlerExtension instance
|
||||
|
||||
Scenario: A concrete A2A transport satisfies A2ATransportExtension
|
||||
Given a concrete A2ATransportExtension implementation
|
||||
Then it should be recognized as an A2ATransportExtension instance
|
||||
|
||||
Scenario: A concrete A2A extension method satisfies A2AExtensionMethodExtension
|
||||
Given a concrete A2AExtensionMethodExtension implementation
|
||||
Then it should be recognized as an A2AExtensionMethodExtension instance
|
||||
|
||||
Scenario: A concrete event handler satisfies EventHandlerExtension
|
||||
Given a concrete EventHandlerExtension implementation
|
||||
Then it should be recognized as an EventHandlerExtension instance
|
||||
|
||||
Scenario: A concrete event filter satisfies EventFilterExtension
|
||||
Given a concrete EventFilterExtension implementation
|
||||
Then it should be recognized as an EventFilterExtension instance
|
||||
|
||||
Scenario: A concrete config source satisfies ConfigSourceExtension
|
||||
Given a concrete ConfigSourceExtension implementation
|
||||
Then it should be recognized as a ConfigSourceExtension instance
|
||||
|
||||
Scenario: A concrete config validator satisfies ConfigValidatorExtension
|
||||
Given a concrete ConfigValidatorExtension implementation
|
||||
Then it should be recognized as a ConfigValidatorExtension instance
|
||||
|
||||
Scenario: A concrete safety guardrail satisfies SafetyGuardrailExtension
|
||||
Given a concrete SafetyGuardrailExtension implementation
|
||||
Then it should be recognized as a SafetyGuardrailExtension instance
|
||||
And calling its guardrail methods should succeed
|
||||
@@ -0,0 +1,787 @@
|
||||
"""Step definitions for plugin_extension_points.feature.
|
||||
|
||||
Tests all 30 spec-defined extension points: registration, lookup
|
||||
by name, lookup by category, protocol type correctness, entry point
|
||||
discovery, context subsystem wiring, and idempotent registration.
|
||||
|
||||
Based on issue #939.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.infrastructure.plugins.extension_catalog import (
|
||||
TOTAL_EXTENSION_POINTS,
|
||||
get_extension_point_definitions,
|
||||
get_extension_points_by_category,
|
||||
register_all_extension_points,
|
||||
)
|
||||
from cleveragents.infrastructure.plugins.extension_protocols import (
|
||||
ALL_EXTENSION_PROTOCOLS,
|
||||
A2AExtensionMethodExtension,
|
||||
A2ATransportExtension,
|
||||
ConfigSourceExtension,
|
||||
ConfigValidatorExtension,
|
||||
ContextPipelineComponentExtension,
|
||||
ContextStorageBackendExtension,
|
||||
ContextStrategyExtension,
|
||||
EventFilterExtension,
|
||||
EventHandlerExtension,
|
||||
OutputFormatExtension,
|
||||
OutputMaterializerExtension,
|
||||
OutputRendererExtension,
|
||||
ResourceResolverExtension,
|
||||
ResourceTypeHandlerExtension,
|
||||
SafetyGuardrailExtension,
|
||||
SkillProviderExtension,
|
||||
SkillTemplateExtension,
|
||||
ToolMiddlewareExtension,
|
||||
ToolProviderExtension,
|
||||
ValidationRuleProviderExtension,
|
||||
ValidationRunnerExtension,
|
||||
)
|
||||
from cleveragents.infrastructure.plugins.manager import PluginManager
|
||||
from cleveragents.infrastructure.plugins.types import ExtensionPoint
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fresh PluginManager
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a fresh PluginManager for extension point registration")
|
||||
def step_fresh_pm(context: Context) -> None:
|
||||
context.ep_manager = PluginManager()
|
||||
|
||||
|
||||
@given("a PluginManager with all extension points registered")
|
||||
def step_pm_with_all_eps(context: Context) -> None:
|
||||
context.ep_manager = PluginManager()
|
||||
register_all_extension_points(context.ep_manager)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Register all extension points
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I register all spec-defined extension points")
|
||||
def step_register_all(context: Context) -> None:
|
||||
context.ep_registered_count = register_all_extension_points(
|
||||
context.ep_manager,
|
||||
)
|
||||
|
||||
|
||||
@when("I register all spec-defined extension points again")
|
||||
def step_register_all_again(context: Context) -> None:
|
||||
register_all_extension_points(context.ep_manager)
|
||||
|
||||
|
||||
@then("exactly {count:d} extension points should be registered")
|
||||
def step_exactly_n_registered(context: Context, count: int) -> None:
|
||||
eps = context.ep_manager.list_extension_points()
|
||||
assert len(eps) == count, f"Expected {count}, got {len(eps)}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Extension point definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I retrieve the extension point definitions")
|
||||
def step_retrieve_defs(context: Context) -> None:
|
||||
context.ep_definitions = get_extension_point_definitions()
|
||||
|
||||
|
||||
@then("I should get exactly {count:d} extension point definitions")
|
||||
def step_defs_count(context: Context, count: int) -> None:
|
||||
assert len(context.ep_definitions) == count, (
|
||||
f"Expected {count}, got {len(context.ep_definitions)}"
|
||||
)
|
||||
|
||||
|
||||
@then("each definition should have a non-empty name")
|
||||
def step_each_def_has_name(context: Context) -> None:
|
||||
for ep in context.ep_definitions:
|
||||
assert ep.name, f"Extension point has empty name: {ep}"
|
||||
|
||||
|
||||
@then("each definition should have a protocol_type")
|
||||
def step_each_def_has_protocol(context: Context) -> None:
|
||||
for ep in context.ep_definitions:
|
||||
assert ep.protocol_type is not None, (
|
||||
f"Extension point {ep.name} has no protocol_type"
|
||||
)
|
||||
|
||||
|
||||
@then("each definition should have a non-empty description")
|
||||
def step_each_def_has_description(context: Context) -> None:
|
||||
for ep in context.ep_definitions:
|
||||
assert ep.description, (
|
||||
f"Extension point {ep.name} has empty description"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lookup by name
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I look up extension point "{name}"')
|
||||
def step_lookup_by_name(context: Context, name: str) -> None:
|
||||
eps = context.ep_manager.list_extension_points()
|
||||
matches = [ep for ep in eps if ep.name == name]
|
||||
context.ep_lookup_result: ExtensionPoint | None = (
|
||||
matches[0] if matches else None
|
||||
)
|
||||
|
||||
|
||||
@then('the looked-up extension point name should be "{name}"')
|
||||
def step_lookup_name_is(context: Context, name: str) -> None:
|
||||
assert context.ep_lookup_result is not None, (
|
||||
f"Extension point '{name}' not found"
|
||||
)
|
||||
assert context.ep_lookup_result.name == name
|
||||
|
||||
|
||||
@then("the looked-up extension point should be None")
|
||||
def step_lookup_is_none(context: Context) -> None:
|
||||
assert context.ep_lookup_result is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lookup by category
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I retrieve extension points grouped by category")
|
||||
def step_grouped_by_category(context: Context) -> None:
|
||||
context.ep_categories = get_extension_points_by_category()
|
||||
|
||||
|
||||
@then('the "{category}" category should have {count:d} extension points')
|
||||
def step_category_count(context: Context, category: str, count: int) -> None:
|
||||
actual = len(context.ep_categories.get(category, []))
|
||||
assert actual == count, (
|
||||
f"Category '{category}': expected {count}, got {actual}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Protocol type correctness
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the protocol type name should be "{proto_name}"')
|
||||
def step_protocol_type_name(context: Context, proto_name: str) -> None:
|
||||
assert context.ep_lookup_result is not None
|
||||
actual = context.ep_lookup_result.protocol_type.__name__
|
||||
assert actual == proto_name, f"Expected '{proto_name}', got '{actual}'"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point discovery
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I discover extension point plugins from group "{group}"')
|
||||
def step_discover_ep_group(context: Context, group: str) -> None:
|
||||
context.ep_discovered = context.ep_manager.discover(group=group)
|
||||
|
||||
|
||||
@then("the entry point discovered list should be empty")
|
||||
def step_ep_discovered_empty(context: Context) -> None:
|
||||
assert len(context.ep_discovered) == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TOTAL_EXTENSION_POINTS constant
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the TOTAL_EXTENSION_POINTS constant should equal {value:d}")
|
||||
def step_total_constant(context: Context, value: int) -> None:
|
||||
assert value == TOTAL_EXTENSION_POINTS, (
|
||||
f"Expected {value}, got {TOTAL_EXTENSION_POINTS}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Context extension point wiring in ACMSPipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("an ACMSPipeline wired with the plugin manager")
|
||||
def step_pipeline_with_pm(context: Context) -> None:
|
||||
from cleveragents.application.services.acms_service import ACMSPipeline
|
||||
|
||||
context.ep_pipeline = ACMSPipeline(
|
||||
plugin_manager=context.ep_manager,
|
||||
)
|
||||
|
||||
|
||||
@given("an ACMSPipeline without a plugin manager")
|
||||
def step_pipeline_without_pm(context: Context) -> None:
|
||||
from cleveragents.application.services.acms_service import ACMSPipeline
|
||||
|
||||
context.ep_pipeline = ACMSPipeline()
|
||||
|
||||
|
||||
@then("the pipeline should report {count:d} context extension points")
|
||||
def step_pipeline_context_eps(context: Context, count: int) -> None:
|
||||
actual = len(context.ep_pipeline.context_extension_points)
|
||||
assert actual == count, (
|
||||
f"Expected {count} context extension points, got {actual}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Runtime-checkable protocols
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I retrieve all extension protocol types")
|
||||
def step_retrieve_all_protocols(context: Context) -> None:
|
||||
context.ep_protocol_map = ALL_EXTENSION_PROTOCOLS
|
||||
|
||||
|
||||
@then("every protocol should be runtime-checkable")
|
||||
def step_all_protocols_runtime_checkable(context: Context) -> None:
|
||||
for name, proto in context.ep_protocol_map.items():
|
||||
# runtime_checkable protocols have _is_runtime_protocol = True
|
||||
is_rc: Any = getattr(proto, "_is_runtime_protocol", False)
|
||||
assert is_rc, (
|
||||
f"Protocol {name} ({proto.__name__}) is not runtime-checkable"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# registry_key matches category
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("each definition registry_key should match its name category prefix")
|
||||
def step_registry_key_matches(context: Context) -> None:
|
||||
for ep in context.ep_definitions:
|
||||
expected_category = ep.name.split(".")[0]
|
||||
assert ep.registry_key == expected_category, (
|
||||
f"Extension point '{ep.name}': registry_key '{ep.registry_key}' "
|
||||
f"does not match expected category '{expected_category}'"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Concrete protocol implementations for coverage
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _StubContextStrategy:
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "stub"
|
||||
|
||||
def can_handle(self, request: Any) -> float:
|
||||
return 1.0
|
||||
|
||||
def assemble(self, fragments: Any, budget: Any) -> list[Any]:
|
||||
return []
|
||||
|
||||
|
||||
class _StubPipelineComponent:
|
||||
@property
|
||||
def component_name(self) -> str:
|
||||
return "stub_component"
|
||||
|
||||
def process(self, fragments: Any, context: Any) -> list[Any]:
|
||||
return list(fragments)
|
||||
|
||||
|
||||
class _StubStorageBackend:
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def backend_type(self) -> str:
|
||||
return "memory"
|
||||
|
||||
def store(self, key: str, data: Any) -> None:
|
||||
self._store[key] = data
|
||||
|
||||
def retrieve(self, key: str) -> Any:
|
||||
return self._store.get(key)
|
||||
|
||||
|
||||
class _StubOutputRenderer:
|
||||
@property
|
||||
def format_name(self) -> str:
|
||||
return "text"
|
||||
|
||||
def render(self, data: Any) -> str:
|
||||
return str(data)
|
||||
|
||||
|
||||
class _StubOutputMaterializer:
|
||||
@property
|
||||
def target_type(self) -> str:
|
||||
return "stdout"
|
||||
|
||||
def materialize(self, content: str, destination: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class _StubOutputFormat:
|
||||
@property
|
||||
def format_id(self) -> str:
|
||||
return "custom"
|
||||
|
||||
def serialize(self, data: Any) -> str:
|
||||
return str(data)
|
||||
|
||||
def deserialize(self, raw: str) -> Any:
|
||||
return raw
|
||||
|
||||
|
||||
class _StubValidationRunner:
|
||||
@property
|
||||
def runner_name(self) -> str:
|
||||
return "stub_runner"
|
||||
|
||||
def validate(self, target: Any) -> list[Any]:
|
||||
return []
|
||||
|
||||
|
||||
class _StubValidationRuleProvider:
|
||||
@property
|
||||
def provider_name(self) -> str:
|
||||
return "stub_provider"
|
||||
|
||||
def get_rules(self) -> list[Any]:
|
||||
return []
|
||||
|
||||
|
||||
class _StubToolProvider:
|
||||
@property
|
||||
def provider_name(self) -> str:
|
||||
return "stub_tools"
|
||||
|
||||
def list_tools(self) -> list[str]:
|
||||
return ["tool1"]
|
||||
|
||||
def invoke(self, tool_name: str, arguments: Any) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
class _StubToolMiddleware:
|
||||
@property
|
||||
def middleware_name(self) -> str:
|
||||
return "stub_mw"
|
||||
|
||||
def before_invoke(self, tool_name: str, arguments: Any) -> dict[str, Any]:
|
||||
return dict(arguments)
|
||||
|
||||
def after_invoke(self, tool_name: str, result: Any) -> Any:
|
||||
return result
|
||||
|
||||
|
||||
class _StubSkillProvider:
|
||||
@property
|
||||
def provider_name(self) -> str:
|
||||
return "stub_skills"
|
||||
|
||||
def list_skills(self) -> list[str]:
|
||||
return ["skill1"]
|
||||
|
||||
def get_skill(self, skill_name: str) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
class _StubSkillTemplate:
|
||||
@property
|
||||
def template_name(self) -> str:
|
||||
return "stub_template"
|
||||
|
||||
def render(self, context: Any) -> str:
|
||||
return "rendered"
|
||||
|
||||
|
||||
class _StubResourceResolver:
|
||||
@property
|
||||
def resolver_name(self) -> str:
|
||||
return "stub_resolver"
|
||||
|
||||
def resolve(self, uri: str) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
class _StubResourceTypeHandler:
|
||||
@property
|
||||
def handled_type(self) -> str:
|
||||
return "file"
|
||||
|
||||
def can_handle(self, resource_type: str) -> bool:
|
||||
return resource_type == "file"
|
||||
|
||||
def load(self, uri: str) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
class _StubA2ATransport:
|
||||
@property
|
||||
def transport_name(self) -> str:
|
||||
return "stub_transport"
|
||||
|
||||
def send(self, destination: str, payload: Any) -> None:
|
||||
pass
|
||||
|
||||
def receive(self) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
class _StubA2AExtensionMethod:
|
||||
@property
|
||||
def method_name(self) -> str:
|
||||
return "stub_method"
|
||||
|
||||
def execute(self, params: Any) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
class _StubEventHandler:
|
||||
@property
|
||||
def handler_name(self) -> str:
|
||||
return "stub_handler"
|
||||
|
||||
def handle(self, event: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class _StubEventFilter:
|
||||
@property
|
||||
def filter_name(self) -> str:
|
||||
return "stub_filter"
|
||||
|
||||
def should_propagate(self, event: Any) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class _StubConfigSource:
|
||||
@property
|
||||
def source_name(self) -> str:
|
||||
return "stub_source"
|
||||
|
||||
def load(self) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
class _StubConfigValidator:
|
||||
@property
|
||||
def validator_name(self) -> str:
|
||||
return "stub_validator"
|
||||
|
||||
def validate(self, config: Any) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
class _StubSafetyGuardrail:
|
||||
@property
|
||||
def guardrail_name(self) -> str:
|
||||
return "stub_guardrail"
|
||||
|
||||
def check(self, action: Any) -> bool:
|
||||
return True
|
||||
|
||||
def explain(self) -> str:
|
||||
return "Always allows"
|
||||
|
||||
|
||||
# --- Context Strategy ---
|
||||
|
||||
@given("a concrete ContextStrategyExtension implementation")
|
||||
def step_concrete_ctx_strategy(context: Context) -> None:
|
||||
context.ep_impl = _StubContextStrategy()
|
||||
|
||||
|
||||
@then("it should be recognized as a ContextStrategyExtension instance")
|
||||
def step_isinstance_ctx_strategy(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ContextStrategyExtension)
|
||||
|
||||
|
||||
@then("calling its protocol methods should succeed")
|
||||
def step_call_ctx_strategy_methods(context: Context) -> None:
|
||||
impl = context.ep_impl
|
||||
assert impl.name == "stub"
|
||||
assert impl.can_handle({}) == 1.0
|
||||
assert impl.assemble([], None) == []
|
||||
|
||||
|
||||
# --- Pipeline Component ---
|
||||
|
||||
@given("a concrete ContextPipelineComponentExtension implementation")
|
||||
def step_concrete_pipeline(context: Context) -> None:
|
||||
context.ep_impl = _StubPipelineComponent()
|
||||
|
||||
|
||||
@then("it should be recognized as a ContextPipelineComponentExtension instance")
|
||||
def step_isinstance_pipeline(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ContextPipelineComponentExtension)
|
||||
|
||||
|
||||
@then("calling its pipeline component methods should succeed")
|
||||
def step_call_pipeline_methods(context: Context) -> None:
|
||||
impl = context.ep_impl
|
||||
assert impl.component_name == "stub_component"
|
||||
assert impl.process(["a", "b"], {}) == ["a", "b"]
|
||||
|
||||
|
||||
# --- Storage Backend ---
|
||||
|
||||
@given("a concrete ContextStorageBackendExtension implementation")
|
||||
def step_concrete_storage(context: Context) -> None:
|
||||
context.ep_impl = _StubStorageBackend()
|
||||
|
||||
|
||||
@then("it should be recognized as a ContextStorageBackendExtension instance")
|
||||
def step_isinstance_storage(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ContextStorageBackendExtension)
|
||||
|
||||
|
||||
@then("calling its storage methods should succeed")
|
||||
def step_call_storage_methods(context: Context) -> None:
|
||||
impl = context.ep_impl
|
||||
assert impl.backend_type == "memory"
|
||||
impl.store("k", "v")
|
||||
assert impl.retrieve("k") == "v"
|
||||
|
||||
|
||||
# --- Output Renderer ---
|
||||
|
||||
@given("a concrete OutputRendererExtension implementation")
|
||||
def step_concrete_renderer(context: Context) -> None:
|
||||
context.ep_impl = _StubOutputRenderer()
|
||||
|
||||
|
||||
@then("it should be recognized as an OutputRendererExtension instance")
|
||||
def step_isinstance_renderer(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, OutputRendererExtension)
|
||||
|
||||
|
||||
@then("calling its renderer methods should succeed")
|
||||
def step_call_renderer_methods(context: Context) -> None:
|
||||
impl = context.ep_impl
|
||||
assert impl.format_name == "text"
|
||||
assert impl.render(42) == "42"
|
||||
|
||||
|
||||
# --- Output Materializer ---
|
||||
|
||||
@given("a concrete OutputMaterializerExtension implementation")
|
||||
def step_concrete_materializer(context: Context) -> None:
|
||||
context.ep_impl = _StubOutputMaterializer()
|
||||
|
||||
|
||||
@then("it should be recognized as an OutputMaterializerExtension instance")
|
||||
def step_isinstance_materializer(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, OutputMaterializerExtension)
|
||||
|
||||
|
||||
# --- Output Format ---
|
||||
|
||||
@given("a concrete OutputFormatExtension implementation")
|
||||
def step_concrete_format(context: Context) -> None:
|
||||
context.ep_impl = _StubOutputFormat()
|
||||
|
||||
|
||||
@then("it should be recognized as an OutputFormatExtension instance")
|
||||
def step_isinstance_format(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, OutputFormatExtension)
|
||||
|
||||
|
||||
# --- Validation Runner ---
|
||||
|
||||
@given("a concrete ValidationRunnerExtension implementation")
|
||||
def step_concrete_val_runner(context: Context) -> None:
|
||||
context.ep_impl = _StubValidationRunner()
|
||||
|
||||
|
||||
@then("it should be recognized as a ValidationRunnerExtension instance")
|
||||
def step_isinstance_val_runner(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ValidationRunnerExtension)
|
||||
|
||||
|
||||
# --- Validation Rule Provider ---
|
||||
|
||||
@given("a concrete ValidationRuleProviderExtension implementation")
|
||||
def step_concrete_val_rule(context: Context) -> None:
|
||||
context.ep_impl = _StubValidationRuleProvider()
|
||||
|
||||
|
||||
@then("it should be recognized as a ValidationRuleProviderExtension instance")
|
||||
def step_isinstance_val_rule(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ValidationRuleProviderExtension)
|
||||
|
||||
|
||||
# --- Tool Provider ---
|
||||
|
||||
@given("a concrete ToolProviderExtension implementation")
|
||||
def step_concrete_tool_provider(context: Context) -> None:
|
||||
context.ep_impl = _StubToolProvider()
|
||||
|
||||
|
||||
@then("it should be recognized as a ToolProviderExtension instance")
|
||||
def step_isinstance_tool_provider(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ToolProviderExtension)
|
||||
|
||||
|
||||
# --- Tool Middleware ---
|
||||
|
||||
@given("a concrete ToolMiddlewareExtension implementation")
|
||||
def step_concrete_tool_mw(context: Context) -> None:
|
||||
context.ep_impl = _StubToolMiddleware()
|
||||
|
||||
|
||||
@then("it should be recognized as a ToolMiddlewareExtension instance")
|
||||
def step_isinstance_tool_mw(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ToolMiddlewareExtension)
|
||||
|
||||
|
||||
# --- Skill Provider ---
|
||||
|
||||
@given("a concrete SkillProviderExtension implementation")
|
||||
def step_concrete_skill_provider(context: Context) -> None:
|
||||
context.ep_impl = _StubSkillProvider()
|
||||
|
||||
|
||||
@then("it should be recognized as a SkillProviderExtension instance")
|
||||
def step_isinstance_skill_provider(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, SkillProviderExtension)
|
||||
|
||||
|
||||
# --- Skill Template ---
|
||||
|
||||
@given("a concrete SkillTemplateExtension implementation")
|
||||
def step_concrete_skill_template(context: Context) -> None:
|
||||
context.ep_impl = _StubSkillTemplate()
|
||||
|
||||
|
||||
@then("it should be recognized as a SkillTemplateExtension instance")
|
||||
def step_isinstance_skill_template(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, SkillTemplateExtension)
|
||||
|
||||
|
||||
# --- Resource Resolver ---
|
||||
|
||||
@given("a concrete ResourceResolverExtension implementation")
|
||||
def step_concrete_res_resolver(context: Context) -> None:
|
||||
context.ep_impl = _StubResourceResolver()
|
||||
|
||||
|
||||
@then("it should be recognized as a ResourceResolverExtension instance")
|
||||
def step_isinstance_res_resolver(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ResourceResolverExtension)
|
||||
|
||||
|
||||
# --- Resource Type Handler ---
|
||||
|
||||
@given("a concrete ResourceTypeHandlerExtension implementation")
|
||||
def step_concrete_res_handler(context: Context) -> None:
|
||||
context.ep_impl = _StubResourceTypeHandler()
|
||||
|
||||
|
||||
@then("it should be recognized as a ResourceTypeHandlerExtension instance")
|
||||
def step_isinstance_res_handler(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ResourceTypeHandlerExtension)
|
||||
|
||||
|
||||
# --- A2A Transport ---
|
||||
|
||||
@given("a concrete A2ATransportExtension implementation")
|
||||
def step_concrete_a2a_transport(context: Context) -> None:
|
||||
context.ep_impl = _StubA2ATransport()
|
||||
|
||||
|
||||
@then("it should be recognized as an A2ATransportExtension instance")
|
||||
def step_isinstance_a2a_transport(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, A2ATransportExtension)
|
||||
|
||||
|
||||
# --- A2A Extension Method ---
|
||||
|
||||
@given("a concrete A2AExtensionMethodExtension implementation")
|
||||
def step_concrete_a2a_method(context: Context) -> None:
|
||||
context.ep_impl = _StubA2AExtensionMethod()
|
||||
|
||||
|
||||
@then("it should be recognized as an A2AExtensionMethodExtension instance")
|
||||
def step_isinstance_a2a_method(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, A2AExtensionMethodExtension)
|
||||
|
||||
|
||||
# --- Event Handler ---
|
||||
|
||||
@given("a concrete EventHandlerExtension implementation")
|
||||
def step_concrete_event_handler(context: Context) -> None:
|
||||
context.ep_impl = _StubEventHandler()
|
||||
|
||||
|
||||
@then("it should be recognized as an EventHandlerExtension instance")
|
||||
def step_isinstance_event_handler(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, EventHandlerExtension)
|
||||
|
||||
|
||||
# --- Event Filter ---
|
||||
|
||||
@given("a concrete EventFilterExtension implementation")
|
||||
def step_concrete_event_filter(context: Context) -> None:
|
||||
context.ep_impl = _StubEventFilter()
|
||||
|
||||
|
||||
@then("it should be recognized as an EventFilterExtension instance")
|
||||
def step_isinstance_event_filter(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, EventFilterExtension)
|
||||
|
||||
|
||||
# --- Config Source ---
|
||||
|
||||
@given("a concrete ConfigSourceExtension implementation")
|
||||
def step_concrete_config_source(context: Context) -> None:
|
||||
context.ep_impl = _StubConfigSource()
|
||||
|
||||
|
||||
@then("it should be recognized as a ConfigSourceExtension instance")
|
||||
def step_isinstance_config_source(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ConfigSourceExtension)
|
||||
|
||||
|
||||
# --- Config Validator ---
|
||||
|
||||
@given("a concrete ConfigValidatorExtension implementation")
|
||||
def step_concrete_config_validator(context: Context) -> None:
|
||||
context.ep_impl = _StubConfigValidator()
|
||||
|
||||
|
||||
@then("it should be recognized as a ConfigValidatorExtension instance")
|
||||
def step_isinstance_config_validator(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, ConfigValidatorExtension)
|
||||
|
||||
|
||||
# --- Safety Guardrail ---
|
||||
|
||||
@given("a concrete SafetyGuardrailExtension implementation")
|
||||
def step_concrete_safety(context: Context) -> None:
|
||||
context.ep_impl = _StubSafetyGuardrail()
|
||||
|
||||
|
||||
@then("it should be recognized as a SafetyGuardrailExtension instance")
|
||||
def step_isinstance_safety(context: Context) -> None:
|
||||
assert isinstance(context.ep_impl, SafetyGuardrailExtension)
|
||||
|
||||
|
||||
@then("calling its guardrail methods should succeed")
|
||||
def step_call_safety_methods(context: Context) -> None:
|
||||
impl = context.ep_impl
|
||||
assert impl.guardrail_name == "stub_guardrail"
|
||||
assert impl.check("some_action") is True
|
||||
assert impl.explain() == "Always allows"
|
||||
@@ -103,6 +103,9 @@ from cleveragents.infrastructure.database.repositories import (
|
||||
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
|
||||
from cleveragents.infrastructure.events.reactive import ReactiveEventBus
|
||||
from cleveragents.infrastructure.observability.metrics_emitter import MetricsEmitter
|
||||
from cleveragents.infrastructure.plugins.extension_catalog import (
|
||||
register_all_extension_points,
|
||||
)
|
||||
from cleveragents.infrastructure.plugins.manager import PluginManager
|
||||
from cleveragents.providers.registry import ProviderRegistry, get_provider_registry
|
||||
from cleveragents.shared.redaction import redact_value
|
||||
@@ -771,6 +774,7 @@ class Container(containers.DeclarativeContainer):
|
||||
ACMSPipeline,
|
||||
settings=settings,
|
||||
unit_of_work=unit_of_work,
|
||||
plugin_manager=plugin_manager,
|
||||
)
|
||||
|
||||
# ACMS ResourceFileWatcher — watches resource files for changes (#578).
|
||||
@@ -856,6 +860,19 @@ def get_container() -> Container:
|
||||
error_type=type(exc).__name__,
|
||||
error_message=redact_value(str(exc)),
|
||||
)
|
||||
# Register all 30 spec-defined extension points on the
|
||||
# PluginManager singleton so plugins can be discovered and
|
||||
# validated against typed Protocol interfaces (#939).
|
||||
try:
|
||||
pm = _container.plugin_manager()
|
||||
register_all_extension_points(pm)
|
||||
except Exception as exc:
|
||||
_logger.warning(
|
||||
"extension_points_deferred",
|
||||
reason="Failed to register extension points at startup.",
|
||||
error_type=type(exc).__name__,
|
||||
error_message=redact_value(str(exc)),
|
||||
)
|
||||
return _container
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import structlog
|
||||
if TYPE_CHECKING:
|
||||
from cleveragents.config.settings import Settings
|
||||
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
|
||||
from cleveragents.infrastructure.plugins.manager import PluginManager
|
||||
|
||||
from cleveragents.domain.models.acms.crp import (
|
||||
ContextRequest,
|
||||
@@ -626,6 +627,7 @@ class ACMSPipeline:
|
||||
*,
|
||||
settings: Settings | None = None,
|
||||
unit_of_work: UnitOfWork | None = None,
|
||||
plugin_manager: PluginManager | None = None,
|
||||
# Phase 1 — Strategy Orchestration
|
||||
strategy_selector: StrategySelector | None = None,
|
||||
budget_allocator: BudgetAllocator | None = None,
|
||||
@@ -672,6 +674,53 @@ class ACMSPipeline:
|
||||
self._preamble_generator = preamble_generator or DefaultPreambleGenerator()
|
||||
self._skeleton_compressor = skeleton_compressor or DefaultSkeletonCompressor()
|
||||
|
||||
# Plugin extension point integration (#939).
|
||||
# Store the plugin manager reference so discovered context.*
|
||||
# extension points can be queried at runtime.
|
||||
self._plugin_manager = plugin_manager
|
||||
self._discover_context_extensions()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Plugin extension point discovery
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _discover_context_extensions(self) -> None:
|
||||
"""Query context.* extension points from the PluginManager.
|
||||
|
||||
Logs discovered extension points for observability. Plugin
|
||||
implementations are not auto-activated here — they are wired
|
||||
on demand when a matching strategy or pipeline component is
|
||||
requested.
|
||||
"""
|
||||
if self._plugin_manager is None:
|
||||
return
|
||||
|
||||
context_eps = [
|
||||
ep
|
||||
for ep in self._plugin_manager.list_extension_points()
|
||||
if ep.name.startswith("context.")
|
||||
]
|
||||
if context_eps:
|
||||
self._logger.info(
|
||||
"context_extensions_discovered",
|
||||
count=len(context_eps),
|
||||
names=[ep.name for ep in context_eps],
|
||||
)
|
||||
|
||||
@property
|
||||
def context_extension_points(self) -> list[str]:
|
||||
"""Return names of registered context.* extension points.
|
||||
|
||||
Returns an empty list if no plugin manager is wired.
|
||||
"""
|
||||
if self._plugin_manager is None:
|
||||
return []
|
||||
return [
|
||||
ep.name
|
||||
for ep in self._plugin_manager.list_extension_points()
|
||||
if ep.name.startswith("context.")
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Budget enforcement helper
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -10,9 +10,11 @@ Key components:
|
||||
- :class:`PluginManager` — lifecycle management (discover/activate/deactivate).
|
||||
- :class:`PluginDescriptor` — immutable metadata for a discovered plugin.
|
||||
- :class:`PluginState` — lifecycle state enum.
|
||||
- :func:`register_all_extension_points` — registers all 30 spec-defined
|
||||
extension points.
|
||||
|
||||
Based on ``docs/specification.md`` Extension Points Summary and
|
||||
issue #585.
|
||||
issue #585, #939.
|
||||
|
||||
ISSUES CLOSED: #585
|
||||
"""
|
||||
@@ -25,6 +27,12 @@ from cleveragents.infrastructure.plugins.exceptions import (
|
||||
PluginNotFoundError,
|
||||
ProtocolMismatchError,
|
||||
)
|
||||
from cleveragents.infrastructure.plugins.extension_catalog import (
|
||||
TOTAL_EXTENSION_POINTS,
|
||||
get_extension_point_definitions,
|
||||
get_extension_points_by_category,
|
||||
register_all_extension_points,
|
||||
)
|
||||
from cleveragents.infrastructure.plugins.loader import PluginLoader
|
||||
from cleveragents.infrastructure.plugins.manager import PluginManager
|
||||
from cleveragents.infrastructure.plugins.types import (
|
||||
@@ -34,6 +42,7 @@ from cleveragents.infrastructure.plugins.types import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TOTAL_EXTENSION_POINTS",
|
||||
"ExtensionPoint",
|
||||
"PluginDescriptor",
|
||||
"PluginError",
|
||||
@@ -43,4 +52,7 @@ __all__ = [
|
||||
"PluginNotFoundError",
|
||||
"PluginState",
|
||||
"ProtocolMismatchError",
|
||||
"get_extension_point_definitions",
|
||||
"get_extension_points_by_category",
|
||||
"register_all_extension_points",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
"""Extension point catalog for all 30 spec-defined extension points.
|
||||
|
||||
Provides :func:`register_all_extension_points` which registers every
|
||||
extension point defined in ``docs/specification.md`` into the
|
||||
:class:`PluginManager`.
|
||||
|
||||
Each extension point has a name, description, Protocol type, and
|
||||
category derived from its dotted name prefix.
|
||||
|
||||
Based on ``docs/specification.md`` Extension Points Summary and
|
||||
issue #939.
|
||||
|
||||
ISSUES CLOSED: #939
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
from cleveragents.infrastructure.plugins.extension_protocols import (
|
||||
A2AExtensionMethodExtension,
|
||||
A2ATransportExtension,
|
||||
ConfigSourceExtension,
|
||||
ConfigValidatorExtension,
|
||||
ContextPipelineComponentExtension,
|
||||
ContextStorageBackendExtension,
|
||||
ContextStrategyExtension,
|
||||
EventFilterExtension,
|
||||
EventHandlerExtension,
|
||||
OutputFormatExtension,
|
||||
OutputMaterializerExtension,
|
||||
OutputRendererExtension,
|
||||
ResourceResolverExtension,
|
||||
ResourceTypeHandlerExtension,
|
||||
SafetyGuardrailExtension,
|
||||
SkillProviderExtension,
|
||||
SkillTemplateExtension,
|
||||
ToolMiddlewareExtension,
|
||||
ToolProviderExtension,
|
||||
ValidationRuleProviderExtension,
|
||||
ValidationRunnerExtension,
|
||||
)
|
||||
from cleveragents.infrastructure.plugins.types import ExtensionPoint
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structural protocol so the catalog is not tightly coupled to
|
||||
# PluginManager — any object with ``register_extension_point`` works.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ExtensionPointRegistrar(Protocol):
|
||||
"""Structural type for :func:`register_all_extension_points`.
|
||||
|
||||
Any object with a ``register_extension_point`` method satisfies
|
||||
this protocol (e.g. :class:`PluginManager`).
|
||||
"""
|
||||
|
||||
def register_extension_point(self, extension_point: ExtensionPoint) -> None:
|
||||
"""Register a single extension point."""
|
||||
...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Extension point definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _ExtensionPointDef:
|
||||
"""Internal descriptor for building ExtensionPoint instances."""
|
||||
|
||||
__slots__ = ("description", "name", "protocol_type")
|
||||
|
||||
def __init__(
|
||||
self, name: str, protocol_type: type[Any], description: str
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.protocol_type = protocol_type
|
||||
self.description = description
|
||||
|
||||
|
||||
def _category_from_name(name: str) -> str:
|
||||
"""Extract category prefix from a dotted extension point name.
|
||||
|
||||
Examples::
|
||||
|
||||
>>> _category_from_name("context.strategy")
|
||||
'context'
|
||||
>>> _category_from_name("context.pipeline_component.scorer")
|
||||
'context'
|
||||
"""
|
||||
return name.split(".")[0]
|
||||
|
||||
|
||||
# The authoritative list of all 30 spec-defined extension points.
|
||||
_EXTENSION_POINT_DEFS: tuple[_ExtensionPointDef, ...] = (
|
||||
# --- Context (12) ---
|
||||
_ExtensionPointDef(
|
||||
"context.strategy",
|
||||
ContextStrategyExtension,
|
||||
"Pluggable context assembly strategy",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.strategy_selector",
|
||||
ContextPipelineComponentExtension,
|
||||
"Selects which context strategies to invoke",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.budget_allocator",
|
||||
ContextPipelineComponentExtension,
|
||||
"Distributes token budget across strategies",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.strategy_executor",
|
||||
ContextPipelineComponentExtension,
|
||||
"Executes selected strategies in parallel",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.deduplicator",
|
||||
ContextPipelineComponentExtension,
|
||||
"Removes duplicate context fragments",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.depth_resolver",
|
||||
ContextPipelineComponentExtension,
|
||||
"Resolves detail depth levels for fragments",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.scorer",
|
||||
ContextPipelineComponentExtension,
|
||||
"Scores context fragments by relevance",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.packer",
|
||||
ContextPipelineComponentExtension,
|
||||
"Packs scored fragments within token budget",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.orderer",
|
||||
ContextPipelineComponentExtension,
|
||||
"Orders packed fragments for presentation",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.preamble_generator",
|
||||
ContextPipelineComponentExtension,
|
||||
"Generates preamble text for assembled context",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.pipeline_component.skeleton_compressor",
|
||||
ContextPipelineComponentExtension,
|
||||
"Compresses code skeletons to save tokens",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"context.storage_backend",
|
||||
ContextStorageBackendExtension,
|
||||
"Pluggable storage backend for context data",
|
||||
),
|
||||
# --- Output (3) ---
|
||||
_ExtensionPointDef(
|
||||
"output.renderer",
|
||||
OutputRendererExtension,
|
||||
"Renders assembled output to a specific format",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"output.materializer",
|
||||
OutputMaterializerExtension,
|
||||
"Writes rendered output to a destination",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"output.format",
|
||||
OutputFormatExtension,
|
||||
"Custom output serialization format",
|
||||
),
|
||||
# --- Validation (2) ---
|
||||
_ExtensionPointDef(
|
||||
"validation.runner",
|
||||
ValidationRunnerExtension,
|
||||
"Pluggable validation execution engine",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"validation.rule_provider",
|
||||
ValidationRuleProviderExtension,
|
||||
"Provides validation rules from external sources",
|
||||
),
|
||||
# --- Tools (2) ---
|
||||
_ExtensionPointDef(
|
||||
"tool.provider",
|
||||
ToolProviderExtension,
|
||||
"Registers external tools for agent use",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"tool.middleware",
|
||||
ToolMiddlewareExtension,
|
||||
"Intercepts tool invocations for cross-cutting concerns",
|
||||
),
|
||||
# --- Skills (2) ---
|
||||
_ExtensionPointDef(
|
||||
"skill.provider",
|
||||
SkillProviderExtension,
|
||||
"Provides skill definitions from external sources",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"skill.template",
|
||||
SkillTemplateExtension,
|
||||
"Custom skill template renderer",
|
||||
),
|
||||
# --- Resources (2) ---
|
||||
_ExtensionPointDef(
|
||||
"resource.resolver",
|
||||
ResourceResolverExtension,
|
||||
"Resolves resource URIs to content",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"resource.type_handler",
|
||||
ResourceTypeHandlerExtension,
|
||||
"Handles loading of specific resource types",
|
||||
),
|
||||
# --- A2A (2) ---
|
||||
_ExtensionPointDef(
|
||||
"a2a.transport",
|
||||
A2ATransportExtension,
|
||||
"Agent-to-agent communication transport layer",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"a2a.extension_method",
|
||||
A2AExtensionMethodExtension,
|
||||
"Custom A2A protocol extension method",
|
||||
),
|
||||
# --- Events (2) ---
|
||||
_ExtensionPointDef(
|
||||
"event.handler",
|
||||
EventHandlerExtension,
|
||||
"Handles domain events from the event bus",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"event.filter",
|
||||
EventFilterExtension,
|
||||
"Filters events before propagation",
|
||||
),
|
||||
# --- Config (2) ---
|
||||
_ExtensionPointDef(
|
||||
"config.source",
|
||||
ConfigSourceExtension,
|
||||
"External configuration source provider",
|
||||
),
|
||||
_ExtensionPointDef(
|
||||
"config.validator",
|
||||
ConfigValidatorExtension,
|
||||
"Validates configuration values against rules",
|
||||
),
|
||||
# --- Safety (1) ---
|
||||
_ExtensionPointDef(
|
||||
"safety.guardrail",
|
||||
SafetyGuardrailExtension,
|
||||
"Safety guardrail for agent action inspection",
|
||||
),
|
||||
)
|
||||
|
||||
# Public constant: number of spec-defined extension points.
|
||||
TOTAL_EXTENSION_POINTS: int = 30
|
||||
|
||||
|
||||
def get_extension_point_definitions() -> tuple[ExtensionPoint, ...]:
|
||||
"""Return all 30 spec-defined extension points as ExtensionPoint models.
|
||||
|
||||
Each entry includes the point's name, Protocol type, description,
|
||||
and a ``registry_key`` equal to its category prefix.
|
||||
"""
|
||||
return tuple(
|
||||
ExtensionPoint(
|
||||
name=defn.name,
|
||||
protocol_type=defn.protocol_type,
|
||||
description=defn.description,
|
||||
registry_key=_category_from_name(defn.name),
|
||||
)
|
||||
for defn in _EXTENSION_POINT_DEFS
|
||||
)
|
||||
|
||||
|
||||
def get_extension_points_by_category() -> dict[str, list[ExtensionPoint]]:
|
||||
"""Return extension points grouped by category.
|
||||
|
||||
Categories are derived from the first dotted segment of each
|
||||
extension point name (e.g. ``"context"``, ``"output"``).
|
||||
"""
|
||||
grouped: dict[str, list[ExtensionPoint]] = {}
|
||||
for ep in get_extension_point_definitions():
|
||||
category = _category_from_name(ep.name)
|
||||
grouped.setdefault(category, []).append(ep)
|
||||
return grouped
|
||||
|
||||
|
||||
def register_all_extension_points(manager: ExtensionPointRegistrar) -> int:
|
||||
"""Register all 30 spec-defined extension points on *manager*.
|
||||
|
||||
Args:
|
||||
manager: Object with a ``register_extension_point`` method
|
||||
(typically :class:`PluginManager`).
|
||||
|
||||
Returns:
|
||||
The number of extension points registered.
|
||||
"""
|
||||
count = 0
|
||||
for ep in get_extension_point_definitions():
|
||||
manager.register_extension_point(ep)
|
||||
count += 1
|
||||
return count
|
||||
@@ -0,0 +1,337 @@
|
||||
"""Typed Protocol interfaces for all spec-defined extension points.
|
||||
|
||||
Defines ``@runtime_checkable`` Protocol classes for each of the 30
|
||||
extension point categories. Each Protocol specifies the minimum
|
||||
interface that a plugin implementation must satisfy.
|
||||
|
||||
Based on ``docs/specification.md`` Extension Points Summary and
|
||||
issue #939.
|
||||
|
||||
ISSUES CLOSED: #939
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Context extension protocols (12 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ContextStrategyExtension(Protocol):
|
||||
"""Protocol for context strategy plugins."""
|
||||
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
|
||||
def can_handle(self, request: Mapping[str, Any]) -> float: ...
|
||||
|
||||
def assemble(self, fragments: Sequence[Any], budget: Any) -> Sequence[Any]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ContextPipelineComponentExtension(Protocol):
|
||||
"""Protocol for pipeline stage components.
|
||||
|
||||
Covers all 10 pipeline phases: strategy_selector,
|
||||
budget_allocator, strategy_executor, deduplicator,
|
||||
depth_resolver, scorer, packer, orderer,
|
||||
preamble_generator, skeleton_compressor.
|
||||
"""
|
||||
|
||||
@property
|
||||
def component_name(self) -> str: ...
|
||||
|
||||
def process(self, fragments: Sequence[Any], context: Mapping[str, Any]) -> Sequence[Any]: ... # noqa: E501
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ContextStorageBackendExtension(Protocol):
|
||||
"""Protocol for context storage backends (text, vector, graph)."""
|
||||
|
||||
@property
|
||||
def backend_type(self) -> str: ...
|
||||
|
||||
def store(self, key: str, data: Any) -> None: ...
|
||||
|
||||
def retrieve(self, key: str) -> Any: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output extension protocols (3 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class OutputRendererExtension(Protocol):
|
||||
"""Protocol for output renderer plugins."""
|
||||
|
||||
@property
|
||||
def format_name(self) -> str: ...
|
||||
|
||||
def render(self, data: Any) -> str: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class OutputMaterializerExtension(Protocol):
|
||||
"""Protocol for output materializer plugins."""
|
||||
|
||||
@property
|
||||
def target_type(self) -> str: ...
|
||||
|
||||
def materialize(self, content: str, destination: str) -> None: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class OutputFormatExtension(Protocol):
|
||||
"""Protocol for custom output format plugins."""
|
||||
|
||||
@property
|
||||
def format_id(self) -> str: ...
|
||||
|
||||
def serialize(self, data: Any) -> str: ...
|
||||
|
||||
def deserialize(self, raw: str) -> Any: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validation extension protocols (2 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ValidationRunnerExtension(Protocol):
|
||||
"""Protocol for validation runner plugins."""
|
||||
|
||||
@property
|
||||
def runner_name(self) -> str: ...
|
||||
|
||||
def validate(self, target: Any) -> Sequence[Any]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ValidationRuleProviderExtension(Protocol):
|
||||
"""Protocol for validation rule provider plugins."""
|
||||
|
||||
@property
|
||||
def provider_name(self) -> str: ...
|
||||
|
||||
def get_rules(self) -> Sequence[Any]: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool extension protocols (2 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ToolProviderExtension(Protocol):
|
||||
"""Protocol for tool provider plugins."""
|
||||
|
||||
@property
|
||||
def provider_name(self) -> str: ...
|
||||
|
||||
def list_tools(self) -> Sequence[str]: ...
|
||||
|
||||
def invoke(self, tool_name: str, arguments: Mapping[str, Any]) -> Any: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ToolMiddlewareExtension(Protocol):
|
||||
"""Protocol for tool middleware plugins."""
|
||||
|
||||
@property
|
||||
def middleware_name(self) -> str: ...
|
||||
|
||||
def before_invoke(self, tool_name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]: ... # noqa: E501
|
||||
|
||||
def after_invoke(self, tool_name: str, result: Any) -> Any: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Skill extension protocols (2 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SkillProviderExtension(Protocol):
|
||||
"""Protocol for skill provider plugins."""
|
||||
|
||||
@property
|
||||
def provider_name(self) -> str: ...
|
||||
|
||||
def list_skills(self) -> Sequence[str]: ...
|
||||
|
||||
def get_skill(self, skill_name: str) -> Any: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SkillTemplateExtension(Protocol):
|
||||
"""Protocol for skill template plugins."""
|
||||
|
||||
@property
|
||||
def template_name(self) -> str: ...
|
||||
|
||||
def render(self, context: Mapping[str, Any]) -> str: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resource extension protocols (2 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ResourceResolverExtension(Protocol):
|
||||
"""Protocol for resource resolver plugins."""
|
||||
|
||||
@property
|
||||
def resolver_name(self) -> str: ...
|
||||
|
||||
def resolve(self, uri: str) -> Any: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ResourceTypeHandlerExtension(Protocol):
|
||||
"""Protocol for resource type handler plugins."""
|
||||
|
||||
@property
|
||||
def handled_type(self) -> str: ...
|
||||
|
||||
def can_handle(self, resource_type: str) -> bool: ...
|
||||
|
||||
def load(self, uri: str) -> Any: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A2A extension protocols (2 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class A2ATransportExtension(Protocol):
|
||||
"""Protocol for A2A transport plugins."""
|
||||
|
||||
@property
|
||||
def transport_name(self) -> str: ...
|
||||
|
||||
def send(self, destination: str, payload: Any) -> None: ...
|
||||
|
||||
def receive(self) -> Any: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class A2AExtensionMethodExtension(Protocol):
|
||||
"""Protocol for A2A extension method plugins."""
|
||||
|
||||
@property
|
||||
def method_name(self) -> str: ...
|
||||
|
||||
def execute(self, params: Mapping[str, Any]) -> Any: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Event extension protocols (2 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class EventHandlerExtension(Protocol):
|
||||
"""Protocol for event handler plugins."""
|
||||
|
||||
@property
|
||||
def handler_name(self) -> str: ...
|
||||
|
||||
def handle(self, event: Any) -> None: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class EventFilterExtension(Protocol):
|
||||
"""Protocol for event filter plugins."""
|
||||
|
||||
@property
|
||||
def filter_name(self) -> str: ...
|
||||
|
||||
def should_propagate(self, event: Any) -> bool: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config extension protocols (2 extension points)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ConfigSourceExtension(Protocol):
|
||||
"""Protocol for configuration source plugins."""
|
||||
|
||||
@property
|
||||
def source_name(self) -> str: ...
|
||||
|
||||
def load(self) -> Mapping[str, Any]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ConfigValidatorExtension(Protocol):
|
||||
"""Protocol for configuration validator plugins."""
|
||||
|
||||
@property
|
||||
def validator_name(self) -> str: ...
|
||||
|
||||
def validate(self, config: Mapping[str, Any]) -> Sequence[str]: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Safety extension protocols (1 extension point)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SafetyGuardrailExtension(Protocol):
|
||||
"""Protocol for safety guardrail plugins."""
|
||||
|
||||
@property
|
||||
def guardrail_name(self) -> str: ...
|
||||
|
||||
def check(self, action: Any) -> bool: ...
|
||||
|
||||
def explain(self) -> str: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Re-export mapping for programmatic access
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ALL_EXTENSION_PROTOCOLS: dict[str, type[Any]] = {
|
||||
"context.strategy": ContextStrategyExtension,
|
||||
"context.pipeline_component.strategy_selector": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.budget_allocator": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.strategy_executor": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.deduplicator": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.depth_resolver": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.scorer": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.packer": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.orderer": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.preamble_generator": ContextPipelineComponentExtension,
|
||||
"context.pipeline_component.skeleton_compressor": ContextPipelineComponentExtension,
|
||||
"context.storage_backend": ContextStorageBackendExtension,
|
||||
"output.renderer": OutputRendererExtension,
|
||||
"output.materializer": OutputMaterializerExtension,
|
||||
"output.format": OutputFormatExtension,
|
||||
"validation.runner": ValidationRunnerExtension,
|
||||
"validation.rule_provider": ValidationRuleProviderExtension,
|
||||
"tool.provider": ToolProviderExtension,
|
||||
"tool.middleware": ToolMiddlewareExtension,
|
||||
"skill.provider": SkillProviderExtension,
|
||||
"skill.template": SkillTemplateExtension,
|
||||
"resource.resolver": ResourceResolverExtension,
|
||||
"resource.type_handler": ResourceTypeHandlerExtension,
|
||||
"a2a.transport": A2ATransportExtension,
|
||||
"a2a.extension_method": A2AExtensionMethodExtension,
|
||||
"event.handler": EventHandlerExtension,
|
||||
"event.filter": EventFilterExtension,
|
||||
"config.source": ConfigSourceExtension,
|
||||
"config.validator": ConfigValidatorExtension,
|
||||
"safety.guardrail": SafetyGuardrailExtension,
|
||||
}
|
||||
Reference in New Issue
Block a user