refactor(a2a): rename ACP module and symbols to A2A standard #705

Merged
freemo merged 1 commits from refactor/m7-acp-to-a2a-rename into master 2026-03-12 14:45:06 +00:00
42 changed files with 854 additions and 854 deletions
@@ -1,9 +1,9 @@
"""ASV benchmarks for ACP facade dispatch, version negotiation, and event queue.
"""ASV benchmarks for A2A facade dispatch, version negotiation, and event queue.
Measures the performance of:
- AcpLocalFacade dispatch overhead per operation
- AcpVersionNegotiator negotiation throughput
- AcpEventQueue publish and get_events throughput
- A2aLocalFacade dispatch overhead per operation
- A2aVersionNegotiator negotiation throughput
- A2aEventQueue publish and get_events throughput
"""
from __future__ import annotations
@@ -23,10 +23,10 @@ import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.acp.events import AcpEventQueue # noqa: E402
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
from cleveragents.acp.models import AcpEvent, AcpRequest # noqa: E402
from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
from cleveragents.a2a.events import A2aEventQueue # noqa: E402
from cleveragents.a2a.facade import A2aLocalFacade # noqa: E402
from cleveragents.a2a.models import A2aEvent, A2aRequest # noqa: E402
from cleveragents.a2a.versioning import A2aVersionNegotiator # noqa: E402
# ---------------------------------------------------------------------------
# Facade dispatch benchmarks
@@ -34,17 +34,17 @@ from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
class FacadeDispatchSuite:
"""Benchmark AcpLocalFacade.dispatch() overhead."""
"""Benchmark A2aLocalFacade.dispatch() overhead."""
timeout = 60
def setup(self) -> None:
self.facade = AcpLocalFacade()
self.session_req = AcpRequest(operation="session.create")
self.plan_req = AcpRequest(
self.facade = A2aLocalFacade()
self.session_req = A2aRequest(operation="session.create")
self.plan_req = A2aRequest(
operation="plan.execute", params={"plan_id": "BENCH001"}
)
self.context_req = AcpRequest(operation="context.get")
self.context_req = A2aRequest(operation="context.get")
def time_dispatch_session_create(self) -> None:
self.facade.dispatch(self.session_req)
@@ -57,7 +57,7 @@ class FacadeDispatchSuite:
def time_dispatch_all_operations(self) -> None:
for op in self.facade.list_operations():
req = AcpRequest(operation=op)
req = A2aRequest(operation=op)
self.facade.dispatch(req)
def time_list_operations(self) -> None:
@@ -70,12 +70,12 @@ class FacadeDispatchSuite:
class VersionNegotiationSuite:
"""Benchmark AcpVersionNegotiator throughput."""
"""Benchmark A2aVersionNegotiator throughput."""
timeout = 60
def setup(self) -> None:
self.negotiator = AcpVersionNegotiator()
self.negotiator = A2aVersionNegotiator()
def time_negotiate_supported(self) -> None:
self.negotiator.negotiate("1.0")
@@ -96,13 +96,13 @@ class VersionNegotiationSuite:
class EventQueueSuite:
"""Benchmark AcpEventQueue throughput."""
"""Benchmark A2aEventQueue throughput."""
timeout = 60
def setup(self) -> None:
self.queue = AcpEventQueue()
self.event = AcpEvent(event_type="bench.event")
self.queue = A2aEventQueue()
self.event = A2aEvent(event_type="bench.event")
def time_publish_single(self) -> None:
self.queue.publish(self.event)
@@ -118,7 +118,7 @@ class EventQueueSuite:
self.queue.subscribe_local(lambda e: None)
def time_publish_with_subscriber(self) -> None:
q = AcpEventQueue()
q = A2aEventQueue()
q.subscribe_local(lambda e: None)
for _ in range(100):
q.publish(self.event)
+22 -22
View File
@@ -1,10 +1,10 @@
"""ASV benchmarks for M6 autonomy acceptance suite runtime.
Measures the performance of:
- ACP local facade dispatch operations
- A2A local facade dispatch operations
- Automation guard evaluation
- AutomationProfileService resolution precedence
- ACP event queue publish/subscribe
- A2A event queue publish/subscribe
- Fixture loading overhead
"""
@@ -25,10 +25,10 @@ import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.acp.events import AcpEventQueue # noqa: E402
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
from cleveragents.acp.models import AcpEvent, AcpRequest # noqa: E402
from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
from cleveragents.a2a.events import A2aEventQueue # noqa: E402
from cleveragents.a2a.facade import A2aLocalFacade # noqa: E402
from cleveragents.a2a.models import A2aEvent, A2aRequest # noqa: E402
from cleveragents.a2a.versioning import A2aVersionNegotiator # noqa: E402
from cleveragents.application.services.automation_profile_service import ( # noqa: E402
AutomationProfileService,
)
@@ -43,23 +43,23 @@ _FIXTURES_DIR = Path(__file__).resolve().parents[1] / "features" / "fixtures" /
class M6FacadeDispatchSuite:
"""Benchmark ACP local facade dispatch operations."""
"""Benchmark A2A local facade dispatch operations."""
def setup(self) -> None:
self._facade = AcpLocalFacade()
self._facade = A2aLocalFacade()
def time_session_create(self) -> None:
"""Benchmark session.create dispatch."""
self._facade.dispatch(AcpRequest(operation="session.create", params={}))
self._facade.dispatch(A2aRequest(operation="session.create", params={}))
def time_plan_create(self) -> None:
"""Benchmark plan.create dispatch."""
self._facade.dispatch(AcpRequest(operation="plan.create", params={}))
self._facade.dispatch(A2aRequest(operation="plan.create", params={}))
def time_plan_execute(self) -> None:
"""Benchmark plan.execute dispatch."""
self._facade.dispatch(
AcpRequest(
A2aRequest(
operation="plan.execute",
params={"plan_id": "01M6SM0KE00000000000000001"},
)
@@ -170,16 +170,16 @@ class M6ProfileResolutionSuite:
)
def time_version_negotiation(self) -> None:
"""Benchmark ACP version negotiation."""
negotiator = AcpVersionNegotiator()
"""Benchmark A2A version negotiation."""
negotiator = A2aVersionNegotiator()
negotiator.negotiate("1.0")
class M6EventQueueSuite:
"""Benchmark ACP event queue operations."""
"""Benchmark A2A event queue operations."""
def setup(self) -> None:
self._queue = AcpEventQueue()
self._queue = A2aEventQueue()
def teardown(self) -> None:
if not self._queue.is_closed:
@@ -187,13 +187,13 @@ class M6EventQueueSuite:
def time_publish_event(self) -> None:
"""Benchmark publishing a single event."""
self._queue.publish(AcpEvent(event_type="plan.progress", data={"step": 1}))
self._queue.publish(A2aEvent(event_type="plan.progress", data={"step": 1}))
def time_subscribe_and_publish(self) -> None:
"""Benchmark subscribe + publish cycle."""
q = AcpEventQueue()
q = A2aEventQueue()
sub_id = q.subscribe_local(lambda _e: None)
q.publish(AcpEvent(event_type="test.event", data={}))
q.publish(A2aEvent(event_type="test.event", data={}))
q.unsubscribe(sub_id)
q.close()
@@ -205,9 +205,9 @@ class M6EventQueueSuite:
class M6FixtureLoadSuite:
"""Benchmark loading M6 fixture files."""
def time_load_acp_facade_flows(self) -> None:
"""Benchmark loading acp_facade_flows.json."""
with open(_FIXTURES_DIR / "acp_facade_flows.json") as f:
def time_load_a2a_facade_flows(self) -> None:
"""Benchmark loading a2a_facade_flows.json."""
with open(_FIXTURES_DIR / "a2a_facade_flows.json") as f:
json.load(f)
def time_load_autonomy_guardrails(self) -> None:
@@ -223,7 +223,7 @@ class M6FixtureLoadSuite:
def time_load_all_fixtures(self) -> None:
"""Benchmark loading all M6 fixture files."""
for fname in (
"acp_facade_flows.json",
"a2a_facade_flows.json",
"autonomy_guardrails.json",
"automation_profiles.json",
):
+2 -2
View File
@@ -23,12 +23,12 @@ import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.acp.clients import ( # noqa: E402
from cleveragents.a2a.clients import ( # noqa: E402
StubAuthClient,
StubRemoteExecutionClient,
StubServerClient,
)
from cleveragents.acp.server_config import ServerConnectionConfig # noqa: E402
from cleveragents.a2a.server_config import ServerConnectionConfig # noqa: E402
from cleveragents.application.services.config_service import _REGISTRY # noqa: E402
# ---------------------------------------------------------------------------
@@ -1,8 +1,8 @@
@phase2 @acp @coverage-boost
Feature: ACP clients coverage boost
@phase2 @a2a @coverage-boost
Feature: A2A clients coverage boost
As a developer
I want complete coverage of Protocol default bodies and stub validation branches
So that all code paths in cleveragents.acp.clients are exercised
So that all code paths in cleveragents.a2a.clients are exercised
# -----------------------------------------------------------------------
# Protocol default method bodies (the `...` expressions)
@@ -1,5 +1,5 @@
Feature: ACP local facade coverage boost — uncovered validation paths
As a developer maintaining the ACP local facade
Feature: A2A local facade coverage boost — uncovered validation paths
As a developer maintaining the A2A local facade
I want all error-handling and validation paths exercised
So that edge-case regressions are caught early
@@ -7,8 +7,8 @@ Feature: ACP local facade coverage boost — uncovered validation paths
# Constructor validation (line 82)
# ---------------------------------------------------------------
Scenario: Constructing AcpLocalFacade with a non-dict raises TypeError
When I try to create an AcpLocalFacade with a non-dict services argument
Scenario: Constructing A2aLocalFacade with a non-dict raises TypeError
When I try to create an A2aLocalFacade with a non-dict services argument
Then a TypeError should be raised with message "services must be a dict or None"
# ---------------------------------------------------------------
@@ -1,5 +1,5 @@
Feature: ACP local facade wiring to live services
As a client of the ACP local facade
Feature: A2A local facade wiring to live services
As a client of the A2A local facade
I want operations to route to real application services
So that the facade delegates to live implementations
@@ -8,20 +8,20 @@ Feature: ACP local facade wiring to live services
# ---------------------------------------------------------------
Scenario: session.create delegates to SessionService
Given a wired AcpLocalFacade with a mock SessionService
Given a wired A2aLocalFacade with a mock SessionService
When I dispatch wired operation "session.create" with params {"actor_name": "local/test-actor"}
Then the wired response status should be "ok"
And wired response data key "session_id" equals "MOCK-SESSION-001"
And wired response data key "status" equals "created"
Scenario: session.close delegates to SessionService
Given a wired AcpLocalFacade with a mock SessionService
Given a wired A2aLocalFacade with a mock SessionService
When I dispatch wired operation "session.close" with params {"session_id": "MOCK-SESSION-001"}
Then the wired response status should be "ok"
And wired response data key "status" equals "closed"
Scenario: session.close without session_id returns error
Given a wired AcpLocalFacade with a mock SessionService
Given a wired A2aLocalFacade with a mock SessionService
When I dispatch wired operation "session.close" with params {}
Then the wired response status should be "error"
@@ -30,38 +30,38 @@ Feature: ACP local facade wiring to live services
# ---------------------------------------------------------------
Scenario: plan.create delegates to PlanLifecycleService
Given a wired AcpLocalFacade with a mock PlanLifecycleService
Given a wired A2aLocalFacade with a mock PlanLifecycleService
When I dispatch wired operation "plan.create" with params {"action_name": "local/test-action"}
Then the wired response status should be "ok"
And wired response data key "plan_id" equals "MOCK-PLAN-001"
And wired response data key "status" equals "created"
Scenario: plan.create without action_name returns error
Given a wired AcpLocalFacade with a mock PlanLifecycleService
Given a wired A2aLocalFacade with a mock PlanLifecycleService
When I dispatch wired operation "plan.create" with params {}
Then the wired response status should be "error"
Scenario: plan.execute delegates to PlanLifecycleService
Given a wired AcpLocalFacade with a mock PlanLifecycleService
Given a wired A2aLocalFacade with a mock PlanLifecycleService
When I dispatch wired operation "plan.execute" with params {"plan_id": "MOCK-PLAN-001"}
Then the wired response status should be "ok"
And wired response data key "plan_id" equals "MOCK-PLAN-001"
Scenario: plan.status delegates to PlanLifecycleService
Given a wired AcpLocalFacade with a mock PlanLifecycleService
Given a wired A2aLocalFacade with a mock PlanLifecycleService
When I dispatch wired operation "plan.status" with params {"plan_id": "MOCK-PLAN-001"}
Then the wired response status should be "ok"
And wired response data key "plan_id" equals "MOCK-PLAN-001"
And wired response data key "phase" equals "strategize"
Scenario: plan.diff delegates to PlanLifecycleService
Given a wired AcpLocalFacade with a mock PlanLifecycleService
Given a wired A2aLocalFacade with a mock PlanLifecycleService
When I dispatch wired operation "plan.diff" with params {"plan_id": "MOCK-PLAN-001"}
Then the wired response status should be "ok"
And wired response data key "plan_id" equals "MOCK-PLAN-001"
Scenario: plan.apply delegates to PlanLifecycleService
Given a wired AcpLocalFacade with a mock PlanLifecycleService
Given a wired A2aLocalFacade with a mock PlanLifecycleService
When I dispatch wired operation "plan.apply" with params {"plan_id": "MOCK-PLAN-001"}
Then the wired response status should be "ok"
And wired response data key "plan_id" equals "MOCK-PLAN-001"
@@ -71,13 +71,13 @@ Feature: ACP local facade wiring to live services
# ---------------------------------------------------------------
Scenario: registry.list_tools delegates to ToolRegistry
Given a wired AcpLocalFacade with a mock ToolRegistry
Given a wired A2aLocalFacade with a mock ToolRegistry
When I dispatch wired operation "registry.list_tools" with params {}
Then the wired response status should be "ok"
And wired response data should contain tools list with 2 items
Scenario: registry.list_resources delegates to ResourceRegistryService
Given a wired AcpLocalFacade with a mock ResourceRegistryService
Given a wired A2aLocalFacade with a mock ResourceRegistryService
When I dispatch wired operation "registry.list_resources" with params {}
Then the wired response status should be "ok"
And wired response data should contain resources list with 1 items
@@ -87,7 +87,7 @@ Feature: ACP local facade wiring to live services
# ---------------------------------------------------------------
Scenario: context.get returns stub pending ACMS pipeline
Given a wired AcpLocalFacade with no services
Given a wired A2aLocalFacade with no services
When I dispatch wired operation "context.get" with params {}
Then the wired response status should be "ok"
And wired response data key "stub" equals "True"
@@ -96,8 +96,8 @@ Feature: ACP local facade wiring to live services
# Event wiring
# ---------------------------------------------------------------
Scenario: event.subscribe delegates to AcpEventQueue
Given a wired AcpLocalFacade with a mock AcpEventQueue
Scenario: event.subscribe delegates to A2aEventQueue
Given a wired A2aLocalFacade with a mock A2aEventQueue
When I dispatch wired operation "event.subscribe" with params {}
Then the wired response status should be "ok"
And wired response data key "status" equals "subscribed"
@@ -108,25 +108,25 @@ Feature: ACP local facade wiring to live services
# ---------------------------------------------------------------
Scenario: NOT_FOUND error code for missing resources
Given a wired AcpLocalFacade with a raising SessionService for not-found
Given a wired A2aLocalFacade with a raising SessionService for not-found
When I dispatch wired operation "session.close" with params {"session_id": "nonexistent"}
Then the wired response status should be "error"
And wired response error code should be "NOT_FOUND"
Scenario: VALIDATION_ERROR code for validation failures
Given a wired AcpLocalFacade with a raising service for validation-error
Given a wired A2aLocalFacade with a raising service for validation-error
When I dispatch wired operation "plan.create" with params {"action_name": "bad"}
Then the wired response status should be "error"
And wired response error code should be "VALIDATION_ERROR"
Scenario: PLAN_ERROR code for plan failures
Given a wired AcpLocalFacade with a raising service for plan-error
Given a wired A2aLocalFacade with a raising service for plan-error
When I dispatch wired operation "plan.execute" with params {"plan_id": "P1"}
Then the wired response status should be "error"
And wired response error code should be "PLAN_ERROR"
Scenario: INVALID_STATE code for business rule violations
Given a wired AcpLocalFacade with a raising service for invalid-state
Given a wired A2aLocalFacade with a raising service for invalid-state
When I dispatch wired operation "plan.apply" with params {"plan_id": "P1"}
Then the wired response status should be "error"
And wired response error code should be "INVALID_STATE"
@@ -136,24 +136,24 @@ Feature: ACP local facade wiring to live services
# ---------------------------------------------------------------
Scenario: session.create stub when no service wired
Given a wired AcpLocalFacade with no services
Given a wired A2aLocalFacade with no services
When I dispatch wired operation "session.create" with params {}
Then the wired response status should be "ok"
And wired response data key "status" equals "created"
Scenario: plan.create stub when no service wired
Given a wired AcpLocalFacade with no services
Given a wired A2aLocalFacade with no services
When I dispatch wired operation "plan.create" with params {}
Then the wired response status should be "ok"
And wired response data key "status" equals "created"
Scenario: registry.list_tools stub when no service wired
Given a wired AcpLocalFacade with no services
Given a wired A2aLocalFacade with no services
When I dispatch wired operation "registry.list_tools" with params {}
Then the wired response status should be "ok"
Scenario: event.subscribe stub when no service wired
Given a wired AcpLocalFacade with no services
Given a wired A2aLocalFacade with no services
When I dispatch wired operation "event.subscribe" with params {}
Then the wired response status should be "ok"
And wired response data key "status" equals "subscribed"
+197 -197
View File
@@ -1,39 +1,39 @@
Feature: Consolidated Misc
Combined scenarios from: acp_facade, agents_base_uncovered_lines, bridge_coverage, coverage_improvements, m6_autonomy_acceptance, scale_test, container_coverage_r2, lsp_registry, memory_service_coverage, migration_runner_coverage, system_coverage_boost, yaml_engine_direct_coverage
Combined scenarios from: a2a_facade, agents_base_uncovered_lines, bridge_coverage, coverage_improvements, m6_autonomy_acceptance, scale_test, container_coverage_r2, lsp_registry, memory_service_coverage, migration_runner_coverage, system_coverage_boost, yaml_engine_direct_coverage
# ============================================================
# Originally from: acp_facade.feature
# Feature: ACP Local Facade and Server Stubs
# Originally from: a2a_facade.feature
# Feature: A2A Local Facade and Server Stubs
# ============================================================
Scenario: Create facade with no services
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
Then the facade should be created successfully
Scenario: Create facade with services dict
Given a new AcpLocalFacade with services {"session": "mock_session"}
Given a new A2aLocalFacade with services {"session": "mock_session"}
Then the facade should be created successfully
Scenario: Register a service on the facade
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I register a service named "planner" on the facade
Then the service should be registered successfully
Scenario: Register service with empty name raises error
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I try to register a service with an empty name
Then an ACP ValueError should be raised
Then an A2A ValueError should be raised
# -----------------------------------------------------------------------
# AcpLocalFacade — dispatch for each supported operation
# A2aLocalFacade — dispatch for each supported operation
# -----------------------------------------------------------------------
Scenario: Dispatch session.create returns session_id and status
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "session.create" with params {}
Then the response status should be "ok"
And response data includes key "session_id"
@@ -41,14 +41,14 @@ Feature: Consolidated Misc
Scenario: Dispatch session.close returns status closed
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "session.close" with params {}
Then the response status should be "ok"
And response data key "status" equals "closed"
Scenario: Dispatch plan.create returns plan_id and status
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "plan.create" with params {}
Then the response status should be "ok"
And response data includes key "plan_id"
@@ -56,7 +56,7 @@ Feature: Consolidated Misc
Scenario: Dispatch plan.execute returns plan_id and status queued
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "plan.execute" with params {"plan_id": "PLAN001"}
Then the response status should be "ok"
And response data key "plan_id" equals "PLAN001"
@@ -64,7 +64,7 @@ Feature: Consolidated Misc
Scenario: Dispatch plan.status returns plan_id and phase
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "plan.status" with params {"plan_id": "PLAN001"}
Then the response status should be "ok"
And response data key "plan_id" equals "PLAN001"
@@ -72,7 +72,7 @@ Feature: Consolidated Misc
Scenario: Dispatch plan.diff returns plan_id and empty changes
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "plan.diff" with params {"plan_id": "PLAN001"}
Then the response status should be "ok"
And response data key "plan_id" equals "PLAN001"
@@ -80,7 +80,7 @@ Feature: Consolidated Misc
Scenario: Dispatch plan.apply returns plan_id and status applied
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "plan.apply" with params {"plan_id": "PLAN001"}
Then the response status should be "ok"
And response data key "plan_id" equals "PLAN001"
@@ -88,51 +88,51 @@ Feature: Consolidated Misc
Scenario: Dispatch registry.list_tools returns empty tools
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "registry.list_tools" with params {}
Then the response status should be "ok"
And response data includes key "tools"
Scenario: Dispatch registry.list_resources returns empty resources
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "registry.list_resources" with params {}
Then the response status should be "ok"
And response data includes key "resources"
Scenario: Dispatch context.get returns empty context
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "context.get" with params {}
Then the response status should be "ok"
And response data includes key "context"
Scenario: Dispatch event.subscribe returns subscription_id and status
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I dispatch operation "event.subscribe" with params {}
Then the response status should be "ok"
And response data includes key "subscription_id"
And response data key "status" equals "subscribed"
# -----------------------------------------------------------------------
# AcpLocalFacade — unknown operation
# A2aLocalFacade — unknown operation
# -----------------------------------------------------------------------
Scenario: Dispatch unknown operation raises AcpOperationNotFoundError
Given a new AcpLocalFacade with no services
Scenario: Dispatch unknown operation raises A2aOperationNotFoundError
Given a new A2aLocalFacade with no services
When I dispatch an unknown operation "does.not.exist"
Then an AcpOperationNotFoundError should be raised
Then an A2aOperationNotFoundError should be raised
And the error operation attribute should be "does.not.exist"
# -----------------------------------------------------------------------
# AcpLocalFacade — list_operations
# A2aLocalFacade — list_operations
# -----------------------------------------------------------------------
Scenario: list_operations returns all supported operations
Given a new AcpLocalFacade with no services
Given a new A2aLocalFacade with no services
When I call list_operations on the facade
Then the operations list should contain "session.create"
And the operations list should contain "plan.create"
@@ -143,161 +143,161 @@ Feature: Consolidated Misc
And the operations list should have 11 items
# -----------------------------------------------------------------------
# AcpHttpTransport — all stubs raise AcpNotAvailableError
# A2aHttpTransport — all stubs raise A2aNotAvailableError
# -----------------------------------------------------------------------
Scenario: Transport send raises AcpNotAvailableError
Given a new AcpHttpTransport
Scenario: Transport send raises A2aNotAvailableError
Given a new A2aHttpTransport
When I try to send a request via the transport
Then an AcpNotAvailableError should be raised
Then an A2aNotAvailableError should be raised
Scenario: Transport connect raises AcpNotAvailableError
Given a new AcpHttpTransport
Scenario: Transport connect raises A2aNotAvailableError
Given a new A2aHttpTransport
When I try to connect via the transport to "http://localhost:8080"
Then an AcpNotAvailableError should be raised
Then an A2aNotAvailableError should be raised
Scenario: Transport disconnect raises AcpNotAvailableError
Given a new AcpHttpTransport
Scenario: Transport disconnect raises A2aNotAvailableError
Given a new A2aHttpTransport
When I try to disconnect the transport
Then an AcpNotAvailableError should be raised
Then an A2aNotAvailableError should be raised
Scenario: Transport is_connected returns False
Given a new AcpHttpTransport
Given a new A2aHttpTransport
Then the transport should not be connected
# -----------------------------------------------------------------------
# AcpEventQueue — local mode works
# A2aEventQueue — local mode works
# -----------------------------------------------------------------------
Scenario: Publish event to local queue
Given a new AcpEventQueue
Given a new A2aEventQueue
When I publish an event with type "plan.started"
Then the event queue should have 1 event
Scenario: Subscribe locally and receive event
Given a new AcpEventQueue
Given a new A2aEventQueue
And I subscribe locally with a callback
When I publish an event with type "plan.completed"
Then the callback should have been called with event type "plan.completed"
Scenario: Unsubscribe removes subscription
Given a new AcpEventQueue
Given a new A2aEventQueue
And I subscribe locally with a callback
When I unsubscribe using the subscription id
Then the unsubscribe should return True
Scenario: Unsubscribe non-existent returns False
Given a new AcpEventQueue
Given a new A2aEventQueue
When I unsubscribe using a non-existent subscription id
Then the unsubscribe should return False
Scenario: Get events respects limit
Given a new AcpEventQueue
Given a new A2aEventQueue
When I publish 5 events
And I get events with limit 3
Then I should receive 3 events
# -----------------------------------------------------------------------
# AcpEventQueue — remote stub raises
# A2aEventQueue — remote stub raises
# -----------------------------------------------------------------------
Scenario: Remote subscribe raises AcpNotAvailableError
Given a new AcpEventQueue
Scenario: Remote subscribe raises A2aNotAvailableError
Given a new A2aEventQueue
When I try to subscribe remotely to "http://remote:9090/events"
Then an AcpNotAvailableError should be raised
Then an A2aNotAvailableError should be raised
# -----------------------------------------------------------------------
# AcpVersionNegotiator
# A2aVersionNegotiator
# -----------------------------------------------------------------------
Scenario: Negotiate supported version succeeds
Given a new AcpVersionNegotiator
Given a new A2aVersionNegotiator
When I negotiate version "1.0"
Then the negotiated version should be "1.0"
Scenario: Negotiate unsupported version raises error
Given a new AcpVersionNegotiator
Given a new A2aVersionNegotiator
When I try to negotiate version "2.0"
Then an AcpVersionMismatchError should be raised
Then an A2aVersionMismatchError should be raised
And the error requested_version should be "2.0"
Scenario: is_supported returns True for valid version
Given a new AcpVersionNegotiator
Given a new A2aVersionNegotiator
Then version "1.0" should be supported
Scenario: is_supported returns False for invalid version
Given a new AcpVersionNegotiator
Given a new A2aVersionNegotiator
Then version "99.0" should not be supported
Scenario: get_current returns current version
Given a new AcpVersionNegotiator
Given a new A2aVersionNegotiator
Then the current version should be "1.0"
# -----------------------------------------------------------------------
# AcpRequest model validation
# A2aRequest model validation
# -----------------------------------------------------------------------
Scenario: AcpRequest with valid operation succeeds
When I create an AcpRequest with operation "session.create"
Scenario: A2aRequest with valid operation succeeds
When I create an A2aRequest with operation "session.create"
Then the request should have a non-empty request_id
And the request acp_version should be "1.0"
And the request a2a_version should be "1.0"
Scenario: AcpRequest with empty operation fails validation
When I try to create an AcpRequest with empty operation
Scenario: A2aRequest with empty operation fails validation
When I try to create an A2aRequest with empty operation
Then a validation error should be raised
# -----------------------------------------------------------------------
# AcpResponse model validation
# A2aResponse model validation
# -----------------------------------------------------------------------
Scenario: AcpResponse with valid status ok succeeds
When I create an AcpResponse with status "ok" and request_id "REQ001"
Scenario: A2aResponse with valid status ok succeeds
When I create an A2aResponse with status "ok" and request_id "REQ001"
Then the response should be valid
Scenario: AcpResponse with invalid status fails validation
When I try to create an AcpResponse with status "maybe"
Scenario: A2aResponse with invalid status fails validation
When I try to create an A2aResponse with status "maybe"
Then a validation error should be raised
# -----------------------------------------------------------------------
# AcpErrorDetail model validation
# A2aErrorDetail model validation
# -----------------------------------------------------------------------
Scenario: AcpErrorDetail with valid fields succeeds
When I create an AcpErrorDetail with code "NOT_FOUND" and message "gone"
Scenario: A2aErrorDetail with valid fields succeeds
When I create an A2aErrorDetail with code "NOT_FOUND" and message "gone"
Then the error detail should be valid
Scenario: AcpErrorDetail with empty code fails validation
When I try to create an AcpErrorDetail with empty code
Scenario: A2aErrorDetail with empty code fails validation
When I try to create an A2aErrorDetail with empty code
Then a validation error should be raised
# -----------------------------------------------------------------------
# AcpEvent model construction
# A2aEvent model construction
# -----------------------------------------------------------------------
Scenario: AcpEvent auto-generates event_id and timestamp
When I create an AcpEvent with type "plan.progress"
Scenario: A2aEvent auto-generates event_id and timestamp
When I create an A2aEvent with type "plan.progress"
Then the event should have a non-empty event_id
And the event should have a non-empty timestamp
@@ -306,24 +306,24 @@ Feature: Consolidated Misc
# -----------------------------------------------------------------------
Scenario: AcpError is a CleverAgentsError
Then AcpError should be a subclass of CleverAgentsError
Scenario: A2aError is a CleverAgentsError
Then A2aError should be a subclass of CleverAgentsError
Scenario: AcpNotAvailableError is an AcpError
Then AcpNotAvailableError should be a subclass of AcpError
Scenario: A2aNotAvailableError is an A2aError
Then A2aNotAvailableError should be a subclass of A2aError
Scenario: AcpVersionMismatchError is an AcpError
Then AcpVersionMismatchError should be a subclass of AcpError
Scenario: A2aVersionMismatchError is an A2aError
Then A2aVersionMismatchError should be a subclass of A2aError
Scenario: AcpOperationNotFoundError is an AcpError
Then AcpOperationNotFoundError should be a subclass of AcpError
Scenario: A2aOperationNotFoundError is an A2aError
Then A2aOperationNotFoundError should be a subclass of A2aError
Scenario: AcpNotAvailableError has default message
When I create an AcpNotAvailableError with default message
Scenario: A2aNotAvailableError has default message
When I create an A2aNotAvailableError with default message
Then the error message should contain "not available in local mode"
@@ -485,17 +485,17 @@ Feature: Consolidated Misc
# Feature: M6 autonomy acceptance smoke tests
# ============================================================
Scenario: M6 smoke load ACP facade flows fixture
Scenario: M6 smoke load A2A facade flows fixture
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke load the ACP facade flows fixture
And a m6 smoke A2A local facade
When I m6 smoke load the A2A facade flows fixture
Then the m6 smoke facade fixture should have a session lifecycle entry
And the m6 smoke facade fixture should have a plan lifecycle entry
Scenario: M6 smoke load autonomy guardrails fixture
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke load the autonomy guardrails fixture
Then the m6 smoke guardrails fixture should have a denylist entry
And the m6 smoke guardrails fixture should have an allowlist entry
@@ -504,237 +504,237 @@ Feature: Consolidated Misc
Scenario: M6 smoke load automation profiles fixture
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke load the automation profiles fixture
Then the m6 smoke profiles fixture should list all 8 built-in names
And the m6 smoke profiles fixture should have a custom profile entry
# --- ACP facade session operations ---
# --- A2A facade session operations ---
Scenario: M6 smoke ACP session create returns session id
Scenario: M6 smoke A2A session create returns session id
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "session.create" with params {}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data should contain key "session_id"
And the m6 smoke response data should contain key "status"
Scenario: M6 smoke ACP session close returns closed status
Scenario: M6 smoke A2A session close returns closed status
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "session.close" with params {}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data "status" should equal "closed"
# --- ACP facade plan operations ---
# --- A2A facade plan operations ---
Scenario: M6 smoke ACP plan create returns plan id
Scenario: M6 smoke A2A plan create returns plan id
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "plan.create" with params {}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data should contain key "plan_id"
Scenario: M6 smoke ACP plan execute queues plan
Scenario: M6 smoke A2A plan execute queues plan
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "plan.execute" with params {"plan_id": "01M6SM0KE00000000000000001"}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data "status" should equal "queued"
Scenario: M6 smoke ACP plan status returns phase
Scenario: M6 smoke A2A plan status returns phase
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "plan.status" with params {"plan_id": "01M6SM0KE00000000000000001"}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data should contain key "phase"
Scenario: M6 smoke ACP plan diff returns changes list
Scenario: M6 smoke A2A plan diff returns changes list
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "plan.diff" with params {"plan_id": "01M6SM0KE00000000000000001"}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data should contain key "changes"
Scenario: M6 smoke ACP plan apply returns applied status
Scenario: M6 smoke A2A plan apply returns applied status
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "plan.apply" with params {"plan_id": "01M6SM0KE00000000000000001"}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data "status" should equal "applied"
# --- ACP facade registry and context operations ---
# --- A2A facade registry and context operations ---
Scenario: M6 smoke ACP registry list tools returns empty list
Scenario: M6 smoke A2A registry list tools returns empty list
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "registry.list_tools" with params {}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data should contain key "tools"
Scenario: M6 smoke ACP registry list resources returns empty list
Scenario: M6 smoke A2A registry list resources returns empty list
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "registry.list_resources" with params {}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data should contain key "resources"
Scenario: M6 smoke ACP context get returns context dict
Scenario: M6 smoke A2A context get returns context dict
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "context.get" with params {}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data should contain key "context"
Scenario: M6 smoke ACP event subscribe returns subscription id
Scenario: M6 smoke A2A event subscribe returns subscription id
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch "event.subscribe" with params {}
Then the m6 smoke response status should be "ok"
And the m6 smoke response data should contain key "subscription_id"
# --- ACP facade error handling ---
# --- A2A facade error handling ---
Scenario: M6 smoke ACP unknown operation raises error
Scenario: M6 smoke A2A unknown operation raises error
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke dispatch unknown operation "nonexistent.op"
Then the m6 smoke facade should raise AcpOperationNotFoundError
Then the m6 smoke facade should raise A2aOperationNotFoundError
Scenario: M6 smoke ACP dispatch with invalid request type raises TypeError
Scenario: M6 smoke A2A dispatch with invalid request type raises TypeError
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke dispatch with a non-AcpRequest object
And a m6 smoke A2A local facade
When I m6 smoke dispatch with a non-A2aRequest object
Then the m6 smoke facade should raise TypeError
# --- ACP facade service registration ---
# --- A2A facade service registration ---
Scenario: M6 smoke ACP register service stores service
Scenario: M6 smoke A2A register service stores service
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke register service "plan_service" on the facade
Then the m6 smoke facade should have service "plan_service"
Scenario: M6 smoke ACP list operations returns all supported
Scenario: M6 smoke A2A list operations returns all supported
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke list facade operations
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
# --- ACP event queue ---
# --- A2A event queue ---
Scenario: M6 smoke ACP event queue publish and retrieve
Scenario: M6 smoke A2A event queue publish and retrieve
Given a m6 smoke test runner
And a m6 smoke ACP local facade
Given a m6 smoke ACP event queue
And a m6 smoke A2A local facade
Given a m6 smoke A2A event queue
When I m6 smoke publish an event with type "plan.progress"
Then the m6 smoke event queue should have 1 event
And the m6 smoke last event type should be "plan.progress"
Scenario: M6 smoke ACP event queue subscribe local callback
Scenario: M6 smoke A2A event queue subscribe local callback
Given a m6 smoke test runner
And a m6 smoke ACP local facade
Given a m6 smoke ACP event queue
And a m6 smoke A2A local facade
Given a m6 smoke A2A event queue
When I m6 smoke subscribe a local callback
And I m6 smoke publish an event with type "plan.complete"
Then the m6 smoke callback should have been called once
Scenario: M6 smoke ACP event queue unsubscribe
Scenario: M6 smoke A2A event queue unsubscribe
Given a m6 smoke test runner
And a m6 smoke ACP local facade
Given a m6 smoke ACP event queue
And a m6 smoke A2A local facade
Given a m6 smoke A2A event queue
When I m6 smoke subscribe a local callback
And I m6 smoke unsubscribe the callback
And I m6 smoke publish an event with type "plan.complete"
Then the m6 smoke callback should not have been called
Scenario: M6 smoke ACP event queue close prevents publish
Scenario: M6 smoke A2A event queue close prevents publish
Given a m6 smoke test runner
And a m6 smoke ACP local facade
Given a m6 smoke ACP event queue
And a m6 smoke A2A local facade
Given a m6 smoke A2A event queue
When I m6 smoke close the event queue
Then the m6 smoke publishing should raise RuntimeError
Scenario: M6 smoke ACP event queue remote subscribe raises error
Scenario: M6 smoke A2A event queue remote subscribe raises error
Given a m6 smoke test runner
And a m6 smoke ACP local facade
Given a m6 smoke ACP event queue
And a m6 smoke A2A local facade
Given a m6 smoke A2A event queue
When I m6 smoke attempt remote subscribe to "https://example.com/events"
Then the m6 smoke facade should raise AcpNotAvailableError
Then the m6 smoke facade should raise A2aNotAvailableError
# --- ACP HTTP transport stub ---
# --- A2A HTTP transport stub ---
Scenario: M6 smoke ACP transport send raises not available
Scenario: M6 smoke A2A transport send raises not available
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke attempt transport send
Then the m6 smoke facade should raise AcpNotAvailableError
Then the m6 smoke facade should raise A2aNotAvailableError
Scenario: M6 smoke ACP transport connect raises not available
Scenario: M6 smoke A2A transport connect raises not available
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke attempt transport connect to "https://example.com/acp"
Then the m6 smoke facade should raise AcpNotAvailableError
And a m6 smoke A2A local facade
When I m6 smoke attempt transport connect to "https://example.com/a2a"
Then the m6 smoke facade should raise A2aNotAvailableError
Scenario: M6 smoke ACP transport disconnect raises not available
Scenario: M6 smoke A2A transport disconnect raises not available
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke attempt transport disconnect
Then the m6 smoke facade should raise AcpNotAvailableError
Then the m6 smoke facade should raise A2aNotAvailableError
Scenario: M6 smoke ACP transport is_connected returns false
Scenario: M6 smoke A2A transport is_connected returns false
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke check transport is_connected
Then the m6 smoke transport should not be connected
# --- ACP version negotiation ---
# --- A2A version negotiation ---
Scenario: M6 smoke ACP version negotiation accepts 1.0
Scenario: M6 smoke A2A version negotiation accepts 1.0
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke negotiate ACP version "1.0"
And a m6 smoke A2A local facade
When I m6 smoke negotiate A2A version "1.0"
Then the m6 smoke negotiated version should be "1.0"
Scenario: M6 smoke ACP version negotiation rejects unsupported
Scenario: M6 smoke A2A version negotiation rejects unsupported
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke negotiate ACP version "2.0"
Then the m6 smoke facade should raise AcpVersionMismatchError
And a m6 smoke A2A local facade
When I m6 smoke negotiate A2A version "2.0"
Then the m6 smoke facade should raise A2aVersionMismatchError
Scenario: M6 smoke ACP version is_supported returns correct result
Scenario: M6 smoke A2A version is_supported returns correct result
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke check if version "1.0" is supported
Then the m6 smoke version support should be true
When I m6 smoke check if version "99.0" is supported
@@ -745,7 +745,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke all 8 built-in profiles exist
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke list all built-in profiles
Then the m6 smoke profile count should be 8
And the m6 smoke profiles should include "manual"
@@ -754,7 +754,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke manual profile has all thresholds at 1.0
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke load built-in profile "manual"
Then the m6 smoke profile auto_strategize should be 1.0
And the m6 smoke profile auto_execute should be 1.0
@@ -764,7 +764,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke full-auto profile has no gates
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke load built-in profile "full-auto"
Then the m6 smoke profile auto_strategize should be 0.0
And the m6 smoke profile auto_execute should be 0.0
@@ -777,7 +777,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke create custom namespaced profile
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke create a profile named "acme/strict" with auto_apply 1.0
Then the m6 smoke created profile name should be "acme/strict"
And the m6 smoke created profile auto_apply should be 1.0
@@ -785,21 +785,21 @@ Feature: Consolidated Misc
Scenario: M6 smoke profile name validation rejects invalid
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke create a profile with invalid name "has spaces"
Then the m6 smoke creation should raise ValueError
Scenario: M6 smoke profile threshold validation rejects out of range
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
When I m6 smoke create a profile with auto_strategize 1.5
Then the m6 smoke creation should raise ValueError
Scenario: M6 smoke profile from_yaml loads correctly
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke temporary profile YAML file
When I m6 smoke load profile from the temp YAML
Then the m6 smoke loaded profile name should be "test-yaml-profile"
@@ -809,7 +809,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke guard denylist blocks denied tool
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke profile with denylist guard for "rm_rf"
When I m6 smoke check guard for tool "rm_rf"
Then the m6 smoke guard result should not be allowed
@@ -818,7 +818,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke guard denylist allows non-denied tool
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke profile with denylist guard for "rm_rf"
When I m6 smoke check guard for tool "read_file"
Then the m6 smoke guard result should be allowed
@@ -826,7 +826,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke guard allowlist blocks unlisted tool
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke profile with allowlist guard for "read_file" and "search"
When I m6 smoke check guard for tool "write_file"
Then the m6 smoke guard result should not be allowed
@@ -835,7 +835,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke guard max tool calls blocks at limit
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke profile with max 5 tool calls per step
When I m6 smoke check guard for tool "llm_call" with 5 calls so far
Then the m6 smoke guard result should not be allowed
@@ -844,7 +844,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke guard cost budget blocks at cap
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke profile with max cost 10.0
When I m6 smoke check guard for tool "llm_call" with cost 10.0
Then the m6 smoke guard result should not be allowed
@@ -853,7 +853,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke guard write approval blocks write operations
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke profile with write approval required
When I m6 smoke check guard for tool "write_file" as a write operation
Then the m6 smoke guard result should not be allowed
@@ -862,7 +862,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke guard apply approval blocks apply phase
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke profile with apply approval required
When I m6 smoke check guard for tool "__apply__"
Then the m6 smoke guard result should not be allowed
@@ -871,7 +871,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke guard with no guards allows everything
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke profile with no guards
When I m6 smoke check guard for tool "anything"
Then the m6 smoke guard result should be allowed
@@ -881,7 +881,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke profile resolution plan takes precedence
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke automation profile service
When I m6 smoke resolve profile with plan "ci" action "auto" project "manual"
Then the m6 smoke resolved profile name should be "ci"
@@ -889,7 +889,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke profile resolution action takes precedence over project
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke automation profile service
When I m6 smoke resolve profile with plan null action "auto" project "manual"
Then the m6 smoke resolved profile name should be "auto"
@@ -897,7 +897,7 @@ Feature: Consolidated Misc
Scenario: M6 smoke profile resolution falls back to global default
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke automation profile service
When I m6 smoke resolve profile with plan null action null project null
Then the m6 smoke resolved profile name should be "manual"
@@ -907,39 +907,39 @@ Feature: Consolidated Misc
Scenario: M6 smoke service evaluate guard delegates to profile
Given a m6 smoke test runner
And a m6 smoke ACP local facade
And a m6 smoke A2A local facade
Given a m6 smoke automation profile service
When I m6 smoke evaluate guard for profile "manual" and tool "read_file"
Then the m6 smoke guard result should be allowed
# --- ACP model validation ---
# --- A2A model validation ---
Scenario: M6 smoke AcpRequest validates non-empty operation
Scenario: M6 smoke A2aRequest validates non-empty operation
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke create AcpRequest with empty operation
And a m6 smoke A2A local facade
When I m6 smoke create A2aRequest with empty operation
Then the m6 smoke creation should raise ValueError
Scenario: M6 smoke AcpResponse validates status values
Scenario: M6 smoke A2aResponse validates status values
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke create AcpResponse with invalid status "maybe"
And a m6 smoke A2A local facade
When I m6 smoke create A2aResponse with invalid status "maybe"
Then the m6 smoke creation should raise ValueError
Scenario: M6 smoke AcpEvent validates non-empty event_type
Scenario: M6 smoke A2aEvent validates non-empty event_type
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke create AcpEvent with empty event_type
And a m6 smoke A2A local facade
When I m6 smoke create A2aEvent with empty event_type
Then the m6 smoke creation should raise ValueError
Scenario: M6 smoke AcpErrorDetail validates non-empty fields
Scenario: M6 smoke A2aErrorDetail validates non-empty fields
Given a m6 smoke test runner
And a m6 smoke ACP local facade
When I m6 smoke create AcpErrorDetail with empty code
And a m6 smoke A2A local facade
When I m6 smoke create A2aErrorDetail with empty code
Then the m6 smoke creation should raise ValueError
@@ -1,5 +1,5 @@
{
"description": "ACP local-mode facade operation flows for M6 autonomy acceptance tests",
"description": "A2A local-mode facade operation flows for M6 autonomy acceptance tests",
"fixtures": [
{
"name": "session_lifecycle",
@@ -35,7 +35,7 @@
{
"name": "unknown_operation",
"operations": [
{"operation": "nonexistent.op", "params": {}, "expected_error": "AcpOperationNotFoundError"}
{"operation": "nonexistent.op", "params": {}, "expected_error": "A2aOperationNotFoundError"}
]
}
]
+7 -7
View File
@@ -145,20 +145,20 @@ Feature: LSP server stub JSON-RPC protocol
And the server exit code should be 1
# ---------------------------------------------------------------------------
# ACP facade wiring
# A2A facade wiring
# ---------------------------------------------------------------------------
@lsp_acp
Scenario: LSP server stores provided ACP facade
Given an LSP server with ACP facade
@lsp_a2a
Scenario: LSP server stores provided A2A facade
Given an LSP server with A2A facade
And an initialize request with id 1
When the LSP server processes all messages
Then the response for id 1 should have result
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
And the server should hold the provided ACP facade
And the server should hold the provided A2A facade
@lsp_acp
Scenario: LSP server lazily creates ACP facade on property access
@lsp_a2a
Scenario: LSP server lazily creates A2A facade on property access
Given an initialize request with id 1
And an exit notification
When the LSP server processes all messages
+5 -5
View File
@@ -50,7 +50,7 @@ Feature: Async Resource Cleanup and Leak Prevention
And the state manager should be marked as closed
Scenario: Subscriptions are disposed on cleanup
Given I have an ACP event queue with 3 active subscriptions
Given I have an A2A event queue with 3 active subscriptions
When I close the event queue
Then all subscriptions should be removed
And the subscription count should be zero
@@ -106,14 +106,14 @@ Feature: Async Resource Cleanup and Leak Prevention
And I try to reset state after close
Then a RuntimeError should be raised mentioning "closed"
Scenario: Publish after close raises RuntimeError on AcpEventQueue
Given I have an ACP event queue with 1 active subscriptions
Scenario: Publish after close raises RuntimeError on A2aEventQueue
Given I have an A2A event queue with 1 active subscriptions
When I close the event queue
And I try to publish an event after close
Then a RuntimeError should be raised mentioning "closed"
Scenario: AcpEventQueue exposes is_closed property
Given I have an ACP event queue with 0 active subscriptions
Scenario: A2aEventQueue exposes is_closed property
Given I have an A2A event queue with 0 active subscriptions
Then the event queue is_closed should be False
When I close the event queue
Then the event queue is_closed should be True
+1 -1
View File
@@ -1,4 +1,4 @@
@phase2 @acp @server-cli-coverage
@phase2 @a2a @server-cli-coverage
Feature: Server CLI command coverage boost
As a developer
I want full test coverage for server.py CLI commands
+1 -1
View File
@@ -1,4 +1,4 @@
@phase2 @acp @server-stubs
@phase2 @a2a @server-stubs
Feature: Server Client Stubs and Connection Config
As a developer
I want protocol stubs for server communication
@@ -1,4 +1,4 @@
"""Step definitions for ACP clients coverage boost scenarios.
"""Step definitions for A2A clients coverage boost scenarios.
Covers:
- Protocol default method bodies (the ``...`` expression on lines 28, 36, 52, 63, 74, 90, 101)
@@ -10,7 +10,7 @@ from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.acp.clients import (
from cleveragents.a2a.clients import (
AuthClient,
RemoteExecutionClient,
ServerClient,
@@ -1,6 +1,6 @@
"""Step definitions for ACP facade coverage-boost scenarios.
"""Step definitions for A2A facade coverage-boost scenarios.
Targets uncovered lines in ``src/cleveragents/acp/facade.py``:
Targets uncovered lines in ``src/cleveragents/a2a/facade.py``:
| Line | Code path |
|------|--------------------------------------------------------|
@@ -20,8 +20,8 @@ from unittest.mock import MagicMock
from behave import given, then, use_step_matcher, when
from behave.runner import Context
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.acp.models import AcpRequest
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.a2a.models import A2aRequest
use_step_matcher("re")
@@ -61,7 +61,7 @@ def _build_mock_plan_lifecycle_service() -> MagicMock:
@given(r"a coverage-boost facade with a mock PlanLifecycleService")
def step_cb_facade_plan(context: Context) -> None:
context.cb_facade = AcpLocalFacade(
context.cb_facade = A2aLocalFacade(
services={"plan_lifecycle_service": _build_mock_plan_lifecycle_service()}
)
@@ -71,11 +71,11 @@ def step_cb_facade_plan(context: Context) -> None:
# ---------------------------------------------------------------------------
@when(r"I try to create an AcpLocalFacade with a non-dict services argument")
@when(r"I try to create an A2aLocalFacade with a non-dict services argument")
def step_cb_create_facade_non_dict(context: Context) -> None:
context.cb_caught_error = None
try:
AcpLocalFacade(services=["not", "a", "dict"]) # type: ignore[arg-type]
A2aLocalFacade(services=["not", "a", "dict"]) # type: ignore[arg-type]
except TypeError as exc:
context.cb_caught_error = exc
@@ -86,7 +86,7 @@ def step_cb_create_facade_non_dict(context: Context) -> None:
)
def step_cb_dispatch(context: Context, operation: str, params_json: str) -> None:
params: dict[str, Any] = json.loads(params_json)
request = AcpRequest(operation=operation, params=params)
request = A2aRequest(operation=operation, params=params)
context.cb_response = context.cb_facade.dispatch(request)
@@ -1,4 +1,4 @@
"""Step definitions for ACP facade and stubs Behave scenarios."""
"""Step definitions for A2A facade and stubs Behave scenarios."""
from __future__ import annotations
@@ -9,39 +9,39 @@ from behave import given, then, use_step_matcher, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.acp.errors import (
AcpError,
AcpNotAvailableError,
AcpOperationNotFoundError,
AcpVersionMismatchError,
from cleveragents.a2a.errors import (
A2aError,
A2aNotAvailableError,
A2aOperationNotFoundError,
A2aVersionMismatchError,
)
from cleveragents.acp.events import AcpEventQueue
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.acp.models import (
AcpErrorDetail,
AcpEvent,
AcpRequest,
AcpResponse,
from cleveragents.a2a.events import A2aEventQueue
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.a2a.models import (
A2aErrorDetail,
A2aEvent,
A2aRequest,
A2aResponse,
)
from cleveragents.acp.transport import AcpHttpTransport
from cleveragents.acp.versioning import AcpVersionNegotiator
from cleveragents.a2a.transport import A2aHttpTransport
from cleveragents.a2a.versioning import A2aVersionNegotiator
from cleveragents.core.exceptions import CleverAgentsError
use_step_matcher("re")
# AcpLocalFacade — creation and service registration
# A2aLocalFacade — creation and service registration
@given(r"a new AcpLocalFacade with no services")
@given(r"a new A2aLocalFacade with no services")
def step_facade_no_services(context: Context) -> None:
context.facade = AcpLocalFacade()
context.facade = A2aLocalFacade()
@given(r"a new AcpLocalFacade with services (?P<services_json>.+)")
@given(r"a new A2aLocalFacade with services (?P<services_json>.+)")
def step_facade_with_services(context: Context, services_json: str) -> None:
services = json.loads(services_json)
context.facade = AcpLocalFacade(services=services)
context.facade = A2aLocalFacade(services=services)
@then(r"the facade should be created successfully")
@@ -70,20 +70,20 @@ def step_register_empty_name(context: Context) -> None:
context.error = exc
@then(r"an ACP ValueError should be raised")
@then(r"an A2A ValueError should be raised")
def step_value_error_raised(context: Context) -> None:
assert isinstance(context.caught_error, ValueError), (
f"Expected ValueError, got {type(context.caught_error)}"
)
# AcpLocalFacade — dispatch operations
# A2aLocalFacade — dispatch operations
@when(r'I dispatch operation "(?P<operation>[^"]+)" with params (?P<params_json>.+)')
def step_dispatch_operation(context: Context, operation: str, params_json: str) -> None:
params: dict[str, Any] = json.loads(params_json)
request = AcpRequest(operation=operation, params=params)
request = A2aRequest(operation=operation, params=params)
context.response = context.facade.dispatch(request)
@@ -108,7 +108,7 @@ def step_response_data_key_value(context: Context, key: str, value: str) -> None
assert str(actual) == value, f"Expected '{value}', got '{actual}'"
# AcpLocalFacade — unknown operation
# A2aLocalFacade — unknown operation
@when(r'I dispatch an unknown operation "(?P<operation>[^"]+)"')
@@ -116,16 +116,16 @@ def step_dispatch_unknown(context: Context, operation: str) -> None:
context.caught_error = None
context.error = None
try:
request = AcpRequest(operation=operation)
request = A2aRequest(operation=operation)
context.facade.dispatch(request)
except AcpOperationNotFoundError as exc:
except A2aOperationNotFoundError as exc:
context.caught_error = exc
context.error = exc
@then(r"an AcpOperationNotFoundError should be raised")
@then(r"an A2aOperationNotFoundError should be raised")
def step_op_not_found_raised(context: Context) -> None:
assert isinstance(context.caught_error, AcpOperationNotFoundError)
assert isinstance(context.caught_error, A2aOperationNotFoundError)
@then(r'the error operation attribute should be "(?P<operation>[^"]+)"')
@@ -133,7 +133,7 @@ def step_error_operation_attr(context: Context, operation: str) -> None:
assert context.caught_error.operation == operation
# AcpLocalFacade — list_operations
# A2aLocalFacade — list_operations
@when(r"I call list_operations on the facade")
@@ -154,12 +154,12 @@ def step_operations_count(context: Context, count: str) -> None:
)
# AcpHttpTransport
# A2aHttpTransport
@given(r"a new AcpHttpTransport")
@given(r"a new A2aHttpTransport")
def step_transport(context: Context) -> None:
context.transport = AcpHttpTransport()
context.transport = A2aHttpTransport()
@when(r"I try to send a request via the transport")
@@ -167,9 +167,9 @@ def step_transport_send(context: Context) -> None:
context.caught_error = None
context.error = None
try:
request = AcpRequest(operation="test.op")
request = A2aRequest(operation="test.op")
context.transport.send(request)
except AcpNotAvailableError as exc:
except A2aNotAvailableError as exc:
context.caught_error = exc
context.error = exc
@@ -180,7 +180,7 @@ def step_transport_connect(context: Context, url: str) -> None:
context.error = None
try:
context.transport.connect(url)
except AcpNotAvailableError as exc:
except A2aNotAvailableError as exc:
context.caught_error = exc
context.error = exc
@@ -191,15 +191,15 @@ def step_transport_disconnect(context: Context) -> None:
context.error = None
try:
context.transport.disconnect()
except AcpNotAvailableError as exc:
except A2aNotAvailableError as exc:
context.caught_error = exc
context.error = exc
@then(r"an AcpNotAvailableError should be raised")
@then(r"an A2aNotAvailableError should be raised")
def step_not_available_raised(context: Context) -> None:
assert isinstance(context.caught_error, AcpNotAvailableError), (
f"Expected AcpNotAvailableError, got {type(context.caught_error)}"
assert isinstance(context.caught_error, A2aNotAvailableError), (
f"Expected A2aNotAvailableError, got {type(context.caught_error)}"
)
@@ -208,18 +208,18 @@ def step_transport_not_connected(context: Context) -> None:
assert context.transport.is_connected() is False
# AcpEventQueue — local mode
# A2aEventQueue — local mode
@given(r"a new AcpEventQueue")
@given(r"a new A2aEventQueue")
def step_event_queue(context: Context) -> None:
context.queue = AcpEventQueue()
context.callback_events: list[AcpEvent] = []
context.queue = A2aEventQueue()
context.callback_events: list[A2aEvent] = []
@given(r"I subscribe locally with a callback")
def step_subscribe_local(context: Context) -> None:
def _cb(event: AcpEvent) -> None:
def _cb(event: A2aEvent) -> None:
context.callback_events.append(event)
context.subscription_id = context.queue.subscribe_local(_cb)
@@ -227,14 +227,14 @@ def step_subscribe_local(context: Context) -> None:
@when(r'I publish an event with type "(?P<event_type>[^"]+)"')
def step_publish_event(context: Context, event_type: str) -> None:
event = AcpEvent(event_type=event_type)
event = A2aEvent(event_type=event_type)
context.queue.publish(event)
@when(r"I publish (?P<count>\d+) events")
def step_publish_n_events(context: Context, count: str) -> None:
for i in range(int(count)):
event = AcpEvent(event_type=f"test.event.{i}")
event = A2aEvent(event_type=f"test.event.{i}")
context.queue.publish(event)
@@ -284,7 +284,7 @@ def step_received_count(context: Context, count: str) -> None:
)
# AcpEventQueue — remote stub
# A2aEventQueue — remote stub
@when(r'I try to subscribe remotely to "(?P<endpoint>[^"]+)"')
@@ -293,17 +293,17 @@ def step_remote_subscribe(context: Context, endpoint: str) -> None:
context.error = None
try:
context.queue.subscribe_remote(endpoint)
except AcpNotAvailableError as exc:
except A2aNotAvailableError as exc:
context.caught_error = exc
context.error = exc
# AcpVersionNegotiator
# A2aVersionNegotiator
@given(r"a new AcpVersionNegotiator")
@given(r"a new A2aVersionNegotiator")
def step_version_negotiator(context: Context) -> None:
context.negotiator = AcpVersionNegotiator()
context.negotiator = A2aVersionNegotiator()
@when(r'I negotiate version "(?P<version>[^"]+)"')
@@ -322,14 +322,14 @@ def step_negotiate_fail(context: Context, version: str) -> None:
context.error = None
try:
context.negotiator.negotiate(version)
except AcpVersionMismatchError as exc:
except A2aVersionMismatchError as exc:
context.caught_error = exc
context.error = exc
@then(r"an AcpVersionMismatchError should be raised")
@then(r"an A2aVersionMismatchError should be raised")
def step_version_mismatch_raised(context: Context) -> None:
assert isinstance(context.caught_error, AcpVersionMismatchError)
assert isinstance(context.caught_error, A2aVersionMismatchError)
@then(r'the error requested_version should be "(?P<version>[^"]+)"')
@@ -352,12 +352,12 @@ def step_current_version(context: Context, version: str) -> None:
assert context.negotiator.get_current() == version
# AcpRequest model validation
# A2aRequest model validation
@when(r'I create an AcpRequest with operation "(?P<operation>[^"]+)"')
@when(r'I create an A2aRequest with operation "(?P<operation>[^"]+)"')
def step_create_request(context: Context, operation: str) -> None:
context.request = AcpRequest(operation=operation)
context.request = A2aRequest(operation=operation)
@then(r"the request should have a non-empty request_id")
@@ -365,17 +365,17 @@ def step_request_has_id(context: Context) -> None:
assert context.request.request_id, "request_id should not be empty"
@then(r'the request acp_version should be "(?P<version>[^"]+)"')
@then(r'the request a2a_version should be "(?P<version>[^"]+)"')
def step_request_version(context: Context, version: str) -> None:
assert context.request.acp_version == version
assert context.request.a2a_version == version
@when(r"I try to create an AcpRequest with empty operation")
@when(r"I try to create an A2aRequest with empty operation")
def step_create_request_empty(context: Context) -> None:
context.caught_error = None
context.error = None
try:
AcpRequest(operation="")
A2aRequest(operation="")
except ValidationError as exc:
context.caught_error = exc
context.error = exc
@@ -386,14 +386,14 @@ def step_create_request_empty(context: Context) -> None:
# We reuse those shared step definitions.
# AcpResponse model validation
# A2aResponse model validation
@when(
r'I create an AcpResponse with status "(?P<status>[^"]+)" and request_id "(?P<rid>[^"]+)"'
r'I create an A2aResponse with status "(?P<status>[^"]+)" and request_id "(?P<rid>[^"]+)"'
)
def step_create_response(context: Context, status: str, rid: str) -> None:
context.response = AcpResponse(request_id=rid, status=status)
context.response = A2aResponse(request_id=rid, status=status)
@then(r"the response should be valid")
@@ -401,25 +401,25 @@ def step_response_valid(context: Context) -> None:
assert context.response is not None
@when(r'I try to create an AcpResponse with status "(?P<status>[^"]+)"')
@when(r'I try to create an A2aResponse with status "(?P<status>[^"]+)"')
def step_create_response_invalid(context: Context, status: str) -> None:
context.caught_error = None
context.error = None
try:
AcpResponse(request_id="REQ", status=status)
A2aResponse(request_id="REQ", status=status)
except ValidationError as exc:
context.caught_error = exc
context.error = exc
# AcpErrorDetail model validation
# A2aErrorDetail model validation
@when(
r'I create an AcpErrorDetail with code "(?P<code>[^"]+)" and message "(?P<msg>[^"]+)"'
r'I create an A2aErrorDetail with code "(?P<code>[^"]+)" and message "(?P<msg>[^"]+)"'
)
def step_create_error_detail(context: Context, code: str, msg: str) -> None:
context.error_detail = AcpErrorDetail(code=code, message=msg)
context.error_detail = A2aErrorDetail(code=code, message=msg)
@then(r"the error detail should be valid")
@@ -427,23 +427,23 @@ def step_error_detail_valid(context: Context) -> None:
assert context.error_detail is not None
@when(r"I try to create an AcpErrorDetail with empty code")
@when(r"I try to create an A2aErrorDetail with empty code")
def step_create_error_detail_empty(context: Context) -> None:
context.caught_error = None
context.error = None
try:
AcpErrorDetail(code="", message="test")
A2aErrorDetail(code="", message="test")
except ValidationError as exc:
context.caught_error = exc
context.error = exc
# AcpEvent model construction
# A2aEvent model construction
@when(r'I create an AcpEvent with type "(?P<event_type>[^"]+)"')
@when(r'I create an A2aEvent with type "(?P<event_type>[^"]+)"')
def step_create_event(context: Context, event_type: str) -> None:
context.event = AcpEvent(event_type=event_type)
context.event = A2aEvent(event_type=event_type)
@then(r"the event should have a non-empty event_id")
@@ -459,29 +459,29 @@ def step_event_has_timestamp(context: Context) -> None:
# Error hierarchy
@then(r"AcpError should be a subclass of CleverAgentsError")
def step_acp_error_hierarchy(context: Context) -> None:
assert issubclass(AcpError, CleverAgentsError)
@then(r"A2aError should be a subclass of CleverAgentsError")
def step_a2a_error_hierarchy(context: Context) -> None:
assert issubclass(A2aError, CleverAgentsError)
@then(r"AcpNotAvailableError should be a subclass of AcpError")
@then(r"A2aNotAvailableError should be a subclass of A2aError")
def step_not_available_hierarchy(context: Context) -> None:
assert issubclass(AcpNotAvailableError, AcpError)
assert issubclass(A2aNotAvailableError, A2aError)
@then(r"AcpVersionMismatchError should be a subclass of AcpError")
@then(r"A2aVersionMismatchError should be a subclass of A2aError")
def step_version_mismatch_hierarchy(context: Context) -> None:
assert issubclass(AcpVersionMismatchError, AcpError)
assert issubclass(A2aVersionMismatchError, A2aError)
@then(r"AcpOperationNotFoundError should be a subclass of AcpError")
@then(r"A2aOperationNotFoundError should be a subclass of A2aError")
def step_op_not_found_hierarchy(context: Context) -> None:
assert issubclass(AcpOperationNotFoundError, AcpError)
assert issubclass(A2aOperationNotFoundError, A2aError)
@when(r"I create an AcpNotAvailableError with default message")
@when(r"I create an A2aNotAvailableError with default message")
def step_create_not_available_default(context: Context) -> None:
context.caught_error = AcpNotAvailableError()
context.caught_error = A2aNotAvailableError()
context.error = context.caught_error
@@ -1,7 +1,7 @@
"""Step definitions for ACP facade wiring Behave scenarios.
"""Step definitions for A2A facade wiring Behave scenarios.
All mocks in this file are lightweight test doubles that simulate the
service contracts used by :class:`AcpLocalFacade`. They live here
service contracts used by :class:`A2aLocalFacade`. They live here
(inside the test tree) per the project's mock-placement policy.
"""
@@ -14,9 +14,9 @@ from unittest.mock import MagicMock
from behave import given, then, use_step_matcher, when
from behave.runner import Context
from cleveragents.acp.events import AcpEventQueue
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.acp.models import AcpRequest
from cleveragents.a2a.events import A2aEventQueue
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.a2a.models import A2aRequest
from cleveragents.core.exceptions import (
BusinessRuleViolation,
PlanError,
@@ -112,74 +112,74 @@ def _build_mock_resource_registry_service() -> MagicMock:
# ---------------------------------------------------------------------------
@given(r"a wired AcpLocalFacade with a mock SessionService")
@given(r"a wired A2aLocalFacade with a mock SessionService")
def step_wired_facade_session(context: Context) -> None:
context.wired_facade = AcpLocalFacade(
context.wired_facade = A2aLocalFacade(
services={"session_service": _build_mock_session_service()}
)
@given(r"a wired AcpLocalFacade with a mock PlanLifecycleService")
@given(r"a wired A2aLocalFacade with a mock PlanLifecycleService")
def step_wired_facade_plan(context: Context) -> None:
context.wired_facade = AcpLocalFacade(
context.wired_facade = A2aLocalFacade(
services={"plan_lifecycle_service": _build_mock_plan_lifecycle_service()}
)
@given(r"a wired AcpLocalFacade with a mock ToolRegistry")
@given(r"a wired A2aLocalFacade with a mock ToolRegistry")
def step_wired_facade_tool_registry(context: Context) -> None:
context.wired_facade = AcpLocalFacade(
context.wired_facade = A2aLocalFacade(
services={"tool_registry": _build_mock_tool_registry()}
)
@given(r"a wired AcpLocalFacade with a mock ResourceRegistryService")
@given(r"a wired A2aLocalFacade with a mock ResourceRegistryService")
def step_wired_facade_resource_registry(context: Context) -> None:
context.wired_facade = AcpLocalFacade(
context.wired_facade = A2aLocalFacade(
services={
"resource_registry_service": (_build_mock_resource_registry_service())
}
)
@given(r"a wired AcpLocalFacade with a mock AcpEventQueue")
@given(r"a wired A2aLocalFacade with a mock A2aEventQueue")
def step_wired_facade_event_queue(context: Context) -> None:
context.wired_facade = AcpLocalFacade(services={"event_queue": AcpEventQueue()})
context.wired_facade = A2aLocalFacade(services={"event_queue": A2aEventQueue()})
@given(r"a wired AcpLocalFacade with no services")
@given(r"a wired A2aLocalFacade with no services")
def step_wired_facade_no_services(context: Context) -> None:
context.wired_facade = AcpLocalFacade()
context.wired_facade = A2aLocalFacade()
@given(r"a wired AcpLocalFacade with a raising SessionService for not-found")
@given(r"a wired A2aLocalFacade with a raising SessionService for not-found")
def step_wired_facade_not_found(context: Context) -> None:
svc = MagicMock()
svc.delete.side_effect = ResourceNotFoundError(
resource_type="session", resource_id="nonexistent"
)
context.wired_facade = AcpLocalFacade(services={"session_service": svc})
context.wired_facade = A2aLocalFacade(services={"session_service": svc})
@given(r"a wired AcpLocalFacade with a raising service for validation-error")
@given(r"a wired A2aLocalFacade with a raising service for validation-error")
def step_wired_facade_validation_error(context: Context) -> None:
svc = MagicMock()
svc.use_action.side_effect = ValidationError("Invalid action args")
context.wired_facade = AcpLocalFacade(services={"plan_lifecycle_service": svc})
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
@given(r"a wired AcpLocalFacade with a raising service for plan-error")
@given(r"a wired A2aLocalFacade with a raising service for plan-error")
def step_wired_facade_plan_error(context: Context) -> None:
svc = MagicMock()
svc.execute_plan.side_effect = PlanError("Plan execution failed")
context.wired_facade = AcpLocalFacade(services={"plan_lifecycle_service": svc})
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
@given(r"a wired AcpLocalFacade with a raising service for invalid-state")
@given(r"a wired A2aLocalFacade with a raising service for invalid-state")
def step_wired_facade_invalid_state(context: Context) -> None:
svc = MagicMock()
svc.apply_plan.side_effect = BusinessRuleViolation("Cannot apply in current state")
context.wired_facade = AcpLocalFacade(services={"plan_lifecycle_service": svc})
context.wired_facade = A2aLocalFacade(services={"plan_lifecycle_service": svc})
# ---------------------------------------------------------------------------
@@ -193,7 +193,7 @@ def step_wired_facade_invalid_state(context: Context) -> None:
)
def step_dispatch_wired(context: Context, operation: str, params_json: str) -> None:
params: dict[str, Any] = json.loads(params_json)
request = AcpRequest(operation=operation, params=params)
request = A2aRequest(operation=operation, params=params)
context.wired_response = context.wired_facade.dispatch(request)
+4 -4
View File
@@ -19,8 +19,8 @@ from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.acp.models import AcpRequest
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.a2a.models import A2aRequest
from cleveragents.application.services.cleanup_service import CleanupService
from cleveragents.cli.commands.resource import app as resource_app
from cleveragents.core.exceptions import NotFoundError
@@ -252,7 +252,7 @@ def step_create_active_container_no_docker_with_session(
@given("a facade with no session service")
def step_create_facade_no_session(context: Context) -> None:
context.facade = AcpLocalFacade(services={})
context.facade = A2aLocalFacade(services={})
# ── When steps ───────────────────────────────────────────────
@@ -348,7 +348,7 @@ def step_list_for_empty_session(context: Context) -> None:
@when('I close session "{session_id}" via the facade')
def step_close_session_via_facade(context: Context, session_id: str) -> None:
request = AcpRequest(
request = A2aRequest(
operation="session.close",
params={"session_id": session_id},
)
+10 -10
View File
@@ -1,7 +1,7 @@
"""Step definitions for LSP server stub BDD tests.
Covers the JSON-RPC transport, protocol handshake, error handling,
ACP facade wiring, and CLI serve command for the LSP server stub.
A2A facade wiring, and CLI serve command for the LSP server stub.
"""
from __future__ import annotations
@@ -15,7 +15,7 @@ from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.cli.commands.lsp import app as lsp_app
from cleveragents.lsp.server import (
MAX_CONTENT_LENGTH,
@@ -38,7 +38,7 @@ def step_mock_transport(context: Context) -> None:
context.lsp_server: LspServer | None = None
context.lsp_responses: list[dict[str, Any]] = []
context.lsp_exit_code: int | None = None
context.lsp_facade: AcpLocalFacade | None = None
context.lsp_facade: A2aLocalFacade | None = None
context.lsp_error: Exception | None = None
@@ -108,9 +108,9 @@ def step_missing_method(context: Context, req_id: int) -> None:
context.lsp_transport.send_raw(framed)
@given("an LSP server with ACP facade")
@given("an LSP server with A2A facade")
def step_server_with_facade(context: Context) -> None:
context.lsp_facade = AcpLocalFacade()
context.lsp_facade = A2aLocalFacade()
# ---------------------------------------------------------------------------
@@ -349,12 +349,12 @@ def step_error_no_data(context: Context, req_id: int) -> None:
)
@then("the server should hold the provided ACP facade")
@then("the server should hold the provided A2A facade")
def step_server_holds_facade(context: Context) -> None:
assert context.lsp_server is not None, "No server was created"
assert context.lsp_facade is not None, "No facade was provided"
assert context.lsp_server.facade is context.lsp_facade, (
"Server does not hold the provided ACP facade instance"
"Server does not hold the provided A2A facade instance"
)
@@ -364,10 +364,10 @@ def step_server_facade_lazy(context: Context) -> None:
# Access the public property — this triggers lazy creation
facade = context.lsp_server.facade
assert facade is not None, "Facade was not lazily created"
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.a2a.facade import A2aLocalFacade
assert isinstance(facade, AcpLocalFacade), (
f"Expected AcpLocalFacade, got {type(facade).__name__}"
assert isinstance(facade, A2aLocalFacade), (
f"Expected A2aLocalFacade, got {type(facade).__name__}"
)
+63 -63
View File
@@ -1,4 +1,4 @@
"""Step definitions for M6 ACP facade, event queue, transport, and version tests.
"""Step definitions for M6 A2A facade, event queue, transport, and version tests.
Split from ``m6_autonomy_acceptance_steps.py`` to stay under the project's
500-line guideline. All step names keep the ``m6 smoke`` prefix to avoid
@@ -14,21 +14,21 @@ from unittest.mock import MagicMock
from behave import given, then, when
from behave.runner import Context
from cleveragents.acp.errors import (
AcpNotAvailableError,
AcpOperationNotFoundError,
AcpVersionMismatchError,
from cleveragents.a2a.errors import (
A2aNotAvailableError,
A2aOperationNotFoundError,
A2aVersionMismatchError,
)
from cleveragents.acp.events import AcpEventQueue
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.acp.models import (
AcpErrorDetail,
AcpEvent,
AcpRequest,
AcpResponse,
from cleveragents.a2a.events import A2aEventQueue
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.a2a.models import (
A2aErrorDetail,
A2aEvent,
A2aRequest,
A2aResponse,
)
from cleveragents.acp.transport import AcpHttpTransport
from cleveragents.acp.versioning import AcpVersionNegotiator
from cleveragents.a2a.transport import A2aHttpTransport
from cleveragents.a2a.versioning import A2aVersionNegotiator
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "m6"
@@ -56,20 +56,20 @@ def step_m6_smoke_runner(context: Context) -> None:
context.m6_profiles_list = []
@given("a m6 smoke ACP local facade")
@given("a m6 smoke A2A local facade")
def step_m6_smoke_facade(context: Context) -> None:
"""Create an AcpLocalFacade for testing."""
context.m6_facade = AcpLocalFacade()
"""Create an A2aLocalFacade for testing."""
context.m6_facade = A2aLocalFacade()
# -----------------------------------------------------------------------
# Fixture loading — ACP facade flows
# Fixture loading — A2A facade flows
# -----------------------------------------------------------------------
@when("I m6 smoke load the ACP facade flows fixture")
@when("I m6 smoke load the A2A facade flows fixture")
def step_m6_smoke_load_facade_fixture(context: Context) -> None:
with open(_FIXTURES_DIR / "acp_facade_flows.json") as f:
with open(_FIXTURES_DIR / "a2a_facade_flows.json") as f:
context.m6_fixture_data = json.load(f)
@@ -86,7 +86,7 @@ def step_m6_smoke_facade_plan(context: Context) -> None:
# -----------------------------------------------------------------------
# ACP facade dispatch operations
# A2A facade dispatch operations
# -----------------------------------------------------------------------
@@ -97,7 +97,7 @@ def step_m6_smoke_dispatch(
params_json: str,
) -> None:
params = json.loads(params_json)
request = AcpRequest(operation=operation, params=params)
request = A2aRequest(operation=operation, params=params)
context.m6_response = context.m6_facade.dispatch(request)
@@ -124,26 +124,26 @@ def step_m6_smoke_response_value(
# -----------------------------------------------------------------------
# ACP facade error handling
# A2A facade error handling
# -----------------------------------------------------------------------
@when('I m6 smoke dispatch unknown operation "{operation}"')
def step_m6_smoke_dispatch_unknown(context: Context, operation: str) -> None:
request = AcpRequest(operation=operation, params={})
request = A2aRequest(operation=operation, params={})
try:
context.m6_facade.dispatch(request)
context.m6_error = None
except AcpOperationNotFoundError as exc:
except A2aOperationNotFoundError as exc:
context.m6_error = exc
@then("the m6 smoke facade should raise AcpOperationNotFoundError")
@then("the m6 smoke facade should raise A2aOperationNotFoundError")
def step_m6_smoke_error_op_not_found(context: Context) -> None:
assert isinstance(context.m6_error, AcpOperationNotFoundError)
assert isinstance(context.m6_error, A2aOperationNotFoundError)
@when("I m6 smoke dispatch with a non-AcpRequest object")
@when("I m6 smoke dispatch with a non-A2aRequest object")
def step_m6_smoke_dispatch_invalid(context: Context) -> None:
try:
context.m6_facade.dispatch("not a request") # type: ignore[arg-type]
@@ -158,7 +158,7 @@ def step_m6_smoke_error_type(context: Context) -> None:
# -----------------------------------------------------------------------
# ACP facade service registration and list operations
# A2A facade service registration and list operations
# -----------------------------------------------------------------------
@@ -189,19 +189,19 @@ def step_m6_smoke_ops_count(context: Context, count: int) -> None:
# -----------------------------------------------------------------------
# ACP event queue
# A2A event queue
# -----------------------------------------------------------------------
@given("a m6 smoke ACP event queue")
@given("a m6 smoke A2A event queue")
def step_m6_smoke_event_queue(context: Context) -> None:
context.m6_event_queue = AcpEventQueue()
context.m6_event_queue = A2aEventQueue()
context.m6_callback_calls = []
@when('I m6 smoke publish an event with type "{event_type}"')
def step_m6_smoke_publish_event(context: Context, event_type: str) -> None:
event = AcpEvent(event_type=event_type, data={"test": True})
event = A2aEvent(event_type=event_type, data={"test": True})
context.m6_event_queue.publish(event)
@@ -219,7 +219,7 @@ def step_m6_smoke_last_event_type(context: Context, event_type: str) -> None:
@when("I m6 smoke subscribe a local callback")
def step_m6_smoke_subscribe_local(context: Context) -> None:
def _callback(event: AcpEvent) -> None:
def _callback(event: A2aEvent) -> None:
context.m6_callback_calls.append(event)
context.m6_subscription_id = context.m6_event_queue.subscribe_local(_callback)
@@ -247,7 +247,7 @@ def step_m6_smoke_close_queue(context: Context) -> None:
@then("the m6 smoke publishing should raise RuntimeError")
def step_m6_smoke_publish_after_close(context: Context) -> None:
event = AcpEvent(event_type="after.close", data={})
event = A2aEvent(event_type="after.close", data={})
try:
context.m6_event_queue.publish(event)
raise AssertionError("Expected RuntimeError")
@@ -260,54 +260,54 @@ def step_m6_smoke_remote_subscribe(context: Context, endpoint: str) -> None:
try:
context.m6_event_queue.subscribe_remote(endpoint)
context.m6_error = None
except AcpNotAvailableError as exc:
except A2aNotAvailableError as exc:
context.m6_error = exc
@then("the m6 smoke facade should raise AcpNotAvailableError")
@then("the m6 smoke facade should raise A2aNotAvailableError")
def step_m6_smoke_error_not_available(context: Context) -> None:
assert isinstance(context.m6_error, AcpNotAvailableError)
assert isinstance(context.m6_error, A2aNotAvailableError)
# -----------------------------------------------------------------------
# ACP HTTP transport stub
# A2A HTTP transport stub
# -----------------------------------------------------------------------
@when("I m6 smoke attempt transport send")
def step_m6_smoke_transport_send(context: Context) -> None:
transport = AcpHttpTransport()
request = AcpRequest(operation="plan.create", params={})
transport = A2aHttpTransport()
request = A2aRequest(operation="plan.create", params={})
try:
transport.send(request)
context.m6_error = None
except AcpNotAvailableError as exc:
except A2aNotAvailableError as exc:
context.m6_error = exc
@when('I m6 smoke attempt transport connect to "{url}"')
def step_m6_smoke_transport_connect(context: Context, url: str) -> None:
transport = AcpHttpTransport()
transport = A2aHttpTransport()
try:
transport.connect(url)
context.m6_error = None
except AcpNotAvailableError as exc:
except A2aNotAvailableError as exc:
context.m6_error = exc
@when("I m6 smoke attempt transport disconnect")
def step_m6_smoke_transport_disconnect(context: Context) -> None:
transport = AcpHttpTransport()
transport = A2aHttpTransport()
try:
transport.disconnect()
context.m6_error = None
except AcpNotAvailableError as exc:
except A2aNotAvailableError as exc:
context.m6_error = exc
@when("I m6 smoke check transport is_connected")
def step_m6_smoke_transport_connected(context: Context) -> None:
transport = AcpHttpTransport()
transport = A2aHttpTransport()
context.m6_transport_connected = transport.is_connected()
@@ -317,17 +317,17 @@ def step_m6_smoke_transport_not_connected(context: Context) -> None:
# -----------------------------------------------------------------------
# ACP version negotiation
# A2A version negotiation
# -----------------------------------------------------------------------
@when('I m6 smoke negotiate ACP version "{version}"')
@when('I m6 smoke negotiate A2A version "{version}"')
def step_m6_smoke_negotiate(context: Context, version: str) -> None:
negotiator = AcpVersionNegotiator()
negotiator = A2aVersionNegotiator()
try:
context.m6_version_result = negotiator.negotiate(version)
context.m6_error = None
except AcpVersionMismatchError as exc:
except A2aVersionMismatchError as exc:
context.m6_error = exc
@@ -336,14 +336,14 @@ def step_m6_smoke_negotiated(context: Context, version: str) -> None:
assert context.m6_version_result == version
@then("the m6 smoke facade should raise AcpVersionMismatchError")
@then("the m6 smoke facade should raise A2aVersionMismatchError")
def step_m6_smoke_error_version(context: Context) -> None:
assert isinstance(context.m6_error, AcpVersionMismatchError)
assert isinstance(context.m6_error, A2aVersionMismatchError)
@when('I m6 smoke check if version "{version}" is supported')
def step_m6_smoke_version_supported(context: Context, version: str) -> None:
negotiator = AcpVersionNegotiator()
negotiator = A2aVersionNegotiator()
context.m6_version_supported = negotiator.is_supported(version)
@@ -358,41 +358,41 @@ def step_m6_smoke_version_false(context: Context) -> None:
# -----------------------------------------------------------------------
# ACP model validation
# A2A model validation
# -----------------------------------------------------------------------
@when("I m6 smoke create AcpRequest with empty operation")
@when("I m6 smoke create A2aRequest with empty operation")
def step_m6_smoke_invalid_request(context: Context) -> None:
try:
AcpRequest(operation="")
A2aRequest(operation="")
context.m6_error = None
except ValueError as exc:
context.m6_error = exc
@when('I m6 smoke create AcpResponse with invalid status "{status}"')
@when('I m6 smoke create A2aResponse with invalid status "{status}"')
def step_m6_smoke_invalid_response(context: Context, status: str) -> None:
try:
AcpResponse(request_id="test", status=status)
A2aResponse(request_id="test", status=status)
context.m6_error = None
except ValueError as exc:
context.m6_error = exc
@when("I m6 smoke create AcpEvent with empty event_type")
@when("I m6 smoke create A2aEvent with empty event_type")
def step_m6_smoke_invalid_event(context: Context) -> None:
try:
AcpEvent(event_type="")
A2aEvent(event_type="")
context.m6_error = None
except ValueError as exc:
context.m6_error = exc
@when("I m6 smoke create AcpErrorDetail with empty code")
@when("I m6 smoke create A2aErrorDetail with empty code")
def step_m6_smoke_invalid_error_detail(context: Context) -> None:
try:
AcpErrorDetail(code="", message="test")
A2aErrorDetail(code="", message="test")
context.m6_error = None
except ValueError as exc:
context.m6_error = exc
+5 -5
View File
@@ -7,7 +7,7 @@ from pathlib import Path
from behave import given, then, when
from cleveragents.acp.events import AcpEventQueue
from cleveragents.a2a.events import A2aEventQueue
from cleveragents.core.async_cleanup import AsyncResourceTracker
from cleveragents.langgraph.state import GraphState, StateManager
@@ -132,9 +132,9 @@ def step_create_state_manager(context):
)
@given("I have an ACP event queue with {count:d} active subscriptions")
@given("I have an A2A event queue with {count:d} active subscriptions")
def step_create_event_queue(context, count):
context.event_queue = AcpEventQueue()
context.event_queue = A2aEventQueue()
context.subscription_ids = []
for _ in range(count):
sub_id = context.event_queue.subscribe_local(lambda _evt: None)
@@ -483,10 +483,10 @@ def step_try_reset_after_close(context):
@when("I try to publish an event after close")
def step_try_publish_after_close(context):
from cleveragents.acp.models import AcpEvent
from cleveragents.a2a.models import A2aEvent
try:
context.event_queue.publish(AcpEvent(event_type="test", data={}))
context.event_queue.publish(A2aEvent(event_type="test", data={}))
context.runtime_error = None
except RuntimeError as exc:
context.runtime_error = exc
+2 -2
View File
@@ -9,7 +9,7 @@ from behave import given, then, use_step_matcher, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.acp.clients import (
from cleveragents.a2a.clients import (
AuthClient,
RemoteExecutionClient,
ServerClient,
@@ -17,7 +17,7 @@ from cleveragents.acp.clients import (
StubRemoteExecutionClient,
StubServerClient,
)
from cleveragents.acp.server_config import ServerConnectionConfig
from cleveragents.a2a.server_config import ServerConnectionConfig
from cleveragents.application.services.config_service import _REGISTRY
use_step_matcher("re")
@@ -1,49 +1,49 @@
*** Settings ***
Documentation Smoke tests for ACP local facade and server stubs
Documentation Smoke tests for A2A local facade and server stubs
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_acp_facade.py
${HELPER} ${CURDIR}/helper_a2a_facade.py
*** Test Cases ***
ACP Local Facade Dispatch Session Create
A2A Local Facade Dispatch Session Create
[Documentation] Verify local facade dispatches session.create successfully
${result}= Run Process ${PYTHON} ${HELPER} facade-dispatch cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-facade-dispatch-ok
Should Contain ${result.stdout} a2a-facade-dispatch-ok
ACP HTTP Transport Stub Error
[Documentation] Verify HTTP transport raises AcpNotAvailableError
A2A HTTP Transport Stub Error
[Documentation] Verify HTTP transport raises A2aNotAvailableError
${result}= Run Process ${PYTHON} ${HELPER} transport-stub cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-transport-stub-ok
Should Contain ${result.stdout} a2a-transport-stub-ok
ACP Event Queue Local Mode
A2A Event Queue Local Mode
[Documentation] Verify event queue publish and subscribe work locally
${result}= Run Process ${PYTHON} ${HELPER} event-queue cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-event-queue-ok
Should Contain ${result.stdout} a2a-event-queue-ok
ACP Version Negotiation
A2A Version Negotiation
[Documentation] Verify version negotiation succeeds for 1.0 and fails for 2.0
${result}= Run Process ${PYTHON} ${HELPER} version-negotiate cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-version-negotiate-ok
Should Contain ${result.stdout} a2a-version-negotiate-ok
ACP Operation Listing
A2A Operation Listing
[Documentation] Verify list_operations returns all expected operations
${result}= Run Process ${PYTHON} ${HELPER} list-operations cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-list-operations-ok
Should Contain ${result.stdout} a2a-list-operations-ok
@@ -1,11 +1,11 @@
*** Settings ***
Documentation Integration tests for ACP local facade wiring to live services
Documentation Integration tests for A2A local facade wiring to live services
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_acp_facade_wiring.py
${HELPER} ${CURDIR}/helper_a2a_facade_wiring.py
*** Test Cases ***
Wired Facade Session Create
@@ -14,7 +14,7 @@ Wired Facade Session Create
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-session-create-ok
Should Contain ${result.stdout} a2a-wired-session-create-ok
Wired Facade Session Close
[Documentation] Verify session.close routes to SessionService
@@ -22,7 +22,7 @@ Wired Facade Session Close
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-session-close-ok
Should Contain ${result.stdout} a2a-wired-session-close-ok
Wired Facade Plan Create
[Documentation] Verify plan.create routes to PlanLifecycleService
@@ -30,7 +30,7 @@ Wired Facade Plan Create
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-plan-create-ok
Should Contain ${result.stdout} a2a-wired-plan-create-ok
Wired Facade Plan Status
[Documentation] Verify plan.status routes to PlanLifecycleService
@@ -38,7 +38,7 @@ Wired Facade Plan Status
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-plan-status-ok
Should Contain ${result.stdout} a2a-wired-plan-status-ok
Wired Facade Registry Tools
[Documentation] Verify registry.list_tools routes to ToolRegistry
@@ -46,7 +46,7 @@ Wired Facade Registry Tools
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-registry-tools-ok
Should Contain ${result.stdout} a2a-wired-registry-tools-ok
Wired Facade Registry Resources
[Documentation] Verify registry.list_resources routes to ResourceRegistryService
@@ -54,7 +54,7 @@ Wired Facade Registry Resources
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-registry-resources-ok
Should Contain ${result.stdout} a2a-wired-registry-resources-ok
Wired Facade Context Stub
[Documentation] Verify context.get returns stub while ACMS pipeline is pending
@@ -62,20 +62,20 @@ Wired Facade Context Stub
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-context-stub-ok
Should Contain ${result.stdout} a2a-wired-context-stub-ok
Wired Facade Event Subscribe
[Documentation] Verify event.subscribe routes to AcpEventQueue
[Documentation] Verify event.subscribe routes to A2aEventQueue
${result}= Run Process ${PYTHON} ${HELPER} wired-event-subscribe cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-event-subscribe-ok
Should Contain ${result.stdout} a2a-wired-event-subscribe-ok
Wired Facade Error Mapping
[Documentation] Verify domain exceptions map to correct ACP error codes
[Documentation] Verify domain exceptions map to correct A2A error codes
${result}= Run Process ${PYTHON} ${HELPER} wired-error-mapping cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} acp-wired-error-mapping-ok
Should Contain ${result.stdout} a2a-wired-error-mapping-ok
@@ -1,4 +1,4 @@
"""Helper script for acp_facade.robot smoke tests.
"""Helper script for a2a_facade.robot smoke tests.
Each subcommand is a self-contained check that prints a sentinel on success.
"""
@@ -13,15 +13,15 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.acp.errors import ( # noqa: E402
AcpNotAvailableError,
AcpVersionMismatchError,
from cleveragents.a2a.errors import ( # noqa: E402
A2aNotAvailableError,
A2aVersionMismatchError,
)
from cleveragents.acp.events import AcpEventQueue # noqa: E402
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
from cleveragents.acp.models import AcpEvent, AcpRequest # noqa: E402
from cleveragents.acp.transport import AcpHttpTransport # noqa: E402
from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
from cleveragents.a2a.events import A2aEventQueue # noqa: E402
from cleveragents.a2a.facade import A2aLocalFacade # noqa: E402
from cleveragents.a2a.models import A2aEvent, A2aRequest # noqa: E402
from cleveragents.a2a.transport import A2aHttpTransport # noqa: E402
from cleveragents.a2a.versioning import A2aVersionNegotiator # noqa: E402
# ---------------------------------------------------------------------------
# Subcommands
@@ -30,28 +30,28 @@ from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
def facade_dispatch() -> None:
"""Dispatch session.create via local facade."""
facade = AcpLocalFacade()
request = AcpRequest(operation="session.create")
facade = A2aLocalFacade()
request = A2aRequest(operation="session.create")
response = facade.dispatch(request)
if response.status == "ok" and "session_id" in response.data:
print("acp-facade-dispatch-ok")
print("a2a-facade-dispatch-ok")
else:
print(f"FAIL: unexpected response {response}", file=sys.stderr)
sys.exit(1)
def transport_stub() -> None:
"""Verify transport stub raises AcpNotAvailableError."""
transport = AcpHttpTransport()
"""Verify transport stub raises A2aNotAvailableError."""
transport = A2aHttpTransport()
try:
transport.connect("http://localhost:8080")
print("FAIL: should have raised", file=sys.stderr)
sys.exit(1)
except AcpNotAvailableError:
except A2aNotAvailableError:
pass
if transport.is_connected() is False:
print("acp-transport-stub-ok")
print("a2a-transport-stub-ok")
else:
print("FAIL: is_connected should be False", file=sys.stderr)
sys.exit(1)
@@ -59,12 +59,12 @@ def transport_stub() -> None:
def event_queue() -> None:
"""Verify local event queue publish/subscribe."""
queue = AcpEventQueue()
received: list[AcpEvent] = []
queue = A2aEventQueue()
received: list[A2aEvent] = []
queue.subscribe_local(lambda e: received.append(e))
queue.publish(AcpEvent(event_type="test.event"))
queue.publish(A2aEvent(event_type="test.event"))
if len(received) == 1 and received[0].event_type == "test.event":
print("acp-event-queue-ok")
print("a2a-event-queue-ok")
else:
print(f"FAIL: received={received}", file=sys.stderr)
sys.exit(1)
@@ -72,7 +72,7 @@ def event_queue() -> None:
def version_negotiate() -> None:
"""Verify version negotiation."""
negotiator = AcpVersionNegotiator()
negotiator = A2aVersionNegotiator()
result = negotiator.negotiate("1.0")
if result != "1.0":
print("FAIL: expected 1.0", file=sys.stderr)
@@ -82,19 +82,19 @@ def version_negotiate() -> None:
negotiator.negotiate("2.0")
print("FAIL: should have raised", file=sys.stderr)
sys.exit(1)
except AcpVersionMismatchError:
except A2aVersionMismatchError:
pass
print("acp-version-negotiate-ok")
print("a2a-version-negotiate-ok")
def list_operations() -> None:
"""Verify list_operations returns expected operations."""
facade = AcpLocalFacade()
facade = A2aLocalFacade()
ops = facade.list_operations()
expected = {"session.create", "plan.create", "plan.execute", "context.get"}
if expected.issubset(set(ops)) and len(ops) == 11:
print("acp-list-operations-ok")
print("a2a-list-operations-ok")
else:
print(f"FAIL: ops={ops}", file=sys.stderr)
sys.exit(1)
@@ -1,6 +1,6 @@
"""Helper script for acp_facade_wiring.robot integration tests.
"""Helper script for a2a_facade_wiring.robot integration tests.
Each subcommand exercises the wired ACP facade with lightweight mock
Each subcommand exercises the wired A2A facade with lightweight mock
services and prints a sentinel on success.
"""
@@ -15,12 +15,12 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.acp.errors import ( # noqa: E402
from cleveragents.a2a.errors import ( # noqa: E402
map_domain_error,
)
from cleveragents.acp.events import AcpEventQueue # noqa: E402
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
from cleveragents.acp.models import AcpRequest # noqa: E402
from cleveragents.a2a.events import A2aEventQueue # noqa: E402
from cleveragents.a2a.facade import A2aLocalFacade # noqa: E402
from cleveragents.a2a.models import A2aRequest # noqa: E402
from cleveragents.core.exceptions import ( # noqa: E402
BusinessRuleViolation,
PlanError,
@@ -102,10 +102,10 @@ def _mock_resource_registry_service() -> MagicMock:
def wired_session_create() -> None:
"""Dispatch session.create through a wired facade."""
facade = AcpLocalFacade(services={"session_service": _mock_session_service()})
resp = facade.dispatch(AcpRequest(operation="session.create"))
facade = A2aLocalFacade(services={"session_service": _mock_session_service()})
resp = facade.dispatch(A2aRequest(operation="session.create"))
if resp.status == "ok" and resp.data["session_id"] == "INTEG-SESSION-001":
print("acp-wired-session-create-ok")
print("a2a-wired-session-create-ok")
else:
print(f"FAIL: {resp}", file=sys.stderr)
sys.exit(1)
@@ -113,15 +113,15 @@ def wired_session_create() -> None:
def wired_session_close() -> None:
"""Dispatch session.close through a wired facade."""
facade = AcpLocalFacade(services={"session_service": _mock_session_service()})
facade = A2aLocalFacade(services={"session_service": _mock_session_service()})
resp = facade.dispatch(
AcpRequest(
A2aRequest(
operation="session.close",
params={"session_id": "INTEG-SESSION-001"},
)
)
if resp.status == "ok" and resp.data["status"] == "closed":
print("acp-wired-session-close-ok")
print("a2a-wired-session-close-ok")
else:
print(f"FAIL: {resp}", file=sys.stderr)
sys.exit(1)
@@ -129,17 +129,17 @@ def wired_session_close() -> None:
def wired_plan_create() -> None:
"""Dispatch plan.create through a wired facade."""
facade = AcpLocalFacade(
facade = A2aLocalFacade(
services={"plan_lifecycle_service": _mock_plan_lifecycle_service()}
)
resp = facade.dispatch(
AcpRequest(
A2aRequest(
operation="plan.create",
params={"action_name": "local/test"},
)
)
if resp.status == "ok" and resp.data["plan_id"] == "INTEG-PLAN-001":
print("acp-wired-plan-create-ok")
print("a2a-wired-plan-create-ok")
else:
print(f"FAIL: {resp}", file=sys.stderr)
sys.exit(1)
@@ -147,17 +147,17 @@ def wired_plan_create() -> None:
def wired_plan_status() -> None:
"""Dispatch plan.status through a wired facade."""
facade = AcpLocalFacade(
facade = A2aLocalFacade(
services={"plan_lifecycle_service": _mock_plan_lifecycle_service()}
)
resp = facade.dispatch(
AcpRequest(
A2aRequest(
operation="plan.status",
params={"plan_id": "INTEG-PLAN-001"},
)
)
if resp.status == "ok" and resp.data["phase"] == "strategize":
print("acp-wired-plan-status-ok")
print("a2a-wired-plan-status-ok")
else:
print(f"FAIL: {resp}", file=sys.stderr)
sys.exit(1)
@@ -165,11 +165,11 @@ def wired_plan_status() -> None:
def wired_registry_tools() -> None:
"""Dispatch registry.list_tools through a wired facade."""
facade = AcpLocalFacade(services={"tool_registry": _mock_tool_registry()})
resp = facade.dispatch(AcpRequest(operation="registry.list_tools"))
facade = A2aLocalFacade(services={"tool_registry": _mock_tool_registry()})
resp = facade.dispatch(A2aRequest(operation="registry.list_tools"))
tools = resp.data.get("tools", [])
if resp.status == "ok" and len(tools) == 1:
print("acp-wired-registry-tools-ok")
print("a2a-wired-registry-tools-ok")
else:
print(f"FAIL: {resp}", file=sys.stderr)
sys.exit(1)
@@ -177,13 +177,13 @@ def wired_registry_tools() -> None:
def wired_registry_resources() -> None:
"""Dispatch registry.list_resources through a wired facade."""
facade = AcpLocalFacade(
facade = A2aLocalFacade(
services={"resource_registry_service": _mock_resource_registry_service()}
)
resp = facade.dispatch(AcpRequest(operation="registry.list_resources"))
resp = facade.dispatch(A2aRequest(operation="registry.list_resources"))
resources = resp.data.get("resources", [])
if resp.status == "ok" and len(resources) == 1:
print("acp-wired-registry-resources-ok")
print("a2a-wired-registry-resources-ok")
else:
print(f"FAIL: {resp}", file=sys.stderr)
sys.exit(1)
@@ -191,10 +191,10 @@ def wired_registry_resources() -> None:
def wired_context_stub() -> None:
"""Dispatch context.get and verify stub response."""
facade = AcpLocalFacade()
resp = facade.dispatch(AcpRequest(operation="context.get"))
facade = A2aLocalFacade()
resp = facade.dispatch(A2aRequest(operation="context.get"))
if resp.status == "ok" and resp.data.get("stub") is True:
print("acp-wired-context-stub-ok")
print("a2a-wired-context-stub-ok")
else:
print(f"FAIL: {resp}", file=sys.stderr)
sys.exit(1)
@@ -202,19 +202,19 @@ def wired_context_stub() -> None:
def wired_event_subscribe() -> None:
"""Dispatch event.subscribe through a wired facade."""
queue = AcpEventQueue()
facade = AcpLocalFacade(services={"event_queue": queue})
resp = facade.dispatch(AcpRequest(operation="event.subscribe"))
queue = A2aEventQueue()
facade = A2aLocalFacade(services={"event_queue": queue})
resp = facade.dispatch(A2aRequest(operation="event.subscribe"))
sub_id = resp.data.get("subscription_id", "")
if resp.status == "ok" and sub_id:
print("acp-wired-event-subscribe-ok")
print("a2a-wired-event-subscribe-ok")
else:
print(f"FAIL: {resp}", file=sys.stderr)
sys.exit(1)
def wired_error_mapping() -> None:
"""Verify domain-to-ACP error code mapping."""
"""Verify domain-to-A2A error code mapping."""
cases: list[tuple[Exception, str]] = [
(ResourceNotFoundError(resource_type="x", resource_id="1"), "NOT_FOUND"),
(ValidationError("bad"), "VALIDATION_ERROR"),
@@ -229,7 +229,7 @@ def wired_error_mapping() -> None:
file=sys.stderr,
)
sys.exit(1)
print("acp-wired-error-mapping-ok")
print("a2a-wired-error-mapping-ok")
# ---------------------------------------------------------------------------
+49 -49
View File
@@ -14,16 +14,16 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.acp.errors import ( # noqa: E402
AcpNotAvailableError,
AcpOperationNotFoundError,
AcpVersionMismatchError,
from cleveragents.a2a.errors import ( # noqa: E402
A2aNotAvailableError,
A2aOperationNotFoundError,
A2aVersionMismatchError,
)
from cleveragents.acp.events import AcpEventQueue # noqa: E402
from cleveragents.acp.facade import AcpLocalFacade # noqa: E402
from cleveragents.acp.models import AcpEvent, AcpRequest # noqa: E402
from cleveragents.acp.transport import AcpHttpTransport # noqa: E402
from cleveragents.acp.versioning import AcpVersionNegotiator # noqa: E402
from cleveragents.a2a.events import A2aEventQueue # noqa: E402
from cleveragents.a2a.facade import A2aLocalFacade # noqa: E402
from cleveragents.a2a.models import A2aEvent, A2aRequest # noqa: E402
from cleveragents.a2a.transport import A2aHttpTransport # noqa: E402
from cleveragents.a2a.versioning import A2aVersionNegotiator # noqa: E402
from cleveragents.application.services.automation_profile_service import ( # noqa: E402
AutomationProfileService,
)
@@ -44,13 +44,13 @@ _FIXTURES_DIR = Path(__file__).resolve().parents[1] / "features" / "fixtures" /
def facade_session() -> None:
"""Dispatch session.create and session.close."""
facade = AcpLocalFacade()
facade = A2aLocalFacade()
resp_create = facade.dispatch(AcpRequest(operation="session.create", params={}))
resp_create = facade.dispatch(A2aRequest(operation="session.create", params={}))
assert resp_create.status == "ok", f"Expected ok, got {resp_create.status}"
assert "session_id" in resp_create.data
resp_close = facade.dispatch(AcpRequest(operation="session.close", params={}))
resp_close = facade.dispatch(A2aRequest(operation="session.close", params={}))
assert resp_close.status == "ok"
assert resp_close.data["status"] == "closed"
@@ -59,33 +59,33 @@ def facade_session() -> None:
def facade_plan() -> None:
"""Dispatch plan create/execute/status/diff/apply."""
facade = AcpLocalFacade()
facade = A2aLocalFacade()
resp = facade.dispatch(AcpRequest(operation="plan.create", params={}))
resp = facade.dispatch(A2aRequest(operation="plan.create", params={}))
assert resp.status == "ok"
plan_id = resp.data["plan_id"]
assert plan_id
resp = facade.dispatch(
AcpRequest(operation="plan.execute", params={"plan_id": plan_id})
A2aRequest(operation="plan.execute", params={"plan_id": plan_id})
)
assert resp.status == "ok"
assert resp.data["status"] == "queued"
resp = facade.dispatch(
AcpRequest(operation="plan.status", params={"plan_id": plan_id})
A2aRequest(operation="plan.status", params={"plan_id": plan_id})
)
assert resp.status == "ok"
assert "phase" in resp.data
resp = facade.dispatch(
AcpRequest(operation="plan.diff", params={"plan_id": plan_id})
A2aRequest(operation="plan.diff", params={"plan_id": plan_id})
)
assert resp.status == "ok"
assert "changes" in resp.data
resp = facade.dispatch(
AcpRequest(operation="plan.apply", params={"plan_id": plan_id})
A2aRequest(operation="plan.apply", params={"plan_id": plan_id})
)
assert resp.status == "ok"
assert resp.data["status"] == "applied"
@@ -94,13 +94,13 @@ def facade_plan() -> None:
def facade_unknown_op() -> None:
"""Verify unknown operation raises AcpOperationNotFoundError."""
facade = AcpLocalFacade()
"""Verify unknown operation raises A2aOperationNotFoundError."""
facade = A2aLocalFacade()
try:
facade.dispatch(AcpRequest(operation="nonexistent.op", params={}))
print("FAIL: expected AcpOperationNotFoundError")
facade.dispatch(A2aRequest(operation="nonexistent.op", params={}))
print("FAIL: expected A2aOperationNotFoundError")
sys.exit(1)
except AcpOperationNotFoundError:
except A2aOperationNotFoundError:
pass
print("m6-facade-unknown-op-ok")
@@ -108,17 +108,17 @@ def facade_unknown_op() -> None:
def event_queue() -> None:
"""Publish events and verify local subscriber receives them."""
queue = AcpEventQueue()
queue = A2aEventQueue()
received = []
sub_id = queue.subscribe_local(lambda e: received.append(e))
queue.publish(AcpEvent(event_type="plan.progress", data={"step": 1}))
queue.publish(A2aEvent(event_type="plan.progress", data={"step": 1}))
assert len(received) == 1
assert received[0].event_type == "plan.progress"
# Unsubscribe and verify no more callbacks
queue.unsubscribe(sub_id)
queue.publish(AcpEvent(event_type="plan.complete", data={}))
queue.publish(A2aEvent(event_type="plan.complete", data={}))
assert len(received) == 1 # still 1
# Verify get_events returns both
@@ -128,7 +128,7 @@ def event_queue() -> None:
# Close and verify publish raises
queue.close()
try:
queue.publish(AcpEvent(event_type="after.close", data={}))
queue.publish(A2aEvent(event_type="after.close", data={}))
print("FAIL: expected RuntimeError")
sys.exit(1)
except RuntimeError:
@@ -138,31 +138,31 @@ def event_queue() -> None:
def transport_stub() -> None:
"""Verify HTTP transport stub raises AcpNotAvailableError."""
transport = AcpHttpTransport()
"""Verify HTTP transport stub raises A2aNotAvailableError."""
transport = A2aHttpTransport()
# send
try:
transport.send(AcpRequest(operation="plan.create", params={}))
print("FAIL: expected AcpNotAvailableError on send")
transport.send(A2aRequest(operation="plan.create", params={}))
print("FAIL: expected A2aNotAvailableError on send")
sys.exit(1)
except AcpNotAvailableError:
except A2aNotAvailableError:
pass
# connect
try:
transport.connect("https://example.com/acp")
print("FAIL: expected AcpNotAvailableError on connect")
transport.connect("https://example.com/a2a")
print("FAIL: expected A2aNotAvailableError on connect")
sys.exit(1)
except AcpNotAvailableError:
except A2aNotAvailableError:
pass
# disconnect
try:
transport.disconnect()
print("FAIL: expected AcpNotAvailableError on disconnect")
print("FAIL: expected A2aNotAvailableError on disconnect")
sys.exit(1)
except AcpNotAvailableError:
except A2aNotAvailableError:
pass
# is_connected
@@ -172,8 +172,8 @@ def transport_stub() -> None:
def version_negotiation() -> None:
"""Negotiate supported and unsupported ACP versions."""
negotiator = AcpVersionNegotiator()
"""Negotiate supported and unsupported A2A versions."""
negotiator = A2aVersionNegotiator()
# Supported
result = negotiator.negotiate("1.0")
@@ -184,9 +184,9 @@ def version_negotiation() -> None:
# Unsupported
try:
negotiator.negotiate("2.0")
print("FAIL: expected AcpVersionMismatchError")
print("FAIL: expected A2aVersionMismatchError")
sys.exit(1)
except AcpVersionMismatchError:
except A2aVersionMismatchError:
pass
assert negotiator.is_supported("99.0") is False
@@ -271,7 +271,7 @@ def profile_resolution() -> None:
def fixture_loading() -> None:
"""Load all M6 fixture files and verify structure."""
for fname in (
"acp_facade_flows.json",
"a2a_facade_flows.json",
"autonomy_guardrails.json",
"automation_profiles.json",
):
@@ -286,16 +286,16 @@ def fixture_loading() -> None:
def full_flow() -> None:
"""End-to-end: facade dispatch + guard check + profile resolution."""
# Step 1: Facade dispatch
facade = AcpLocalFacade()
resp = facade.dispatch(AcpRequest(operation="session.create", params={}))
facade = A2aLocalFacade()
resp = facade.dispatch(A2aRequest(operation="session.create", params={}))
assert resp.status == "ok"
resp = facade.dispatch(AcpRequest(operation="plan.create", params={}))
resp = facade.dispatch(A2aRequest(operation="plan.create", params={}))
assert resp.status == "ok"
plan_id = resp.data["plan_id"]
resp = facade.dispatch(
AcpRequest(operation="plan.execute", params={"plan_id": plan_id})
A2aRequest(operation="plan.execute", params={"plan_id": plan_id})
)
assert resp.status == "ok"
@@ -318,12 +318,12 @@ def full_flow() -> None:
assert p.name == "ci"
# Step 4: Version negotiation
negotiator = AcpVersionNegotiator()
negotiator = A2aVersionNegotiator()
assert negotiator.negotiate("1.0") == "1.0"
# Step 5: Event queue
queue = AcpEventQueue()
queue.publish(AcpEvent(event_type="flow.complete", data={}))
queue = A2aEventQueue()
queue.publish(A2aEvent(event_type="flow.complete", data={}))
assert len(queue.get_events()) == 1
queue.close()
+2 -2
View File
@@ -14,7 +14,7 @@ _SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.acp.clients import ( # noqa: E402
from cleveragents.a2a.clients import ( # noqa: E402
AuthClient,
RemoteExecutionClient,
ServerClient,
@@ -22,7 +22,7 @@ from cleveragents.acp.clients import ( # noqa: E402
StubRemoteExecutionClient,
StubServerClient,
)
from cleveragents.acp.server_config import ServerConnectionConfig # noqa: E402
from cleveragents.a2a.server_config import ServerConnectionConfig # noqa: E402
from cleveragents.application.services.config_service import _REGISTRY # noqa: E402
# ---------------------------------------------------------------------------
+10 -10
View File
@@ -1,5 +1,5 @@
*** Settings ***
Documentation M6 autonomy acceptance E2E smoke tests — ACP facade, guardrails, audit
Documentation M6 autonomy acceptance E2E smoke tests — A2A facade, guardrails, audit
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
@@ -8,7 +8,7 @@ Suite Teardown Cleanup Test Environment
${HELPER} ${CURDIR}/helper_m6_autonomy_acceptance.py
*** Test Cases ***
M6 ACP Facade Session Lifecycle
M6 A2A Facade Session Lifecycle
[Documentation] Dispatch session.create and session.close via local facade
${result}= Run Process ${PYTHON} ${HELPER} facade-session cwd=${WORKSPACE} timeout=30s
Log ${result.stdout}
@@ -16,7 +16,7 @@ M6 ACP Facade Session Lifecycle
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} m6-facade-session-ok
M6 ACP Facade Plan Lifecycle
M6 A2A Facade Plan Lifecycle
[Documentation] Dispatch plan create/execute/status/diff/apply operations
${result}= Run Process ${PYTHON} ${HELPER} facade-plan cwd=${WORKSPACE} timeout=30s
Log ${result.stdout}
@@ -24,15 +24,15 @@ M6 ACP Facade Plan Lifecycle
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} m6-facade-plan-ok
M6 ACP Facade Unknown Operation Error
[Documentation] Verify unknown operations raise AcpOperationNotFoundError
M6 A2A Facade Unknown Operation Error
[Documentation] Verify unknown operations raise A2aOperationNotFoundError
${result}= Run Process ${PYTHON} ${HELPER} facade-unknown-op cwd=${WORKSPACE} timeout=30s
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} m6-facade-unknown-op-ok
M6 ACP Event Queue Publish Subscribe
M6 A2A Event Queue Publish Subscribe
[Documentation] Publish events and verify local subscriber receives them
${result}= Run Process ${PYTHON} ${HELPER} event-queue cwd=${WORKSPACE} timeout=30s
Log ${result.stdout}
@@ -40,16 +40,16 @@ M6 ACP Event Queue Publish Subscribe
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} m6-event-queue-ok
M6 ACP Transport Stub Rejects All
[Documentation] Verify HTTP transport stub raises AcpNotAvailableError
M6 A2A Transport Stub Rejects All
[Documentation] Verify HTTP transport stub raises A2aNotAvailableError
${result}= Run Process ${PYTHON} ${HELPER} transport-stub cwd=${WORKSPACE} timeout=30s
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} m6-transport-stub-ok
M6 ACP Version Negotiation
[Documentation] Negotiate supported and unsupported ACP versions
M6 A2A Version Negotiation
[Documentation] Negotiate supported and unsupported A2A versions
${result}= Run Process ${PYTHON} ${HELPER} version-negotiation cwd=${WORKSPACE} timeout=30s
Log ${result.stdout}
Log ${result.stderr}
+5 -5
View File
@@ -1,7 +1,7 @@
*** Settings ***
Documentation Integration smoke tests for async resource cleanup (#321).
... Validates that AsyncResourceTracker, enhanced bridge cleanup,
... AcpEventQueue.close(), and StateManager.close() work end-to-end
... A2aEventQueue.close(), and StateManager.close() work end-to-end
... by executing small Python driver scripts in a subprocess.
Library OperatingSystem
@@ -78,14 +78,14 @@ Test AsyncResourceTracker Timeout Warning
${output} = Get File ${TEST_OUTPUT}
Should Contain ${output} PASS: timeout warning
Test AcpEventQueue Close
[Documentation] Verify AcpEventQueue.close() removes subscriptions
Test A2aEventQueue Close
[Documentation] Verify A2aEventQueue.close() removes subscriptions
${script} = Catenate SEPARATOR=\n
... import sys
... sys.path.insert(0, '${WORKSPACE_ROOT}/src')
... from cleveragents.acp.events import AcpEventQueue
... from cleveragents.a2a.events import A2aEventQueue
...
... q = AcpEventQueue()
... q = A2aEventQueue()
... q.subscribe_local(lambda e: None)
... q.subscribe_local(lambda e: None)
... assert len(q._subscriptions) == 2
+69
View File
@@ -0,0 +1,69 @@
"""A2A (Agent-to-Agent Protocol) integration package.
Provides the local-mode facade, server-mode transport stubs, request/response
models, event streaming stubs, and version negotiation for the A2A boundary.
In **local mode** the :class:`A2aLocalFacade` maps A2A operation names to
direct Python method calls on existing application services. No serialization,
no network, no authentication.
In **server mode** the :class:`A2aHttpTransport` is a stub that raises
:class:`A2aNotAvailableError` for every operation. When server mode is
implemented the concrete transport will replace these stubs.
Server client protocols (:class:`ServerClient`, :class:`RemoteExecutionClient`,
:class:`AuthClient`) and their stub implementations are provided for forward
compatibility. :class:`ServerConnectionConfig` validates connection parameters.
"""
from __future__ import annotations
from cleveragents.a2a.clients import (
AuthClient,
RemoteExecutionClient,
ServerClient,
StubAuthClient,
StubRemoteExecutionClient,
StubServerClient,
)
from cleveragents.a2a.errors import (
A2aError,
A2aNotAvailableError,
A2aOperationNotFoundError,
A2aVersionMismatchError,
)
from cleveragents.a2a.events import A2aEventQueue
from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.a2a.models import (
A2aErrorDetail,
A2aEvent,
A2aRequest,
A2aResponse,
A2aVersion,
)
from cleveragents.a2a.server_config import ServerConnectionConfig
from cleveragents.a2a.transport import A2aHttpTransport
from cleveragents.a2a.versioning import A2aVersionNegotiator
__all__ = [
"A2aError",
"A2aErrorDetail",
"A2aEvent",
"A2aEventQueue",
"A2aHttpTransport",
"A2aLocalFacade",
"A2aNotAvailableError",
"A2aOperationNotFoundError",
"A2aRequest",
"A2aResponse",
"A2aVersion",
"A2aVersionMismatchError",
"A2aVersionNegotiator",
"AuthClient",
"RemoteExecutionClient",
"ServerClient",
"ServerConnectionConfig",
"StubAuthClient",
"StubRemoteExecutionClient",
"StubServerClient",
]
@@ -1,11 +1,11 @@
"""ACP error hierarchy and domain-to-ACP error mapping.
"""A2A error hierarchy and domain-to-A2A error mapping.
All ACP-specific exceptions inherit from :class:`AcpError` which itself
All A2A-specific exceptions inherit from :class:`A2aError` which itself
extends the project-wide :class:`CleverAgentsError`. This mirrors the
pattern established by the LSP package.
The :func:`map_domain_error` function translates domain exceptions into
structured :class:`AcpErrorDetail` instances using a well-defined error
structured :class:`A2aErrorDetail` instances using a well-defined error
code taxonomy so that clients receive consistent, machine-readable error
responses regardless of which internal service produced the fault.
"""
@@ -26,7 +26,7 @@ from cleveragents.core.exceptions import (
)
# ---------------------------------------------------------------------------
# ACP error code constants
# A2A error code constants
# ---------------------------------------------------------------------------
NOT_FOUND: str = "NOT_FOUND"
@@ -44,8 +44,8 @@ INTERNAL_ERROR: str = "INTERNAL_ERROR"
# ---------------------------------------------------------------------------
class AcpError(CleverAgentsError):
"""Base exception for all ACP errors."""
class A2aError(CleverAgentsError):
"""Base exception for all A2A errors."""
def __init__(
self,
@@ -55,19 +55,19 @@ class AcpError(CleverAgentsError):
super().__init__(message, details)
class AcpNotAvailableError(AcpError):
class A2aNotAvailableError(A2aError):
"""Raised when a server-mode operation is attempted in local mode."""
def __init__(
self,
message: str = "ACP server transport is not available in local mode",
message: str = "A2A server transport is not available in local mode",
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message, details)
class AcpVersionMismatchError(AcpError):
"""Raised when client and server ACP versions are incompatible."""
class A2aVersionMismatchError(A2aError):
"""Raised when client and server A2A versions are incompatible."""
def __init__(
self,
@@ -81,8 +81,8 @@ class AcpVersionMismatchError(AcpError):
self.supported_versions = supported_versions
class AcpOperationNotFoundError(AcpError):
"""Raised when an unknown ACP operation is requested."""
class A2aOperationNotFoundError(A2aError):
"""Raised when an unknown A2A operation is requested."""
def __init__(
self,
@@ -95,15 +95,15 @@ class AcpOperationNotFoundError(AcpError):
# ---------------------------------------------------------------------------
# Domain-to-ACP error mapping
# Domain-to-A2A error mapping
# ---------------------------------------------------------------------------
def map_domain_error(exc: Exception) -> tuple[str, str]:
"""Map a domain exception to an ACP error code and message.
"""Map a domain exception to an A2A error code and message.
Returns a ``(code, message)`` tuple suitable for constructing an
:class:`~cleveragents.acp.models.AcpErrorDetail`.
:class:`~cleveragents.a2a.models.A2aErrorDetail`.
The mapping precedence is intentional: more specific exception types
are checked before their base classes so that, for example, a
@@ -148,9 +148,9 @@ __all__ = [
"NOT_FOUND",
"PLAN_ERROR",
"VALIDATION_ERROR",
"AcpError",
"AcpNotAvailableError",
"AcpOperationNotFoundError",
"AcpVersionMismatchError",
"A2aError",
"A2aNotAvailableError",
"A2aOperationNotFoundError",
"A2aVersionMismatchError",
"map_domain_error",
]
@@ -1,8 +1,8 @@
"""ACP event streaming — local queue and remote stub.
"""A2A event streaming — local queue and remote stub.
The :class:`AcpEventQueue` provides a working in-memory event queue for
The :class:`A2aEventQueue` provides a working in-memory event queue for
local mode and a stub for remote subscriptions that raises
:class:`AcpNotAvailableError`.
:class:`A2aNotAvailableError`.
"""
from __future__ import annotations
@@ -13,8 +13,8 @@ from typing import Any
import structlog
from ulid import ULID
from cleveragents.acp.errors import AcpNotAvailableError
from cleveragents.acp.models import AcpEvent
from cleveragents.a2a.errors import A2aNotAvailableError
from cleveragents.a2a.models import A2aEvent
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
@@ -24,12 +24,12 @@ _REMOTE_MSG = (
)
class AcpEventQueue:
class A2aEventQueue:
"""In-memory event queue with local pub/sub and remote stub."""
def __init__(self) -> None:
self._events: list[AcpEvent] = []
self._subscriptions: dict[str, Callable[[AcpEvent], Any]] = {}
self._events: list[A2aEvent] = []
self._subscriptions: dict[str, Callable[[A2aEvent], Any]] = {}
self._is_closed: bool = False
# ------------------------------------------------------------------
@@ -41,15 +41,15 @@ class AcpEventQueue:
"""Whether this queue has been closed."""
return self._is_closed
def publish(self, event: AcpEvent) -> None:
def publish(self, event: A2aEvent) -> None:
"""Append *event* to the local queue and notify subscribers."""
if self._is_closed:
raise RuntimeError("Cannot publish to a closed event queue")
if not isinstance(event, AcpEvent):
raise TypeError("event must be an AcpEvent instance")
if not isinstance(event, A2aEvent):
raise TypeError("event must be an A2aEvent instance")
self._events.append(event)
logger.debug(
"acp.event.published",
"a2a.event.published",
event_id=event.event_id,
event_type=event.event_type,
)
@@ -58,17 +58,17 @@ class AcpEventQueue:
callback(event)
except Exception:
logger.exception(
"acp.event.callback_error",
"a2a.event.callback_error",
subscription_id=sub_id,
)
def subscribe_local(self, callback: Callable[[AcpEvent], Any]) -> str:
def subscribe_local(self, callback: Callable[[A2aEvent], Any]) -> str:
"""Register a local callback and return a subscription ID."""
if not callable(callback):
raise TypeError("callback must be callable")
sub_id = str(ULID())
self._subscriptions[sub_id] = callback
logger.debug("acp.event.subscribed", subscription_id=sub_id)
logger.debug("a2a.event.subscribed", subscription_id=sub_id)
return sub_id
def unsubscribe(self, subscription_id: str) -> bool:
@@ -77,10 +77,10 @@ class AcpEventQueue:
raise ValueError("subscription_id must be a non-empty string")
removed = self._subscriptions.pop(subscription_id, None) is not None
if removed:
logger.debug("acp.event.unsubscribed", subscription_id=subscription_id)
logger.debug("a2a.event.unsubscribed", subscription_id=subscription_id)
return removed
def get_events(self, limit: int = 100) -> list[AcpEvent]:
def get_events(self, limit: int = 100) -> list[A2aEvent]:
"""Return the most recent *limit* events from the queue."""
if not isinstance(limit, int) or limit < 1:
raise ValueError("limit must be a positive integer")
@@ -97,7 +97,7 @@ class AcpEventQueue:
self._subscriptions.clear()
self._events.clear()
if count:
logger.info("acp.event_queue.closed", subscription_count=count)
logger.info("a2a.event_queue.closed", subscription_count=count)
# ------------------------------------------------------------------
# Remote stub (raises)
@@ -106,17 +106,17 @@ class AcpEventQueue:
def subscribe_remote(self, endpoint: str) -> None:
"""Subscribe to a remote event stream.
Always raises :class:`AcpNotAvailableError` in local mode.
Always raises :class:`A2aNotAvailableError` in local mode.
"""
if not endpoint or not isinstance(endpoint, str):
raise ValueError("endpoint must be a non-empty string")
logger.warning("%s (endpoint=%s)", _REMOTE_MSG, endpoint)
raise AcpNotAvailableError(
raise A2aNotAvailableError(
_REMOTE_MSG,
details={"endpoint": endpoint},
)
__all__ = [
"AcpEventQueue",
"A2aEventQueue",
]
@@ -1,6 +1,6 @@
"""ACP local-mode facade routing operations to application services.
"""A2A local-mode facade routing operations to application services.
In local mode every ACP operation maps 1:1 to a method call on the
In local mode every A2A operation maps 1:1 to a method call on the
appropriate application service. No serialization, network, or auth.
Service dependencies are supplied via the ``services`` dict passed to the
@@ -12,7 +12,7 @@ constructor. Expected keys:
| ``plan_lifecycle_service`` | ``PlanLifecycleService`` | plan.* operations |
| ``tool_registry`` | ``ToolRegistry`` | registry.list_tools |
| ``resource_registry_service``| ``ResourceRegistryService`` | registry.list_resources|
| ``event_queue`` | ``AcpEventQueue`` | event.subscribe |
| ``event_queue`` | ``A2aEventQueue`` | event.subscribe |
When a service is absent the handler falls back to a safe stub
response so the facade never crashes due to missing wiring.
@@ -26,19 +26,19 @@ from typing import TYPE_CHECKING, Any
import structlog
from ulid import ULID
from cleveragents.acp.errors import (
AcpOperationNotFoundError,
from cleveragents.a2a.errors import (
A2aOperationNotFoundError,
map_domain_error,
)
from cleveragents.acp.models import (
AcpErrorDetail,
AcpRequest,
AcpResponse,
AcpVersion,
from cleveragents.a2a.models import (
A2aErrorDetail,
A2aRequest,
A2aResponse,
A2aVersion,
)
if TYPE_CHECKING:
from cleveragents.acp.events import AcpEventQueue
from cleveragents.a2a.events import A2aEventQueue
from cleveragents.application.services.plan_lifecycle_service import (
PlanLifecycleService,
)
@@ -69,8 +69,8 @@ _SUPPORTED_OPERATIONS: list[str] = [
]
class AcpLocalFacade:
"""Local-mode facade that dispatches ACP operations to services.
class A2aLocalFacade:
"""Local-mode facade that dispatches A2A operations to services.
Accepts service dependencies via the ``services`` dict. Each handler
checks for the presence of the required service and delegates to it
@@ -82,7 +82,7 @@ class AcpLocalFacade:
raise TypeError("services must be a dict or None")
self._services: dict[str, Any] = dict(services) if services else {}
# PERF-1 fix: cache the handler dispatch dict instead of
# rebuilding it on every ACP request.
# rebuilding it on every A2A request.
self._handler_map: dict[str, Any] | None = None
# ------------------------------------------------------------------
@@ -108,57 +108,57 @@ class AcpLocalFacade:
return svc # type: ignore[return-value]
@property
def _event_queue(self) -> AcpEventQueue | None:
def _event_queue(self) -> A2aEventQueue | None:
return self._services.get("event_queue") # type: ignore[return-value]
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def dispatch(self, request: AcpRequest) -> AcpResponse:
"""Route an :class:`AcpRequest` to the appropriate handler.
def dispatch(self, request: A2aRequest) -> A2aResponse:
"""Route an :class:`A2aRequest` to the appropriate handler.
Returns an :class:`AcpResponse` with ``status='ok'`` on success or
Returns an :class:`A2aResponse` with ``status='ok'`` on success or
``status='error'`` when the operation fails. Domain exceptions
are mapped to ACP error codes via :func:`map_domain_error`.
are mapped to A2A error codes via :func:`map_domain_error`.
"""
if not isinstance(request, AcpRequest):
raise TypeError("request must be an AcpRequest instance")
if not isinstance(request, A2aRequest):
raise TypeError("request must be an A2aRequest instance")
start = time.monotonic()
try:
data = self._route_operation(request.operation, request.params)
elapsed = (time.monotonic() - start) * 1000.0
logger.info(
"acp.local.dispatch",
"a2a.local.dispatch",
operation=request.operation,
request_id=request.request_id,
timing_ms=round(elapsed, 2),
)
return AcpResponse(
acp_version=AcpVersion.CURRENT,
return A2aResponse(
a2a_version=A2aVersion.CURRENT,
request_id=request.request_id,
status="ok",
data=data,
timing_ms=round(elapsed, 2),
)
except AcpOperationNotFoundError:
except A2aOperationNotFoundError:
raise
except Exception as exc:
elapsed = (time.monotonic() - start) * 1000.0
code, message = map_domain_error(exc)
logger.error(
"acp.local.dispatch.error",
"a2a.local.dispatch.error",
operation=request.operation,
request_id=request.request_id,
error_code=code,
error=message,
)
return AcpResponse(
acp_version=AcpVersion.CURRENT,
return A2aResponse(
a2a_version=A2aVersion.CURRENT,
request_id=request.request_id,
status="error",
error=AcpErrorDetail(
error=A2aErrorDetail(
code=code,
message=message,
),
@@ -173,10 +173,10 @@ class AcpLocalFacade:
# PERF-1 fix: invalidate cached handler map so new service
# wiring is picked up on the next dispatch.
self._handler_map = None
logger.debug("acp.local.service_registered", service_name=name)
logger.debug("a2a.local.service_registered", service_name=name)
def list_operations(self) -> list[str]:
"""Return the list of supported ACP operation names."""
"""Return the list of supported A2A operation names."""
return list(_SUPPORTED_OPERATIONS)
# ------------------------------------------------------------------
@@ -191,8 +191,8 @@ class AcpLocalFacade:
"""Map *operation* to a handler and return the result dict."""
handler = self._handlers().get(operation)
if handler is None:
raise AcpOperationNotFoundError(
message=f"Unknown ACP operation: {operation}",
raise A2aOperationNotFoundError(
message=f"Unknown A2A operation: {operation}",
operation=operation,
)
return handler(params)
@@ -267,13 +267,13 @@ class AcpLocalFacade:
)
if stopped:
logger.info(
"acp.session.close.devcontainer_cleanup",
"a2a.session.close.devcontainer_cleanup",
session_id=session_id,
stopped_count=len(stopped),
)
except Exception:
logger.warning(
"acp.session.close.devcontainer_cleanup_failed",
"a2a.session.close.devcontainer_cleanup_failed",
session_id=session_id,
exc_info=True,
)
@@ -406,5 +406,5 @@ class AcpLocalFacade:
__all__ = [
"AcpLocalFacade",
"A2aLocalFacade",
]
@@ -1,6 +1,6 @@
"""ACP request/response envelope models.
"""A2A request/response envelope models.
Pydantic v2 models for the ACP wire format. In local mode these are
Pydantic v2 models for the A2A wire format. In local mode these are
used purely as validated data containers no serialization to JSON
actually occurs over a network.
"""
@@ -18,8 +18,8 @@ from ulid import ULID
# ---------------------------------------------------------------------------
class AcpVersion:
"""ACP protocol version constants."""
class A2aVersion:
"""A2A protocol version constants."""
CURRENT: str = "1.0"
SUPPORTED: tuple[str, ...] = ("1.0",)
@@ -45,8 +45,8 @@ def _iso_now_factory() -> str:
# ---------------------------------------------------------------------------
class AcpErrorDetail(BaseModel):
"""Structured error payload inside an :class:`AcpResponse`."""
class A2aErrorDetail(BaseModel):
"""Structured error payload inside an :class:`A2aResponse`."""
model_config = ConfigDict(strict=False)
@@ -62,12 +62,12 @@ class AcpErrorDetail(BaseModel):
return value
class AcpRequest(BaseModel):
"""Inbound ACP operation envelope."""
class A2aRequest(BaseModel):
"""Inbound A2A operation envelope."""
model_config = ConfigDict(strict=False)
acp_version: str = AcpVersion.CURRENT
a2a_version: str = A2aVersion.CURRENT
request_id: str = ""
operation: str
params: dict[str, Any] = {}
@@ -81,22 +81,22 @@ class AcpRequest(BaseModel):
return value
@model_validator(mode="after")
def _default_request_id(self) -> AcpRequest:
def _default_request_id(self) -> A2aRequest:
if not self.request_id:
self.request_id = _ulid_factory()
return self
class AcpResponse(BaseModel):
"""Outbound ACP result envelope."""
class A2aResponse(BaseModel):
"""Outbound A2A result envelope."""
model_config = ConfigDict(strict=False)
acp_version: str = AcpVersion.CURRENT
a2a_version: str = A2aVersion.CURRENT
request_id: str
status: str
data: dict[str, Any] = {}
error: AcpErrorDetail | None = None
error: A2aErrorDetail | None = None
timing_ms: float | None = None
@field_validator("status")
@@ -107,7 +107,7 @@ class AcpResponse(BaseModel):
return value
class AcpEvent(BaseModel):
class A2aEvent(BaseModel):
"""Server-sent event envelope for plan progress and streaming."""
model_config = ConfigDict(strict=False)
@@ -126,7 +126,7 @@ class AcpEvent(BaseModel):
return value
@model_validator(mode="after")
def _set_defaults(self) -> AcpEvent:
def _set_defaults(self) -> A2aEvent:
if not self.event_id:
self.event_id = _ulid_factory()
if not self.timestamp:
@@ -135,9 +135,9 @@ class AcpEvent(BaseModel):
__all__ = [
"AcpErrorDetail",
"AcpEvent",
"AcpRequest",
"AcpResponse",
"AcpVersion",
"A2aErrorDetail",
"A2aEvent",
"A2aRequest",
"A2aResponse",
"A2aVersion",
]
@@ -1,6 +1,6 @@
"""ACP server-mode HTTP transport stub.
"""A2A server-mode HTTP transport stub.
Every method raises :class:`AcpNotAvailableError`. When server mode is
Every method raises :class:`A2aNotAvailableError`. When server mode is
implemented (separate project) the concrete transport will replace this stub.
"""
@@ -8,57 +8,57 @@ from __future__ import annotations
import structlog
from cleveragents.acp.errors import AcpNotAvailableError
from cleveragents.acp.models import AcpRequest, AcpResponse
from cleveragents.a2a.errors import A2aNotAvailableError
from cleveragents.a2a.models import A2aRequest, A2aResponse
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
_SERVER_MODE_MSG = (
"ACP HTTP transport is not available in local mode"
"A2A HTTP transport is not available in local mode"
" - server mode will be implemented as a separate project"
)
class AcpHttpTransport:
class A2aHttpTransport:
"""Stub HTTP transport — all mutating methods raise on invocation."""
def send(self, request: AcpRequest) -> AcpResponse:
"""Send an ACP request over HTTP.
def send(self, request: A2aRequest) -> A2aResponse:
"""Send an A2A request over HTTP.
Always raises :class:`AcpNotAvailableError` in local mode.
Always raises :class:`A2aNotAvailableError` in local mode.
"""
if not isinstance(request, AcpRequest):
raise TypeError("request must be an AcpRequest instance")
if not isinstance(request, A2aRequest):
raise TypeError("request must be an A2aRequest instance")
logger.warning(
"%s (operation=%s)",
_SERVER_MODE_MSG,
request.operation,
)
raise AcpNotAvailableError(
raise A2aNotAvailableError(
_SERVER_MODE_MSG,
details={"operation": request.operation},
)
def connect(self, base_url: str) -> None:
"""Open a connection to the ACP server.
"""Open a connection to the A2A server.
Always raises :class:`AcpNotAvailableError` in local mode.
Always raises :class:`A2aNotAvailableError` in local mode.
"""
if not base_url or not isinstance(base_url, str):
raise ValueError("base_url must be a non-empty string")
logger.warning("%s (url=%s)", _SERVER_MODE_MSG, base_url)
raise AcpNotAvailableError(
raise A2aNotAvailableError(
_SERVER_MODE_MSG,
details={"base_url": base_url},
)
def disconnect(self) -> None:
"""Close the connection to the ACP server.
"""Close the connection to the A2A server.
Always raises :class:`AcpNotAvailableError` in local mode.
Always raises :class:`A2aNotAvailableError` in local mode.
"""
logger.warning(_SERVER_MODE_MSG)
raise AcpNotAvailableError(_SERVER_MODE_MSG)
raise A2aNotAvailableError(_SERVER_MODE_MSG)
def is_connected(self) -> bool:
"""Return connection status. Always ``False`` in local mode."""
@@ -66,5 +66,5 @@ class AcpHttpTransport:
__all__ = [
"AcpHttpTransport",
"A2aHttpTransport",
]
@@ -1,7 +1,7 @@
"""ACP version negotiation.
"""A2A version negotiation.
Ensures that a requested ACP version is within the set of supported
versions. Raises :class:`AcpVersionMismatchError` when the requested
Ensures that a requested A2A version is within the set of supported
versions. Raises :class:`A2aVersionMismatchError` when the requested
version is not supported.
"""
@@ -9,13 +9,13 @@ from __future__ import annotations
import structlog
from cleveragents.acp.errors import AcpVersionMismatchError
from cleveragents.a2a.errors import A2aVersionMismatchError
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
class AcpVersionNegotiator:
"""Negotiates ACP protocol versions."""
class A2aVersionNegotiator:
"""Negotiates A2A protocol versions."""
CURRENT_VERSION: str = "1.0"
SUPPORTED_VERSIONS: tuple[str, ...] = ("1.0",)
@@ -25,16 +25,16 @@ class AcpVersionNegotiator:
Raises:
ValueError: If *requested* is empty.
AcpVersionMismatchError: If version is not in
A2aVersionMismatchError: If version is not in
:attr:`SUPPORTED_VERSIONS`.
"""
if not requested or not isinstance(requested, str):
raise ValueError("requested version must be a non-empty string")
if requested in self.SUPPORTED_VERSIONS:
logger.debug("acp.version.negotiated", version=requested)
logger.debug("a2a.version.negotiated", version=requested)
return requested
raise AcpVersionMismatchError(
message=f"ACP version '{requested}' is not supported",
raise A2aVersionMismatchError(
message=f"A2A version '{requested}' is not supported",
requested_version=requested,
supported_versions=list(self.SUPPORTED_VERSIONS),
)
@@ -46,10 +46,10 @@ class AcpVersionNegotiator:
return version in self.SUPPORTED_VERSIONS
def get_current(self) -> str:
"""Return the current ACP protocol version."""
"""Return the current A2A protocol version."""
return self.CURRENT_VERSION
__all__ = [
"AcpVersionNegotiator",
"A2aVersionNegotiator",
]
-69
View File
@@ -1,69 +0,0 @@
"""ACP (Agent Communication Protocol) integration package.
Provides the local-mode facade, server-mode transport stubs, request/response
models, event streaming stubs, and version negotiation for the ACP boundary.
In **local mode** the :class:`AcpLocalFacade` maps ACP operation names to
direct Python method calls on existing application services. No serialization,
no network, no authentication.
In **server mode** the :class:`AcpHttpTransport` is a stub that raises
:class:`AcpNotAvailableError` for every operation. When server mode is
implemented the concrete transport will replace these stubs.
Server client protocols (:class:`ServerClient`, :class:`RemoteExecutionClient`,
:class:`AuthClient`) and their stub implementations are provided for forward
compatibility. :class:`ServerConnectionConfig` validates connection parameters.
"""
from __future__ import annotations
from cleveragents.acp.clients import (
AuthClient,
RemoteExecutionClient,
ServerClient,
StubAuthClient,
StubRemoteExecutionClient,
StubServerClient,
)
from cleveragents.acp.errors import (
AcpError,
AcpNotAvailableError,
AcpOperationNotFoundError,
AcpVersionMismatchError,
)
from cleveragents.acp.events import AcpEventQueue
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.acp.models import (
AcpErrorDetail,
AcpEvent,
AcpRequest,
AcpResponse,
AcpVersion,
)
from cleveragents.acp.server_config import ServerConnectionConfig
from cleveragents.acp.transport import AcpHttpTransport
from cleveragents.acp.versioning import AcpVersionNegotiator
__all__ = [
"AcpError",
"AcpErrorDetail",
"AcpEvent",
"AcpEventQueue",
"AcpHttpTransport",
"AcpLocalFacade",
"AcpNotAvailableError",
"AcpOperationNotFoundError",
"AcpRequest",
"AcpResponse",
"AcpVersion",
"AcpVersionMismatchError",
"AcpVersionNegotiator",
"AuthClient",
"RemoteExecutionClient",
"ServerClient",
"ServerConnectionConfig",
"StubAuthClient",
"StubRemoteExecutionClient",
"StubServerClient",
]
+2 -2
View File
@@ -394,8 +394,8 @@ def serve(
" All other methods → MethodNotFound (-32601)"
)
# Lazy import: LspServer transitively imports AcpLocalFacade
# which pulls the full ACP dependency chain (~200ms cold).
# Lazy import: LspServer transitively imports A2aLocalFacade
# which pulls the full A2A dependency chain (~200ms cold).
# Non-serve subcommands (add, remove, list, show) do not
# need LspServer, so deferring the import avoids that cost
# for the common case.
+1 -1
View File
@@ -15,7 +15,7 @@ from pydantic import ValidationError
from rich.console import Console
from rich.panel import Panel
from cleveragents.acp.server_config import ServerConnectionConfig
from cleveragents.a2a.server_config import ServerConnectionConfig
from cleveragents.application.services.config_service import ConfigService
from cleveragents.cli.formatting import OutputFormat, format_output
+11 -11
View File
@@ -5,8 +5,8 @@ Content-Length header framing. Supports the ``initialize``,
``shutdown``, and ``exit`` lifecycle methods. All other LSP
methods return a JSON-RPC ``MethodNotFound`` (-32601) error.
An :class:`AcpLocalFacade` instance is stored so that the
dispatch path can be wired through ACP when server mode lands.
An :class:`A2aLocalFacade` instance is stored so that the
dispatch path can be wired through A2A when server mode lands.
In the current stub the facade is a placeholder all methods
are handled by hardcoded stub handlers.
@@ -26,7 +26,7 @@ from typing import Any, BinaryIO
import structlog
from cleveragents.acp.facade import AcpLocalFacade
from cleveragents.a2a.facade import A2aLocalFacade
logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)
@@ -99,11 +99,11 @@ class LspServer:
The server reads Content-Length framed JSON-RPC messages from
*input_stream* and writes responses to *output_stream*. LSP
requests are routed through an :class:`AcpLocalFacade` instance
requests are routed through an :class:`A2aLocalFacade` instance
in local mode.
Attributes:
_facade: ACP local-mode facade for request routing.
_facade: A2A local-mode facade for request routing.
_input: Binary stream to read from (default: ``sys.stdin.buffer``).
_output: Binary stream to write to (default: ``sys.stdout.buffer``).
_running: Whether the event loop is active.
@@ -115,7 +115,7 @@ class LspServer:
self,
input_stream: BinaryIO | None = None,
output_stream: BinaryIO | None = None,
facade: AcpLocalFacade | None = None,
facade: A2aLocalFacade | None = None,
) -> None:
if input_stream is not None and not hasattr(input_stream, "read"):
raise TypeError("input_stream must support read()")
@@ -123,22 +123,22 @@ class LspServer:
raise TypeError("output_stream must support write()")
self._input: BinaryIO = input_stream or sys.stdin.buffer
self._output: BinaryIO = output_stream or sys.stdout.buffer
self._facade: AcpLocalFacade | None = facade
self._facade: A2aLocalFacade | None = facade
self._running: bool = False
self._initialized: bool = False
self._shutdown_requested: bool = False
@property
def facade(self) -> AcpLocalFacade:
"""Return the ACP facade, creating it lazily on first access.
def facade(self) -> A2aLocalFacade:
"""Return the A2A facade, creating it lazily on first access.
The facade is stored for future server-mode wiring but is
not called in the current stub. Lazy creation avoids the
overhead of ``AcpLocalFacade()`` construction when the
overhead of ``A2aLocalFacade()`` construction when the
facade is never needed (e.g., benchmarks).
"""
if self._facade is None:
self._facade = AcpLocalFacade()
self._facade = A2aLocalFacade()
return self._facade
@property