opencode-a2a-server 0.2.2__tar.gz → 0.2.3__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 (63) hide show
  1. {opencode_a2a_server-0.2.2/src/opencode_a2a_server.egg-info → opencode_a2a_server-0.2.3}/PKG-INFO +53 -4
  2. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/README.md +52 -3
  3. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/docs/guide.md +23 -3
  4. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/app.py +12 -2
  5. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/config.py +17 -0
  6. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/opencode_client.py +19 -3
  7. opencode_a2a_server-0.2.3/src/opencode_a2a_server/opencode_upstream.py +122 -0
  8. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3/src/opencode_a2a_server.egg-info}/PKG-INFO +53 -4
  9. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/SOURCES.txt +2 -0
  10. opencode_a2a_server-0.2.3/tests/test_opencode_upstream.py +162 -0
  11. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_settings.py +10 -0
  12. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.github/workflows/ci.yml +0 -0
  13. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.github/workflows/dependency-health.yml +0 -0
  14. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.github/workflows/publish.yml +0 -0
  15. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.gitignore +0 -0
  16. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.pre-commit-config.yaml +0 -0
  17. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.secrets.baseline +0 -0
  18. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/AGENTS.md +0 -0
  19. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/CONTRIBUTING.md +0 -0
  20. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/LICENSE +0 -0
  21. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/SECURITY.md +0 -0
  22. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/pyproject.toml +0 -0
  23. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/README.md +0 -0
  24. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/dependency_health.sh +0 -0
  25. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/doctor.sh +0 -0
  26. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/health_common.sh +0 -0
  27. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/lint.sh +0 -0
  28. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/smoke_test_built_cli.sh +0 -0
  29. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/setup.cfg +0 -0
  30. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/__init__.py +0 -0
  31. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/agent.py +0 -0
  32. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/cli.py +0 -0
  33. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/extension_contracts.py +0 -0
  34. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/jsonrpc_ext.py +0 -0
  35. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/jsonrpc_models.py +0 -0
  36. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/parts_mapper.py +0 -0
  37. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/text_parts.py +0 -0
  38. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/dependency_links.txt +0 -0
  39. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/entry_points.txt +0 -0
  40. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/requires.txt +0 -0
  41. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/top_level.txt +0 -0
  42. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/__init__.py +0 -0
  43. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/helpers.py +0 -0
  44. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_agent_card.py +0 -0
  45. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_agent_errors.py +0 -0
  46. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_call_context_builder.py +0 -0
  47. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_cancel_contract.py +0 -0
  48. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_cancellation.py +0 -0
  49. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_cli.py +0 -0
  50. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_directory_validation.py +0 -0
  51. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_extension_contract_consistency.py +0 -0
  52. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_jsonrpc_models.py +0 -0
  53. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_jsonrpc_unsupported_method.py +0 -0
  54. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_metrics.py +0 -0
  55. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_multipart_input.py +0 -0
  56. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_agent_session_binding.py +0 -0
  57. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_client_params.py +0 -0
  58. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_session_extension.py +0 -0
  59. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_script_health_contract.py +0 -0
  60. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_session_ownership.py +0 -0
  61. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_streaming_output_contract.py +0 -0
  62. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_transport_contract.py +0 -0
  63. {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-a2a-server
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A2A wrapper service for opencode
5
5
  Author: liujuanjuan1984@Intelligent-Internet
6
6
  License-Expression: Apache-2.0
@@ -186,7 +186,10 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
186
186
  opencode serve
187
187
 
188
188
  A2A_BEARER_TOKEN=prod-token \
189
+ A2A_HOST=127.0.0.1 \
190
+ A2A_PORT=8000 \
189
191
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
192
+ OPENCODE_MANAGED_SERVER=true \
190
193
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
191
194
  opencode-a2a-server serve
192
195
  ```
@@ -196,12 +199,25 @@ exposes to OpenCode.
196
199
 
197
200
  Default address: `http://127.0.0.1:8000`
198
201
 
202
+ OpenCode upstream modes:
203
+
204
+ - Managed upstream: set `OPENCODE_MANAGED_SERVER=true` and
205
+ `opencode-a2a-server` will start a local `opencode serve`, capture its actual
206
+ listening URL, and stop it on shutdown.
207
+ - External upstream: you start and manage `opencode serve` yourself, then point
208
+ `OPENCODE_BASE_URL` at that HTTP endpoint.
209
+
199
210
  Common runtime variables:
200
211
 
201
212
  | Variable | Required | Default | Purpose |
202
213
  | --- | --- | --- | --- |
203
214
  | `A2A_BEARER_TOKEN` | Yes | None | Bearer token required for authenticated runtime requests. |
204
- | `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint. |
215
+ | `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint for externally managed upstream mode. |
216
+ | `OPENCODE_MANAGED_SERVER` | No | `false` | Start and manage a local `opencode serve` child process. |
217
+ | `OPENCODE_MANAGED_SERVER_HOST` | No | `127.0.0.1` | Bind host used when managed upstream mode is enabled. |
218
+ | `OPENCODE_MANAGED_SERVER_PORT` | No | auto-pick | Bind port used when managed upstream mode is enabled. |
219
+ | `OPENCODE_COMMAND` | No | `opencode` | OpenCode CLI executable used for managed upstream mode. |
220
+ | `OPENCODE_STARTUP_TIMEOUT` | No | `20` | Seconds to wait for managed upstream startup. |
205
221
  | `OPENCODE_WORKSPACE_ROOT` | No | None | Default workspace root exposed to OpenCode. |
206
222
  | `OPENCODE_PROVIDER_ID` | No | None | Default provider for the upstream runtime. |
207
223
  | `OPENCODE_MODEL_ID` | No | None | Default model for the upstream runtime. Set together with `OPENCODE_PROVIDER_ID`. |
@@ -220,6 +236,9 @@ Common runtime variables:
220
236
  If you omit `OPENCODE_PROVIDER_ID` / `OPENCODE_MODEL_ID`, `opencode serve`
221
237
  uses your local OpenCode defaults (for example `~/.config/opencode/opencode.json`).
222
238
 
239
+ When `OPENCODE_MANAGED_SERVER=true`, `OPENCODE_BASE_URL` is ignored and the
240
+ runtime binds itself to the managed child process instead.
241
+
223
242
  For provider-specific auth, model IDs, and config details, use the OpenCode
224
243
  official docs and CLI:
225
244
 
@@ -240,7 +259,7 @@ Use any supervisor you prefer for long-running operation:
240
259
  The project no longer ships built-in host bootstrap or process-manager
241
260
  wrappers. The official product surface is the runtime entrypoint itself.
242
261
 
243
- Minimal `systemd` example:
262
+ Minimal self-managed `systemd` example:
244
263
 
245
264
  1. Create an env file such as `/etc/opencode-a2a/alpha.env`:
246
265
 
@@ -249,7 +268,7 @@ A2A_BEARER_TOKEN=replace-me
249
268
  A2A_HOST=127.0.0.1
250
269
  A2A_PORT=8000
251
270
  A2A_PUBLIC_URL=https://a2a.example.com
252
- OPENCODE_BASE_URL=http://127.0.0.1:4096
271
+ OPENCODE_MANAGED_SERVER=true
253
272
  OPENCODE_WORKSPACE_ROOT=/srv/my-workspace
254
273
  ```
255
274
 
@@ -275,6 +294,33 @@ WantedBy=multi-user.target
275
294
 
276
295
  Replace `ExecStart` with the absolute path returned by `command -v opencode-a2a-server`.
277
296
 
297
+ Minimal managed-upstream foreground example:
298
+
299
+ ```bash
300
+ A2A_BEARER_TOKEN=dev-token \
301
+ A2A_HOST=127.0.0.1 \
302
+ A2A_PORT=8000 \
303
+ A2A_PUBLIC_URL=http://127.0.0.1:8000 \
304
+ OPENCODE_MANAGED_SERVER=true \
305
+ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
306
+ opencode-a2a-server serve
307
+ ```
308
+
309
+ Advanced: externally managed upstream
310
+
311
+ Use this mode when you intentionally want `opencode serve` and
312
+ `opencode-a2a-server` to be supervised independently.
313
+
314
+ ```bash
315
+ OPENCODE_BASE_URL=http://127.0.0.1:4096 \
316
+ A2A_BEARER_TOKEN=dev-token \
317
+ A2A_HOST=127.0.0.1 \
318
+ A2A_PORT=8000 \
319
+ A2A_PUBLIC_URL=http://127.0.0.1:8000 \
320
+ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
321
+ opencode-a2a-server serve
322
+ ```
323
+
278
324
  Migration notes:
279
325
 
280
326
  - `OPENCODE_DIRECTORY` has been removed. Use `OPENCODE_WORKSPACE_ROOT`.
@@ -297,6 +343,9 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
297
343
  opencode serve
298
344
 
299
345
  A2A_BEARER_TOKEN=dev-token \
346
+ OPENCODE_BASE_URL=http://127.0.0.1:4096 \
347
+ A2A_HOST=127.0.0.1 \
348
+ A2A_PORT=8000 \
300
349
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
301
350
  uv run opencode-a2a-server serve
302
351
  ```
@@ -148,7 +148,10 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
148
148
  opencode serve
149
149
 
150
150
  A2A_BEARER_TOKEN=prod-token \
151
+ A2A_HOST=127.0.0.1 \
152
+ A2A_PORT=8000 \
151
153
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
154
+ OPENCODE_MANAGED_SERVER=true \
152
155
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
153
156
  opencode-a2a-server serve
154
157
  ```
@@ -158,12 +161,25 @@ exposes to OpenCode.
158
161
 
159
162
  Default address: `http://127.0.0.1:8000`
160
163
 
164
+ OpenCode upstream modes:
165
+
166
+ - Managed upstream: set `OPENCODE_MANAGED_SERVER=true` and
167
+ `opencode-a2a-server` will start a local `opencode serve`, capture its actual
168
+ listening URL, and stop it on shutdown.
169
+ - External upstream: you start and manage `opencode serve` yourself, then point
170
+ `OPENCODE_BASE_URL` at that HTTP endpoint.
171
+
161
172
  Common runtime variables:
162
173
 
163
174
  | Variable | Required | Default | Purpose |
164
175
  | --- | --- | --- | --- |
165
176
  | `A2A_BEARER_TOKEN` | Yes | None | Bearer token required for authenticated runtime requests. |
166
- | `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint. |
177
+ | `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint for externally managed upstream mode. |
178
+ | `OPENCODE_MANAGED_SERVER` | No | `false` | Start and manage a local `opencode serve` child process. |
179
+ | `OPENCODE_MANAGED_SERVER_HOST` | No | `127.0.0.1` | Bind host used when managed upstream mode is enabled. |
180
+ | `OPENCODE_MANAGED_SERVER_PORT` | No | auto-pick | Bind port used when managed upstream mode is enabled. |
181
+ | `OPENCODE_COMMAND` | No | `opencode` | OpenCode CLI executable used for managed upstream mode. |
182
+ | `OPENCODE_STARTUP_TIMEOUT` | No | `20` | Seconds to wait for managed upstream startup. |
167
183
  | `OPENCODE_WORKSPACE_ROOT` | No | None | Default workspace root exposed to OpenCode. |
168
184
  | `OPENCODE_PROVIDER_ID` | No | None | Default provider for the upstream runtime. |
169
185
  | `OPENCODE_MODEL_ID` | No | None | Default model for the upstream runtime. Set together with `OPENCODE_PROVIDER_ID`. |
@@ -182,6 +198,9 @@ Common runtime variables:
182
198
  If you omit `OPENCODE_PROVIDER_ID` / `OPENCODE_MODEL_ID`, `opencode serve`
183
199
  uses your local OpenCode defaults (for example `~/.config/opencode/opencode.json`).
184
200
 
201
+ When `OPENCODE_MANAGED_SERVER=true`, `OPENCODE_BASE_URL` is ignored and the
202
+ runtime binds itself to the managed child process instead.
203
+
185
204
  For provider-specific auth, model IDs, and config details, use the OpenCode
186
205
  official docs and CLI:
187
206
 
@@ -202,7 +221,7 @@ Use any supervisor you prefer for long-running operation:
202
221
  The project no longer ships built-in host bootstrap or process-manager
203
222
  wrappers. The official product surface is the runtime entrypoint itself.
204
223
 
205
- Minimal `systemd` example:
224
+ Minimal self-managed `systemd` example:
206
225
 
207
226
  1. Create an env file such as `/etc/opencode-a2a/alpha.env`:
208
227
 
@@ -211,7 +230,7 @@ A2A_BEARER_TOKEN=replace-me
211
230
  A2A_HOST=127.0.0.1
212
231
  A2A_PORT=8000
213
232
  A2A_PUBLIC_URL=https://a2a.example.com
214
- OPENCODE_BASE_URL=http://127.0.0.1:4096
233
+ OPENCODE_MANAGED_SERVER=true
215
234
  OPENCODE_WORKSPACE_ROOT=/srv/my-workspace
216
235
  ```
217
236
 
@@ -237,6 +256,33 @@ WantedBy=multi-user.target
237
256
 
238
257
  Replace `ExecStart` with the absolute path returned by `command -v opencode-a2a-server`.
239
258
 
259
+ Minimal managed-upstream foreground example:
260
+
261
+ ```bash
262
+ A2A_BEARER_TOKEN=dev-token \
263
+ A2A_HOST=127.0.0.1 \
264
+ A2A_PORT=8000 \
265
+ A2A_PUBLIC_URL=http://127.0.0.1:8000 \
266
+ OPENCODE_MANAGED_SERVER=true \
267
+ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
268
+ opencode-a2a-server serve
269
+ ```
270
+
271
+ Advanced: externally managed upstream
272
+
273
+ Use this mode when you intentionally want `opencode serve` and
274
+ `opencode-a2a-server` to be supervised independently.
275
+
276
+ ```bash
277
+ OPENCODE_BASE_URL=http://127.0.0.1:4096 \
278
+ A2A_BEARER_TOKEN=dev-token \
279
+ A2A_HOST=127.0.0.1 \
280
+ A2A_PORT=8000 \
281
+ A2A_PUBLIC_URL=http://127.0.0.1:8000 \
282
+ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
283
+ opencode-a2a-server serve
284
+ ```
285
+
240
286
  Migration notes:
241
287
 
242
288
  - `OPENCODE_DIRECTORY` has been removed. Use `OPENCODE_WORKSPACE_ROOT`.
@@ -259,6 +305,9 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
259
305
  opencode serve
260
306
 
261
307
  A2A_BEARER_TOKEN=dev-token \
308
+ OPENCODE_BASE_URL=http://127.0.0.1:4096 \
309
+ A2A_HOST=127.0.0.1 \
310
+ A2A_PORT=8000 \
262
311
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
263
312
  uv run opencode-a2a-server serve
264
313
  ```
@@ -25,8 +25,16 @@ own process manager, container runtime, or host orchestration.
25
25
  Key variables to understand protocol behavior:
26
26
 
27
27
  - `A2A_BEARER_TOKEN`: required for all authenticated runtime requests.
28
- - `OPENCODE_BASE_URL`: upstream OpenCode HTTP endpoint. Default:
29
- `http://127.0.0.1:4096`.
28
+ - `OPENCODE_BASE_URL`: upstream OpenCode HTTP endpoint for externally managed
29
+ upstream mode. Default: `http://127.0.0.1:4096`.
30
+ - `OPENCODE_MANAGED_SERVER`: when enabled, the service starts a local
31
+ `opencode serve`, captures its actual listening URL, and uses that as the
32
+ internal upstream endpoint.
33
+ - `OPENCODE_MANAGED_SERVER_HOST` / `OPENCODE_MANAGED_SERVER_PORT`: bind address
34
+ for managed upstream mode. If the port is omitted, the service picks a free
35
+ localhost port before starting the child process.
36
+ - `OPENCODE_COMMAND`: OpenCode CLI executable used for managed upstream mode.
37
+ - `OPENCODE_STARTUP_TIMEOUT`: startup timeout for managed upstream mode.
30
38
  - `OPENCODE_WORKSPACE_ROOT`: service-level default workspace root exposed to
31
39
  OpenCode when clients do not request a narrower directory override.
32
40
  - `OPENCODE_PROVIDER_ID` / `OPENCODE_MODEL_ID`: default upstream model
@@ -54,14 +62,26 @@ Key variables to understand protocol behavior:
54
62
  optional stream timeout override.
55
63
  - Runtime authentication is bearer-token only via `A2A_BEARER_TOKEN`.
56
64
 
57
- Minimal runtime example:
65
+ Minimal managed-upstream example:
58
66
 
59
67
  ```bash
60
68
  A2A_BEARER_TOKEN=dev-token \
61
69
  A2A_HOST=127.0.0.1 \
62
70
  A2A_PORT=8000 \
63
71
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
72
+ OPENCODE_MANAGED_SERVER=true \
73
+ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
74
+ opencode-a2a-server serve
75
+ ```
76
+
77
+ Advanced externally managed upstream example:
78
+
79
+ ```bash
64
80
  OPENCODE_BASE_URL=http://127.0.0.1:4096 \
81
+ A2A_BEARER_TOKEN=dev-token \
82
+ A2A_HOST=127.0.0.1 \
83
+ A2A_PORT=8000 \
84
+ A2A_PUBLIC_URL=http://127.0.0.1:8000 \
65
85
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
66
86
  opencode-a2a-server serve
67
87
  ```
@@ -69,6 +69,7 @@ from .jsonrpc_ext import (
69
69
  OpencodeSessionQueryJSONRPCApplication,
70
70
  )
71
71
  from .opencode_client import OpencodeClient
72
+ from .opencode_upstream import ManagedOpencodeServer, launch_managed_opencode_server
72
73
 
73
74
  logger = logging.getLogger(__name__)
74
75
 
@@ -993,8 +994,17 @@ def create_app(settings: Settings) -> FastAPI:
993
994
 
994
995
  @asynccontextmanager
995
996
  async def lifespan(_app: FastAPI):
996
- yield
997
- await client.close()
997
+ managed_upstream: ManagedOpencodeServer | None = None
998
+ try:
999
+ if settings.opencode_managed_server:
1000
+ managed_upstream = await launch_managed_opencode_server(settings)
1001
+ await client.rebind_base_url(managed_upstream.base_url)
1002
+ _app.state.managed_opencode_upstream = managed_upstream
1003
+ yield
1004
+ finally:
1005
+ if managed_upstream is not None:
1006
+ await managed_upstream.close()
1007
+ await client.close()
998
1008
 
999
1009
  agent_card = build_agent_card(settings)
1000
1010
  context_builder = IdentityAwareCallContextBuilder()
@@ -19,6 +19,23 @@ class Settings(BaseSettings):
19
19
 
20
20
  # OpenCode settings
21
21
  opencode_base_url: str = Field(default="http://127.0.0.1:4096", alias="OPENCODE_BASE_URL")
22
+ opencode_managed_server: bool = Field(default=False, alias="OPENCODE_MANAGED_SERVER")
23
+ opencode_managed_server_host: str = Field(
24
+ default="127.0.0.1",
25
+ alias="OPENCODE_MANAGED_SERVER_HOST",
26
+ )
27
+ opencode_managed_server_port: int | None = Field(
28
+ default=None,
29
+ ge=1,
30
+ le=65535,
31
+ alias="OPENCODE_MANAGED_SERVER_PORT",
32
+ )
33
+ opencode_command: str = Field(default="opencode", alias="OPENCODE_COMMAND")
34
+ opencode_startup_timeout: float = Field(
35
+ default=20.0,
36
+ gt=0.0,
37
+ alias="OPENCODE_STARTUP_TIMEOUT",
38
+ )
22
39
  opencode_workspace_root: str | None = Field(default=None, alias="OPENCODE_WORKSPACE_ROOT")
23
40
  opencode_provider_id: str | None = Field(default=None, alias="OPENCODE_PROVIDER_ID")
24
41
  opencode_model_id: str | None = Field(default=None, alias="OPENCODE_MODEL_ID")
@@ -54,15 +54,27 @@ class OpencodeClient:
54
54
  self._interrupt_request_ttl_seconds = 600.0
55
55
  self._interrupt_request_clock = time.monotonic
56
56
  self._interrupt_requests: dict[str, InterruptRequestBinding] = {}
57
- self._client = httpx.AsyncClient(
58
- base_url=self._base_url,
59
- timeout=settings.opencode_timeout,
57
+ self._client = self._build_http_client(self._base_url)
58
+
59
+ def _build_http_client(self, base_url: str) -> httpx.AsyncClient:
60
+ return httpx.AsyncClient(
61
+ base_url=base_url,
62
+ timeout=self._settings.opencode_timeout,
60
63
  headers={"Accept": "application/json"},
61
64
  )
62
65
 
63
66
  async def close(self) -> None:
64
67
  await self._client.aclose()
65
68
 
69
+ async def rebind_base_url(self, base_url: str) -> None:
70
+ normalized = base_url.rstrip("/")
71
+ if normalized == self._base_url:
72
+ return
73
+ previous_client = self._client
74
+ self._base_url = normalized
75
+ self._client = self._build_http_client(self._base_url)
76
+ await previous_client.aclose()
77
+
66
78
  @staticmethod
67
79
  def _response_body_preview(response: httpx.Response, *, limit: int = 200) -> str:
68
80
  body = response.text.strip()
@@ -177,6 +189,10 @@ class OpencodeClient:
177
189
  def settings(self) -> Settings:
178
190
  return self._settings
179
191
 
192
+ @property
193
+ def base_url(self) -> str:
194
+ return self._base_url
195
+
180
196
  def _query_params(self, directory: str | None = None) -> dict[str, str]:
181
197
  d = directory or self._directory
182
198
  if not d:
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import re
6
+ import shutil
7
+ import socket
8
+ from collections import deque
9
+ from dataclasses import dataclass
10
+
11
+ from .config import Settings
12
+
13
+ _LISTENING_PATTERN = re.compile(r"opencode server listening on (?P<url>https?://\S+)")
14
+ _OUTPUT_BUFFER_LIMIT = 40
15
+
16
+
17
+ def _pick_managed_server_port(host: str) -> int:
18
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
19
+ sock.bind((host, 0))
20
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
21
+ return int(sock.getsockname()[1])
22
+
23
+
24
+ def _resolve_opencode_command(command: str) -> str:
25
+ if os.path.sep in command:
26
+ if os.path.exists(command):
27
+ return command
28
+ raise RuntimeError(f"Managed upstream command not found: {command}")
29
+ resolved = shutil.which(command)
30
+ if resolved:
31
+ return resolved
32
+ raise RuntimeError(
33
+ f"Managed upstream command not found on PATH: {command}. "
34
+ "Install opencode or set OPENCODE_COMMAND."
35
+ )
36
+
37
+
38
+ @dataclass
39
+ class ManagedOpencodeServer:
40
+ process: asyncio.subprocess.Process
41
+ base_url: str
42
+ _output_task: asyncio.Task[None]
43
+
44
+ async def close(self) -> None:
45
+ if self.process.returncode is None:
46
+ self.process.terminate()
47
+ try:
48
+ await asyncio.wait_for(self.process.wait(), timeout=5.0)
49
+ except TimeoutError:
50
+ self.process.kill()
51
+ await self.process.wait()
52
+ await self._output_task
53
+
54
+
55
+ async def _consume_process_output(
56
+ stream: asyncio.StreamReader | None,
57
+ *,
58
+ ready_future: asyncio.Future[str],
59
+ output_buffer: deque[str],
60
+ ) -> None:
61
+ if stream is None:
62
+ return
63
+ while True:
64
+ line = await stream.readline()
65
+ if not line:
66
+ return
67
+ text = line.decode("utf-8", errors="replace").rstrip()
68
+ if not text:
69
+ continue
70
+ output_buffer.append(text)
71
+ match = _LISTENING_PATTERN.search(text)
72
+ if match and not ready_future.done():
73
+ ready_future.set_result(match.group("url"))
74
+
75
+
76
+ async def launch_managed_opencode_server(settings: Settings) -> ManagedOpencodeServer:
77
+ host = settings.opencode_managed_server_host
78
+ port = settings.opencode_managed_server_port or _pick_managed_server_port(host)
79
+ command = _resolve_opencode_command(settings.opencode_command)
80
+ cmd = [
81
+ command,
82
+ "serve",
83
+ "--hostname",
84
+ host,
85
+ "--port",
86
+ str(port),
87
+ ]
88
+ process = await asyncio.create_subprocess_exec(
89
+ *cmd,
90
+ stdout=asyncio.subprocess.PIPE,
91
+ stderr=asyncio.subprocess.STDOUT,
92
+ )
93
+ ready_future: asyncio.Future[str] = asyncio.get_running_loop().create_future()
94
+ output_buffer: deque[str] = deque(maxlen=_OUTPUT_BUFFER_LIMIT)
95
+ output_task = asyncio.create_task(
96
+ _consume_process_output(
97
+ process.stdout,
98
+ ready_future=ready_future,
99
+ output_buffer=output_buffer,
100
+ )
101
+ )
102
+ try:
103
+ base_url = await asyncio.wait_for(ready_future, timeout=settings.opencode_startup_timeout)
104
+ except Exception as exc:
105
+ if process.returncode is None:
106
+ process.terminate()
107
+ try:
108
+ await asyncio.wait_for(process.wait(), timeout=5.0)
109
+ except TimeoutError:
110
+ process.kill()
111
+ await process.wait()
112
+ await output_task
113
+ buffered_output = "\n".join(output_buffer).strip() or "<no output>"
114
+ raise RuntimeError(
115
+ "Managed OpenCode upstream failed to become ready. "
116
+ f"command={' '.join(cmd)} output={buffered_output}"
117
+ ) from exc
118
+ return ManagedOpencodeServer(
119
+ process=process,
120
+ base_url=base_url.rstrip("/"),
121
+ _output_task=output_task,
122
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-a2a-server
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A2A wrapper service for opencode
5
5
  Author: liujuanjuan1984@Intelligent-Internet
6
6
  License-Expression: Apache-2.0
@@ -186,7 +186,10 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
186
186
  opencode serve
187
187
 
188
188
  A2A_BEARER_TOKEN=prod-token \
189
+ A2A_HOST=127.0.0.1 \
190
+ A2A_PORT=8000 \
189
191
  A2A_PUBLIC_URL=http://127.0.0.1:8000 \
192
+ OPENCODE_MANAGED_SERVER=true \
190
193
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
191
194
  opencode-a2a-server serve
192
195
  ```
@@ -196,12 +199,25 @@ exposes to OpenCode.
196
199
 
197
200
  Default address: `http://127.0.0.1:8000`
198
201
 
202
+ OpenCode upstream modes:
203
+
204
+ - Managed upstream: set `OPENCODE_MANAGED_SERVER=true` and
205
+ `opencode-a2a-server` will start a local `opencode serve`, capture its actual
206
+ listening URL, and stop it on shutdown.
207
+ - External upstream: you start and manage `opencode serve` yourself, then point
208
+ `OPENCODE_BASE_URL` at that HTTP endpoint.
209
+
199
210
  Common runtime variables:
200
211
 
201
212
  | Variable | Required | Default | Purpose |
202
213
  | --- | --- | --- | --- |
203
214
  | `A2A_BEARER_TOKEN` | Yes | None | Bearer token required for authenticated runtime requests. |
204
- | `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint. |
215
+ | `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint for externally managed upstream mode. |
216
+ | `OPENCODE_MANAGED_SERVER` | No | `false` | Start and manage a local `opencode serve` child process. |
217
+ | `OPENCODE_MANAGED_SERVER_HOST` | No | `127.0.0.1` | Bind host used when managed upstream mode is enabled. |
218
+ | `OPENCODE_MANAGED_SERVER_PORT` | No | auto-pick | Bind port used when managed upstream mode is enabled. |
219
+ | `OPENCODE_COMMAND` | No | `opencode` | OpenCode CLI executable used for managed upstream mode. |
220
+ | `OPENCODE_STARTUP_TIMEOUT` | No | `20` | Seconds to wait for managed upstream startup. |
205
221
  | `OPENCODE_WORKSPACE_ROOT` | No | None | Default workspace root exposed to OpenCode. |
206
222
  | `OPENCODE_PROVIDER_ID` | No | None | Default provider for the upstream runtime. |
207
223
  | `OPENCODE_MODEL_ID` | No | None | Default model for the upstream runtime. Set together with `OPENCODE_PROVIDER_ID`. |
@@ -220,6 +236,9 @@ Common runtime variables:
220
236
  If you omit `OPENCODE_PROVIDER_ID` / `OPENCODE_MODEL_ID`, `opencode serve`
221
237
  uses your local OpenCode defaults (for example `~/.config/opencode/opencode.json`).
222
238
 
239
+ When `OPENCODE_MANAGED_SERVER=true`, `OPENCODE_BASE_URL` is ignored and the
240
+ runtime binds itself to the managed child process instead.
241
+
223
242
  For provider-specific auth, model IDs, and config details, use the OpenCode
224
243
  official docs and CLI:
225
244
 
@@ -240,7 +259,7 @@ Use any supervisor you prefer for long-running operation:
240
259
  The project no longer ships built-in host bootstrap or process-manager
241
260
  wrappers. The official product surface is the runtime entrypoint itself.
242
261
 
243
- Minimal `systemd` example:
262
+ Minimal self-managed `systemd` example:
244
263
 
245
264
  1. Create an env file such as `/etc/opencode-a2a/alpha.env`:
246
265
 
@@ -249,7 +268,7 @@ A2A_BEARER_TOKEN=replace-me
249
268
  A2A_HOST=127.0.0.1
250
269
  A2A_PORT=8000
251
270
  A2A_PUBLIC_URL=https://a2a.example.com
252
- OPENCODE_BASE_URL=http://127.0.0.1:4096
271
+ OPENCODE_MANAGED_SERVER=true
253
272
  OPENCODE_WORKSPACE_ROOT=/srv/my-workspace
254
273
  ```
255
274
 
@@ -275,6 +294,33 @@ WantedBy=multi-user.target
275
294
 
276
295
  Replace `ExecStart` with the absolute path returned by `command -v opencode-a2a-server`.
277
296
 
297
+ Minimal managed-upstream foreground example:
298
+
299
+ ```bash
300
+ A2A_BEARER_TOKEN=dev-token \
301
+ A2A_HOST=127.0.0.1 \
302
+ A2A_PORT=8000 \
303
+ A2A_PUBLIC_URL=http://127.0.0.1:8000 \
304
+ OPENCODE_MANAGED_SERVER=true \
305
+ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
306
+ opencode-a2a-server serve
307
+ ```
308
+
309
+ Advanced: externally managed upstream
310
+
311
+ Use this mode when you intentionally want `opencode serve` and
312
+ `opencode-a2a-server` to be supervised independently.
313
+
314
+ ```bash
315
+ OPENCODE_BASE_URL=http://127.0.0.1:4096 \
316
+ A2A_BEARER_TOKEN=dev-token \
317
+ A2A_HOST=127.0.0.1 \
318
+ A2A_PORT=8000 \
319
+ A2A_PUBLIC_URL=http://127.0.0.1:8000 \
320
+ OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
321
+ opencode-a2a-server serve
322
+ ```
323
+
278
324
  Migration notes:
279
325
 
280
326
  - `OPENCODE_DIRECTORY` has been removed. Use `OPENCODE_WORKSPACE_ROOT`.
@@ -297,6 +343,9 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
297
343
  opencode serve
298
344
 
299
345
  A2A_BEARER_TOKEN=dev-token \
346
+ OPENCODE_BASE_URL=http://127.0.0.1:4096 \
347
+ A2A_HOST=127.0.0.1 \
348
+ A2A_PORT=8000 \
300
349
  OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
301
350
  uv run opencode-a2a-server serve
302
351
  ```
@@ -27,6 +27,7 @@ src/opencode_a2a_server/extension_contracts.py
27
27
  src/opencode_a2a_server/jsonrpc_ext.py
28
28
  src/opencode_a2a_server/jsonrpc_models.py
29
29
  src/opencode_a2a_server/opencode_client.py
30
+ src/opencode_a2a_server/opencode_upstream.py
30
31
  src/opencode_a2a_server/parts_mapper.py
31
32
  src/opencode_a2a_server/text_parts.py
32
33
  src/opencode_a2a_server.egg-info/PKG-INFO
@@ -52,6 +53,7 @@ tests/test_multipart_input.py
52
53
  tests/test_opencode_agent_session_binding.py
53
54
  tests/test_opencode_client_params.py
54
55
  tests/test_opencode_session_extension.py
56
+ tests/test_opencode_upstream.py
55
57
  tests/test_script_health_contract.py
56
58
  tests/test_session_ownership.py
57
59
  tests/test_settings.py
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+ from fastapi.testclient import TestClient
7
+
8
+ from opencode_a2a_server.app import create_app
9
+ from opencode_a2a_server.opencode_upstream import (
10
+ ManagedOpencodeServer,
11
+ _resolve_opencode_command,
12
+ launch_managed_opencode_server,
13
+ )
14
+ from tests.helpers import make_settings
15
+
16
+
17
+ class _DummyProcess:
18
+ def __init__(self, lines: list[str], *, returncode: int | None = None) -> None:
19
+ self.stdout = asyncio.StreamReader()
20
+ for line in lines:
21
+ self.stdout.feed_data(line.encode("utf-8"))
22
+ self.stdout.feed_eof()
23
+ self.returncode = returncode
24
+ self.terminated = False
25
+ self.killed = False
26
+
27
+ def terminate(self) -> None:
28
+ self.terminated = True
29
+ self.returncode = 0
30
+
31
+ def kill(self) -> None:
32
+ self.killed = True
33
+ self.returncode = -9
34
+
35
+ async def wait(self) -> int:
36
+ if self.returncode is None:
37
+ self.returncode = 0
38
+ return self.returncode
39
+
40
+
41
+ def test_resolve_opencode_command_errors_when_not_found(monkeypatch) -> None:
42
+ monkeypatch.setattr("opencode_a2a_server.opencode_upstream.shutil.which", lambda _: None)
43
+
44
+ with pytest.raises(RuntimeError) as excinfo:
45
+ _resolve_opencode_command("opencode")
46
+
47
+ assert "OPENCODE_COMMAND" in str(excinfo.value)
48
+
49
+
50
+ @pytest.mark.asyncio
51
+ async def test_launch_managed_opencode_server_uses_listening_url(monkeypatch) -> None:
52
+ dummy = _DummyProcess(
53
+ [
54
+ "Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.\n",
55
+ "opencode server listening on http://127.0.0.1:40419\n",
56
+ ]
57
+ )
58
+ captured: list[str] = []
59
+
60
+ async def _fake_create_subprocess_exec(*cmd, **kwargs): # noqa: ANN001
61
+ del kwargs
62
+ captured.extend(cmd)
63
+ return dummy
64
+
65
+ monkeypatch.setattr(
66
+ "opencode_a2a_server.opencode_upstream._resolve_opencode_command",
67
+ lambda command: command,
68
+ )
69
+ monkeypatch.setattr(
70
+ "opencode_a2a_server.opencode_upstream._pick_managed_server_port",
71
+ lambda host: 40419,
72
+ )
73
+ monkeypatch.setattr(
74
+ "opencode_a2a_server.opencode_upstream.asyncio.create_subprocess_exec",
75
+ _fake_create_subprocess_exec,
76
+ )
77
+ settings = make_settings(
78
+ opencode_managed_server=True,
79
+ opencode_command="opencode",
80
+ opencode_startup_timeout=1.0,
81
+ )
82
+
83
+ handle = await launch_managed_opencode_server(settings)
84
+
85
+ assert captured == ["opencode", "serve", "--hostname", "127.0.0.1", "--port", "40419"]
86
+ assert handle.base_url == "http://127.0.0.1:40419"
87
+ await handle.close()
88
+ assert dummy.terminated is True
89
+
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_launch_managed_opencode_server_surfaces_startup_failure(monkeypatch) -> None:
93
+ dummy = _DummyProcess(["fatal: boom\n"], returncode=1)
94
+
95
+ async def _fake_create_subprocess_exec(*_cmd, **_kwargs): # noqa: ANN001
96
+ return dummy
97
+
98
+ monkeypatch.setattr(
99
+ "opencode_a2a_server.opencode_upstream._resolve_opencode_command",
100
+ lambda command: command,
101
+ )
102
+ monkeypatch.setattr(
103
+ "opencode_a2a_server.opencode_upstream._pick_managed_server_port",
104
+ lambda host: 40420,
105
+ )
106
+ monkeypatch.setattr(
107
+ "opencode_a2a_server.opencode_upstream.asyncio.create_subprocess_exec",
108
+ _fake_create_subprocess_exec,
109
+ )
110
+ settings = make_settings(
111
+ opencode_managed_server=True,
112
+ opencode_command="opencode",
113
+ opencode_startup_timeout=0.01,
114
+ )
115
+
116
+ with pytest.raises(RuntimeError) as excinfo:
117
+ await launch_managed_opencode_server(settings)
118
+
119
+ assert "failed to become ready" in str(excinfo.value)
120
+ assert "fatal: boom" in str(excinfo.value)
121
+
122
+
123
+ def test_create_app_manages_upstream_lifecycle(monkeypatch) -> None:
124
+ import opencode_a2a_server.app as app_module
125
+
126
+ calls: list[str] = []
127
+
128
+ class _DummyHandle(ManagedOpencodeServer):
129
+ def __init__(self) -> None:
130
+ self.process = _DummyProcess([])
131
+ self.base_url = "http://127.0.0.1:40421"
132
+ self._output_task = asyncio.get_event_loop().create_task(asyncio.sleep(0))
133
+
134
+ async def close(self) -> None: # type: ignore[override]
135
+ calls.append("managed_close")
136
+
137
+ async def _fake_launch(settings): # noqa: ANN001
138
+ assert settings.opencode_managed_server is True
139
+ calls.append("launch")
140
+ return _DummyHandle()
141
+
142
+ async def _fake_rebind(self, base_url: str) -> None: # noqa: ANN001
143
+ calls.append(f"rebind:{base_url}")
144
+
145
+ async def _fake_close(self) -> None: # noqa: ANN001
146
+ calls.append("client_close")
147
+
148
+ monkeypatch.setattr(app_module, "launch_managed_opencode_server", _fake_launch)
149
+ monkeypatch.setattr(app_module.OpencodeClient, "rebind_base_url", _fake_rebind)
150
+ monkeypatch.setattr(app_module.OpencodeClient, "close", _fake_close)
151
+
152
+ app = create_app(
153
+ make_settings(
154
+ a2a_bearer_token="test-token",
155
+ opencode_managed_server=True,
156
+ )
157
+ )
158
+
159
+ with TestClient(app):
160
+ assert calls[:2] == ["launch", "rebind:http://127.0.0.1:40421"]
161
+
162
+ assert calls == ["launch", "rebind:http://127.0.0.1:40421", "managed_close", "client_close"]
@@ -22,6 +22,11 @@ def test_settings_valid():
22
22
  env = {
23
23
  "A2A_BEARER_TOKEN": "test-token",
24
24
  "OPENCODE_TIMEOUT": "300",
25
+ "OPENCODE_MANAGED_SERVER": "true",
26
+ "OPENCODE_MANAGED_SERVER_HOST": "127.0.0.1",
27
+ "OPENCODE_MANAGED_SERVER_PORT": "42111",
28
+ "OPENCODE_COMMAND": "/usr/local/bin/opencode",
29
+ "OPENCODE_STARTUP_TIMEOUT": "9.5",
25
30
  "OPENCODE_WORKSPACE_ROOT": "/srv/workspaces/alpha",
26
31
  "A2A_MAX_REQUEST_BODY_BYTES": "2048",
27
32
  "A2A_CANCEL_ABORT_TIMEOUT_SECONDS": "0.75",
@@ -31,6 +36,11 @@ def test_settings_valid():
31
36
  settings = Settings.from_env()
32
37
  assert settings.a2a_bearer_token == "test-token"
33
38
  assert settings.opencode_timeout == 300.0
39
+ assert settings.opencode_managed_server is True
40
+ assert settings.opencode_managed_server_host == "127.0.0.1"
41
+ assert settings.opencode_managed_server_port == 42111
42
+ assert settings.opencode_command == "/usr/local/bin/opencode"
43
+ assert settings.opencode_startup_timeout == 9.5
34
44
  assert settings.opencode_workspace_root == "/srv/workspaces/alpha"
35
45
  assert settings.a2a_max_request_body_bytes == 2048
36
46
  assert settings.a2a_cancel_abort_timeout_seconds == 0.75