mvgeos-core 0.6.6__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.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: mvgeos-core
3
+ Version: 0.6.6
4
+ Summary: Canonical loop vocabulary for MvgeOS: abort, invocations, spells, events, and the pure turn loop
5
+ License-Expression: MIT
6
+ Requires-Dist: pydantic>=2.0
7
+ Requires-Python: >=3.13
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "mvgeos-core"
3
+ version = "0.6.6"
4
+ description = "Canonical loop vocabulary for MvgeOS: abort, invocations, spells, events, and the pure turn loop"
5
+ requires-python = ">=3.13"
6
+ license = "MIT"
7
+ dependencies = ["pydantic>=2.0"]
8
+
9
+ [build-system]
10
+ requires = ["uv_build>=0.11.2,<0.13"]
11
+ build-backend = "uv_build"
12
+
13
+ [tool.ruff]
14
+ extend = "../pyproject.toml"
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "mvgeos-core"
3
+ version = "0.6.6"
4
+ description = "Canonical loop vocabulary for MvgeOS: abort, invocations, spells, events, and the pure turn loop"
5
+ requires-python = ">=3.13"
6
+ license = "MIT"
7
+ dependencies = [
8
+ "pydantic>=2.0",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["uv_build>=0.11.2,<0.13"]
13
+ build-backend = "uv_build"
14
+
15
+ [tool.ruff]
16
+ extend = "../pyproject.toml"
@@ -0,0 +1,228 @@
1
+ """Canonical loop vocabulary for MvgeOS.
2
+
3
+ Zero first-party dependencies. Leaf packages (provider, runes, tome) and the
4
+ agent depend on core, never the reverse.
5
+ """
6
+
7
+ from mvgeos_core.abort import AbortController, AbortError, AbortSignal
8
+ from mvgeos_core.approval import (
9
+ ApprovalDecision,
10
+ ApprovalOutcome,
11
+ ApprovalPresenter,
12
+ ApprovalReasonCode,
13
+ ApprovalRequest,
14
+ ApprovalScope,
15
+ SpellGateHandler,
16
+ allow,
17
+ deny,
18
+ normalize_arguments,
19
+ )
20
+ from mvgeos_core.channel import (
21
+ ChannelConfig,
22
+ Model,
23
+ MvgeResponse,
24
+ RealmResponse,
25
+ StopReason,
26
+ )
27
+ from mvgeos_core.constants import (
28
+ DEFAULT_AGENT_NAME,
29
+ DEFAULT_MODEL,
30
+ GLOBAL_DIR_ENV,
31
+ PROJECT_RUNE_PATH,
32
+ )
33
+ from mvgeos_core.dispatcher import BatchResult, SpellDispatcher
34
+ from mvgeos_core.errors import (
35
+ AuthenticationError,
36
+ MaxTurnsExceededError,
37
+ MissingApiKeyError,
38
+ MvgeError,
39
+ RateLimitError,
40
+ SpellDiscoveryError,
41
+ SpellNotFoundError,
42
+ SpellTimeoutError,
43
+ TomeIncompatibleError,
44
+ TomeResumeError,
45
+ UpstreamTimeoutError,
46
+ to_error,
47
+ )
48
+ from mvgeos_core.event_bus import EventBus
49
+ from mvgeos_core.events import (
50
+ ContemplationLevel,
51
+ ContentType,
52
+ MvgeEvent,
53
+ MvgeEventType,
54
+ PromptSource,
55
+ QueueMode,
56
+ )
57
+ from mvgeos_core.invocations import MvgeInvocation, SummonerRequest
58
+ from mvgeos_core.layers import (
59
+ SCOPE_PRECEDENCE,
60
+ ResolvedLayer,
61
+ Scope,
62
+ agent_dir,
63
+ agent_extensions_dir,
64
+ agent_skills_dir,
65
+ agents_dir,
66
+ approval_dir,
67
+ auth_dir,
68
+ auth_file,
69
+ default_rune_paths,
70
+ extensions_dir,
71
+ global_agents_dir,
72
+ global_file,
73
+ history_file,
74
+ models_file,
75
+ resolve_rune_layers,
76
+ resolve_rune_paths,
77
+ scope_rank,
78
+ sessions_dir,
79
+ skills_dir,
80
+ )
81
+ from mvgeos_core.loop import (
82
+ EmitSink,
83
+ LoopCallbacks,
84
+ LoopContext,
85
+ StreamFn,
86
+ run_loop,
87
+ )
88
+ from mvgeos_core.sandbox import MvgeSandbox, SandboxTimeoutError
89
+ from mvgeos_core.spell_schema import generate_spell_schema
90
+ from mvgeos_core.spells import (
91
+ ExecutionMode,
92
+ MvgeSpell,
93
+ SpellExecutionMode,
94
+ SpellResult,
95
+ SpellResultMessage,
96
+ SpellSignal,
97
+ SpellStatus,
98
+ SpellUpdateCallback,
99
+ )
100
+ from mvgeos_core.telemetry import (
101
+ GEN_AI_AGENT_NAME,
102
+ GEN_AI_CONVERSATION_ID,
103
+ GEN_AI_OPERATION_NAME,
104
+ GEN_AI_REQUEST_MAX_TOKENS,
105
+ GEN_AI_REQUEST_MODEL,
106
+ GEN_AI_REQUEST_TEMPERATURE,
107
+ GEN_AI_REQUEST_TOP_P,
108
+ GEN_AI_RESPONSE_FINISH_REASONS,
109
+ GEN_AI_RESPONSE_MODEL,
110
+ GEN_AI_SYSTEM,
111
+ GEN_AI_TOOL_CALL_ID,
112
+ GEN_AI_TOOL_DEFINITIONS,
113
+ GEN_AI_TOOL_NAME,
114
+ GEN_AI_TOOL_TYPE,
115
+ GEN_AI_USAGE_INPUT_TOKENS,
116
+ GEN_AI_USAGE_OUTPUT_TOKENS,
117
+ GEN_AI_USAGE_TOTAL_TOKENS,
118
+ MVGEOS_ORCHESTRATOR,
119
+ create_genai_chat_attributes,
120
+ create_genai_tool_attributes,
121
+ derive_provider_from_model,
122
+ )
123
+
124
+ __all__ = [
125
+ "AbortController",
126
+ "AbortError",
127
+ "AbortSignal",
128
+ "ApprovalDecision",
129
+ "ApprovalOutcome",
130
+ "ApprovalPresenter",
131
+ "ApprovalReasonCode",
132
+ "ApprovalRequest",
133
+ "ApprovalScope",
134
+ "SpellGateHandler",
135
+ "allow",
136
+ "deny",
137
+ "normalize_arguments",
138
+ "AuthenticationError",
139
+ "BatchResult",
140
+ "ChannelConfig",
141
+ "ContemplationLevel",
142
+ "ContentType",
143
+ "DEFAULT_AGENT_NAME",
144
+ "DEFAULT_MODEL",
145
+ "GLOBAL_DIR_ENV",
146
+ "EmitSink",
147
+ "EventBus",
148
+ "ExecutionMode",
149
+ "GEN_AI_AGENT_NAME",
150
+ "GEN_AI_CONVERSATION_ID",
151
+ "GEN_AI_OPERATION_NAME",
152
+ "PROJECT_RUNE_PATH",
153
+ "agent_dir",
154
+ "agent_extensions_dir",
155
+ "agent_skills_dir",
156
+ "agents_dir",
157
+ "approval_dir",
158
+ "auth_dir",
159
+ "auth_file",
160
+ "global_file",
161
+ "history_file",
162
+ "models_file",
163
+ "skills_dir",
164
+ "default_rune_paths",
165
+ "extensions_dir",
166
+ "global_agents_dir",
167
+ "GEN_AI_REQUEST_MAX_TOKENS",
168
+ "GEN_AI_REQUEST_MODEL",
169
+ "GEN_AI_REQUEST_TEMPERATURE",
170
+ "GEN_AI_REQUEST_TOP_P",
171
+ "GEN_AI_RESPONSE_FINISH_REASONS",
172
+ "GEN_AI_RESPONSE_MODEL",
173
+ "GEN_AI_SYSTEM",
174
+ "GEN_AI_TOOL_CALL_ID",
175
+ "GEN_AI_TOOL_DEFINITIONS",
176
+ "GEN_AI_TOOL_NAME",
177
+ "GEN_AI_TOOL_TYPE",
178
+ "GEN_AI_USAGE_INPUT_TOKENS",
179
+ "GEN_AI_USAGE_OUTPUT_TOKENS",
180
+ "GEN_AI_USAGE_TOTAL_TOKENS",
181
+ "LoopCallbacks",
182
+ "LoopContext",
183
+ "MaxTurnsExceededError",
184
+ "MissingApiKeyError",
185
+ "Model",
186
+ "MvgeError",
187
+ "MvgeEvent",
188
+ "MvgeEventType",
189
+ "MvgeInvocation",
190
+ "MvgeResponse",
191
+ "MvgeSandbox",
192
+ "MvgeSpell",
193
+ "MVGEOS_ORCHESTRATOR",
194
+ "PromptSource",
195
+ "QueueMode",
196
+ "RateLimitError",
197
+ "RealmResponse",
198
+ "ResolvedLayer",
199
+ "SCOPE_PRECEDENCE",
200
+ "SandboxTimeoutError",
201
+ "SpellDispatcher",
202
+ "SpellDiscoveryError",
203
+ "SpellExecutionMode",
204
+ "SpellNotFoundError",
205
+ "SpellResult",
206
+ "SpellResultMessage",
207
+ "SpellSignal",
208
+ "SpellStatus",
209
+ "SpellTimeoutError",
210
+ "SpellUpdateCallback",
211
+ "StopReason",
212
+ "Scope",
213
+ "StreamFn",
214
+ "SummonerRequest",
215
+ "TomeIncompatibleError",
216
+ "TomeResumeError",
217
+ "UpstreamTimeoutError",
218
+ "create_genai_chat_attributes",
219
+ "create_genai_tool_attributes",
220
+ "derive_provider_from_model",
221
+ "generate_spell_schema",
222
+ "resolve_rune_layers",
223
+ "resolve_rune_paths",
224
+ "scope_rank",
225
+ "sessions_dir",
226
+ "run_loop",
227
+ "to_error",
228
+ ]
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ from collections.abc import Callable
6
+
7
+
8
+ class AbortError(Exception):
9
+ """Raised when an operation is cancelled via AbortSignal."""
10
+
11
+
12
+ class AbortSignal:
13
+ """Cancellation signal, mirroring the web AbortSignal API (Pi-compatible).
14
+
15
+ A signal is created with ``aborted`` False. Calling ``controller.abort()``
16
+ flips the flag and fires any registered callbacks. Consumers should check
17
+ ``aborted`` or call ``raise_if_aborted()`` at safe checkpoints.
18
+ """
19
+
20
+ def __init__(self, controller: AbortController) -> None:
21
+ self._controller = controller
22
+ self._aborted = False
23
+ self._callbacks: list[Callable[[], None]] = []
24
+ self._wait_future: asyncio.Future[None] | None = None
25
+
26
+ @property
27
+ def aborted(self) -> bool:
28
+ return self._aborted
29
+
30
+ def on_abort(self, callback: Callable[[], None]) -> None:
31
+ if self._aborted:
32
+ callback()
33
+ else:
34
+ self._callbacks.append(callback)
35
+
36
+ def raise_if_aborted(self) -> None:
37
+ if self._aborted:
38
+ raise AbortError("Operation aborted")
39
+
40
+ async def wait(self) -> None:
41
+ """Block until the signal is aborted or the await is cancelled.
42
+
43
+ Multiple concurrent calls to wait() will all resolve when the signal is aborted.
44
+ """
45
+ if self._aborted:
46
+ return
47
+ if self._wait_future is not None and not self._wait_future.done():
48
+ await self._wait_future
49
+ return
50
+ self._wait_future = asyncio.get_event_loop().create_future()
51
+
52
+ def _set_result() -> None:
53
+ if self._wait_future is not None and not self._wait_future.done():
54
+ self._wait_future.set_result(None)
55
+
56
+ self._callbacks.append(_set_result)
57
+ try:
58
+ await self._wait_future
59
+ finally:
60
+ if _set_result in self._callbacks:
61
+ self._callbacks.remove(_set_result)
62
+
63
+
64
+ class AbortController:
65
+ """Controller that owns an :class:`AbortSignal` and can abort it.
66
+
67
+ Mirrors the web ``AbortController`` API and Pi's cancellation model.
68
+ """
69
+
70
+ def __init__(self) -> None:
71
+ self._signal = AbortSignal(self)
72
+
73
+ @property
74
+ def signal(self) -> AbortSignal:
75
+ return self._signal
76
+
77
+ def abort(self) -> None:
78
+ if self._signal._aborted:
79
+ return
80
+ self._signal._aborted = True
81
+ for callback in list(self._signal._callbacks):
82
+ with contextlib.suppress(Exception):
83
+ callback()
84
+ self._signal._callbacks.clear()
85
+ wait_future = self._signal._wait_future
86
+ if wait_future is not None and not wait_future.done():
87
+ wait_future.set_result(None)
88
+
89
+
90
+ __all__ = [
91
+ "AbortController",
92
+ "AbortError",
93
+ "AbortSignal",
94
+ ]
@@ -0,0 +1,217 @@
1
+ """Gate vocabulary for the critical spell-approval path.
2
+
3
+ These types are the contract between the spell dispatcher (which builds the
4
+ request), the critical gate handlers registered by runes (which decide), and
5
+ the host-bound presenter (which surfaces the decision to the Summoner).
6
+
7
+ The gate is fail-closed: any exception, cancellation, malformed response, or
8
+ stale digest denies the cast. This module carries no I/O and no first-party
9
+ dependencies.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import json
16
+ from collections.abc import Awaitable, Callable, Mapping
17
+ from dataclasses import dataclass, field
18
+ from enum import StrEnum
19
+ from types import MappingProxyType
20
+ from typing import Any
21
+
22
+
23
+ class ApprovalOutcome(StrEnum):
24
+ ALLOW = "allow"
25
+ DENY = "deny"
26
+
27
+
28
+ class ApprovalScope(StrEnum):
29
+ ONCE = "once"
30
+ SPELL = "spell"
31
+ SESSION = "session"
32
+ PROJECT = "project"
33
+
34
+
35
+ class ApprovalReasonCode(StrEnum):
36
+ USER = "user"
37
+ RULE = "rule"
38
+ READ_ONLY = "read_only"
39
+ FAILURE = "failure"
40
+
41
+
42
+ def _coerce_outcome(value: ApprovalOutcome | str) -> ApprovalOutcome:
43
+ try:
44
+ return ApprovalOutcome(value)
45
+ except ValueError as exc:
46
+ raise ValueError(f"Unknown approval outcome: {value!r}") from exc
47
+
48
+
49
+ def _coerce_scope(value: ApprovalScope | str) -> ApprovalScope:
50
+ try:
51
+ return ApprovalScope(value)
52
+ except ValueError as exc:
53
+ raise ValueError(f"Unknown approval scope: {value!r}") from exc
54
+
55
+
56
+ def _coerce_reason_code(value: ApprovalReasonCode | str) -> ApprovalReasonCode:
57
+ try:
58
+ return ApprovalReasonCode(value)
59
+ except ValueError as exc:
60
+ raise ValueError(f"Unknown approval reason code: {value!r}") from exc
61
+
62
+
63
+ def _freeze_mapping(value: Mapping[str, Any]) -> Mapping[str, Any]:
64
+ if isinstance(value, MappingProxyType):
65
+ return value
66
+ return MappingProxyType(dict(value))
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class ApprovalRequest:
71
+ """One immutable approval question for a single spell cast.
72
+
73
+ The dispatcher builds this from the validated, normalized argument copy
74
+ and the gate approves exactly this data; the dispatcher then executes the
75
+ approved copy. ``spell_identity`` is engine-derived (never rune-chosen)
76
+ attribution the policy layer matches rules against.
77
+ """
78
+
79
+ cast_id: str
80
+ spell_name: str
81
+ spell_identity: Mapping[str, str] = field(default_factory=dict)
82
+ arguments: Mapping[str, Any] = field(default_factory=dict)
83
+ argument_digest: str = ""
84
+ project_root: str = ""
85
+ tome_id: str = ""
86
+ agent_name: str = ""
87
+ # True when the spell carried a parameter schema and the dispatcher
88
+ # validated/normalized arguments against it before building this
89
+ # request. False marks a schemaless cast: policy may never match an
90
+ # allow rule against it, and presenters must show an "unvalidated
91
+ # arguments" warning.
92
+ schema_validated: bool = True
93
+
94
+ def __post_init__(self) -> None:
95
+ object.__setattr__(self, "spell_identity", _freeze_mapping(self.spell_identity))
96
+ object.__setattr__(self, "arguments", _freeze_mapping(self.arguments))
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class ApprovalDecision:
101
+ """The verdict for one :class:`ApprovalRequest`.
102
+
103
+ ``request_digest`` must equal the request's ``argument_digest``; a
104
+ decision bound to any other digest is stale and denies the cast.
105
+ """
106
+
107
+ outcome: ApprovalOutcome
108
+ scope: ApprovalScope = ApprovalScope.ONCE
109
+ reason_code: ApprovalReasonCode = ApprovalReasonCode.RULE
110
+ request_digest: str = ""
111
+
112
+ def __post_init__(self) -> None:
113
+ object.__setattr__(self, "outcome", _coerce_outcome(self.outcome))
114
+ object.__setattr__(self, "scope", _coerce_scope(self.scope))
115
+ object.__setattr__(self, "reason_code", _coerce_reason_code(self.reason_code))
116
+
117
+
118
+ def deny(
119
+ request: ApprovalRequest,
120
+ reason_code: ApprovalReasonCode | str = ApprovalReasonCode.FAILURE,
121
+ scope: ApprovalScope | str = ApprovalScope.ONCE,
122
+ ) -> ApprovalDecision:
123
+ """Build a denial decision already bound to ``request``."""
124
+ return ApprovalDecision(
125
+ outcome=ApprovalOutcome.DENY,
126
+ scope=_coerce_scope(scope),
127
+ reason_code=_coerce_reason_code(reason_code),
128
+ request_digest=request.argument_digest,
129
+ )
130
+
131
+
132
+ def allow(
133
+ request: ApprovalRequest,
134
+ reason_code: ApprovalReasonCode | str = ApprovalReasonCode.RULE,
135
+ scope: ApprovalScope | str = ApprovalScope.ONCE,
136
+ ) -> ApprovalDecision:
137
+ """Build an allow decision already bound to ``request``."""
138
+ return ApprovalDecision(
139
+ outcome=ApprovalOutcome.ALLOW,
140
+ scope=_coerce_scope(scope),
141
+ reason_code=_coerce_reason_code(reason_code),
142
+ request_digest=request.argument_digest,
143
+ )
144
+
145
+
146
+ def _deep_freeze(value: Any) -> Any:
147
+ """Recursively freeze JSON-like data into immutable containers.
148
+
149
+ dicts become ``MappingProxyType``, lists/tuples become tuples; scalars
150
+ pass through. The frozen tree shares no mutable state with the input.
151
+ """
152
+ if isinstance(value, dict):
153
+ return MappingProxyType(
154
+ {key: _deep_freeze(item) for key, item in value.items()}
155
+ )
156
+ if isinstance(value, (list, tuple)):
157
+ return tuple(_deep_freeze(item) for item in value)
158
+ return value
159
+
160
+
161
+ def normalize_arguments(
162
+ arguments: Any,
163
+ ) -> tuple[Mapping[str, Any], str]:
164
+ """Freeze arguments to canonical JSON and digest them.
165
+
166
+ Args:
167
+ arguments: The validated argument mapping for one cast.
168
+
169
+ Returns:
170
+ A tuple of (recursively immutable argument view, ``"sha256:<hex>"``
171
+ digest) where the digest is computed over ``json.dumps`` with
172
+ ``sort_keys=True, separators=(",", ":"), ensure_ascii=True``.
173
+
174
+ Raises:
175
+ ValueError: If ``arguments`` is not a dict or is not JSON-serializable.
176
+ """
177
+ if not isinstance(arguments, dict):
178
+ raise ValueError(
179
+ "Spell arguments must be a JSON object (dict); "
180
+ f"got {type(arguments).__name__}"
181
+ )
182
+ try:
183
+ canonical = json.dumps(
184
+ arguments, sort_keys=True, separators=(",", ":"), ensure_ascii=True
185
+ )
186
+ except (TypeError, ValueError) as exc:
187
+ raise ValueError(f"Spell arguments are not JSON-serializable: {exc}") from exc
188
+ digest = "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
189
+ # Freeze after dumping: the frozen tree shares no mutable state with the
190
+ # input, so a gate cannot mutate nested values after display and change
191
+ # what the dispatcher executes.
192
+ frozen = _deep_freeze(arguments)
193
+ # `arguments` is a dict, so `_deep_freeze` returned a MappingProxyType.
194
+ return frozen, digest
195
+
196
+
197
+ SpellGateHandler = Callable[
198
+ [ApprovalRequest], "ApprovalDecision | Awaitable[ApprovalDecision]"
199
+ ]
200
+ """A critical gate handler: receives the request, returns or awaits a decision."""
201
+
202
+ ApprovalPresenter = Callable[[ApprovalRequest], Awaitable[ApprovalDecision]]
203
+ """Host-bound UI surface: awaits the Summoner's decision for one request."""
204
+
205
+
206
+ __all__ = [
207
+ "ApprovalDecision",
208
+ "ApprovalOutcome",
209
+ "ApprovalPresenter",
210
+ "ApprovalReasonCode",
211
+ "ApprovalRequest",
212
+ "ApprovalScope",
213
+ "SpellGateHandler",
214
+ "allow",
215
+ "deny",
216
+ "normalize_arguments",
217
+ ]
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
5
+ from typing import Any
6
+
7
+
8
+ class StopReason(StrEnum):
9
+ PENDING = "pending"
10
+ STOP = "stop"
11
+ LENGTH = "length"
12
+ SPELL_USE = "spellUse"
13
+ ERROR = "error"
14
+ ABORTED = "aborted"
15
+
16
+
17
+ @dataclass
18
+ class MvgeResponse:
19
+ role: str = "assistant"
20
+ content: list[dict[str, Any]] = field(default_factory=list)
21
+ realm: str = ""
22
+ model: str = ""
23
+ mana_usage: dict[str, float] = field(default_factory=dict)
24
+ stop_reason: StopReason = StopReason.PENDING
25
+ error_message: str | None = None
26
+ timestamp: float = 0.0
27
+
28
+
29
+ @dataclass
30
+ class Model:
31
+ id: str
32
+ name: str
33
+ realm: str
34
+ base_url: str
35
+ api_key: str
36
+ max_completion_mana: int = 0
37
+ context_window: int = 128000
38
+ max_tokens: int = 4096
39
+ headers: dict[str, str] = field(default_factory=dict)
40
+ supported_parameters: list[str] = field(default_factory=list)
41
+ supported_contemplation_levels: list[str] = field(default_factory=list)
42
+ is_free: bool = False
43
+
44
+ @property
45
+ def free(self) -> bool:
46
+ """Return True if the model is free of charge."""
47
+ return self.is_free or self.id.endswith(":free") or self.id == "openrouter/free"
48
+
49
+ @property
50
+ def provider(self) -> str:
51
+ """The organization that provides this model (derived from the model ID)."""
52
+ return self.id.split("/")[0] if "/" in self.id else self.realm
53
+
54
+ @property
55
+ def provider_prefix(self) -> str:
56
+ """The provider prefix before '/' in id, or realm, or id."""
57
+ return self.id.split("/")[0] if "/" in self.id else (self.realm or self.id)
58
+
59
+ @property
60
+ def supports_contemplation(self) -> bool:
61
+ """Return True if the model supports contemplation/reasoning."""
62
+ return (
63
+ bool(self.supported_contemplation_levels)
64
+ or "reasoning" in self.supported_parameters
65
+ or "thinking" in self.supported_parameters
66
+ )
67
+
68
+
69
+ @dataclass
70
+ class ChannelConfig:
71
+ model: Model
72
+ temperature: float = 0.7
73
+ max_tokens: int = 4096
74
+ max_output_mana: int | None = None
75
+ timeout_ms: int = 120000
76
+ max_retries: int = 3
77
+ contemplation_level: str = "medium"
78
+ contemplation_budget: int | None = None
79
+ exclude_contemplation: bool = False
80
+ spells: list[dict[str, Any]] = field(default_factory=list)
81
+ tools: list[dict[str, Any]] = field(default_factory=list)
82
+ meta_data: dict[str, Any] = field(default_factory=dict)
83
+ system_prompt: str = ""
84
+
85
+ def __post_init__(self) -> None:
86
+ if self.tools and not self.spells:
87
+ self.spells = self.tools
88
+ elif self.spells and not self.tools:
89
+ self.tools = self.spells
90
+
91
+
92
+ @dataclass
93
+ class RealmResponse:
94
+ model: Model
95
+ invocation: Any | None = None
96
+ mana_used: int = 0
97
+ stop_reason: str = "stop"
98
+ error_message: str | None = None
99
+ error_code: str | None = None
100
+ retry_after: float | None = None
101
+ limit_source: str | None = None
102
+ remedy_hint: str | None = None
103
+ reset_at: float | None = None
104
+ quota_limit: int | None = None
105
+ quota_remaining: int | None = None
106
+
107
+
108
+ __all__ = [
109
+ "ChannelConfig",
110
+ "Model",
111
+ "MvgeResponse",
112
+ "RealmResponse",
113
+ "StopReason",
114
+ ]