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.
- opencode/.generated.json +39 -0
- opencode/__init__.py +221 -0
- opencode/_base_client.py +539 -0
- opencode/_client.py +255 -0
- opencode/_constants.py +27 -0
- opencode/_errors.py +547 -0
- opencode/_events.py +732 -0
- opencode/_exceptions.py +271 -0
- opencode/_models.py +54 -0
- opencode/_pagination.py +192 -0
- opencode/_pty.py +586 -0
- opencode/_qs.py +95 -0
- opencode/_resource.py +42 -0
- opencode/_rpc.py +489 -0
- opencode/_server.py +335 -0
- opencode/_streaming.py +248 -0
- opencode/_types.py +77 -0
- opencode/_unions.py +46 -0
- opencode/_version.py +8 -0
- opencode/lib/__init__.py +5 -0
- opencode/lib/_ready.py +91 -0
- opencode/lib/_session.py +231 -0
- opencode/py.typed +0 -0
- opencode/resources/__init__.py +364 -0
- opencode/resources/agent.py +179 -0
- opencode/resources/command.py +100 -0
- opencode/resources/config.py +240 -0
- opencode/resources/credential.py +384 -0
- opencode/resources/debug.py +178 -0
- opencode/resources/event.py +88 -0
- opencode/resources/experimental.py +765 -0
- opencode/resources/file.py +355 -0
- opencode/resources/form.py +100 -0
- opencode/resources/generate.py +110 -0
- opencode/resources/integration.py +1033 -0
- opencode/resources/location.py +162 -0
- opencode/resources/mcp.py +492 -0
- opencode/resources/message.py +152 -0
- opencode/resources/migration.py +118 -0
- opencode/resources/model.py +177 -0
- opencode/resources/permission.py +704 -0
- opencode/resources/plugin.py +250 -0
- opencode/resources/project.py +187 -0
- opencode/resources/provider.py +183 -0
- opencode/resources/pty.py +522 -0
- opencode/resources/reference.py +100 -0
- opencode/resources/rpc.py +113 -0
- opencode/resources/server.py +230 -0
- opencode/resources/session.py +3567 -0
- opencode/resources/shell.py +412 -0
- opencode/resources/skill.py +100 -0
- opencode/resources/vcs.py +500 -0
- opencode/resources/voice.py +113 -0
- opencode/resources/websearch.py +184 -0
- opencode/resources/worktree.py +336 -0
- opencode/testing.py +508 -0
- opencode/types/__init__.py +1462 -0
- opencode/types/_generated.py +8540 -0
- opencode_python_sdk-0.0.1.dev1.dist-info/METADATA +468 -0
- opencode_python_sdk-0.0.1.dev1.dist-info/RECORD +62 -0
- opencode_python_sdk-0.0.1.dev1.dist-info/WHEEL +4 -0
- 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
opencode/lib/__init__.py
ADDED
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)
|
opencode/lib/_session.py
ADDED
|
@@ -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
|