opencode-a2a 0.4.0__tar.gz → 0.5.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.
- {opencode_a2a-0.4.0/src/opencode_a2a.egg-info → opencode_a2a-0.5.0}/PKG-INFO +4 -2
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/README.md +1 -1
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/docs/guide.md +36 -6
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/pyproject.toml +2 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/cli.py +2 -10
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/client/__init__.py +0 -4
- opencode_a2a-0.5.0/src/opencode_a2a/client/agent_card.py +81 -0
- opencode_a2a-0.5.0/src/opencode_a2a/client/client.py +317 -0
- opencode_a2a-0.5.0/src/opencode_a2a/client/error_mapping.py +148 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/client/errors.py +21 -0
- opencode_a2a-0.5.0/src/opencode_a2a/client/payload_text.py +180 -0
- opencode_a2a-0.5.0/src/opencode_a2a/client/request_context.py +86 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/config.py +47 -19
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/contracts/extensions.py +5 -8
- opencode_a2a-0.5.0/src/opencode_a2a/execution/event_helpers.py +37 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/executor.py +88 -777
- opencode_a2a-0.5.0/src/opencode_a2a/execution/session_manager.py +157 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/stream_events.py +1 -1
- opencode_a2a-0.5.0/src/opencode_a2a/execution/stream_runtime.py +578 -0
- opencode_a2a-0.5.0/src/opencode_a2a/execution/tool_error_mapping.py +141 -0
- opencode_a2a-0.4.0/src/opencode_a2a/execution/upstream_errors.py → opencode_a2a-0.5.0/src/opencode_a2a/execution/upstream_error_translator.py +6 -82
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/jsonrpc/application.py +134 -245
- opencode_a2a-0.5.0/src/opencode_a2a/jsonrpc/error_responses.py +137 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/opencode_upstream_client.py +32 -79
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/parts/text.py +0 -4
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/profile/runtime.py +7 -6
- opencode_a2a-0.5.0/src/opencode_a2a/runtime_state.py +20 -0
- opencode_a2a-0.5.0/src/opencode_a2a/sandbox_policy.py +111 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/server/application.py +198 -21
- opencode_a2a-0.5.0/src/opencode_a2a/server/state_store.py +664 -0
- opencode_a2a-0.5.0/src/opencode_a2a/server/task_store.py +116 -0
- opencode_a2a-0.5.0/src/opencode_a2a/upstream_taxonomy.py +95 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0/src/opencode_a2a.egg-info}/PKG-INFO +4 -2
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/SOURCES.txt +25 -2
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/requires.txt +2 -0
- opencode_a2a-0.5.0/tests/client/test_agent_card.py +68 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/client/test_client_facade.py +15 -299
- opencode_a2a-0.5.0/tests/client/test_error_mapping.py +81 -0
- opencode_a2a-0.5.0/tests/client/test_payload_text.py +104 -0
- opencode_a2a-0.5.0/tests/client/test_request_context.py +54 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/config/test_settings.py +49 -4
- opencode_a2a-0.5.0/tests/conftest.py +36 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/contracts/test_extension_contract_consistency.py +1 -1
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_cancellation.py +23 -3
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_directory_validation.py +20 -8
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_metrics.py +2 -4
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_multipart_input.py +3 -3
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_opencode_agent_session_binding.py +150 -37
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_session_ownership.py +167 -18
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_streaming_output_contract_core.py +41 -0
- opencode_a2a-0.5.0/tests/jsonrpc/test_error_responses.py +79 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/jsonrpc/test_jsonrpc_unsupported_method.py +35 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/jsonrpc/test_opencode_session_extension_commands.py +5 -2
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/jsonrpc/test_opencode_session_extension_interrupts.py +7 -7
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/jsonrpc/test_opencode_session_extension_prompt_async.py +8 -7
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/parts/test_parts_text.py +2 -2
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/profile/test_profile_runtime.py +17 -0
- opencode_a2a-0.5.0/tests/server/test_a2a_client_manager.py +199 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/server/test_agent_card.py +19 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/server/test_app_behaviors.py +1 -1
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/server/test_cli.py +6 -15
- opencode_a2a-0.5.0/tests/server/test_database_app_persistence.py +210 -0
- opencode_a2a-0.5.0/tests/server/test_state_store.py +193 -0
- opencode_a2a-0.5.0/tests/server/test_task_store_factory.py +78 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/server/test_transport_contract.py +61 -35
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/support/helpers.py +12 -7
- opencode_a2a-0.5.0/tests/support/session_extensions.py +19 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/support/streaming_output.py +3 -3
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/upstream/test_opencode_upstream_client_params.py +54 -21
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/uv.lock +113 -0
- opencode_a2a-0.4.0/src/opencode_a2a/client/client.py +0 -608
- opencode_a2a-0.4.0/src/opencode_a2a/client/types.py +0 -21
- opencode_a2a-0.4.0/tests/support/session_extensions.py +0 -10
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/.github/workflows/ci.yml +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/.github/workflows/dependency-health.yml +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/.github/workflows/publish.yml +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/.gitignore +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/.pre-commit-config.yaml +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/.secrets.baseline +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/AGENTS.md +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/CONTRIBUTING.md +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/LICENSE +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/SECURITY.md +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/scripts/README.md +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/scripts/check_coverage.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/scripts/dependency_health.sh +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/scripts/doctor.sh +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/scripts/health_common.sh +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/scripts/lint.sh +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/scripts/smoke_test_built_cli.sh +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/setup.cfg +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/client/config.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/contracts/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/request_context.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/stream_state.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/jsonrpc/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/jsonrpc/methods.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/jsonrpc/params.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/parts/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/parts/mapping.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/profile/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/server/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/server/agent_card.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/server/openapi.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a/server/request_parsing.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/dependency_links.txt +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/entry_points.txt +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/top_level.txt +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/client/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/client/test_client_config.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/config/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/contracts/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_agent_errors.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_agent_helpers.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_streaming_output_contract_blocks.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_streaming_output_contract_interrupts.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/execution/test_streaming_output_contract_logging.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/jsonrpc/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/jsonrpc/test_jsonrpc_methods.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/jsonrpc/test_jsonrpc_params.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/jsonrpc/test_opencode_session_extension_queries.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/package/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/package/test_version.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/parts/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/profile/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/scripts/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/scripts/test_script_health_contract.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/server/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/server/test_call_context_builder.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/server/test_cancel_contract.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/support/__init__.py +0 -0
- {opencode_a2a-0.4.0 → opencode_a2a-0.5.0}/tests/upstream/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-a2a
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: OpenCode A2A runtime
|
|
5
5
|
Author: liujuanjuan1984@Intelligent-Internet
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -20,10 +20,12 @@ Requires-Python: >=3.11
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: a2a-sdk==0.3.25
|
|
23
|
+
Requires-Dist: aiosqlite<1.0,>=0.20
|
|
23
24
|
Requires-Dist: fastapi<1.0,>=0.110
|
|
24
25
|
Requires-Dist: httpx<1.0,>=0.27
|
|
25
26
|
Requires-Dist: pydantic<3.0,>=2.6
|
|
26
27
|
Requires-Dist: pydantic-settings<3.0,>=2.2
|
|
28
|
+
Requires-Dist: sqlalchemy<3.0,>=2.0
|
|
27
29
|
Requires-Dist: sse-starlette<4.0,>=2.1
|
|
28
30
|
Requires-Dist: uvicorn<1.0,>=0.29
|
|
29
31
|
Provides-Extra: dev
|
|
@@ -115,7 +117,7 @@ A2A_HOST=127.0.0.1 \
|
|
|
115
117
|
A2A_PORT=8000 \
|
|
116
118
|
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
117
119
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
118
|
-
opencode-a2a
|
|
120
|
+
opencode-a2a
|
|
119
121
|
```
|
|
120
122
|
|
|
121
123
|
Verify that the service is up:
|
|
@@ -55,10 +55,10 @@ Key variables to understand protocol behavior:
|
|
|
55
55
|
also logged with preview truncation controlled by `A2A_LOG_BODY_LIMIT`.
|
|
56
56
|
- `A2A_MAX_REQUEST_BODY_BYTES`: runtime request-body limit. Oversized requests
|
|
57
57
|
return HTTP `413`.
|
|
58
|
-
- `
|
|
59
|
-
|
|
58
|
+
- `A2A_PENDING_SESSION_CLAIM_TTL_SECONDS`: lease duration for pending preferred
|
|
59
|
+
session claims before they expire and stop blocking other identities.
|
|
60
60
|
- `A2A_INTERRUPT_REQUEST_TTL_SECONDS`: active retention window for the
|
|
61
|
-
|
|
61
|
+
interrupt request binding registry used by `a2a.interrupt.*`
|
|
62
62
|
callback methods. Default: `10800` seconds (`180` minutes).
|
|
63
63
|
- `A2A_INTERRUPT_REQUEST_TOMBSTONE_TTL_SECONDS`: retention window for expired
|
|
64
64
|
interrupt tombstones after active TTL has elapsed. During this window,
|
|
@@ -76,6 +76,12 @@ Key variables to understand protocol behavior:
|
|
|
76
76
|
- `A2A_CLIENT_BEARER_TOKEN`: optional bearer token attached to outbound peer
|
|
77
77
|
calls made by the embedded A2A client and `a2a_call` tool path.
|
|
78
78
|
- `A2A_CLIENT_SUPPORTED_TRANSPORTS`: ordered outbound transport preference list.
|
|
79
|
+
- `A2A_TASK_STORE_BACKEND`: runtime state backend. Supported values: `database`,
|
|
80
|
+
`memory`. Default: `database`.
|
|
81
|
+
- `A2A_TASK_STORE_DATABASE_URL`: database URL used by the default durable
|
|
82
|
+
backend. Default: `sqlite+aiosqlite:///./opencode-a2a.db`.
|
|
83
|
+
- `A2A_TASK_STORE_TABLE_NAME` / `A2A_TASK_STORE_CREATE_TABLE`: database task
|
|
84
|
+
store table name and whether to auto-create database tables on startup.
|
|
79
85
|
- Runtime authentication is bearer-token only via `A2A_BEARER_TOKEN`.
|
|
80
86
|
- The same outbound client flags are also honored by the server-side embedded
|
|
81
87
|
A2A client used for peer calls and `a2a_call` tool execution:
|
|
@@ -154,7 +160,31 @@ A2A_HOST=127.0.0.1 \
|
|
|
154
160
|
A2A_PORT=8000 \
|
|
155
161
|
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
156
162
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
157
|
-
opencode-a2a
|
|
163
|
+
opencode-a2a
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
By default, the service uses a SQLite-backed durable state store:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
|
|
170
|
+
A2A_BEARER_TOKEN=dev-token \
|
|
171
|
+
A2A_TASK_STORE_DATABASE_URL=sqlite+aiosqlite:///./opencode-a2a.db \
|
|
172
|
+
opencode-a2a
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
With the default `database` backend, the service persists:
|
|
176
|
+
|
|
177
|
+
- task records
|
|
178
|
+
- session binding / ownership state
|
|
179
|
+
- interrupt request bindings and tombstones
|
|
180
|
+
|
|
181
|
+
In-flight asyncio locks, outbound A2A client caches, and stream-local
|
|
182
|
+
aggregation buffers remain process-local runtime state.
|
|
183
|
+
|
|
184
|
+
To opt into an ephemeral development profile, set:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
A2A_TASK_STORE_BACKEND=memory
|
|
158
188
|
```
|
|
159
189
|
|
|
160
190
|
## Troubleshooting Provider Auth State
|
|
@@ -825,8 +855,8 @@ Notes:
|
|
|
825
855
|
|
|
826
856
|
- `request_id` must be a live interrupt request observed from stream metadata
|
|
827
857
|
(`metadata.shared.interrupt.request_id`).
|
|
828
|
-
- The server keeps an
|
|
829
|
-
|
|
858
|
+
- The server keeps an interrupt binding registry; callbacks with unknown or
|
|
859
|
+
expired `request_id` are rejected.
|
|
830
860
|
- The cache retention windows are controlled by
|
|
831
861
|
`A2A_INTERRUPT_REQUEST_TTL_SECONDS` (default: `10800` seconds / `180`
|
|
832
862
|
minutes) and `A2A_INTERRUPT_REQUEST_TOMBSTONE_TTL_SECONDS` (default: `600`
|
|
@@ -25,10 +25,12 @@ classifiers = [
|
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
27
|
"a2a-sdk==0.3.25",
|
|
28
|
+
"aiosqlite>=0.20,<1.0",
|
|
28
29
|
"fastapi>=0.110,<1.0",
|
|
29
30
|
"httpx>=0.27,<1.0",
|
|
30
31
|
"pydantic>=2.6,<3.0",
|
|
31
32
|
"pydantic-settings>=2.2,<3.0",
|
|
33
|
+
"sqlalchemy>=2.0,<3.0",
|
|
32
34
|
"sse-starlette>=2.1,<4.0",
|
|
33
35
|
"uvicorn>=0.29,<1.0",
|
|
34
36
|
]
|
|
@@ -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.
|
|
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"]
|