adcp 2.12.1__py3-none-any.whl → 2.12.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. adcp/__init__.py +2 -1
  2. adcp/protocols/a2a.py +315 -204
  3. adcp/protocols/mcp.py +95 -16
  4. adcp/types/_generated.py +476 -97
  5. adcp/types/aliases.py +1 -3
  6. adcp/types/generated_poc/adagents.py +1 -1
  7. adcp/types/generated_poc/core/activation_key.py +1 -1
  8. adcp/types/generated_poc/core/assets/audio_asset.py +1 -1
  9. adcp/types/generated_poc/core/assets/css_asset.py +1 -1
  10. adcp/types/generated_poc/core/assets/daast_asset.py +1 -1
  11. adcp/types/generated_poc/core/assets/html_asset.py +1 -1
  12. adcp/types/generated_poc/core/assets/image_asset.py +1 -1
  13. adcp/types/generated_poc/core/assets/javascript_asset.py +1 -1
  14. adcp/types/generated_poc/core/assets/text_asset.py +1 -1
  15. adcp/types/generated_poc/core/assets/url_asset.py +1 -1
  16. adcp/types/generated_poc/core/assets/vast_asset.py +1 -1
  17. adcp/types/generated_poc/core/assets/video_asset.py +1 -1
  18. adcp/types/generated_poc/core/assets/webhook_asset.py +1 -1
  19. adcp/types/generated_poc/core/brand_manifest.py +1 -1
  20. adcp/types/generated_poc/core/context.py +1 -1
  21. adcp/types/generated_poc/core/creative_asset.py +1 -1
  22. adcp/types/generated_poc/core/creative_assignment.py +1 -1
  23. adcp/types/generated_poc/core/creative_filters.py +1 -1
  24. adcp/types/generated_poc/core/creative_manifest.py +1 -1
  25. adcp/types/generated_poc/core/creative_policy.py +1 -1
  26. adcp/types/generated_poc/core/delivery_metrics.py +1 -1
  27. adcp/types/generated_poc/core/deployment.py +1 -1
  28. adcp/types/generated_poc/core/destination.py +1 -1
  29. adcp/types/generated_poc/core/dimensions.py +1 -1
  30. adcp/types/generated_poc/core/error.py +1 -1
  31. adcp/types/generated_poc/core/ext.py +1 -1
  32. adcp/types/generated_poc/core/format.py +1 -1
  33. adcp/types/generated_poc/core/format_id.py +1 -1
  34. adcp/types/generated_poc/core/frequency_cap.py +1 -1
  35. adcp/types/generated_poc/core/measurement.py +1 -1
  36. adcp/types/generated_poc/core/media_buy.py +1 -1
  37. adcp/types/generated_poc/core/package.py +1 -1
  38. adcp/types/generated_poc/core/performance_feedback.py +1 -1
  39. adcp/types/generated_poc/core/placement.py +1 -1
  40. adcp/types/generated_poc/core/product.py +1 -1
  41. adcp/types/generated_poc/core/product_filters.py +1 -1
  42. adcp/types/generated_poc/core/promoted_offerings.py +1 -1
  43. adcp/types/generated_poc/core/promoted_products.py +1 -1
  44. adcp/types/generated_poc/core/property.py +1 -1
  45. adcp/types/generated_poc/core/property_id.py +1 -1
  46. adcp/types/generated_poc/core/property_tag.py +1 -1
  47. adcp/types/generated_poc/core/protocol_envelope.py +1 -1
  48. adcp/types/generated_poc/core/publisher_property_selector.py +1 -1
  49. adcp/types/generated_poc/core/push_notification_config.py +1 -1
  50. adcp/types/generated_poc/core/reporting_capabilities.py +1 -1
  51. adcp/types/generated_poc/core/response.py +1 -1
  52. adcp/types/generated_poc/core/signal_filters.py +1 -1
  53. adcp/types/generated_poc/core/sub_asset.py +1 -1
  54. adcp/types/generated_poc/core/targeting.py +1 -1
  55. adcp/types/generated_poc/core/webhook_payload.py +1 -1
  56. adcp/types/generated_poc/creative/list_creative_formats_request.py +1 -1
  57. adcp/types/generated_poc/creative/list_creative_formats_response.py +1 -1
  58. adcp/types/generated_poc/creative/preview_creative_request.py +1 -1
  59. adcp/types/generated_poc/creative/preview_creative_response.py +1 -1
  60. adcp/types/generated_poc/creative/preview_render.py +1 -1
  61. adcp/types/generated_poc/enums/adcp_domain.py +1 -1
  62. adcp/types/generated_poc/enums/asset_content_type.py +1 -1
  63. adcp/types/generated_poc/enums/auth_scheme.py +1 -1
  64. adcp/types/generated_poc/enums/available_metric.py +1 -1
  65. adcp/types/generated_poc/enums/channels.py +1 -1
  66. adcp/types/generated_poc/enums/co_branding_requirement.py +1 -1
  67. adcp/types/generated_poc/enums/creative_action.py +1 -1
  68. adcp/types/generated_poc/enums/creative_agent_capability.py +1 -1
  69. adcp/types/generated_poc/enums/creative_sort_field.py +1 -1
  70. adcp/types/generated_poc/enums/creative_status.py +1 -1
  71. adcp/types/generated_poc/enums/daast_tracking_event.py +1 -1
  72. adcp/types/generated_poc/enums/daast_version.py +1 -1
  73. adcp/types/generated_poc/enums/delivery_type.py +1 -1
  74. adcp/types/generated_poc/enums/dimension_unit.py +1 -1
  75. adcp/types/generated_poc/enums/feed_format.py +1 -1
  76. adcp/types/generated_poc/enums/feedback_source.py +1 -1
  77. adcp/types/generated_poc/enums/format_category.py +1 -1
  78. adcp/types/generated_poc/enums/format_id_parameter.py +1 -1
  79. adcp/types/generated_poc/enums/frequency_cap_scope.py +1 -1
  80. adcp/types/generated_poc/enums/history_entry_type.py +1 -1
  81. adcp/types/generated_poc/enums/http_method.py +1 -1
  82. adcp/types/generated_poc/enums/identifier_types.py +1 -1
  83. adcp/types/generated_poc/enums/javascript_module_type.py +1 -1
  84. adcp/types/generated_poc/enums/landing_page_requirement.py +1 -1
  85. adcp/types/generated_poc/enums/markdown_flavor.py +1 -1
  86. adcp/types/generated_poc/enums/media_buy_status.py +1 -1
  87. adcp/types/generated_poc/enums/metric_type.py +1 -1
  88. adcp/types/generated_poc/enums/notification_type.py +1 -1
  89. adcp/types/generated_poc/enums/pacing.py +1 -1
  90. adcp/types/generated_poc/enums/preview_output_format.py +1 -1
  91. adcp/types/generated_poc/enums/pricing_model.py +1 -1
  92. adcp/types/generated_poc/enums/property_type.py +1 -1
  93. adcp/types/generated_poc/enums/publisher_identifier_types.py +1 -1
  94. adcp/types/generated_poc/enums/reporting_frequency.py +1 -1
  95. adcp/types/generated_poc/enums/signal_catalog_type.py +1 -1
  96. adcp/types/generated_poc/enums/sort_direction.py +1 -1
  97. adcp/types/generated_poc/enums/standard_format_ids.py +1 -1
  98. adcp/types/generated_poc/enums/task_status.py +1 -1
  99. adcp/types/generated_poc/enums/task_type.py +1 -1
  100. adcp/types/generated_poc/enums/update_frequency.py +1 -1
  101. adcp/types/generated_poc/enums/url_asset_type.py +1 -1
  102. adcp/types/generated_poc/enums/validation_mode.py +1 -1
  103. adcp/types/generated_poc/enums/vast_tracking_event.py +1 -1
  104. adcp/types/generated_poc/enums/vast_version.py +1 -1
  105. adcp/types/generated_poc/enums/webhook_response_type.py +1 -1
  106. adcp/types/generated_poc/enums/webhook_security_method.py +1 -1
  107. adcp/types/generated_poc/media_buy/build_creative_request.py +1 -1
  108. adcp/types/generated_poc/media_buy/build_creative_response.py +1 -1
  109. adcp/types/generated_poc/media_buy/create_media_buy_request.py +1 -1
  110. adcp/types/generated_poc/media_buy/create_media_buy_response.py +1 -1
  111. adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +1 -1
  112. adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +1 -1
  113. adcp/types/generated_poc/media_buy/get_products_request.py +1 -1
  114. adcp/types/generated_poc/media_buy/get_products_response.py +1 -1
  115. adcp/types/generated_poc/media_buy/list_authorized_properties_request.py +1 -1
  116. adcp/types/generated_poc/media_buy/list_authorized_properties_response.py +1 -1
  117. adcp/types/generated_poc/media_buy/list_creative_formats_request.py +1 -1
  118. adcp/types/generated_poc/media_buy/list_creative_formats_response.py +1 -1
  119. adcp/types/generated_poc/media_buy/list_creatives_request.py +1 -1
  120. adcp/types/generated_poc/media_buy/list_creatives_response.py +1 -1
  121. adcp/types/generated_poc/media_buy/package_request.py +1 -1
  122. adcp/types/generated_poc/media_buy/provide_performance_feedback_request.py +1 -1
  123. adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +1 -1
  124. adcp/types/generated_poc/media_buy/sync_creatives_request.py +1 -1
  125. adcp/types/generated_poc/media_buy/sync_creatives_response.py +1 -1
  126. adcp/types/generated_poc/media_buy/update_media_buy_request.py +1 -1
  127. adcp/types/generated_poc/media_buy/update_media_buy_response.py +1 -1
  128. adcp/types/generated_poc/pricing_options/cpc_option.py +1 -1
  129. adcp/types/generated_poc/pricing_options/cpcv_option.py +1 -1
  130. adcp/types/generated_poc/pricing_options/cpm_auction_option.py +1 -1
  131. adcp/types/generated_poc/pricing_options/cpm_fixed_option.py +1 -1
  132. adcp/types/generated_poc/pricing_options/cpp_option.py +1 -1
  133. adcp/types/generated_poc/pricing_options/cpv_option.py +1 -1
  134. adcp/types/generated_poc/pricing_options/flat_rate_option.py +1 -1
  135. adcp/types/generated_poc/pricing_options/vcpm_auction_option.py +1 -1
  136. adcp/types/generated_poc/pricing_options/vcpm_fixed_option.py +1 -1
  137. adcp/types/generated_poc/protocols/adcp_extension.py +1 -1
  138. adcp/types/generated_poc/signals/activate_signal_request.py +1 -1
  139. adcp/types/generated_poc/signals/activate_signal_response.py +1 -1
  140. adcp/types/generated_poc/signals/get_signals_request.py +1 -1
  141. adcp/types/generated_poc/signals/get_signals_response.py +1 -1
  142. {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/METADATA +1 -1
  143. adcp-2.12.2.dist-info/RECORD +176 -0
  144. adcp-2.12.1.dist-info/RECORD +0 -176
  145. {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/WHEEL +0 -0
  146. {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/entry_points.txt +0 -0
  147. {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/licenses/LICENSE +0 -0
  148. {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/top_level.txt +0 -0
adcp/protocols/a2a.py CHANGED
@@ -1,10 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- """A2A protocol adapter using HTTP client.
4
-
5
- The official a2a-sdk is primarily for building A2A servers. For client functionality,
6
- we implement the A2A protocol using HTTP requests as per the A2A specification.
7
- """
3
+ """A2A protocol adapter using the official a2a-sdk client."""
8
4
 
9
5
  import logging
10
6
  import time
@@ -12,6 +8,17 @@ from typing import Any
12
8
  from uuid import uuid4
13
9
 
14
10
  import httpx
11
+ from a2a.client import A2ACardResolver, A2AClient
12
+ from a2a.types import (
13
+ DataPart,
14
+ Message,
15
+ MessageSendParams,
16
+ Part,
17
+ Role,
18
+ SendMessageRequest,
19
+ Task,
20
+ TextPart,
21
+ )
15
22
 
16
23
  from adcp.exceptions import (
17
24
  ADCPAuthenticationError,
@@ -25,157 +32,258 @@ logger = logging.getLogger(__name__)
25
32
 
26
33
 
27
34
  class A2AAdapter(ProtocolAdapter):
28
- """Adapter for A2A protocol following the Agent2Agent specification."""
35
+ """Adapter for A2A protocol using official a2a-sdk client."""
29
36
 
30
37
  def __init__(self, agent_config: AgentConfig):
31
- """Initialize A2A adapter with reusable HTTP client."""
38
+ """Initialize A2A adapter with official A2A client."""
32
39
  super().__init__(agent_config)
33
- self._client: httpx.AsyncClient | None = None
40
+ self._httpx_client: httpx.AsyncClient | None = None
41
+ self._a2a_client: A2AClient | None = None
34
42
 
35
- async def _get_client(self) -> httpx.AsyncClient:
43
+ async def _get_httpx_client(self) -> httpx.AsyncClient:
36
44
  """Get or create the HTTP client with connection pooling."""
37
- if self._client is None:
38
- # Configure connection pooling for better performance
45
+ if self._httpx_client is None:
39
46
  limits = httpx.Limits(
40
47
  max_keepalive_connections=10,
41
48
  max_connections=20,
42
49
  keepalive_expiry=30.0,
43
50
  )
44
- self._client = httpx.AsyncClient(limits=limits)
51
+
52
+ headers = {}
53
+ if self.agent_config.auth_token:
54
+ if self.agent_config.auth_type == "bearer":
55
+ headers["Authorization"] = f"Bearer {self.agent_config.auth_token}"
56
+ else:
57
+ headers[self.agent_config.auth_header] = self.agent_config.auth_token
58
+
59
+ self._httpx_client = httpx.AsyncClient(
60
+ limits=limits,
61
+ headers=headers,
62
+ timeout=self.agent_config.timeout,
63
+ )
45
64
  logger.debug(
46
65
  f"Created HTTP client with connection pooling for agent {self.agent_config.id}"
47
66
  )
48
- return self._client
67
+ return self._httpx_client
68
+
69
+ async def _get_a2a_client(self) -> A2AClient:
70
+ """Get or create the A2A client."""
71
+ if self._a2a_client is None:
72
+ httpx_client = await self._get_httpx_client()
73
+
74
+ # Use A2ACardResolver to fetch the agent card
75
+ card_resolver = A2ACardResolver(
76
+ httpx_client=httpx_client,
77
+ base_url=self.agent_config.agent_uri,
78
+ )
79
+
80
+ try:
81
+ agent_card = await card_resolver.get_agent_card()
82
+ logger.debug(f"Fetched agent card for {self.agent_config.id}")
83
+ except httpx.HTTPStatusError as e:
84
+ status_code = e.response.status_code
85
+ if status_code in (401, 403):
86
+ raise ADCPAuthenticationError(
87
+ f"Authentication failed: HTTP {status_code}",
88
+ agent_id=self.agent_config.id,
89
+ agent_uri=self.agent_config.agent_uri,
90
+ ) from e
91
+ else:
92
+ raise ADCPConnectionError(
93
+ f"Failed to fetch agent card: HTTP {status_code}",
94
+ agent_id=self.agent_config.id,
95
+ agent_uri=self.agent_config.agent_uri,
96
+ ) from e
97
+ except httpx.TimeoutException as e:
98
+ raise ADCPTimeoutError(
99
+ f"Timeout fetching agent card: {e}",
100
+ agent_id=self.agent_config.id,
101
+ agent_uri=self.agent_config.agent_uri,
102
+ timeout=self.agent_config.timeout,
103
+ ) from e
104
+ except httpx.HTTPError as e:
105
+ raise ADCPConnectionError(
106
+ f"Failed to fetch agent card: {e}",
107
+ agent_id=self.agent_config.id,
108
+ agent_uri=self.agent_config.agent_uri,
109
+ ) from e
110
+
111
+ self._a2a_client = A2AClient(
112
+ httpx_client=httpx_client,
113
+ agent_card=agent_card,
114
+ )
115
+ logger.debug(f"Created A2A client for agent {self.agent_config.id}")
116
+
117
+ return self._a2a_client
49
118
 
50
119
  async def close(self) -> None:
51
120
  """Close the HTTP client and clean up resources."""
52
- if self._client is not None:
121
+ if self._httpx_client is not None:
53
122
  logger.debug(f"Closing A2A adapter client for agent {self.agent_config.id}")
54
- await self._client.aclose()
55
- self._client = None
123
+ await self._httpx_client.aclose()
124
+ self._httpx_client = None
125
+ self._a2a_client = None
56
126
 
57
- async def _call_a2a_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
127
+ async def _call_a2a_tool(
128
+ self, tool_name: str, params: dict[str, Any], use_explicit_skill: bool = True
129
+ ) -> TaskResult[Any]:
58
130
  """
59
- Call a tool using A2A protocol.
131
+ Call a tool using A2A protocol via official a2a-sdk client.
132
+
133
+ Args:
134
+ tool_name: Name of the skill/tool to invoke
135
+ params: Parameters to pass to the skill
136
+ use_explicit_skill: If True, use explicit skill invocation (deterministic).
137
+ If False, use natural language (flexible).
60
138
 
61
- A2A uses a tasks/send endpoint to initiate tasks. The agent responds with
62
- task status and may require multiple roundtrips for completion.
139
+ The default is explicit skill invocation for predictable, repeatable behavior.
140
+ See: https://docs.adcontextprotocol.org/docs/protocols/a2a-guide
63
141
  """
64
142
  start_time = time.time() if self.agent_config.debug else None
65
- client = await self._get_client()
66
-
67
- headers = {"Content-Type": "application/json"}
68
-
69
- if self.agent_config.auth_token:
70
- # Support custom auth headers and types
71
- if self.agent_config.auth_type == "bearer":
72
- headers[self.agent_config.auth_header] = f"Bearer {self.agent_config.auth_token}"
73
- else:
74
- headers[self.agent_config.auth_header] = self.agent_config.auth_token
75
-
76
- # Construct A2A message
77
- message = {
78
- "role": "user",
79
- "parts": [
80
- {
81
- "type": "text",
82
- "text": self._format_tool_request(tool_name, params),
143
+ a2a_client = await self._get_a2a_client()
144
+
145
+ # Build A2A message
146
+ message_id = str(uuid4())
147
+
148
+ if use_explicit_skill:
149
+ # Explicit skill invocation (deterministic)
150
+ # Use DataPart with skill name and parameters
151
+ data_part = DataPart(
152
+ data={
153
+ "skill": tool_name,
154
+ "parameters": params,
83
155
  }
84
- ],
85
- }
156
+ )
157
+ message = Message(
158
+ message_id=message_id,
159
+ role=Role.user,
160
+ parts=[Part(root=data_part)],
161
+ )
162
+ else:
163
+ # Natural language invocation (flexible)
164
+ # Agent interprets intent from text
165
+ text_part = TextPart(text=self._format_tool_request(tool_name, params))
166
+ message = Message(
167
+ message_id=message_id,
168
+ role=Role.user,
169
+ parts=[Part(root=text_part)],
170
+ )
86
171
 
87
- # A2A uses message/send endpoint
88
- url = f"{self.agent_config.agent_uri}/message/send"
172
+ # Build request params
173
+ params_obj = MessageSendParams(message=message)
89
174
 
90
- request_data = {
91
- "message": message,
92
- "context_id": str(uuid4()),
93
- }
175
+ # Build request
176
+ request = SendMessageRequest(
177
+ id=str(uuid4()),
178
+ params=params_obj,
179
+ )
94
180
 
95
181
  debug_info = None
182
+ debug_request: dict[str, Any] = {}
96
183
  if self.agent_config.debug:
97
184
  debug_request = {
98
- "url": url,
99
- "method": "POST",
100
- "headers": {
101
- k: (
102
- v
103
- if k.lower() not in ("authorization", self.agent_config.auth_header.lower())
104
- else "***"
105
- )
106
- for k, v in headers.items()
107
- },
108
- "body": request_data,
185
+ "method": "send_message",
186
+ "message_id": message_id,
187
+ "tool": tool_name,
188
+ "params": params,
109
189
  }
110
190
 
111
191
  try:
112
- response = await client.post(
113
- url,
114
- json=request_data,
115
- headers=headers,
116
- timeout=self.agent_config.timeout,
117
- )
118
- response.raise_for_status()
192
+ # Use official A2A client
193
+ sdk_response = await a2a_client.send_message(request)
194
+
195
+ # SendMessageResponse is a RootModel union - unwrap it to get the actual response
196
+ # (either JSONRPCSuccessResponse or JSONRPCErrorResponse)
197
+ response = sdk_response.root if hasattr(sdk_response, "root") else sdk_response
198
+
199
+ # Handle JSON-RPC error response
200
+ if hasattr(response, "error"):
201
+ error_msg = response.error.message if response.error.message else "Unknown error"
202
+ if self.agent_config.debug and start_time:
203
+ duration_ms = (time.time() - start_time) * 1000
204
+ debug_info = DebugInfo(
205
+ request=debug_request,
206
+ response={"error": response.error.model_dump()},
207
+ duration_ms=duration_ms,
208
+ )
209
+ return TaskResult[Any](
210
+ status=TaskStatus.FAILED,
211
+ error=error_msg,
212
+ success=False,
213
+ debug_info=debug_info,
214
+ )
215
+
216
+ # Handle success response
217
+ if hasattr(response, "result"):
218
+ result = response.result
219
+
220
+ if self.agent_config.debug and start_time:
221
+ duration_ms = (time.time() - start_time) * 1000
222
+ debug_info = DebugInfo(
223
+ request=debug_request,
224
+ response={"result": result.model_dump()},
225
+ duration_ms=duration_ms,
226
+ )
119
227
 
120
- data = response.json()
228
+ # Result can be either Task or Message
229
+ if isinstance(result, Task):
230
+ return self._process_task_response(result, debug_info)
231
+ else:
232
+ # Message response (shouldn't happen for send_message, but handle it)
233
+ agent_id = self.agent_config.id
234
+ logger.warning(f"Received Message instead of Task from A2A agent {agent_id}")
235
+ return TaskResult[Any](
236
+ status=TaskStatus.COMPLETED,
237
+ data=None,
238
+ message="Received message response",
239
+ success=True,
240
+ debug_info=debug_info,
241
+ )
121
242
 
243
+ # Shouldn't reach here
244
+ return TaskResult[Any](
245
+ status=TaskStatus.FAILED,
246
+ error="Invalid response from A2A client",
247
+ success=False,
248
+ debug_info=debug_info,
249
+ )
250
+
251
+ except httpx.HTTPStatusError as e:
252
+ status_code = e.response.status_code
122
253
  if self.agent_config.debug and start_time:
123
254
  duration_ms = (time.time() - start_time) * 1000
124
255
  debug_info = DebugInfo(
125
256
  request=debug_request,
126
- response={"status": response.status_code, "body": data},
257
+ response={"error": str(e), "status_code": status_code},
127
258
  duration_ms=duration_ms,
128
259
  )
129
260
 
130
- # Parse A2A response format per canonical spec
131
- # A2A tasks have lifecycle: submitted, working, completed, failed, input-required
132
- task_status = data.get("status")
133
-
134
- if task_status == "completed":
135
- # Extract the result from the artifacts array
136
- result_data = self._extract_result(data)
137
-
138
- # Check for task-level errors in the payload
139
- errors = result_data.get("errors", []) if isinstance(result_data, dict) else []
140
- has_errors = bool(errors)
141
-
142
- return TaskResult[Any](
143
- status=TaskStatus.COMPLETED,
144
- data=result_data,
145
- message=self._extract_text_part(data),
146
- success=not has_errors,
147
- metadata={
148
- "task_id": data.get("taskId"),
149
- "context_id": data.get("contextId"),
150
- },
151
- debug_info=debug_info,
152
- )
153
- elif task_status == "failed":
154
- # Protocol-level failure - extract error message from TextPart
155
- error_msg = self._extract_text_part(data) or "Task failed"
156
- return TaskResult[Any](
157
- status=TaskStatus.FAILED,
158
- error=error_msg,
159
- success=False,
160
- debug_info=debug_info,
161
- )
261
+ if status_code in (401, 403):
262
+ error_msg = f"Authentication failed: HTTP {status_code}"
162
263
  else:
163
- # Handle all interim states (submitted, working, pending, input-required)
164
- # These don't need to have structured AdCP content - only completed responses do
165
- return TaskResult[Any](
166
- status=TaskStatus.SUBMITTED,
167
- data=None, # Interim responses may not have structured AdCP content
168
- message=self._extract_text_part(data),
169
- success=True,
170
- metadata={
171
- "task_id": data.get("taskId"),
172
- "context_id": data.get("contextId"),
173
- "status": task_status, # submitted, working, pending, input-required, etc.
174
- },
175
- debug_info=debug_info,
176
- )
264
+ error_msg = f"HTTP {status_code} error: {e}"
177
265
 
178
- except httpx.HTTPError as e:
266
+ return TaskResult[Any](
267
+ status=TaskStatus.FAILED,
268
+ error=error_msg,
269
+ success=False,
270
+ debug_info=debug_info,
271
+ )
272
+ except httpx.TimeoutException as e:
273
+ if self.agent_config.debug and start_time:
274
+ duration_ms = (time.time() - start_time) * 1000
275
+ debug_info = DebugInfo(
276
+ request=debug_request,
277
+ response={"error": str(e)},
278
+ duration_ms=duration_ms,
279
+ )
280
+ return TaskResult[Any](
281
+ status=TaskStatus.FAILED,
282
+ error=f"Timeout: {e}",
283
+ success=False,
284
+ debug_info=debug_info,
285
+ )
286
+ except Exception as e:
179
287
  if self.agent_config.debug and start_time:
180
288
  duration_ms = (time.time() - start_time) * 1000
181
289
  debug_info = DebugInfo(
@@ -190,16 +298,62 @@ class A2AAdapter(ProtocolAdapter):
190
298
  debug_info=debug_info,
191
299
  )
192
300
 
301
+ def _process_task_response(self, task: Task, debug_info: DebugInfo | None) -> TaskResult[Any]:
302
+ """Process a Task response from A2A into our TaskResult format."""
303
+ task_state = task.status.state
304
+
305
+ if task_state == "completed":
306
+ # Extract the result from the artifacts array
307
+ result_data = self._extract_result_from_task(task)
308
+
309
+ # Check for task-level errors in the payload
310
+ errors = result_data.get("errors", []) if isinstance(result_data, dict) else []
311
+ has_errors = bool(errors)
312
+
313
+ return TaskResult[Any](
314
+ status=TaskStatus.COMPLETED,
315
+ data=result_data,
316
+ message=self._extract_text_from_task(task),
317
+ success=not has_errors,
318
+ metadata={
319
+ "task_id": task.id,
320
+ "context_id": task.context_id,
321
+ },
322
+ debug_info=debug_info,
323
+ )
324
+ elif task_state == "failed":
325
+ # Protocol-level failure - extract error message from TextPart
326
+ error_msg = self._extract_text_from_task(task) or "Task failed"
327
+ return TaskResult[Any](
328
+ status=TaskStatus.FAILED,
329
+ error=error_msg,
330
+ success=False,
331
+ debug_info=debug_info,
332
+ )
333
+ else:
334
+ # Handle all interim states (submitted, working, input-required, etc.)
335
+ return TaskResult[Any](
336
+ status=TaskStatus.SUBMITTED,
337
+ data=None, # Interim responses may not have structured AdCP content
338
+ message=self._extract_text_from_task(task),
339
+ success=True,
340
+ metadata={
341
+ "task_id": task.id,
342
+ "context_id": task.context_id,
343
+ "status": task_state,
344
+ },
345
+ debug_info=debug_info,
346
+ )
347
+
193
348
  def _format_tool_request(self, tool_name: str, params: dict[str, Any]) -> str:
194
349
  """Format tool request as natural language for A2A."""
195
- # For AdCP tools, we format as a structured request
196
350
  import json
197
351
 
198
352
  return f"Execute tool: {tool_name}\nParameters: {json.dumps(params, indent=2)}"
199
353
 
200
- def _extract_result(self, response_data: dict[str, Any]) -> Any:
354
+ def _extract_result_from_task(self, task: Task) -> Any:
201
355
  """
202
- Extract result data from A2A response following canonical format.
356
+ Extract result data from A2A Task following canonical format.
203
357
 
204
358
  Per A2A response spec:
205
359
  - Responses MUST include at least one DataPart (kind: "data")
@@ -207,68 +361,55 @@ class A2AAdapter(ProtocolAdapter):
207
361
  - When multiple artifacts exist, use the last one (most recent in streaming)
208
362
  - DataParts contain structured AdCP payload
209
363
  """
210
- artifacts = response_data.get("artifacts", [])
211
-
212
- if not artifacts:
213
- logger.warning("A2A response missing required artifacts array")
214
- return response_data
364
+ if not task.artifacts:
365
+ logger.warning("A2A Task missing required artifacts array")
366
+ return {}
215
367
 
216
368
  # Use last artifact (most recent in streaming scenarios)
217
- # A2A spec doesn't define artifact.status, so we simply take the last one
218
- target_artifact = artifacts[-1]
369
+ target_artifact = task.artifacts[-1]
219
370
 
220
- parts = target_artifact.get("parts", [])
221
-
222
- if not parts:
223
- logger.warning("A2A response artifact has no parts")
224
- return response_data
371
+ if not target_artifact.parts:
372
+ logger.warning("A2A Task artifact has no parts")
373
+ return {}
225
374
 
226
375
  # Find all DataParts (kind: "data")
227
- data_parts = [p for p in parts if p.get("kind") == "data"]
376
+ # Note: Parts are wrapped in a Part union type, access via .root
377
+ from a2a.types import DataPart
378
+
379
+ data_parts = [p.root for p in target_artifact.parts if isinstance(p.root, DataPart)]
228
380
 
229
381
  if not data_parts:
230
- logger.warning("A2A response missing required DataPart (kind: 'data')")
231
- return response_data
382
+ logger.warning("A2A Task missing required DataPart (kind: 'data')")
383
+ return {}
232
384
 
233
385
  # Use last DataPart as authoritative (handles streaming scenarios within an artifact)
234
386
  last_data_part = data_parts[-1]
235
- data = last_data_part.get("data", {})
387
+ data = last_data_part.data
236
388
 
237
389
  # Some A2A implementations (e.g., ADK) wrap the response in {"response": {...}}
238
390
  # Unwrap it to get the actual AdCP payload if present
239
- # ADK is inconsistent - some DataParts have the wrapper, others don't
240
391
  if isinstance(data, dict) and "response" in data:
241
392
  # If response is the only key, unwrap completely
242
393
  if len(data) == 1:
243
394
  return data["response"]
244
395
  # If there are other keys alongside response, prefer the wrapped content
245
- # but keep it flexible for edge cases
246
396
  return data["response"]
247
397
 
248
398
  return data
249
399
 
250
- def _extract_text_part(self, response_data: dict[str, Any]) -> str | None:
251
- """
252
- Extract human-readable message from TextPart if present.
253
-
254
- Uses last artifact (same logic as _extract_result).
255
- """
256
- artifacts = response_data.get("artifacts", [])
257
-
258
- if not artifacts:
400
+ def _extract_text_from_task(self, task: Task) -> str | None:
401
+ """Extract human-readable message from TextPart if present."""
402
+ if not task.artifacts:
259
403
  return None
260
404
 
261
405
  # Use last artifact (most recent in streaming scenarios)
262
- # A2A spec doesn't define artifact.status, so we simply take the last one
263
- target_artifact = artifacts[-1]
264
-
265
- parts = target_artifact.get("parts", [])
406
+ target_artifact = task.artifacts[-1]
266
407
 
267
408
  # Find TextPart (kind: "text")
268
- for part in parts:
269
- if part.get("kind") == "text":
270
- text = part.get("text")
271
- return str(text) if text is not None else None
409
+ # Note: Parts are wrapped in a Part union type, access via .root
410
+ for part in target_artifact.parts:
411
+ if isinstance(part.root, TextPart):
412
+ return part.root.text
272
413
 
273
414
  return None
274
415
 
@@ -332,36 +473,17 @@ class A2AAdapter(ProtocolAdapter):
332
473
  """
333
474
  List available tools from A2A agent.
334
475
 
335
- Note: A2A doesn't have a standard tools/list endpoint. Agents expose
336
- their capabilities through the agent card. For AdCP, we rely on the
337
- standard AdCP tool set.
476
+ Uses A2A client which already fetched the agent card during initialization.
338
477
  """
339
- client = await self._get_client()
340
-
341
- headers = {"Content-Type": "application/json"}
342
-
343
- if self.agent_config.auth_token:
344
- # Support custom auth headers and types
345
- if self.agent_config.auth_type == "bearer":
346
- headers[self.agent_config.auth_header] = f"Bearer {self.agent_config.auth_token}"
347
- else:
348
- headers[self.agent_config.auth_header] = self.agent_config.auth_token
349
-
350
- # Try to fetch agent card from standard A2A location
351
- # A2A spec uses /.well-known/agent.json for agent card
352
- url = f"{self.agent_config.agent_uri}/.well-known/agent.json"
353
-
354
- logger.debug(f"Fetching A2A agent card for {self.agent_config.id} from {url}")
478
+ # Get the A2A client (which already fetched the agent card)
479
+ a2a_client = await self._get_a2a_client()
355
480
 
481
+ # Fetch the agent card using the official method
356
482
  try:
357
- response = await client.get(url, headers=headers, timeout=self.agent_config.timeout)
358
- response.raise_for_status()
359
-
360
- data = response.json()
483
+ agent_card = await a2a_client.get_card()
361
484
 
362
485
  # Extract skills from agent card
363
- skills = data.get("skills", [])
364
- tool_names = [skill.get("name", "") for skill in skills if skill.get("name")]
486
+ tool_names = [skill.name for skill in agent_card.skills if skill.name]
365
487
 
366
488
  logger.info(f"Found {len(tool_names)} tools from A2A agent {self.agent_config.id}")
367
489
  return tool_names
@@ -402,7 +524,7 @@ class A2AAdapter(ProtocolAdapter):
402
524
  """
403
525
  Get agent information including AdCP extension metadata from A2A agent card.
404
526
 
405
- Fetches the agent card from /.well-known/agent.json and extracts:
527
+ Uses A2A client's get_card() method to fetch the agent card and extracts:
406
528
  - Basic agent info (name, description, version)
407
529
  - AdCP extension (extensions.adcp.adcp_version, extensions.adcp.protocols_supported)
408
530
  - Available skills/tools
@@ -410,47 +532,36 @@ class A2AAdapter(ProtocolAdapter):
410
532
  Returns:
411
533
  Dictionary with agent metadata
412
534
  """
413
- client = await self._get_client()
414
-
415
- headers = {"Content-Type": "application/json"}
416
-
417
- if self.agent_config.auth_token:
418
- if self.agent_config.auth_type == "bearer":
419
- headers[self.agent_config.auth_header] = f"Bearer {self.agent_config.auth_token}"
420
- else:
421
- headers[self.agent_config.auth_header] = self.agent_config.auth_token
535
+ # Get the A2A client (which already fetched the agent card)
536
+ a2a_client = await self._get_a2a_client()
422
537
 
423
- url = f"{self.agent_config.agent_uri}/.well-known/agent.json"
424
-
425
- logger.debug(f"Fetching A2A agent info for {self.agent_config.id} from {url}")
538
+ logger.debug(f"Fetching A2A agent info for {self.agent_config.id}")
426
539
 
427
540
  try:
428
- response = await client.get(url, headers=headers, timeout=self.agent_config.timeout)
429
- response.raise_for_status()
430
-
431
- agent_card = response.json()
541
+ agent_card = await a2a_client.get_card()
432
542
 
433
543
  # Extract basic info
434
544
  info: dict[str, Any] = {
435
- "name": agent_card.get("name"),
436
- "description": agent_card.get("description"),
437
- "version": agent_card.get("version"),
545
+ "name": agent_card.name,
546
+ "description": agent_card.description,
547
+ "version": agent_card.version,
438
548
  "protocol": "a2a",
439
549
  }
440
550
 
441
551
  # Extract skills/tools
442
- skills = agent_card.get("skills", [])
443
- tool_names = [skill.get("name") for skill in skills if skill.get("name")]
552
+ tool_names = [skill.name for skill in agent_card.skills if skill.name]
444
553
  if tool_names:
445
554
  info["tools"] = tool_names
446
555
 
447
556
  # Extract AdCP extension metadata
448
- extensions = agent_card.get("extensions", {})
449
- adcp_ext = extensions.get("adcp", {})
450
-
451
- if adcp_ext:
452
- info["adcp_version"] = adcp_ext.get("adcp_version")
453
- info["protocols_supported"] = adcp_ext.get("protocols_supported")
557
+ # Note: AgentCard type doesn't include extensions in the SDK,
558
+ # but it may be present at runtime
559
+ extensions = getattr(agent_card, "extensions", None)
560
+ if extensions:
561
+ adcp_ext = extensions.get("adcp")
562
+ if adcp_ext:
563
+ info["adcp_version"] = adcp_ext.get("adcp_version")
564
+ info["protocols_supported"] = adcp_ext.get("protocols_supported")
454
565
 
455
566
  logger.info(f"Retrieved agent info for {self.agent_config.id}")
456
567
  return info