Skip to content

fix(decisioning): three Emma cross-cutting findings + matrix runner#341

Merged
bokelley merged 2 commits intomainfrom
bokelley/emma-cross-cutting-followup
May 1, 2026
Merged

fix(decisioning): three Emma cross-cutting findings + matrix runner#341
bokelley merged 2 commits intomainfrom
bokelley/emma-cross-cutting-followup

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 1, 2026

Summary

Post-#340 Emma matrix run produced clean reports from 3 of 4 backends (sales 8/10, signals 9.5/10, stability 8/10 — all up from 2/10, 8/10, 5/10 pre-fixes). All three working reports flagged the same three issues:

A1: ADCPHandler.get_adcp_capabilities boot warning. Same TYPE_CHECKING bug we fixed for handler.py in PR #338, but in base.py. The framework's own method warns on every cold boot about an annotation no adopter can fix. Move types to module scope. Same fix shape as #338.

A2: Example field-name drift — three examples referenced fields that don't exist on the actual schemas:

  • hello_seller_signals.py:105req.signal_idreq.signal_agent_segment_id (silent dep-unknown pre-fix)
  • hello_seller_creative.py docstring — req.brief/req.format_idreq.message/req.target_format_id
  • hello_seller.py — added "Minimum valid buyer payloads" docstring with worked create_media_buy example

A3: Narrowed ValidationError details on the wire (Stability AI P1). When a platform method raises ValidationError directly, the wire used to carry "see details for cause" with empty details. PR #340's narrowing was server-log only. _internal_error_details now special-cases ValidationError and populates details.validation_errors with narrowed field paths.

Bonus: scripts/run_emma_matrix.sh ships in the repo (with the --add-dir / --dangerously-skip-permissions fixes that surfaced these findings).

Test plan

  • CI green on Python 3.10–3.13
  • 1 new test: test_invoke_validation_error_surfaces_narrowed_field_paths — pins the new wire shape
  • test_call_tool_invokes_handler updated: now sends minimum-valid GetProductsRequest payload (typed dispatch correctly rejects empty dict post-fix)
  • 2879 pass full sweep (was 2878)

🤖 Generated with Claude Code

bokelley and others added 2 commits May 1, 2026 13:35
… run)

The post-#340 Emma matrix run (sales 8/10, signals 9.5/10, stability
8/10) surfaced three items every working backend flagged:

A1 — base.py TYPE_CHECKING leak. ``ADCPHandler.get_adcp_capabilities``
(and every other framework baseline method) had its Request type
imported under ``if TYPE_CHECKING:``. The dispatcher's
``_resolve_params_pydantic_model`` calls ``get_type_hints`` at boot;
forward refs failed, ``NameError`` got swallowed, every adopter saw
``"typed params annotation failed to resolve … unresolved name:
GetAdcpCapabilitiesRequest"`` on cold boot. The remediation text told
adopters to import a model — but they CAN'T, it's not their method.
Same bug class as PR #338's handler.py fix; we missed base.py because
the framework methods inherit ``not_supported`` defaults so the
wire-failure mode was invisible until #338's resolver-warning bump
made it console noise.

A2 — example field-name drift. Three examples referenced fields that
don't exist on the actual schemas:
* ``hello_seller_signals.py:105`` read ``req.signal_id``;
  ``ActivateSignalRequest`` carries ``signal_agent_segment_id``. Pre-
  fix: silent ``dep-unknown`` deployments. Now correctly reads the
  spec field AND iterates buyer-supplied ``destinations``.
* ``hello_seller_creative.py`` docstring said ``req.brief`` /
  ``req.format_id``; ``BuildCreativeRequest`` actually carries
  ``message`` / ``target_format_id``.
* ``hello_seller.py`` lacked a buyer-side request example. Adopters
  copy-pasted seller stub and got INVALID_REQUEST. Added a
  "Minimum valid buyer payloads" docstring section with a worked
  ``create_media_buy`` payload.

A3 — narrowed ValidationError details on the wire. When a platform
method raises ``pydantic.ValidationError`` directly (typically the
seller's code constructed an invalid response), the wire used to say
"see details for cause" with empty details. PR #340's narrowing was
server-log only. ``_internal_error_details`` now special-cases
ValidationError and populates ``details.validation_errors`` with the
narrowed field-path list (filtered through ``narrow_union_errors``).
Defensive: a narrowing bug never 500s the wire — fall back silently
to ``caused_by`` only.

Plus: ``scripts/run_emma_matrix.sh`` (the runner that surfaced these
findings) ships in the repo for future re-validation.

Tests: 1 new dispatch test pinning ValidationError surfacing.
``test_call_tool_invokes_handler`` updated to send a minimum-valid
``GetProductsRequest`` payload now that typed dispatch correctly
rejects empty dicts.

Test count: 2879 passed (was 2878).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Decisioning AdcpError isn't a subclass of adcp.exceptions.ADCPError,
so 'except ADCPError' in serve.py didn't catch it — propagated to
FastMCP's default handler and details was silently dropped on the
MCP wire (A2A buyers got it via data.details, MCP buyers got nothing).

Three-part fix:

1. translate.py:translate_error — recognize decisioning AdcpError
   (lazy import) and extract code/message/suggestion/recovery/details/
   field. ADCPTaskError branch also lifts errors[0].details so
   create_tool_caller's INVALID_REQUEST projection (with #341's
   narrowed validation_errors) reaches MCP buyers.

2. translate.py:_to_mcp — accept details and serialise as a
   '\nDetails: <json>' suffix in the ToolError text payload.
   Truncated to 8 KB.

3. serve.py:_call_handler — follow-on except branch catches
   decisioning AdcpError (lazy import + isinstance) and routes
   through translate_error.

Tests: 3 new pinning caused_by + validation_errors surfacing and
the 8 KB truncation.

Test count: 2882 passed (was 2879).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 4938ee1 into main May 1, 2026
12 checks passed
@bokelley bokelley deleted the bokelley/emma-cross-cutting-followup branch May 1, 2026 18:02
bokelley added a commit that referenced this pull request May 1, 2026
ad-tech-protocol-expert called out three follow-ups from PR #341.
This PR ships #2 + #3 with concrete fixes; #1 (MCP structuredContent
migration) is documented as known-bridge — needs lowlevel
Server.call_tool refactor.

#3 — Truncation marker buyer-parseability. Pre-fix the MCP
ToolError text-suffix path emitted bare '...' as the truncation
marker, making buyer json.loads throw JSONDecodeError with no
signal of why. Now emits a sentinel object: {_truncated: true,
_reason: 'size' | 'non_serializable', _partial: '...'} — ALWAYS
valid JSON. Buyers split on '\nDetails: ' prefix, json.loads the
tail, and branch on the marker. Iterative fit replaces naive
head-room subtract so quote/backslash escaping in _partial doesn't
blow past the cap.

#2 — caused_by.type Python-only debug breadcrumb. Documented in
_internal_error_details that this field is a debug hint for the
seller dev reading their own server logs, NOT a cross-language
contract. JS sellers won't emit Python-typed exception names. Buyers
wanting structured retry/fix/abandon semantics should read
'recovery' (terminal/correctable/transient), which IS the spec
contract.

#1 — MCP structuredContent migration. Documented as bridge in
_to_mcp's docstring with the file:line citation of FastMCP's
lossy _make_error_result. The protocol-correct fix needs lowlevel
Server.call_tool registration so we can return CallToolResult
directly with structuredContent=adcp_error + isError=True;
deferred to a separate effort because it touches the FastMCP→
lowlevel boundary throughout serve.py.

Tests: 1 updated (truncation now asserts parseable sentinel),
1 new (unserializable details emits sentinel, not malformed JSON).
Test count: 2883 (was 2882).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 1, 2026
Latest Emma matrix run (post-#341/#342) caught three small DX items
all four backends agreed on. Fold into PR #342 since it's the same
scope (protocol-polish follow-ups).

1. examples/hello_seller.py:222 — wire-field bug. Stub returned
   {deliveries: [...]} but the schema at
   schemas/cache/media-buy/get-media-buy-delivery-response.json
   requires media_buy_deliveries. A buyer copying this example
   would get INVALID_RESPONSE on call. Sales 9/10 P1.

2. examples/hello_seller.py docstring overclaims. Module docstring
   said Three platform methods (get_products, create_media_buy,
   get_media_buy_delivery) but the class actually implements all
   five required sales-non-guaranteed methods (per
   REQUIRED_METHODS_PER_SPECIALISM, validated at boot). Trim the
   claim to match reality.

3. serve() boot log line. Two of four matrix backends flagged
   silent-boot — adopters had to curl to confirm the listener was
   up. Now logs one INFO line at the bind boundary:
   MCP listening on http://host:port/mcp (transport=streamable-http)
   for MCP, A2A listening on http://0.0.0.0:port/ for A2A.
   Framework-controlled (uvicorn's startup log can be filtered/
   quieted; this line always lands).

Test count: 2883 (existing test_get_media_buy_delivery_returns_zeros
updated to assert the spec field name; pins drift).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 1, 2026
* fix(server): protocol-polish follow-ups from PR #341 review

ad-tech-protocol-expert called out three follow-ups from PR #341.
This PR ships #2 + #3 with concrete fixes; #1 (MCP structuredContent
migration) is documented as known-bridge — needs lowlevel
Server.call_tool refactor.

#3 — Truncation marker buyer-parseability. Pre-fix the MCP
ToolError text-suffix path emitted bare '...' as the truncation
marker, making buyer json.loads throw JSONDecodeError with no
signal of why. Now emits a sentinel object: {_truncated: true,
_reason: 'size' | 'non_serializable', _partial: '...'} — ALWAYS
valid JSON. Buyers split on '\nDetails: ' prefix, json.loads the
tail, and branch on the marker. Iterative fit replaces naive
head-room subtract so quote/backslash escaping in _partial doesn't
blow past the cap.

#2 — caused_by.type Python-only debug breadcrumb. Documented in
_internal_error_details that this field is a debug hint for the
seller dev reading their own server logs, NOT a cross-language
contract. JS sellers won't emit Python-typed exception names. Buyers
wanting structured retry/fix/abandon semantics should read
'recovery' (terminal/correctable/transient), which IS the spec
contract.

#1 — MCP structuredContent migration. Documented as bridge in
_to_mcp's docstring with the file:line citation of FastMCP's
lossy _make_error_result. The protocol-correct fix needs lowlevel
Server.call_tool registration so we can return CallToolResult
directly with structuredContent=adcp_error + isError=True;
deferred to a separate effort because it touches the FastMCP→
lowlevel boundary throughout serve.py.

Tests: 1 updated (truncation now asserts parseable sentinel),
1 new (unserializable details emits sentinel, not malformed JSON).
Test count: 2883 (was 2882).

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

* fix: matrix-run findings (boot-log, hello_seller schema, docstring trim)

Latest Emma matrix run (post-#341/#342) caught three small DX items
all four backends agreed on. Fold into PR #342 since it's the same
scope (protocol-polish follow-ups).

1. examples/hello_seller.py:222 — wire-field bug. Stub returned
   {deliveries: [...]} but the schema at
   schemas/cache/media-buy/get-media-buy-delivery-response.json
   requires media_buy_deliveries. A buyer copying this example
   would get INVALID_RESPONSE on call. Sales 9/10 P1.

2. examples/hello_seller.py docstring overclaims. Module docstring
   said Three platform methods (get_products, create_media_buy,
   get_media_buy_delivery) but the class actually implements all
   five required sales-non-guaranteed methods (per
   REQUIRED_METHODS_PER_SPECIALISM, validated at boot). Trim the
   claim to match reality.

3. serve() boot log line. Two of four matrix backends flagged
   silent-boot — adopters had to curl to confirm the listener was
   up. Now logs one INFO line at the bind boundary:
   MCP listening on http://host:port/mcp (transport=streamable-http)
   for MCP, A2A listening on http://0.0.0.0:port/ for A2A.
   Framework-controlled (uvicorn's startup log can be filtered/
   quieted; this line always lands).

Test count: 2883 (existing test_get_media_buy_delivery_returns_zeros
updated to assert the spec field name; pins drift).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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