From 4efdc2729296bc9c80de37308aa9244d09b9f484 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 3 May 2026 21:21:55 -0400 Subject: [PATCH 1/2] feat(decisioning): advertise_all kwarg + handler.get_advertised_tools (#519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces ``advertise_all`` on ``create_adcp_server_from_platform`` for parity with ``serve()``, and adds a ``handler.get_advertised_tools()`` inspection method that returns the same set ``tools/list`` would emit at serve time — without standing up a network port. Salesagent migration feedback #14: a fresh ``MockSellerPlatform`` (5 methods) shows ~50 entries in ``handler.advertised_tools`` (the class-level universe) but only ~10 actually advertise on the wire after the per-instance specialism filter. The new method makes the effective set readable from the handler instance directly, with the ``advertise_all`` flag controllable at factory time and overridable per-call. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/adcp/decisioning/handler.py | 37 ++++++++++++++++++ src/adcp/decisioning/serve.py | 12 ++++++ tests/test_decisioning_serve.py | 67 +++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/src/adcp/decisioning/handler.py b/src/adcp/decisioning/handler.py index 33be0cbc1..e43bb66bf 100644 --- a/src/adcp/decisioning/handler.py +++ b/src/adcp/decisioning/handler.py @@ -695,6 +695,41 @@ def advertised_tools_for_instance(self) -> frozenset[str]: serving |= set(tools) return frozenset(serving) + def get_advertised_tools(self, *, advertise_all: bool | None = None) -> frozenset[str]: + """Names ``tools/list`` will return when this handler is served. + + The class-level :attr:`advertised_tools` set is the *universe* + of tools the handler base supports across all specialisms (~50 + entries on :class:`PlatformHandler`). What buyers actually see + on the wire is narrower: + + 1. Per-instance specialism filter — :meth:`advertised_tools_for_instance` + intersects the universe with the platform's claimed + specialisms (a sales-only adopter drops audience/governance + tools). + 2. Override-detection filter — tools whose handler method is + still the SDK's ``not_supported`` default are dropped + (``advertise_all=False``, the spec-aligned default). + + This method runs the same pipeline :func:`adcp.server.serve` + runs at boot, so adopters can inspect the effective set without + standing up a network port. The default ``advertise_all`` value + is whatever was configured on + :func:`adcp.decisioning.create_adcp_server_from_platform` + (``False`` when not set). + + :param advertise_all: Override the configured value for this + call. ``True`` returns the per-specialism set without the + override filter; ``False`` applies the full filter. + :returns: Frozen set of tool names. + """ + from adcp.server.mcp_tools import get_tools_for_handler + + effective = self._advertise_all if advertise_all is None else advertise_all + return frozenset( + tool["name"] for tool in get_tools_for_handler(self, advertise_all=effective) + ) + def __init__( self, platform: DecisioningPlatform, @@ -709,6 +744,7 @@ def __init__( buyer_agent_registry: BuyerAgentRegistry | None = None, config_store: ProductConfigStore | None = None, property_list_fetcher: PropertyListFetcher | None = None, + advertise_all: bool = False, ) -> None: super().__init__() self._platform = platform @@ -722,6 +758,7 @@ def __init__( self._buyer_agent_registry = buyer_agent_registry self._config_store = config_store self._property_list_fetcher = property_list_fetcher + self._advertise_all = advertise_all # Cache whether the platform's create_media_buy accepts 'configs' # so we only pay the inspect.signature cost at construction time. diff --git a/src/adcp/decisioning/serve.py b/src/adcp/decisioning/serve.py index b32503a10..93a90cfa0 100644 --- a/src/adcp/decisioning/serve.py +++ b/src/adcp/decisioning/serve.py @@ -87,6 +87,7 @@ def create_adcp_server_from_platform( buyer_agent_registry: BuyerAgentRegistry | None = None, config_store: ProductConfigStore | None = None, property_list_fetcher: PropertyListFetcher | None = None, + advertise_all: bool = False, ) -> tuple[PlatformHandler, ThreadPoolExecutor, TaskRegistry]: """Build the :class:`PlatformHandler` + supporting wiring from a :class:`DecisioningPlatform`. @@ -176,6 +177,15 @@ def create_adcp_server_from_platform( (avoid duplicate delivery; idempotency-key dedup at the receiver would handle it but explicit suppression matches the v5 manual-emit posture for adopters mid-migration). + :param advertise_all: Mirror of the same flag on :func:`serve` — + controls how :meth:`PlatformHandler.get_advertised_tools` and + the eventual ``tools/list`` response filter the handler's tool + universe. ``False`` (default, spec-aligned) drops tools whose + method is still the SDK's ``not_supported`` shim; ``True`` + advertises every tool the platform's claimed specialisms cover + regardless of override status. Stored on the returned handler + so adopters can call ``handler.get_advertised_tools()`` to + inspect the effective set without standing up a server. To wire a :class:`ProposalManager` (v1 two-platform composition), pass it on a :class:`PlatformRouter` via @@ -286,6 +296,7 @@ def create_adcp_server_from_platform( buyer_agent_registry=buyer_agent_registry, config_store=config_store, property_list_fetcher=property_list_fetcher, + advertise_all=advertise_all, ) # Boot-time fail-fast: property_list_filtering declared but no fetcher wired. @@ -428,6 +439,7 @@ def serve( buyer_agent_registry=buyer_agent_registry, config_store=config_store, property_list_fetcher=property_list_fetcher, + advertise_all=advertise_all, ) # Phase 1 sandbox-authority — wire the comply controller's account diff --git a/tests/test_decisioning_serve.py b/tests/test_decisioning_serve.py index c68f773ea..2d67e45d9 100644 --- a/tests/test_decisioning_serve.py +++ b/tests/test_decisioning_serve.py @@ -504,3 +504,70 @@ def test_serve_does_not_fire_gate_for_platform_without_webhook_eligible_tools() handler, executor, _ = create_adcp_server_from_platform(platform) assert handler._webhook_sender is None executor.shutdown(wait=True) + + +# ---- advertise_all kwarg + get_advertised_tools method ---- + + +def test_get_advertised_tools_filters_to_claimed_specialisms() -> None: + """``handler.get_advertised_tools()`` returns the effective set + ``serve()`` would advertise — per-instance specialism filter + + protocol/discovery always-ons. Materially smaller than the + handler's class-level tool universe.""" + platform = _SalesPlatformWithRequiredMethods() + handler, executor, _ = create_adcp_server_from_platform( + platform, auto_emit_completion_webhooks=False + ) + advertised = handler.get_advertised_tools() + # The five overridden sales methods appear. + for tool in ( + "get_products", + "create_media_buy", + "update_media_buy", + "sync_creatives", + "get_media_buy_delivery", + ): + assert tool in advertised + # ``get_adcp_capabilities`` is always-on (protocol discovery). + assert "get_adcp_capabilities" in advertised + # Tools from specialisms the platform didn't claim are filtered out. + assert "build_creative" not in advertised # creative-builder + assert "acquire_rights" not in advertised # brand-rights + # Effective set is materially smaller than the class-level universe. + # (~10 vs ~40 for a sales-only platform.) + assert len(advertised) < len(type(handler).advertised_tools) + executor.shutdown(wait=True) + + +def test_get_advertised_tools_returns_frozenset() -> None: + """API guarantee: ``get_advertised_tools()`` returns a frozenset so + callers can intersect/union with other sets without worrying about + mutation.""" + platform = _SalesPlatformWithRequiredMethods() + handler, executor, _ = create_adcp_server_from_platform( + platform, auto_emit_completion_webhooks=False + ) + advertised = handler.get_advertised_tools() + assert isinstance(advertised, frozenset) + executor.shutdown(wait=True) + + +def test_create_adcp_server_from_platform_stores_advertise_all_on_handler() -> None: + """``advertise_all=True`` on the factory threads to the handler's + configured default for :meth:`get_advertised_tools`.""" + platform = _SalesPlatformWithRequiredMethods() + handler, executor, _ = create_adcp_server_from_platform( + platform, advertise_all=True, auto_emit_completion_webhooks=False + ) + assert handler._advertise_all is True + executor.shutdown(wait=True) + + +def test_create_adcp_server_from_platform_advertise_all_default_false() -> None: + """``advertise_all`` defaults to False, matching :func:`serve`.""" + platform = _SalesPlatformWithRequiredMethods() + handler, executor, _ = create_adcp_server_from_platform( + platform, auto_emit_completion_webhooks=False + ) + assert handler._advertise_all is False + executor.shutdown(wait=True) From b1bc90f8633bd1e4dd7c743770850d589f257d7b Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 3 May 2026 21:24:17 -0400 Subject: [PATCH 2/2] test: cover get_advertised_tools per-call override Adds a test that constructs a handler with advertise_all=False and verifies passing advertise_all=True on the method call returns a wider set than the call without the override. Closes the only branch in the method body not previously covered. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_decisioning_serve.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_decisioning_serve.py b/tests/test_decisioning_serve.py index 2d67e45d9..8897795eb 100644 --- a/tests/test_decisioning_serve.py +++ b/tests/test_decisioning_serve.py @@ -534,11 +534,24 @@ def test_get_advertised_tools_filters_to_claimed_specialisms() -> None: assert "build_creative" not in advertised # creative-builder assert "acquire_rights" not in advertised # brand-rights # Effective set is materially smaller than the class-level universe. - # (~10 vs ~40 for a sales-only platform.) assert len(advertised) < len(type(handler).advertised_tools) executor.shutdown(wait=True) +def test_get_advertised_tools_per_call_override_wins_over_configured_default() -> None: + """The ``advertise_all`` kwarg on the method overrides the value + configured at factory time. Lets adopters inspect both modes from + a single handler.""" + platform = _SalesPlatformWithRequiredMethods() + handler, executor, _ = create_adcp_server_from_platform( + platform, advertise_all=False, auto_emit_completion_webhooks=False + ) + forced_universe = handler.get_advertised_tools(advertise_all=True) + forced_filtered = handler.get_advertised_tools(advertise_all=False) + assert forced_universe >= forced_filtered + executor.shutdown(wait=True) + + def test_get_advertised_tools_returns_frozenset() -> None: """API guarantee: ``get_advertised_tools()`` returns a frozenset so callers can intersect/union with other sets without worrying about