Proposal
ADCPHandler methods currently take untyped params: dict[str, Any] and callers lean on params.get(...) everywhere. Every round of DX validation (rounds 4-7, see #205) flagged this as the biggest structural boilerplate complaint:
- Agents re-learn field names from the schema for every handler.
- No IDE autocomplete on request fields.
- No Pydantic validation at the handler boundary — schema validation happens in the transport layer, which is too late for business-logic code.
- Typos in
params.get("brand") vs params.get("brnad") land at runtime as silent None.
Target API
class MySeller(ADCPHandler):
async def get_products(
self,
params: GetProductsRequest,
context: ToolContext | None = None,
) -> GetProductsResponse:
# params.brief, params.promoted_offering, etc. — typed, validated
...
Handler registration would inspect the annotation and use the right <ToolName>Request Pydantic model to deserialize params before calling the handler. Handlers that still declare params: dict continue to work (back-compat).
Scope (estimate — multi-day, not 30-min)
- Base class dispatch layer —
ADCPHandler._dispatch(tool_name, raw_params, context) inspects the override's signature annotation. If it's a Pydantic model, deserialize; else pass dict (back-compat).
- Response validation — optional: if the handler's return annotation is a Pydantic model, validate the return on the way out. Gives typed throughout.
- Examples — rewrite
examples/seller_agent.py to use typed signatures. Reference implementation across seller/generative/retail.
- Skills — update all 5
build-*-agent skills to teach the typed pattern as the primary path, with a one-paragraph note on the legacy dict path for migrators.
- MCP tool registration —
_generate_pydantic_schemas() already maps tool name → request model. Dispatch layer should read from the same source of truth, so there's one mapping.
- Tests — exhaustive coverage of: typed handler with typed model, typed handler with dict fallback, typed handler raising Pydantic ValidationError, sibling handlers with mixed typed/untyped signatures.
Open questions
- Return-type enforcement policy: strict (reject return shape that doesn't match the declared model) or lax (best-effort coerce)? The current response builders (
products_response(...), media_buy_response(...)) return dicts; users may have already adopted the pattern of returning dicts. A strict return-type policy would break them.
- Error surface when Pydantic validation fails at the handler boundary: should we return a structured AdCP error (
INVALID_REQUEST with field=...) from the dispatch layer, or let the exception propagate? First option is friendlier; second is more transparent.
- Generic request type: some handlers (e.g.,
comply_test_controller) take a union type. Current generator falls back to a stub for unions — does the typed-dispatch layer need to handle unions specially, or can it just require the author to model_validate the union themselves inside the handler?
Priority
4.1+ feature, not 4.0-gating. The current dict-based API works and is well-tested; this is DX polish worth landing in a dedicated PR with its own review cycle rather than rushing into a release cut.
Motivating agent feedback (verbatim from round 7)
"Every handler is params, context=None with manual params.get(...). The SDK has generated request models — typed handler signatures (params: SyncCreativesRequest) would give autocomplete + validation for free."
— round-7 creative skill validator (.context/dx-runs/creative/report.md)
"Handler takes params: dict[str, Any] when every other part of the SDK is strict Pydantic. Feels like the one place we stopped short."
— paraphrased from round-7 seller skill boilerplate complaint
Related: #205
Proposal
ADCPHandlermethods currently take untypedparams: dict[str, Any]and callers lean onparams.get(...)everywhere. Every round of DX validation (rounds 4-7, see #205) flagged this as the biggest structural boilerplate complaint:params.get("brand")vsparams.get("brnad")land at runtime as silentNone.Target API
Handler registration would inspect the annotation and use the right
<ToolName>RequestPydantic model to deserializeparamsbefore calling the handler. Handlers that still declareparams: dictcontinue to work (back-compat).Scope (estimate — multi-day, not 30-min)
ADCPHandler._dispatch(tool_name, raw_params, context)inspects the override's signature annotation. If it's a Pydantic model, deserialize; else pass dict (back-compat).examples/seller_agent.pyto use typed signatures. Reference implementation across seller/generative/retail.build-*-agentskills to teach the typed pattern as the primary path, with a one-paragraph note on the legacy dict path for migrators._generate_pydantic_schemas()already maps tool name → request model. Dispatch layer should read from the same source of truth, so there's one mapping.Open questions
products_response(...),media_buy_response(...)) return dicts; users may have already adopted the pattern of returning dicts. A strict return-type policy would break them.INVALID_REQUESTwithfield=...) from the dispatch layer, or let the exception propagate? First option is friendlier; second is more transparent.comply_test_controller) take a union type. Current generator falls back to a stub for unions — does the typed-dispatch layer need to handle unions specially, or can it just require the author tomodel_validatethe union themselves inside the handler?Priority
4.1+ feature, not 4.0-gating. The current dict-based API works and is well-tested; this is DX polish worth landing in a dedicated PR with its own review cycle rather than rushing into a release cut.
Motivating agent feedback (verbatim from round 7)
Related: #205