opencode-python-sdk 0.0.1.dev1__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.
Files changed (62) hide show
  1. opencode/.generated.json +39 -0
  2. opencode/__init__.py +221 -0
  3. opencode/_base_client.py +539 -0
  4. opencode/_client.py +255 -0
  5. opencode/_constants.py +27 -0
  6. opencode/_errors.py +547 -0
  7. opencode/_events.py +732 -0
  8. opencode/_exceptions.py +271 -0
  9. opencode/_models.py +54 -0
  10. opencode/_pagination.py +192 -0
  11. opencode/_pty.py +586 -0
  12. opencode/_qs.py +95 -0
  13. opencode/_resource.py +42 -0
  14. opencode/_rpc.py +489 -0
  15. opencode/_server.py +335 -0
  16. opencode/_streaming.py +248 -0
  17. opencode/_types.py +77 -0
  18. opencode/_unions.py +46 -0
  19. opencode/_version.py +8 -0
  20. opencode/lib/__init__.py +5 -0
  21. opencode/lib/_ready.py +91 -0
  22. opencode/lib/_session.py +231 -0
  23. opencode/py.typed +0 -0
  24. opencode/resources/__init__.py +364 -0
  25. opencode/resources/agent.py +179 -0
  26. opencode/resources/command.py +100 -0
  27. opencode/resources/config.py +240 -0
  28. opencode/resources/credential.py +384 -0
  29. opencode/resources/debug.py +178 -0
  30. opencode/resources/event.py +88 -0
  31. opencode/resources/experimental.py +765 -0
  32. opencode/resources/file.py +355 -0
  33. opencode/resources/form.py +100 -0
  34. opencode/resources/generate.py +110 -0
  35. opencode/resources/integration.py +1033 -0
  36. opencode/resources/location.py +162 -0
  37. opencode/resources/mcp.py +492 -0
  38. opencode/resources/message.py +152 -0
  39. opencode/resources/migration.py +118 -0
  40. opencode/resources/model.py +177 -0
  41. opencode/resources/permission.py +704 -0
  42. opencode/resources/plugin.py +250 -0
  43. opencode/resources/project.py +187 -0
  44. opencode/resources/provider.py +183 -0
  45. opencode/resources/pty.py +522 -0
  46. opencode/resources/reference.py +100 -0
  47. opencode/resources/rpc.py +113 -0
  48. opencode/resources/server.py +230 -0
  49. opencode/resources/session.py +3567 -0
  50. opencode/resources/shell.py +412 -0
  51. opencode/resources/skill.py +100 -0
  52. opencode/resources/vcs.py +500 -0
  53. opencode/resources/voice.py +113 -0
  54. opencode/resources/websearch.py +184 -0
  55. opencode/resources/worktree.py +336 -0
  56. opencode/testing.py +508 -0
  57. opencode/types/__init__.py +1462 -0
  58. opencode/types/_generated.py +8540 -0
  59. opencode_python_sdk-0.0.1.dev1.dist-info/METADATA +468 -0
  60. opencode_python_sdk-0.0.1.dev1.dist-info/RECORD +62 -0
  61. opencode_python_sdk-0.0.1.dev1.dist-info/WHEEL +4 -0
  62. opencode_python_sdk-0.0.1.dev1.dist-info/licenses/LICENSE +21 -0
opencode/_unions.py ADDED
@@ -0,0 +1,46 @@
1
+ """Runtime support for discriminated unions in the generated models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable, Iterable
10
+
11
+ __all__ = ["UNKNOWN_TAG", "discriminator"]
12
+
13
+ UNKNOWN_TAG = "__unknown__"
14
+ """Tag of the fallback member that receives variants this client version does not know."""
15
+
16
+
17
+ def discriminator(
18
+ key: str,
19
+ attribute: str,
20
+ tags: Iterable[str],
21
+ patterns: Iterable[tuple[str, str]] = (),
22
+ ) -> Callable[[Any], str]:
23
+ """Builds a ``pydantic.Discriminator`` function.
24
+
25
+ ``key`` is the wire field and ``attribute`` the model attribute that hold the tag. The tag selects a member by
26
+ exact value, then by regular expression. Anything else selects the ``UNKNOWN_TAG`` member, so a newer server can
27
+ add variants without breaking older clients.
28
+ """
29
+ known = frozenset(tags)
30
+ compiled = [(re.compile(pattern), member) for pattern, member in patterns]
31
+
32
+ def select(value: Any) -> str:
33
+ tag: object = (
34
+ cast("dict[str, Any]", value).get(key) if isinstance(value, dict) else getattr(value, attribute, None)
35
+ )
36
+ if isinstance(tag, bool):
37
+ tag = "true" if tag else "false"
38
+ if isinstance(tag, str):
39
+ if tag in known:
40
+ return tag
41
+ for pattern, member in compiled:
42
+ if pattern.search(tag):
43
+ return member
44
+ return UNKNOWN_TAG
45
+
46
+ return select
opencode/_version.py ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("opencode-python-sdk")
7
+ except PackageNotFoundError: # running from a source tree that is not installed
8
+ __version__ = "0.0.0"
@@ -0,0 +1,5 @@
1
+ """Hand-written helpers layered over the generated client."""
2
+
3
+ from ._session import RunResult
4
+
5
+ __all__ = ["RunResult"]
opencode/lib/_ready.py ADDED
@@ -0,0 +1,91 @@
1
+ """``client.wait_until_ready``: poll until agents and MCP servers have loaded.
2
+
3
+ OpenCode loads agents and connects MCP servers in the background after it starts listening, so a freshly booted
4
+ server can answer requests before the agent a caller needs exists.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from typing import TYPE_CHECKING
11
+
12
+ import anyio
13
+
14
+ from .._exceptions import APIConnectionError, ServiceUnavailableError
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterable
18
+
19
+ from .._client import AsyncOpencode, Opencode
20
+ from ..types import AgentListResponse, McpListResponse
21
+
22
+ __all__ = ["POLL_INTERVAL", "async_wait_until_ready", "wait_until_ready"]
23
+
24
+ POLL_INTERVAL = 0.25
25
+ """Seconds between polls."""
26
+
27
+ _TRANSIENT = (APIConnectionError, ServiceUnavailableError)
28
+
29
+
30
+ def _missing(
31
+ agents: frozenset[str],
32
+ mcp_servers: frozenset[str],
33
+ agent_list: AgentListResponse | None,
34
+ mcp_list: McpListResponse | None,
35
+ ) -> list[str]:
36
+ missing: list[str] = []
37
+ if agents:
38
+ present = {agent.id for agent in agent_list.data} if agent_list is not None else set[str]()
39
+ missing += [f"agent {name!r}" for name in sorted(agents - present)]
40
+ if mcp_servers:
41
+ connected = (
42
+ {server.name for server in mcp_list.data if server.status.status == "connected"}
43
+ if mcp_list is not None
44
+ else set[str]()
45
+ )
46
+ missing += [f"MCP server {name!r}" for name in sorted(mcp_servers - connected)]
47
+ return missing
48
+
49
+
50
+ def wait_until_ready(
51
+ client: Opencode, *, agents: Iterable[str] = (), mcp_servers: Iterable[str] = (), timeout: float = 60
52
+ ) -> None:
53
+ wanted_agents, wanted_mcp = frozenset(agents), frozenset(mcp_servers)
54
+ deadline = time.monotonic() + timeout
55
+ while True:
56
+ agent_list = mcp_list = None
57
+ missing = ["server"]
58
+ try:
59
+ client.server.info()
60
+ agent_list = client.agent.list() if wanted_agents else None
61
+ mcp_list = client.mcp.list() if wanted_mcp else None
62
+ missing = _missing(wanted_agents, wanted_mcp, agent_list, mcp_list)
63
+ except _TRANSIENT:
64
+ pass
65
+ if not missing:
66
+ return
67
+ if time.monotonic() >= deadline:
68
+ raise TimeoutError(f"OpenCode not ready after {timeout}s; still waiting for {', '.join(missing)}")
69
+ time.sleep(POLL_INTERVAL)
70
+
71
+
72
+ async def async_wait_until_ready(
73
+ client: AsyncOpencode, *, agents: Iterable[str] = (), mcp_servers: Iterable[str] = (), timeout: float = 60
74
+ ) -> None:
75
+ wanted_agents, wanted_mcp = frozenset(agents), frozenset(mcp_servers)
76
+ deadline = time.monotonic() + timeout
77
+ while True:
78
+ agent_list = mcp_list = None
79
+ missing = ["server"]
80
+ try:
81
+ await client.server.info()
82
+ agent_list = await client.agent.list() if wanted_agents else None
83
+ mcp_list = await client.mcp.list() if wanted_mcp else None
84
+ missing = _missing(wanted_agents, wanted_mcp, agent_list, mcp_list)
85
+ except _TRANSIENT:
86
+ pass
87
+ if not missing:
88
+ return
89
+ if time.monotonic() >= deadline:
90
+ raise TimeoutError(f"OpenCode not ready after {timeout}s; still waiting for {', '.join(missing)}")
91
+ await anyio.sleep(POLL_INTERVAL)
@@ -0,0 +1,231 @@
1
+ """``client.session.run``: create or reuse a session, prompt it, wait until idle, and collect the result.
2
+
3
+ The generated ``SessionResource`` / ``AsyncSessionResource`` inherit from the classes here (see
4
+ ``codegen/render.py``, ``HELPER_BASES``), so these methods sit next to the generated ones.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ from dataclasses import dataclass, field
11
+ from typing import TYPE_CHECKING, cast
12
+
13
+ import anyio
14
+
15
+ from .._resource import AsyncAPIResource, SyncAPIResource
16
+ from .._types import NOT_GIVEN, NotGiven, Timeout
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Sequence
20
+
21
+ from ..resources.session import AsyncSessionResource, SessionResource
22
+ from ..types import (
23
+ AgentID,
24
+ ModelRef,
25
+ PromptInputFileAttachment,
26
+ SessionID,
27
+ SessionInfo,
28
+ SessionMessageAssistant,
29
+ SessionMessageInfo,
30
+ SessionStructuredError,
31
+ TokenUsageInfo,
32
+ )
33
+
34
+ __all__ = ["AsyncSessionHelpers", "RunResult", "SessionHelpers"]
35
+
36
+
37
+ @dataclass
38
+ class RunResult:
39
+ """What :meth:`SessionHelpers.run` returns."""
40
+
41
+ session: SessionInfo
42
+ """The session as it was after the run. ``session.cost`` and ``session.tokens`` cover the whole session."""
43
+ messages: list[SessionMessageInfo] = field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
44
+ """Messages this run added, oldest first, starting with the prompt."""
45
+
46
+ @property
47
+ def assistant_messages(self) -> list[SessionMessageAssistant]:
48
+ """The assistant messages of this run, oldest first."""
49
+ from ..types import SessionMessageAssistant
50
+
51
+ return [message for message in self.messages if isinstance(message, SessionMessageAssistant)]
52
+
53
+ @property
54
+ def text(self) -> str:
55
+ """Text of the last assistant message, or ``""`` when the agent produced none."""
56
+ from ..types import SessionMessageAssistantText
57
+
58
+ for message in reversed(self.assistant_messages):
59
+ parts = [item.text for item in message.content if isinstance(item, SessionMessageAssistantText)]
60
+ if parts:
61
+ return "".join(parts)
62
+ return ""
63
+
64
+ @property
65
+ def outcome(self) -> str | None:
66
+ """``succeeded``, ``failed`` or ``interrupted``, from the run's idle marker or the session."""
67
+ from ..types import SessionMessageIdle
68
+
69
+ for message in reversed(self.messages):
70
+ if isinstance(message, SessionMessageIdle):
71
+ return message.outcome
72
+ return self.session.outcome
73
+
74
+ @property
75
+ def error(self) -> SessionStructuredError | None:
76
+ """Why the run failed, from its idle marker or last assistant message, or ``None``."""
77
+ from ..types import SessionMessageAssistant, SessionMessageIdle
78
+
79
+ for message in reversed(self.messages):
80
+ if isinstance(message, (SessionMessageIdle, SessionMessageAssistant)) and message.error is not None:
81
+ return message.error
82
+ return None
83
+
84
+ @property
85
+ def cost(self) -> float:
86
+ """Cost of this run in USD, summed over its assistant messages."""
87
+ return sum(message.cost or 0.0 for message in self.assistant_messages)
88
+
89
+ @property
90
+ def tokens(self) -> TokenUsageInfo:
91
+ """Token usage of this run, summed over its assistant messages."""
92
+ from ..types import TokenUsageInfo, TokenUsageInfoCache
93
+
94
+ total = TokenUsageInfo(input=0, output=0, reasoning=0, cache=TokenUsageInfoCache(read=0, write=0))
95
+ for message in self.assistant_messages:
96
+ usage = message.tokens
97
+ if usage is None:
98
+ continue
99
+ total.input += usage.input
100
+ total.output += usage.output
101
+ total.reasoning += usage.reasoning
102
+ total.cache.read += usage.cache.read
103
+ total.cache.write += usage.cache.write
104
+ return total
105
+
106
+
107
+ def _new_messages(newest_first: list[SessionMessageInfo], marker: str | None) -> list[SessionMessageInfo]:
108
+ from ..types import SessionMessageUser
109
+
110
+ collected: list[SessionMessageInfo] = []
111
+ for message in newest_first:
112
+ if marker is not None and message.id == marker:
113
+ break
114
+ collected.append(message)
115
+ collected.reverse()
116
+ # Drop anything that was already queued before our prompt.
117
+ for index, message in enumerate(collected):
118
+ if isinstance(message, SessionMessageUser):
119
+ return collected[index:]
120
+ return collected
121
+
122
+
123
+ class SessionHelpers(SyncAPIResource):
124
+ """Hand-written helpers on ``client.session``."""
125
+
126
+ def run(
127
+ self,
128
+ text: str,
129
+ *,
130
+ agent: AgentID | None = None,
131
+ model: ModelRef | None = None,
132
+ files: Sequence[PromptInputFileAttachment] | NotGiven = NOT_GIVEN,
133
+ session_id: SessionID | None = None,
134
+ timeout: Timeout = None,
135
+ ) -> RunResult:
136
+ """Runs one prompt to completion and returns what the agent produced.
137
+
138
+ Creates a session with ``agent`` and ``model`` (or reuses ``session_id``, switching its agent and model when
139
+ given), sends ``text``, blocks until the session is idle, and reads back the new messages and the session's
140
+ usage.
141
+
142
+ If the wait is interrupted (``KeyboardInterrupt``, a timeout, or any other error), the session is
143
+ interrupted on the server before the error propagates, so no agent keeps running unattended.
144
+
145
+ Args:
146
+ text: The prompt.
147
+ agent: Agent for a new session, or to switch an existing one to.
148
+ model: Model for a new session, or to switch an existing one to. Set it with ``agent`` so the session
149
+ does not start on another model.
150
+ files: File attachments; images can be ``data:`` URLs.
151
+ session_id: Continue this session instead of creating one.
152
+ timeout: Seconds to wait for the session to become idle. ``None`` waits forever.
153
+
154
+ Example::
155
+
156
+ result = client.session.run("Summarize README.md", agent="build")
157
+ print(result.text, result.cost)
158
+ """
159
+ resource = cast("SessionResource", self)
160
+ if session_id is None:
161
+ session = resource.create(agent=agent or NOT_GIVEN, model=model or NOT_GIVEN)
162
+ session_id = session.id
163
+ marker = None
164
+ else:
165
+ if agent is not None:
166
+ resource.switch_agent(session_id, agent=agent)
167
+ if model is not None:
168
+ resource.switch_model(session_id, model=model)
169
+ latest = self._client.message.list(session_id, order="desc", limit=1).data
170
+ marker = latest[0].id if latest else None
171
+
172
+ resource.prompt(session_id, text=text, files=files)
173
+ try:
174
+ resource.wait(session_id, timeout=timeout)
175
+ except BaseException:
176
+ with contextlib.suppress(Exception):
177
+ resource.interrupt(session_id)
178
+ raise
179
+
180
+ newest_first: list[SessionMessageInfo] = []
181
+ for message in self._client.message.list(session_id, order="desc"):
182
+ if marker is not None and message.id == marker:
183
+ break
184
+ newest_first.append(message)
185
+ return RunResult(session=resource.get(session_id), messages=_new_messages(newest_first, marker))
186
+
187
+
188
+ class AsyncSessionHelpers(AsyncAPIResource):
189
+ """Hand-written helpers on ``client.session`` of :class:`opencode.AsyncOpencode`."""
190
+
191
+ async def run(
192
+ self,
193
+ text: str,
194
+ *,
195
+ agent: AgentID | None = None,
196
+ model: ModelRef | None = None,
197
+ files: Sequence[PromptInputFileAttachment] | NotGiven = NOT_GIVEN,
198
+ session_id: SessionID | None = None,
199
+ timeout: Timeout = None,
200
+ ) -> RunResult:
201
+ """Runs one prompt to completion. See :meth:`SessionHelpers.run`.
202
+
203
+ Cancelling the task interrupts the session on the server before the cancellation propagates.
204
+ """
205
+ resource = cast("AsyncSessionResource", self)
206
+ if session_id is None:
207
+ session = await resource.create(agent=agent or NOT_GIVEN, model=model or NOT_GIVEN)
208
+ session_id = session.id
209
+ marker = None
210
+ else:
211
+ if agent is not None:
212
+ await resource.switch_agent(session_id, agent=agent)
213
+ if model is not None:
214
+ await resource.switch_model(session_id, model=model)
215
+ latest = (await self._client.message.list(session_id, order="desc", limit=1)).data
216
+ marker = latest[0].id if latest else None
217
+
218
+ await resource.prompt(session_id, text=text, files=files)
219
+ try:
220
+ await resource.wait(session_id, timeout=timeout)
221
+ except BaseException:
222
+ with anyio.CancelScope(shield=True), contextlib.suppress(Exception):
223
+ await resource.interrupt(session_id)
224
+ raise
225
+
226
+ newest_first: list[SessionMessageInfo] = []
227
+ async for message in self._client.message.list(session_id, order="desc"):
228
+ if marker is not None and message.id == marker:
229
+ break
230
+ newest_first.append(message)
231
+ return RunResult(session=await resource.get(session_id), messages=_new_messages(newest_first, marker))
opencode/py.typed ADDED
File without changes