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:
2026-03-18 05:49:08 +00:00
committed by Forgejo
parent 399939a6db
commit ad98d41d61
5 changed files with 372 additions and 4 deletions
+66
View File
@@ -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"
+2 -2
View File
@@ -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}"
)
+1 -1
View File
@@ -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)
+194 -1
View File
@@ -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",