opencode-runtime 0.4.0__tar.gz → 0.4.1__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 (36) hide show
  1. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/.gitignore +1 -1
  2. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/CHANGELOG.md +7 -0
  3. opencode_runtime-0.4.1/PKG-INFO +125 -0
  4. opencode_runtime-0.4.1/README.md +92 -0
  5. opencode_runtime-0.4.1/docs/cli.md +99 -0
  6. opencode_runtime-0.4.1/docs/http-client.md +96 -0
  7. opencode_runtime-0.4.1/docs/opencode-config.md +94 -0
  8. opencode_runtime-0.4.1/docs/sessions.md +102 -0
  9. opencode_runtime-0.4.1/docs/streaming.md +150 -0
  10. opencode_runtime-0.4.1/docs/users-and-workspaces.md +125 -0
  11. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/pyproject.toml +2 -2
  12. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/__init__.py +1 -1
  13. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/client.py +1 -5
  14. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/event.py +3 -2
  15. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/response.py +1 -1
  16. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/server.py +1 -1
  17. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/session.py +4 -1
  18. opencode_runtime-0.4.0/PKG-INFO +0 -299
  19. opencode_runtime-0.4.0/README.md +0 -266
  20. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/.github/workflows/ci.yml +0 -0
  21. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/.github/workflows/publish.yml +0 -0
  22. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/CONTRIBUTING.md +0 -0
  23. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/LICENSE +0 -0
  24. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/cli.py +0 -0
  25. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/exceptions.py +0 -0
  26. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/py.typed +0 -0
  27. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/registry.py +0 -0
  28. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/src/opencode_runtime/runtime.py +0 -0
  29. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/tests/test.py +0 -0
  30. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/tests/test_cli.py +0 -0
  31. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/tests/test_client.py +0 -0
  32. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/tests/test_multi_tenant.py +0 -0
  33. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/tests/test_registry.py +0 -0
  34. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/tests/test_runtime.py +0 -0
  35. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/tests/test_server.py +0 -0
  36. {opencode_runtime-0.4.0 → opencode_runtime-0.4.1}/tests/test_session.py +0 -0
@@ -38,7 +38,7 @@ MANIFEST
38
38
  *.whl
39
39
 
40
40
  # opencode-runtime runtime state
41
- .opencode-runtime/
41
+ .opencode-runtime/*
42
42
 
43
43
  # macOS
44
44
  .DS_Store
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.4.1] - 2026-07-09
6
+
7
+ ### Fixed
8
+ - `server.py`: set `OPENCODE_CONFIG` env var instead of the unrecognised `OPENCODE_CONFIG_HOME` — without this fix the `config` dict (model, permissions, etc.) was never read by the opencode binary, silently ignored every time
9
+ - `client.py`: SSE stream now terminates only on `session.idle` or `session.error`; previously `session.status {type: "idle"}` caused the generator to exit early, dropping any events that followed — including `permission.asked`
10
+ - `session.py`: `ask()` now propagates the real server error message from `event.raw["properties"]["error"]`; previously it always raised the generic fallback `"unknown error from opencode server"` because `event.text` is always `None` for `session.error` events
11
+
5
12
  ## [0.4.0] - 2026-07-07
6
13
 
7
14
  ### Changed
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencode-runtime
3
+ Version: 0.4.1
4
+ Summary: Embed OpenCode in your Python application.
5
+ Project-URL: Homepage, https://github.com/ashish16052/opencode-runtime
6
+ Project-URL: Repository, https://github.com/ashish16052/opencode-runtime
7
+ Project-URL: Issues, https://github.com/ashish16052/opencode-runtime/issues
8
+ Author: Ashish Mohite
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: AI coding agent,agent runtime,lifecycle management,multi-user,opencode,workspace isolation
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx
23
+ Provides-Extra: dev
24
+ Requires-Dist: build; extra == 'dev'
25
+ Requires-Dist: hatchling; extra == 'dev'
26
+ Requires-Dist: mypy; extra == 'dev'
27
+ Requires-Dist: pytest; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio; extra == 'dev'
29
+ Requires-Dist: pytest-timeout; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # opencode-runtime
35
+
36
+ **Embed OpenCode in your Python application.**
37
+
38
+ OpenCode is a great CLI coding agent. But to embed it in a product, SaaS
39
+ backend, or multi-user automation system, you need more than a CLI process.
40
+
41
+ opencode-runtime turns OpenCode into a managed application runtime for
42
+ Python. It uses OpenCode as the agent harness and adds what you need to
43
+ run it as part of an application: lifecycle management, per-user
44
+ workspaces, reusable sessions, health checks, streaming output, and
45
+ multi-server orchestration.
46
+
47
+ ## Install
48
+
49
+ ```sh
50
+ pip install opencode-runtime
51
+ npm install -g opencode-ai # opencode must be on PATH
52
+ ```
53
+
54
+ ## Use
55
+
56
+ ### Your first session
57
+
58
+ ```python
59
+ from opencode_runtime import OpenCodeRuntime
60
+
61
+ async with OpenCodeRuntime() as r:
62
+ session = await r.session()
63
+ response = await session.ask("Explain this repo")
64
+ print(response.text)
65
+ ```
66
+
67
+ ### One server per user, automatically
68
+
69
+ Every `(workspace, user_id)` pair gets its own isolated server, workspace, and conversation history — started on first use, reused after:
70
+
71
+ ```python
72
+ async with OpenCodeRuntime(runtime_dir=".opencode-runtime") as r:
73
+ s1 = await r.session(workspace="org_a", user_id="u_1")
74
+ s2 = await r.session(workspace="org_b", user_id="u_2")
75
+ ```
76
+
77
+ ### Stream every token as it arrives
78
+
79
+ ```python
80
+ async for event in session.stream("Review this PR"):
81
+ if event.type == "message.part.delta" and event.text:
82
+ print(event.text, end="", flush=True)
83
+ ```
84
+
85
+ ## Inspect
86
+
87
+ Servers started from Python are visible and manageable from the terminal:
88
+
89
+ ```sh
90
+ opencode-runtime ps
91
+ ```
92
+
93
+ ```
94
+ ID PID PORT STATUS UPTIME WORKSPACE USER PROJECT
95
+ ──────────────────────────────────────────────────────────────────────────────────
96
+ 39dce5beb4debfaa 12051 58409 ● alive Up 5m org_a u_1 ~/Developer/myproject
97
+ 81fa29acb3e9210f 12088 58411 ● alive Up 3m org_b u_2 ~/Developer/myproject
98
+ ```
99
+
100
+ ```sh
101
+ opencode-runtime health 39dce5beb4debfaa
102
+ opencode-runtime stop-all
103
+ ```
104
+
105
+ ## Guides
106
+
107
+ - [OpenCode config](docs/opencode-config.md) — models, permissions, agents, skills
108
+ - [Sessions](docs/sessions.md) — continuation, resume across restarts
109
+ - [Users & workspaces](docs/users-and-workspaces.md) — multi-tenant isolation
110
+ - [Streaming](docs/streaming.md) — event types, tool calls, permissions, cost
111
+ - [CLI](docs/cli.md) — manage your fleet from the terminal
112
+ - [HTTP client](docs/http-client.md) — direct server access
113
+
114
+ ## Requirements
115
+
116
+ - Python 3.10+
117
+ - `opencode` 1.0+ on PATH
118
+
119
+ ## Contributing
120
+
121
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
122
+
123
+ ## License
124
+
125
+ Apache 2.0
@@ -0,0 +1,92 @@
1
+ # opencode-runtime
2
+
3
+ **Embed OpenCode in your Python application.**
4
+
5
+ OpenCode is a great CLI coding agent. But to embed it in a product, SaaS
6
+ backend, or multi-user automation system, you need more than a CLI process.
7
+
8
+ opencode-runtime turns OpenCode into a managed application runtime for
9
+ Python. It uses OpenCode as the agent harness and adds what you need to
10
+ run it as part of an application: lifecycle management, per-user
11
+ workspaces, reusable sessions, health checks, streaming output, and
12
+ multi-server orchestration.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pip install opencode-runtime
18
+ npm install -g opencode-ai # opencode must be on PATH
19
+ ```
20
+
21
+ ## Use
22
+
23
+ ### Your first session
24
+
25
+ ```python
26
+ from opencode_runtime import OpenCodeRuntime
27
+
28
+ async with OpenCodeRuntime() as r:
29
+ session = await r.session()
30
+ response = await session.ask("Explain this repo")
31
+ print(response.text)
32
+ ```
33
+
34
+ ### One server per user, automatically
35
+
36
+ Every `(workspace, user_id)` pair gets its own isolated server, workspace, and conversation history — started on first use, reused after:
37
+
38
+ ```python
39
+ async with OpenCodeRuntime(runtime_dir=".opencode-runtime") as r:
40
+ s1 = await r.session(workspace="org_a", user_id="u_1")
41
+ s2 = await r.session(workspace="org_b", user_id="u_2")
42
+ ```
43
+
44
+ ### Stream every token as it arrives
45
+
46
+ ```python
47
+ async for event in session.stream("Review this PR"):
48
+ if event.type == "message.part.delta" and event.text:
49
+ print(event.text, end="", flush=True)
50
+ ```
51
+
52
+ ## Inspect
53
+
54
+ Servers started from Python are visible and manageable from the terminal:
55
+
56
+ ```sh
57
+ opencode-runtime ps
58
+ ```
59
+
60
+ ```
61
+ ID PID PORT STATUS UPTIME WORKSPACE USER PROJECT
62
+ ──────────────────────────────────────────────────────────────────────────────────
63
+ 39dce5beb4debfaa 12051 58409 ● alive Up 5m org_a u_1 ~/Developer/myproject
64
+ 81fa29acb3e9210f 12088 58411 ● alive Up 3m org_b u_2 ~/Developer/myproject
65
+ ```
66
+
67
+ ```sh
68
+ opencode-runtime health 39dce5beb4debfaa
69
+ opencode-runtime stop-all
70
+ ```
71
+
72
+ ## Guides
73
+
74
+ - [OpenCode config](docs/opencode-config.md) — models, permissions, agents, skills
75
+ - [Sessions](docs/sessions.md) — continuation, resume across restarts
76
+ - [Users & workspaces](docs/users-and-workspaces.md) — multi-tenant isolation
77
+ - [Streaming](docs/streaming.md) — event types, tool calls, permissions, cost
78
+ - [CLI](docs/cli.md) — manage your fleet from the terminal
79
+ - [HTTP client](docs/http-client.md) — direct server access
80
+
81
+ ## Requirements
82
+
83
+ - Python 3.10+
84
+ - `opencode` 1.0+ on PATH
85
+
86
+ ## Contributing
87
+
88
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
89
+
90
+ ## License
91
+
92
+ Apache 2.0
@@ -0,0 +1,99 @@
1
+ # CLI
2
+
3
+ Every server your Python app starts is also visible and controllable from the terminal — same registry, no separate daemon. The CLI is how you operate your OpenCode fleet in production: inspect running servers, health-check tenants, drain a workspace, or kill everything before a deploy.
4
+
5
+ Think of it as `kubectl` for your OpenCode fleet.
6
+
7
+ ## See everything that's running
8
+
9
+ ```sh
10
+ opencode-runtime ps
11
+ ```
12
+
13
+ ```
14
+ ID PID PORT STATUS UPTIME WORKSPACE USER PROJECT
15
+ ──────────────────────────────────────────────────────────────────────────────────
16
+ 39dce5beb4debfaa 12051 58409 ● alive Up 5m org_a u_1 ~/Developer/myproject
17
+ 81fa29acb3e9210f 12088 58411 ● alive Up 3m org_b u_2 ~/Developer/myproject
18
+ c3f2a1d9e8b74f05 13204 58413 ● alive Up 1h org_c u_3 ~/Developer/myproject
19
+ ```
20
+
21
+ Every server your Python app has started — across all users and workspaces — is visible here. PID, port, uptime, which tenant, which user, which project. No guessing, no digging through logs.
22
+
23
+ ## Health check a specific server
24
+
25
+ ```sh
26
+ opencode-runtime health 39dce5beb4debfaa
27
+ ```
28
+
29
+ Hit the health endpoint of any server by ID. Use this in a monitoring script, a deployment check, or just to verify a server is responsive after a spike in traffic.
30
+
31
+ Pipe it into your alerting:
32
+
33
+ ```sh
34
+ opencode-runtime health 39dce5beb4debfaa || pagerduty-alert "opencode server down"
35
+ ```
36
+
37
+ ## Start a server manually
38
+
39
+ Spin up a server outside of Python — useful for pre-warming tenants before they hit your API, or for running one-off tasks from the terminal:
40
+
41
+ ```sh
42
+ opencode-runtime serve --workspace acme --user-id alice
43
+ ```
44
+
45
+ The server registers in the same shared registry your Python app uses. When your app later calls `r.session(workspace="acme", user_id="alice")`, it reuses the already-running server instead of starting a new one.
46
+
47
+ ## Drain a tenant
48
+
49
+ A tenant is causing runaway resource usage. Stop their server without touching any other tenant:
50
+
51
+ ```sh
52
+ # find the server
53
+ opencode-runtime ps | grep acme
54
+
55
+ # stop just that one
56
+ opencode-runtime stop 39dce5beb4debfaa
57
+ ```
58
+
59
+ Next request from that tenant will cold-start a fresh server — clean slate, no lingering state.
60
+
61
+ ## Emergency stop
62
+
63
+ Deploy gone wrong. A bug is spawning servers faster than expected. Kill everything instantly:
64
+
65
+ ```sh
66
+ opencode-runtime stop-all
67
+ ```
68
+
69
+ All servers stopped, all ports freed. Your app will restart them on next use.
70
+
71
+ ## Use as a sidecar in production
72
+
73
+ The CLI reads from the same registry as the Python library — no separate daemon, no IPC. Run it alongside your application in any environment:
74
+
75
+ ```sh
76
+ # in one terminal — your app
77
+ python app.py
78
+
79
+ # in another — live monitoring
80
+ watch -n 5 opencode-runtime ps
81
+ ```
82
+
83
+ Or wire it into your infra tooling. The `ps` output is structured enough to parse, `health` exits non-zero on failure, and `stop`/`stop-all` are safe to call from scripts or runbooks.
84
+
85
+ ```sh
86
+ # runbook: drain all servers before a deploy
87
+ opencode-runtime stop-all
88
+ deploy.sh
89
+ ```
90
+
91
+ ## Command reference
92
+
93
+ | Command | Description |
94
+ |---|---|
95
+ | `opencode-runtime ps` | List all running servers with ID, PID, port, status, uptime, workspace, user, project |
96
+ | `opencode-runtime serve` | Start a background server. Accepts `--workspace`, `--user-id` |
97
+ | `opencode-runtime health <id>` | Health check a server by ID. Exits non-zero if unhealthy |
98
+ | `opencode-runtime stop <id>` | Stop a specific server by ID |
99
+ | `opencode-runtime stop-all` | Stop all running servers |
@@ -0,0 +1,96 @@
1
+ # Raw client
2
+
3
+ `session.raw_client` gives you direct access to every OpenCode server endpoint — anything not covered by `ask()` and `stream()`. It exposes two methods: `get(path)` and `post(path, body)`, both return parsed JSON.
4
+
5
+ The examples below follow a single story: a code review backend that receives a PR, runs an agent review, tracks progress, stores the result, and handles rollback.
6
+
7
+ ```python
8
+ from opencode_runtime import OpenCodeRuntime
9
+
10
+ async with OpenCodeRuntime(runtime_dir=".opencode-runtime") as r:
11
+ session = await r.session(workspace="acme", user_id="alice")
12
+ client = session.raw_client
13
+ ```
14
+
15
+ ## 1. Pick the right agent
16
+
17
+ Before running the review, confirm which agents are available and pick the one best suited for the task:
18
+
19
+ ```python
20
+ agents = await client.get("/agent")
21
+ review_agents = [a for a in agents if "review" in a["name"].lower()]
22
+
23
+ # fall back to "plan" (read-only, no edits) if no dedicated reviewer
24
+ agent = review_agents[0]["name"] if review_agents else "plan"
25
+ ```
26
+
27
+ ## 2. Run the review
28
+
29
+ Use the chosen agent for this specific call:
30
+
31
+ ```python
32
+ response = await session.ask(
33
+ "Review this PR for security issues, logic bugs, and test coverage gaps",
34
+ agent=agent,
35
+ )
36
+ print(response.text)
37
+ ```
38
+
39
+ ## 3. Track todos
40
+
41
+ For complex PRs, the agent creates a todo list as it works through the review. Poll it to show progress in your UI:
42
+
43
+ ```python
44
+ todos = await client.get(f"/session/{session.session_id}/todo")
45
+ for todo in todos:
46
+ icon = {"completed": "✓", "in_progress": "⟳", "pending": "○", "cancelled": "✗"}.get(todo["status"], "?")
47
+ print(f" {icon} {todo['content']}")
48
+ ```
49
+
50
+ ## 4. Store the full conversation
51
+
52
+ After the review, fetch the full message history to persist in your database — useful for audit trails, displaying review threads in your UI, or feeding into downstream pipelines:
53
+
54
+ ```python
55
+ messages = await client.get(f"/session/{session.session_id}/message")
56
+ for entry in messages:
57
+ info = entry["info"]
58
+ role = info["role"]
59
+ cost = info.get("cost", 0) if role == "assistant" else 0
60
+ text = next((p["text"] for p in entry["parts"] if p["type"] == "text"), "")
61
+ db.insert(session_id=session.session_id, role=role, text=text, cost=cost)
62
+ ```
63
+
64
+ ## 5. Revert if the agent made unwanted changes
65
+
66
+ If the agent edited files as part of the review and you want to roll them back:
67
+
68
+ ```python
69
+ messages = await client.get(f"/session/{session.session_id}/message")
70
+
71
+ # revert to just before the last assistant turn
72
+ message_id = messages[-2]["info"]["id"]
73
+ await client.post(
74
+ f"/session/{session.session_id}/revert",
75
+ {"messageID": message_id},
76
+ )
77
+ ```
78
+
79
+ ## 6. Report cost
80
+
81
+ At the end of the review, read the final cost from the last assistant message for billing or quota tracking:
82
+
83
+ ```python
84
+ messages = await client.get(f"/session/{session.session_id}/message")
85
+ assistant_messages = [e for e in messages if e["info"]["role"] == "assistant"]
86
+
87
+ if assistant_messages:
88
+ last = assistant_messages[-1]["info"]
89
+ cost = last.get("cost", 0)
90
+ tokens = last.get("tokens", {})
91
+ print(f"Review cost: ${cost:.5f} ({tokens.get('input', 0)} in / {tokens.get('output', 0)} out)")
92
+ ```
93
+
94
+ ## Full endpoint reference
95
+
96
+ See the [OpenCode server docs](https://opencode.ai/docs/server) for all endpoints and the full type definitions in [types.gen.ts](https://github.com/anomalyco/opencode/blob/dev/packages/sdk/js/src/gen/types.gen.ts).
@@ -0,0 +1,94 @@
1
+ # Configuration & materials
2
+
3
+ ## Config
4
+
5
+ Pass any valid `opencode.json` keys as a dict. opencode-runtime writes this as the server's config before starting. **`runtime_dir` must be set for config to take effect** — without it no config file is written.
6
+
7
+ ```python
8
+ from opencode_runtime import OpenCodeRuntime
9
+
10
+ async with OpenCodeRuntime(
11
+ runtime_dir=".opencode-runtime",
12
+ config={
13
+ "model": "anthropic/claude-sonnet-4-5",
14
+ "permission": {"bash": "deny"},
15
+ },
16
+ ) as r:
17
+ session = await r.session()
18
+ response = await session.ask("Analyse the architecture")
19
+ print(response.text)
20
+ ```
21
+
22
+ See the [OpenCode config reference](https://opencode.ai/docs/config/) for all available keys.
23
+
24
+ ## Materials
25
+
26
+ Pass a directory of OpenCode-native files and they are copied into the server before it starts. This is how you bring custom instructions, agents, and skills to a managed server.
27
+
28
+ ```python
29
+ from opencode_runtime import OpenCodeRuntime
30
+
31
+ async with OpenCodeRuntime(
32
+ runtime_dir=".opencode-runtime",
33
+ materials="./opencode-materials",
34
+ ) as r:
35
+ session = await r.session()
36
+ response = await session.ask("Follow the instructions in AGENTS.md")
37
+ print(response.text)
38
+ ```
39
+
40
+ What can go in a materials directory:
41
+
42
+ | File / path | Purpose | Docs |
43
+ |---|---|---|
44
+ | `opencode.json` | Server config (model, permissions, MCP servers, …) | [Config](https://opencode.ai/docs/config/) |
45
+ | `AGENTS.md` | Rules and instructions included in every session | [Rules](https://opencode.ai/docs/rules/) |
46
+ | `.opencode/agents/` | Custom agent definitions (markdown format) | [Agents](https://opencode.ai/docs/agents/#markdown) |
47
+ | `.opencode/skills/` | Reusable skill definitions | [Agent Skills](https://opencode.ai/docs/skills/) |
48
+
49
+ ## project_dir
50
+
51
+ The directory OpenCode runs against — the working directory of the subprocess. Defaults to `.` (wherever your Python process is running).
52
+
53
+ ```python
54
+ async with OpenCodeRuntime(project_dir="/path/to/repo") as r:
55
+ session = await r.session()
56
+ response = await session.ask("What does this project do?")
57
+ print(response.text)
58
+ ```
59
+
60
+ **When to use:** when your Python backend runs from a different location than the repo you want OpenCode to work on — for example, a service that checks out customer repos to a temp directory and points OpenCode at each one.
61
+
62
+ ## runtime_dir
63
+
64
+ When set, each server gets its own isolated `HOME` and config file (`opencode.json`) under `runtime_dir/servers/<key>/`. Without it, OpenCode uses your real user environment and any `config` or `materials` passed to the runtime are ignored.
65
+
66
+ ```python
67
+ async with OpenCodeRuntime(runtime_dir=".opencode-runtime") as r:
68
+ session = await r.session()
69
+ response = await session.ask("What does this project do?")
70
+ print(response.text)
71
+ ```
72
+
73
+ **When to use:** any multi-user or production setup. Without `runtime_dir`, all servers share your real home directory — meaning shared OpenCode config, shared history, and API keys from your personal environment. Setting it gives every server a clean, isolated slate.
74
+
75
+ ## Session-level overrides
76
+
77
+ `config`, `materials`, and `env` can all be overridden per session. Session config is shallow-merged with runtime config; session materials replace runtime materials; session env is merged with runtime env. Session keys win in all cases.
78
+
79
+ ```python
80
+ async with OpenCodeRuntime(
81
+ config={"model": "anthropic/claude-sonnet-4-5"},
82
+ materials="./base-materials",
83
+ env={"API_KEY": "default"},
84
+ ) as r:
85
+ session = await r.session(
86
+ config={"model": "anthropic/claude-opus-4-5"},
87
+ materials="./org-a-materials",
88
+ env={"API_KEY": "org-a-key"},
89
+ )
90
+ response = await session.ask("Analyse the architecture")
91
+ print(response.text)
92
+ ```
93
+
94
+ Note: `config` and `materials` affect server identity — different values produce a separate server process. `env` does not; if the server is already running, a different `env` passed to `session()` has no effect.
@@ -0,0 +1,102 @@
1
+ # Sessions
2
+
3
+ ## Single ask
4
+
5
+ The simplest usage — send a message, get a response:
6
+
7
+ ```python
8
+ from opencode_runtime import OpenCodeRuntime
9
+
10
+ async with OpenCodeRuntime() as r:
11
+ session = await r.session()
12
+ response = await session.ask("What does this project do?")
13
+ print(response.text)
14
+ ```
15
+
16
+ `response.text` is the full assistant reply. `response.raw` contains all raw events if you need them (tool calls, thinking, status updates).
17
+
18
+ ## Multi-turn conversation
19
+
20
+ Multiple `ask()` calls on the same session continue the same conversation — OpenCode keeps the full history server-side:
21
+
22
+ ```python
23
+ async with OpenCodeRuntime() as r:
24
+ session = await r.session()
25
+ await session.ask("Explain this repo's architecture")
26
+ await session.ask("Which file handles authentication?") # has full context
27
+ response = await session.ask("What would you change about it?")
28
+ print(response.text)
29
+ ```
30
+
31
+ To start a fresh independent conversation, get a new session:
32
+
33
+ ```python
34
+ async with OpenCodeRuntime() as r:
35
+ session_a = await r.session() # conversation A
36
+ session_b = await r.session() # conversation B — isolated, no shared history
37
+ ```
38
+
39
+ ## Resume across restarts
40
+
41
+ `session_id` is set after the first `ask()` or `stream()` call. Persist it to resume the conversation later:
42
+
43
+ ```python
44
+ # First run
45
+ async with OpenCodeRuntime() as r:
46
+ session = await r.session()
47
+ await session.ask("Explain this repo")
48
+ saved_id = session.session_id # store in your DB
49
+
50
+ # Later — picks up the same conversation
51
+ async with OpenCodeRuntime() as r:
52
+ session = await r.session(session_id=saved_id)
53
+ response = await session.ask("Where did we get to?")
54
+ print(response.text)
55
+ ```
56
+
57
+ ## Per-message overrides
58
+
59
+ `ask()` and `stream()` accept per-message overrides for model, agent, tools, and system prompt. These apply to that message only — they don't change the session's config:
60
+
61
+ ```python
62
+ async with OpenCodeRuntime() as r:
63
+ session = await r.session()
64
+
65
+ # use a specific agent for one task
66
+ review = await session.ask(
67
+ "Review this PR for security issues",
68
+ agent="security-auditor",
69
+ )
70
+
71
+ # override model for a single expensive call
72
+ deep = await session.ask(
73
+ "Redesign the auth module",
74
+ model="anthropic/claude-opus-4-5",
75
+ )
76
+
77
+ # disable bash for one call
78
+ safe = await session.ask(
79
+ "Summarise the test coverage",
80
+ tools={"bash": False},
81
+ )
82
+ ```
83
+
84
+ ## Abort
85
+
86
+ Cancel a running session mid-flight:
87
+
88
+ ```python
89
+ import asyncio
90
+ from opencode_runtime import OpenCodeRuntime
91
+
92
+ async with OpenCodeRuntime() as r:
93
+ session = await r.session()
94
+
95
+ async def run():
96
+ await session.ask("Refactor the entire codebase")
97
+
98
+ task = asyncio.create_task(run())
99
+ await asyncio.sleep(5)
100
+ await session.abort() # cancels the in-progress session server-side
101
+ task.cancel()
102
+ ```