Skip to content

Commit f7062f2

Browse files
bokelleyclaude
andauthored
feat: align A2A adapter with canonical response format (#83)
* docs: add CLAUDE.md with Python SDK development learnings Document key learnings from building the Python AdCP SDK: **Type Safety & Code Generation:** - Auto-generate Pydantic models from upstream schemas - Handle missing schema types with documented type aliases - Use TYPE_CHECKING for optional dependencies - Use cast() for JSON deserialization type safety **Testing Strategy:** - Mock at the right level (_get_client() not httpx class) - httpx response.json() is SYNCHRONOUS not async - Test the API as it exists, not as we wish it existed **CI/CD & Release:** - Verify secret names before changing them (PYPY_API_TOKEN not PYPI_API_TOKEN) - Release Please automates version bumps and PyPI publishing - Entry points in pyproject.toml enable uvx usage **Python Patterns:** - String escaping order matters (backslash first, then quotes) - Atomic file operations for config files - Connection pooling for HTTP clients - Python 3.10+ required for | union syntax 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: align A2A adapter with canonical response format Update A2A adapter to match PR #238 specification for A2A response structure: - Use artifacts[].parts[] structure with typed parts (DataPart, TextPart, FilePart) - Implement "last DataPart is authoritative" rule for streaming scenarios - Distinguish protocol-level failures (status: "failed") from task-level errors (errors array) - Use taskId/contextId fields per A2A spec (not nested task.id) - Add comprehensive tests for all response patterns including multiple DataParts This ensures the Python SDK correctly handles AdCP responses over A2A protocol per the canonical specification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: fix import ordering in test files * fix: use first completed artifact in A2A responses Correctly implements the A2A canonical response format for handling multiple artifacts in streaming scenarios. When multiple artifacts exist, now selects the first artifact with status="completed" rather than simply using the first artifact in the array. This handles cases where early artifacts may have status="working" and later artifacts have status="completed", ensuring we use the correct completed data rather than intermediate progress updates. Updated both _extract_result() and _extract_text_part() methods to use consistent artifact selection logic, with fallback to first artifact if no status field is present. Added comprehensive test case for multiple artifacts with different statuses to verify the implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: extract and populate message field from A2A TextParts Ensures that the human-readable message from A2A TextParts is properly extracted and included in TaskResult.message for all response statuses (completed, working, submitted, input-required), not just failures. This provides consistent access to the natural language summary of task results, which is valuable for logging, user feedback, and AI agent comprehension. Updated test to verify message extraction works correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update schema sync script for new upstream directory structure The AdCP repository restructured schemas from `static/schemas/v1/` to `static/schemas/source/`. Updated sync script to: - Use correct path: `static/schemas/source` instead of `v1` - Make index.json fetch optional (gracefully handle 404) - Continue schema discovery even if index.json is missing This fixes CI failures in the "Validate schemas are up-to-date" check. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add versioned AdCP schema targeting Implements explicit AdCP version targeting for schema synchronization and type generation. This ensures the SDK is built against a specific AdCP specification version. Changes: - Added `get_adcp_version()` function to return target AdCP version - Updated schema sync script to use versioned paths (e.g., `static/schemas/v2/`) - Added `--version` flag to CLI to show SDK and AdCP spec versions - Exported `get_adcp_version` in public API Currently uses "source" as the version path since upstream hasn't migrated to versioned directories yet. Will be updated to "v2" when upstream completes the migration. Usage: `adcp --version` - Show SDK version and target AdCP spec `python -c "from adcp import get_adcp_version; print(get_adcp_version())"` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: sync schemas from authoritative public API Changed schema synchronization to use the canonical, versioned public API at https://adcontextprotocol.org/schemas/ instead of GitHub repository. Changes: - Updated sync script to fetch from public website (authoritative source) - Extract schema refs from index.json recursively - Preserve directory structure (core/, enums/, etc.) - Set target AdCP version to "v1" (current production version) - Successfully syncs 118 schemas from v1 index The public API provides versioned, stable schema endpoints that are the source of truth for AdCP implementations. The website handles version aliasing (e.g., /v1/ -> /2.4.0/) transparently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: move AdCP version to dedicated config file Moved the target AdCP version from hardcoded function to a simple ADCP_VERSION file at project root. This simplifies version management and removes the awkward sys.path manipulation in scripts. Changes: - Created ADCP_VERSION file containing "v1" - Updated get_adcp_version() to read from file - Updated sync script to read directly from file - Removed sys.path manipulation hack To upgrade to v2: 1. Edit ADCP_VERSION file to "v2" 2. Run sync script to download v2 schemas 3. Regenerate types 4. Done 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: remove version subdirectory from schema cache Simplified schema cache structure by removing the version subdirectory. Schemas are now stored directly in schemas/cache/ with their category structure (core/, enums/, etc.). Changes: - Removed version parameter from download_schema_file() - Store schemas directly in CACHE_DIR instead of CACHE_DIR/version/ - Removed symlink creation (no longer needed) - Updated output messages to show target version and cache location Structure: Before: schemas/cache/1.0.0/core/package.json After: schemas/cache/core/package.json Rationale: - SDK targets single AdCP version at build time - No runtime version switching needed - Simpler paths for code generation and maintenance - When upgrading to v2, just re-sync and regenerate types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * build: include ADCP_VERSION file in package distribution Added ADCP_VERSION to package distribution so it's available at runtime when the package is installed via pip. Changes: - Created MANIFEST.in to explicitly include ADCP_VERSION - Updated pyproject.toml with data-files configuration - Ensures get_adcp_version() can read the file after installation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update schema ref fixing for new directory structure Updated fix_schema_refs.py to work with the new schema cache structure without version subdirectories. Changes: - Changed path from schemas/cache/latest to schemas/cache - Added recursive file search (rglob) for subdirectories - Convert absolute refs to proper relative paths based on file location - Handle cross-directory refs (e.g., ../core/error.json) - Handle same-directory refs (e.g., error.json) Example conversions: From: /schemas/2.4.0/core/error.json In media-buy/: ../core/error.json In core/: error.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update generate_types.py to use new cache structure The script was looking for schemas in schemas/cache/1.0.0/ which no longer exists after we simplified the cache structure. Updated to: - Use schemas/cache/ directly (removed version subdirectory) - Recursively find schemas in all subdirectories (core, enums, etc.) - Process all 115+ schema files instead of 0 This fixes the CI failure where schema generation was failing with "Models not found in the input data". 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: remove $id fields from schemas to fix code generation The $id fields in schemas were causing datamodel-code-generator to fail when resolving relative $ref paths. When it saw a relative ref like "../core/context.json" and an absolute $id like "/schemas/2.4.0/signals/get-signals-request.json", it tried to resolve to the filesystem path "/schemas/2.4.0/core/context.json" which doesn't exist. Solution: Remove $id fields from all schemas during the fix_schema_refs step. These fields aren't needed for code generation and were causing the FileNotFoundError. This fixes: FileNotFoundError: [Errno 2] No such file or directory: '/schemas/2.4.0/signals/../core/context.json' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: download transitive schema dependencies The sync script was only downloading schemas referenced in index.json, missing transitive dependencies like context.json that are referenced by other schemas but not in the index itself. Changes: - Added discover_transitive_refs() to extract all $ref URLs from schemas - Follow transitive closure of dependencies during sync - Now downloads 137 schemas instead of 118 This fixes the code generation failure where datamodel-code-generator couldn't find context.json referenced by signal schemas. FileNotFoundError: [Errno 2] No such file or directory: '.schema_temp/../core/context.json' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: rewrite relative $ref paths in flattened schemas The generate_types.py script flattens all schemas into a single temp directory, but relative refs like "../core/context.json" were not being rewritten to "./context.json". This caused datamodel-code-generator to fail when trying to resolve the relative paths. Updated rewrite_refs() to handle: - Absolute paths: /schemas/v1/core/error.json -> ./error.json - Relative paths: ../core/error.json -> ./error.json - Same-dir refs: ./error.json -> ./error.json (unchanged) This fixes: FileNotFoundError: [Errno 2] No such file or directory: '.schema_temp/../core/context.json' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: handle directory path refs in schema rewriting Some schemas (like adagents.json) have refs without ../ or ./ prefix, just directory paths like "core/property-id.json". These weren't being rewritten to flat references, causing the code generator to look for subdirectories in the temp directory. Updated rewrite_refs() to handle all path formats: - Absolute: /schemas/v1/core/error.json -> ./error.json - Relative: ../core/error.json -> ./error.json - Directory: core/error.json -> ./error.json - Same-dir: ./error.json -> ./error.json (unchanged) The simplified logic now rewrites any ref containing "/" to just the filename with ./ prefix. This fixes: FileNotFoundError: [Errno 2] No such file or directory: '.schema_temp/core/property-id.json' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: preserve directory structure instead of flattening schemas Critical bug: Flattening all schemas to a single directory caused filename collisions. We have: - media-buy/list-creative-formats-request.json - creative/list-creative-formats-request.json When flattened, one would overwrite the other, losing data. Solution: Preserve the directory structure in the temp directory and let datamodel-code-generator handle the relative refs correctly. The schemas already have correct relative refs from fix_schema_refs.py, so we just need to copy them as-is maintaining the directory structure. Removed the rewrite_refs() function entirely since it's no longer needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: convert hyphens to underscores in directory names for Python compatibility Python module names cannot contain hyphens, only underscores. When datamodel-code-generator creates imports, it generates invalid syntax like "from ..pricing-options import" which causes Black to fail parsing. Directories with hyphens: - pricing-options - media-buy - creative/asset-types Solution: When copying schemas to temp directory, replace hyphens with underscores in directory names (pricing-options -> pricing_options). This makes the generated imports valid Python: "from ..pricing_options import ..." This fixes: black.parsing.InvalidInput: Cannot parse for target version Python 3.10: from ..pricing-options import (...) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: rewrite schema refs to use underscores for Python module compatibility When copying schemas to the temp directory, we now: 1. Convert directory names from hyphens to underscores (media-buy -> media_buy) 2. Rewrite $ref paths to match the renamed directories This fixes the FileNotFoundError where datamodel-code-generator was looking for schemas with hyphenated directory names while we had renamed them to use underscores for valid Python identifiers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update consolidate_exports and post_generate_fixes for directory structure - Updated consolidate_exports.py to recursively scan subdirectories - Updated module path generation to handle nested directories (e.g., core.error) - Fixed post_generate_fixes.py paths to match new directory structure - Updated fix_forward_references() to recursively scan and fix all subdirectories - Improved forward reference pattern matching to handle imports like "from ..core import brand_manifest as brand_manifest_1" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: remove deprecated types from imports Removed types that no longer exist in the generated schemas: - Details, Domain, DomainBreakdown, Filters - HistoryItem, MarkdownAsset, PackageStatus - Task, TasksGetRequest, TasksGetResponse, TasksListRequest, TasksListResponse These types were removed from the upstream AdCP specification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: resolve test failures and align A2A protocol with spec Systematic fixes for all 13 test failures: 1. **Pydantic Forward References** (8 tests fixed) - Re-enabled post_generate_fixes.py to fix brand_manifest import aliases - Added model_rebuild() calls in consolidate_exports.py for forward refs - Fixed promoted_offerings.py to use correct import alias 2. **A2A Protocol Spec Compliance** (1 test fixed) - Removed non-existent artifact.status field checks - Use last artifact (most recent) per A2A spec - Simplified logic: artifacts[-1] instead of searching for "completed" - Updated test to match correct behavior 3. **Directory Structure Updates** (3 tests fixed) - Updated PropertyTag/PropertyId module path expectations - Tests now expect core/ subdirectory structure - Made import pattern matching more flexible 4. **Schema Evolution** (1 test fixed) - Updated Package field count from 12 to 13 fields All 300 tests now passing. Changes align with A2A specification which defines artifacts with parts but no status field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: sync schemas and regenerate types from upstream Synced 6 schema updates from adcontextprotocol.org/schemas/v1: - core/creative-asset.json - Added placement_ids field - core/creative-filters.json - Filter updates - core/product-filters.json - Filter updates - media-buy/sync-creatives-request.json - Request updates - media-buy/update-media-buy-request.json - Request updates - protocols/adcp-extension.json - New schema for agent card extensions Regenerated all Pydantic types to reflect schema changes. All 300 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add get_info command to expose AdCP agent card extensions Implement support for querying agent metadata including the new AdCP extension fields from agent cards. This allows clients to discover: - Agent name, description, version - AdCP version the agent implements (extensions.adcp.adcp_version) - Supported protocol domains (extensions.adcp.protocols_supported) - Available tools/skills Changes: - Add get_agent_info() to ProtocolAdapter base class - Implement for A2A adapter (reads from /.well-known/agent.json) - Implement for MCP adapter (extracts from server capabilities) - Add get_info() method to ADCPClient - Add get_info CLI command (no parameters, returns agent metadata) - Add comprehensive tests for both A2A and MCP adapters Usage: adcp myagent get_info # Returns: name, version, protocol, tools, adcp_version, protocols_supported All 302 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: wrap long comment line to satisfy linter Fixes E501 line too long error in CI (103 > 100 characters). Split comment across two lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: handle ADK response wrapper and interim status correctly Fixes two issues with A2A protocol response handling: 1. **ADK Response Wrapper**: ADK wraps responses in {"response": {...}} - Added unwrapping logic in _extract_result() - Detects single-key dict with "response" and unwraps it - Maintains backward compatibility with non-wrapped responses 2. **Interim Response Handling**: Working/pending/input-required states - These interim responses don't need structured AdCP content - Changed to return data=None for interim statuses - Added status to metadata for tracking - Only "completed" responses require structured AdCP payload Tests: - test_call_tool_with_response_wrapper: Verifies ADK wrapper unwrapping - test_interim_response_working: Verifies interim responses work without data - All 304 tests passing This aligns with the A2A spec requirement that only completed responses must contain structured AdCP content in the DataPart. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: handle ADK's inconsistent response wrapper flexibly ADK is inconsistent - some DataParts have {"response": {...}} wrapper, others don't. Updated unwrapping logic to always extract "response" key when present, regardless of whether it's the only key or if there are additional metadata keys alongside it. Changes: - Unwrap "response" key whenever present (not just single-key dicts) - Handle cases where ADK includes both "response" and metadata keys - Maintain backward compatibility with non-wrapped responses Test added: - test_call_tool_with_response_wrapper_and_metadata: Verifies handling of {"response": {...}, "metadata": {...}} pattern All 305 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: consolidate all interim status handling Simplified the A2A interim response handling by consolidating all interim statuses (submitted, working, pending, input-required) into a single code path. Previously "working" had its own special case that was identical to the else block. Changes: - Removed redundant "working" special case - Single else block handles all interim states consistently - Clearer documentation: only "completed" requires structured AdCP data - Added "submitted" to the list of documented interim states Tests: - test_interim_response_working: Verifies working status - test_interim_response_submitted: Verifies submitted status - Both confirm data=None for interim responses All 306 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update for latest schema changes and FormatId discriminated union - Remove Render type (removed from upstream schemas) - Add FormatId backward compatibility union (FormatId1 | FormatId2) - Fix module-qualified FormatId references in generated code - Add comprehensive model_rebuild() calls for FormatId-related types - Update ImageAsset and VideoAsset to include required width/height fields - Add PreviewCreativeRequest discriminator for request_type field - Fix FormatId2-only references to use full union type - Update consolidate_exports.py to include all necessary model rebuilds Test results: 291 passing (up from 280), 15 failing (down from 26) The remaining failures are related to upstream schema evolution that requires updates to test fixtures and mock data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: complete FormatId union support and update all ImageAsset usages - Add core/**/*.py to FormatId union reference fixes - Add GetProductsRequest and other request types to model_rebuild list - Update all ImageAsset creations to include required width/height fields - preview_cache.py: _create_sample_manifest_for_format_id - test_preview_html.py: all test manifests Test results: 306 passing (all tests passing!) This completes the schema sync work. All tests now pass with the new FormatId discriminated union and updated asset dimension requirements. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: simplify FormatId to single type with optional dimensions After PR #246 was merged upstream, the FormatId schema was simplified from a complex oneOf/not/anyOf discriminated union to a single type with optional width/height fields using JSON Schema dependencies. This eliminates the need for: - FormatId1 and FormatId2 union types - fix_format_id_references() and fix_format_id_union_references() workarounds - 25+ model_rebuild() calls for types referencing FormatId Changes: - Updated schemas from upstream (format-id.json now uses dependencies) - Regenerated types with single FormatId class - Removed FormatId union workarounds from post_generate_fixes.py - Simplified model_rebuild() calls in consolidate_exports.py (removed all FormatId-related rebuilds) - Updated type imports in __init__.py and test files All 306 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: add FormatIdParameter to __all__ exports Fixes linter error F401 where FormatIdParameter was imported but not included in __all__. This was missed when removing FormatId1 and FormatId2 from the exports. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: update to ADCP v2 schema version Switch from v1 to v2 to track the latest 2.x.x branch of the AdCP specification. All schemas are currently identical between v1 and v2, so no code changes are required. All 306 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: update schema cache hashes for v2 endpoint Updates the cached schema index URL from v1 to v2 following the ADCP_VERSION update. The index hash remains the same as both endpoints currently serve identical schemas. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: preserve success state for A2A interim responses When A2A returns interim status (submitted/working), the response has: - success=True (request was successful) - data=None (no structured AdCP content yet) - status=SUBMITTED/WORKING Previously, _parse_response() would incorrectly set success=False when data=None, breaking the A2A protocol flow. This fix preserves the original success state so interim responses remain successful. For MCP, this doesn't change behavior since MCP always has data on completed tasks, and failed tasks already have success=False. Fixes the issue where A2A agents returning 'working' status would be treated as failures instead of interim states. All 306 tests passing, including A2A interim response tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: update to AdCP 2.5.0 schema version Updates ADCP_VERSION from v2 to 2.5.0 to track the 2.5.x patch release line. This ensures the SDK picks up bug fixes and patches within 2.5.x without automatically jumping to 2.6.0 or higher. Changes: - Updated ADCP_VERSION to 2.5.0 - Synced schemas from 2.5.0 endpoint - Fixed schema $id references for code generation - Regenerated types (only timestamp changed) - Updated schema cache hashes All 306 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f307aae commit f7062f2

File tree

430 files changed

+4364
-12427
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

430 files changed

+4364
-12427
lines changed

ADCP_VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2.5.0

CLAUDE.md

Lines changed: 0 additions & 481 deletions
Large diffs are not rendered by default.

MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include ADCP_VERSION
2+
include README.md
3+
include LICENSE
4+
recursive-include src/adcp py.typed

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ where = ["src"]
6363
[tool.setuptools.package-data]
6464
adcp = ["py.typed"]
6565

66+
# Include ADCP_VERSION file in package
67+
[tool.setuptools]
68+
include-package-data = true
69+
70+
[tool.setuptools.data-files]
71+
"." = ["ADCP_VERSION"]
72+
6673
[tool.black]
6774
line-length = 100
6875
target-version = ["py310", "py311", "py312"]

schemas/cache/.hashes.json

Lines changed: 141 additions & 142 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)