meerkat-mobkit 0.4.9__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.
Files changed (32) hide show
  1. meerkat_mobkit-0.4.9/PKG-INFO +24 -0
  2. meerkat_mobkit-0.4.9/meerkat_mobkit/__init__.py +171 -0
  3. meerkat_mobkit-0.4.9/meerkat_mobkit/_client.py +521 -0
  4. meerkat_mobkit-0.4.9/meerkat_mobkit/_sse.py +110 -0
  5. meerkat_mobkit-0.4.9/meerkat_mobkit/_transport.py +236 -0
  6. meerkat_mobkit-0.4.9/meerkat_mobkit/agent_builder.py +118 -0
  7. meerkat_mobkit-0.4.9/meerkat_mobkit/builder.py +158 -0
  8. meerkat_mobkit-0.4.9/meerkat_mobkit/config/__init__.py +4 -0
  9. meerkat_mobkit-0.4.9/meerkat_mobkit/config/auth.py +47 -0
  10. meerkat_mobkit-0.4.9/meerkat_mobkit/config/memory.py +30 -0
  11. meerkat_mobkit-0.4.9/meerkat_mobkit/config/session_store.py +45 -0
  12. meerkat_mobkit-0.4.9/meerkat_mobkit/errors.py +32 -0
  13. meerkat_mobkit-0.4.9/meerkat_mobkit/events.py +257 -0
  14. meerkat_mobkit-0.4.9/meerkat_mobkit/helpers.py +147 -0
  15. meerkat_mobkit-0.4.9/meerkat_mobkit/models.py +145 -0
  16. meerkat_mobkit-0.4.9/meerkat_mobkit/py.typed +0 -0
  17. meerkat_mobkit-0.4.9/meerkat_mobkit/runtime.py +822 -0
  18. meerkat_mobkit-0.4.9/meerkat_mobkit/types.py +549 -0
  19. meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/PKG-INFO +24 -0
  20. meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/SOURCES.txt +30 -0
  21. meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/dependency_links.txt +1 -0
  22. meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/requires.txt +3 -0
  23. meerkat_mobkit-0.4.9/meerkat_mobkit.egg-info/top_level.txt +1 -0
  24. meerkat_mobkit-0.4.9/pyproject.toml +40 -0
  25. meerkat_mobkit-0.4.9/setup.cfg +4 -0
  26. meerkat_mobkit-0.4.9/tests/test_builder.py +80 -0
  27. meerkat_mobkit-0.4.9/tests/test_callback_tools.py +171 -0
  28. meerkat_mobkit-0.4.9/tests/test_errors.py +41 -0
  29. meerkat_mobkit-0.4.9/tests/test_events.py +115 -0
  30. meerkat_mobkit-0.4.9/tests/test_init.py +76 -0
  31. meerkat_mobkit-0.4.9/tests/test_models.py +86 -0
  32. meerkat_mobkit-0.4.9/tests/test_types.py +517 -0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: meerkat-mobkit
3
+ Version: 0.4.9
4
+ Summary: Python SDK for MobKit — companion orchestration platform for the Meerkat multi-agent runtime
5
+ Author: Luka Crnkovic-Friis
6
+ License: MIT OR Apache-2.0
7
+ Project-URL: Homepage, https://docs.rkat.ai
8
+ Project-URL: Repository, https://github.com/lukacf/meerkat-mobkit
9
+ Project-URL: Documentation, https://docs.rkat.ai
10
+ Keywords: agent,llm,ai,meerkat,orchestration,sdk
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
@@ -0,0 +1,171 @@
1
+ """MobKit Python SDK.
2
+
3
+ Usage::
4
+
5
+ from meerkat_mobkit import MobKit, MobKitRuntime, MobKitBuilder
6
+ from meerkat_mobkit import DiscoverySpec, PreSpawnData, SessionQuery
7
+ from meerkat_mobkit import SessionAgentBuilder, SessionBuildOptions
8
+ from meerkat_mobkit.errors import MobKitError, RpcError, NotConnectedError
9
+ from meerkat_mobkit.types import StatusResult, CapabilitiesResult
10
+ from meerkat_mobkit.events import MobEvent, AgentEvent
11
+
12
+ Module authoring helpers are available via::
13
+
14
+ from meerkat_mobkit.helpers import ModuleSpec, define_module, ...
15
+ """
16
+ from __future__ import annotations
17
+
18
+ # Builder + Runtime
19
+ from .builder import MobKit, MobKitBuilder
20
+ from .runtime import MobKitRuntime, ToolCaller
21
+
22
+ # Data models
23
+ from .models import DiscoverySpec, PreSpawnData, SessionBuildOptions, SessionQuery
24
+
25
+ # Agent builder protocol (public contract — CallbackDispatcher is internal)
26
+ from .agent_builder import SessionAgentBuilder
27
+
28
+ # Errors
29
+ from .errors import (
30
+ CapabilityUnavailableError,
31
+ ContractMismatchError,
32
+ MobKitError,
33
+ NotConnectedError,
34
+ RpcError,
35
+ TransportError,
36
+ )
37
+
38
+ # Typed return models
39
+ from .types import (
40
+ CallToolResult,
41
+ CapabilitiesResult,
42
+ DeliveryHistoryResult,
43
+ DeliveryResult,
44
+ ErrorCategory,
45
+ ErrorEvent,
46
+ EventEnvelope,
47
+ EventQuery,
48
+ GatingAuditEntry,
49
+ GatingDecisionResult,
50
+ GatingEvaluateResult,
51
+ GatingPendingEntry,
52
+ KeepAliveConfig,
53
+ MEMBER_STATE_ACTIVE,
54
+ MEMBER_STATE_RETIRING,
55
+ MemberSnapshot,
56
+ MemoryIndexResult,
57
+ MemoryQueryResult,
58
+ MemoryStoreInfo,
59
+ PersistedEvent,
60
+ ReconcileEdgesReport,
61
+ ReconcileResult,
62
+ RediscoverReport,
63
+ RoutingResolution,
64
+ RuntimeRouteResult,
65
+ SendMessageResult,
66
+ SpawnMemberResult,
67
+ SpawnResult,
68
+ StatusResult,
69
+ SubscribeResult,
70
+ UnifiedAgentEvent,
71
+ UnifiedModuleEvent,
72
+ )
73
+
74
+ # Typed events
75
+ from .events import (
76
+ AgentEvent,
77
+ Event,
78
+ EventStream,
79
+ MobEvent,
80
+ RunCompleted,
81
+ RunFailed,
82
+ RunStarted,
83
+ TextComplete,
84
+ TextDelta,
85
+ ToolCallRequested,
86
+ ToolExecutionCompleted,
87
+ ToolExecutionStarted,
88
+ ToolResultReceived,
89
+ TurnCompleted,
90
+ TurnStarted,
91
+ UnknownEvent,
92
+ )
93
+
94
+ # Config modules (importable as meerkat_mobkit.auth, etc.)
95
+ from .config import auth, memory, session_store
96
+
97
+ __all__ = [
98
+ # Builder + Runtime
99
+ "MobKit",
100
+ "MobKitBuilder",
101
+ "MobKitRuntime",
102
+ # Data models
103
+ "DiscoverySpec",
104
+ "PreSpawnData",
105
+ "SessionBuildOptions",
106
+ "SessionQuery",
107
+ # Agent builder
108
+ "SessionAgentBuilder",
109
+ # Errors
110
+ "MobKitError",
111
+ "TransportError",
112
+ "RpcError",
113
+ "CapabilityUnavailableError",
114
+ "ContractMismatchError",
115
+ "NotConnectedError",
116
+ # Typed return models
117
+ "StatusResult",
118
+ "CapabilitiesResult",
119
+ "ReconcileResult",
120
+ "SpawnResult",
121
+ "SpawnMemberResult",
122
+ "SendMessageResult",
123
+ "SubscribeResult",
124
+ "KeepAliveConfig",
125
+ "EventEnvelope",
126
+ "RoutingResolution",
127
+ "DeliveryResult",
128
+ "DeliveryHistoryResult",
129
+ "MemoryQueryResult",
130
+ "MemoryStoreInfo",
131
+ "MemoryIndexResult",
132
+ "MEMBER_STATE_ACTIVE",
133
+ "MEMBER_STATE_RETIRING",
134
+ "MemberSnapshot",
135
+ "RuntimeRouteResult",
136
+ "GatingEvaluateResult",
137
+ "GatingDecisionResult",
138
+ "GatingAuditEntry",
139
+ "GatingPendingEntry",
140
+ "CallToolResult",
141
+ "ErrorCategory",
142
+ "ErrorEvent",
143
+ "EventQuery",
144
+ "PersistedEvent",
145
+ "UnifiedAgentEvent",
146
+ "UnifiedModuleEvent",
147
+ "ReconcileEdgesReport",
148
+ "RediscoverReport",
149
+ "ToolCaller",
150
+ # Typed events
151
+ "Event",
152
+ "MobEvent",
153
+ "AgentEvent",
154
+ "EventStream",
155
+ "RunStarted",
156
+ "RunCompleted",
157
+ "RunFailed",
158
+ "TurnStarted",
159
+ "TextDelta",
160
+ "TextComplete",
161
+ "ToolCallRequested",
162
+ "ToolResultReceived",
163
+ "TurnCompleted",
164
+ "ToolExecutionStarted",
165
+ "ToolExecutionCompleted",
166
+ "UnknownEvent",
167
+ # Config modules
168
+ "auth",
169
+ "memory",
170
+ "session_store",
171
+ ]
@@ -0,0 +1,521 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import subprocess
7
+ from typing import Any, Callable, Literal, Mapping, Protocol, TypedDict, cast
8
+ from urllib import request as urllib_request
9
+ from urllib.error import HTTPError, URLError
10
+
11
+
12
+ class JsonRpcRequest(TypedDict):
13
+ jsonrpc: Literal["2.0"]
14
+ id: str
15
+ method: str
16
+ params: dict[str, Any]
17
+
18
+
19
+ class JsonRpcSuccess(TypedDict):
20
+ jsonrpc: Literal["2.0"]
21
+ id: str
22
+ result: Any
23
+
24
+
25
+ class JsonRpcErrorBody(TypedDict):
26
+ code: int
27
+ message: str
28
+
29
+
30
+ class JsonRpcErrorResponse(TypedDict):
31
+ jsonrpc: Literal["2.0"]
32
+ id: str
33
+ error: JsonRpcErrorBody
34
+
35
+
36
+ JsonRpcResponse = JsonRpcSuccess | JsonRpcErrorResponse
37
+
38
+
39
+ class MobkitStatusResult(TypedDict):
40
+ contract_version: str
41
+ running: bool
42
+ loaded_modules: list[str]
43
+
44
+
45
+ class MobkitCapabilitiesResult(TypedDict):
46
+ contract_version: str
47
+ methods: list[str]
48
+ loaded_modules: list[str]
49
+
50
+
51
+ class MobkitReconcileResult(TypedDict):
52
+ accepted: bool
53
+ reconciled_modules: list[str]
54
+ added: int
55
+
56
+
57
+ class MobkitSpawnMemberResult(TypedDict):
58
+ accepted: bool
59
+ module_id: str
60
+
61
+
62
+ class MobkitSubscribeKeepAlive(TypedDict):
63
+ interval_ms: int
64
+ event: str
65
+
66
+
67
+ class MobkitSubscribeEventEnvelope(TypedDict):
68
+ event_id: str
69
+ source: str
70
+ timestamp_ms: int
71
+ event: Any
72
+
73
+
74
+ class MobkitSubscribeResult(TypedDict):
75
+ scope: Literal["mob", "agent", "interaction"]
76
+ replay_from_event_id: str | None
77
+ keep_alive: MobkitSubscribeKeepAlive
78
+ keep_alive_comment: str
79
+ event_frames: list[str]
80
+ events: list[MobkitSubscribeEventEnvelope]
81
+
82
+
83
+ class MobkitSubscribeParams(TypedDict, total=False):
84
+ scope: Literal["mob", "agent", "interaction"]
85
+ last_event_id: str
86
+ agent_id: str
87
+
88
+
89
+ class AsyncRpcTransport(Protocol):
90
+ async def __call__(self, request: JsonRpcRequest) -> Any:
91
+ ...
92
+
93
+
94
+ class SyncRpcTransport(Protocol):
95
+ def __call__(self, request: JsonRpcRequest) -> Any:
96
+ ...
97
+
98
+
99
+ class MobkitRpcError(RuntimeError):
100
+ def __init__(self, code: int, message: str, request_id: str, method: str):
101
+ super().__init__(message)
102
+ self.code = code
103
+ self.request_id = request_id
104
+ self.method = method
105
+
106
+
107
+ def create_gateway_sync_transport(gateway_bin: str) -> SyncRpcTransport:
108
+ def transport(request: JsonRpcRequest) -> Any:
109
+ request_json = json.dumps(request)
110
+ proc = subprocess.run(
111
+ [gateway_bin],
112
+ check=False,
113
+ capture_output=True,
114
+ text=True,
115
+ env={**os.environ, "MOBKIT_RPC_REQUEST": request_json},
116
+ )
117
+ if proc.returncode != 0:
118
+ raise RuntimeError(
119
+ f"gateway failed (status={proc.returncode}): {proc.stderr.strip()}"
120
+ )
121
+
122
+ try:
123
+ return json.loads(proc.stdout)
124
+ except json.JSONDecodeError as exc:
125
+ raise ValueError("gateway returned non-JSON response") from exc
126
+
127
+ return transport
128
+
129
+
130
+ def create_gateway_async_transport(gateway_bin: str) -> AsyncRpcTransport:
131
+ async def transport(request: JsonRpcRequest) -> Any:
132
+ request_json = json.dumps(request)
133
+ proc = await asyncio.create_subprocess_exec(
134
+ gateway_bin,
135
+ env={**os.environ, "MOBKIT_RPC_REQUEST": request_json},
136
+ stdout=asyncio.subprocess.PIPE,
137
+ stderr=asyncio.subprocess.PIPE,
138
+ )
139
+ stdout, stderr = await proc.communicate()
140
+ if proc.returncode != 0:
141
+ stderr_text = stderr.decode("utf-8", errors="replace").strip()
142
+ raise RuntimeError(
143
+ f"gateway failed (status={proc.returncode}): {stderr_text}"
144
+ )
145
+
146
+ try:
147
+ return json.loads(stdout.decode("utf-8"))
148
+ except json.JSONDecodeError as exc:
149
+ raise ValueError("gateway returned non-JSON response") from exc
150
+
151
+ return transport
152
+
153
+
154
+ def create_http_transport(
155
+ endpoint: str,
156
+ *,
157
+ headers: Mapping[str, str] | None = None,
158
+ timeout_seconds: float = 10.0,
159
+ ) -> AsyncRpcTransport:
160
+ base_headers = {"content-type": "application/json", "accept": "application/json"}
161
+ if headers:
162
+ base_headers.update(dict(headers))
163
+
164
+ async def transport(request: JsonRpcRequest) -> Any:
165
+ request_bytes = json.dumps(request).encode("utf-8")
166
+ http_request = urllib_request.Request(
167
+ endpoint,
168
+ data=request_bytes,
169
+ method="POST",
170
+ headers=base_headers,
171
+ )
172
+ try:
173
+ body = await asyncio.to_thread(_read_http_body, http_request, timeout_seconds)
174
+ except HTTPError as exc:
175
+ body = exc.read().decode("utf-8", errors="replace")
176
+ raise RuntimeError(
177
+ f"http transport failed (status={exc.code}): {body}"
178
+ ) from exc
179
+ except URLError as exc:
180
+ raise RuntimeError(f"http transport failed: {exc.reason}") from exc
181
+
182
+ try:
183
+ return json.loads(body)
184
+ except json.JSONDecodeError as exc:
185
+ raise ValueError("http transport returned non-JSON response") from exc
186
+
187
+ return transport
188
+
189
+
190
+ class MobkitTypedClient:
191
+ def __init__(self, gateway_bin: str):
192
+ self.gateway_bin = gateway_bin
193
+ self._sync_transport = create_gateway_sync_transport(gateway_bin)
194
+
195
+ @classmethod
196
+ def from_persistent(cls, transport: SyncRpcTransport) -> "MobkitTypedClient":
197
+ instance = cls.__new__(cls)
198
+ instance.gateway_bin = ""
199
+ instance._sync_transport = transport
200
+ return instance
201
+
202
+ def rpc(
203
+ self, request_id: str, method: str, params: Mapping[str, Any] | None = None
204
+ ) -> JsonRpcResponse:
205
+ payload = self._sync_transport(_build_request(request_id, method, params))
206
+ return _parse_json_rpc_response(payload, request_id)
207
+
208
+ def status(self, request_id: str = "status") -> MobkitStatusResult:
209
+ return cast(
210
+ MobkitStatusResult,
211
+ _unwrap_typed_result(
212
+ self.rpc(request_id, "mobkit/status", {}),
213
+ request_id,
214
+ "mobkit/status",
215
+ _is_status_result,
216
+ ),
217
+ )
218
+
219
+ def capabilities(self, request_id: str = "capabilities") -> MobkitCapabilitiesResult:
220
+ return cast(
221
+ MobkitCapabilitiesResult,
222
+ _unwrap_typed_result(
223
+ self.rpc(request_id, "mobkit/capabilities", {}),
224
+ request_id,
225
+ "mobkit/capabilities",
226
+ _is_capabilities_result,
227
+ ),
228
+ )
229
+
230
+ def reconcile(
231
+ self, modules: list[str], request_id: str = "reconcile"
232
+ ) -> MobkitReconcileResult:
233
+ return cast(
234
+ MobkitReconcileResult,
235
+ _unwrap_typed_result(
236
+ self.rpc(request_id, "mobkit/reconcile", {"modules": modules}),
237
+ request_id,
238
+ "mobkit/reconcile",
239
+ _is_reconcile_result,
240
+ ),
241
+ )
242
+
243
+ def spawn_member(
244
+ self, module_id: str, request_id: str = "spawn_member"
245
+ ) -> MobkitSpawnMemberResult:
246
+ return cast(
247
+ MobkitSpawnMemberResult,
248
+ _unwrap_typed_result(
249
+ self.rpc(request_id, "mobkit/spawn_member", {"module_id": module_id}),
250
+ request_id,
251
+ "mobkit/spawn_member",
252
+ _is_spawn_member_result,
253
+ ),
254
+ )
255
+
256
+ def subscribe_events(
257
+ self,
258
+ params: MobkitSubscribeParams | None = None,
259
+ request_id: str = "events_subscribe",
260
+ ) -> MobkitSubscribeResult:
261
+ return cast(
262
+ MobkitSubscribeResult,
263
+ _unwrap_typed_result(
264
+ self.rpc(
265
+ request_id,
266
+ "mobkit/events/subscribe",
267
+ dict(params) if params is not None else {},
268
+ ),
269
+ request_id,
270
+ "mobkit/events/subscribe",
271
+ _is_subscribe_result,
272
+ ),
273
+ )
274
+
275
+
276
+ class MobkitAsyncTypedClient:
277
+ def __init__(self, transport: AsyncRpcTransport):
278
+ self._transport = transport
279
+
280
+ @classmethod
281
+ def from_gateway_bin(cls, gateway_bin: str) -> "MobkitAsyncTypedClient":
282
+ return cls(create_gateway_async_transport(gateway_bin))
283
+
284
+ @classmethod
285
+ def from_http(
286
+ cls,
287
+ endpoint: str,
288
+ *,
289
+ headers: Mapping[str, str] | None = None,
290
+ timeout_seconds: float = 10.0,
291
+ ) -> "MobkitAsyncTypedClient":
292
+ return cls(
293
+ create_http_transport(
294
+ endpoint,
295
+ headers=headers,
296
+ timeout_seconds=timeout_seconds,
297
+ )
298
+ )
299
+
300
+ async def rpc(
301
+ self, request_id: str, method: str, params: Mapping[str, Any] | None = None
302
+ ) -> JsonRpcResponse:
303
+ payload = await self._transport(_build_request(request_id, method, params))
304
+ return _parse_json_rpc_response(payload, request_id)
305
+
306
+ async def request(
307
+ self,
308
+ request_id: str,
309
+ method: str,
310
+ params: Mapping[str, Any] | None,
311
+ validator: Callable[[Any], bool],
312
+ ) -> Any:
313
+ response = await self.rpc(request_id, method, params)
314
+ return _unwrap_typed_result(response, request_id, method, validator)
315
+
316
+ async def status(self, request_id: str = "status") -> MobkitStatusResult:
317
+ return cast(
318
+ MobkitStatusResult,
319
+ await self.request(request_id, "mobkit/status", {}, _is_status_result),
320
+ )
321
+
322
+ async def capabilities(
323
+ self, request_id: str = "capabilities"
324
+ ) -> MobkitCapabilitiesResult:
325
+ return cast(
326
+ MobkitCapabilitiesResult,
327
+ await self.request(
328
+ request_id,
329
+ "mobkit/capabilities",
330
+ {},
331
+ _is_capabilities_result,
332
+ ),
333
+ )
334
+
335
+ async def reconcile(
336
+ self, modules: list[str], request_id: str = "reconcile"
337
+ ) -> MobkitReconcileResult:
338
+ return cast(
339
+ MobkitReconcileResult,
340
+ await self.request(
341
+ request_id,
342
+ "mobkit/reconcile",
343
+ {"modules": modules},
344
+ _is_reconcile_result,
345
+ ),
346
+ )
347
+
348
+ async def spawn_member(
349
+ self, module_id: str, request_id: str = "spawn_member"
350
+ ) -> MobkitSpawnMemberResult:
351
+ return cast(
352
+ MobkitSpawnMemberResult,
353
+ await self.request(
354
+ request_id,
355
+ "mobkit/spawn_member",
356
+ {"module_id": module_id},
357
+ _is_spawn_member_result,
358
+ ),
359
+ )
360
+
361
+ async def subscribe_events(
362
+ self,
363
+ params: MobkitSubscribeParams | None = None,
364
+ request_id: str = "events_subscribe",
365
+ ) -> MobkitSubscribeResult:
366
+ return cast(
367
+ MobkitSubscribeResult,
368
+ await self.request(
369
+ request_id,
370
+ "mobkit/events/subscribe",
371
+ dict(params) if params is not None else {},
372
+ _is_subscribe_result,
373
+ ),
374
+ )
375
+
376
+
377
+ def _read_http_body(http_request: urllib_request.Request, timeout_seconds: float) -> str:
378
+ with urllib_request.urlopen(http_request, timeout=timeout_seconds) as response:
379
+ return response.read().decode("utf-8")
380
+
381
+
382
+ def _build_request(
383
+ request_id: str,
384
+ method: str,
385
+ params: Mapping[str, Any] | None,
386
+ ) -> JsonRpcRequest:
387
+ return {
388
+ "jsonrpc": "2.0",
389
+ "id": request_id,
390
+ "method": method,
391
+ "params": dict(params) if params is not None else {},
392
+ }
393
+
394
+
395
+ def _parse_json_rpc_response(payload: Any, request_id: str) -> JsonRpcResponse:
396
+ if not isinstance(payload, dict):
397
+ raise ValueError("invalid JSON-RPC response envelope")
398
+ if payload.get("jsonrpc") != "2.0" or payload.get("id") != request_id:
399
+ raise ValueError("invalid JSON-RPC response envelope")
400
+
401
+ has_result = "result" in payload
402
+ has_error = "error" in payload
403
+ if has_result == has_error:
404
+ raise ValueError("invalid JSON-RPC response envelope")
405
+
406
+ if has_error:
407
+ error = payload.get("error")
408
+ if not isinstance(error, dict):
409
+ raise ValueError("invalid JSON-RPC response envelope")
410
+ code = error.get("code")
411
+ message = error.get("message")
412
+ if not isinstance(code, int) or isinstance(code, bool):
413
+ raise ValueError("invalid JSON-RPC response envelope")
414
+ if not isinstance(message, str):
415
+ raise ValueError("invalid JSON-RPC response envelope")
416
+
417
+ return cast(JsonRpcResponse, payload)
418
+
419
+
420
+ def _unwrap_typed_result(
421
+ response: JsonRpcResponse,
422
+ request_id: str,
423
+ method: str,
424
+ validator: Callable[[Any], bool],
425
+ ) -> Any:
426
+ if "error" in response:
427
+ error = response["error"]
428
+ raise MobkitRpcError(error["code"], error["message"], request_id, method)
429
+
430
+ result = response["result"]
431
+ if not validator(result):
432
+ raise ValueError(f"invalid result payload for {method}")
433
+ return result
434
+
435
+
436
+ def _is_status_result(value: Any) -> bool:
437
+ return (
438
+ isinstance(value, dict)
439
+ and isinstance(value.get("contract_version"), str)
440
+ and isinstance(value.get("running"), bool)
441
+ and _is_string_list(value.get("loaded_modules"))
442
+ )
443
+
444
+
445
+ def _is_capabilities_result(value: Any) -> bool:
446
+ return (
447
+ isinstance(value, dict)
448
+ and isinstance(value.get("contract_version"), str)
449
+ and _is_string_list(value.get("methods"))
450
+ and _is_string_list(value.get("loaded_modules"))
451
+ )
452
+
453
+
454
+ def _is_reconcile_result(value: Any) -> bool:
455
+ return (
456
+ isinstance(value, dict)
457
+ and isinstance(value.get("accepted"), bool)
458
+ and _is_string_list(value.get("reconciled_modules"))
459
+ and isinstance(value.get("added"), int)
460
+ and not isinstance(value.get("added"), bool)
461
+ )
462
+
463
+
464
+ def _is_spawn_member_result(value: Any) -> bool:
465
+ return (
466
+ isinstance(value, dict)
467
+ and isinstance(value.get("accepted"), bool)
468
+ and isinstance(value.get("module_id"), str)
469
+ )
470
+
471
+
472
+ def _is_subscribe_result(value: Any) -> bool:
473
+ if not isinstance(value, dict):
474
+ return False
475
+
476
+ scope = value.get("scope")
477
+ if scope not in {"mob", "agent", "interaction"}:
478
+ return False
479
+
480
+ replay = value.get("replay_from_event_id")
481
+ if replay is not None and not isinstance(replay, str):
482
+ return False
483
+
484
+ keep_alive = value.get("keep_alive")
485
+ if not isinstance(keep_alive, dict):
486
+ return False
487
+
488
+ interval = keep_alive.get("interval_ms")
489
+ if not isinstance(interval, int) or isinstance(interval, bool):
490
+ return False
491
+ if not isinstance(keep_alive.get("event"), str):
492
+ return False
493
+
494
+ if not isinstance(value.get("keep_alive_comment"), str):
495
+ return False
496
+
497
+ if not _is_string_list(value.get("event_frames")):
498
+ return False
499
+
500
+ events = value.get("events")
501
+ if not isinstance(events, list):
502
+ return False
503
+
504
+ for event in events:
505
+ if not isinstance(event, dict):
506
+ return False
507
+ timestamp = event.get("timestamp_ms")
508
+ if (
509
+ not isinstance(event.get("event_id"), str)
510
+ or not isinstance(event.get("source"), str)
511
+ or not isinstance(timestamp, int)
512
+ or isinstance(timestamp, bool)
513
+ or "event" not in event
514
+ ):
515
+ return False
516
+
517
+ return True
518
+
519
+
520
+ def _is_string_list(value: Any) -> bool:
521
+ return isinstance(value, list) and all(isinstance(item, str) for item in value)