a2a-handler 0.1.24__tar.gz → 0.1.26__tar.gz

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 (61) hide show
  1. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/PKG-INFO +5 -5
  2. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/README.md +1 -1
  3. a2a_handler-0.1.26/pyproject.toml +95 -0
  4. a2a_handler-0.1.24/pyproject.toml → a2a_handler-0.1.26/pyproject.toml.orig +4 -4
  5. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/_helpers.py +6 -10
  6. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/card.py +2 -2
  7. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/message.py +55 -51
  8. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/mcp/server.py +2 -1
  9. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/__init__.py +3 -7
  10. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/app.py +18 -18
  11. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/card.py +47 -23
  12. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/service.py +253 -164
  13. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/artifacts.py +3 -1
  14. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/card.py +3 -2
  15. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/messages.py +7 -17
  16. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/tasks.py +58 -69
  17. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/tab.py +11 -10
  18. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/validation.py +62 -33
  19. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/webhook.py +4 -0
  20. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/LICENSE +0 -0
  21. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/__init__.py +0 -0
  22. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/auth.py +0 -0
  23. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/__init__.py +0 -0
  24. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/mcp.py +0 -0
  25. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/schema.py +0 -0
  26. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/server.py +0 -0
  27. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/session.py +0 -0
  28. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/task.py +0 -0
  29. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/__init__.py +0 -0
  30. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/config.py +0 -0
  31. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/dotenv.py +0 -0
  32. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/input_validation.py +0 -0
  33. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/logging.py +0 -0
  34. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/output.py +0 -0
  35. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/mcp/__init__.py +0 -0
  36. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/agent.py +0 -0
  37. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/ollama.py +0 -0
  38. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/tools/__init__.py +0 -0
  39. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/tools/a2a_docs.py +0 -0
  40. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/tools/handler_docs.py +0 -0
  41. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/tools/source.py +0 -0
  42. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/servers.py +0 -0
  43. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/session.py +0 -0
  44. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/__init__.py +0 -0
  45. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/app.py +0 -0
  46. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/app.tcss +0 -0
  47. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/commands/__init__.py +0 -0
  48. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/commands/palette.py +0 -0
  49. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/commands/screens.py +0 -0
  50. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/__init__.py +0 -0
  51. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/auth.py +0 -0
  52. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/headers.py +0 -0
  53. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/input.py +0 -0
  54. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/logs.py +0 -0
  55. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/__init__.py +0 -0
  56. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/save.py +0 -0
  57. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/session.py +0 -0
  58. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/tabs.py +0 -0
  59. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/types.py +0 -0
  60. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/views.py +0 -0
  61. {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: a2a-handler
3
- Version: 0.1.24
3
+ Version: 0.1.26
4
4
  Summary: An A2A Protocol client TUI and CLI.
5
5
  Keywords: a2a,a2a-protocol,agent,agent-to-agent,ai,cli,tui
6
6
  Author: Al Duncanson
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Classifier: Typing :: Typed
21
- Requires-Dist: a2a-sdk>=0.3.24,<1.0.0
21
+ Requires-Dist: a2a-sdk[http-server]>=1.0.0,<2.0.0
22
22
  Requires-Dist: click>=8.0.0
23
- Requires-Dist: google-adk>=2.0.0
23
+ Requires-Dist: google-adk[mcp]>=2.5.0
24
24
  Requires-Dist: httpx>=0.27.0
25
25
  Requires-Dist: litellm>=1.0.0
26
26
  Requires-Dist: python-dotenv>=1.0.0
@@ -28,7 +28,7 @@ Requires-Dist: rich>=13.0.0
28
28
  Requires-Dist: textual>=0.47.0
29
29
  Requires-Dist: uvicorn>=0.30.0
30
30
  Requires-Dist: textual-serve>=1.1.0
31
- Requires-Dist: mcp[cli]>=1.27.1
31
+ Requires-Dist: mcp[cli]>=1.27.1,<2
32
32
  Requires-Dist: truststore>=0.9.0
33
33
  Requires-Dist: platformdirs>=4.0.0
34
34
  Requires-Python: >=3.11
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
  # Handler
43
43
 
44
44
  [![CI](https://github.com/alDuncanson/handler/actions/workflows/ci.yml/badge.svg)](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
45
- [![A2A Protocol](https://img.shields.io/badge/A2A_Protocol-v0.3.0-blue)](https://a2a-protocol.org/latest/)
45
+ [![A2A Protocol](https://img.shields.io/badge/A2A_Protocol-v1.0.0-blue)](https://a2a-protocol.org/latest/)
46
46
  [![PyPI version](https://img.shields.io/pypi/v/a2a-handler)](https://pypi.org/project/a2a-handler/)
47
47
  [![PyPI - Status](https://img.shields.io/pypi/status/a2a-handler)](https://pypi.org/project/a2a-handler/)
48
48
  [![Pepy total downloads](https://img.shields.io/pepy/dt/a2a-handler?label=total%20downloads)](https://pepy.tech/projects/a2a-handler)
@@ -1,7 +1,7 @@
1
1
  # Handler
2
2
 
3
3
  [![CI](https://github.com/alDuncanson/handler/actions/workflows/ci.yml/badge.svg)](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
4
- [![A2A Protocol](https://img.shields.io/badge/A2A_Protocol-v0.3.0-blue)](https://a2a-protocol.org/latest/)
4
+ [![A2A Protocol](https://img.shields.io/badge/A2A_Protocol-v1.0.0-blue)](https://a2a-protocol.org/latest/)
5
5
  [![PyPI version](https://img.shields.io/pypi/v/a2a-handler)](https://pypi.org/project/a2a-handler/)
6
6
  [![PyPI - Status](https://img.shields.io/pypi/status/a2a-handler)](https://pypi.org/project/a2a-handler/)
7
7
  [![Pepy total downloads](https://img.shields.io/pepy/dt/a2a-handler?label=total%20downloads)](https://pepy.tech/projects/a2a-handler)
@@ -0,0 +1,95 @@
1
+ [project]
2
+ name = "a2a-handler"
3
+ version = "0.1.26"
4
+ description = "An A2A Protocol client TUI and CLI."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "GPL-3.0-or-later"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "a2a",
11
+ "a2a-protocol",
12
+ "agent",
13
+ "agent-to-agent",
14
+ "ai",
15
+ "cli",
16
+ "tui",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Environment :: Console",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Software Development :: Libraries :: Python Modules",
29
+ "Typing :: Typed",
30
+ ]
31
+ dependencies = [
32
+ "a2a-sdk[http-server]>=1.0.0,<2.0.0",
33
+ "click>=8.0.0",
34
+ "google-adk[mcp]>=2.5.0",
35
+ "httpx>=0.27.0",
36
+ "litellm>=1.0.0",
37
+ "python-dotenv>=1.0.0",
38
+ "rich>=13.0.0",
39
+ "textual>=0.47.0",
40
+ "uvicorn>=0.30.0",
41
+ "textual-serve>=1.1.0",
42
+ "mcp[cli]>=1.27.1,<2",
43
+ "truststore>=0.9.0",
44
+ "platformdirs>=4.0.0",
45
+ ]
46
+
47
+ [[project.authors]]
48
+ name = "Al Duncanson"
49
+ email = "alDuncanson@proton.me"
50
+
51
+ [project.urls]
52
+ Homepage = "https://github.com/alDuncanson/handler"
53
+ Repository = "https://github.com/alDuncanson/handler"
54
+ Documentation = "https://github.com/alDuncanson/handler/tree/main/docs"
55
+ Issues = "https://github.com/alDuncanson/handler/issues"
56
+ Changelog = "https://github.com/alDuncanson/handler/releases"
57
+
58
+ [project.scripts]
59
+ handler = "a2a_handler.cli:main"
60
+
61
+ [dependency-groups]
62
+ dev = [
63
+ "ruff>=0.8.0",
64
+ "textual-dev>=1.8.0",
65
+ "pytest>=8.0.0",
66
+ "pytest-asyncio>=0.23.0",
67
+ "ty>=0.0.1a27",
68
+ "pytest-cov>=7.1.0",
69
+ "pytest-textual-snapshot>=1.0.0",
70
+ ]
71
+
72
+ [build-system]
73
+ requires = ["uv_build>=0.9.15,<0.10.0"]
74
+ build-backend = "uv_build"
75
+
76
+ [tool.uv.build-backend]
77
+ module-root = "src"
78
+
79
+ [tool.pytest.ini_options]
80
+ asyncio_mode = "auto"
81
+
82
+ [tool.ruff.lint.per-file-ignores]
83
+ "src/a2a_handler/cli/__init__.py" = ["E402"]
84
+
85
+ [tool.coverage.run]
86
+ source = ["a2a_handler"]
87
+
88
+ [tool.coverage.report]
89
+ show_missing = true
90
+ skip_empty = true
91
+ exclude_lines = [
92
+ "pragma: no cover",
93
+ "if TYPE_CHECKING:",
94
+ "if __name__ == .__main__.",
95
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "a2a-handler"
3
- version = "0.1.24"
3
+ version = "0.1.26"
4
4
  description = "An A2A Protocol client TUI and CLI."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -32,9 +32,9 @@ classifiers = [
32
32
  "Typing :: Typed",
33
33
  ]
34
34
  dependencies = [
35
- "a2a-sdk>=0.3.24,<1.0.0",
35
+ "a2a-sdk[http-server]>=1.0.0,<2.0.0",
36
36
  "click>=8.0.0",
37
- "google-adk>=2.0.0",
37
+ "google-adk[mcp]>=2.5.0",
38
38
  "httpx>=0.27.0",
39
39
  "litellm>=1.0.0",
40
40
  "python-dotenv>=1.0.0",
@@ -42,7 +42,7 @@ dependencies = [
42
42
  "textual>=0.47.0",
43
43
  "uvicorn>=0.30.0",
44
44
  "textual-serve>=1.1.0",
45
- "mcp[cli]>=1.27.1",
45
+ "mcp[cli]>=1.27.1,<2",
46
46
  "truststore>=0.9.0",
47
47
  "platformdirs>=4.0.0",
48
48
  ]
@@ -8,8 +8,8 @@ import httpx
8
8
  import click
9
9
  from a2a.client.errors import (
10
10
  A2AClientError,
11
- A2AClientHTTPError,
12
11
  A2AClientTimeoutError,
12
+ AgentCardResolutionError,
13
13
  )
14
14
 
15
15
  from a2a_handler.auth import (
@@ -162,15 +162,11 @@ def handle_client_error(e: Exception, agent_url: str, output: Output | None) ->
162
162
  message = "Request timed out"
163
163
  error_code = "request_timeout"
164
164
  suggestion = "Retry the request or increase timeout settings"
165
- elif isinstance(e, A2AClientHTTPError):
166
- log.error("A2A client error: %s", e)
167
- if "connection" in str(e).lower():
168
- message = f"Connection failed: Is the server running at {agent_url}?"
169
- error_code = "connection_failed"
170
- suggestion = "Verify the agent URL and that the server is reachable"
171
- else:
172
- message = str(e)
173
- error_code = "a2a_http_error"
165
+ elif isinstance(e, AgentCardResolutionError):
166
+ log.error("Agent card resolution failed for %s: %s", agent_url, e)
167
+ message = f"Could not resolve agent card: {e}"
168
+ error_code = "agent_card_resolution_error"
169
+ suggestion = "Verify the agent URL and that the server exposes an agent card"
174
170
  details = {"agent_url": agent_url}
175
171
  elif isinstance(e, A2AClientError):
176
172
  log.error("A2A client error: %s", e)
@@ -8,7 +8,7 @@ from a2a.types import AgentCard
8
8
 
9
9
  from a2a_handler.common import Output, get_logger
10
10
  from a2a_handler.common.input_validation import InputValidationError, validate_agent_url
11
- from a2a_handler.service import A2AService
11
+ from a2a_handler.service import A2AService, to_json_dict
12
12
  from a2a_handler.validation import (
13
13
  ValidationResult,
14
14
  ValidationSource,
@@ -90,7 +90,7 @@ def _format_agent_card(card_data: object, output: Output) -> None:
90
90
  """Format and display an agent card as JSON."""
91
91
  card_dict: dict[str, Any]
92
92
  if isinstance(card_data, AgentCard):
93
- card_dict = card_data.model_dump(exclude_none=True)
93
+ card_dict = to_json_dict(card_data)
94
94
  elif isinstance(card_data, dict):
95
95
  card_dict = {str(key): value for key, value in card_data.items()}
96
96
  else:
@@ -2,12 +2,13 @@
2
2
 
3
3
  import asyncio
4
4
  import json
5
+ from collections.abc import Sequence
5
6
  from dataclasses import replace
6
7
  from typing import Any
7
8
  from typing import Optional
8
9
 
9
10
  import click
10
- from a2a.types import DataPart, FilePart, Message, Part, Role, TextPart
11
+ from a2a.types import Message, Part, Role
11
12
 
12
13
  from a2a_handler.auth import (
13
14
  AuthCredentials,
@@ -28,10 +29,16 @@ from a2a_handler.service import (
28
29
  A2AService,
29
30
  A2AResponse,
30
31
  StreamEvent,
32
+ part_data,
33
+ part_file,
34
+ part_kind,
35
+ part_text,
31
36
  protocol_dump,
32
37
  response_context_id,
33
38
  response_state,
34
39
  response_task_id,
40
+ state_label,
41
+ to_json_dict,
35
42
  )
36
43
  from a2a_handler.session import get_session, update_session
37
44
 
@@ -409,7 +416,7 @@ def _format_response_text(response: A2AResponse) -> str:
409
416
  lines: list[str] = []
410
417
  state = response_state(response)
411
418
  if state:
412
- lines.append(f"State: {state.value}")
419
+ lines.append(f"State: {state_label(state)}")
413
420
  context_id = response_context_id(response)
414
421
  if context_id:
415
422
  lines.append(f"Context ID: {context_id}")
@@ -435,7 +442,7 @@ def _format_response_parts(response: A2AResponse) -> str:
435
442
 
436
443
  if not formatted_parts and response.history:
437
444
  for message in response.history:
438
- if message.role == Role.agent:
445
+ if message.role == Role.ROLE_AGENT:
439
446
  formatted = _format_message_parts(message)
440
447
  if formatted:
441
448
  formatted_parts.append(formatted)
@@ -448,10 +455,10 @@ def _format_stream_event_text(event: StreamEvent) -> str:
448
455
  if event.message:
449
456
  return _format_message_parts(event.message)
450
457
 
451
- if event.status and event.status.status and event.status.status.message:
458
+ if event.status and event.status.status.HasField("message"):
452
459
  return _format_message_parts(event.status.status.message)
453
460
 
454
- if event.artifact and event.artifact.artifact:
461
+ if event.artifact and event.artifact.artifact.parts:
455
462
  return _format_parts(event.artifact.artifact.parts)
456
463
 
457
464
  if event.event_type == "task" and event.task:
@@ -466,7 +473,7 @@ def _format_stream_event_summaries(event: StreamEvent) -> list[str]:
466
473
 
467
474
  if event.state:
468
475
  summaries.append(
469
- _format_event_summary("task", event.state.value, event.task_id)
476
+ _format_event_summary("task", state_label(event.state), event.task_id)
470
477
  )
471
478
 
472
479
  parts = _stream_event_parts(event)
@@ -481,39 +488,40 @@ def _format_stream_event_summaries(event: StreamEvent) -> list[str]:
481
488
 
482
489
  def _stream_event_parts(event: StreamEvent) -> list[Part]:
483
490
  """Return the parts carried by a stream event, if any."""
484
- if event.message and event.message.role == Role.agent:
485
- return event.message.parts or []
486
- if event.status and event.status.status and event.status.status.message:
491
+ if event.message and event.message.role == Role.ROLE_AGENT:
492
+ return list(event.message.parts)
493
+ if event.status and event.status.status.HasField("message"):
487
494
  message = event.status.status.message
488
- if message.role == Role.agent:
489
- return message.parts or []
490
- if event.artifact and event.artifact.artifact:
491
- return event.artifact.artifact.parts or []
495
+ if message.role == Role.ROLE_AGENT:
496
+ return list(message.parts)
497
+ if event.artifact and event.artifact.artifact.parts:
498
+ return list(event.artifact.artifact.parts)
492
499
  return []
493
500
 
494
501
 
495
502
  def _format_part_event_summaries(part: Part, task_id: str | None) -> list[str]:
496
503
  """Summarize a stream part without rendering its full content."""
497
- root = part.root
498
- if isinstance(root, TextPart):
504
+ kind = part_kind(part)
505
+ if kind == "text":
499
506
  return [_format_event_summary("message", "text", task_id)]
500
- if isinstance(root, DataPart):
501
- if _is_tool_call_data(root.data):
507
+ if kind == "data":
508
+ data = part_data(part)
509
+ if _is_tool_call_data(data):
502
510
  return [
503
511
  _format_event_summary(
504
- "tool call", str(root.data.get("name", "unknown")), task_id
512
+ "tool call", str(data.get("name", "unknown")), task_id
505
513
  )
506
514
  ]
507
- if _is_tool_response_data(root.data):
515
+ if _is_tool_response_data(data):
508
516
  return [
509
517
  _format_event_summary(
510
- "tool result", str(root.data.get("name", "unknown")), task_id
518
+ "tool result", str(data.get("name", "unknown")), task_id
511
519
  )
512
520
  ]
513
521
  return [_format_event_summary("message", "data", task_id)]
514
- if isinstance(root, FilePart):
522
+ if kind == "file":
515
523
  return [_format_event_summary("message", "file", task_id)]
516
- return [_format_event_summary("message", getattr(root, "kind", "part"), task_id)]
524
+ return [_format_event_summary("message", kind, task_id)]
517
525
 
518
526
 
519
527
  def _format_event_summary(kind: str, detail: str, task_id: str | None) -> str:
@@ -542,12 +550,12 @@ def _stream_text_delta(emitted_text: str, event_text: str) -> str:
542
550
 
543
551
  def _format_message_parts(message: Message) -> str:
544
552
  """Format a protocol message's parts, skipping echoed user messages."""
545
- if message.role != Role.agent:
553
+ if message.role != Role.ROLE_AGENT:
546
554
  return ""
547
555
  return _format_parts(message.parts)
548
556
 
549
557
 
550
- def _format_parts(parts: list[Part] | None) -> str:
558
+ def _format_parts(parts: Sequence[Part] | None) -> str:
551
559
  """Format A2A message/artifact parts for human-readable CLI output."""
552
560
  if not parts:
553
561
  return ""
@@ -562,50 +570,46 @@ def _format_parts(parts: list[Part] | None) -> str:
562
570
 
563
571
  def _format_part(part: Part) -> str:
564
572
  """Format a single A2A part for human-readable CLI output."""
565
- root = part.root
566
- if isinstance(root, TextPart):
567
- return root.text
568
- if isinstance(root, DataPart):
569
- return _format_data_part(root)
570
- if isinstance(root, FilePart):
571
- return _format_file_part(root)
573
+ kind = part_kind(part)
574
+ if kind == "text":
575
+ return part_text(part)
576
+ if kind == "data":
577
+ return _format_data_part(part_data(part))
578
+ if kind == "file":
579
+ return _format_file_part(part)
572
580
 
573
- return "```json\n" + json.dumps(part.model_dump(mode="json"), indent=2) + "\n```"
581
+ return "```json\n" + json.dumps(to_json_dict(part), indent=2) + "\n```"
574
582
 
575
583
 
576
- def _format_data_part(part: DataPart) -> str:
584
+ def _format_data_part(data: Any) -> str:
577
585
  """Format structured data parts while hiding internal tool chatter."""
578
- data = part.data
579
586
  if _is_tool_call_data(data) or _is_tool_response_data(data):
580
587
  return ""
581
588
  return "```json\n" + json.dumps(data, indent=2, default=str) + "\n```"
582
589
 
583
590
 
584
- def _is_tool_call_data(data: dict[str, Any]) -> bool:
591
+ def _is_tool_call_data(data: Any) -> bool:
585
592
  """Return whether a data part looks like an internal tool call."""
586
- return {"id", "name", "args"}.issubset(data)
593
+ return isinstance(data, dict) and {"id", "name", "args"}.issubset(data)
587
594
 
588
595
 
589
- def _is_tool_response_data(data: dict[str, Any]) -> bool:
596
+ def _is_tool_response_data(data: Any) -> bool:
590
597
  """Return whether a data part looks like an internal tool response."""
591
- return {"id", "name", "response"}.issubset(data)
598
+ return isinstance(data, dict) and {"id", "name", "response"}.issubset(data)
592
599
 
593
600
 
594
- def _format_file_part(part: FilePart) -> str:
601
+ def _format_file_part(part: Part) -> str:
595
602
  """Format file parts without dumping inline bytes into the terminal."""
596
- file_part = part.file
597
- name = getattr(file_part, "name", None) or "unnamed file"
598
- mime_type = getattr(file_part, "mime_type", None)
599
- uri = getattr(file_part, "uri", None)
600
- inline_bytes = getattr(file_part, "bytes", None)
603
+ info = part_file(part)
604
+ name = info.get("name") or "unnamed file"
601
605
 
602
606
  details = [name]
603
- if mime_type:
604
- details.append(mime_type)
605
- if uri:
606
- details.append(uri)
607
- elif inline_bytes:
608
- details.append(f"inline bytes, {len(inline_bytes)} base64 chars")
607
+ if info.get("media_type"):
608
+ details.append(info["media_type"])
609
+ if info.get("uri"):
610
+ details.append(info["uri"])
611
+ elif info.get("num_bytes"):
612
+ details.append(f"inline bytes, {info['num_bytes']} bytes")
609
613
  return "[file: " + ", ".join(details) + "]"
610
614
 
611
615
 
@@ -617,7 +621,7 @@ def _stream_event_dump(event: StreamEvent) -> dict[str, object]:
617
621
  if event.task_id:
618
622
  data["taskId"] = event.task_id
619
623
  if event.state:
620
- data["state"] = event.state.value
624
+ data["state"] = state_label(event.state)
621
625
  if event.text:
622
626
  data["text"] = event.text
623
627
  if event.message:
@@ -28,6 +28,7 @@ from a2a_handler.service import (
28
28
  push_config_dump,
29
29
  response_context_id,
30
30
  response_task_id,
31
+ to_json_dict,
31
32
  )
32
33
  from a2a_handler.session import (
33
34
  clear_session,
@@ -220,7 +221,7 @@ def create_mcp_server() -> FastMCP:
220
221
  service = A2AService(http_client, agent_url, credentials=credentials)
221
222
  card = await service.get_card()
222
223
 
223
- return card.model_dump(exclude_none=True)
224
+ return to_json_dict(card)
224
225
 
225
226
  @mcp.tool()
226
227
  async def send_message(
@@ -60,13 +60,9 @@ def run_server(
60
60
  agent = create_llm_agent(model=effective_model)
61
61
  agent_card = build_agent_card(agent, host, port, require_auth=require_auth)
62
62
 
63
- streaming_enabled = (
64
- agent_card.capabilities.streaming if agent_card.capabilities else False
65
- )
66
- push_notifications_enabled = (
67
- agent_card.capabilities.push_notifications if agent_card.capabilities else False
68
- )
69
- auth_enabled = agent_card.security_schemes is not None
63
+ streaming_enabled = bool(agent_card.capabilities.streaming)
64
+ push_notifications_enabled = bool(agent_card.capabilities.push_notifications)
65
+ auth_enabled = bool(agent_card.security_schemes)
70
66
 
71
67
  logger.info(
72
68
  "Agent card capabilities: streaming=%s, push_notifications=%s, auth=%s",
@@ -5,15 +5,16 @@ from collections.abc import AsyncIterator, Awaitable, Callable
5
5
  from contextlib import asynccontextmanager
6
6
 
7
7
  import httpx
8
- from a2a.server.apps import A2AStarletteApplication
8
+ from a2a.server.context import ServerCallContext
9
9
  from a2a.server.request_handlers import DefaultRequestHandler
10
+ from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
10
11
  from a2a.server.tasks import (
11
12
  BasePushNotificationSender,
12
13
  InMemoryPushNotificationConfigStore,
13
14
  InMemoryTaskStore,
14
15
  )
15
- from a2a.types import AgentCard, InvalidParamsError, PushNotificationConfig
16
- from a2a.utils.errors import ServerError
16
+ from a2a.types import AgentCard, TaskPushNotificationConfig
17
+ from a2a.utils.errors import InvalidParamsError
17
18
  from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
18
19
  from google.adk.agents.llm_agent import Agent
19
20
  from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
@@ -48,7 +49,8 @@ class ValidatingPushNotificationConfigStore(InMemoryPushNotificationConfigStore)
48
49
  async def set_info(
49
50
  self,
50
51
  task_id: str,
51
- notification_config: PushNotificationConfig,
52
+ notification_config: TaskPushNotificationConfig,
53
+ context: ServerCallContext,
52
54
  ) -> None:
53
55
  """Validate webhook settings before storing push configuration."""
54
56
  try:
@@ -57,13 +59,11 @@ class ValidatingPushNotificationConfigStore(InMemoryPushNotificationConfigStore)
57
59
  if notification_config.token:
58
60
  reject_control_chars(notification_config.token, "webhook_token")
59
61
  except InputValidationError as error:
60
- raise ServerError(
61
- error=InvalidParamsError(
62
- message=f"{error.code}: {error.message}",
63
- )
62
+ raise InvalidParamsError(
63
+ message=f"{error.code}: {error.message}",
64
64
  ) from error
65
65
 
66
- await super().set_info(task_id, notification_config)
66
+ await super().set_info(task_id, notification_config, context)
67
67
 
68
68
 
69
69
  def generate_api_key() -> str:
@@ -186,6 +186,7 @@ def create_a2a_application(
186
186
  request_handler = DefaultRequestHandler(
187
187
  agent_executor=agent_executor,
188
188
  task_store=task_store,
189
+ agent_card=agent_card,
189
190
  push_config_store=push_notification_config_store,
190
191
  push_sender=push_notification_sender,
191
192
  )
@@ -196,13 +197,13 @@ def create_a2a_application(
196
197
  Middleware(APIKeyAuthMiddleware, api_key=api_key) # type: ignore[arg-type]
197
198
  )
198
199
 
199
- async def setup_a2a_routes(application: Starlette) -> None:
200
- a2a_starlette_app = A2AStarletteApplication(
201
- agent_card=agent_card,
202
- http_handler=request_handler,
203
- )
204
- a2a_starlette_app.add_routes_to_app(application)
205
- logger.info("A2A routes configured with push notification support")
200
+ # Serve the agent card plus a JSON-RPC endpoint. ``enable_v0_3_compat``
201
+ # lets legacy v0.3 clients call the same endpoint as v1.0 clients.
202
+ routes = [
203
+ *create_agent_card_routes(agent_card),
204
+ *create_jsonrpc_routes(request_handler, rpc_url="/", enable_v0_3_compat=True),
205
+ ]
206
+ logger.info("A2A routes configured with push notification support")
206
207
 
207
208
  async def cleanup_http_client() -> None:
208
209
  await http_client.aclose()
@@ -210,12 +211,11 @@ def create_a2a_application(
210
211
 
211
212
  @asynccontextmanager
212
213
  async def lifespan(application: Starlette) -> AsyncIterator[None]:
213
- await setup_a2a_routes(application)
214
214
  try:
215
215
  yield
216
216
  finally:
217
217
  await cleanup_http_client()
218
218
 
219
- application = Starlette(middleware=middleware, lifespan=lifespan)
219
+ application = Starlette(routes=routes, middleware=middleware, lifespan=lifespan)
220
220
 
221
221
  return application
@@ -5,10 +5,17 @@ from importlib.metadata import version as package_version
5
5
  from a2a.types import (
6
6
  AgentCapabilities,
7
7
  AgentCard,
8
+ AgentInterface,
8
9
  AgentSkill,
9
10
  APIKeySecurityScheme,
10
- In,
11
+ SecurityRequirement,
11
12
  SecurityScheme,
13
+ StringList,
14
+ )
15
+ from a2a.utils.constants import (
16
+ PROTOCOL_VERSION_0_3,
17
+ PROTOCOL_VERSION_1_0,
18
+ TransportProtocol,
12
19
  )
13
20
  from google.adk.agents.llm_agent import Agent
14
21
 
@@ -25,6 +32,9 @@ def build_agent_card(
25
32
  ) -> AgentCard:
26
33
  """Build an AgentCard with streaming and push notification capabilities.
27
34
 
35
+ The card advertises both A2A v1.0 and v0.3 JSON-RPC interfaces so that
36
+ both current and legacy clients can connect to the built-in server.
37
+
28
38
  Args:
29
39
  agent: The ADK agent
30
40
  host: Host address for the RPC URL
@@ -111,30 +121,44 @@ def build_agent_card(
111
121
 
112
122
  logger.debug("Building agent card with RPC URL: %s", rpc_endpoint_url)
113
123
 
114
- security_schemes: dict[str, SecurityScheme] | None = None
115
- security: list[dict[str, list[str]]] | None = None
124
+ # Advertise both protocol versions on the same JSON-RPC endpoint so v0.3
125
+ # clients keep working alongside v1.0 clients.
126
+ supported_interfaces = [
127
+ AgentInterface(
128
+ url=rpc_endpoint_url,
129
+ protocol_binding=TransportProtocol.JSONRPC.value,
130
+ protocol_version=PROTOCOL_VERSION_1_0,
131
+ ),
132
+ AgentInterface(
133
+ url=rpc_endpoint_url,
134
+ protocol_binding=TransportProtocol.JSONRPC.value,
135
+ protocol_version=PROTOCOL_VERSION_0_3,
136
+ ),
137
+ ]
138
+
139
+ card_kwargs: dict = {
140
+ "name": agent.name,
141
+ "description": agent.description or "Handler A2A agent",
142
+ "version": package_version("a2a-handler"),
143
+ "supported_interfaces": supported_interfaces,
144
+ "capabilities": agent_capabilities,
145
+ "skills": skills,
146
+ "default_input_modes": ["text/plain"],
147
+ "default_output_modes": ["text/plain"],
148
+ }
116
149
 
117
150
  if require_auth:
118
- api_key_scheme = SecurityScheme(
119
- root=APIKeySecurityScheme(
120
- type="apiKey",
121
- name="X-API-Key",
122
- in_=In.header,
151
+ card_kwargs["security_schemes"] = {
152
+ "apiKey": SecurityScheme(
153
+ api_key_security_scheme=APIKeySecurityScheme(
154
+ name="X-API-Key",
155
+ location="header",
156
+ )
123
157
  )
124
- )
125
- security_schemes = {"apiKey": api_key_scheme}
126
- security = [{"apiKey": []}]
158
+ }
159
+ card_kwargs["security_requirements"] = [
160
+ SecurityRequirement(schemes={"apiKey": StringList()})
161
+ ]
127
162
  logger.info("API key authentication enabled")
128
163
 
129
- return AgentCard(
130
- name=agent.name,
131
- description=agent.description or "Handler A2A agent",
132
- url=rpc_endpoint_url,
133
- version=package_version("a2a-handler"),
134
- capabilities=agent_capabilities,
135
- skills=skills,
136
- default_input_modes=["text/plain"],
137
- default_output_modes=["text/plain"],
138
- security_schemes=security_schemes,
139
- security=security,
140
- )
164
+ return AgentCard(**card_kwargs)