Repository navigation
feat(decisioning): let a raised AdcpError reject a request with every error (#1780) - #1804
Conversation
… error (#1780) A seller that validates a whole request could only raise one AdcpError, so the payload errors[] carried one entry even when the caller listed every problem in details["errors"]. AdcpError takes errors=[...] (AdcpError, generated Error or wire mapping entries) and AdcpError.from_errors([...]) builds the raise from the list, the first entry filling adcp_error. build_mcp_error_result, the A2A failed-task builder and translate_error write the whole list to errors[]; adcp_error is unchanged. A details["errors"] list whose entries carry a string code and message is lifted into errors[] as well. An entry without recovery takes the raised error's; entry details are sanitized like to_wire(). A single-error raise still mirrors adcp_error into errors[] (#1558). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — additive multi-error support on AdcpError, no blocking findings.
Checked: feat(decisioning): is the correct semver signal — no public export removed/renamed, no required↔optional flip, no response-shape break; buyers still deserialize errors[] as a list. _error_entry_to_wire sanitizes entry details and refuses malformed entries (TypeError/ValueError) consistent with to_wire. from_errors recovery-default propagation is consistent between the first computation and __init__ re-normalization. The details["errors"] auto-lift (types.py L174-187) is gated on well-formed string code+message entries and leaves other shapes alone; the documented behavior change is handled in the PR body. Both A2A failed-task and MCP paths route through _payload_errors, single-error still mirrors (#1558). tests/test_adcp_error_multi.py:462 reproduces the issue end-to-end through build_mcp_error_result; 28 tests cover construction, sanitization, both transports, and _register_tool.
high_risk is true (modified files under decisioning/** and server/**), but with zero medium-or-higher findings these modifications are presumed safe (rows 4-5 do not fire). No gated paths, no no-auto-approve team match, no prior decision. Falls through to row 9.
Problem
A seller that validates a whole
create_media_buy/update_media_buyrequest finds several problems at once, but a decisioning platform can only report them by raisingAdcpError, which carries one code, message and field.build_mcp_error_resultand the A2A failed-task builder wroteerrors: [<that one error>], even when the caller put the whole list indetails["errors"](the slotAdcpError's docstring named for multi-error preflight). The issue's repro printed1 [None]. Sellers had to lift the list in a response enhancer themselves.Change
AdcpError(..., errors=[...]): entries areAdcpErrors, generatedErrormodels, or wire mappings with a stringcodeandmessage. The raised error still fills the envelopeadcp_error, and every entry goes into the payloaderrors[]. An entry withoutrecoverytakes the raised error's. Entrydetailsgo throughsanitize_error_detailsliketo_wire(). A malformed entry raises at construction (TypeError/ValueError).AdcpError.from_errors([...])builds the raise from the list, with the first entry fillingadcp_error, soadcp_error == errors[0]. It returns a baseAdcpErroreven when the first entry is a subclass.AdcpError.errors_to_wire()returns the payload list: every entry, or[to_wire()]when there is no list.details["errors"]slot is lifted. Whenerrors=is not passed and everydetails["errors"]entry is a mapping with a stringcodeandmessage, those entries fillerrors[]. Any other shape (strings,{"path", "msg"}dicts, an empty list) is left alone, as before.adcp_error.detailsis unchanged.build_mcp_error_result, the A2A executor's failed-task DataPart andtranslate_error(..., "a2a")write the whole list. A single-error raise still mirrorsadcp_errorintoerrors[](RaisedAdcpErroronly fillsadcp_error, so storyboards that readerrors[0]fail #1558/fix(server): mirror a raised AdcpError into the payload errors array #1592), so existing output is unchanged.docs/handler-authoring.md.Not covered: an async
TaskHandofffailure still records one error throughregistry.fail(task_id, exc.to_wire()), sotasks/getand the failure webhook reporterrors: [adcp_error]. Carrying the list there means a task-registry storage change, so it's left as a follow-up.Adopter note: this change is additive. A seller whose response enhancer already lifts
details.errorsintoerrors[](as the Scope3 sales agent'sproject_wire_errorsdoes) gets the list from the SDK now. An enhancer that replaceserrorskeeps working; one that appends would duplicate the entries and should be removed.Tests
tests/test_adcp_error_multi.py(28 tests) covers:from_errorswith all three entry types, and the subclass entry;details["errors"]lift, including the shapes it must leave alone;build_mcp_error_result;_register_tool;translate_errorA2A data, and the A2A executor's failed task.Locally:
make lintandmake typecheck-allpass. The targeted error suites (test_adcp_error_multi,test_mcp_structured_error,test_a2a_structured_error,test_translate,test_decisioning_types, 149 tests) pass. I stopped the full local run partway because the sandbox ran out of memory, so CI's matrix is the full-suite gate.A matching non-breaking PR against
main(SDK 9) follows.Closes #1780
🤖 Generated with Claude Code
Open workspace in Conductor