opencode-agent-sdk 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,82 @@
1
+ """MCP tool decorator and server creation, matching claude_agent_sdk API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from dataclasses import dataclass
7
+ from typing import Any, Callable
8
+
9
+
10
+ @dataclass
11
+ class SdkMcpTool:
12
+ """A tool registered via the @tool decorator."""
13
+
14
+ name: str
15
+ description: str
16
+ input_schema: dict[str, Any]
17
+ handler: Callable[..., Any]
18
+
19
+
20
+ def tool(
21
+ name: str,
22
+ description: str,
23
+ input_schema: dict[str, Any] | None = None,
24
+ ) -> Callable[[Callable[..., Any]], SdkMcpTool]:
25
+ """Decorator to create an MCP tool from a function.
26
+
27
+ Usage::
28
+
29
+ @tool(name="greet", description="Say hello", input_schema={"type": "object", "properties": {"name": {"type": "string"}}})
30
+ def greet(name: str) -> str:
31
+ return f"Hello, {name}!"
32
+ """
33
+
34
+ def decorator(fn: Callable[..., Any]) -> SdkMcpTool:
35
+ return SdkMcpTool(
36
+ name=name,
37
+ description=description,
38
+ input_schema=input_schema or {"type": "object", "properties": {}},
39
+ handler=fn,
40
+ )
41
+
42
+ return decorator
43
+
44
+
45
+ def create_sdk_mcp_server(
46
+ name: str,
47
+ version: str | None = None,
48
+ tools: list[SdkMcpTool] | None = None,
49
+ ) -> dict[str, Any]:
50
+ """Create an MCP server configuration for use with AgentOptions.mcp_servers.
51
+
52
+ For ACP, tools run in-process via a local stdio MCP server.
53
+ This returns a config dict that SDKClient uses to spawn a Python
54
+ MCP server subprocess hosting the given tools.
55
+
56
+ Returns a dict suitable for ``AgentOptions.mcp_servers``::
57
+
58
+ server = create_sdk_mcp_server("my-tools", tools=[my_tool])
59
+ options = AgentOptions(mcp_servers={"my-tools": server})
60
+ """
61
+ tool_list = tools or []
62
+ tool_defs = []
63
+ for t in tool_list:
64
+ tool_defs.append({
65
+ "name": t.name,
66
+ "description": t.description,
67
+ "inputSchema": t.input_schema,
68
+ })
69
+
70
+ # Store tools for the in-process MCP server runner
71
+ _TOOL_REGISTRY[name] = tool_list
72
+
73
+ return {
74
+ "command": sys.executable,
75
+ "args": ["-m", "opencode_agent_sdk._mcp_runner", name],
76
+ "_tools": tool_defs,
77
+ "_version": version or "1.0.0",
78
+ }
79
+
80
+
81
+ # Global registry for in-process tool serving
82
+ _TOOL_REGISTRY: dict[str, list[SdkMcpTool]] = {}
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Callable
5
+
6
+
7
+ @dataclass
8
+ class TextBlock:
9
+ text: str
10
+ type: str = "text"
11
+
12
+
13
+ @dataclass
14
+ class ToolUseBlock:
15
+ id: str
16
+ name: str
17
+ input: dict[str, Any]
18
+ type: str = "tool_use"
19
+
20
+
21
+ @dataclass
22
+ class AssistantMessage:
23
+ content: list[TextBlock | ToolUseBlock]
24
+ role: str = "assistant"
25
+
26
+
27
+ @dataclass
28
+ class ResultMessage:
29
+ usage: dict[str, Any] = field(default_factory=dict)
30
+ total_cost_usd: float = 0.0
31
+ session_id: str = ""
32
+ duration_ms: float = 0.0
33
+ num_turns: int = 0
34
+ is_error: bool = False
35
+
36
+
37
+ @dataclass
38
+ class SystemMessage:
39
+ subtype: str
40
+ data: dict[str, Any] = field(default_factory=dict)
41
+
42
+
43
+ @dataclass
44
+ class HookMatcher:
45
+ matcher: str | None
46
+ hooks: list[Callable[..., Any]]
47
+ timeout: float = 30.0
48
+
49
+
50
+ # Type aliases matching claude_agent_sdk
51
+ HookInput = dict[str, Any]
52
+ HookContext = dict[str, Any]
53
+ HookJSONOutput = dict[str, Any]
@@ -0,0 +1,385 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencode-agent-sdk
3
+ Version: 0.2.0
4
+ Summary: Open-source Agent SDK backed by OpenCode ACP (drop-in replacement for claude_agent_sdk)
5
+ Author: OpenCode
6
+ License: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: anyio
18
+ Requires-Dist: httpx
19
+ Provides-Extra: opencode-ai
20
+ Requires-Dist: opencode-ai>=0.1.0a36; extra == "opencode-ai"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == "dev"
23
+
24
+ # OpenCode Agent SDK for Python
25
+
26
+ Python SDK for building agents backed by [OpenCode](https://github.com/nichochar/opencode). Drop-in replacement for `claude_agent_sdk` with support for any LLM provider (Anthropic, OpenAI, xAI, etc.).
27
+
28
+ <video src="https://github.com/user-attachments/assets/061fb862-3253-46aa-92a7-71dc9258960a"
29
+ autoplay
30
+ muted
31
+ loop
32
+ playsinline
33
+ controls>
34
+ </video>
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install opencode-agent-sdk
40
+ ```
41
+
42
+ **Prerequisites:**
43
+
44
+ - Python 3.10+
45
+ - An OpenCode server (`opencode serve`) **or** the `opencode` CLI installed locally
46
+
47
+ ## Quick Start
48
+
49
+ ```python
50
+ import asyncio
51
+ from opencode_agent_sdk import SDKClient, AgentOptions, AssistantMessage, TextBlock
52
+
53
+ async def main():
54
+ client = SDKClient(options=AgentOptions(
55
+ model="claude-haiku-4-5",
56
+ server_url="http://localhost:54321",
57
+ ))
58
+
59
+ await client.connect()
60
+ await client.query("What is 2 + 2?")
61
+
62
+ async for message in client.receive_response():
63
+ if isinstance(message, AssistantMessage):
64
+ for block in message.content:
65
+ if isinstance(block, TextBlock):
66
+ print(block.text)
67
+
68
+ await client.disconnect()
69
+
70
+ asyncio.run(main())
71
+ ```
72
+
73
+ ## SDKClient
74
+
75
+ `SDKClient` supports bidirectional conversations with an LLM via OpenCode. It works in two transport modes:
76
+
77
+ - **HTTP mode** — communicates with a running `opencode serve` instance over REST
78
+ - **Subprocess mode** — spawns `opencode acp` locally over stdio JSON-RPC
79
+
80
+ ### HTTP Mode (recommended)
81
+
82
+ Start the server, then connect:
83
+
84
+ ```bash
85
+ docker compose up -d # starts opencode serve on port 54321
86
+ ```
87
+
88
+ ```python
89
+ from opencode_agent_sdk import SDKClient, AgentOptions
90
+
91
+ client = SDKClient(options=AgentOptions(
92
+ model="claude-haiku-4-5",
93
+ server_url="http://localhost:54321",
94
+ system_prompt="You are a helpful assistant",
95
+ ))
96
+
97
+ await client.connect()
98
+ await client.query("Hello!")
99
+
100
+ async for msg in client.receive_response():
101
+ print(msg)
102
+
103
+ await client.disconnect()
104
+ ```
105
+
106
+ ### Subprocess Mode
107
+
108
+ When `server_url` is not set, the SDK spawns `opencode acp` as a child process:
109
+
110
+ ```python
111
+ client = SDKClient(options=AgentOptions(
112
+ cwd="/path/to/project",
113
+ model="claude-haiku-4-5",
114
+ ))
115
+ ```
116
+
117
+ ### Resuming Sessions
118
+
119
+ ```python
120
+ options = AgentOptions(
121
+ resume="session-id-from-previous-run",
122
+ server_url="http://localhost:54321",
123
+ )
124
+ ```
125
+
126
+ ## AgentOptions
127
+
128
+ | Field | Type | Default | Description |
129
+ |-------|------|---------|-------------|
130
+ | `cwd` | `str` | `"."` | Working directory |
131
+ | `model` | `str` | `""` | Model identifier (e.g. `"claude-haiku-4-5"`) |
132
+ | `provider_id` | `str` | `"anthropic"` | Provider identifier |
133
+ | `system_prompt` | `str` | `""` | System prompt for the LLM |
134
+ | `server_url` | `str` | `""` | OpenCode server URL; enables HTTP mode when set |
135
+ | `mcp_servers` | `dict` | `{}` | MCP server configurations |
136
+ | `allowed_tools` | `list[str]` | `[]` | Tools the agent is allowed to use |
137
+ | `permission_mode` | `str` | `""` | Permission mode for tool execution |
138
+ | `hooks` | `dict` | `{}` | Hook matchers keyed by event type |
139
+ | `max_turns` | `int` | `100` | Maximum conversation turns |
140
+ | `resume` | `str \| None` | `None` | Session ID to resume |
141
+
142
+ ## Custom Tools (MCP Servers)
143
+
144
+ Define tools as Python functions and expose them as in-process MCP servers:
145
+
146
+ ```python
147
+ from opencode_agent_sdk import tool, create_sdk_mcp_server, SDKClient, AgentOptions
148
+
149
+ @tool("greet", "Greet a user", {"type": "object", "properties": {"name": {"type": "string"}}})
150
+ def greet_user(args):
151
+ return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
152
+
153
+ server = create_sdk_mcp_server("my-tools", tools=[greet_user])
154
+
155
+ client = SDKClient(options=AgentOptions(
156
+ mcp_servers={"my-tools": server},
157
+ allowed_tools=["mcp__my-tools__greet"],
158
+ server_url="http://localhost:54321",
159
+ ))
160
+ ```
161
+
162
+ You can mix in-process SDK servers with external MCP servers:
163
+
164
+ ```python
165
+ options = AgentOptions(
166
+ mcp_servers={
167
+ "internal": sdk_server, # In-process SDK server
168
+ "external": { # External stdio server
169
+ "command": "external-server",
170
+ "args": ["--port", "8080"],
171
+ },
172
+ }
173
+ )
174
+ ```
175
+
176
+ ## Hooks
177
+
178
+ Hooks let you intercept and control tool execution. They run deterministically at specific points in the agent loop.
179
+
180
+ ```python
181
+ from opencode_agent_sdk import SDKClient, AgentOptions, HookMatcher
182
+
183
+ async def check_bash_command(input_data, tool_use_id, context):
184
+ tool_input = input_data["tool_input"]
185
+ command = tool_input.get("command", "")
186
+
187
+ if "rm -rf" in command:
188
+ return {
189
+ "hookSpecificOutput": {
190
+ "hookEventName": "PreToolUse",
191
+ "permissionDecision": "deny",
192
+ "permissionDecisionReason": "Destructive command blocked",
193
+ }
194
+ }
195
+ return {}
196
+
197
+ options = AgentOptions(
198
+ allowed_tools=["Bash"],
199
+ hooks={
200
+ "PreToolUse": [
201
+ HookMatcher(matcher="Bash", hooks=[check_bash_command]),
202
+ ],
203
+ },
204
+ server_url="http://localhost:54321",
205
+ )
206
+
207
+ client = SDKClient(options=options)
208
+ await client.connect()
209
+ await client.query("Run: echo hello")
210
+
211
+ async for msg in client.receive_response():
212
+ print(msg)
213
+
214
+ await client.disconnect()
215
+ ```
216
+
217
+ Hook event types: `"PreToolUse"`, `"Stop"`
218
+
219
+ ## Types
220
+
221
+ See [src/opencode_agent_sdk/types.py](src/opencode_agent_sdk/types.py) for complete type definitions:
222
+
223
+ - `AssistantMessage` — LLM response containing `TextBlock` and/or `ToolUseBlock`
224
+ - `ResultMessage` — Final message with usage stats, cost, and session info
225
+ - `SystemMessage` — Internal events (init, tool results, thoughts)
226
+ - `TextBlock` — Text content from the LLM
227
+ - `ToolUseBlock` — Tool invocation with name and input
228
+ - `HookMatcher` — Matches tool names to hook functions
229
+
230
+ ## Error Handling
231
+
232
+ ```python
233
+ from opencode_agent_sdk._errors import ProcessError
234
+
235
+ try:
236
+ await client.connect()
237
+ except ProcessError as e:
238
+ print(f"Failed with exit code: {e.exit_code}")
239
+ ```
240
+
241
+ ## Migrating from claude_agent_sdk
242
+
243
+ This SDK mirrors the `claude_agent_sdk` API. Migration requires renaming imports:
244
+
245
+ ```python
246
+ # Before (claude_agent_sdk)
247
+ from claude_agent_sdk import (
248
+ ClaudeAgentOptions, ClaudeSDKClient, AssistantMessage,
249
+ ResultMessage, SystemMessage, TextBlock, ToolUseBlock, HookMatcher,
250
+ )
251
+ from claude_agent_sdk._errors import ProcessError
252
+
253
+ # After (opencode_agent_sdk)
254
+ from opencode_agent_sdk import (
255
+ AgentOptions, SDKClient, AssistantMessage,
256
+ ResultMessage, SystemMessage, TextBlock, ToolUseBlock, HookMatcher,
257
+ )
258
+ from opencode_agent_sdk._errors import ProcessError
259
+ ```
260
+
261
+ All method calls, message types, hooks, and tool decorators stay the same. Only the class names change:
262
+
263
+ | claude_agent_sdk | opencode_agent_sdk |
264
+ |------------------|-------------------|
265
+ | `ClaudeSDKClient` | `SDKClient` |
266
+ | `ClaudeAgentOptions` | `AgentOptions` |
267
+
268
+ ## Demo: End-to-End Walkthrough
269
+
270
+ A full working demo that connects to `opencode serve`, sends a prompt to clone a GitHub repo, and streams the LLM response back through the SDK.
271
+
272
+ ### 1. Configure API keys
273
+
274
+ Create a `.env` file in the project root with your provider key:
275
+
276
+ ```bash
277
+ ANTHROPIC_API_KEY=sk-ant-...
278
+ ```
279
+
280
+ ### 2. Start the server
281
+
282
+ ```bash
283
+ docker compose up -d opencode
284
+ ```
285
+
286
+ This builds and starts the `opencode serve` container on port 54321.
287
+
288
+ ### 3. Install dependencies
289
+
290
+ ```bash
291
+ uv sync
292
+ ```
293
+
294
+ ### 4. Run the E2E demo
295
+
296
+ ```bash
297
+ uv run python scripts/e2e_test.py
298
+ ```
299
+
300
+ ### Expected output
301
+
302
+ ```
303
+ ============================================================
304
+ E2E Test: Clone repo & explain project
305
+ ============================================================
306
+ Server: http://127.0.0.1:54321
307
+
308
+ [*] Connecting ...
309
+ [*] Connected.
310
+
311
+ [>] Prompt:
312
+ Clone the repo https://github.com/dingkwang/opencode-agent-sdk-python and then
313
+ explain what the project does. Give a concise summary of its purpose,
314
+ architecture, and key components.
315
+
316
+ ------------------------------------------------------------
317
+
318
+ [system:init]
319
+ [system:step_start]
320
+
321
+ [assistant]
322
+ ## opencode-agent-sdk-python
323
+ ### Purpose
324
+ An open-source Python SDK that serves as a drop-in replacement for Anthropic's
325
+ proprietary `claude_agent_sdk`. It delegates all LLM work to OpenCode — an
326
+ open-source headless server that supports any provider ...
327
+ ...
328
+
329
+ ============================================================
330
+ [result] session = ses_...
331
+ cost = $0.024723
332
+ turns = 1
333
+ is_error = False
334
+ ============================================================
335
+
336
+ [*] Message counts: {'system': 2, 'assistant': 1, 'result': 1}
337
+ [*] E2E test complete.
338
+ ```
339
+
340
+ ### What's happening
341
+
342
+ 1. `SDKClient` creates an HTTP session against `opencode serve`
343
+ 2. `query()` sends the user prompt via `POST /session/{id}/message`
344
+ 3. `receive_response()` yields typed messages: `SystemMessage` (init, step events), `AssistantMessage` (LLM text/tool calls), and `ResultMessage` (cost, session ID, turn count)
345
+ 4. `disconnect()` cleans up the session
346
+
347
+ ### Customizing the demo
348
+
349
+ Set a custom server URL via environment variable:
350
+
351
+ ```bash
352
+ OPENCODE_SERVER_URL=http://your-host:54321 uv run python scripts/e2e_test.py
353
+ ```
354
+
355
+ ## Running with Docker
356
+
357
+ ```bash
358
+ # Start opencode serve
359
+ docker compose up -d
360
+
361
+ # Run the integration test
362
+ docker compose run --rm test
363
+ ```
364
+
365
+ The Docker setup uses `opencode-ai` v1.2.6 and exposes the REST API on port 54321. Pass provider API keys via `.env` (e.g. `ANTHROPIC_API_KEY`).
366
+
367
+ ## Development
368
+
369
+ ```bash
370
+ # Install dependencies
371
+ uv sync
372
+
373
+ # Run tests
374
+ uv run pytest
375
+
376
+ # Run demo against a running opencode serve
377
+ uv run python scripts/opencode_ai_demo.py
378
+
379
+ # Interactive multi-turn chat
380
+ uv run python scripts/chat.py
381
+ ```
382
+
383
+ ## License
384
+
385
+ MIT
@@ -0,0 +1,13 @@
1
+ opencode_agent_sdk/__init__.py,sha256=JYWf2ZgML1Msnj_GNANh53HZrKSBUbjGQCycdHqV_DY,572
2
+ opencode_agent_sdk/_errors.py,sha256=iP-goiRAM1gRIoDm503jQkK0HP0SE3p00R4hljhI0js,281
3
+ opencode_agent_sdk/client.py,sha256=wko7SHiLDd23390pht7KSV1qhj31sxwxLVcOXgmJZkI,7686
4
+ opencode_agent_sdk/tools.py,sha256=x3mCCjSH1wxn6DQAL02icyOU8i5RPH9g1qiaqSwPjwA,2297
5
+ opencode_agent_sdk/types.py,sha256=a2yTnCPQu6w1SfDUcHNgrphhSBzqlmwWRDboST4ZwQI,979
6
+ opencode_agent_sdk/_internal/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ opencode_agent_sdk/_internal/acp.py,sha256=DpZfMtTEXLCClutp5H5SFXdJV3UbFm8MJUs6-BCO5EI,13466
8
+ opencode_agent_sdk/_internal/http_transport.py,sha256=ACM4I9-57wizjCt9jvgOkpzorS33j46q1CC0d6muDMo,13038
9
+ opencode_agent_sdk/_internal/transport.py,sha256=YNd3WepNpLpWsydNJ4hX8yukvSCXf4B-F8fBKy6LJcE,3403
10
+ opencode_agent_sdk-0.2.0.dist-info/METADATA,sha256=yae6Gz5uxnIBE2hvxQSiVtgcDAXP-7BGGqX0SuTcT_Y,10535
11
+ opencode_agent_sdk-0.2.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
12
+ opencode_agent_sdk-0.2.0.dist-info/top_level.txt,sha256=pAgKlVvKFtrUjAqIt0OquHRVQZ7LfEE0CIyiLjrWM74,19
13
+ opencode_agent_sdk-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ opencode_agent_sdk