opencode-python-sdk 0.1.2__py3-none-any.whl → 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.
Files changed (46) hide show
  1. opencode/__init__.py +62 -1
  2. opencode/_base_client.py +24 -3
  3. opencode/_client.py +90 -2
  4. opencode/_events.py +732 -0
  5. opencode/_rpc.py +489 -0
  6. opencode/_streaming.py +22 -1
  7. opencode/lib/__init__.py +5 -0
  8. opencode/lib/_ready.py +91 -0
  9. opencode/lib/_session.py +231 -0
  10. opencode/resources/agent.py +13 -11
  11. opencode/resources/command.py +9 -7
  12. opencode/resources/config.py +12 -10
  13. opencode/resources/credential.py +13 -11
  14. opencode/resources/debug.py +9 -7
  15. opencode/resources/event.py +9 -5
  16. opencode/resources/experimental.py +31 -29
  17. opencode/resources/file.py +15 -13
  18. opencode/resources/form.py +9 -7
  19. opencode/resources/generate.py +9 -7
  20. opencode/resources/integration.py +28 -26
  21. opencode/resources/location.py +9 -7
  22. opencode/resources/mcp.py +13 -11
  23. opencode/resources/message.py +9 -8
  24. opencode/resources/migration.py +10 -12
  25. opencode/resources/model.py +12 -10
  26. opencode/resources/permission.py +29 -27
  27. opencode/resources/plugin.py +12 -10
  28. opencode/resources/project.py +14 -12
  29. opencode/resources/provider.py +13 -11
  30. opencode/resources/pty.py +23 -21
  31. opencode/resources/reference.py +9 -7
  32. opencode/resources/rpc.py +9 -7
  33. opencode/resources/server.py +14 -12
  34. opencode/resources/session.py +92 -90
  35. opencode/resources/shell.py +19 -17
  36. opencode/resources/skill.py +9 -7
  37. opencode/resources/vcs.py +22 -20
  38. opencode/resources/voice.py +9 -7
  39. opencode/resources/websearch.py +13 -11
  40. opencode/resources/worktree.py +13 -11
  41. opencode/testing.py +508 -0
  42. {opencode_python_sdk-0.1.2.dist-info → opencode_python_sdk-0.2.0.dist-info}/METADATA +126 -1
  43. opencode_python_sdk-0.2.0.dist-info/RECORD +62 -0
  44. opencode_python_sdk-0.1.2.dist-info/RECORD +0 -56
  45. {opencode_python_sdk-0.1.2.dist-info → opencode_python_sdk-0.2.0.dist-info}/WHEEL +0 -0
  46. {opencode_python_sdk-0.1.2.dist-info → opencode_python_sdk-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -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))
@@ -5,18 +5,20 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from typing import cast
8
+ from typing import TYPE_CHECKING, cast
9
9
 
10
10
  from .._base_client import RequestOptions
11
11
  from .._qs import encode_path_segment
12
12
  from .._resource import AsyncAPIResource, SyncAPIResource
13
13
  from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
14
- from ..types import (
15
- AgentGetResponse,
16
- AgentID,
17
- AgentListResponse,
18
- Location,
19
- )
14
+
15
+ if TYPE_CHECKING:
16
+ from ..types import (
17
+ AgentGetResponse,
18
+ AgentID,
19
+ AgentListResponse,
20
+ Location,
21
+ )
20
22
 
21
23
  __all__ = ["AgentResource", "AsyncAgentResource"]
22
24
 
@@ -56,7 +58,7 @@ class AgentResource(SyncAPIResource):
56
58
  extra_body=extra_body,
57
59
  timeout=timeout,
58
60
  ),
59
- response_type=AgentListResponse,
61
+ response_type="AgentListResponse",
60
62
  ),
61
63
  )
62
64
 
@@ -94,7 +96,7 @@ class AgentResource(SyncAPIResource):
94
96
  extra_body=extra_body,
95
97
  timeout=timeout,
96
98
  ),
97
- response_type=AgentGetResponse,
99
+ response_type="AgentGetResponse",
98
100
  ),
99
101
  )
100
102
 
@@ -134,7 +136,7 @@ class AsyncAgentResource(AsyncAPIResource):
134
136
  extra_body=extra_body,
135
137
  timeout=timeout,
136
138
  ),
137
- response_type=AgentListResponse,
139
+ response_type="AgentListResponse",
138
140
  ),
139
141
  )
140
142
 
@@ -172,6 +174,6 @@ class AsyncAgentResource(AsyncAPIResource):
172
174
  extra_body=extra_body,
173
175
  timeout=timeout,
174
176
  ),
175
- response_type=AgentGetResponse,
177
+ response_type="AgentGetResponse",
176
178
  ),
177
179
  )
@@ -5,15 +5,17 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from typing import cast
8
+ from typing import TYPE_CHECKING, cast
9
9
 
10
10
  from .._base_client import RequestOptions
11
11
  from .._resource import AsyncAPIResource, SyncAPIResource
12
12
  from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
13
- from ..types import (
14
- CommandListResponse,
15
- Location,
16
- )
13
+
14
+ if TYPE_CHECKING:
15
+ from ..types import (
16
+ CommandListResponse,
17
+ Location,
18
+ )
17
19
 
18
20
  __all__ = ["CommandResource", "AsyncCommandResource"]
19
21
 
@@ -53,7 +55,7 @@ class CommandResource(SyncAPIResource):
53
55
  extra_body=extra_body,
54
56
  timeout=timeout,
55
57
  ),
56
- response_type=CommandListResponse,
58
+ response_type="CommandListResponse",
57
59
  ),
58
60
  )
59
61
 
@@ -93,6 +95,6 @@ class AsyncCommandResource(AsyncAPIResource):
93
95
  extra_body=extra_body,
94
96
  timeout=timeout,
95
97
  ),
96
- response_type=CommandListResponse,
98
+ response_type="CommandListResponse",
97
99
  ),
98
100
  )
@@ -6,16 +6,18 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import builtins
9
- from typing import cast
9
+ from typing import TYPE_CHECKING, cast
10
10
 
11
11
  from .._base_client import RequestOptions
12
12
  from .._resource import AsyncAPIResource, SyncAPIResource
13
13
  from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
14
- from ..types import (
15
- ConfigEntry,
16
- ConfigShellOption,
17
- Location,
18
- )
14
+
15
+ if TYPE_CHECKING:
16
+ from ..types import (
17
+ ConfigEntry,
18
+ ConfigShellOption,
19
+ Location,
20
+ )
19
21
 
20
22
  __all__ = ["ConfigResource", "AsyncConfigResource"]
21
23
 
@@ -55,7 +57,7 @@ class ConfigResource(SyncAPIResource):
55
57
  extra_body=extra_body,
56
58
  timeout=timeout,
57
59
  ),
58
- response_type=builtins.list[ConfigEntry],
60
+ response_type="builtins.list[ConfigEntry]",
59
61
  ),
60
62
  )
61
63
 
@@ -89,7 +91,7 @@ class ConfigResource(SyncAPIResource):
89
91
  extra_body=extra_body,
90
92
  timeout=timeout,
91
93
  ),
92
- response_type=builtins.list[ConfigShellOption],
94
+ response_type="builtins.list[ConfigShellOption]",
93
95
  ),
94
96
  )
95
97
 
@@ -164,7 +166,7 @@ class AsyncConfigResource(AsyncAPIResource):
164
166
  extra_body=extra_body,
165
167
  timeout=timeout,
166
168
  ),
167
- response_type=builtins.list[ConfigEntry],
169
+ response_type="builtins.list[ConfigEntry]",
168
170
  ),
169
171
  )
170
172
 
@@ -198,7 +200,7 @@ class AsyncConfigResource(AsyncAPIResource):
198
200
  extra_body=extra_body,
199
201
  timeout=timeout,
200
202
  ),
201
- response_type=builtins.list[ConfigShellOption],
203
+ response_type="builtins.list[ConfigShellOption]",
202
204
  ),
203
205
  )
204
206
 
@@ -6,18 +6,20 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import builtins
9
- from typing import cast
9
+ from typing import TYPE_CHECKING, cast
10
10
 
11
11
  from .._base_client import RequestOptions
12
12
  from .._qs import encode_path_segment
13
13
  from .._resource import AsyncAPIResource, SyncAPIResource
14
14
  from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
15
- from ..types import (
16
- CredentialEntry,
17
- CredentialID,
18
- CredentialValue,
19
- IntegrationID,
20
- )
15
+
16
+ if TYPE_CHECKING:
17
+ from ..types import (
18
+ CredentialEntry,
19
+ CredentialID,
20
+ CredentialValue,
21
+ IntegrationID,
22
+ )
21
23
 
22
24
  __all__ = ["CredentialResource", "AsyncCredentialResource"]
23
25
 
@@ -54,7 +56,7 @@ class CredentialResource(SyncAPIResource):
54
56
  extra_body=extra_body,
55
57
  timeout=timeout,
56
58
  ),
57
- response_type=builtins.list[CredentialEntry],
59
+ response_type="builtins.list[CredentialEntry]",
58
60
  unwrap=True,
59
61
  ),
60
62
  )
@@ -101,7 +103,7 @@ class CredentialResource(SyncAPIResource):
101
103
  extra_body=extra_body,
102
104
  timeout=timeout,
103
105
  ),
104
- response_type=CredentialEntry,
106
+ response_type="CredentialEntry",
105
107
  unwrap=True,
106
108
  ),
107
109
  )
@@ -234,7 +236,7 @@ class AsyncCredentialResource(AsyncAPIResource):
234
236
  extra_body=extra_body,
235
237
  timeout=timeout,
236
238
  ),
237
- response_type=builtins.list[CredentialEntry],
239
+ response_type="builtins.list[CredentialEntry]",
238
240
  unwrap=True,
239
241
  ),
240
242
  )
@@ -281,7 +283,7 @@ class AsyncCredentialResource(AsyncAPIResource):
281
283
  extra_body=extra_body,
282
284
  timeout=timeout,
283
285
  ),
284
- response_type=CredentialEntry,
286
+ response_type="CredentialEntry",
285
287
  unwrap=True,
286
288
  ),
287
289
  )
@@ -7,15 +7,17 @@ from __future__ import annotations
7
7
 
8
8
  import builtins
9
9
  from functools import cached_property
10
- from typing import cast
10
+ from typing import TYPE_CHECKING, cast
11
11
 
12
12
  from .._base_client import RequestOptions
13
13
  from .._resource import AsyncAPIResource, SyncAPIResource
14
14
  from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
15
- from ..types import (
16
- Location,
17
- LocationPublicRef,
18
- )
15
+
16
+ if TYPE_CHECKING:
17
+ from ..types import (
18
+ Location,
19
+ LocationPublicRef,
20
+ )
19
21
 
20
22
  __all__ = ["DebugResource", "AsyncDebugResource"]
21
23
 
@@ -52,7 +54,7 @@ class DebugLocationResource(SyncAPIResource):
52
54
  extra_body=extra_body,
53
55
  timeout=timeout,
54
56
  ),
55
- response_type=builtins.list[LocationPublicRef],
57
+ response_type="builtins.list[LocationPublicRef]",
56
58
  ),
57
59
  )
58
60
 
@@ -130,7 +132,7 @@ class AsyncDebugLocationResource(AsyncAPIResource):
130
132
  extra_body=extra_body,
131
133
  timeout=timeout,
132
134
  ),
133
- response_type=builtins.list[LocationPublicRef],
135
+ response_type="builtins.list[LocationPublicRef]",
134
136
  ),
135
137
  )
136
138
 
@@ -5,13 +5,17 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from typing import TYPE_CHECKING
9
+
8
10
  from .._base_client import RequestOptions
9
11
  from .._resource import AsyncAPIResource, SyncAPIResource
10
12
  from .._streaming import AsyncStream, Stream
11
13
  from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
12
- from ..types import (
13
- Event,
14
- )
14
+
15
+ if TYPE_CHECKING:
16
+ from ..types import (
17
+ Event,
18
+ )
15
19
 
16
20
  __all__ = ["EventResource", "AsyncEventResource"]
17
21
 
@@ -46,7 +50,7 @@ class EventResource(SyncAPIResource):
46
50
  extra_body=extra_body,
47
51
  timeout=timeout,
48
52
  ),
49
- item_type=Event,
53
+ item_type="Event",
50
54
  )
51
55
 
52
56
 
@@ -80,5 +84,5 @@ class AsyncEventResource(AsyncAPIResource):
80
84
  extra_body=extra_body,
81
85
  timeout=timeout,
82
86
  ),
83
- item_type=Event,
87
+ item_type="Event",
84
88
  )