Repository navigation
feat(errors): AdCP 3.3 mutation_outcome and unsupported-targeting-dimension details (#1642) - #1811
Conversation
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — additive AdCP 3.3 mutation_outcome support plus UnsupportedTargetingDimensionError, no blocking or medium findings.
What I checked:
- Dispatch boundary correctness:
dispatched[0]is set at mcp_tools.py:4120 immediately before the singleawait method(call_params, ctx)call, somark_not_applied(mcp_tools.py:536) only claimsnot_appliedon pre-dispatch rejections and never on handler raises. Verified by tests at test_mutation_outcome.py:851-897. - Fail-closed normalization: buyer-side
normalize_mutation_outcomemaps unknown/empty/non-string to "unknown" (never discarded); parametrized at test_mutation_outcome.py:921-935. - Semver signal:
feat:with all exports additive. Snapshot carries the one new top-level namenormalize_mutation_outcome; decisioning additions aren't captured bysnapshot_modules(). No public-surface break.
Risk posture: high_risk is true but all matched files are (modified) with no medium-or-worse finding, so the heuristic does not escalate (no deletions, no gated paths, no team gate). gated_paths is false; no no-auto-approve team match. Rows 1–8 do not fire → row 9 approve.
4cfaa1d to
523df2e
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean pass, additive AdCP 3.3 error.mutation_outcome support.
No critical/high/medium findings. Walking the decision table top-down:
- Rows 1 (no critical/high), 4/8 (no medium) do not fire.
- Row 2: gated_paths is false (and review_decision is APPROVED anyway).
- Row 3/5: high_risk is true but all high-risk reasons are
(modified)with no medium finding on them; no(deleted)entries. No escalation trigger. - Row 6: prior decision was
approve, notescalate— not sticky. - Row 7: no no-auto-approve team match.
- Falls through to row 9 → approve.
What I checked: change is additive (new optional kwargs, exports, and properties), so feat: is the correct semver signal — no public-surface removal, no required→optional flip, no enum member removal. The mutating-tool gate was verified: call_tool only marks not_applied when is_mutating(method_name) and dispatch has not fired (mcp_tools.py:3670-3681, 4150-4151), so a post-dispatch handler raise keeps no claim. _error_entry_to_wire and to_wire propagate mutation_outcome to both adcp_error and errors[] consistently; mark_not_applied invalidates cached error_info. DR-0032/0033 minor-release pathway is covered: new UnsupportedTargetingDimensionError provides the refused pathway, and test_mutation_outcome.py (+301) covers framework-set, handler-kept, read, idempotency-conflict, and buyer-normalization paths. Public API snapshot fixture updated.
5c6067a to
9c786b4
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Clean pass — no critical/high/medium findings. This matches the prior approve decision on an earlier head; the delta equals the full PR and the reviewer re-verified it end to end.
What was checked:
- Additive AdCP 3.3
error.mutation_outcomeandUnsupportedTargetingDimensionErrorunderfeat(errors):— additive across the public surface, withpublic_api_snapshot.jsonand__all__updated, so no semver-break signal is required. - Fail-safe dispatch gate:
dispatched[0]=Trueset immediately beforemethod(), somark_not_appliedfires only on pre-handler rejections ofis_mutatingtools — nonot_appliedclaim once the handler runs. mark_not_appliedmutatesADCPTaskError.errorsin place and invalidates the@cached_property error_info(exceptions.py:715) — the one sanctioned mutation of the otherwise-immutable list._mutation_outcomeprojection reachesadcp_error,errors[], A2A DataPart, andtranslate_errordata; dispatch re-raise preserves the first-entry claim.- Minor-release pathway (DR-0032/0033): 3.2/3.0
core/error.jsonkeep the field open, so it is droppable on the response path; buyernormalize_mutation_outcome(absent→None, non-string/empty/unrecognized→"unknown") is covered by parametrized tests at test_mutation_outcome.py:924-930.
high_risk is true but all reasons are (modified) with zero medium-or-higher findings, so no escalation trigger applies (rows 3/5 require a deletion or a medium finding). gated_paths is false; review_decision is APPROVED. No no-auto-approve team match. None of rows 1–8 fire → row 9 approve.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — additive AdCP 3.3 error.mutation_outcome field + UnsupportedTargetingDimensionError.
What I checked:
- Semver signal:
feat(errors):is correct. New optional kwarg (defaultNone), new read-only properties, new exports — no removed/renamed/retyped public surface, no required→optional flips on public models, no enum member removals. Non-breaking, so release-please cutting a minor is correct. - Mutation-outcome dispatch gate (mcp_tools.py):
dispatched[0]=Trueis set only immediately beforeawait method(...), after all pre-dispatch validation/hooks/version-conversion. Pre-dispatch rejection →not_applied; handler raise stays unclaimed → INTERNAL_ERROR wrap;except Exceptioncorrectly leaves cancellation alone. IDEMPOTENCY_CONFLICT marksnot_appliedat the lookup site. - DR-0032 wire-pathway: field is droppable for 3.2 peers (open error schema, informational — dropping does not change spend/targeting/caps/account identity), documented in MIGRATION_v9_to_v10.md. Pathway test present (test_mutation_outcome.py).
- Largest-file rule: test_mutation_outcome.py L921-935 pins the buyer-read normalization matrix — verified safe.
- Public API snapshot: delta complete (top-level only).
Decision table: No critical/high/medium findings (row 1 N/A). gated_paths false (row 2 N/A). All high-risk reasons are (modified) with no medium finding, so rows 3/5 N/A. No data-loss/schema/infra medium (row 4 N/A). Prior decision was approve, not escalate (row 6 N/A). No no-auto-approve team match (row 7 N/A). Zero medium findings (<3, row 8 N/A). Falls through to row 9 → approve.
…ension details (#1642) AdcpError(mutation_outcome=...) projects error.mutation_outcome onto adcp_error and errors[] on MCP and A2A, and translate_error's A2A data; an ADCPTaskError projects its first entry's claim, and the decisioning dispatcher keeps it when it re-raises a task error as AdcpError. The framework sets not_applied only where it holds pre-commit evidence: a structured rejection create_tool_caller raises before a mutating handler runs (schema and model validation, version gates, pre-validation hooks) and IDEMPOTENCY_CONFLICT, detected on lookup before anything is written. A claim the raise already makes is kept; errors a handler raises, and the INTERNAL_ERROR wrap of an unexpected exception, carry no claim. Reads never carry it. UnsupportedTargetingDimensionError (an UnsupportedFeatureError) emits UNSUPPORTED_FEATURE with error-details/unsupported-targeting-dimension validated against the generated model and error.field at the targeting path; unsupported_targeting_dimension_details reads it back. Buyers read TaskResult.mutation_outcome, ADCPTaskError.mutation_outcome and AdcpErrorInfo.mutation_outcome, normalized by normalize_mutation_outcome: absent is None, an unrecognized, empty or non-string value is "unknown". The 3.2 and 3.0 core/error.json schemas allow additional properties, so the field needs no gating for older callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9c786b4 to
1dcc988
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — AdCP 3.3 mutation_outcome plumbing is sound and purely additive on the public surface.
What I checked:
- No critical/high/medium findings in this run; clean pass consistent with the prior approve (head 9c786b4).
- Public-surface change is additive: new kw-only
AdcpError(mutation_outcome=...), new exports, snapshot regenerated —feat:is the correct semver signal, no breaking-change gate triggered. create_tool_callersetsnot_appliedonly on pre-dispatch rejections of mutating tools, gated by a singledispatched[0] = Trueat the sole dispatch point (mcp_tools.py:4201); INTERNAL_ERROR legacy-projection wrap and reads carry no framework claim;IDEMPOTENCY_CONFLICTstamped at lookup in store.py/reservation.py.mark_not_appliedinvalidates theerror_infocached_property; projection reaches MCP, A2A anderrors[]; buyer-sidenormalize_mutation_outcomereads unrecognized/empty/non-string as"unknown"per spec (tests/test_mutation_outcome.py:921-968).- 32 new tests cover every branch.
Gate evaluation:
- No no-auto-approve team match.
- gated_paths: false; review_decision: APPROVED.
- high_risk: true but all reasons are
(modified)with NO medium-or-worse findings — row 5 does not fire; no(deleted)entries (row 3 n/a). - Prior decision was
approve, so sticky-escalation (row 6) n/a.
None of rows 1–8 fire → row 9 approve.
Spec
core/error.jsonmutation_outcome(not_applied,unknown,applied): whether a failed state-changing request changed seller state. It goes only on errors that terminate a mutation, never on reads or warnings.not_appliedrequires pre-commit evidence. Receivers MUST read an unrecognized, empty or non-string value asunknown.UNSUPPORTED_FEATUREfor an unsupported or removed targeting dimension SHOULD carryerror-details/unsupported-targeting-dimension.json, witherror.fieldat the targeting path (adcp#8052).Change
Seller
AdcpError(..., mutation_outcome=...), typed by the exportedMutationOutcomeliteral.to_wire(),build_mcp_error_result, the A2A failed-task DataPart andtranslate_error's A2A data all project it, so it reaches bothadcp_erroranderrors[]. AnADCPTaskErrorprojects its first entry's claim. The decisioning dispatcher keeps the claim when it re-raises a task error asAdcpError.from_errorstakes the first entry's claim.not_appliedonly where it holds pre-commit evidence:create_tool_caller, which serves both MCP and A2A, on a structured rejection raised before a mutating handler (IDEMPOTENT_TASKS) runs: schema and model validation, version gates, pre-validation hooks and legacy adapters (mark_not_applied);IDEMPOTENCY_CONFLICTfromIdempotencyStoreand the PG reservation, which is detected on lookup before anything is written.INTERNAL_ERRORwrap of an unexpected exception, carry no claim, as the spec requires. Reads never carry one.PlatformHandlerrejections raised before_invoke_platform_method. Some of those paths take reservations (proposal consumption) before the platform call, so "before the platform method" is not "before any write". Adopters setmutation_outcomethemselves where they have the evidence.UnsupportedTargetingDimensionErrorsubclassesUnsupportedFeatureError, soexcept UnsupportedFeatureErrorstill catches it. It emitsUNSUPPORTED_FEATURE,correctable, with the details validated against the generatedUnsupportedTargetingDimensionDetails.fieldis required. Areplacement_dimensionbecomes the default suggestion.unsupported_targeting_dimension_details(error)reads the details back.Buyer
normalize_mutation_outcome(error)is exported at top level. It returnsNonewhen the field is absent; any present value outside the three literals reads as"unknown".AdcpErrorInfo.mutation_outcome,ADCPTaskError.mutation_outcome(first entry), andTaskResult.mutation_outcome(fromadcp_error, else the firsterrors[]entry).Older callers: the 3.2 and 3.0
core/error.jsonschemas allow additional properties, so the field needs no gating.Docs:
handler-authoring.mdsection;MIGRATION_v9_to_v10.mdrow, since pre-dispatch rejections of mutating tools now carry a new key inadcp_error/errors[].Tests
tests/test_mutation_outcome.py(32 tests) covers:mark_not_applied;create_tool_caller:create_media_buy→not_applied;get_products→ no claim;unknownkept;IDEMPOTENCY_CONFLICTthroughIdempotencyStore.wrap;ADCPTaskErrorandTaskResult;UnsupportedTargetingDimensionErrorshape, validation and reader.The public API snapshot was regenerated (+
normalize_mutation_outcome).Closes #1642
🤖 Generated with Claude Code