opencode-a2a 0.4.0__tar.gz → 0.4.1__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 (129) hide show
  1. {opencode_a2a-0.4.0/src/opencode_a2a.egg-info → opencode_a2a-0.4.1}/PKG-INFO +2 -2
  2. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/README.md +1 -1
  3. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/docs/guide.md +1 -1
  4. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/cli.py +2 -10
  5. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/client/__init__.py +0 -4
  6. opencode_a2a-0.4.1/src/opencode_a2a/client/agent_card.py +81 -0
  7. opencode_a2a-0.4.1/src/opencode_a2a/client/client.py +317 -0
  8. opencode_a2a-0.4.1/src/opencode_a2a/client/error_mapping.py +148 -0
  9. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/client/errors.py +21 -0
  10. opencode_a2a-0.4.1/src/opencode_a2a/client/payload_text.py +180 -0
  11. opencode_a2a-0.4.1/src/opencode_a2a/client/request_context.py +86 -0
  12. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/config.py +20 -18
  13. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/contracts/extensions.py +2 -6
  14. opencode_a2a-0.4.1/src/opencode_a2a/error_taxonomy.py +95 -0
  15. opencode_a2a-0.4.1/src/opencode_a2a/execution/event_helpers.py +37 -0
  16. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/execution/executor.py +82 -776
  17. opencode_a2a-0.4.1/src/opencode_a2a/execution/session_manager.py +140 -0
  18. opencode_a2a-0.4.1/src/opencode_a2a/execution/stream_runtime.py +578 -0
  19. opencode_a2a-0.4.1/src/opencode_a2a/execution/tool_error_mapping.py +141 -0
  20. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/execution/upstream_errors.py +6 -82
  21. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/jsonrpc/application.py +130 -241
  22. opencode_a2a-0.4.1/src/opencode_a2a/jsonrpc/error_mapping.py +137 -0
  23. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/parts/text.py +0 -4
  24. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/profile/runtime.py +7 -6
  25. opencode_a2a-0.4.1/src/opencode_a2a/sandbox_policy.py +111 -0
  26. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/server/application.py +145 -15
  27. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1/src/opencode_a2a.egg-info}/PKG-INFO +2 -2
  28. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a.egg-info/SOURCES.txt +17 -1
  29. opencode_a2a-0.4.1/tests/client/test_agent_card.py +68 -0
  30. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/client/test_client_facade.py +15 -299
  31. opencode_a2a-0.4.1/tests/client/test_error_mapping.py +81 -0
  32. opencode_a2a-0.4.1/tests/client/test_payload_text.py +104 -0
  33. opencode_a2a-0.4.1/tests/client/test_request_context.py +54 -0
  34. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/config/test_settings.py +34 -4
  35. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_cancellation.py +3 -3
  36. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_directory_validation.py +20 -8
  37. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_metrics.py +0 -2
  38. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_opencode_agent_session_binding.py +148 -25
  39. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_session_ownership.py +20 -17
  40. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_streaming_output_contract_core.py +41 -0
  41. opencode_a2a-0.4.1/tests/jsonrpc/test_error_mapping.py +79 -0
  42. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/jsonrpc/test_jsonrpc_unsupported_method.py +35 -0
  43. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/jsonrpc/test_opencode_session_extension_commands.py +5 -2
  44. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/jsonrpc/test_opencode_session_extension_prompt_async.py +8 -7
  45. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/parts/test_parts_text.py +2 -2
  46. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/profile/test_profile_runtime.py +17 -0
  47. opencode_a2a-0.4.1/tests/server/test_a2a_client_manager.py +199 -0
  48. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/server/test_agent_card.py +19 -0
  49. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/server/test_app_behaviors.py +1 -1
  50. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/server/test_cli.py +6 -15
  51. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/server/test_transport_contract.py +19 -23
  52. opencode_a2a-0.4.1/tests/support/session_extensions.py +19 -0
  53. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/upstream/test_opencode_upstream_client_params.py +33 -0
  54. opencode_a2a-0.4.0/src/opencode_a2a/client/client.py +0 -608
  55. opencode_a2a-0.4.0/src/opencode_a2a/client/types.py +0 -21
  56. opencode_a2a-0.4.0/tests/support/session_extensions.py +0 -10
  57. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/.github/workflows/ci.yml +0 -0
  58. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/.github/workflows/dependency-health.yml +0 -0
  59. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/.github/workflows/publish.yml +0 -0
  60. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/.gitignore +0 -0
  61. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/.pre-commit-config.yaml +0 -0
  62. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/.secrets.baseline +0 -0
  63. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/AGENTS.md +0 -0
  64. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/CONTRIBUTING.md +0 -0
  65. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/LICENSE +0 -0
  66. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/SECURITY.md +0 -0
  67. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/pyproject.toml +0 -0
  68. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/scripts/README.md +0 -0
  69. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/scripts/check_coverage.py +0 -0
  70. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/scripts/dependency_health.sh +0 -0
  71. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/scripts/doctor.sh +0 -0
  72. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/scripts/health_common.sh +0 -0
  73. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/scripts/lint.sh +0 -0
  74. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/scripts/smoke_test_built_cli.sh +0 -0
  75. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/setup.cfg +0 -0
  76. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/__init__.py +0 -0
  77. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/client/config.py +0 -0
  78. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/contracts/__init__.py +0 -0
  79. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/execution/__init__.py +0 -0
  80. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/execution/request_context.py +0 -0
  81. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/execution/stream_events.py +0 -0
  82. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/execution/stream_state.py +0 -0
  83. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/jsonrpc/__init__.py +0 -0
  84. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/jsonrpc/methods.py +0 -0
  85. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/jsonrpc/params.py +0 -0
  86. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/opencode_upstream_client.py +0 -0
  87. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/parts/__init__.py +0 -0
  88. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/parts/mapping.py +0 -0
  89. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/profile/__init__.py +0 -0
  90. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/server/__init__.py +0 -0
  91. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/server/agent_card.py +0 -0
  92. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/server/openapi.py +0 -0
  93. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a/server/request_parsing.py +0 -0
  94. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a.egg-info/dependency_links.txt +0 -0
  95. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a.egg-info/entry_points.txt +0 -0
  96. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a.egg-info/requires.txt +0 -0
  97. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/src/opencode_a2a.egg-info/top_level.txt +0 -0
  98. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/__init__.py +0 -0
  99. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/client/__init__.py +0 -0
  100. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/client/test_client_config.py +0 -0
  101. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/config/__init__.py +0 -0
  102. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/contracts/__init__.py +0 -0
  103. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/contracts/test_extension_contract_consistency.py +0 -0
  104. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/__init__.py +0 -0
  105. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_agent_errors.py +0 -0
  106. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_agent_helpers.py +0 -0
  107. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_multipart_input.py +0 -0
  108. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_streaming_output_contract_blocks.py +0 -0
  109. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_streaming_output_contract_interrupts.py +0 -0
  110. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/execution/test_streaming_output_contract_logging.py +0 -0
  111. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/jsonrpc/__init__.py +0 -0
  112. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/jsonrpc/test_jsonrpc_methods.py +0 -0
  113. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/jsonrpc/test_jsonrpc_params.py +0 -0
  114. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/jsonrpc/test_opencode_session_extension_interrupts.py +0 -0
  115. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/jsonrpc/test_opencode_session_extension_queries.py +0 -0
  116. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/package/__init__.py +0 -0
  117. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/package/test_version.py +0 -0
  118. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/parts/__init__.py +0 -0
  119. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/profile/__init__.py +0 -0
  120. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/scripts/__init__.py +0 -0
  121. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/scripts/test_script_health_contract.py +0 -0
  122. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/server/__init__.py +0 -0
  123. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/server/test_call_context_builder.py +0 -0
  124. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/server/test_cancel_contract.py +0 -0
  125. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/support/__init__.py +0 -0
  126. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/support/helpers.py +0 -0
  127. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/support/streaming_output.py +0 -0
  128. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/tests/upstream/__init__.py +0 -0
  129. {opencode_a2a-0.4.0 → opencode_a2a-0.4.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-a2a
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: OpenCode A2A runtime
5
5
  Author: liujuanjuan1984@Intelligent-Internet
6
6
  License-Expression: Apache-2.0
@@ -115,7 +115,7 @@ A2A_HOST=127.0.0.1 \
115
115
  A2A_PORT=8000 \
116
116
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
117
117
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
118
- opencode-a2a serve
118
+ opencode-a2a
119
119
  ```
120
120
 
121
121
  Verify that the service is up:
@@ -77,7 +77,7 @@ A2A_HOST=127.0.0.1 \
77
77
  A2A_PORT=8000 \
78
78
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
79
79
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
80
- opencode-a2a serve
80
+ opencode-a2a
81
81
  ```
82
82
 
83
83
  Verify that the service is up:
@@ -154,7 +154,7 @@ A2A_HOST=127.0.0.1 \
154
154
  A2A_PORT=8000 \
155
155
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
156
156
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
157
- opencode-a2a serve
157
+ opencode-a2a
158
158
  ```
159
159
 
160
160
  ## Troubleshooting Provider Auth State
@@ -53,7 +53,8 @@ def build_parser() -> argparse.ArgumentParser:
53
53
  parser = argparse.ArgumentParser(
54
54
  prog="opencode-a2a",
55
55
  description=(
56
- "OpenCode A2A runtime. Deployment supervision is intentionally left to the operator."
56
+ "OpenCode A2A runtime. Run without a subcommand to start the service."
57
+ " Deployment supervision is intentionally left to the operator."
57
58
  ),
58
59
  formatter_class=argparse.ArgumentDefaultsHelpFormatter,
59
60
  )
@@ -64,11 +65,6 @@ def build_parser() -> argparse.ArgumentParser:
64
65
  )
65
66
 
66
67
  subparsers = parser.add_subparsers(dest="command")
67
- subparsers.add_parser(
68
- "serve",
69
- help="Start the OpenCode A2A runtime using environment-based settings.",
70
- description="Start the OpenCode A2A runtime using environment-based settings.",
71
- )
72
68
 
73
69
  call_parser = subparsers.add_parser(
74
70
  "call",
@@ -95,10 +91,6 @@ def main(argv: Sequence[str] | None = None) -> int:
95
91
  return 0
96
92
 
97
93
  namespace = parser.parse_args(args)
98
- if namespace.command == "serve":
99
- serve_main()
100
- return 0
101
-
102
94
  if namespace.command == "call":
103
95
  return asyncio.run(run_call(namespace.agent_url, namespace.text, namespace.token))
104
96
 
@@ -10,7 +10,6 @@ from .errors import (
10
10
  A2AUnsupportedBindingError,
11
11
  A2AUnsupportedOperationError,
12
12
  )
13
- from .types import A2AClientEvent, A2AClientEventStream, A2AClientMetadata
14
13
 
15
14
  __all__ = [
16
15
  "A2AClient",
@@ -21,8 +20,5 @@ __all__ = [
21
20
  "A2AUnsupportedBindingError",
22
21
  "A2AUnsupportedOperationError",
23
22
  "A2AClientSettings",
24
- "A2AClientEvent",
25
- "A2AClientEventStream",
26
- "A2AClientMetadata",
27
23
  "load_settings",
28
24
  ]
@@ -0,0 +1,81 @@
1
+ """Helpers for agent-card URL normalization and resolver bootstrap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from urllib.parse import urlsplit, urlunsplit
7
+
8
+ import httpx
9
+ from a2a.client.card_resolver import A2ACardResolver
10
+ from a2a.utils.constants import (
11
+ AGENT_CARD_WELL_KNOWN_PATH,
12
+ EXTENDED_AGENT_CARD_PATH,
13
+ PREV_AGENT_CARD_WELL_KNOWN_PATH,
14
+ )
15
+
16
+ from .request_context import build_default_headers
17
+
18
+
19
+ def normalize_agent_card_endpoint(agent_url: str) -> tuple[str, str]:
20
+ parsed_url = urlsplit(agent_url)
21
+ if not parsed_url.scheme or not parsed_url.netloc:
22
+ raise ValueError(f"agent_url must be absolute URL: {agent_url}")
23
+
24
+ path = parsed_url.path or ""
25
+ normalized_no_leading = path.rstrip("/").lstrip("/")
26
+ candidate_paths = (
27
+ AGENT_CARD_WELL_KNOWN_PATH,
28
+ PREV_AGENT_CARD_WELL_KNOWN_PATH,
29
+ EXTENDED_AGENT_CARD_PATH,
30
+ )
31
+
32
+ base_path = normalized_no_leading
33
+ agent_card_path = AGENT_CARD_WELL_KNOWN_PATH
34
+ for candidate_path in candidate_paths:
35
+ card_suffix = candidate_path.lstrip("/")
36
+ if normalized_no_leading.endswith(card_suffix):
37
+ base_path = normalized_no_leading[: -len(card_suffix)].rstrip("/")
38
+ agent_card_path = candidate_path
39
+ break
40
+
41
+ base_url = urlunsplit(
42
+ (
43
+ parsed_url.scheme,
44
+ parsed_url.netloc,
45
+ f"/{base_path}" if base_path else "",
46
+ "",
47
+ "",
48
+ )
49
+ ).rstrip("/")
50
+ return base_url, agent_card_path
51
+
52
+
53
+ def build_agent_card_resolver(
54
+ agent_url: str,
55
+ httpx_client: httpx.AsyncClient,
56
+ ) -> A2ACardResolver:
57
+ base_url, agent_card_path = normalize_agent_card_endpoint(agent_url)
58
+ return A2ACardResolver(
59
+ httpx_client=httpx_client,
60
+ base_url=base_url,
61
+ agent_card_path=agent_card_path,
62
+ )
63
+
64
+
65
+ def build_resolver_http_kwargs(
66
+ *,
67
+ bearer_token: str | None,
68
+ timeout: float,
69
+ ) -> dict[str, Any]:
70
+ http_kwargs: dict[str, Any] = {"timeout": timeout}
71
+ default_headers = build_default_headers(bearer_token)
72
+ if default_headers:
73
+ http_kwargs["headers"] = default_headers
74
+ return http_kwargs
75
+
76
+
77
+ __all__ = [
78
+ "build_agent_card_resolver",
79
+ "build_resolver_http_kwargs",
80
+ "normalize_agent_card_endpoint",
81
+ ]
@@ -0,0 +1,317 @@
1
+ """A2A client facade for opencode-a2a consumers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import AsyncIterator, Mapping
7
+ from typing import Any, cast
8
+ from uuid import uuid4
9
+
10
+ import httpx
11
+ from a2a.client import Client, ClientConfig, ClientFactory
12
+ from a2a.client.errors import (
13
+ A2AClientHTTPError,
14
+ A2AClientJSONError,
15
+ A2AClientJSONRPCError,
16
+ )
17
+ from a2a.types import (
18
+ Message,
19
+ Part,
20
+ Role,
21
+ Task,
22
+ TaskArtifactUpdateEvent,
23
+ TaskIdParams,
24
+ TaskQueryParams,
25
+ TaskStatusUpdateEvent,
26
+ TextPart,
27
+ )
28
+
29
+ from .agent_card import build_agent_card_resolver, build_resolver_http_kwargs
30
+ from .config import A2AClientSettings, load_settings
31
+ from .error_mapping import (
32
+ map_agent_card_error,
33
+ map_operation_error,
34
+ )
35
+ from .errors import A2AUnsupportedBindingError
36
+ from .payload_text import extract_text as extract_text_from_payload
37
+ from .request_context import build_call_context, build_client_interceptors, split_request_metadata
38
+
39
+
40
+ class A2AClient:
41
+ """Factory-style facade for lightweight A2A client bootstrap and calls."""
42
+
43
+ def __init__(
44
+ self,
45
+ agent_url: str,
46
+ *,
47
+ settings: A2AClientSettings | None = None,
48
+ httpx_client: httpx.AsyncClient | None = None,
49
+ ) -> None:
50
+ if not agent_url or not agent_url.strip():
51
+ raise ValueError("agent_url must be non-empty")
52
+ self.agent_url = agent_url.rstrip("/")
53
+ self._settings = settings or load_settings({})
54
+ self._owns_httpx_client = httpx_client is None
55
+ self._httpx_client = httpx_client
56
+ self._client: Client | None = None
57
+ self._agent_card: object | None = None
58
+ self._lock = asyncio.Lock()
59
+ self._request_lock = asyncio.Lock()
60
+ self._active_requests = 0
61
+
62
+ async def close(self) -> None:
63
+ """Close cached client resources and owned HTTP transport."""
64
+ self._client = None
65
+ if self._httpx_client is not None and self._owns_httpx_client:
66
+ await self._httpx_client.aclose()
67
+
68
+ def is_busy(self) -> bool:
69
+ """Report whether this facade currently has in-flight work."""
70
+ return self._active_requests > 0
71
+
72
+ async def get_agent_card(self) -> Any:
73
+ """Fetch and cache peer Agent Card."""
74
+ if self._agent_card is not None:
75
+ return self._agent_card
76
+
77
+ resolver = build_agent_card_resolver(
78
+ self.agent_url,
79
+ await self._get_httpx_client(),
80
+ )
81
+ try:
82
+ card = await resolver.get_agent_card(
83
+ http_kwargs=build_resolver_http_kwargs(
84
+ bearer_token=self._settings.bearer_token,
85
+ timeout=self._settings.card_fetch_timeout,
86
+ )
87
+ )
88
+ except (
89
+ A2AClientHTTPError,
90
+ A2AClientJSONError,
91
+ httpx.TimeoutException,
92
+ httpx.TransportError,
93
+ ) as exc:
94
+ raise map_agent_card_error(exc) from exc
95
+ self._agent_card = card
96
+ return card
97
+
98
+ async def send_message(
99
+ self,
100
+ text: str,
101
+ *,
102
+ context_id: str | None = None,
103
+ task_id: str | None = None,
104
+ message_id: str | None = None,
105
+ metadata: Mapping[str, Any] | None = None,
106
+ extensions: list[str] | None = None,
107
+ ) -> AsyncIterator[
108
+ Message | tuple[Task, TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None] | None
109
+ ]:
110
+ """Send one user message and stream protocol events."""
111
+ await self._acquire_operation()
112
+ try:
113
+ client = await self._ensure_client()
114
+ request_metadata, extra_headers = split_request_metadata(metadata)
115
+ request = self._build_user_message(
116
+ text=text,
117
+ context_id=context_id,
118
+ task_id=task_id,
119
+ message_id=message_id,
120
+ )
121
+ try:
122
+ async for event in client.send_message(
123
+ request,
124
+ context=build_call_context(self._settings.bearer_token, extra_headers),
125
+ request_metadata=request_metadata,
126
+ extensions=extensions,
127
+ ):
128
+ yield event
129
+ except (
130
+ A2AClientHTTPError,
131
+ A2AClientJSONRPCError,
132
+ httpx.TimeoutException,
133
+ httpx.TransportError,
134
+ ) as exc:
135
+ raise map_operation_error("message/send", exc) from exc
136
+ finally:
137
+ await self._release_operation()
138
+
139
+ async def send(
140
+ self,
141
+ text: str,
142
+ *,
143
+ context_id: str | None = None,
144
+ task_id: str | None = None,
145
+ message_id: str | None = None,
146
+ metadata: Mapping[str, Any] | None = None,
147
+ extensions: list[str] | None = None,
148
+ ) -> Message | tuple[Task, TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None] | None:
149
+ """Send a message and return the terminal response/event."""
150
+ last_event: (
151
+ Message | tuple[Task, TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None] | None
152
+ ) = None
153
+ async for event in self.send_message(
154
+ text,
155
+ context_id=context_id,
156
+ task_id=task_id,
157
+ message_id=message_id,
158
+ metadata=metadata,
159
+ extensions=extensions,
160
+ ):
161
+ last_event = event
162
+ return last_event
163
+
164
+ async def get_task(
165
+ self,
166
+ task_id: str,
167
+ *,
168
+ history_length: int | None = None,
169
+ metadata: Mapping[str, Any] | None = None,
170
+ ) -> Task:
171
+ """Fetch one task by id."""
172
+ await self._acquire_operation()
173
+ try:
174
+ client = await self._ensure_client()
175
+ request_metadata, extra_headers = split_request_metadata(metadata)
176
+ try:
177
+ return await client.get_task(
178
+ TaskQueryParams(
179
+ id=task_id,
180
+ history_length=history_length,
181
+ metadata=request_metadata or {},
182
+ ),
183
+ context=build_call_context(self._settings.bearer_token, extra_headers),
184
+ )
185
+ except (
186
+ A2AClientHTTPError,
187
+ A2AClientJSONRPCError,
188
+ httpx.TimeoutException,
189
+ httpx.TransportError,
190
+ ) as exc:
191
+ raise map_operation_error("tasks/get", exc) from exc
192
+ finally:
193
+ await self._release_operation()
194
+
195
+ async def cancel_task(
196
+ self,
197
+ task_id: str,
198
+ *,
199
+ metadata: Mapping[str, Any] | None = None,
200
+ ) -> Task:
201
+ """Cancel one task by id."""
202
+ await self._acquire_operation()
203
+ try:
204
+ client = await self._ensure_client()
205
+ request_metadata, extra_headers = split_request_metadata(metadata)
206
+ try:
207
+ return await client.cancel_task(
208
+ TaskIdParams(id=task_id, metadata=request_metadata or {}),
209
+ context=build_call_context(self._settings.bearer_token, extra_headers),
210
+ )
211
+ except (
212
+ A2AClientHTTPError,
213
+ A2AClientJSONRPCError,
214
+ httpx.TimeoutException,
215
+ httpx.TransportError,
216
+ ) as exc:
217
+ raise map_operation_error("tasks/cancel", exc) from exc
218
+ finally:
219
+ await self._release_operation()
220
+
221
+ async def resubscribe_task(
222
+ self,
223
+ task_id: str,
224
+ *,
225
+ metadata: Mapping[str, Any] | None = None,
226
+ ) -> AsyncIterator[tuple[Task, TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None]]:
227
+ """Resubscribe to task updates."""
228
+ await self._acquire_operation()
229
+ try:
230
+ client = await self._ensure_client()
231
+ request_metadata, extra_headers = split_request_metadata(metadata)
232
+ try:
233
+ async for event in client.resubscribe(
234
+ TaskIdParams(id=task_id, metadata=request_metadata or {}),
235
+ context=build_call_context(self._settings.bearer_token, extra_headers),
236
+ ):
237
+ yield event
238
+ except (
239
+ A2AClientHTTPError,
240
+ A2AClientJSONRPCError,
241
+ httpx.TimeoutException,
242
+ httpx.TransportError,
243
+ ) as exc:
244
+ raise map_operation_error("tasks/resubscribe", exc) from exc
245
+ finally:
246
+ await self._release_operation()
247
+
248
+ async def _ensure_client(self) -> Client:
249
+ async with self._lock:
250
+ if self._client is not None:
251
+ return self._client
252
+ return await self._build_client()
253
+
254
+ async def _build_client(self) -> Client:
255
+ card = await self.get_agent_card()
256
+ config = ClientConfig(
257
+ streaming=True,
258
+ polling=False,
259
+ httpx_client=await self._get_httpx_client(),
260
+ supported_transports=list(self._settings.supported_transports),
261
+ use_client_preference=self._settings.use_client_preference,
262
+ )
263
+ try:
264
+ factory = ClientFactory(config, consumers=None)
265
+ client = factory.create(
266
+ card,
267
+ interceptors=build_client_interceptors(self._settings.bearer_token),
268
+ )
269
+ except ValueError as exc:
270
+ raise A2AUnsupportedBindingError(
271
+ f"No supported transport found for {self.agent_url}"
272
+ ) from exc
273
+ self._client = client
274
+ return client
275
+
276
+ async def _get_httpx_client(self) -> httpx.AsyncClient:
277
+ if self._httpx_client is not None:
278
+ return self._httpx_client
279
+ self._httpx_client = httpx.AsyncClient(timeout=self._settings.default_timeout)
280
+ return self._httpx_client
281
+
282
+ async def _acquire_operation(self) -> None:
283
+ async with self._request_lock:
284
+ self._active_requests += 1
285
+
286
+ async def _release_operation(self) -> None:
287
+ async with self._request_lock:
288
+ if self._active_requests > 0:
289
+ self._active_requests -= 1
290
+
291
+ def _build_user_message(
292
+ self,
293
+ *,
294
+ text: str,
295
+ context_id: str | None,
296
+ task_id: str | None,
297
+ message_id: str | None,
298
+ ) -> Message:
299
+ return Message(
300
+ role=Role.user,
301
+ message_id=message_id or str(uuid4()),
302
+ context_id=context_id,
303
+ task_id=task_id,
304
+ parts=self._normalize_parts(text),
305
+ metadata=None,
306
+ )
307
+
308
+ @classmethod
309
+ def extract_text(cls, payload: Any) -> str | None:
310
+ return extract_text_from_payload(payload)
311
+
312
+ # keep parts construction explicitly typed for mypy compatibility in older stubs
313
+ def _normalize_parts(self, text: str) -> list[Part]:
314
+ return [cast(Part, TextPart(text=text))]
315
+
316
+
317
+ __all__ = ["A2AClient"]
@@ -0,0 +1,148 @@
1
+ """Centralized error mapping for outbound A2A client operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+ from a2a.client.errors import A2AClientHTTPError, A2AClientJSONError, A2AClientJSONRPCError
7
+
8
+ from .errors import (
9
+ A2AAgentUnavailableError,
10
+ A2AAuthenticationError,
11
+ A2AClientError,
12
+ A2AClientResetRequiredError,
13
+ A2APeerProtocolError,
14
+ A2APermissionDeniedError,
15
+ A2ATimeoutError,
16
+ A2AUnsupportedOperationError,
17
+ )
18
+
19
+
20
+ def _attach_http_status(error: A2AClientError, status: int | None) -> A2AClientError:
21
+ error.http_status = status
22
+ return error
23
+
24
+
25
+ def _extract_jsonrpc_error_payload(
26
+ exc: A2AClientJSONRPCError,
27
+ ) -> tuple[str | None, int | None, object]:
28
+ error = getattr(exc, "error", None)
29
+ if error is None:
30
+ return None, None, None
31
+ return (
32
+ getattr(error, "message", None),
33
+ getattr(error, "code", None),
34
+ getattr(error, "data", None),
35
+ )
36
+
37
+
38
+ def map_jsonrpc_error(exc: A2AClientJSONRPCError) -> A2AClientError:
39
+ _message, code, data = _extract_jsonrpc_error_payload(exc)
40
+ if code == -32601:
41
+ parsed_error = A2AUnsupportedOperationError(
42
+ "Remote A2A peer does not support the requested operation"
43
+ )
44
+ parsed_error.error_code = "method_not_supported"
45
+ parsed_error.code = code
46
+ parsed_error.data = data
47
+ return parsed_error
48
+ if code == -32602:
49
+ return A2APeerProtocolError(
50
+ "Remote A2A peer rejected the request payload",
51
+ error_code="invalid_params",
52
+ rpc_code=code,
53
+ data=data,
54
+ )
55
+ if code == -32603:
56
+ reset_required = A2AClientResetRequiredError(
57
+ "Remote A2A peer entered an unstable state and requires a fresh client session"
58
+ )
59
+ reset_required.code = code
60
+ reset_required.data = data
61
+ return reset_required
62
+ return A2APeerProtocolError(
63
+ "Remote A2A peer returned a protocol error",
64
+ error_code="peer_protocol_error",
65
+ rpc_code=code,
66
+ data=data,
67
+ )
68
+
69
+
70
+ def map_http_error(operation: str, exc: A2AClientHTTPError) -> A2AClientError:
71
+ status = exc.status_code
72
+ if status == 401:
73
+ return _attach_http_status(
74
+ A2AAuthenticationError(
75
+ f"Remote A2A peer rejected {operation} due to authentication failure"
76
+ ),
77
+ status,
78
+ )
79
+ if status == 403:
80
+ return _attach_http_status(
81
+ A2APermissionDeniedError(
82
+ f"Remote A2A peer rejected {operation} due to insufficient permissions"
83
+ ),
84
+ status,
85
+ )
86
+ if status in {404, 405, 409, 501}:
87
+ return _attach_http_status(
88
+ A2AUnsupportedOperationError(f"Remote A2A peer does not support {operation}"),
89
+ status,
90
+ )
91
+ if status == 408:
92
+ return _attach_http_status(
93
+ A2ATimeoutError(f"Remote A2A peer timed out during {operation}"),
94
+ status,
95
+ )
96
+ if status in {429, 502, 503, 504}:
97
+ return _attach_http_status(
98
+ A2AClientResetRequiredError(
99
+ f"Remote A2A peer is temporarily unstable during {operation}"
100
+ ),
101
+ status,
102
+ )
103
+ return _attach_http_status(
104
+ A2AAgentUnavailableError(f"Remote A2A peer is unavailable for {operation}"),
105
+ status,
106
+ )
107
+
108
+
109
+ def map_transport_error(
110
+ operation: str,
111
+ exc: httpx.TimeoutException | httpx.TransportError,
112
+ ) -> A2AClientError:
113
+ if isinstance(exc, httpx.TimeoutException):
114
+ return A2ATimeoutError(f"Remote A2A peer timed out during {operation}")
115
+ return A2AAgentUnavailableError(f"Remote A2A peer is unreachable for {operation}")
116
+
117
+
118
+ def map_operation_error(
119
+ operation: str,
120
+ exc: A2AClientHTTPError | A2AClientJSONRPCError | httpx.TimeoutException | httpx.TransportError,
121
+ ) -> A2AClientError:
122
+ if isinstance(exc, A2AClientHTTPError):
123
+ return map_http_error(operation, exc)
124
+ if isinstance(exc, A2AClientJSONRPCError):
125
+ return map_jsonrpc_error(exc)
126
+ return map_transport_error(operation, exc)
127
+
128
+
129
+ def map_agent_card_error(
130
+ exc: A2AClientHTTPError | A2AClientJSONError | httpx.TimeoutException | httpx.TransportError,
131
+ ) -> A2AClientError:
132
+ if isinstance(exc, A2AClientHTTPError):
133
+ return map_http_error("agent-card/fetch", exc)
134
+ if isinstance(exc, A2AClientJSONError):
135
+ return A2APeerProtocolError(
136
+ "Remote A2A peer returned an invalid agent card payload",
137
+ error_code="invalid_agent_card",
138
+ )
139
+ return map_transport_error("agent-card/fetch", exc)
140
+
141
+
142
+ __all__ = [
143
+ "map_agent_card_error",
144
+ "map_http_error",
145
+ "map_jsonrpc_error",
146
+ "map_operation_error",
147
+ "map_transport_error",
148
+ ]
@@ -18,12 +18,30 @@ class A2AAgentUnavailableError(A2AClientError):
18
18
  error_code = "agent_unavailable"
19
19
 
20
20
 
21
+ class A2AAuthenticationError(A2AClientError):
22
+ """Raised when a remote A2A peer rejects authentication."""
23
+
24
+ error_code = "authentication_failed"
25
+
26
+
27
+ class A2APermissionDeniedError(A2AClientError):
28
+ """Raised when a remote A2A peer rejects authorization."""
29
+
30
+ error_code = "permission_denied"
31
+
32
+
21
33
  class A2AClientResetRequiredError(A2AAgentUnavailableError):
22
34
  """Raised when the cached transport should be rebuilt."""
23
35
 
24
36
  error_code = "reset_required"
25
37
 
26
38
 
39
+ class A2ATimeoutError(A2AAgentUnavailableError):
40
+ """Raised when an outbound call to a remote A2A peer times out."""
41
+
42
+ error_code = "timeout"
43
+
44
+
27
45
  class A2AUnsupportedBindingError(A2AClientError):
28
46
  """Raised when local and remote transport configuration has no overlap."""
29
47
 
@@ -58,7 +76,10 @@ class A2APeerProtocolError(A2AClientError):
58
76
  __all__ = [
59
77
  "A2AClientError",
60
78
  "A2AAgentUnavailableError",
79
+ "A2AAuthenticationError",
80
+ "A2APermissionDeniedError",
61
81
  "A2AClientResetRequiredError",
82
+ "A2ATimeoutError",
62
83
  "A2AUnsupportedBindingError",
63
84
  "A2AUnsupportedOperationError",
64
85
  "A2APeerProtocolError",