opencode-a2a 0.4.1__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.1/src/opencode_a2a.egg-info → opencode_a2a-0.5.0}/PKG-INFO +3 -1
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/docs/guide.md +35 -5
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/pyproject.toml +2 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/config.py +29 -3
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/contracts/extensions.py +3 -2
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/executor.py +6 -1
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/session_manager.py +42 -25
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/stream_events.py +1 -1
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/stream_runtime.py +3 -3
- opencode_a2a-0.4.1/src/opencode_a2a/execution/upstream_errors.py → opencode_a2a-0.5.0/src/opencode_a2a/execution/upstream_error_translator.py +3 -3
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/jsonrpc/application.py +5 -5
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/opencode_upstream_client.py +32 -79
- opencode_a2a-0.5.0/src/opencode_a2a/runtime_state.py +20 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/server/application.py +53 -6
- 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.4.1 → opencode_a2a-0.5.0/src/opencode_a2a.egg-info}/PKG-INFO +3 -1
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/SOURCES.txt +11 -4
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/requires.txt +2 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/config/test_settings.py +15 -0
- opencode_a2a-0.5.0/tests/conftest.py +36 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/contracts/test_extension_contract_consistency.py +1 -1
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_cancellation.py +22 -2
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_metrics.py +2 -2
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_multipart_input.py +3 -3
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_opencode_agent_session_binding.py +2 -12
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_session_ownership.py +161 -15
- opencode_a2a-0.4.1/tests/jsonrpc/test_error_mapping.py → opencode_a2a-0.5.0/tests/jsonrpc/test_error_responses.py +1 -1
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/jsonrpc/test_opencode_session_extension_interrupts.py +7 -7
- 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.1 → opencode_a2a-0.5.0}/tests/server/test_transport_contract.py +42 -12
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/support/helpers.py +12 -7
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/support/streaming_output.py +3 -3
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/upstream/test_opencode_upstream_client_params.py +21 -21
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/uv.lock +113 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/.github/workflows/ci.yml +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/.github/workflows/dependency-health.yml +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/.github/workflows/publish.yml +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/.gitignore +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/.pre-commit-config.yaml +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/.secrets.baseline +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/AGENTS.md +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/CONTRIBUTING.md +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/LICENSE +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/README.md +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/SECURITY.md +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/scripts/README.md +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/scripts/check_coverage.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/scripts/dependency_health.sh +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/scripts/doctor.sh +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/scripts/health_common.sh +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/scripts/lint.sh +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/scripts/smoke_test_built_cli.sh +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/setup.cfg +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/cli.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/client/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/client/agent_card.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/client/client.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/client/config.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/client/error_mapping.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/client/errors.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/client/payload_text.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/client/request_context.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/contracts/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/event_helpers.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/request_context.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/stream_state.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/execution/tool_error_mapping.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/jsonrpc/__init__.py +0 -0
- /opencode_a2a-0.4.1/src/opencode_a2a/jsonrpc/error_mapping.py → /opencode_a2a-0.5.0/src/opencode_a2a/jsonrpc/error_responses.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/jsonrpc/methods.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/jsonrpc/params.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/parts/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/parts/mapping.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/parts/text.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/profile/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/profile/runtime.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/sandbox_policy.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/server/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/server/agent_card.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/server/openapi.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a/server/request_parsing.py +0 -0
- /opencode_a2a-0.4.1/src/opencode_a2a/error_taxonomy.py → /opencode_a2a-0.5.0/src/opencode_a2a/upstream_taxonomy.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/dependency_links.txt +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/entry_points.txt +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/src/opencode_a2a.egg-info/top_level.txt +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/client/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/client/test_agent_card.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/client/test_client_config.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/client/test_client_facade.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/client/test_error_mapping.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/client/test_payload_text.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/client/test_request_context.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/config/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/contracts/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_agent_errors.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_agent_helpers.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_directory_validation.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_streaming_output_contract_blocks.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_streaming_output_contract_core.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_streaming_output_contract_interrupts.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/execution/test_streaming_output_contract_logging.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/jsonrpc/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/jsonrpc/test_jsonrpc_methods.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/jsonrpc/test_jsonrpc_params.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/jsonrpc/test_jsonrpc_unsupported_method.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/jsonrpc/test_opencode_session_extension_commands.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/jsonrpc/test_opencode_session_extension_prompt_async.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/jsonrpc/test_opencode_session_extension_queries.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/package/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/package/test_version.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/parts/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/parts/test_parts_text.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/profile/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/profile/test_profile_runtime.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/scripts/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/scripts/test_script_health_contract.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/server/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/server/test_a2a_client_manager.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/server/test_agent_card.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/server/test_app_behaviors.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/server/test_call_context_builder.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/server/test_cancel_contract.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/server/test_cli.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/support/__init__.py +0 -0
- {opencode_a2a-0.4.1 → opencode_a2a-0.5.0}/tests/support/session_extensions.py +0 -0
- {opencode_a2a-0.4.1 → 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
|
|
@@ -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:
|
|
@@ -157,6 +163,30 @@ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
|
157
163
|
opencode-a2a
|
|
158
164
|
```
|
|
159
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
|
|
188
|
+
```
|
|
189
|
+
|
|
160
190
|
## Troubleshooting Provider Auth State
|
|
161
191
|
|
|
162
192
|
If one deployment works while another fails against the same upstream provider,
|
|
@@ -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
|
]
|
|
@@ -35,6 +35,7 @@ WriteAccessScope = Literal[
|
|
|
35
35
|
"custom",
|
|
36
36
|
]
|
|
37
37
|
OutsideWorkspaceAccess = Literal["unknown", "allowed", "disallowed", "custom"]
|
|
38
|
+
TaskStoreBackend = Literal["memory", "database"]
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def _parse_declared_list(value: Any) -> tuple[str, ...]:
|
|
@@ -132,9 +133,11 @@ class Settings(BaseSettings):
|
|
|
132
133
|
a2a_port: int = Field(default=8000, alias="A2A_PORT")
|
|
133
134
|
a2a_bearer_token: str = Field(..., min_length=1, alias="A2A_BEARER_TOKEN")
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
a2a_pending_session_claim_ttl_seconds: float = Field(
|
|
137
|
+
default=30.0,
|
|
138
|
+
gt=0.0,
|
|
139
|
+
alias="A2A_PENDING_SESSION_CLAIM_TTL_SECONDS",
|
|
140
|
+
)
|
|
138
141
|
a2a_interrupt_request_ttl_seconds: float = Field(
|
|
139
142
|
default=10_800.0,
|
|
140
143
|
ge=0.0,
|
|
@@ -176,7 +179,30 @@ class Settings(BaseSettings):
|
|
|
176
179
|
alias="A2A_CLIENT_SUPPORTED_TRANSPORTS",
|
|
177
180
|
)
|
|
178
181
|
|
|
182
|
+
# Task store settings
|
|
183
|
+
a2a_task_store_backend: TaskStoreBackend = Field(
|
|
184
|
+
default="database",
|
|
185
|
+
alias="A2A_TASK_STORE_BACKEND",
|
|
186
|
+
)
|
|
187
|
+
a2a_task_store_database_url: str | None = Field(
|
|
188
|
+
default="sqlite+aiosqlite:///./opencode-a2a.db",
|
|
189
|
+
alias="A2A_TASK_STORE_DATABASE_URL",
|
|
190
|
+
)
|
|
191
|
+
a2a_task_store_table_name: str = Field(
|
|
192
|
+
default="tasks",
|
|
193
|
+
min_length=1,
|
|
194
|
+
alias="A2A_TASK_STORE_TABLE_NAME",
|
|
195
|
+
)
|
|
196
|
+
a2a_task_store_create_table: bool = Field(
|
|
197
|
+
default=True,
|
|
198
|
+
alias="A2A_TASK_STORE_CREATE_TABLE",
|
|
199
|
+
)
|
|
200
|
+
|
|
179
201
|
@model_validator(mode="after")
|
|
180
202
|
def _validate_sandbox_policy(self) -> Settings:
|
|
181
203
|
SandboxPolicy.from_settings(self).validate_configuration()
|
|
204
|
+
if self.a2a_task_store_backend == "database" and not self.a2a_task_store_database_url:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
"A2A_TASK_STORE_DATABASE_URL is required when A2A_TASK_STORE_BACKEND=database"
|
|
207
|
+
)
|
|
182
208
|
return self
|
|
@@ -450,8 +450,9 @@ def build_session_binding_extension_params(
|
|
|
450
450
|
"message to that upstream session."
|
|
451
451
|
),
|
|
452
452
|
(
|
|
453
|
-
"Otherwise, the server will create a new upstream session and
|
|
454
|
-
"the (identity, contextId)->session_id mapping
|
|
453
|
+
"Otherwise, the server will create a new upstream session and retain "
|
|
454
|
+
"the (identity, contextId)->session_id mapping according to the "
|
|
455
|
+
"configured task/state store backend and TTL policy."
|
|
455
456
|
),
|
|
456
457
|
],
|
|
457
458
|
}
|
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from ..server.application import A2AClientManager
|
|
14
|
+
from ..server.state_store import SessionStateRepository
|
|
14
15
|
|
|
15
16
|
import httpx
|
|
16
17
|
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
@@ -72,7 +73,7 @@ from .stream_state import (
|
|
|
72
73
|
_TTLCache,
|
|
73
74
|
)
|
|
74
75
|
from .tool_error_mapping import build_tool_error, map_a2a_tool_exception
|
|
75
|
-
from .
|
|
76
|
+
from .upstream_error_translator import (
|
|
76
77
|
_await_stream_terminal_signal,
|
|
77
78
|
_extract_upstream_error_detail,
|
|
78
79
|
_format_inband_upstream_error,
|
|
@@ -530,7 +531,9 @@ class OpencodeAgentExecutor(AgentExecutor):
|
|
|
530
531
|
cancel_abort_timeout_seconds: float = 2.0,
|
|
531
532
|
session_cache_ttl_seconds: int = 3600,
|
|
532
533
|
session_cache_maxsize: int = 10_000,
|
|
534
|
+
pending_session_claim_ttl_seconds: float = 30.0,
|
|
533
535
|
a2a_client_manager: A2AClientManager | None = None,
|
|
536
|
+
session_state_repository: SessionStateRepository | None = None,
|
|
534
537
|
) -> None:
|
|
535
538
|
self._client = client
|
|
536
539
|
self._streaming_enabled = streaming_enabled
|
|
@@ -544,6 +547,8 @@ class OpencodeAgentExecutor(AgentExecutor):
|
|
|
544
547
|
client=client,
|
|
545
548
|
session_cache_ttl_seconds=session_cache_ttl_seconds,
|
|
546
549
|
session_cache_maxsize=session_cache_maxsize,
|
|
550
|
+
pending_session_claim_ttl_seconds=pending_session_claim_ttl_seconds,
|
|
551
|
+
state_repository=session_state_repository,
|
|
547
552
|
)
|
|
548
553
|
self._stream_runtime = StreamRuntime(
|
|
549
554
|
client=client,
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
|
|
5
|
-
from .
|
|
5
|
+
from ..server.state_store import MemorySessionStateRepository, SessionStateRepository
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class SessionManager:
|
|
@@ -12,18 +12,15 @@ class SessionManager:
|
|
|
12
12
|
client,
|
|
13
13
|
session_cache_ttl_seconds: int = 3600,
|
|
14
14
|
session_cache_maxsize: int = 10_000,
|
|
15
|
+
pending_session_claim_ttl_seconds: float = 30.0,
|
|
16
|
+
state_repository: SessionStateRepository | None = None,
|
|
15
17
|
) -> None:
|
|
16
18
|
self._client = client
|
|
17
|
-
self.
|
|
19
|
+
self._state_repository = state_repository or MemorySessionStateRepository(
|
|
18
20
|
ttl_seconds=session_cache_ttl_seconds,
|
|
19
21
|
maxsize=session_cache_maxsize,
|
|
22
|
+
pending_claim_ttl_seconds=pending_session_claim_ttl_seconds,
|
|
20
23
|
)
|
|
21
|
-
self._session_owners = _TTLCache(
|
|
22
|
-
ttl_seconds=session_cache_ttl_seconds,
|
|
23
|
-
maxsize=session_cache_maxsize,
|
|
24
|
-
refresh_on_get=True,
|
|
25
|
-
)
|
|
26
|
-
self._pending_session_claims: dict[str, str] = {}
|
|
27
24
|
self._lock = asyncio.Lock()
|
|
28
25
|
self._inflight_session_creates: dict[tuple[str, str], asyncio.Task[str]] = {}
|
|
29
26
|
self._session_locks: dict[str, asyncio.Lock] = {}
|
|
@@ -43,13 +40,21 @@ class SessionManager:
|
|
|
43
40
|
session_id=preferred_session_id,
|
|
44
41
|
)
|
|
45
42
|
if not pending_claim:
|
|
46
|
-
self.
|
|
43
|
+
async with self._lock:
|
|
44
|
+
await self._state_repository.set_session(
|
|
45
|
+
identity=identity,
|
|
46
|
+
context_id=context_id,
|
|
47
|
+
session_id=preferred_session_id,
|
|
48
|
+
)
|
|
47
49
|
return preferred_session_id, pending_claim
|
|
48
50
|
|
|
49
51
|
task: asyncio.Task[str] | None = None
|
|
50
52
|
cache_key = (identity, context_id)
|
|
51
53
|
async with self._lock:
|
|
52
|
-
existing = self.
|
|
54
|
+
existing = await self._state_repository.get_session(
|
|
55
|
+
identity=cache_key[0],
|
|
56
|
+
context_id=cache_key[1],
|
|
57
|
+
)
|
|
53
58
|
if existing:
|
|
54
59
|
return existing, False
|
|
55
60
|
task = self._inflight_session_creates.get(cache_key)
|
|
@@ -68,14 +73,18 @@ class SessionManager:
|
|
|
68
73
|
raise
|
|
69
74
|
|
|
70
75
|
async with self._lock:
|
|
71
|
-
owner = self.
|
|
76
|
+
owner = await self._state_repository.get_owner(session_id=session_id)
|
|
72
77
|
if owner and owner != identity:
|
|
73
78
|
if self._inflight_session_creates.get(cache_key) is task:
|
|
74
79
|
self._inflight_session_creates.pop(cache_key, None)
|
|
75
80
|
raise PermissionError(f"Session {session_id} is not owned by you")
|
|
76
|
-
self.
|
|
81
|
+
await self._state_repository.set_session(
|
|
82
|
+
identity=cache_key[0],
|
|
83
|
+
context_id=cache_key[1],
|
|
84
|
+
session_id=session_id,
|
|
85
|
+
)
|
|
77
86
|
if not owner:
|
|
78
|
-
self.
|
|
87
|
+
await self._state_repository.set_owner(session_id=session_id, identity=identity)
|
|
79
88
|
if self._inflight_session_creates.get(cache_key) is task:
|
|
80
89
|
self._inflight_session_creates.pop(cache_key, None)
|
|
81
90
|
return session_id, False
|
|
@@ -89,37 +98,45 @@ class SessionManager:
|
|
|
89
98
|
) -> None:
|
|
90
99
|
await self.finalize_session_claim(identity=identity, session_id=session_id)
|
|
91
100
|
async with self._lock:
|
|
92
|
-
self.
|
|
101
|
+
await self._state_repository.set_session(
|
|
102
|
+
identity=identity,
|
|
103
|
+
context_id=context_id,
|
|
104
|
+
session_id=session_id,
|
|
105
|
+
)
|
|
93
106
|
|
|
94
107
|
async def claim_preferred_session(self, *, identity: str, session_id: str) -> bool:
|
|
95
108
|
async with self._lock:
|
|
96
|
-
owner = self.
|
|
97
|
-
pending_owner = self.
|
|
109
|
+
owner = await self._state_repository.get_owner(session_id=session_id)
|
|
110
|
+
pending_owner = await self._state_repository.get_pending_claim(session_id=session_id)
|
|
98
111
|
if owner and owner != identity:
|
|
99
112
|
raise PermissionError(f"Session {session_id} is not owned by you")
|
|
100
113
|
if pending_owner and pending_owner != identity:
|
|
101
114
|
raise PermissionError(f"Session {session_id} is not owned by you")
|
|
102
115
|
if owner == identity:
|
|
103
116
|
return False
|
|
104
|
-
self.
|
|
117
|
+
await self._state_repository.set_pending_claim(session_id=session_id, identity=identity)
|
|
105
118
|
return True
|
|
106
119
|
|
|
107
120
|
async def finalize_session_claim(self, *, identity: str, session_id: str) -> None:
|
|
108
121
|
async with self._lock:
|
|
109
|
-
owner = self.
|
|
110
|
-
pending_owner = self.
|
|
122
|
+
owner = await self._state_repository.get_owner(session_id=session_id)
|
|
123
|
+
pending_owner = await self._state_repository.get_pending_claim(session_id=session_id)
|
|
111
124
|
if owner and owner != identity:
|
|
112
125
|
raise PermissionError(f"Session {session_id} is not owned by you")
|
|
113
126
|
if pending_owner and pending_owner != identity:
|
|
114
127
|
raise PermissionError(f"Session {session_id} is not owned by you")
|
|
115
|
-
self.
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
await self._state_repository.set_owner(session_id=session_id, identity=identity)
|
|
129
|
+
await self._state_repository.clear_pending_claim(
|
|
130
|
+
session_id=session_id,
|
|
131
|
+
identity=identity,
|
|
132
|
+
)
|
|
118
133
|
|
|
119
134
|
async def release_preferred_session_claim(self, *, identity: str, session_id: str) -> None:
|
|
120
135
|
async with self._lock:
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
await self._state_repository.clear_pending_claim(
|
|
137
|
+
session_id=session_id,
|
|
138
|
+
identity=identity,
|
|
139
|
+
)
|
|
123
140
|
|
|
124
141
|
async def get_session_lock(self, session_id: str) -> asyncio.Lock:
|
|
125
142
|
async with self._lock:
|
|
@@ -136,5 +153,5 @@ class SessionManager:
|
|
|
136
153
|
context_id: str,
|
|
137
154
|
) -> asyncio.Task[str] | None:
|
|
138
155
|
async with self._lock:
|
|
139
|
-
self.
|
|
156
|
+
await self._state_repository.pop_session(identity=identity, context_id=context_id)
|
|
140
157
|
return self._inflight_session_creates.pop((identity, context_id), None)
|
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
|
8
8
|
from a2a.types import TaskState
|
|
9
9
|
|
|
10
10
|
from .stream_state import BlockType
|
|
11
|
-
from .
|
|
11
|
+
from .upstream_error_translator import (
|
|
12
12
|
_format_inband_upstream_error,
|
|
13
13
|
_format_stream_terminal_error,
|
|
14
14
|
_StreamTerminalSignal,
|
|
@@ -45,7 +45,7 @@ from .stream_state import (
|
|
|
45
45
|
_StreamOutputState,
|
|
46
46
|
_StreamPartState,
|
|
47
47
|
)
|
|
48
|
-
from .
|
|
48
|
+
from .upstream_error_translator import _StreamTerminalSignal
|
|
49
49
|
|
|
50
50
|
logger = logging.getLogger("opencode_a2a.execution.executor")
|
|
51
51
|
|
|
@@ -429,7 +429,7 @@ class StreamRuntime:
|
|
|
429
429
|
None,
|
|
430
430
|
)
|
|
431
431
|
if callable(remember_request):
|
|
432
|
-
remember_request(
|
|
432
|
+
await remember_request(
|
|
433
433
|
request_id=request_id,
|
|
434
434
|
session_id=session_id,
|
|
435
435
|
interrupt_type=asked["interrupt_type"],
|
|
@@ -456,7 +456,7 @@ class StreamRuntime:
|
|
|
456
456
|
None,
|
|
457
457
|
)
|
|
458
458
|
if callable(discard_request):
|
|
459
|
-
discard_request(resolved_request_id)
|
|
459
|
+
await discard_request(resolved_request_id)
|
|
460
460
|
if cleared_pending:
|
|
461
461
|
await _emit_interrupt_status(
|
|
462
462
|
state=TaskState.working,
|
|
@@ -7,13 +7,13 @@ from dataclasses import dataclass
|
|
|
7
7
|
import httpx
|
|
8
8
|
from a2a.types import TaskState
|
|
9
9
|
|
|
10
|
-
from ..
|
|
10
|
+
from ..opencode_upstream_client import UpstreamContractError
|
|
11
|
+
from ..upstream_taxonomy import (
|
|
11
12
|
extract_upstream_error_detail as _extract_upstream_error_detail,
|
|
12
13
|
)
|
|
13
|
-
from ..
|
|
14
|
+
from ..upstream_taxonomy import (
|
|
14
15
|
resolve_upstream_http_error_profile as _resolve_upstream_error_profile,
|
|
15
16
|
)
|
|
16
|
-
from ..opencode_upstream_client import UpstreamContractError
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@dataclass(frozen=True)
|
|
@@ -22,7 +22,7 @@ from ..contracts.extensions import (
|
|
|
22
22
|
SESSION_QUERY_ERROR_BUSINESS_CODES,
|
|
23
23
|
)
|
|
24
24
|
from ..opencode_upstream_client import OpencodeUpstreamClient, UpstreamContractError
|
|
25
|
-
from .
|
|
25
|
+
from .error_responses import (
|
|
26
26
|
interrupt_not_found_error,
|
|
27
27
|
invalid_params_error,
|
|
28
28
|
method_not_supported_error,
|
|
@@ -776,7 +776,7 @@ class OpencodeSessionQueryJSONRPCApplication(A2AFastAPIApplication):
|
|
|
776
776
|
)
|
|
777
777
|
resolve_request = getattr(self._upstream_client, "resolve_interrupt_request", None)
|
|
778
778
|
if callable(resolve_request):
|
|
779
|
-
status, binding = resolve_request(request_id)
|
|
779
|
+
status, binding = await resolve_request(request_id)
|
|
780
780
|
if status != "active" or binding is None:
|
|
781
781
|
return self._generate_error_response(
|
|
782
782
|
base_request.id,
|
|
@@ -818,7 +818,7 @@ class OpencodeSessionQueryJSONRPCApplication(A2AFastAPIApplication):
|
|
|
818
818
|
else:
|
|
819
819
|
resolve_session = getattr(self._upstream_client, "resolve_interrupt_session", None)
|
|
820
820
|
if callable(resolve_session):
|
|
821
|
-
if not resolve_session(request_id):
|
|
821
|
+
if not await resolve_session(request_id):
|
|
822
822
|
return self._generate_error_response(
|
|
823
823
|
base_request.id,
|
|
824
824
|
interrupt_not_found_error(
|
|
@@ -869,7 +869,7 @@ class OpencodeSessionQueryJSONRPCApplication(A2AFastAPIApplication):
|
|
|
869
869
|
await self._upstream_client.question_reject(request_id, directory=directory)
|
|
870
870
|
discard_request = getattr(self._upstream_client, "discard_interrupt_request", None)
|
|
871
871
|
if callable(discard_request):
|
|
872
|
-
discard_request(request_id)
|
|
872
|
+
await discard_request(request_id)
|
|
873
873
|
except ValueError as exc:
|
|
874
874
|
return self._generate_error_response(
|
|
875
875
|
base_request.id,
|
|
@@ -880,7 +880,7 @@ class OpencodeSessionQueryJSONRPCApplication(A2AFastAPIApplication):
|
|
|
880
880
|
if upstream_status == 404:
|
|
881
881
|
discard_request = getattr(self._upstream_client, "discard_interrupt_request", None)
|
|
882
882
|
if callable(discard_request):
|
|
883
|
-
discard_request(request_id)
|
|
883
|
+
await discard_request(request_id)
|
|
884
884
|
return self._generate_error_response(
|
|
885
885
|
base_request.id,
|
|
886
886
|
interrupt_not_found_error(
|
|
@@ -12,6 +12,8 @@ import httpx
|
|
|
12
12
|
|
|
13
13
|
from .config import Settings
|
|
14
14
|
from .parts.text import extract_text_from_parts
|
|
15
|
+
from .runtime_state import InterruptRequestBinding
|
|
16
|
+
from .server.state_store import InterruptRequestRepository, MemoryInterruptRequestRepository
|
|
15
17
|
|
|
16
18
|
_UNSET = object()
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
@@ -29,25 +31,13 @@ class OpencodeMessage:
|
|
|
29
31
|
raw: dict[str, Any]
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
@dataclass(frozen=True)
|
|
33
|
-
class InterruptRequestBinding:
|
|
34
|
-
request_id: str
|
|
35
|
-
session_id: str
|
|
36
|
-
interrupt_type: str
|
|
37
|
-
identity: str | None
|
|
38
|
-
task_id: str | None
|
|
39
|
-
context_id: str | None
|
|
40
|
-
expires_at: float
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@dataclass(frozen=True)
|
|
44
|
-
class InterruptRequestTombstone:
|
|
45
|
-
request_id: str
|
|
46
|
-
expires_at: float
|
|
47
|
-
|
|
48
|
-
|
|
49
34
|
class OpencodeUpstreamClient:
|
|
50
|
-
def __init__(
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
settings: Settings,
|
|
38
|
+
*,
|
|
39
|
+
interrupt_request_repository: InterruptRequestRepository | None = None,
|
|
40
|
+
) -> None:
|
|
51
41
|
self._settings = settings
|
|
52
42
|
self._base_url = settings.opencode_base_url.rstrip("/")
|
|
53
43
|
self._directory = settings.opencode_workspace_root
|
|
@@ -61,10 +51,20 @@ class OpencodeUpstreamClient:
|
|
|
61
51
|
settings.a2a_interrupt_request_tombstone_ttl_seconds
|
|
62
52
|
)
|
|
63
53
|
self._interrupt_request_clock = time.monotonic
|
|
64
|
-
self.
|
|
65
|
-
|
|
54
|
+
self._interrupt_request_repository = (
|
|
55
|
+
interrupt_request_repository
|
|
56
|
+
or MemoryInterruptRequestRepository(
|
|
57
|
+
request_ttl_seconds=self._interrupt_request_ttl_seconds,
|
|
58
|
+
tombstone_ttl_seconds=self._interrupt_request_tombstone_ttl_seconds,
|
|
59
|
+
clock=self._interrupt_request_clock,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
66
62
|
self._client = self._build_http_client(self._base_url)
|
|
67
63
|
|
|
64
|
+
def _sync_interrupt_clock(self) -> None:
|
|
65
|
+
if isinstance(self._interrupt_request_repository, MemoryInterruptRequestRepository):
|
|
66
|
+
self._interrupt_request_repository._clock = self._interrupt_request_clock
|
|
67
|
+
|
|
68
68
|
def _build_http_client(self, base_url: str) -> httpx.AsyncClient:
|
|
69
69
|
return httpx.AsyncClient(
|
|
70
70
|
base_url=base_url,
|
|
@@ -157,36 +157,7 @@ class OpencodeUpstreamClient:
|
|
|
157
157
|
)
|
|
158
158
|
return self._require_boolean_response(endpoint=endpoint, payload=data)
|
|
159
159
|
|
|
160
|
-
def
|
|
161
|
-
expired = [
|
|
162
|
-
request_id
|
|
163
|
-
for request_id, binding in self._interrupt_requests.items()
|
|
164
|
-
if binding.expires_at <= now
|
|
165
|
-
]
|
|
166
|
-
for request_id in expired:
|
|
167
|
-
self._interrupt_requests.pop(request_id, None)
|
|
168
|
-
self._remember_interrupt_request_tombstone(request_id, now=now)
|
|
169
|
-
|
|
170
|
-
def _prune_interrupt_request_tombstones(self, *, now: float) -> None:
|
|
171
|
-
expired = [
|
|
172
|
-
request_id
|
|
173
|
-
for request_id, tombstone in self._interrupt_request_tombstones.items()
|
|
174
|
-
if tombstone.expires_at <= now
|
|
175
|
-
]
|
|
176
|
-
for request_id in expired:
|
|
177
|
-
self._interrupt_request_tombstones.pop(request_id, None)
|
|
178
|
-
|
|
179
|
-
def _remember_interrupt_request_tombstone(self, request_id: str, *, now: float) -> None:
|
|
180
|
-
ttl = self._interrupt_request_tombstone_ttl_seconds
|
|
181
|
-
if ttl <= 0:
|
|
182
|
-
self._interrupt_request_tombstones.pop(request_id, None)
|
|
183
|
-
return
|
|
184
|
-
self._interrupt_request_tombstones[request_id] = InterruptRequestTombstone(
|
|
185
|
-
request_id=request_id,
|
|
186
|
-
expires_at=now + ttl,
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
def remember_interrupt_request(
|
|
160
|
+
async def remember_interrupt_request(
|
|
190
161
|
self,
|
|
191
162
|
*,
|
|
192
163
|
request_id: str,
|
|
@@ -202,12 +173,8 @@ class OpencodeUpstreamClient:
|
|
|
202
173
|
kind = interrupt_type.strip()
|
|
203
174
|
if not request or not session or kind not in {"permission", "question"}:
|
|
204
175
|
return
|
|
205
|
-
|
|
206
|
-
self.
|
|
207
|
-
self._prune_interrupt_request_tombstones(now=now)
|
|
208
|
-
ttl = self._interrupt_request_ttl_seconds if ttl_seconds is None else ttl_seconds
|
|
209
|
-
expires_at = now + max(0.0, float(ttl))
|
|
210
|
-
self._interrupt_requests[request] = InterruptRequestBinding(
|
|
176
|
+
self._sync_interrupt_clock()
|
|
177
|
+
await self._interrupt_request_repository.remember(
|
|
211
178
|
request_id=request,
|
|
212
179
|
session_id=session,
|
|
213
180
|
interrupt_type=kind,
|
|
@@ -216,44 +183,30 @@ class OpencodeUpstreamClient:
|
|
|
216
183
|
context_id=(
|
|
217
184
|
context_id.strip() if isinstance(context_id, str) and context_id.strip() else None
|
|
218
185
|
),
|
|
219
|
-
|
|
186
|
+
ttl_seconds=ttl_seconds,
|
|
220
187
|
)
|
|
221
|
-
self._interrupt_request_tombstones.pop(request, None)
|
|
222
188
|
|
|
223
|
-
def resolve_interrupt_request(
|
|
189
|
+
async def resolve_interrupt_request(
|
|
224
190
|
self,
|
|
225
191
|
request_id: str,
|
|
226
192
|
) -> tuple[str, InterruptRequestBinding | None]:
|
|
227
193
|
request = request_id.strip()
|
|
228
194
|
if not request:
|
|
229
195
|
return "missing", None
|
|
230
|
-
|
|
231
|
-
self.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return "expired", None
|
|
236
|
-
return "missing", None
|
|
237
|
-
if binding.expires_at <= now:
|
|
238
|
-
self._interrupt_requests.pop(request, None)
|
|
239
|
-
self._prune_interrupt_requests(now=now)
|
|
240
|
-
self._remember_interrupt_request_tombstone(request, now=now)
|
|
241
|
-
return "expired", None
|
|
242
|
-
self._prune_interrupt_requests(now=now)
|
|
243
|
-
return "active", binding
|
|
244
|
-
|
|
245
|
-
def resolve_interrupt_session(self, request_id: str) -> str | None:
|
|
246
|
-
status, binding = self.resolve_interrupt_request(request_id)
|
|
196
|
+
self._sync_interrupt_clock()
|
|
197
|
+
return await self._interrupt_request_repository.resolve(request_id=request)
|
|
198
|
+
|
|
199
|
+
async def resolve_interrupt_session(self, request_id: str) -> str | None:
|
|
200
|
+
status, binding = await self.resolve_interrupt_request(request_id)
|
|
247
201
|
if status != "active" or binding is None:
|
|
248
202
|
return None
|
|
249
203
|
return binding.session_id
|
|
250
204
|
|
|
251
|
-
def discard_interrupt_request(self, request_id: str) -> None:
|
|
205
|
+
async def discard_interrupt_request(self, request_id: str) -> None:
|
|
252
206
|
request = request_id.strip()
|
|
253
207
|
if not request:
|
|
254
208
|
return
|
|
255
|
-
self.
|
|
256
|
-
self._interrupt_request_tombstones.pop(request, None)
|
|
209
|
+
await self._interrupt_request_repository.discard(request_id=request)
|
|
257
210
|
|
|
258
211
|
@property
|
|
259
212
|
def stream_timeout(self) -> float | None:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class InterruptRequestBinding:
|
|
8
|
+
request_id: str
|
|
9
|
+
session_id: str
|
|
10
|
+
interrupt_type: str
|
|
11
|
+
identity: str | None
|
|
12
|
+
task_id: str | None
|
|
13
|
+
context_id: str | None
|
|
14
|
+
expires_at: float
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class InterruptRequestTombstone:
|
|
19
|
+
request_id: str
|
|
20
|
+
expires_at: float
|