opencode-agent-sdk 0.4.6__tar.gz → 0.4.8__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 (26) hide show
  1. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/PKG-INFO +7 -3
  2. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/README.md +6 -2
  3. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/pyproject.toml +1 -1
  4. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/__init__.py +1 -1
  5. opencode_agent_sdk-0.4.8/src/opencode_agent_sdk/_errors.py +29 -0
  6. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/_internal/acp.py +19 -4
  7. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/_internal/http_transport.py +9 -5
  8. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/types.py +10 -1
  9. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk.egg-info/PKG-INFO +7 -3
  10. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk.egg-info/SOURCES.txt +2 -1
  11. opencode_agent_sdk-0.4.8/tests/test_types.py +22 -0
  12. opencode_agent_sdk-0.4.6/src/opencode_agent_sdk/_errors.py +0 -9
  13. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/setup.cfg +0 -0
  14. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/_internal/__init__.py +0 -0
  15. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/_internal/transport.py +0 -0
  16. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/_mcp_bridge.py +0 -0
  17. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/client.py +0 -0
  18. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/model_registry.py +0 -0
  19. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk/tools.py +0 -0
  20. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk.egg-info/dependency_links.txt +0 -0
  21. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk.egg-info/requires.txt +0 -0
  22. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/src/opencode_agent_sdk.egg-info/top_level.txt +0 -0
  23. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/tests/test_common.py +0 -0
  24. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/tests/test_model_registry.py +0 -0
  25. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/tests/test_opencode_agent.py +0 -0
  26. {opencode_agent_sdk-0.4.6 → opencode_agent_sdk-0.4.8}/tests/test_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-agent-sdk
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: Open-source Agent SDK backed by OpenCode ACP (drop-in replacement for claude_agent_sdk)
5
5
  Author: OpenCode
6
6
  License: MIT
@@ -71,9 +71,13 @@ async def main():
71
71
  asyncio.run(main())
72
72
  ```
73
73
 
74
- ## SDKClient
74
+ ## Why OpenCode Agent SDK?
75
75
 
76
- `SDKClient` supports bidirectional conversations with an LLM via OpenCode. It works in two transport modes:
76
+ - **Drop-in Replacement**: Seamlessly switch from `claude_agent_sdk` by just changing your imports.
77
+ - **Open Source & Headless**: Take full control of your agent infrastructure. No more proprietary black boxes.
78
+ - **Multi-Provider Support**: Use any LLM (Anthropic, OpenAI, xAI, Google, Locall) via OpenCode's backend.
79
+ - **Native SSE Streaming**: Real-time response streaming for a better user experience.
80
+ - **Advanced Control**: Fine-grained tool permission hooks and MCP server support builtin.
77
81
 
78
82
  - **HTTP mode** — communicates with a running `opencode serve` instance over REST
79
83
  - **Subprocess mode** — spawns `opencode acp` locally over stdio JSON-RPC
@@ -47,9 +47,13 @@ async def main():
47
47
  asyncio.run(main())
48
48
  ```
49
49
 
50
- ## SDKClient
50
+ ## Why OpenCode Agent SDK?
51
51
 
52
- `SDKClient` supports bidirectional conversations with an LLM via OpenCode. It works in two transport modes:
52
+ - **Drop-in Replacement**: Seamlessly switch from `claude_agent_sdk` by just changing your imports.
53
+ - **Open Source & Headless**: Take full control of your agent infrastructure. No more proprietary black boxes.
54
+ - **Multi-Provider Support**: Use any LLM (Anthropic, OpenAI, xAI, Google, Locall) via OpenCode's backend.
55
+ - **Native SSE Streaming**: Real-time response streaming for a better user experience.
56
+ - **Advanced Control**: Fine-grained tool permission hooks and MCP server support builtin.
53
57
 
54
58
  - **HTTP mode** — communicates with a running `opencode serve` instance over REST
55
59
  - **Subprocess mode** — spawns `opencode acp` locally over stdio JSON-RPC
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "opencode-agent-sdk"
7
- version = "0.4.6"
7
+ version = "0.4.8"
8
8
  description = "Open-source Agent SDK backed by OpenCode ACP (drop-in replacement for claude_agent_sdk)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -31,4 +31,4 @@ __all__ = [
31
31
  "tool",
32
32
  ]
33
33
 
34
- __version__ = "0.2.0"
34
+ __version__ = "0.4.8"
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class SDKError(Exception):
5
+ """Base exception for all OpenCode Agent SDK errors."""
6
+ pass
7
+
8
+
9
+ class ProcessError(SDKError):
10
+ """Raised when the opencode subprocess exits with an error or fails to start."""
11
+ def __init__(self, message: str, exit_code: int | None = None) -> None:
12
+ super().__init__(message)
13
+ self.exit_code = exit_code
14
+
15
+
16
+ class TransportError(SDKError):
17
+ """Raised when there is a communication error with the OpenCode server."""
18
+ pass
19
+
20
+
21
+ class SessionError(SDKError):
22
+ """Raised when a session-related error occurs (e.g., missing session, expiration)."""
23
+ pass
24
+
25
+
26
+ class ToolError(SDKError):
27
+ """Raised when a tool execution hook fails or returns an error."""
28
+ pass
29
+
@@ -341,15 +341,28 @@ class ACPSession:
341
341
  text = content.get("text", "")
342
342
  if text:
343
343
  self._text_buffer += text
344
+ # Flush immediately so consumers can stream text incrementally
345
+ yield AssistantMessage(
346
+ content=[TextBlock(text=self._text_buffer)]
347
+ )
348
+ self._text_buffer = ""
344
349
 
345
350
  elif update_type == "tool_call":
346
351
  tool_call_id = session_update.get("toolCallId", "")
352
+ tool_name = session_update.get("title", "")
347
353
  self._tool_calls[tool_call_id] = {
348
354
  "id": tool_call_id,
349
- "name": session_update.get("title", ""),
355
+ "name": tool_name,
350
356
  "input": session_update.get("rawInput", {}),
351
357
  "status": session_update.get("status", "pending"),
352
358
  }
359
+ # Flush text buffer before tool starts so consumers see
360
+ # accumulated text immediately rather than after tool completes
361
+ if self._text_buffer:
362
+ yield AssistantMessage(
363
+ content=[TextBlock(text=self._text_buffer)]
364
+ )
365
+ self._text_buffer = ""
353
366
 
354
367
  elif update_type == "tool_call_update":
355
368
  tool_call_id = session_update.get("toolCallId", "")
@@ -396,10 +409,12 @@ class ACPSession:
396
409
  )
397
410
 
398
411
  elif update_type == "agent_thought_chunk":
412
+ # Yield thinking text as AssistantMessage so consumers can
413
+ # stream it without needing to handle SystemMessage separately.
414
+ # Kept separate from _text_buffer to avoid mixing with response text.
399
415
  content = session_update.get("content", {})
400
416
  text = content.get("text", "")
401
417
  if text:
402
- yield SystemMessage(
403
- subtype="thought",
404
- data={"text": text},
418
+ yield AssistantMessage(
419
+ content=[TextBlock(text=text)]
405
420
  )
@@ -254,7 +254,7 @@ class HTTPTransport:
254
254
  data={
255
255
  "tool_name": tool_name,
256
256
  "tool_id": part.get("callID", part_id),
257
- "error": str(state),
257
+ "error": str(state.get("error", state)),
258
258
  },
259
259
  )
260
260
 
@@ -263,11 +263,15 @@ class HTTPTransport:
263
263
 
264
264
  elif part_type == "step-finish":
265
265
  tokens = part.get("tokens", {})
266
+ from ..types import Usage
267
+ usage = Usage(
268
+ input_tokens=int(tokens.get("input", 0)),
269
+ output_tokens=int(tokens.get("output", 0)),
270
+ cache_creation_input_tokens=tokens.get("cache_creation_input_tokens"),
271
+ cache_read_input_tokens=tokens.get("cache_read_input_tokens"),
272
+ )
266
273
  return ResultMessage(
267
- usage={
268
- "input_tokens": int(tokens.get("input", 0)),
269
- "output_tokens": int(tokens.get("output", 0)),
270
- },
274
+ usage=usage,
271
275
  total_cost_usd=part.get("cost", 0.0),
272
276
  session_id=part.get("sessionID", self._session_id),
273
277
  duration_ms=0.0,
@@ -4,6 +4,15 @@ from dataclasses import dataclass, field
4
4
  from typing import Any, Callable
5
5
 
6
6
 
7
+ @dataclass
8
+ class Usage:
9
+ """Usage stats for a response."""
10
+ input_tokens: int = 0
11
+ output_tokens: int = 0
12
+ cache_creation_input_tokens: int | None = None
13
+ cache_read_input_tokens: int | None = None
14
+
15
+
7
16
  @dataclass
8
17
  class TextBlock:
9
18
  text: str
@@ -26,7 +35,7 @@ class AssistantMessage:
26
35
 
27
36
  @dataclass
28
37
  class ResultMessage:
29
- usage: dict[str, Any] = field(default_factory=dict)
38
+ usage: Usage = field(default_factory=Usage)
30
39
  total_cost_usd: float = 0.0
31
40
  session_id: str = ""
32
41
  duration_ms: float = 0.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-agent-sdk
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: Open-source Agent SDK backed by OpenCode ACP (drop-in replacement for claude_agent_sdk)
5
5
  Author: OpenCode
6
6
  License: MIT
@@ -71,9 +71,13 @@ async def main():
71
71
  asyncio.run(main())
72
72
  ```
73
73
 
74
- ## SDKClient
74
+ ## Why OpenCode Agent SDK?
75
75
 
76
- `SDKClient` supports bidirectional conversations with an LLM via OpenCode. It works in two transport modes:
76
+ - **Drop-in Replacement**: Seamlessly switch from `claude_agent_sdk` by just changing your imports.
77
+ - **Open Source & Headless**: Take full control of your agent infrastructure. No more proprietary black boxes.
78
+ - **Multi-Provider Support**: Use any LLM (Anthropic, OpenAI, xAI, Google, Locall) via OpenCode's backend.
79
+ - **Native SSE Streaming**: Real-time response streaming for a better user experience.
80
+ - **Advanced Control**: Fine-grained tool permission hooks and MCP server support builtin.
77
81
 
78
82
  - **HTTP mode** — communicates with a running `opencode serve` instance over REST
79
83
  - **Subprocess mode** — spawns `opencode acp` locally over stdio JSON-RPC
@@ -19,4 +19,5 @@ src/opencode_agent_sdk/_internal/transport.py
19
19
  tests/test_common.py
20
20
  tests/test_model_registry.py
21
21
  tests/test_opencode_agent.py
22
- tests/test_runner.py
22
+ tests/test_runner.py
23
+ tests/test_types.py
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+ import unittest
3
+ from opencode_agent_sdk.types import ResultMessage, Usage
4
+
5
+ class TestTypes(unittest.TestCase):
6
+ def test_result_message_usage_robustness(self):
7
+ # Testing the new Usage class integration
8
+ usage = Usage(input_tokens=10, output_tokens=20)
9
+ msg = ResultMessage(usage=usage, total_cost_usd=0.01)
10
+
11
+ self.assertEqual(msg.usage.input_tokens, 10)
12
+ self.assertEqual(msg.usage.output_tokens, 20)
13
+ self.assertEqual(msg.total_cost_usd, 0.01)
14
+
15
+ def test_usage_default_values(self):
16
+ usage = Usage()
17
+ self.assertEqual(usage.input_tokens, 0)
18
+ self.assertEqual(usage.output_tokens, 0)
19
+ self.assertIsNone(usage.cache_read_input_tokens)
20
+
21
+ if __name__ == "__main__":
22
+ unittest.main()
@@ -1,9 +0,0 @@
1
- from __future__ import annotations
2
-
3
-
4
- class ProcessError(Exception):
5
- """Raised when the opencode subprocess exits with an error."""
6
-
7
- def __init__(self, message: str, exit_code: int | None = None) -> None:
8
- super().__init__(message)
9
- self.exit_code = exit_code