py-opencode-wrapper 0.1.2__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.
- py_opencode_wrapper-0.1.2/PKG-INFO +161 -0
- py_opencode_wrapper-0.1.2/README.md +148 -0
- py_opencode_wrapper-0.1.2/opencode_wrapper/__init__.py +38 -0
- py_opencode_wrapper-0.1.2/opencode_wrapper/client.py +382 -0
- py_opencode_wrapper-0.1.2/opencode_wrapper/config.py +109 -0
- py_opencode_wrapper-0.1.2/opencode_wrapper/errors.py +39 -0
- py_opencode_wrapper-0.1.2/opencode_wrapper/events.py +192 -0
- py_opencode_wrapper-0.1.2/py_opencode_wrapper.egg-info/PKG-INFO +161 -0
- py_opencode_wrapper-0.1.2/py_opencode_wrapper.egg-info/SOURCES.txt +20 -0
- py_opencode_wrapper-0.1.2/py_opencode_wrapper.egg-info/dependency_links.txt +1 -0
- py_opencode_wrapper-0.1.2/py_opencode_wrapper.egg-info/requires.txt +4 -0
- py_opencode_wrapper-0.1.2/py_opencode_wrapper.egg-info/top_level.txt +1 -0
- py_opencode_wrapper-0.1.2/pyproject.toml +31 -0
- py_opencode_wrapper-0.1.2/setup.cfg +4 -0
- py_opencode_wrapper-0.1.2/tests/test_client_async.py +430 -0
- py_opencode_wrapper-0.1.2/tests/test_config_permission.py +76 -0
- py_opencode_wrapper-0.1.2/tests/test_event_parser.py +118 -0
- py_opencode_wrapper-0.1.2/tests/test_integration_external_directory.py +63 -0
- py_opencode_wrapper-0.1.2/tests/test_integration_multi_agent_weather.py +163 -0
- py_opencode_wrapper-0.1.2/tests/test_integration_opencode.py +93 -0
- py_opencode_wrapper-0.1.2/tests/test_integration_parallel.py +87 -0
- py_opencode_wrapper-0.1.2/tests/test_run_result_fuzzy_text.py +20 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-opencode-wrapper
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Async Python wrapper for OpenCode CLI (opencode run --format json)
|
|
5
|
+
Project-URL: Homepage, https://github.com/idailylife/oc_py_wrapper
|
|
6
|
+
Project-URL: Repository, https://github.com/idailylife/oc_py_wrapper
|
|
7
|
+
Project-URL: Issues, https://github.com/idailylife/oc_py_wrapper/issues
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# oc-py-harness
|
|
15
|
+
|
|
16
|
+
Python **async** wrapper around the [OpenCode](https://opencode.ai/docs/) CLI (`opencode run --format json`). Intended as a subprocess-based executor for **multi-agent workflow** orchestration.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python 3.10+
|
|
21
|
+
- `opencode` on `PATH` (or pass an absolute path to the binary)
|
|
22
|
+
|
|
23
|
+
## Install (local tree)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e ".[dev]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### One-shot run with aggregated result
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import asyncio
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from opencode_wrapper import AsyncOpenCodeClient, RunConfig
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
client = AsyncOpenCodeClient("opencode")
|
|
41
|
+
cfg = RunConfig(
|
|
42
|
+
model="anthropic/claude-sonnet-4-5",
|
|
43
|
+
agent="plan",
|
|
44
|
+
permission={"bash": "deny", "edit": "deny"},
|
|
45
|
+
mcp={
|
|
46
|
+
"demo": {
|
|
47
|
+
"type": "local",
|
|
48
|
+
"command": ["npx", "-y", "@modelcontextprotocol/server-everything"],
|
|
49
|
+
"enabled": True,
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
result = await client.async_run(
|
|
54
|
+
"Summarize the README in one sentence.",
|
|
55
|
+
Path("/path/to/repo"),
|
|
56
|
+
run_cfg=cfg,
|
|
57
|
+
timeout_s=600,
|
|
58
|
+
)
|
|
59
|
+
print(result.exit_code, result.final_text)
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Stream structured JSON events
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
async def stream_example():
|
|
68
|
+
client = AsyncOpenCodeClient()
|
|
69
|
+
cfg = RunConfig(permission={"*": "allow"})
|
|
70
|
+
async for event in client.async_stream("List top-level files.", workspace=".", run_cfg=cfg):
|
|
71
|
+
print(event)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Parallel agents (`asyncio.gather`)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
async def multi():
|
|
78
|
+
# startup_concurrency=1 serialises SQLite initialisation to avoid a known
|
|
79
|
+
# WAL-pragma race in opencode when many instances start simultaneously.
|
|
80
|
+
# startup_delay_s controls how long each slot is held before the next
|
|
81
|
+
# process is allowed to start (default 0.3 s).
|
|
82
|
+
client = AsyncOpenCodeClient(startup_concurrency=1, startup_delay_s=0.3)
|
|
83
|
+
ws = Path("/path/to/monorepo")
|
|
84
|
+
results = await asyncio.gather(*[
|
|
85
|
+
client.async_run(
|
|
86
|
+
f"Explain services/{svc}.",
|
|
87
|
+
ws / "services" / svc,
|
|
88
|
+
run_cfg=RunConfig(agent="explore"),
|
|
89
|
+
timeout_s=600,
|
|
90
|
+
# max_retries=2 (default): retry automatically if opencode crashes
|
|
91
|
+
# during SQLite startup before giving up.
|
|
92
|
+
)
|
|
93
|
+
for svc in ["api", "worker", "gateway"]
|
|
94
|
+
])
|
|
95
|
+
return results
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configuration injection
|
|
99
|
+
|
|
100
|
+
Per-call JSON is merged and passed as `OPENCODE_CONFIG_CONTENT` (see [OpenCode config](https://opencode.ai/docs/config/)). Use `RunConfig` fields:
|
|
101
|
+
|
|
102
|
+
| Field | Purpose |
|
|
103
|
+
|--------|---------|
|
|
104
|
+
| `permission` | `permission` map (`allow` / `ask` / `deny`, patterns) |
|
|
105
|
+
| `mcp` | MCP server definitions |
|
|
106
|
+
| `tools` | Enable/disable tools (including MCP globs) |
|
|
107
|
+
| `config_overrides` | Any extra top-level config keys to deep-merge |
|
|
108
|
+
|
|
109
|
+
Optional env tuning: `disable_autoupdate=True` sets `OPENCODE_DISABLE_AUTOUPDATE=1`.
|
|
110
|
+
|
|
111
|
+
## CLI arguments
|
|
112
|
+
|
|
113
|
+
`RunConfig` maps to flags such as `--agent`, `-m`, `-f`, `--attach`, `--title`, etc. Prompt text is appended as the final `opencode run` message argument.
|
|
114
|
+
|
|
115
|
+
## Tests
|
|
116
|
+
|
|
117
|
+
Unit tests (no real OpenCode / no API calls):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pytest -q -m "not integration"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Integration tests (real `opencode run`, needs working provider auth — **slow**, may incur API usage):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pytest -m integration -q tests/test_integration_opencode.py
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Multi-agent weather workflow** (10 parallel city lookups + 1 summary — **11 API calls**, not run by default):
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
OPENCODE_MULTI_AGENT_WEATHER=1 pytest -m integration -v tests/test_integration_multi_agent_weather.py
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Optional: `OPENCODE_WEATHER_SEQUENTIAL=1` runs the 10 city calls one-by-one (easier on rate limits).
|
|
136
|
+
Per-stage timeouts: `OPENCODE_WEATHER_PER_CITY_TIMEOUT_S`, `OPENCODE_WEATHER_SUMMARY_TIMEOUT_S` (default: same as `OPENCODE_INTEGRATION_TIMEOUT_S`).
|
|
137
|
+
|
|
138
|
+
| Env | Meaning |
|
|
139
|
+
|-----|--------|
|
|
140
|
+
| `OPENCODE_BINARY` | Absolute path to `opencode` if not on `PATH` |
|
|
141
|
+
| `OPENCODE_INTEGRATION=0` | Skip integration tests |
|
|
142
|
+
| `OPENCODE_INTEGRATION_TIMEOUT_S` | Per-test timeout seconds (default `300`) |
|
|
143
|
+
| `OPENCODE_MULTI_AGENT_WEATHER=1` | Enable 11-call weather integration test |
|
|
144
|
+
| `OPENCODE_ENABLE_EXA` | Passed through / defaulted to `1` in that test for web search tools |
|
|
145
|
+
|
|
146
|
+
Default `pytest -q` runs **all** tests; use `-m "not integration"` in CI without OpenCode.
|
|
147
|
+
|
|
148
|
+
## Concurrency notes
|
|
149
|
+
|
|
150
|
+
When running many tasks with `asyncio.gather`, two mitigations are active by default:
|
|
151
|
+
|
|
152
|
+
**Startup serialisation** — `AsyncOpenCodeClient` accepts `startup_concurrency` (default `1`) and `startup_delay_s` (default `0.3`). Only one process at a time enters its SQLite initialisation window; all processes run concurrently afterwards. This avoids a known opencode bug where `PRAGMA journal_mode = WAL` races against `PRAGMA busy_timeout` during concurrent startup, causing immediate crashes.
|
|
153
|
+
|
|
154
|
+
**Automatic retry** — `async_run` accepts `max_retries` (default `2`) and `retry_delay_s` (default `1.0`). If opencode exits non-zero and stderr contains SQLite lock indicators, the call is retried with a short backoff. Non-SQLite failures are raised immediately.
|
|
155
|
+
|
|
156
|
+
Set `startup_concurrency=0` (unlimited) and `max_retries=0` to opt out of both behaviours.
|
|
157
|
+
|
|
158
|
+
## Notes
|
|
159
|
+
|
|
160
|
+
- Event shapes from `--format json` may change between OpenCode versions; unknown fields are preserved in each parsed dict.
|
|
161
|
+
- For fully non-interactive automation, prefer explicit `permission` (`allow`/`deny`) over relying on interactive `ask` prompts.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# oc-py-harness
|
|
2
|
+
|
|
3
|
+
Python **async** wrapper around the [OpenCode](https://opencode.ai/docs/) CLI (`opencode run --format json`). Intended as a subprocess-based executor for **multi-agent workflow** orchestration.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python 3.10+
|
|
8
|
+
- `opencode` on `PATH` (or pass an absolute path to the binary)
|
|
9
|
+
|
|
10
|
+
## Install (local tree)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install -e ".[dev]"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### One-shot run with aggregated result
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import asyncio
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from opencode_wrapper import AsyncOpenCodeClient, RunConfig
|
|
25
|
+
|
|
26
|
+
async def main():
|
|
27
|
+
client = AsyncOpenCodeClient("opencode")
|
|
28
|
+
cfg = RunConfig(
|
|
29
|
+
model="anthropic/claude-sonnet-4-5",
|
|
30
|
+
agent="plan",
|
|
31
|
+
permission={"bash": "deny", "edit": "deny"},
|
|
32
|
+
mcp={
|
|
33
|
+
"demo": {
|
|
34
|
+
"type": "local",
|
|
35
|
+
"command": ["npx", "-y", "@modelcontextprotocol/server-everything"],
|
|
36
|
+
"enabled": True,
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
result = await client.async_run(
|
|
41
|
+
"Summarize the README in one sentence.",
|
|
42
|
+
Path("/path/to/repo"),
|
|
43
|
+
run_cfg=cfg,
|
|
44
|
+
timeout_s=600,
|
|
45
|
+
)
|
|
46
|
+
print(result.exit_code, result.final_text)
|
|
47
|
+
|
|
48
|
+
asyncio.run(main())
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Stream structured JSON events
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
async def stream_example():
|
|
55
|
+
client = AsyncOpenCodeClient()
|
|
56
|
+
cfg = RunConfig(permission={"*": "allow"})
|
|
57
|
+
async for event in client.async_stream("List top-level files.", workspace=".", run_cfg=cfg):
|
|
58
|
+
print(event)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Parallel agents (`asyncio.gather`)
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
async def multi():
|
|
65
|
+
# startup_concurrency=1 serialises SQLite initialisation to avoid a known
|
|
66
|
+
# WAL-pragma race in opencode when many instances start simultaneously.
|
|
67
|
+
# startup_delay_s controls how long each slot is held before the next
|
|
68
|
+
# process is allowed to start (default 0.3 s).
|
|
69
|
+
client = AsyncOpenCodeClient(startup_concurrency=1, startup_delay_s=0.3)
|
|
70
|
+
ws = Path("/path/to/monorepo")
|
|
71
|
+
results = await asyncio.gather(*[
|
|
72
|
+
client.async_run(
|
|
73
|
+
f"Explain services/{svc}.",
|
|
74
|
+
ws / "services" / svc,
|
|
75
|
+
run_cfg=RunConfig(agent="explore"),
|
|
76
|
+
timeout_s=600,
|
|
77
|
+
# max_retries=2 (default): retry automatically if opencode crashes
|
|
78
|
+
# during SQLite startup before giving up.
|
|
79
|
+
)
|
|
80
|
+
for svc in ["api", "worker", "gateway"]
|
|
81
|
+
])
|
|
82
|
+
return results
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Configuration injection
|
|
86
|
+
|
|
87
|
+
Per-call JSON is merged and passed as `OPENCODE_CONFIG_CONTENT` (see [OpenCode config](https://opencode.ai/docs/config/)). Use `RunConfig` fields:
|
|
88
|
+
|
|
89
|
+
| Field | Purpose |
|
|
90
|
+
|--------|---------|
|
|
91
|
+
| `permission` | `permission` map (`allow` / `ask` / `deny`, patterns) |
|
|
92
|
+
| `mcp` | MCP server definitions |
|
|
93
|
+
| `tools` | Enable/disable tools (including MCP globs) |
|
|
94
|
+
| `config_overrides` | Any extra top-level config keys to deep-merge |
|
|
95
|
+
|
|
96
|
+
Optional env tuning: `disable_autoupdate=True` sets `OPENCODE_DISABLE_AUTOUPDATE=1`.
|
|
97
|
+
|
|
98
|
+
## CLI arguments
|
|
99
|
+
|
|
100
|
+
`RunConfig` maps to flags such as `--agent`, `-m`, `-f`, `--attach`, `--title`, etc. Prompt text is appended as the final `opencode run` message argument.
|
|
101
|
+
|
|
102
|
+
## Tests
|
|
103
|
+
|
|
104
|
+
Unit tests (no real OpenCode / no API calls):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pytest -q -m "not integration"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Integration tests (real `opencode run`, needs working provider auth — **slow**, may incur API usage):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pytest -m integration -q tests/test_integration_opencode.py
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Multi-agent weather workflow** (10 parallel city lookups + 1 summary — **11 API calls**, not run by default):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
OPENCODE_MULTI_AGENT_WEATHER=1 pytest -m integration -v tests/test_integration_multi_agent_weather.py
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Optional: `OPENCODE_WEATHER_SEQUENTIAL=1` runs the 10 city calls one-by-one (easier on rate limits).
|
|
123
|
+
Per-stage timeouts: `OPENCODE_WEATHER_PER_CITY_TIMEOUT_S`, `OPENCODE_WEATHER_SUMMARY_TIMEOUT_S` (default: same as `OPENCODE_INTEGRATION_TIMEOUT_S`).
|
|
124
|
+
|
|
125
|
+
| Env | Meaning |
|
|
126
|
+
|-----|--------|
|
|
127
|
+
| `OPENCODE_BINARY` | Absolute path to `opencode` if not on `PATH` |
|
|
128
|
+
| `OPENCODE_INTEGRATION=0` | Skip integration tests |
|
|
129
|
+
| `OPENCODE_INTEGRATION_TIMEOUT_S` | Per-test timeout seconds (default `300`) |
|
|
130
|
+
| `OPENCODE_MULTI_AGENT_WEATHER=1` | Enable 11-call weather integration test |
|
|
131
|
+
| `OPENCODE_ENABLE_EXA` | Passed through / defaulted to `1` in that test for web search tools |
|
|
132
|
+
|
|
133
|
+
Default `pytest -q` runs **all** tests; use `-m "not integration"` in CI without OpenCode.
|
|
134
|
+
|
|
135
|
+
## Concurrency notes
|
|
136
|
+
|
|
137
|
+
When running many tasks with `asyncio.gather`, two mitigations are active by default:
|
|
138
|
+
|
|
139
|
+
**Startup serialisation** — `AsyncOpenCodeClient` accepts `startup_concurrency` (default `1`) and `startup_delay_s` (default `0.3`). Only one process at a time enters its SQLite initialisation window; all processes run concurrently afterwards. This avoids a known opencode bug where `PRAGMA journal_mode = WAL` races against `PRAGMA busy_timeout` during concurrent startup, causing immediate crashes.
|
|
140
|
+
|
|
141
|
+
**Automatic retry** — `async_run` accepts `max_retries` (default `2`) and `retry_delay_s` (default `1.0`). If opencode exits non-zero and stderr contains SQLite lock indicators, the call is retried with a short backoff. Non-SQLite failures are raised immediately.
|
|
142
|
+
|
|
143
|
+
Set `startup_concurrency=0` (unlimited) and `max_retries=0` to opt out of both behaviours.
|
|
144
|
+
|
|
145
|
+
## Notes
|
|
146
|
+
|
|
147
|
+
- Event shapes from `--format json` may change between OpenCode versions; unknown fields are preserved in each parsed dict.
|
|
148
|
+
- For fully non-interactive automation, prefer explicit `permission` (`allow`/`deny`) over relying on interactive `ask` prompts.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""OpenCode CLI async wrapper for Python orchestration."""
|
|
2
|
+
|
|
3
|
+
from opencode_wrapper.client import AsyncOpenCodeClient, build_argv, build_env, resolve_binary
|
|
4
|
+
from opencode_wrapper.config import RunConfig, validate_config_for_run, validate_permission_actions
|
|
5
|
+
from opencode_wrapper.errors import (
|
|
6
|
+
OpenCodeBinaryNotFoundError,
|
|
7
|
+
OpenCodeCancelledError,
|
|
8
|
+
OpenCodeError,
|
|
9
|
+
OpenCodeProcessError,
|
|
10
|
+
OpenCodeTimeoutError,
|
|
11
|
+
)
|
|
12
|
+
from opencode_wrapper.events import (
|
|
13
|
+
RunResult,
|
|
14
|
+
TokenUsage,
|
|
15
|
+
aggregate_run_result,
|
|
16
|
+
parse_event_line,
|
|
17
|
+
run_result_fuzzy_text,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AsyncOpenCodeClient",
|
|
22
|
+
"RunConfig",
|
|
23
|
+
"RunResult",
|
|
24
|
+
"TokenUsage",
|
|
25
|
+
"aggregate_run_result",
|
|
26
|
+
"build_argv",
|
|
27
|
+
"build_env",
|
|
28
|
+
"parse_event_line",
|
|
29
|
+
"run_result_fuzzy_text",
|
|
30
|
+
"resolve_binary",
|
|
31
|
+
"validate_config_for_run",
|
|
32
|
+
"validate_permission_actions",
|
|
33
|
+
"OpenCodeError",
|
|
34
|
+
"OpenCodeBinaryNotFoundError",
|
|
35
|
+
"OpenCodeProcessError",
|
|
36
|
+
"OpenCodeTimeoutError",
|
|
37
|
+
"OpenCodeCancelledError",
|
|
38
|
+
]
|