forked from cleveragents/cleveragents-core
feat(a2a): implement _cleveragents/ extension method routing
Add spec-aligned _cleveragents/ prefixed extension method routing to the A2A local facade per ADR-047. The facade now supports 42 total operations: 31 new extension methods across 6 families plus 11 legacy proprietary names retained for backward compatibility. Extension method families implemented: - _cleveragents/plan/* (13 methods): use, execute, apply, cancel, status, tree, explain, correct, diff, artifacts, prompt, rollback, list. Plan operations delegate to PlanLifecycleService when wired; new operations (cancel, tree, explain, correct, artifacts, prompt, rollback, list) have stub handlers returning safe defaults. - _cleveragents/registry/* (6 methods): tool/list, resource/list, actor/list, skill/list, action/list, project/list. Tool and resource list delegate to existing services; entity lists are stubs. - _cleveragents/context/* (4 methods): show (delegates to existing handler), inspect, simulate, set (stubs). - _cleveragents/health/* (2 methods): check, diagnostics/run. - _cleveragents/sync/* (3 methods): pull, push, status (stubs). - _cleveragents/namespace/* (3 methods): list, show, members (stubs). Legacy proprietary names (session.create, plan.create, etc.) continue to work via the same handler map, marked deprecated. Updated existing test assertions for the expanded operation count (11 -> 42). ISSUES CLOSED: #876
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
@mock_only
|
||||
Feature: A2A extension method routing via _cleveragents/ prefix
|
||||
As a CleverAgents developer
|
||||
I want the A2A local facade to support _cleveragents/ extension methods
|
||||
So that operations use spec-aligned method names per ADR-047
|
||||
|
||||
Scenario: Facade supports all _cleveragents/ extension operations
|
||||
Given a facade instance for extension method testing
|
||||
Then the facade should support _cleveragents/plan/ methods
|
||||
And the facade should support _cleveragents/registry/ methods
|
||||
And the facade should support _cleveragents/context/ methods
|
||||
And the facade should support _cleveragents/health/ methods
|
||||
And the facade should support _cleveragents/sync/ methods
|
||||
And the facade should support _cleveragents/namespace/ methods
|
||||
|
||||
Scenario: Plan use via _cleveragents/plan/use
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "_cleveragents/plan/use" with params {"action_name": "test"}
|
||||
Then the response should be ok
|
||||
And the response data should contain key "plan_id"
|
||||
|
||||
Scenario: Plan execute via _cleveragents/plan/execute
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "_cleveragents/plan/execute" with params {"plan_id": "test-id"}
|
||||
Then the response should be ok
|
||||
|
||||
Scenario: Plan cancel via _cleveragents/plan/cancel
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "_cleveragents/plan/cancel" with params {"plan_id": "test-id"}
|
||||
Then the response should be ok
|
||||
|
||||
Scenario: Plan list via _cleveragents/plan/list
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "_cleveragents/plan/list" with params {}
|
||||
Then the response should be ok
|
||||
And the response data should contain key "plans"
|
||||
|
||||
Scenario: Health check via _cleveragents/health/check
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "_cleveragents/health/check" with params {}
|
||||
Then the response should be ok
|
||||
And the response data should contain key "status"
|
||||
|
||||
Scenario: Sync stub returns not_implemented
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "_cleveragents/sync/pull" with params {}
|
||||
Then the response should be ok
|
||||
And the response data should have "stub" set to true
|
||||
|
||||
Scenario: Namespace stub returns not_implemented
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "_cleveragents/namespace/list" with params {}
|
||||
Then the response should be ok
|
||||
And the response data should have "stub" set to true
|
||||
|
||||
Scenario: Legacy operation names still work
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "plan.create" with params {"action_name": "test"}
|
||||
Then the response should be ok
|
||||
And the response data should contain key "plan_id"
|
||||
|
||||
Scenario: Registry tool list via extension method
|
||||
Given a facade instance for extension method testing
|
||||
When I dispatch "_cleveragents/registry/tool/list" with params {}
|
||||
Then the response should be ok
|
||||
And the response data should contain key "tools"
|
||||
@@ -140,7 +140,7 @@ Feature: Consolidated Misc
|
||||
And the operations list should contain "registry.list_tools"
|
||||
And the operations list should contain "context.get"
|
||||
And the operations list should contain "event.subscribe"
|
||||
And the operations list should have 11 items
|
||||
And the operations list should have 42 items
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# A2aHttpTransport — all stubs raise A2aNotAvailableError
|
||||
@@ -637,7 +637,7 @@ Feature: Consolidated Misc
|
||||
Then the m6 smoke operations should include "session.create"
|
||||
And the m6 smoke operations should include "plan.execute"
|
||||
And the m6 smoke operations should include "event.subscribe"
|
||||
And the m6 smoke operations count should be 11
|
||||
And the m6 smoke operations count should be 42
|
||||
|
||||
# --- A2A event queue ---
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Step definitions for a2a_extension_methods.feature.
|
||||
|
||||
Tests the _cleveragents/ extension method routing in the A2A local
|
||||
facade, verifying spec-aligned operation names are supported and
|
||||
produce valid responses.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.a2a.facade import A2aLocalFacade
|
||||
from cleveragents.a2a.models import A2aRequest
|
||||
|
||||
|
||||
@given("a facade instance for extension method testing")
|
||||
def step_create_extension_facade(context: Any) -> None:
|
||||
"""Create a facade with no services (all handlers return stubs)."""
|
||||
context.facade = A2aLocalFacade()
|
||||
|
||||
|
||||
@when('I dispatch "{operation}" with params {params_json}')
|
||||
def step_dispatch_operation(context: Any, operation: str, params_json: str) -> None:
|
||||
params = json.loads(params_json)
|
||||
request = A2aRequest(operation=operation, params=params)
|
||||
context.response = context.facade.dispatch(request)
|
||||
|
||||
|
||||
@then("the response should be ok")
|
||||
def step_response_ok(context: Any) -> None:
|
||||
assert context.response.status == "ok", (
|
||||
f"Expected 'ok', got '{context.response.status}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the response data should contain key "{key}"')
|
||||
def step_response_has_key(context: Any, key: str) -> None:
|
||||
assert key in context.response.data, (
|
||||
f"Expected key '{key}' in response data, got: {list(context.response.data.keys())}"
|
||||
)
|
||||
|
||||
|
||||
@then('the response data should have "{key}" set to true')
|
||||
def step_response_stub_true(context: Any, key: str) -> None:
|
||||
assert context.response.data.get(key) is True, (
|
||||
f"Expected data['{key}'] to be True, got: {context.response.data.get(key)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the facade should support _cleveragents/plan/ methods")
|
||||
def step_check_plan_methods(context: Any) -> None:
|
||||
ops = context.facade.list_operations()
|
||||
plan_ops = [o for o in ops if o.startswith("_cleveragents/plan/")]
|
||||
assert len(plan_ops) >= 13, (
|
||||
f"Expected >=13 plan extension methods, got {len(plan_ops)}: {plan_ops}"
|
||||
)
|
||||
|
||||
|
||||
@then("the facade should support _cleveragents/registry/ methods")
|
||||
def step_check_registry_methods(context: Any) -> None:
|
||||
ops = context.facade.list_operations()
|
||||
reg_ops = [o for o in ops if o.startswith("_cleveragents/registry/")]
|
||||
assert len(reg_ops) >= 6, (
|
||||
f"Expected >=6 registry extension methods, got {len(reg_ops)}: {reg_ops}"
|
||||
)
|
||||
|
||||
|
||||
@then("the facade should support _cleveragents/context/ methods")
|
||||
def step_check_context_methods(context: Any) -> None:
|
||||
ops = context.facade.list_operations()
|
||||
ctx_ops = [o for o in ops if o.startswith("_cleveragents/context/")]
|
||||
assert len(ctx_ops) >= 4, (
|
||||
f"Expected >=4 context extension methods, got {len(ctx_ops)}: {ctx_ops}"
|
||||
)
|
||||
|
||||
|
||||
@then("the facade should support _cleveragents/health/ methods")
|
||||
def step_check_health_methods(context: Any) -> None:
|
||||
ops = context.facade.list_operations()
|
||||
health_ops = [
|
||||
o
|
||||
for o in ops
|
||||
if o.startswith("_cleveragents/health/")
|
||||
or o.startswith("_cleveragents/diagnostics/")
|
||||
]
|
||||
assert len(health_ops) >= 2, (
|
||||
f"Expected >=2 health/diagnostics methods, got {len(health_ops)}: {health_ops}"
|
||||
)
|
||||
|
||||
|
||||
@then("the facade should support _cleveragents/sync/ methods")
|
||||
def step_check_sync_methods(context: Any) -> None:
|
||||
ops = context.facade.list_operations()
|
||||
sync_ops = [o for o in ops if o.startswith("_cleveragents/sync/")]
|
||||
assert len(sync_ops) >= 3, (
|
||||
f"Expected >=3 sync extension methods, got {len(sync_ops)}: {sync_ops}"
|
||||
)
|
||||
|
||||
|
||||
@then("the facade should support _cleveragents/namespace/ methods")
|
||||
def step_check_namespace_methods(context: Any) -> None:
|
||||
ops = context.facade.list_operations()
|
||||
ns_ops = [o for o in ops if o.startswith("_cleveragents/namespace/")]
|
||||
assert len(ns_ops) >= 3, (
|
||||
f"Expected >=3 namespace extension methods, got {len(ns_ops)}: {ns_ops}"
|
||||
)
|
||||
@@ -93,7 +93,7 @@ def list_operations() -> None:
|
||||
facade = A2aLocalFacade()
|
||||
ops = facade.list_operations()
|
||||
expected = {"session.create", "plan.create", "plan.execute", "context.get"}
|
||||
if expected.issubset(set(ops)) and len(ops) == 11:
|
||||
if expected.issubset(set(ops)) and len(ops) == 42:
|
||||
print("a2a-list-operations-ok")
|
||||
else:
|
||||
print(f"FAIL: ops={ops}", file=sys.stderr)
|
||||
|
||||
@@ -54,7 +54,49 @@ logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
|
||||
# Supported operations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SUPPORTED_OPERATIONS: list[str] = [
|
||||
# Spec-aligned _cleveragents/ extension method names (ADR-047).
|
||||
_EXTENSION_OPERATIONS: list[str] = [
|
||||
# Plan lifecycle
|
||||
"_cleveragents/plan/use",
|
||||
"_cleveragents/plan/execute",
|
||||
"_cleveragents/plan/apply",
|
||||
"_cleveragents/plan/cancel",
|
||||
"_cleveragents/plan/status",
|
||||
"_cleveragents/plan/tree",
|
||||
"_cleveragents/plan/explain",
|
||||
"_cleveragents/plan/correct",
|
||||
"_cleveragents/plan/diff",
|
||||
"_cleveragents/plan/artifacts",
|
||||
"_cleveragents/plan/prompt",
|
||||
"_cleveragents/plan/rollback",
|
||||
"_cleveragents/plan/list",
|
||||
# Registry
|
||||
"_cleveragents/registry/tool/list",
|
||||
"_cleveragents/registry/resource/list",
|
||||
"_cleveragents/registry/actor/list",
|
||||
"_cleveragents/registry/skill/list",
|
||||
"_cleveragents/registry/action/list",
|
||||
"_cleveragents/registry/project/list",
|
||||
# Context
|
||||
"_cleveragents/context/show",
|
||||
"_cleveragents/context/inspect",
|
||||
"_cleveragents/context/simulate",
|
||||
"_cleveragents/context/set",
|
||||
# Health / Diagnostics
|
||||
"_cleveragents/health/check",
|
||||
"_cleveragents/diagnostics/run",
|
||||
# Sync (stub)
|
||||
"_cleveragents/sync/pull",
|
||||
"_cleveragents/sync/push",
|
||||
"_cleveragents/sync/status",
|
||||
# Namespace (stub)
|
||||
"_cleveragents/namespace/list",
|
||||
"_cleveragents/namespace/show",
|
||||
"_cleveragents/namespace/members",
|
||||
]
|
||||
|
||||
# Legacy proprietary operation names (deprecated, kept for backward compat).
|
||||
_LEGACY_OPERATIONS: list[str] = [
|
||||
"session.create",
|
||||
"session.close",
|
||||
"plan.create",
|
||||
@@ -68,6 +110,8 @@ _SUPPORTED_OPERATIONS: list[str] = [
|
||||
"event.subscribe",
|
||||
]
|
||||
|
||||
_SUPPORTED_OPERATIONS: list[str] = _EXTENSION_OPERATIONS + _LEGACY_OPERATIONS
|
||||
|
||||
|
||||
class A2aLocalFacade:
|
||||
"""Local-mode facade that dispatches A2A operations to services.
|
||||
@@ -205,6 +249,47 @@ class A2aLocalFacade:
|
||||
"""
|
||||
if self._handler_map is None:
|
||||
self._handler_map = {
|
||||
# --- _cleveragents/ extension methods (spec-aligned) ---
|
||||
# Plan lifecycle
|
||||
"_cleveragents/plan/use": self._handle_plan_create,
|
||||
"_cleveragents/plan/execute": self._handle_plan_execute,
|
||||
"_cleveragents/plan/apply": self._handle_plan_apply,
|
||||
"_cleveragents/plan/cancel": self._handle_plan_cancel,
|
||||
"_cleveragents/plan/status": self._handle_plan_status,
|
||||
"_cleveragents/plan/tree": self._handle_plan_tree,
|
||||
"_cleveragents/plan/explain": self._handle_plan_explain,
|
||||
"_cleveragents/plan/correct": self._handle_plan_correct,
|
||||
"_cleveragents/plan/diff": self._handle_plan_diff,
|
||||
"_cleveragents/plan/artifacts": self._handle_plan_artifacts,
|
||||
"_cleveragents/plan/prompt": self._handle_plan_prompt,
|
||||
"_cleveragents/plan/rollback": self._handle_plan_rollback,
|
||||
"_cleveragents/plan/list": self._handle_plan_list,
|
||||
# Registry
|
||||
"_cleveragents/registry/tool/list": self._handle_registry_list_tools,
|
||||
"_cleveragents/registry/resource/list": (
|
||||
self._handle_registry_list_resources
|
||||
),
|
||||
"_cleveragents/registry/actor/list": self._handle_registry_list_stub,
|
||||
"_cleveragents/registry/skill/list": self._handle_registry_list_stub,
|
||||
"_cleveragents/registry/action/list": self._handle_registry_list_stub,
|
||||
"_cleveragents/registry/project/list": self._handle_registry_list_stub,
|
||||
# Context
|
||||
"_cleveragents/context/show": self._handle_context_get,
|
||||
"_cleveragents/context/inspect": self._handle_context_stub,
|
||||
"_cleveragents/context/simulate": self._handle_context_stub,
|
||||
"_cleveragents/context/set": self._handle_context_stub,
|
||||
# Health / Diagnostics
|
||||
"_cleveragents/health/check": self._handle_health_check,
|
||||
"_cleveragents/diagnostics/run": self._handle_diagnostics_run,
|
||||
# Sync (stub)
|
||||
"_cleveragents/sync/pull": self._handle_sync_stub,
|
||||
"_cleveragents/sync/push": self._handle_sync_stub,
|
||||
"_cleveragents/sync/status": self._handle_sync_stub,
|
||||
# Namespace (stub)
|
||||
"_cleveragents/namespace/list": self._handle_namespace_stub,
|
||||
"_cleveragents/namespace/show": self._handle_namespace_stub,
|
||||
"_cleveragents/namespace/members": self._handle_namespace_stub,
|
||||
# --- Legacy proprietary names (deprecated) ---
|
||||
"session.create": self._handle_session_create,
|
||||
"session.close": self._handle_session_close,
|
||||
"plan.create": self._handle_plan_create,
|
||||
@@ -404,6 +489,114 @@ class A2aLocalFacade:
|
||||
sub_id = queue.subscribe_local(_noop_callback)
|
||||
return {"subscription_id": sub_id, "status": "subscribed"}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Extension handlers — _cleveragents/plan/* (new operations)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _handle_plan_cancel(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
plan_id = params.get("plan_id", "")
|
||||
svc = self._plan_lifecycle_service
|
||||
if svc is None or not plan_id:
|
||||
return {"plan_id": plan_id, "status": "cancelled", "stub": True}
|
||||
svc.cancel_plan(plan_id)
|
||||
return {"plan_id": plan_id, "status": "cancelled"}
|
||||
|
||||
def _handle_plan_tree(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
plan_id = params.get("plan_id", "")
|
||||
svc = self._plan_lifecycle_service
|
||||
if svc is None or not plan_id:
|
||||
return {"plan_id": plan_id, "tree": [], "stub": True}
|
||||
plan = svc.get_plan(plan_id)
|
||||
return {
|
||||
"plan_id": plan_id,
|
||||
"decision_root_id": plan.decision_root_id if plan else None,
|
||||
"tree": [],
|
||||
}
|
||||
|
||||
def _handle_plan_explain(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
plan_id = params.get("plan_id", "")
|
||||
decision_id = params.get("decision_id", "")
|
||||
return {
|
||||
"plan_id": plan_id,
|
||||
"decision_id": decision_id,
|
||||
"explanation": "Not yet implemented",
|
||||
"stub": True,
|
||||
}
|
||||
|
||||
def _handle_plan_correct(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
plan_id = params.get("plan_id", "")
|
||||
return {"plan_id": plan_id, "status": "corrected", "stub": True}
|
||||
|
||||
def _handle_plan_artifacts(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
plan_id = params.get("plan_id", "")
|
||||
svc = self._plan_lifecycle_service
|
||||
if svc is None or not plan_id:
|
||||
return {"plan_id": plan_id, "artifacts": [], "stub": True}
|
||||
plan = svc.get_plan(plan_id)
|
||||
return {
|
||||
"plan_id": plan_id,
|
||||
"changeset_id": plan.changeset_id if plan else None,
|
||||
"sandbox_refs": list(plan.sandbox_refs) if plan else [],
|
||||
}
|
||||
|
||||
def _handle_plan_prompt(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
plan_id = params.get("plan_id", "")
|
||||
return {"plan_id": plan_id, "status": "guidance_injected", "stub": True}
|
||||
|
||||
def _handle_plan_rollback(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
plan_id = params.get("plan_id", "")
|
||||
return {"plan_id": plan_id, "status": "rolled_back", "stub": True}
|
||||
|
||||
def _handle_plan_list(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
svc = self._plan_lifecycle_service
|
||||
if svc is None:
|
||||
return {"plans": [], "stub": True}
|
||||
plans = svc.list_plans()
|
||||
return {
|
||||
"plans": [
|
||||
{"plan_id": p.identity.plan_id, "phase": p.phase.value} for p in plans
|
||||
],
|
||||
"total": len(plans),
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Extension handlers — _cleveragents/registry/* (stub for new entities)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _handle_registry_list_stub(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"items": [], "stub": True}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Extension handlers — _cleveragents/context/* (stubs)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _handle_context_stub(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"context": {}, "stub": True}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Extension handlers — _cleveragents/health/*
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _handle_health_check(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"status": "healthy", "services": {}}
|
||||
|
||||
def _handle_diagnostics_run(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"status": "ok", "diagnostics": {}, "stub": True}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Extension handlers — _cleveragents/sync/* (stubs)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _handle_sync_stub(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"status": "not_implemented", "stub": True}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Extension handlers — _cleveragents/namespace/* (stubs)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _handle_namespace_stub(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"status": "not_implemented", "stub": True}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"A2aLocalFacade",
|
||||
|
||||
Reference in New Issue
Block a user