Skip to content

feat(webhooks): public to_wire_dict() serialization seam#602

Merged
bokelley merged 2 commits intomainfrom
bokelley/wire-dict-seam
May 8, 2026
Merged

feat(webhooks): public to_wire_dict() serialization seam#602
bokelley merged 2 commits intomainfrom
bokelley/wire-dict-seam

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 8, 2026

Closes #601.

Summary

  • New adcp.webhooks.to_wire_dict(payload) collapses the per-shape isinstance dispatch every adopter was writing around create_a2a_webhook_payload / create_mcp_webhook_payload. Re-exported from the adcp package root.
  • protobuf Task / TaskStatusUpdateEventMessageToDict(preserving_proto_field_name=False) plus the existing 1.0→0.3 enum normalization (TASK_STATE_COMPLETEDcompleted, ROLE_AGENTagent) so 0.3 buyer receivers keep parsing.
  • Pydantic models → model_dump(mode="json", exclude_none=True).
  • Mappingdict() passthrough; anything else → TypeError with a directional message.
  • Internal _payload_to_dict (the existing deliver() helper) is folded into to_wire_dict; behaviour for supported types is unchanged.

The optional bonus from the issue — making create_mcp_webhook_payload return McpWebhookPayload — is intentionally deferred. With to_wire_dict in place, adopters that want the typed shape can call McpWebhookPayload.model_validate(create_mcp_webhook_payload(...)) today, and a follow-up can flip the return type behind a deprecation cycle without re-touching the seam.

Test plan

  • pytest tests/test_webhooks_to_wire_dict.py — 6 new round-trip tests (A2A Task → camelCase id/contextId/artifactId, A2A TaskStatusUpdateEvent → camelCase taskId, MCP dict → snake_case task_id/task_type, MCP Pydantic → snake_case + exclude_none, dict passthrough is a defensive copy, unsupported types raise TypeError).
  • pytest tests/test_webhooks_deliver.py tests/test_webhook_handling.py — 136 passed, no regressions from refactoring _payload_to_dict into to_wire_dict.
  • ruff check + mypy src/adcp/webhooks.py clean.

🤖 Generated with Claude Code

bokelley and others added 2 commits May 8, 2026 11:53
Adopters wrapping create_a2a_webhook_payload (a2a-sdk 1.0+ protobuf
Task / TaskStatusUpdateEvent) and create_mcp_webhook_payload (dict /
McpWebhookPayload) had to write per-shape isinstance dispatch in every
webhook send path. A future a2a-sdk that swaps protobuf for a Pydantic
facade would silently change which branch runs.

to_wire_dict centralizes that dispatch:
- protobuf -> MessageToDict(preserving_proto_field_name=False), with
  the existing 1.0 -> 0.3 enum normalization (TASK_STATE_COMPLETED ->
  completed, ROLE_AGENT -> agent) so 0.3 buyer receivers keep parsing
- Pydantic -> model_dump(mode="json", exclude_none=True)
- Mapping -> dict() passthrough (legacy callers that hand-build the wire body)
- anything else -> TypeError with a directional message

Re-exports the seam from adcp.webhooks and the adcp package root.
Internal _payload_to_dict (used by deliver()) is replaced with
to_wire_dict; behaviour is unchanged for the supported types.

Refs: #601

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.

Uniform serialization seam for create_a2a/mcp_webhook_payload return shapes

1 participant