Skip to main content

Async Python wrapper for OpenCode CLI (opencode run --format json)

Project description

py-opencode-wrapper

Python async wrapper around the OpenCode CLI (opencode run --format json). Intended as a subprocess-based executor for multi-agent workflow orchestration.

Requirements

  • Python 3.8+
  • opencode on PATH (or pass an absolute path to the binary)

Install

From PyPI (most users):

pip install py-opencode-wrapper

The distribution name on PyPI is py-opencode-wrapper; import it as opencode_wrapper:

from opencode_wrapper import AsyncOpenCodeClient, RunConfig

For local development (editable install with test deps):

pip install -e ".[dev]"

Usage

One-shot run with aggregated result

import asyncio
from pathlib import Path

from opencode_wrapper import AsyncOpenCodeClient, RunConfig

async def main():
    client = AsyncOpenCodeClient("opencode")
    cfg = RunConfig(
        model="opencode/big-pickle",
        agent="plan",
        permission={"bash": "deny", "edit": "deny"},
        mcp={
            "demo": {
                "type": "local",
                "command": ["npx", "-y", "@modelcontextprotocol/server-everything"],
                "enabled": True,
            }
        },
    )
    result = await client.async_run(
        "Summarize the README in one sentence.",
        Path("/path/to/repo"),
        run_cfg=cfg,
        timeout_s=600,
    )
    print(result.exit_code, result.final_text)

asyncio.run(main())

Set RunConfig(record_thinking=True) when you want OpenCode reasoning/thinking parts included in result.events and log_file JSON lines. This only maps to OpenCode's display/output flag --thinking; it does not change model reasoning effort. Use variant separately if you intentionally want a provider-specific reasoning effort.

Multi-turn conversation (OpenCodeSession)

For a stateful, multi-turn chat over a single opencode session, use OpenCodeSession as an async context manager. Each send() continues the same session, so the model retains context across turns:

import asyncio
from opencode_wrapper import AsyncOpenCodeClient, OpenCodeSession, RunConfig

async def chat():
    client = AsyncOpenCodeClient()
    async with OpenCodeSession(client, ".", run_cfg=RunConfig(model="opencode/big-pickle")) as s:
        r1 = await s.send("My name is Bob.")
        r2 = await s.send("What is my name?")   # auto-continues → "Bob"
        print(s.session_id, r2.final_text)

asyncio.run(chat())

On enter, the session allocates a private, persistent XDG_DATA_HOME tmpdir that every turn reuses, so opencode's SQLite session DB survives across turns. The first send() creates the session (its id is captured on RunResult.session_id); later turns continue it via --session <id>. Each session is an isolated island — no shared global DB, so no cross-session lock contention — and the tmpdir is removed when the async with block exits. send() accepts per-turn run_cfg and timeout_s overrides.

Stream structured JSON events

async def stream_example():
    client = AsyncOpenCodeClient()
    cfg = RunConfig(permission={"*": "allow"})
    async for event in client.async_stream("List top-level files.", workspace=".", run_cfg=cfg):
        print(event)

Parallel agents (asyncio.gather)

async def multi():
    client = AsyncOpenCodeClient()
    ws = Path("/path/to/monorepo")
    results = await asyncio.gather(*[
        client.async_run(
            f"Explain services/{svc}.",
            ws / "services" / svc,
            run_cfg=RunConfig(agent="explore"),
            timeout_s=600,
        )
        for svc in ["api", "worker", "gateway"]
    ])
    return results

Safe defaults for parallel runs (startup serialisation, private SQLite DB per run, and automatic retry on SQLite-startup crashes) are enabled out of the box — most users don't need to tune them. See Concurrency notes below if you want to.

Configuration injection

Per-call JSON is merged and passed as OPENCODE_CONFIG_CONTENT (see OpenCode config). Use RunConfig fields:

Field Purpose
permission permission map (allow / deny, patterns)
mcp MCP server definitions
tools Enable/disable tools (including MCP globs)
instructions Instruction file paths / glob patterns to inject
config_overrides Any extra top-level config keys to deep-merge

Optional env tuning: disable_autoupdate=True sets OPENCODE_DISABLE_AUTOUPDATE=1. Note: ask is intentionally rejected in subprocess mode (no interactive terminal); use allow or deny.

User config isolation

By default, RunConfig.inherit_user_config=False makes each child opencode process see a sanitized copy of the host's global OpenCode config. The wrapper keeps only provider-selection keys ($schema, provider, disabled_providers, enabled_providers) and drops capability/configuration keys such as mcp, agent, command, tools, plugin, skills, instructions, permission, and model.

This keeps benchmark and orchestration runs reproducible while still allowing provider configuration and opencode auth credentials to work. Project-level config discovered from the workspace is not suppressed.

Set inherit_user_config=True to restore the legacy behavior of inheriting the host OpenCode config as-is. For reproducible runs, pass model, permission, mcp, tools, and instructions explicitly through RunConfig.

CLI arguments

RunConfig maps to flags such as --agent, -m, -f, --attach, --title, etc. Prompt text is appended as the final opencode run message argument.

Tests

Unit tests (no real OpenCode / no API calls):

pytest -q -m "not integration"

Integration tests (real opencode run, needs working provider auth — slow, may incur API usage):

pytest -m integration -q tests/test_integration_opencode.py

Multi-agent weather workflow (10 parallel city lookups + 1 summary — 11 API calls, not run by default):

OPENCODE_MULTI_AGENT_WEATHER=1 pytest -m integration -v tests/test_integration_multi_agent_weather.py

Optional: OPENCODE_WEATHER_SEQUENTIAL=1 runs the 10 city calls one-by-one (easier on rate limits).
Per-stage timeouts: OPENCODE_WEATHER_PER_CITY_TIMEOUT_S, OPENCODE_WEATHER_SUMMARY_TIMEOUT_S (default: same as OPENCODE_INTEGRATION_TIMEOUT_S).

Env Meaning
OPENCODE_BINARY Absolute path to opencode if not on PATH
OPENCODE_INTEGRATION=0 Skip integration tests
OPENCODE_INTEGRATION_TIMEOUT_S Per-test timeout seconds (default 300)
OPENCODE_MULTI_AGENT_WEATHER=1 Enable 11-call weather integration test
OPENCODE_ENABLE_EXA Passed through / defaulted to 1 in that test for web search tools

Default pytest -q runs all tests; use -m "not integration" in CI without OpenCode.

Concurrency notes

The defaults already handle the common pitfalls when running many async_run calls in parallel — you usually don't need to touch any of these.

  • Startup serialisation (startup_concurrency=1, startup_delay_s=0.3) — spaces out SQLite WAL initialisation across processes to avoid a startup race in opencode.
  • DB isolation (isolate_db=True) — each run gets its own XDG_DATA_HOME, so concurrent runs don't share opencode.db and serialise on SQLite write locks during tool execution.
  • Automatic retry (async_run(max_retries=2, retry_delay_s=1.0)) — retries known SQLite-startup crashes with short backoff. Non-SQLite failures still fail fast.

To opt out: pass startup_delay_s=0 (and a large startup_concurrency) to drop the startup pacing, isolate_db=False to share session history across runs, and max_retries=0 to disable retries.

For multi-turn conversations you don't need isolate_db=False. Use OpenCodeSession instead — it keeps one session's DB alive across turns in a private dir, so context is preserved without sharing the global DB.

Notes

  • Event shapes from --format json may change between OpenCode versions; unknown fields are preserved in each parsed dict.
  • For fully non-interactive automation, prefer explicit permission (allow/deny) over relying on interactive ask prompts.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

py_opencode_wrapper-0.3.0.tar.gz (35.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

py_opencode_wrapper-0.3.0-py3-none-any.whl (19.8 kB view details)

Uploaded Python 3

File details

Details for the file py_opencode_wrapper-0.3.0.tar.gz.

File metadata

  • Download URL: py_opencode_wrapper-0.3.0.tar.gz
  • Upload date:
  • Size: 35.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for py_opencode_wrapper-0.3.0.tar.gz
Algorithm Hash digest
SHA256 3b80442d45f5e9f1e13939488481e0e267fb631ae36f1b7052457eb69dd6ca7f
MD5 1f8da589d96aa9b57c908fcbaa83969a
BLAKE2b-256 0bda77e53f9bbdfc63bfe8277296b129d12d3bade9d57db12bea4e5ee05b874d

See more details on using hashes here.

Provenance

The following attestation bundles were made for py_opencode_wrapper-0.3.0.tar.gz:

Publisher: release.yml on idailylife/oc_py_wrapper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file py_opencode_wrapper-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for py_opencode_wrapper-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4e9a22b536038864afcf75e2475047c815b8dd37a7a0257e64b853682aea6d38
MD5 e4bf7380055cc796f1f68a31967ab956
BLAKE2b-256 a0a1fb993f281016bf88448eff1704056d4f171f6cb2c0a9eb1387ff0316b7b7

See more details on using hashes here.

Provenance

The following attestation bundles were made for py_opencode_wrapper-0.3.0-py3-none-any.whl:

Publisher: release.yml on idailylife/oc_py_wrapper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page