opencode-a2a 0.3.1__tar.gz → 0.4.0__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 (113) hide show
  1. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/.github/workflows/ci.yml +4 -2
  2. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/.github/workflows/dependency-health.yml +3 -1
  3. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/.github/workflows/publish.yml +3 -1
  4. opencode_a2a-0.4.0/AGENTS.md +38 -0
  5. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/PKG-INFO +60 -16
  6. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/README.md +59 -15
  7. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/docs/guide.md +64 -3
  8. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/scripts/README.md +1 -1
  9. opencode_a2a-0.4.0/src/opencode_a2a/cli.py +114 -0
  10. opencode_a2a-0.4.0/src/opencode_a2a/client/__init__.py +28 -0
  11. opencode_a2a-0.4.0/src/opencode_a2a/client/client.py +608 -0
  12. opencode_a2a-0.4.0/src/opencode_a2a/client/config.py +177 -0
  13. opencode_a2a-0.4.0/src/opencode_a2a/client/errors.py +65 -0
  14. opencode_a2a-0.4.0/src/opencode_a2a/client/types.py +21 -0
  15. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/config.py +21 -1
  16. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/execution/executor.py +177 -10
  17. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/parts/mapping.py +9 -1
  18. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/server/application.py +42 -5
  19. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a.egg-info/PKG-INFO +60 -16
  20. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a.egg-info/SOURCES.txt +8 -0
  21. opencode_a2a-0.4.0/tests/client/__init__.py +1 -0
  22. opencode_a2a-0.4.0/tests/client/test_client_config.py +44 -0
  23. opencode_a2a-0.4.0/tests/client/test_client_facade.py +653 -0
  24. opencode_a2a-0.4.0/tests/execution/test_opencode_agent_session_binding.py +409 -0
  25. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/server/test_cli.py +26 -0
  26. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/server/test_transport_contract.py +28 -0
  27. opencode_a2a-0.3.1/AGENTS.md +0 -51
  28. opencode_a2a-0.3.1/src/opencode_a2a/cli.py +0 -52
  29. opencode_a2a-0.3.1/tests/execution/test_opencode_agent_session_binding.py +0 -208
  30. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/.gitignore +0 -0
  31. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/.pre-commit-config.yaml +0 -0
  32. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/.secrets.baseline +0 -0
  33. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/CONTRIBUTING.md +0 -0
  34. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/LICENSE +0 -0
  35. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/SECURITY.md +0 -0
  36. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/pyproject.toml +0 -0
  37. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/scripts/check_coverage.py +0 -0
  38. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/scripts/dependency_health.sh +0 -0
  39. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/scripts/doctor.sh +0 -0
  40. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/scripts/health_common.sh +0 -0
  41. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/scripts/lint.sh +0 -0
  42. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/scripts/smoke_test_built_cli.sh +0 -0
  43. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/setup.cfg +0 -0
  44. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/__init__.py +0 -0
  45. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/contracts/__init__.py +0 -0
  46. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/contracts/extensions.py +0 -0
  47. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/execution/__init__.py +0 -0
  48. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/execution/request_context.py +0 -0
  49. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/execution/stream_events.py +0 -0
  50. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/execution/stream_state.py +0 -0
  51. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/execution/upstream_errors.py +0 -0
  52. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/jsonrpc/__init__.py +0 -0
  53. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/jsonrpc/application.py +0 -0
  54. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/jsonrpc/methods.py +0 -0
  55. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/jsonrpc/params.py +0 -0
  56. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/opencode_upstream_client.py +0 -0
  57. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/parts/__init__.py +0 -0
  58. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/parts/text.py +0 -0
  59. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/profile/__init__.py +0 -0
  60. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/profile/runtime.py +0 -0
  61. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/server/__init__.py +0 -0
  62. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/server/agent_card.py +0 -0
  63. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/server/openapi.py +0 -0
  64. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a/server/request_parsing.py +0 -0
  65. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a.egg-info/dependency_links.txt +0 -0
  66. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a.egg-info/entry_points.txt +0 -0
  67. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a.egg-info/requires.txt +0 -0
  68. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/src/opencode_a2a.egg-info/top_level.txt +0 -0
  69. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/__init__.py +0 -0
  70. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/config/__init__.py +0 -0
  71. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/config/test_settings.py +0 -0
  72. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/contracts/__init__.py +0 -0
  73. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/contracts/test_extension_contract_consistency.py +0 -0
  74. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/__init__.py +0 -0
  75. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_agent_errors.py +0 -0
  76. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_agent_helpers.py +0 -0
  77. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_cancellation.py +0 -0
  78. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_directory_validation.py +0 -0
  79. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_metrics.py +0 -0
  80. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_multipart_input.py +0 -0
  81. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_session_ownership.py +0 -0
  82. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_streaming_output_contract_blocks.py +0 -0
  83. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_streaming_output_contract_core.py +0 -0
  84. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_streaming_output_contract_interrupts.py +0 -0
  85. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/execution/test_streaming_output_contract_logging.py +0 -0
  86. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/jsonrpc/__init__.py +0 -0
  87. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/jsonrpc/test_jsonrpc_methods.py +0 -0
  88. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/jsonrpc/test_jsonrpc_params.py +0 -0
  89. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/jsonrpc/test_jsonrpc_unsupported_method.py +0 -0
  90. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/jsonrpc/test_opencode_session_extension_commands.py +0 -0
  91. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/jsonrpc/test_opencode_session_extension_interrupts.py +0 -0
  92. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/jsonrpc/test_opencode_session_extension_prompt_async.py +0 -0
  93. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/jsonrpc/test_opencode_session_extension_queries.py +0 -0
  94. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/package/__init__.py +0 -0
  95. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/package/test_version.py +0 -0
  96. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/parts/__init__.py +0 -0
  97. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/parts/test_parts_text.py +0 -0
  98. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/profile/__init__.py +0 -0
  99. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/profile/test_profile_runtime.py +0 -0
  100. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/scripts/__init__.py +0 -0
  101. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/scripts/test_script_health_contract.py +0 -0
  102. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/server/__init__.py +0 -0
  103. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/server/test_agent_card.py +0 -0
  104. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/server/test_app_behaviors.py +0 -0
  105. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/server/test_call_context_builder.py +0 -0
  106. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/server/test_cancel_contract.py +0 -0
  107. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/support/__init__.py +0 -0
  108. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/support/helpers.py +0 -0
  109. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/support/session_extensions.py +0 -0
  110. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/support/streaming_output.py +0 -0
  111. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/upstream/__init__.py +0 -0
  112. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/tests/upstream/test_opencode_upstream_client_params.py +0 -0
  113. {opencode_a2a-0.3.1 → opencode_a2a-0.4.0}/uv.lock +0 -0
@@ -8,6 +8,8 @@ on:
8
8
 
9
9
  permissions:
10
10
  contents: read
11
+ env:
12
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
11
13
 
12
14
  jobs:
13
15
  quality-gate:
@@ -25,7 +27,7 @@ jobs:
25
27
  - name: Setup uv
26
28
  uses: astral-sh/setup-uv@v4
27
29
  with:
28
- enable-cache: true
30
+ enable-cache: false
29
31
 
30
32
  - name: Sync Dependencies
31
33
  run: uv sync --all-extras --frozen
@@ -73,7 +75,7 @@ jobs:
73
75
  - name: Setup uv
74
76
  uses: astral-sh/setup-uv@v4
75
77
  with:
76
- enable-cache: true
78
+ enable-cache: false
77
79
 
78
80
  - name: Sync Dependencies
79
81
  run: uv sync --all-extras --frozen
@@ -8,6 +8,8 @@ on:
8
8
 
9
9
  permissions:
10
10
  contents: read
11
+ env:
12
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
11
13
 
12
14
  jobs:
13
15
  dependency-health:
@@ -25,7 +27,7 @@ jobs:
25
27
  - name: Setup uv
26
28
  uses: astral-sh/setup-uv@v4
27
29
  with:
28
- enable-cache: true
30
+ enable-cache: false
29
31
 
30
32
  - name: Run dependency health checks
31
33
  run: bash ./scripts/dependency_health.sh
@@ -9,6 +9,8 @@ on:
9
9
  permissions:
10
10
  contents: write
11
11
  id-token: write
12
+ env:
13
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
12
14
 
13
15
  jobs:
14
16
  publish:
@@ -28,7 +30,7 @@ jobs:
28
30
  - name: Setup uv
29
31
  uses: astral-sh/setup-uv@v4
30
32
  with:
31
- enable-cache: true
33
+ enable-cache: false
32
34
 
33
35
  - name: Run regression baseline
34
36
  run: bash ./scripts/doctor.sh
@@ -0,0 +1,38 @@
1
+ # AGENTS.md
2
+
3
+ The following rules apply to coding agent collaboration in this repository. These complement the general [CONTRIBUTING.md](CONTRIBUTING.md) workflow.
4
+
5
+ ## 1. Core Principles
6
+
7
+ - Move tasks forward under secure and traceable conditions.
8
+ - Adhere to the existing repository structure and engineering conventions.
9
+
10
+ ## 2. Collaboration Workflow
11
+
12
+ - Follow the Git, Issue, and PR workflow defined in [CONTRIBUTING.md](CONTRIBUTING.md).
13
+ - Use `gh` CLI for all issue/PR operations (reading, writing, and comments). Do not edit through the web UI.
14
+ - Create a new tracking issue for any development task that does not already have one.
15
+ - Link the issue explicitly in PR descriptions (e.g., `Closes #xx`).
16
+ - Keep status updates synchronized to the relevant issue/PR to avoid duplicate manual work.
17
+
18
+ ## 3. Tooling and Text Conventions
19
+
20
+ - **Language**: Use Simplified Chinese for issues, PRs, and comments. Technical terms may remain in English.
21
+ - **PR Body**: For multi-line bodies, write to a temporary file first and pass it via `gh pr create --body-file`.
22
+ - **References**: Use `#123` for same-repo references; use full URLs for cross-repo links.
23
+
24
+ ## 4. Regression and Validation
25
+
26
+ - Use the primary validation entrypoint for all changes:
27
+ ```bash
28
+ ./scripts/doctor.sh
29
+ ```
30
+ - If `pre-commit` (via `doctor.sh`) auto-fixes files, review the changes before committing.
31
+ - For documentation-only changes, you may skip tests but must self-check all commands and paths.
32
+ - If environment limits prevent full validation, explicitly report what was skipped and why.
33
+
34
+ ## 5. Security and Configuration
35
+
36
+ - **Secrets**: Never commit keys, tokens, or `.env` content.
37
+ - **Logs**: Ensure debug output does not leak access tokens.
38
+ - **Documentation**: Synchronize documentation updates for all deployment or auth-related changes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-a2a
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: OpenCode A2A runtime
5
5
  Author: liujuanjuan1984@Intelligent-Internet
6
6
  License-Expression: Apache-2.0
@@ -46,22 +46,36 @@ deployment boundary.
46
46
 
47
47
  ## What This Is
48
48
 
49
- - An A2A adapter service for `opencode serve`.
50
- - Use it when you need a stable A2A endpoint for apps, gateways, or A2A
51
- clients.
49
+ - An A2A adapter service built on `opencode serve`, with inbound runtime
50
+ exposure plus outbound peer calling.
51
+ - It supports both roles in one process: serving as an A2A Server and hosting an
52
+ embedded A2A Client for `a2a_call`.
53
+
54
+ ## Architecture
52
55
 
53
56
  ```mermaid
54
57
  flowchart TD
55
- Client["a2a-client-hub / any A2A client"]
58
+ External["A2A Clients / a2a-client-hub / Gateways"]
56
59
 
57
- subgraph ServerSide["Server-side"]
58
- Adapter["opencode-a2a\nA2A adapter service"]
59
- Runtime["opencode serve\nOpenCode runtime"]
60
+ subgraph Adapter["opencode-a2a Runtime"]
61
+ Ingress["Inbound A2A Surface\nREST + JSON-RPC"]
62
+ OpenCode["OpenCode Runtime"]
63
+ Outbound["Embedded A2A Client\na2a_call"]
64
+ end
60
65
 
61
- Adapter <--> Runtime
66
+ subgraph Peers["Peer A2A services"]
67
+ PeerA2A["Peer A2A Agent"]
68
+ PeerRuntime["Peer OpenCode Runtime"]
69
+ PeerA2A --> PeerRuntime
62
70
  end
63
71
 
64
- Client <--> Adapter
72
+ External -->|message/send,\nmessage:stream| Ingress
73
+ Ingress -->|tool call| OpenCode
74
+ OpenCode -->|model/tool result events| Ingress
75
+ Ingress -->|a2a_call| Outbound
76
+ Outbound -->|message/send,\nmessage:stream| PeerA2A
77
+ PeerA2A -->|tool result| Outbound
78
+ PeerRuntime -->|task session\nexecution| PeerA2A
65
79
  ```
66
80
 
67
81
  ## Quick Start
@@ -87,6 +101,11 @@ opencode models
87
101
  opencode serve --hostname 127.0.0.1 --port 4096
88
102
  ```
89
103
 
104
+ Treat the deployed OpenCode user's HOME/XDG config directories as part of the
105
+ runtime state. If a packaged or service-managed deployment appears to ignore
106
+ fresh provider env vars, inspect that user's persisted OpenCode auth/config
107
+ files before assuming the A2A adapter layer is overriding credentials.
108
+
90
109
  Then start `opencode-a2a` against that upstream:
91
110
 
92
111
  ```bash
@@ -95,7 +114,6 @@ OPENCODE_BASE_URL=http://127.0.0.1:4096 \
95
114
  A2A_HOST=127.0.0.1 \
96
115
  A2A_PORT=8000 \
97
116
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
98
- A2A_STREAM_SSE_PING_SECONDS=15 \
99
117
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
100
118
  opencode-a2a serve
101
119
  ```
@@ -106,19 +124,41 @@ Verify that the service is up:
106
124
  curl http://127.0.0.1:8000/.well-known/agent-card.json
107
125
  ```
108
126
 
109
- Default local address: `http://127.0.0.1:8000`
110
-
111
- ## What You Get
127
+ ## Capabilities
112
128
 
113
129
  - A2A HTTP+JSON endpoints such as `/v1/message:send` and
114
130
  `/v1/message:stream`
115
131
  - A2A JSON-RPC support on `POST /`
132
+ - Peering capabilities: can act as a client via `opencode-a2a call`
133
+ - Autonomous tool execution: supports `a2a_call` tool for outbound agent-to-agent communication
116
134
  - SSE streaming with normalized `text`, `reasoning`, and `tool_call` blocks
117
- - Explicit REST SSE keepalive configurable through `A2A_STREAM_SSE_PING_SECONDS`
118
135
  - Session continuity through `metadata.shared.session.id`
119
136
  - Request-scoped model selection through `metadata.shared.model`
120
137
  - OpenCode-oriented JSON-RPC extensions for session and model/provider queries
121
138
 
139
+ ## Peering Node / Outbound Access
140
+
141
+ `opencode-a2a` supports a "Peering Node" architecture where a single process handles both inbound (Server) and outbound (Client) A2A traffic.
142
+
143
+ ### CLI Client
144
+ Interact with other A2A agents directly from the command line:
145
+
146
+ ```bash
147
+ opencode-a2a call http://other-agent:8000 "How are you?" --token your-outbound-token
148
+ ```
149
+
150
+ ### Outbound Agent Calls (Tools)
151
+ The server can autonomously execute `a2a_call(url, message)` tool calls emitted by the OpenCode runtime. Results are fetched via A2A and returned to the model as tool results, enabling multi-agent orchestration.
152
+
153
+ When the target peer requires bearer auth, configure `A2A_CLIENT_BEARER_TOKEN`
154
+ for server-side outbound calls. CLI calls can continue using `--token` or
155
+ `A2A_CLIENT_BEARER_TOKEN`.
156
+
157
+ Server-side outbound client settings are fully wired through runtime config:
158
+ `A2A_CLIENT_TIMEOUT_SECONDS`, `A2A_CLIENT_CARD_FETCH_TIMEOUT_SECONDS`,
159
+ `A2A_CLIENT_USE_CLIENT_PREFERENCE`, `A2A_CLIENT_BEARER_TOKEN`, and
160
+ `A2A_CLIENT_SUPPORTED_TRANSPORTS`.
161
+
122
162
  Detailed protocol contracts, examples, and extension docs live in
123
163
  [`docs/guide.md`](docs/guide.md).
124
164
 
@@ -145,7 +185,11 @@ This repository improves the service boundary around OpenCode, but it does not
145
185
  turn OpenCode into a hardened multi-tenant platform.
146
186
 
147
187
  - `A2A_BEARER_TOKEN` protects the A2A surface.
148
- - Provider auth and default model configuration remain on the OpenCode side.
188
+ - Provider auth and default model configuration remain on the OpenCode side; deployment-time
189
+ precedence details and HOME/XDG state impact are documented in
190
+ [docs/guide.md](docs/guide.md#troubleshooting-provider-auth-state).
191
+ - Use `A2A_CLIENT_BEARER_TOKEN` for server-side outbound peer calls initiated by
192
+ `a2a_call`.
149
193
  - Deployment supervision is intentionally BYO. Use `systemd`, Docker,
150
194
  Kubernetes, or another supervisor if you need long-running operation.
151
195
  - For mutually untrusted tenants, run separate instance pairs with isolated
@@ -8,22 +8,36 @@ deployment boundary.
8
8
 
9
9
  ## What This Is
10
10
 
11
- - An A2A adapter service for `opencode serve`.
12
- - Use it when you need a stable A2A endpoint for apps, gateways, or A2A
13
- clients.
11
+ - An A2A adapter service built on `opencode serve`, with inbound runtime
12
+ exposure plus outbound peer calling.
13
+ - It supports both roles in one process: serving as an A2A Server and hosting an
14
+ embedded A2A Client for `a2a_call`.
15
+
16
+ ## Architecture
14
17
 
15
18
  ```mermaid
16
19
  flowchart TD
17
- Client["a2a-client-hub / any A2A client"]
20
+ External["A2A Clients / a2a-client-hub / Gateways"]
18
21
 
19
- subgraph ServerSide["Server-side"]
20
- Adapter["opencode-a2a\nA2A adapter service"]
21
- Runtime["opencode serve\nOpenCode runtime"]
22
+ subgraph Adapter["opencode-a2a Runtime"]
23
+ Ingress["Inbound A2A Surface\nREST + JSON-RPC"]
24
+ OpenCode["OpenCode Runtime"]
25
+ Outbound["Embedded A2A Client\na2a_call"]
26
+ end
22
27
 
23
- Adapter <--> Runtime
28
+ subgraph Peers["Peer A2A services"]
29
+ PeerA2A["Peer A2A Agent"]
30
+ PeerRuntime["Peer OpenCode Runtime"]
31
+ PeerA2A --> PeerRuntime
24
32
  end
25
33
 
26
- Client <--> Adapter
34
+ External -->|message/send,\nmessage:stream| Ingress
35
+ Ingress -->|tool call| OpenCode
36
+ OpenCode -->|model/tool result events| Ingress
37
+ Ingress -->|a2a_call| Outbound
38
+ Outbound -->|message/send,\nmessage:stream| PeerA2A
39
+ PeerA2A -->|tool result| Outbound
40
+ PeerRuntime -->|task session\nexecution| PeerA2A
27
41
  ```
28
42
 
29
43
  ## Quick Start
@@ -49,6 +63,11 @@ opencode models
49
63
  opencode serve --hostname 127.0.0.1 --port 4096
50
64
  ```
51
65
 
66
+ Treat the deployed OpenCode user's HOME/XDG config directories as part of the
67
+ runtime state. If a packaged or service-managed deployment appears to ignore
68
+ fresh provider env vars, inspect that user's persisted OpenCode auth/config
69
+ files before assuming the A2A adapter layer is overriding credentials.
70
+
52
71
  Then start `opencode-a2a` against that upstream:
53
72
 
54
73
  ```bash
@@ -57,7 +76,6 @@ OPENCODE_BASE_URL=http://127.0.0.1:4096 \
57
76
  A2A_HOST=127.0.0.1 \
58
77
  A2A_PORT=8000 \
59
78
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
60
- A2A_STREAM_SSE_PING_SECONDS=15 \
61
79
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
62
80
  opencode-a2a serve
63
81
  ```
@@ -68,19 +86,41 @@ Verify that the service is up:
68
86
  curl http://127.0.0.1:8000/.well-known/agent-card.json
69
87
  ```
70
88
 
71
- Default local address: `http://127.0.0.1:8000`
72
-
73
- ## What You Get
89
+ ## Capabilities
74
90
 
75
91
  - A2A HTTP+JSON endpoints such as `/v1/message:send` and
76
92
  `/v1/message:stream`
77
93
  - A2A JSON-RPC support on `POST /`
94
+ - Peering capabilities: can act as a client via `opencode-a2a call`
95
+ - Autonomous tool execution: supports `a2a_call` tool for outbound agent-to-agent communication
78
96
  - SSE streaming with normalized `text`, `reasoning`, and `tool_call` blocks
79
- - Explicit REST SSE keepalive configurable through `A2A_STREAM_SSE_PING_SECONDS`
80
97
  - Session continuity through `metadata.shared.session.id`
81
98
  - Request-scoped model selection through `metadata.shared.model`
82
99
  - OpenCode-oriented JSON-RPC extensions for session and model/provider queries
83
100
 
101
+ ## Peering Node / Outbound Access
102
+
103
+ `opencode-a2a` supports a "Peering Node" architecture where a single process handles both inbound (Server) and outbound (Client) A2A traffic.
104
+
105
+ ### CLI Client
106
+ Interact with other A2A agents directly from the command line:
107
+
108
+ ```bash
109
+ opencode-a2a call http://other-agent:8000 "How are you?" --token your-outbound-token
110
+ ```
111
+
112
+ ### Outbound Agent Calls (Tools)
113
+ The server can autonomously execute `a2a_call(url, message)` tool calls emitted by the OpenCode runtime. Results are fetched via A2A and returned to the model as tool results, enabling multi-agent orchestration.
114
+
115
+ When the target peer requires bearer auth, configure `A2A_CLIENT_BEARER_TOKEN`
116
+ for server-side outbound calls. CLI calls can continue using `--token` or
117
+ `A2A_CLIENT_BEARER_TOKEN`.
118
+
119
+ Server-side outbound client settings are fully wired through runtime config:
120
+ `A2A_CLIENT_TIMEOUT_SECONDS`, `A2A_CLIENT_CARD_FETCH_TIMEOUT_SECONDS`,
121
+ `A2A_CLIENT_USE_CLIENT_PREFERENCE`, `A2A_CLIENT_BEARER_TOKEN`, and
122
+ `A2A_CLIENT_SUPPORTED_TRANSPORTS`.
123
+
84
124
  Detailed protocol contracts, examples, and extension docs live in
85
125
  [`docs/guide.md`](docs/guide.md).
86
126
 
@@ -107,7 +147,11 @@ This repository improves the service boundary around OpenCode, but it does not
107
147
  turn OpenCode into a hardened multi-tenant platform.
108
148
 
109
149
  - `A2A_BEARER_TOKEN` protects the A2A surface.
110
- - Provider auth and default model configuration remain on the OpenCode side.
150
+ - Provider auth and default model configuration remain on the OpenCode side; deployment-time
151
+ precedence details and HOME/XDG state impact are documented in
152
+ [docs/guide.md](docs/guide.md#troubleshooting-provider-auth-state).
153
+ - Use `A2A_CLIENT_BEARER_TOKEN` for server-side outbound peer calls initiated by
154
+ `a2a_call`.
111
155
  - Deployment supervision is intentionally BYO. Use `systemd`, Docker,
112
156
  Kubernetes, or another supervisor if you need long-running operation.
113
157
  - For mutually untrusted tenants, run separate instance pairs with isolated
@@ -69,7 +69,47 @@ Key variables to understand protocol behavior:
69
69
  `session.abort` in cancel flow.
70
70
  - `OPENCODE_TIMEOUT` / `OPENCODE_TIMEOUT_STREAM`: upstream request timeout and
71
71
  optional stream timeout override.
72
+ - `A2A_CLIENT_TIMEOUT_SECONDS`: outbound client timeout. Default: `30` seconds.
73
+ - `A2A_CLIENT_CARD_FETCH_TIMEOUT_SECONDS`: outbound Agent Card fetch timeout.
74
+ Default: `5` seconds.
75
+ - `A2A_CLIENT_USE_CLIENT_PREFERENCE`: whether the outbound client prefers its own transport choices.
76
+ - `A2A_CLIENT_BEARER_TOKEN`: optional bearer token attached to outbound peer
77
+ calls made by the embedded A2A client and `a2a_call` tool path.
78
+ - `A2A_CLIENT_SUPPORTED_TRANSPORTS`: ordered outbound transport preference list.
72
79
  - Runtime authentication is bearer-token only via `A2A_BEARER_TOKEN`.
80
+ - The same outbound client flags are also honored by the server-side embedded
81
+ A2A client used for peer calls and `a2a_call` tool execution:
82
+ - `A2A_CLIENT_TIMEOUT_SECONDS`
83
+ - `A2A_CLIENT_CARD_FETCH_TIMEOUT_SECONDS`
84
+ - `A2A_CLIENT_USE_CLIENT_PREFERENCE`
85
+ - `A2A_CLIENT_BEARER_TOKEN`
86
+ - `A2A_CLIENT_SUPPORTED_TRANSPORTS`
87
+
88
+ ## Client Initialization Facade (Preview)
89
+
90
+ `opencode-a2a` now includes a minimal client bootstrap module in
91
+ `src/opencode_a2a/client/` to support downstream consumer usage while keeping
92
+ server and client concerns separate.
93
+
94
+ Boundary separation:
95
+
96
+ - Server code owns runtime request handling, transport orchestration, stream
97
+ behavior, and public compatibility profile exposure.
98
+ - Client code owns peer card discovery, SDK client construction, operation call
99
+ helpers, and protocol error normalization.
100
+
101
+ Current client facade API:
102
+
103
+ - `A2AClient.get_agent_card()`
104
+ - `A2AClient.send()` / `A2AClient.send_message()`
105
+ - `A2AClient.get_task()`
106
+ - `A2AClient.cancel_task()`
107
+ - `A2AClient.resubscribe_task()`
108
+
109
+ Server-side outbound peer calls use bearer auth only for now. Configure
110
+ `A2A_CLIENT_BEARER_TOKEN` when the remote agent protects its runtime surface.
111
+ CLI outbound calls may pass `--token` explicitly or use
112
+ `A2A_CLIENT_BEARER_TOKEN`.
73
113
 
74
114
  Execution-boundary metadata is intentionally declarative deployment metadata:
75
115
  it is published through `RuntimeProfile`, Agent Card, OpenAPI, and `/health`,
@@ -99,6 +139,12 @@ starting that upstream process:
99
139
  If your provider uses environment variables for auth, export them before
100
140
  starting `opencode serve`.
101
141
 
142
+ Do not assume startup-script env vars always erase previously persisted
143
+ OpenCode auth state for the deployed user. When debugging provider-auth
144
+ surprises, inspect the deployed user's HOME/XDG config directories and the
145
+ OpenCode files stored there before concluding that `opencode-a2a` changed the
146
+ credential selection.
147
+
102
148
  Then start `opencode-a2a` against that explicit upstream URL:
103
149
 
104
150
  ```bash
@@ -111,6 +157,23 @@ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
111
157
  opencode-a2a serve
112
158
  ```
113
159
 
160
+ ## Troubleshooting Provider Auth State
161
+
162
+ If one deployment works while another fails against the same upstream provider,
163
+ check the deployed OpenCode user's local state before assuming the difference
164
+ comes from the `opencode-a2a` package itself.
165
+
166
+ - Provider auth and service-level model defaults belong to `opencode serve`.
167
+ - The deployed user's HOME/XDG config directories are operational input.
168
+ - Existing OpenCode auth/config files may still influence runtime behavior even
169
+ when you also inject provider env vars from a process manager or shell
170
+ wrapper.
171
+ - Compare the deployed user's OpenCode auth/config files, HOME/XDG values, and
172
+ effective workspace directory before blaming the A2A adapter layer.
173
+ - For OpenCode-specific auth/config troubleshooting, inspect files such as
174
+ `~/.local/share/opencode/auth.json` and `~/.config/opencode/opencode.json`
175
+ (or the equivalent XDG-resolved paths for that service user).
176
+
114
177
  ## Core Behavior
115
178
 
116
179
  - The service forwards A2A `message:send` to OpenCode session/message calls.
@@ -125,9 +188,7 @@ opencode-a2a serve
125
188
  URI.
126
189
  - `DataPart` is currently rejected explicitly; it is not silently downgraded.
127
190
  - Task state defaults to `completed` for successful turns.
128
- - The deployment profile is single-tenant and shared-workspace: one server
129
- instance exposes one OpenCode workspace/environment to all consumers bound to
130
- that instance.
191
+ - The deployment profile is single-tenant and shared-workspace. For detailed isolation principles and security boundaries, see [SECURITY.md](../SECURITY.md).
131
192
 
132
193
  ## Streaming Contract
133
194
 
@@ -12,7 +12,7 @@ remaining repository-maintenance helpers.
12
12
 
13
13
  ## Other Scripts
14
14
 
15
- - [`doctor.sh`](./doctor.sh): local development regression entrypoint (`sync`/`pip check` + lint + tests + coverage policy)
15
+ - [`doctor.sh`](./doctor.sh): primary local development regression entrypoint (uv sync + lint + tests + coverage)
16
16
  - [`dependency_health.sh`](./dependency_health.sh): dependency review entrypoint (`sync`/`pip check` + outdated + audit)
17
17
  - [`check_coverage.py`](./check_coverage.py): enforces the overall coverage floor and per-file minimums for critical modules
18
18
  - [`lint.sh`](./lint.sh): lint helper
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import asyncio
5
+ import os
6
+ import sys
7
+ from collections.abc import Sequence
8
+
9
+ from . import __version__
10
+ from .server.application import main as serve_main
11
+
12
+
13
+ async def run_call(agent_url: str, text: str, token: str | None = None) -> int:
14
+ from a2a.types import Message, TaskArtifactUpdateEvent, TaskStatusUpdateEvent
15
+
16
+ from .client import A2AClient
17
+
18
+ client = A2AClient(agent_url)
19
+ metadata = {}
20
+ if token:
21
+ # Use Authorization header for bearer auth.
22
+ metadata["authorization"] = f"Bearer {token}"
23
+
24
+ try:
25
+ async for event in client.send_message(text, metadata=metadata):
26
+ if isinstance(event, tuple):
27
+ _, update = event
28
+ if isinstance(update, TaskArtifactUpdateEvent):
29
+ artifact = update.artifact
30
+ if artifact and artifact.parts:
31
+ for part in artifact.parts:
32
+ text_val = getattr(part.root, "text", None)
33
+ if isinstance(text_val, str):
34
+ print(text_val, end="", flush=True)
35
+ elif isinstance(update, TaskStatusUpdateEvent):
36
+ if update.status and update.status.state == "failed":
37
+ print(f"\n[Failed] {update.status.message or ''}")
38
+ elif isinstance(event, Message):
39
+ for part in event.parts:
40
+ text_val = getattr(part.root, "text", None)
41
+ if isinstance(text_val, str):
42
+ print(text_val, end="", flush=True)
43
+ print() # New line after completion
44
+ except Exception as exc:
45
+ print(f"\n[Error] {exc}", file=sys.stderr)
46
+ return 1
47
+ finally:
48
+ await client.close()
49
+ return 0
50
+
51
+
52
+ def build_parser() -> argparse.ArgumentParser:
53
+ parser = argparse.ArgumentParser(
54
+ prog="opencode-a2a",
55
+ description=(
56
+ "OpenCode A2A runtime. Deployment supervision is intentionally left to the operator."
57
+ ),
58
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
59
+ )
60
+ parser.add_argument(
61
+ "--version",
62
+ action="version",
63
+ version=f"%(prog)s {__version__}",
64
+ )
65
+
66
+ 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
+
73
+ call_parser = subparsers.add_parser(
74
+ "call",
75
+ help="Call an A2A agent.",
76
+ description="Call an A2A agent using the A2A protocol.",
77
+ )
78
+ call_parser.add_argument("agent_url", help="URL of the agent to call.")
79
+ call_parser.add_argument("text", help="Text message to send.")
80
+ call_parser.add_argument(
81
+ "--token",
82
+ help="Bearer token for authentication (can also use A2A_CLIENT_BEARER_TOKEN env).",
83
+ default=os.environ.get("A2A_CLIENT_BEARER_TOKEN"),
84
+ )
85
+
86
+ return parser
87
+
88
+
89
+ def main(argv: Sequence[str] | None = None) -> int:
90
+ args = list(sys.argv[1:] if argv is None else argv)
91
+ parser = build_parser()
92
+
93
+ if not args:
94
+ serve_main()
95
+ return 0
96
+
97
+ namespace = parser.parse_args(args)
98
+ if namespace.command == "serve":
99
+ serve_main()
100
+ return 0
101
+
102
+ if namespace.command == "call":
103
+ return asyncio.run(run_call(namespace.agent_url, namespace.text, namespace.token))
104
+
105
+ if namespace.command is None:
106
+ serve_main()
107
+ return 0
108
+
109
+ parser.error(f"Unknown command: {namespace.command}")
110
+ return 2
111
+
112
+
113
+ if __name__ == "__main__":
114
+ raise SystemExit(main())
@@ -0,0 +1,28 @@
1
+ """Reusable A2A client utilities and facade types."""
2
+
3
+ from .client import A2AClient
4
+ from .config import A2AClientSettings, load_settings
5
+ from .errors import (
6
+ A2AAgentUnavailableError,
7
+ A2AClientError,
8
+ A2AClientResetRequiredError,
9
+ A2APeerProtocolError,
10
+ A2AUnsupportedBindingError,
11
+ A2AUnsupportedOperationError,
12
+ )
13
+ from .types import A2AClientEvent, A2AClientEventStream, A2AClientMetadata
14
+
15
+ __all__ = [
16
+ "A2AClient",
17
+ "A2AClientError",
18
+ "A2AAgentUnavailableError",
19
+ "A2AClientResetRequiredError",
20
+ "A2APeerProtocolError",
21
+ "A2AUnsupportedBindingError",
22
+ "A2AUnsupportedOperationError",
23
+ "A2AClientSettings",
24
+ "A2AClientEvent",
25
+ "A2AClientEventStream",
26
+ "A2AClientMetadata",
27
+ "load_settings",
28
+ ]