dory-sdk 2.1.0__py3-none-any.whl → 2.1.4__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.
@@ -0,0 +1,749 @@
1
+ """State format version negotiation.
2
+
3
+ Provides version-aware serialization and deserialization to ensure
4
+ compatibility between different versions of the SDK and Orchestrator.
5
+
6
+ Format Versions:
7
+ v1.0 - Original SDK format (StateEnvelope with payload/metadata/checksum)
8
+ v2.0 - Orchestrator-compatible format (ApplicationState structure)
9
+
10
+ The SDK supports reading both formats and can write in either format
11
+ depending on the target version.
12
+ """
13
+
14
+ import hashlib
15
+ import json
16
+ import logging
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from enum import Enum
20
+ from typing import Any, Callable
21
+
22
+ from dory.utils.errors import DoryStateError
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ # =============================================================================
28
+ # Version Constants
29
+ # =============================================================================
30
+
31
+ class StateFormatVersion(Enum):
32
+ """State format versions."""
33
+
34
+ V1_0 = "1.0" # Original SDK format (StateEnvelope)
35
+ V2_0 = "2.0" # Orchestrator-compatible format (ApplicationState)
36
+
37
+ @classmethod
38
+ def latest(cls) -> "StateFormatVersion":
39
+ """Get the latest format version."""
40
+ return cls.V2_0
41
+
42
+ @classmethod
43
+ def from_string(cls, version: str) -> "StateFormatVersion":
44
+ """Parse version string.
45
+
46
+ Args:
47
+ version: Version string like "1.0" or "2.0"
48
+
49
+ Returns:
50
+ StateFormatVersion enum
51
+
52
+ Raises:
53
+ ValueError: If version is not recognized
54
+ """
55
+ # Handle various formats
56
+ version = version.strip().lstrip("v")
57
+
58
+ for v in cls:
59
+ if v.value == version:
60
+ return v
61
+
62
+ raise ValueError(f"Unknown state format version: {version}")
63
+
64
+ def is_compatible_with(self, other: "StateFormatVersion") -> bool:
65
+ """Check if this version can read data from another version."""
66
+ # V2.0 can read V1.0 (backward compatible)
67
+ # V1.0 cannot read V2.0 (forward incompatible)
68
+ if self == StateFormatVersion.V2_0:
69
+ return True # V2 can read all versions
70
+ elif self == StateFormatVersion.V1_0:
71
+ return other == StateFormatVersion.V1_0
72
+ return False
73
+
74
+
75
+ # Current SDK version
76
+ SDK_STATE_VERSION = StateFormatVersion.V2_0
77
+
78
+ # Minimum supported version for reading
79
+ MIN_SUPPORTED_VERSION = StateFormatVersion.V1_0
80
+
81
+
82
+ # =============================================================================
83
+ # State Structures
84
+ # =============================================================================
85
+
86
+ @dataclass
87
+ class StateMetadata:
88
+ """Metadata accompanying state data."""
89
+
90
+ # Timing
91
+ timestamp: float = field(default_factory=time.time)
92
+ timestamp_iso: str = ""
93
+
94
+ # Identity
95
+ processor_id: str = ""
96
+ pod_name: str = ""
97
+ app_name: str = ""
98
+
99
+ # Runtime info
100
+ restart_count: int = 0
101
+ uptime_seconds: float = 0.0
102
+ request_count: int = 0
103
+
104
+ # Version info
105
+ format_version: str = SDK_STATE_VERSION.value
106
+ sdk_version: str = ""
107
+
108
+ def __post_init__(self):
109
+ """Set derived fields."""
110
+ if not self.timestamp_iso:
111
+ self.timestamp_iso = time.strftime(
112
+ "%Y-%m-%dT%H:%M:%SZ",
113
+ time.gmtime(self.timestamp)
114
+ )
115
+
116
+ def to_dict(self) -> dict[str, Any]:
117
+ """Convert to dictionary."""
118
+ return {
119
+ "timestamp": self.timestamp,
120
+ "timestamp_iso": self.timestamp_iso,
121
+ "processor_id": self.processor_id,
122
+ "pod_name": self.pod_name,
123
+ "app_name": self.app_name,
124
+ "restart_count": self.restart_count,
125
+ "uptime_seconds": self.uptime_seconds,
126
+ "request_count": self.request_count,
127
+ "format_version": self.format_version,
128
+ "sdk_version": self.sdk_version,
129
+ }
130
+
131
+ @classmethod
132
+ def from_dict(cls, data: dict[str, Any]) -> "StateMetadata":
133
+ """Create from dictionary."""
134
+ return cls(
135
+ timestamp=data.get("timestamp", time.time()),
136
+ timestamp_iso=data.get("timestamp_iso", ""),
137
+ processor_id=data.get("processor_id", ""),
138
+ pod_name=data.get("pod_name", ""),
139
+ app_name=data.get("app_name", ""),
140
+ restart_count=data.get("restart_count", 0),
141
+ uptime_seconds=data.get("uptime_seconds", 0.0),
142
+ request_count=data.get("request_count", 0),
143
+ format_version=data.get("format_version", SDK_STATE_VERSION.value),
144
+ sdk_version=data.get("sdk_version", ""),
145
+ )
146
+
147
+
148
+ @dataclass
149
+ class VersionedState:
150
+ """Version-aware state container.
151
+
152
+ This is the canonical internal representation that can be
153
+ serialized to any supported format version.
154
+ """
155
+
156
+ # Core data
157
+ data: dict[str, Any]
158
+ metadata: StateMetadata
159
+
160
+ # Integrity
161
+ checksum: str = ""
162
+
163
+ # Optional fields (for Orchestrator compatibility)
164
+ metrics: dict[str, float] = field(default_factory=dict)
165
+ connections: list[str] = field(default_factory=list)
166
+ active_sessions: int = 0
167
+ session_data: dict[str, str] = field(default_factory=dict)
168
+
169
+ def __post_init__(self):
170
+ """Compute checksum if not provided."""
171
+ if not self.checksum:
172
+ self.checksum = self._compute_checksum()
173
+
174
+ def _compute_checksum(self) -> str:
175
+ """Compute SHA256 checksum of data."""
176
+ data_json = json.dumps(self.data, sort_keys=True)
177
+ return hashlib.sha256(data_json.encode()).hexdigest()
178
+
179
+ def validate_checksum(self) -> bool:
180
+ """Validate that checksum matches data."""
181
+ expected = self._compute_checksum()
182
+ return self.checksum == expected
183
+
184
+ @property
185
+ def format_version(self) -> StateFormatVersion:
186
+ """Get format version."""
187
+ return StateFormatVersion.from_string(self.metadata.format_version)
188
+
189
+
190
+ # =============================================================================
191
+ # Version Detection
192
+ # =============================================================================
193
+
194
+ def detect_format_version(data: dict[str, Any]) -> StateFormatVersion:
195
+ """Detect the format version of serialized state.
196
+
197
+ Args:
198
+ data: Parsed JSON state data
199
+
200
+ Returns:
201
+ Detected StateFormatVersion
202
+ """
203
+ # Check for explicit version field
204
+ if "state_version" in data:
205
+ # Orchestrator V2 format
206
+ try:
207
+ return StateFormatVersion.from_string(data["state_version"])
208
+ except ValueError:
209
+ pass
210
+
211
+ if "format_version" in data.get("metadata", {}):
212
+ # SDK format with version in metadata
213
+ try:
214
+ return StateFormatVersion.from_string(data["metadata"]["format_version"])
215
+ except ValueError:
216
+ pass
217
+
218
+ # Check structure to detect format
219
+ if "payload" in data and "metadata" in data and "checksum" in data:
220
+ # V1.0 SDK format (StateEnvelope)
221
+ return StateFormatVersion.V1_0
222
+
223
+ if "data" in data and ("pod_name" in data or "app_name" in data):
224
+ # V2.0 Orchestrator format (ApplicationState)
225
+ return StateFormatVersion.V2_0
226
+
227
+ # Default to V1.0 for backward compatibility
228
+ logger.warning("Could not detect state format version, assuming V1.0")
229
+ return StateFormatVersion.V1_0
230
+
231
+
232
+ # =============================================================================
233
+ # Serializers
234
+ # =============================================================================
235
+
236
+ class StateSerializerV1:
237
+ """Serializer for V1.0 format (original SDK StateEnvelope)."""
238
+
239
+ @staticmethod
240
+ def serialize(state: VersionedState) -> str:
241
+ """Serialize to V1.0 format.
242
+
243
+ Args:
244
+ state: VersionedState to serialize
245
+
246
+ Returns:
247
+ JSON string in V1.0 format
248
+ """
249
+ envelope = {
250
+ "payload": state.data,
251
+ "metadata": {
252
+ "timestamp": state.metadata.timestamp,
253
+ "timestamp_iso": state.metadata.timestamp_iso,
254
+ "processor_id": state.metadata.processor_id,
255
+ "pod_name": state.metadata.pod_name,
256
+ "restart_count": state.metadata.restart_count,
257
+ },
258
+ "checksum": state.checksum,
259
+ }
260
+ return json.dumps(envelope, indent=2)
261
+
262
+ @staticmethod
263
+ def deserialize(data: str) -> VersionedState:
264
+ """Deserialize from V1.0 format.
265
+
266
+ Args:
267
+ data: JSON string in V1.0 format
268
+
269
+ Returns:
270
+ VersionedState
271
+
272
+ Raises:
273
+ DoryStateError: If deserialization fails
274
+ """
275
+ try:
276
+ envelope = json.loads(data)
277
+ except json.JSONDecodeError as e:
278
+ raise DoryStateError(f"Invalid JSON: {e}", cause=e)
279
+
280
+ if "payload" not in envelope:
281
+ raise DoryStateError("Missing 'payload' field in V1.0 state")
282
+
283
+ metadata_dict = envelope.get("metadata", {})
284
+ metadata = StateMetadata(
285
+ timestamp=metadata_dict.get("timestamp", time.time()),
286
+ timestamp_iso=metadata_dict.get("timestamp_iso", ""),
287
+ processor_id=metadata_dict.get("processor_id", ""),
288
+ pod_name=metadata_dict.get("pod_name", ""),
289
+ restart_count=metadata_dict.get("restart_count", 0),
290
+ format_version=StateFormatVersion.V1_0.value,
291
+ )
292
+
293
+ return VersionedState(
294
+ data=envelope["payload"],
295
+ metadata=metadata,
296
+ checksum=envelope.get("checksum", ""),
297
+ )
298
+
299
+
300
+ class StateSerializerV2:
301
+ """Serializer for V2.0 format (Orchestrator ApplicationState)."""
302
+
303
+ @staticmethod
304
+ def serialize(state: VersionedState) -> str:
305
+ """Serialize to V2.0 format (Orchestrator-compatible).
306
+
307
+ Args:
308
+ state: VersionedState to serialize
309
+
310
+ Returns:
311
+ JSON string in V2.0 format
312
+ """
313
+ app_state = {
314
+ # Metadata
315
+ "pod_name": state.metadata.pod_name,
316
+ "app_name": state.metadata.app_name or state.metadata.processor_id,
317
+ "captured_at": state.metadata.timestamp_iso,
318
+ "state_version": StateFormatVersion.V2_0.value,
319
+
320
+ # Application data
321
+ "data": state.data,
322
+ "metrics": state.metrics,
323
+ "connections": state.connections,
324
+
325
+ # Session info
326
+ "active_sessions": state.active_sessions,
327
+ "session_data": state.session_data,
328
+
329
+ # Runtime
330
+ "uptime_seconds": state.metadata.uptime_seconds,
331
+ "request_count": state.metadata.request_count,
332
+ "last_health_time": state.metadata.timestamp_iso,
333
+
334
+ # SDK-specific (for round-trip compatibility)
335
+ "_sdk_metadata": {
336
+ "processor_id": state.metadata.processor_id,
337
+ "restart_count": state.metadata.restart_count,
338
+ "checksum": state.checksum,
339
+ "format_version": StateFormatVersion.V2_0.value,
340
+ "sdk_version": state.metadata.sdk_version,
341
+ },
342
+ }
343
+ return json.dumps(app_state, indent=2)
344
+
345
+ @staticmethod
346
+ def deserialize(data: str) -> VersionedState:
347
+ """Deserialize from V2.0 format.
348
+
349
+ Args:
350
+ data: JSON string in V2.0 format
351
+
352
+ Returns:
353
+ VersionedState
354
+
355
+ Raises:
356
+ DoryStateError: If deserialization fails
357
+ """
358
+ try:
359
+ app_state = json.loads(data)
360
+ except json.JSONDecodeError as e:
361
+ raise DoryStateError(f"Invalid JSON: {e}", cause=e)
362
+
363
+ # Extract SDK metadata if present
364
+ sdk_metadata = app_state.get("_sdk_metadata", {})
365
+
366
+ # Parse captured_at timestamp
367
+ timestamp = time.time()
368
+ timestamp_iso = app_state.get("captured_at", "")
369
+ if timestamp_iso:
370
+ try:
371
+ import datetime
372
+ dt = datetime.datetime.fromisoformat(timestamp_iso.replace("Z", "+00:00"))
373
+ timestamp = dt.timestamp()
374
+ except (ValueError, AttributeError):
375
+ pass
376
+
377
+ metadata = StateMetadata(
378
+ timestamp=timestamp,
379
+ timestamp_iso=timestamp_iso,
380
+ processor_id=sdk_metadata.get("processor_id", app_state.get("app_name", "")),
381
+ pod_name=app_state.get("pod_name", ""),
382
+ app_name=app_state.get("app_name", ""),
383
+ restart_count=sdk_metadata.get("restart_count", 0),
384
+ uptime_seconds=app_state.get("uptime_seconds", 0.0),
385
+ request_count=app_state.get("request_count", 0),
386
+ format_version=StateFormatVersion.V2_0.value,
387
+ sdk_version=sdk_metadata.get("sdk_version", ""),
388
+ )
389
+
390
+ return VersionedState(
391
+ data=app_state.get("data", {}),
392
+ metadata=metadata,
393
+ checksum=sdk_metadata.get("checksum", ""),
394
+ metrics=app_state.get("metrics", {}),
395
+ connections=app_state.get("connections", []),
396
+ active_sessions=app_state.get("active_sessions", 0),
397
+ session_data=app_state.get("session_data", {}),
398
+ )
399
+
400
+
401
+ # =============================================================================
402
+ # Version Negotiation
403
+ # =============================================================================
404
+
405
+ @dataclass
406
+ class VersionNegotiationResult:
407
+ """Result of version negotiation."""
408
+
409
+ agreed_version: StateFormatVersion
410
+ sdk_version: StateFormatVersion
411
+ peer_version: StateFormatVersion | None
412
+ is_compatible: bool
413
+ migration_required: bool
414
+ message: str
415
+
416
+
417
+ class VersionNegotiator:
418
+ """Negotiates state format version between SDK and Orchestrator.
419
+
420
+ Usage:
421
+ negotiator = VersionNegotiator()
422
+
423
+ # Check if we can read incoming state
424
+ result = negotiator.negotiate_read("2.0")
425
+ if result.is_compatible:
426
+ state = negotiator.deserialize(json_data)
427
+
428
+ # Serialize for target version
429
+ json_data = negotiator.serialize(state, target_version="2.0")
430
+ """
431
+
432
+ def __init__(
433
+ self,
434
+ sdk_version: StateFormatVersion = SDK_STATE_VERSION,
435
+ min_version: StateFormatVersion = MIN_SUPPORTED_VERSION,
436
+ ):
437
+ """Initialize negotiator.
438
+
439
+ Args:
440
+ sdk_version: This SDK's state format version
441
+ min_version: Minimum version this SDK can read
442
+ """
443
+ self._sdk_version = sdk_version
444
+ self._min_version = min_version
445
+ self._serializers = {
446
+ StateFormatVersion.V1_0: StateSerializerV1(),
447
+ StateFormatVersion.V2_0: StateSerializerV2(),
448
+ }
449
+
450
+ @property
451
+ def sdk_version(self) -> StateFormatVersion:
452
+ """Get SDK's state format version."""
453
+ return self._sdk_version
454
+
455
+ @property
456
+ def supported_versions(self) -> list[StateFormatVersion]:
457
+ """Get list of supported versions."""
458
+ return list(StateFormatVersion)
459
+
460
+ def negotiate_read(
461
+ self,
462
+ peer_version: str | StateFormatVersion | None = None,
463
+ ) -> VersionNegotiationResult:
464
+ """Negotiate version for reading state from peer.
465
+
466
+ Args:
467
+ peer_version: Version string from peer (e.g., "1.0", "2.0")
468
+
469
+ Returns:
470
+ VersionNegotiationResult
471
+ """
472
+ if peer_version is None:
473
+ # No version info, assume we can read with SDK version
474
+ return VersionNegotiationResult(
475
+ agreed_version=self._sdk_version,
476
+ sdk_version=self._sdk_version,
477
+ peer_version=None,
478
+ is_compatible=True,
479
+ migration_required=False,
480
+ message="No peer version provided, using SDK version",
481
+ )
482
+
483
+ if isinstance(peer_version, str):
484
+ try:
485
+ peer_version = StateFormatVersion.from_string(peer_version)
486
+ except ValueError:
487
+ return VersionNegotiationResult(
488
+ agreed_version=self._sdk_version,
489
+ sdk_version=self._sdk_version,
490
+ peer_version=None,
491
+ is_compatible=False,
492
+ migration_required=False,
493
+ message=f"Unknown peer version: {peer_version}",
494
+ )
495
+
496
+ # Check compatibility
497
+ is_compatible = self._sdk_version.is_compatible_with(peer_version)
498
+ migration_required = peer_version != self._sdk_version
499
+
500
+ if is_compatible:
501
+ message = f"Compatible: SDK {self._sdk_version.value} can read {peer_version.value}"
502
+ else:
503
+ message = f"Incompatible: SDK {self._sdk_version.value} cannot read {peer_version.value}"
504
+
505
+ return VersionNegotiationResult(
506
+ agreed_version=peer_version if is_compatible else self._sdk_version,
507
+ sdk_version=self._sdk_version,
508
+ peer_version=peer_version,
509
+ is_compatible=is_compatible,
510
+ migration_required=migration_required,
511
+ message=message,
512
+ )
513
+
514
+ def negotiate_write(
515
+ self,
516
+ target_version: str | StateFormatVersion | None = None,
517
+ ) -> VersionNegotiationResult:
518
+ """Negotiate version for writing state.
519
+
520
+ Args:
521
+ target_version: Requested target version
522
+
523
+ Returns:
524
+ VersionNegotiationResult
525
+ """
526
+ if target_version is None:
527
+ # Use SDK's default version
528
+ return VersionNegotiationResult(
529
+ agreed_version=self._sdk_version,
530
+ sdk_version=self._sdk_version,
531
+ peer_version=None,
532
+ is_compatible=True,
533
+ migration_required=False,
534
+ message=f"Using SDK default version: {self._sdk_version.value}",
535
+ )
536
+
537
+ if isinstance(target_version, str):
538
+ try:
539
+ target_version = StateFormatVersion.from_string(target_version)
540
+ except ValueError:
541
+ return VersionNegotiationResult(
542
+ agreed_version=self._sdk_version,
543
+ sdk_version=self._sdk_version,
544
+ peer_version=None,
545
+ is_compatible=False,
546
+ migration_required=False,
547
+ message=f"Unknown target version: {target_version}",
548
+ )
549
+
550
+ # Check if we can write this version
551
+ can_write = target_version in self._serializers
552
+
553
+ return VersionNegotiationResult(
554
+ agreed_version=target_version if can_write else self._sdk_version,
555
+ sdk_version=self._sdk_version,
556
+ peer_version=target_version,
557
+ is_compatible=can_write,
558
+ migration_required=target_version != self._sdk_version,
559
+ message=f"{'Can' if can_write else 'Cannot'} write version {target_version.value}",
560
+ )
561
+
562
+ def serialize(
563
+ self,
564
+ state: VersionedState,
565
+ target_version: StateFormatVersion | str | None = None,
566
+ ) -> str:
567
+ """Serialize state to specified format version.
568
+
569
+ Args:
570
+ state: State to serialize
571
+ target_version: Target format version (default: SDK version)
572
+
573
+ Returns:
574
+ JSON string in target format
575
+
576
+ Raises:
577
+ DoryStateError: If serialization fails
578
+ """
579
+ if target_version is None:
580
+ target_version = self._sdk_version
581
+ elif isinstance(target_version, str):
582
+ target_version = StateFormatVersion.from_string(target_version)
583
+
584
+ serializer = self._serializers.get(target_version)
585
+ if not serializer:
586
+ raise DoryStateError(f"No serializer for version {target_version.value}")
587
+
588
+ return serializer.serialize(state)
589
+
590
+ def deserialize(self, data: str) -> VersionedState:
591
+ """Deserialize state, auto-detecting format version.
592
+
593
+ Args:
594
+ data: JSON string
595
+
596
+ Returns:
597
+ VersionedState
598
+
599
+ Raises:
600
+ DoryStateError: If deserialization fails
601
+ """
602
+ try:
603
+ parsed = json.loads(data)
604
+ except json.JSONDecodeError as e:
605
+ raise DoryStateError(f"Invalid JSON: {e}", cause=e)
606
+
607
+ version = detect_format_version(parsed)
608
+
609
+ serializer = self._serializers.get(version)
610
+ if not serializer:
611
+ raise DoryStateError(f"No deserializer for version {version.value}")
612
+
613
+ return serializer.deserialize(data)
614
+
615
+ def migrate(
616
+ self,
617
+ state: VersionedState,
618
+ target_version: StateFormatVersion,
619
+ ) -> VersionedState:
620
+ """Migrate state to a different format version.
621
+
622
+ This doesn't change the data, just updates the metadata
623
+ to indicate the new format version.
624
+
625
+ Args:
626
+ state: State to migrate
627
+ target_version: Target format version
628
+
629
+ Returns:
630
+ New VersionedState with updated version
631
+ """
632
+ # Create new metadata with updated version
633
+ new_metadata = StateMetadata(
634
+ timestamp=state.metadata.timestamp,
635
+ timestamp_iso=state.metadata.timestamp_iso,
636
+ processor_id=state.metadata.processor_id,
637
+ pod_name=state.metadata.pod_name,
638
+ app_name=state.metadata.app_name,
639
+ restart_count=state.metadata.restart_count,
640
+ uptime_seconds=state.metadata.uptime_seconds,
641
+ request_count=state.metadata.request_count,
642
+ format_version=target_version.value,
643
+ sdk_version=state.metadata.sdk_version,
644
+ )
645
+
646
+ return VersionedState(
647
+ data=state.data,
648
+ metadata=new_metadata,
649
+ checksum=state.checksum,
650
+ metrics=state.metrics,
651
+ connections=state.connections,
652
+ active_sessions=state.active_sessions,
653
+ session_data=state.session_data,
654
+ )
655
+
656
+
657
+ # =============================================================================
658
+ # Convenience Functions
659
+ # =============================================================================
660
+
661
+ # Global negotiator instance
662
+ _default_negotiator = VersionNegotiator()
663
+
664
+
665
+ def get_sdk_version() -> str:
666
+ """Get SDK's state format version string."""
667
+ return SDK_STATE_VERSION.value
668
+
669
+
670
+ def get_supported_versions() -> list[str]:
671
+ """Get list of supported version strings."""
672
+ return [v.value for v in StateFormatVersion]
673
+
674
+
675
+ def is_version_supported(version: str) -> bool:
676
+ """Check if a version is supported."""
677
+ try:
678
+ StateFormatVersion.from_string(version)
679
+ return True
680
+ except ValueError:
681
+ return False
682
+
683
+
684
+ def serialize_state(
685
+ data: dict[str, Any],
686
+ processor_id: str,
687
+ pod_name: str = "",
688
+ target_version: str | None = None,
689
+ **kwargs: Any,
690
+ ) -> str:
691
+ """Serialize state data with version negotiation.
692
+
693
+ Args:
694
+ data: State data dictionary
695
+ processor_id: Processor identifier
696
+ pod_name: Pod name
697
+ target_version: Target format version (default: latest)
698
+ **kwargs: Additional metadata fields
699
+
700
+ Returns:
701
+ JSON string in target format
702
+ """
703
+ metadata = StateMetadata(
704
+ processor_id=processor_id,
705
+ pod_name=pod_name,
706
+ app_name=kwargs.get("app_name", processor_id),
707
+ restart_count=kwargs.get("restart_count", 0),
708
+ uptime_seconds=kwargs.get("uptime_seconds", 0.0),
709
+ request_count=kwargs.get("request_count", 0),
710
+ sdk_version=kwargs.get("sdk_version", ""),
711
+ )
712
+
713
+ state = VersionedState(
714
+ data=data,
715
+ metadata=metadata,
716
+ metrics=kwargs.get("metrics", {}),
717
+ connections=kwargs.get("connections", []),
718
+ active_sessions=kwargs.get("active_sessions", 0),
719
+ session_data=kwargs.get("session_data", {}),
720
+ )
721
+
722
+ return _default_negotiator.serialize(state, target_version)
723
+
724
+
725
+ def deserialize_state(data: str) -> tuple[dict[str, Any], StateMetadata]:
726
+ """Deserialize state data with version auto-detection.
727
+
728
+ Args:
729
+ data: JSON string
730
+
731
+ Returns:
732
+ Tuple of (state_data, metadata)
733
+ """
734
+ state = _default_negotiator.deserialize(data)
735
+ return state.data, state.metadata
736
+
737
+
738
+ def get_version_info() -> dict[str, Any]:
739
+ """Get version information for health/status endpoints.
740
+
741
+ Returns:
742
+ Dictionary with version information
743
+ """
744
+ return {
745
+ "state_format_version": SDK_STATE_VERSION.value,
746
+ "supported_versions": get_supported_versions(),
747
+ "min_supported_version": MIN_SUPPORTED_VERSION.value,
748
+ "latest_version": StateFormatVersion.latest().value,
749
+ }