Skip to content

feat(decisioning): comprehensive Emma DX follow-up (P0/P1/P2 + examples)#339

Merged
bokelley merged 5 commits intomainfrom
bokelley/decisioning-comprehensive-fixes
May 1, 2026
Merged

feat(decisioning): comprehensive Emma DX follow-up (P0/P1/P2 + examples)#339
bokelley merged 5 commits intomainfrom
bokelley/decisioning-comprehensive-fixes

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 1, 2026

Summary

Addresses the cross-cutting findings from four Emma backend tests
(sales-direct 2/10, AudioStack 6/10, Stability AI 5/10, Signals 8/10).
Sequenced per adtech-product-expert's recommendation: framework
fixes first, examples last, every commit independently bisectable.

Tier A — Framework P0/P1

A1: Per-specialism tools/list filter (3 of 4 reports flagged this — biggest DX issue)

  • PlatformHandler.advertised_tools_for_instance() intersects the universe of shim coverage with the platform's claimed specialisms via SPECIALISM_TO_ADVERTISED_TOOLS.
  • get_tools_for_handler calls the hook on instances; class-level inspection preserves full universe.
  • Empty per-instance set (novel slug) falls back to class-level — preserves the existing warnings.warn(novel) forward-compat semantic.
  • 9 new tests: drift guards + per-specialism leak guards (sales / signals / creative / hybrid) + advertise_all interaction.

A2: INTERNAL_ERROR wire breadcrumb (Emma AudioStack P2)

  • details.caused_by = {type, message} on the wire envelope when the framework wraps a non-AdcpError exception. Class name + truncated str (200 char cap). No traceback, no module path, no chained __cause__.
  • Defense-in-depth: 200-char truncation against an adopter who throws on secret material with a sloppy repr.
  • Applied to all three wrap sites (sync method, non-projected TypeError, handoff fn).

A3: Boot-time webhook_sender fail-fast (Emma F12 P1)

  • When platform claims any specialism whose tool surface includes a SPEC_WEBHOOK_TASK_TYPES tool AND webhook_sender=None AND auto_emit on, fail at serve() boot. Same posture as validate_platform's governance opt-in gate.
  • Uses per-instance advertised set so test fixtures with no claimed specialisms aren't tripped.

Tier C — Examples (Tier B was a no-op once *Asset collision risk surfaced)

8 new per-Protocol-family templates: creative / signals / audience / governance / brand_rights / content_standards / property_lists / collection_lists. Each <100 lines, runs standalone via serve(), uses canonical type names. The creative example calls out the v4.0 *Content vs *Asset rename that AudioStack/Stability tripped on.

8 new smoke tests boot each example via PlatformHandler and verify the per-specialism filter narrows correctly.

Deferred to follow-up PRs

Per adtech-product-expert's bundle-order recommendation, separable concerns:

  • Port-3001 EADDRINUSE friendly remediation (_bind_reusable_socket)
  • /mcp vs /mcp/ 307 redirect handling
  • ImageAsset discriminated-union error narrowing (60-line pydantic dump → focused)

*Asset legacy re-exports were attempted then reverted — test_asset_aliases_stable.py is enforcing a deliberate v4.0 design (the *Asset names collided with *FormatAsset slot types). Right call: keep the canonical *Content names, document them in the creative example's AudioContent callout.

Test plan

  • CI green on Python 3.10–3.13
  • 2854 tests pass (was 2832 baseline → +22 new)
  • 17 new tests cover: per-specialism filter (9), INTERNAL_ERROR breadcrumb (2), F12 boot gate (4), per-example smoke (8)
  • Existing 152 decisioning tests still green
  • Existing test_asset_aliases_stable.py still passing (no *Asset re-exports added)

🤖 Generated with Claude Code

bokelley and others added 5 commits May 1, 2026 10:20
…ng P1)

Three independent Emma backend tests (sales-direct, AudioStack/creative,
signals-marketplace, Stability AI/creative) all flagged the same bug:
``tools/list`` advertises 40+ tools regardless of the platform's
claimed specialisms. A sales-only adopter saw ``acquire_rights``,
``build_creative``, ``check_governance``; on every call buyers got
NOT_SUPPORTED. The override-detection filter
(``_is_method_overridden``) walks ``PlatformHandler.__mro__`` and
finds the class concretely defines all 40+ shims — every tool shows
as "implemented" regardless of what the underlying platform claims.

Fix: add ``advertised_tools_for_instance(self) -> frozenset[str]`` on
PlatformHandler. The framework's ``get_tools_for_handler`` checks for
this hook on instances and intersects the candidate set with the
per-instance result BEFORE the override-detection filter. The hook
maps each claimed specialism to its per-Protocol-family advertised
set via ``SPECIALISM_TO_ADVERTISED_TOOLS``.

Empty per-instance set (novel specialism slug not in the map) falls
back to the class-level universe — muting the handler entirely on a
forward-compat slug would be worse than over-advertising. Static
inspection by class also keeps the full universe so storyboard tests
and spec-conformance docs aren't disrupted.

Tests: 9 new (drift guards, per-specialism leak guards for
sales/signals/creative/hybrid, novel-specialism fallback,
advertise_all interaction, class-level inspection).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… P2)

Adopters debugging "Error executing tool X: AdcpError[INTERNAL_ERROR /
terminal]: An internal error occurred" had no wire-side breadcrumb —
they had to grep server logs to even see which exception class fired.
The Emma AudioStack backend test (verdict 6/10) explicitly flagged
this: "the wire side An internal error occurred is a dead end."

Add ``details.caused_by`` to the wire envelope when the framework
wraps a non-AdcpError exception:

  {
    "code": "INTERNAL_ERROR",
    "message": "Platform method 'build_creative' raised AttributeError; see details for cause",
    "recovery": "terminal",
    "details": {
      "caused_by": {
        "type": "AttributeError",
        "message": "'dict' object has no attribute 'message'"
      }
    }
  }

Exposes class name + truncated str (200 char cap) — no traceback, no
module path, no chained __cause__. Full repr stays in server logs via
``logger.exception``. Truncation is defense-in-depth against an
adopter who throws on secret material with a sloppy repr; the cap
prevents secret-shaped values from landing on the wire.

Applied to all three INTERNAL_ERROR wrap sites (sync method,
non-projected TypeError, handoff fn). Drift guard: a unit test
verifies the truncation cap matches the constant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adopters claiming any specialism whose tool surface includes a
spec-eligible webhook task type (``create_media_buy``, ``activate_signal``,
``acquire_rights``, etc.) but who skip ``webhook_sender`` would have
every buyer-registered ``push_notification_config.url`` silently
dropped. PR #338 added a runtime WARNING on first call; this commit
adds the boot-time fail-fast that adtech-product-expert called for —
"the same posture as ``validate_platform``'s governance opt-in gate."

``adcp.decisioning.serve.create_adcp_server_from_platform`` now
calls ``validate_webhook_sender_for_platform`` after the handler is
constructed. Uses the per-instance advertised set (NOT the class-level
universe), so test fixtures with no claimed specialisms — and
discovery-only agents — don't accidentally trip the gate.

Adopter remediation paths surfaced in the error:
* Wire a configured ``WebhookSender``.
* Or set ``auto_emit_completion_webhooks=False`` if handling
  webhooks manually.

Tests: 4 new gate behaviors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every Emma backend test (sales 2/10, AudioStack 6/10, Signals 8/10,
Stability 5/10) flagged "no example for my specialism" as P1
friction. Adopters writing creative/signals/audience/governance/etc.
agents had only ``hello_seller.py`` (sales-non-guaranteed) plus the
Protocol's 170-line docstring — adtech-product-expert called this the
"highest-leverage follow-up after tools/list."

Eight new example files, one per non-sales Protocol family:

* ``hello_seller_creative.py`` — CreativeBuilderPlatform. Bare
  CreativeManifest projection, AudioStack/Stability shape.
* ``hello_seller_signals.py`` — SignalsPlatform. Catalog + sync
  activate + TaskHandoff template.
* ``hello_seller_audience.py`` — AudiencePlatform. Demonstrates
  arg-projection ergonomics.
* ``hello_seller_governance.py`` — CampaignGovernancePlatform.
  governance_aware=True opt-in + 4 required methods.
* ``hello_seller_brand_rights.py`` — BrandRightsPlatform. 4-arm
  acquire_rights discriminated union.
* ``hello_seller_content_standards.py`` — ContentStandardsPlatform.
  6 required + optional UNSUPPORTED_FEATURE gating.
* ``hello_seller_property_lists.py`` — PropertyListsPlatform.
  In-memory CRUD + fetch-token + security-critical delete.
* ``hello_seller_collection_lists.py`` — CollectionListsPlatform.
  Mirror of property-lists for collections.

Each example fits in <100 lines, runs standalone, uses canonical
type names (``CreativeManifest``, ``AudioContent``,
``FormatReferenceStructuredObject``). The ``AudioContent`` callout
in the creative example documents the v4.0 payload/slot naming
split that Emma's AudioStack adopter tripped on.

Tests: 8 new — boot each example via PlatformHandler and verify
``advertised_tools_for_instance()`` narrows to the specialism's tools
without leaking to other Protocol families.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-reviewer + adtech-product-expert second-pass on PR #339. Three
P0/P1 findings + the deferred port-collision and /mcp redirect work
folded into the comprehensive bundle.

P0 fix (code-reviewer):
- All 10 hello_seller_*.py examples called ``serve()`` without
  ``webhook_sender``. The new A3 boot-gate (this PR) rejects them
  because their advertised tools are in SPEC_WEBHOOK_TASK_TYPES.
  Run instructions in each example crashed at boot. Fix: pass
  ``auto_emit_completion_webhooks=False`` with a comment teaching
  adopters where to wire ``webhook_sender=`` in production. New
  smoke test ``test_example_boots_via_create_adcp_server_from_platform``
  catches this regression class going forward.

P1 fix (adtech-product-expert):
- Boot-time webhook gate raised ``ValueError``; now raises
  ``AdcpError(INVALID_REQUEST)`` for parity with
  ``validate_platform``'s sibling boot-time gates (governance opt-in,
  missing required methods). Adopter ``except AdcpError`` clauses
  catch all platform-config failures uniformly. ``details.missing``
  + ``details.webhook_eligible_tools`` for programmatic remediation.

Deferred → folded in:
- **Port-3001 EADDRINUSE friendly remediation** (2-of-4 Emma reports).
  ``_bind_reusable_socket`` projects EADDRINUSE OSError to a
  remediation-bearing message citing the busy port + ``port=`` /
  ``ADCP_PORT`` knobs. Other OSErrors (perm denied, address-not-avail)
  pass through unchanged so adopters debugging a different problem
  don't get a misleading port-collision message.
- **/mcp vs /mcp/ 307 redirect** (2-of-4 Emma reports). New ASGI
  middleware ``_wrap_with_path_normalize`` strips trailing slashes
  before dispatch. Buyer libs POSTing to ``/mcp/`` now route to the
  same handler as ``/mcp`` without the 307 (which silently broke
  libs that don't follow redirects on POST — they revert to GET on
  the redirected URL, losing the body). Root path ``/`` left alone
  to avoid health-check 404. Scope-copy semantics preserved so
  outer middlewares aren't affected.

Tests: 2868 pass (was 2854). 6 new (2 port-collision, 4 path-normalize)
+ 8 new example-boot smoke tests. F12 gate test updated to assert
``AdcpError("INVALID_REQUEST")`` + structured ``details``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 985dcd9 into main May 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant