modus-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.
Files changed (98) hide show
  1. modus/__init__.py +144 -0
  2. modus/_auth.py +111 -0
  3. modus/_chat.py +411 -0
  4. modus/_client.py +240 -0
  5. modus/_config.py +89 -0
  6. modus/_deprecation.py +29 -0
  7. modus/_exceptions.py +149 -0
  8. modus/_generated/__init__.py +341 -0
  9. modus/_generated/models.py +3182 -0
  10. modus/_generated/operations.py +891 -0
  11. modus/_http.py +471 -0
  12. modus/_openapi_invoke.py +137 -0
  13. modus/_pagination.py +175 -0
  14. modus/_query.py +38 -0
  15. modus/_retry.py +123 -0
  16. modus/_streaming.py +115 -0
  17. modus/_validation.py +61 -0
  18. modus/_version.py +24 -0
  19. modus/management/__init__.py +74 -0
  20. modus/management/_client.py +168 -0
  21. modus/management/resources/__init__.py +21 -0
  22. modus/management/resources/_access_config_write.py +31 -0
  23. modus/management/resources/context/__init__.py +0 -0
  24. modus/management/resources/context/_content_merge.py +128 -0
  25. modus/management/resources/context/context.py +362 -0
  26. modus/management/resources/context/custom_items.py +318 -0
  27. modus/management/resources/context/items.py +234 -0
  28. modus/management/resources/organization.py +52 -0
  29. modus/management/resources/scopes/__init__.py +0 -0
  30. modus/management/resources/scopes/_update_body.py +38 -0
  31. modus/management/resources/scopes/conversations.py +177 -0
  32. modus/management/resources/scopes/evaluations.py +142 -0
  33. modus/management/resources/scopes/memories.py +273 -0
  34. modus/management/resources/scopes/scopes.py +764 -0
  35. modus/management/resources/scopes/supervision.py +85 -0
  36. modus/management/resources/usage.py +92 -0
  37. modus/management/resources/users.py +56 -0
  38. modus/management/resources/workflows/__init__.py +0 -0
  39. modus/management/resources/workflows/_update_body.py +29 -0
  40. modus/management/resources/workflows/interfaces.py +136 -0
  41. modus/management/resources/workflows/workflows.py +647 -0
  42. modus/management/types/__init__.py +57 -0
  43. modus/management/types/_guardrails_access.py +33 -0
  44. modus/management/types/_serialize.py +52 -0
  45. modus/management/types/agent_selection.py +66 -0
  46. modus/management/types/agents.py +5 -0
  47. modus/management/types/context.py +25 -0
  48. modus/management/types/conversations.py +5 -0
  49. modus/management/types/evaluations.py +25 -0
  50. modus/management/types/json_blob.py +51 -0
  51. modus/management/types/memories.py +69 -0
  52. modus/management/types/ownership.py +9 -0
  53. modus/management/types/skills.py +5 -0
  54. modus/management/types/toolset.py +236 -0
  55. modus/management/types/triggers.py +284 -0
  56. modus/management/types/usage.py +13 -0
  57. modus/management/types/workflow.py +45 -0
  58. modus/py.typed +0 -0
  59. modus/resources/__init__.py +21 -0
  60. modus/resources/connections/__init__.py +3 -0
  61. modus/resources/connections/connections.py +189 -0
  62. modus/resources/context/__init__.py +3 -0
  63. modus/resources/context/context.py +48 -0
  64. modus/resources/context/items.py +410 -0
  65. modus/resources/modus/__init__.py +3 -0
  66. modus/resources/modus/conversations.py +144 -0
  67. modus/resources/modus/modus.py +138 -0
  68. modus/resources/scopes/__init__.py +3 -0
  69. modus/resources/scopes/conversations.py +213 -0
  70. modus/resources/scopes/scopes.py +431 -0
  71. modus/resources/suggestions/__init__.py +3 -0
  72. modus/resources/suggestions/suggestions.py +207 -0
  73. modus/resources/workflows/__init__.py +15 -0
  74. modus/resources/workflows/runs.py +833 -0
  75. modus/resources/workflows/workflow_actions.py +122 -0
  76. modus/resources/workflows/workflows.py +358 -0
  77. modus/types/__init__.py +80 -0
  78. modus/types/agent_runs.py +70 -0
  79. modus/types/agents.py +17 -0
  80. modus/types/chat.py +9 -0
  81. modus/types/compose.py +21 -0
  82. modus/types/connections.py +9 -0
  83. modus/types/context.py +28 -0
  84. modus/types/context_accessors.py +89 -0
  85. modus/types/context_compose.py +18 -0
  86. modus/types/context_values.py +21 -0
  87. modus/types/conversations.py +30 -0
  88. modus/types/json_blob.py +24 -0
  89. modus/types/pagination.py +142 -0
  90. modus/types/runs.py +71 -0
  91. modus/types/skills.py +16 -0
  92. modus/types/suggestions.py +16 -0
  93. modus/types/tool_calls.py +34 -0
  94. modus/types/views.py +6 -0
  95. modus_sdk-0.2.0.dist-info/METADATA +225 -0
  96. modus_sdk-0.2.0.dist-info/RECORD +98 -0
  97. modus_sdk-0.2.0.dist-info/WHEEL +4 -0
  98. modus_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
modus/_client.py ADDED
@@ -0,0 +1,240 @@
1
+ """
2
+ Top-level client classes.
3
+
4
+ Modus — synchronous client for scripts, notebooks, data pipelines
5
+ AsyncModus — asynchronous client for FastAPI, async workflows
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Mapping, Optional, Union
11
+
12
+ import httpx
13
+
14
+ from ._auth import (
15
+ resolve_agent_host,
16
+ resolve_api_key,
17
+ resolve_base_url,
18
+ resolve_max_retries,
19
+ resolve_timeout,
20
+ )
21
+ from ._config import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, ModusConfig
22
+ from ._http import AsyncHttpClient, HttpClient
23
+ from .resources import (
24
+ AsyncConnectionsResource,
25
+ AsyncContextResource,
26
+ AsyncModusResource,
27
+ AsyncScopesResource,
28
+ AsyncSuggestionsResource,
29
+ AsyncWorkflowsResource,
30
+ ConnectionsResource,
31
+ ContextResource,
32
+ ModusResource,
33
+ ScopesResource,
34
+ SuggestionsResource,
35
+ WorkflowsResource,
36
+ )
37
+
38
+
39
+ class Modus:
40
+ """
41
+ Synchronous Modus API client.
42
+
43
+ Instantiate once and reuse across your script or application.
44
+ The client is thread-safe for concurrent GET requests.
45
+
46
+ Args:
47
+ api_key: Your Modus API key. If omitted, reads from the
48
+ MODUS_API_KEY environment variable.
49
+ base_url: API origin without a path suffix (default
50
+ https://api.modus.com).
51
+ agent_host: Agent host origin for streaming runs (e.g.
52
+ https://agent.modus.com). If omitted, reads from the
53
+ MODUS_AGENT_HOST environment variable. When not set,
54
+ streaming falls back to ``base_url``.
55
+ base_urls: Optional service-specific API origins.
56
+ timeout: Request timeout as seconds or httpx.Timeout. Default is
57
+ 5s connect and 300s read.
58
+ max_retries: Number of automatic retries on 429/5xx (default 2).
59
+
60
+ Example:
61
+ import modus
62
+
63
+ client = modus.Modus(api_key="modus_xxx")
64
+
65
+ for scope in client.scopes.list():
66
+ print(scope.name)
67
+
68
+ # Write operations use the management client in the same package:
69
+ from modus.management import ModusManagement
70
+
71
+ mgmt = ModusManagement(api_key="modus_xxx")
72
+ mgmt.context.create_note(
73
+ title="Fiscal Year",
74
+ content="Runs Feb 1 – Jan 31.",
75
+ )
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ api_key: Optional[str] = None,
81
+ *,
82
+ base_url: Optional[str] = None,
83
+ agent_host: Optional[str] = None,
84
+ base_urls: Optional[Mapping[str, str]] = None,
85
+ timeout: Union[float, httpx.Timeout, None] = None,
86
+ max_retries: Optional[int] = None,
87
+ ) -> None:
88
+ resolved_key = resolve_api_key(api_key)
89
+ resolved_url = resolve_base_url(base_url) or DEFAULT_BASE_URL
90
+ resolved_agent_host = resolve_agent_host(agent_host)
91
+ _t = resolve_timeout(timeout)
92
+ resolved_timeout = _t if _t is not None else DEFAULT_TIMEOUT
93
+ resolved_retries = resolve_max_retries(max_retries)
94
+ if resolved_retries is None:
95
+ resolved_retries = DEFAULT_MAX_RETRIES
96
+ self._config = ModusConfig(
97
+ api_key=resolved_key,
98
+ base_url=resolved_url,
99
+ timeout=resolved_timeout,
100
+ max_retries=resolved_retries,
101
+ agent_host=resolved_agent_host,
102
+ base_urls=base_urls or {},
103
+ )
104
+ self._http = HttpClient(self._config)
105
+ # Dedicated HTTP client for streaming to the agent host.
106
+ # When agent_host is not configured, _agent_http shares the same
107
+ # base_url as _http so streaming falls back gracefully.
108
+ if resolved_agent_host is not None:
109
+ _agent_config = ModusConfig(
110
+ api_key=resolved_key,
111
+ base_url=resolved_agent_host,
112
+ timeout=resolved_timeout,
113
+ max_retries=resolved_retries,
114
+ )
115
+ self._agent_http: HttpClient = HttpClient(_agent_config)
116
+ else:
117
+ self._agent_http = self._http
118
+
119
+ self.scopes: ScopesResource = ScopesResource(self._http, self._agent_http, self._config)
120
+ self.modus: ModusResource = ModusResource(self._http, self._agent_http, self._config)
121
+ self.workflows: WorkflowsResource = WorkflowsResource(
122
+ self._http, self._agent_http, self._config
123
+ )
124
+ self.context: ContextResource = ContextResource(self._http, self._config)
125
+ self.connections: ConnectionsResource = ConnectionsResource(self._http, self._config)
126
+ self.suggestions: SuggestionsResource = SuggestionsResource(self._http, self._config)
127
+
128
+ def close(self) -> None:
129
+ """Close the underlying HTTP connection pool."""
130
+ self._http.close()
131
+ if self._agent_http is not self._http:
132
+ self._agent_http.close()
133
+
134
+ def __enter__(self) -> "Modus":
135
+ return self
136
+
137
+ def __exit__(self, *args: object) -> None:
138
+ self.close()
139
+
140
+ def __repr__(self) -> str:
141
+ return f"Modus(base_url={self._config.base_url!r})"
142
+
143
+
144
+ class AsyncModus:
145
+ """
146
+ Asynchronous Modus API client.
147
+
148
+ Args:
149
+ api_key: Your Modus API key. If omitted, reads from MODUS_API_KEY.
150
+ base_url: API origin without a path suffix (default
151
+ https://api.modus.com).
152
+ agent_host: Agent host origin for streaming runs (e.g.
153
+ https://agent.modus.com). If omitted, reads from the
154
+ MODUS_AGENT_HOST environment variable. When not set,
155
+ streaming falls back to ``base_url``.
156
+ base_urls: Optional service-specific API origins.
157
+ timeout: Request timeout as seconds or httpx.Timeout. Default is
158
+ 5s connect and 300s read.
159
+ max_retries: Retries on 429/5xx (default 2).
160
+
161
+ Example:
162
+ import modus
163
+
164
+ client = modus.AsyncModus(api_key="modus_xxx")
165
+
166
+ async def main():
167
+ page = await client.scopes.list()
168
+ for scope in page:
169
+ print(scope.name)
170
+ await client.aclose()
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ api_key: Optional[str] = None,
176
+ *,
177
+ base_url: Optional[str] = None,
178
+ agent_host: Optional[str] = None,
179
+ base_urls: Optional[Mapping[str, str]] = None,
180
+ timeout: Union[float, httpx.Timeout, None] = None,
181
+ max_retries: Optional[int] = None,
182
+ ) -> None:
183
+ resolved_key = resolve_api_key(api_key)
184
+ resolved_url = resolve_base_url(base_url) or DEFAULT_BASE_URL
185
+ resolved_agent_host = resolve_agent_host(agent_host)
186
+ _t = resolve_timeout(timeout)
187
+ resolved_timeout = _t if _t is not None else DEFAULT_TIMEOUT
188
+ resolved_retries = resolve_max_retries(max_retries)
189
+ if resolved_retries is None:
190
+ resolved_retries = DEFAULT_MAX_RETRIES
191
+ self._config = ModusConfig(
192
+ api_key=resolved_key,
193
+ base_url=resolved_url,
194
+ timeout=resolved_timeout,
195
+ max_retries=resolved_retries,
196
+ agent_host=resolved_agent_host,
197
+ base_urls=base_urls or {},
198
+ )
199
+ self._http = AsyncHttpClient(self._config)
200
+ # Dedicated HTTP client for streaming to the agent host.
201
+ if resolved_agent_host is not None:
202
+ _agent_config = ModusConfig(
203
+ api_key=resolved_key,
204
+ base_url=resolved_agent_host,
205
+ timeout=resolved_timeout,
206
+ max_retries=resolved_retries,
207
+ )
208
+ self._agent_http: AsyncHttpClient = AsyncHttpClient(_agent_config)
209
+ else:
210
+ self._agent_http = self._http
211
+
212
+ self.scopes: AsyncScopesResource = AsyncScopesResource(
213
+ self._http, self._agent_http, self._config
214
+ )
215
+ self.modus: AsyncModusResource = AsyncModusResource(self._http, self._agent_http, self._config)
216
+ self.workflows: AsyncWorkflowsResource = AsyncWorkflowsResource(
217
+ self._http, self._agent_http, self._config
218
+ )
219
+ self.context: AsyncContextResource = AsyncContextResource(self._http, self._config)
220
+ self.connections: AsyncConnectionsResource = AsyncConnectionsResource(
221
+ self._http, self._config
222
+ )
223
+ self.suggestions: AsyncSuggestionsResource = AsyncSuggestionsResource(
224
+ self._http, self._config
225
+ )
226
+
227
+ async def aclose(self) -> None:
228
+ """Close the underlying async HTTP connection pool."""
229
+ await self._http.aclose()
230
+ if self._agent_http is not self._http:
231
+ await self._agent_http.aclose()
232
+
233
+ async def __aenter__(self) -> "AsyncModus":
234
+ return self
235
+
236
+ async def __aexit__(self, *args: object) -> None:
237
+ await self.aclose()
238
+
239
+ def __repr__(self) -> str:
240
+ return f"AsyncModus(base_url={self._config.base_url!r})"
modus/_config.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ SDK configuration.
3
+
4
+ ModusConfig holds all settings for a client instance. It is created once
5
+ during Modus.__init__ and passed down to the HTTP client and all resources.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from types import MappingProxyType
12
+ from typing import Mapping, Optional, Union
13
+
14
+ import httpx
15
+
16
+ # Defaults — override via Modus(base_url=..., timeout=..., max_retries=...)
17
+ DEFAULT_BASE_URL = "https://api.modus.com"
18
+
19
+
20
+ def _default_timeout() -> httpx.Timeout:
21
+ """Factory function to create the default timeout object."""
22
+ return httpx.Timeout(
23
+ connect=5.0,
24
+ read=300.0,
25
+ write=10.0,
26
+ pool=5.0,
27
+ )
28
+
29
+
30
+ DEFAULT_TIMEOUT: Union[float, httpx.Timeout] = _default_timeout()
31
+ DEFAULT_MAX_RETRIES = 2 # retried on 429 + 5xx only
32
+
33
+
34
+ def _normalize_base_url(value: str) -> str:
35
+ return value.rstrip("/")
36
+
37
+
38
+ def _normalize_base_urls(value: Mapping[str, str]) -> Mapping[str, str]:
39
+ return MappingProxyType(
40
+ {service: _normalize_base_url(base_url) for service, base_url in value.items()}
41
+ )
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ModusConfig:
46
+ api_key: str
47
+ base_url: str = DEFAULT_BASE_URL
48
+ timeout: Union[float, httpx.Timeout] = field(default_factory=_default_timeout)
49
+ max_retries: int = DEFAULT_MAX_RETRIES
50
+ agent_host: Optional[str] = None
51
+ base_urls: Mapping[str, str] = field(default_factory=dict)
52
+
53
+ def __post_init__(self) -> None:
54
+ # Reject obviously-bad max_retries early — negative/non-int would later
55
+ # bottom out in `range(max_retries + 1)` yielding zero attempts and a
56
+ # confusing "unreachable" RuntimeError from the retry loop.
57
+ if not isinstance(self.max_retries, int) or isinstance(self.max_retries, bool) or self.max_retries < 0:
58
+ raise ValueError(
59
+ f"max_retries must be a non-negative int, got {self.max_retries!r}"
60
+ )
61
+ # frozen=True prevents self.base_url = ... — use object.__setattr__
62
+ object.__setattr__(self, "base_url", _normalize_base_url(self.base_url))
63
+ if self.agent_host is not None:
64
+ object.__setattr__(self, "agent_host", _normalize_base_url(self.agent_host))
65
+ object.__setattr__(self, "base_urls", _normalize_base_urls(self.base_urls))
66
+
67
+ def __repr__(self) -> str:
68
+ masked = f"{self.api_key[:10]}***" if len(self.api_key) > 10 else "***"
69
+ return (
70
+ f"ModusConfig(api_key={masked!r}, base_url={self.base_url!r}, "
71
+ f"timeout={self.timeout!r}, max_retries={self.max_retries!r})"
72
+ )
73
+
74
+
75
+ def resolve_service_base_url(
76
+ config: ModusConfig,
77
+ service: str,
78
+ server_url: Optional[str],
79
+ ) -> str:
80
+ """Return the origin for an operation's owning public service."""
81
+ if service == "modus-api":
82
+ return config.base_url
83
+ if service in config.base_urls:
84
+ return config.base_urls[service]
85
+ if service == "agent-service" and config.agent_host:
86
+ return config.agent_host
87
+ if server_url:
88
+ return _normalize_base_url(server_url)
89
+ return config.base_url
modus/_deprecation.py ADDED
@@ -0,0 +1,29 @@
1
+ """Shared deprecation warnings for public SDK surfaces.
2
+
3
+ Call :func:`warn_deprecated` at the start of any public method that still
4
+ invokes an OpenAPI operation marked ``deprecated: true``. Release N keeps the
5
+ method working and warns; release N+1 removes the operation and the method.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import warnings
11
+
12
+
13
+ def warn_deprecated(*, old: str, new: str, stacklevel: int = 2) -> None:
14
+ """Emit a :class:`DeprecationWarning` naming the replacement surface.
15
+
16
+ Parameters
17
+ ----------
18
+ old:
19
+ The deprecated public name (method, attribute, or path segment).
20
+ new:
21
+ The replacement callers should use.
22
+ stacklevel:
23
+ Passed to :func:`warnings.warn` (default 2 points at the caller).
24
+ """
25
+ warnings.warn(
26
+ f"{old} is deprecated and will be removed in the next release; use {new} instead.",
27
+ DeprecationWarning,
28
+ stacklevel=stacklevel,
29
+ )
modus/_exceptions.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ Modus SDK exception hierarchy.
3
+
4
+ All exceptions raised by the SDK are subclasses of ModusError.
5
+ HTTP errors are mapped from status codes; run errors are raised from SSE events.
6
+
7
+ HTTP mapping:
8
+ 401 -> AuthenticationError
9
+ 403 -> PermissionDeniedError
10
+ 404 -> NotFoundError
11
+ 409 -> ConflictError
12
+ 422 -> UnprocessableError
13
+ 429 -> RateLimitError
14
+ 5xx -> InternalServerError
15
+
16
+ SSE event mapping:
17
+ { type: "error" } -> ModusError (raised from run/stream)
18
+ { type: "cancelled" } -> RunCancelledError
19
+ { type: "stream_timeout" } -> StreamTimeoutError
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, Mapping, Optional
25
+
26
+
27
+ class ModusError(Exception):
28
+ """Base class for all Modus SDK errors."""
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ *,
34
+ status_code: Optional[int] = None,
35
+ request_id: Optional[str] = None,
36
+ response_headers: Optional[Mapping[str, str]] = None,
37
+ body: Optional[str] = None,
38
+ code: Optional[str] = None,
39
+ ) -> None:
40
+ super().__init__(message)
41
+ self.message = message
42
+ self.status_code = status_code
43
+ self.request_id = request_id
44
+ self.response_headers = dict(response_headers) if response_headers else None
45
+ self.body = body
46
+ self.code = code
47
+
48
+ def __repr__(self) -> str:
49
+ return (
50
+ f"{self.__class__.__name__}("
51
+ f"message={self.message!r}, "
52
+ f"status_code={self.status_code!r}, "
53
+ f"request_id={self.request_id!r})"
54
+ )
55
+
56
+
57
+ class APIConnectionError(ModusError):
58
+ """
59
+ Network-level failure (ConnectError, ReadTimeout, etc.) after all
60
+ automatic retries are exhausted.
61
+ """
62
+ def __init__(self, message: str) -> None:
63
+ super().__init__(message)
64
+
65
+
66
+ class AuthenticationError(ModusError):
67
+ """
68
+ The API key is missing, invalid, or expired (HTTP 401).
69
+
70
+ Check that MODUS_API_KEY is set correctly, or pass api_key= explicitly.
71
+ Create a token at app.modus.com → Settings → API Tokens.
72
+ """
73
+ def __init__(self, message: str, **kwargs: Any) -> None:
74
+ super().__init__(message, status_code=401, **kwargs)
75
+
76
+
77
+ class PermissionDeniedError(ModusError):
78
+ """Raised when the API key does not have permission for the requested operation (HTTP 403)."""
79
+ def __init__(self, message: str, **kwargs: Any) -> None:
80
+ super().__init__(message, status_code=403, **kwargs)
81
+
82
+
83
+ class NotFoundError(ModusError):
84
+ """The requested resource does not exist (HTTP 404)."""
85
+ def __init__(self, message: str, **kwargs: Any) -> None:
86
+ super().__init__(message, status_code=404, **kwargs)
87
+
88
+
89
+ class ConflictError(ModusError):
90
+ """
91
+ Idempotency conflict — a run with this ID already exists with a different
92
+ body (HTTP 409). Use a new idempotency key or omit it to auto-generate.
93
+ """
94
+ def __init__(self, message: str, **kwargs: Any) -> None:
95
+ super().__init__(message, status_code=409, **kwargs)
96
+
97
+
98
+ class UnprocessableError(ModusError):
99
+ """
100
+ Request validation failed (HTTP 422).
101
+ The `errors` attribute contains the full validation error detail.
102
+ """
103
+ def __init__(self, message: str, *, errors: Optional[Any] = None, **kwargs: Any) -> None:
104
+ super().__init__(message, status_code=422, **kwargs)
105
+ self.errors = errors
106
+
107
+
108
+ class RateLimitError(ModusError):
109
+ """
110
+ Rate limit exceeded (HTTP 429).
111
+
112
+ The `retry_after` attribute contains the number of seconds to wait
113
+ before retrying, if the server provided a Retry-After header.
114
+ The SDK retries automatically — this error only surfaces after all
115
+ retry attempts are exhausted.
116
+ """
117
+ def __init__(self, message: str, *, retry_after: Optional[int] = None, **kwargs: Any) -> None:
118
+ super().__init__(message, status_code=429, **kwargs)
119
+ self.retry_after = retry_after
120
+
121
+
122
+ class InternalServerError(ModusError):
123
+ """
124
+ The server encountered an internal error (HTTP 5xx).
125
+ The SDK retries automatically — this error only surfaces after all
126
+ retry attempts are exhausted.
127
+ """
128
+ def __init__(self, message: str, *, status_code: int, **kwargs: Any) -> None:
129
+ super().__init__(message, status_code=status_code, **kwargs)
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Run-specific errors (raised from SSE events, not HTTP status codes)
134
+ # ---------------------------------------------------------------------------
135
+
136
+
137
+ class RunCancelledError(ModusError):
138
+ """Raised when a chat stream is cancelled before completing."""
139
+
140
+ def __init__(self, message: str = "Run was cancelled.") -> None:
141
+ super().__init__(message)
142
+
143
+
144
+ class StreamTimeoutError(ModusError):
145
+ """Raised when a chat stream times out before completion."""
146
+
147
+ def __init__(self, message: str = "Stream timed out.") -> None:
148
+ super().__init__(message)
149
+