[object Object]

← back to Exo

feat: Implement Many Interfaces

c0df8e5463041d2781d4f5954da1f00c8251a094 · 2025-07-01 01:37:00 +0100 · Arbion Halili

Files touched

Diff

commit c0df8e5463041d2781d4f5954da1f00c8251a094
Author: Arbion Halili <99731180+ToxicPine@users.noreply.github.com>
Date:   Tue Jul 1 01:37:00 2025 +0100

    feat: Implement Many Interfaces
---
 master/api.py                                      | 45 ++++--------
 master/idempotency.py                              |  2 +-
 shared/types/common.py                             |  9 +++
 .../types/{event_sourcing.py => events/common.py}  |  0
 shared/types/{ => events}/events.py                | 14 +---
 shared/types/models/common.py                      | 22 ++++++
 shared/types/models/metadata.py                    |  8 ++
 shared/types/{model.py => models/sources.py}       | 27 +------
 shared/types/networking/edges.py                   | 85 ++++++++++++++++++++++
 shared/types/networking/services.py                | 42 +++++++++++
 shared/types/networking/topology.py                | 22 ++++++
 shared/types/profiling/common.py                   |  4 +
 shared/types/states/master.py                      | 17 +++++
 shared/types/states/shared.py                      | 10 +++
 shared/types/states/worker.py                      | 14 ++++
 shared/types/tasks/common.py                       | 39 ++++++++++
 shared/types/worker/common.py                      | 13 ++++
 shared/types/{instance.py => worker/downloads.py}  | 59 +++++++--------
 shared/types/worker/instances.py                   | 18 +++++
 shared/types/worker/shards.py                      | 29 ++++++++
 20 files changed, 384 insertions(+), 95 deletions(-)

diff --git a/master/api.py b/master/api.py
index b5455c1d..cc45c786 100644
--- a/master/api.py
+++ b/master/api.py
@@ -1,39 +1,26 @@
-from typing import NewType
-from shared.types.model import ModelId, DownloadProgress, ModelMeta, ModelSource
-from shared.types.instance import InstanceId, Instance
-from shared.types.model import Topology
+from typing import Protocol
 
+from shared.types.models.common import Model, ModelId
+from shared.types.models.sources import ModelSource
+from shared.types.networking.topology import Topology
+from shared.types.worker.common import InstanceId
+from shared.types.worker.downloads import DownloadProgress
+from shared.types.worker.instances import Instance
 
-RequestId = NewType("RequestId", str)
 
+class ControlPlaneAPI(Protocol):
+    def get_topology(self) -> Topology: ...
 
-class ControlPlaneAPI:
-    def get_cluster_state() -> ClusterState:
-        ...
+    def list_instances(self) -> list[Instance]: ...
 
-    def get_topology() -> Topology:
-        ...
+    def get_instance(self, instance_id: InstanceId) -> Instance: ...
 
-    def get_instances() -> list[Instance]:
-        ...
+    def create_instance(self, model_id: ModelId) -> InstanceId: ...
 
-    def get_instance(instance_id: InstanceId) -> Instance:
-        ...
+    def remove_instance(self, instance_id: InstanceId) -> None: ...
 
-    def create_instance(model_id: ModelId) -> InstanceId:
-        ...
-    
-    def delete_instance(instance_id: InstanceId) -> None:
-        ...
-    
-    def get_model_meta(model_id: ModelId) -> ModelMeta:
-        ...
+    def get_model_data(self, model_id: ModelId) -> Model: ...
 
-    def download_model(model_source: ModelSource) -> InstanceId:
-        ...
-        
-    def get_model_download_progress(instance_id: InstanceId) -> DownloadProgress:
-        ...
+    def download_model(self, model_id: ModelId, model_source: ModelSource) -> None: ...
 
-    def create_chat_completion_request() -> RequestId:
-        ...
+    def get_download_progress(self, model_id: ModelId) -> DownloadProgress: ...
diff --git a/master/idempotency.py b/master/idempotency.py
index 661d1e44..d96be620 100644
--- a/master/idempotency.py
+++ b/master/idempotency.py
@@ -1,7 +1,7 @@
 from hashlib import sha3_224 as hasher
 from typing import Sequence, TypeVar
 
-from shared.types.event_sourcing import EventId, EventTypes, IdemKeyGenerator, State
+from shared.types.events.common import EventId, EventTypes, IdemKeyGenerator, State
 
 EventTypeT = TypeVar("EventTypeT", bound=EventTypes)
 
diff --git a/shared/types/common.py b/shared/types/common.py
new file mode 100644
index 00000000..c0fc38ff
--- /dev/null
+++ b/shared/types/common.py
@@ -0,0 +1,9 @@
+from typing import Annotated
+from uuid import UUID
+
+from pydantic import TypeAdapter
+from pydantic.types import UuidVersion
+
+_NodeId = Annotated[UUID, UuidVersion(4)]
+NodeId = type("NodeId", (UUID,), {})
+NodeIdParser: TypeAdapter[NodeId] = TypeAdapter(_NodeId)
diff --git a/shared/types/event_sourcing.py b/shared/types/events/common.py
similarity index 100%
rename from shared/types/event_sourcing.py
rename to shared/types/events/common.py
diff --git a/shared/types/events.py b/shared/types/events/events.py
similarity index 96%
rename from shared/types/events.py
rename to shared/types/events/events.py
index 233221d0..50eb35d9 100644
--- a/shared/types/events.py
+++ b/shared/types/events/events.py
@@ -6,21 +6,15 @@ from uuid import UUID
 from pydantic import BaseModel, TypeAdapter, UuidVersion
 
 from shared.openai import FinishReason, chat
-from shared.types.event_sourcing import Event, EventTypes
-from shared.types.model import ModelId
-
-_NodeId = Annotated[UUID, UuidVersion(4)]
-NodeId = type("NodeId", (UUID,), {})
-NodeIdParser: TypeAdapter[NodeId] = TypeAdapter(_NodeId)
+from shared.types.common import NodeId
+from shared.types.events.common import Event, EventTypes
+from shared.types.models.common import ModelId
+from shared.types.worker.common import InstanceId
 
 _RequestId = Annotated[UUID, UuidVersion(4)]
 RequestId = type("RequestId", (UUID,), {})
 RequestIdParser: TypeAdapter[RequestId] = TypeAdapter(_RequestId)
 
-_InstanceId = Annotated[UUID, UuidVersion(4)]
-InstanceId = type("InstanceId", (UUID,), {})
-InstanceIdParser: TypeAdapter[InstanceId] = TypeAdapter(_InstanceId)
-
 _TimerId = Annotated[UUID, UuidVersion(4)]
 TimerId = type("TimerId", (UUID,), {})
 TimerIdParser: TypeAdapter[TimerId] = TypeAdapter(_TimerId)
diff --git a/shared/types/models/common.py b/shared/types/models/common.py
new file mode 100644
index 00000000..5e2c1127
--- /dev/null
+++ b/shared/types/models/common.py
@@ -0,0 +1,22 @@
+from typing import Annotated, Sequence, final
+from uuid import UUID
+
+from pydantic import BaseModel, TypeAdapter
+from pydantic.types import UuidVersion
+
+from shared.types.models.metadata import ModelMetadata
+from shared.types.models.sources import ModelSource
+
+_ModelId = Annotated[UUID, UuidVersion(4)]
+ModelId = type("ModelId", (UUID,), {})
+ModelIdParser: TypeAdapter[ModelId] = TypeAdapter(_ModelId)
+
+
+@final
+class Model(BaseModel):
+    model_id: ModelId
+    model_sources: Sequence[ModelSource]
+    model_metadata: ModelMetadata
+
+
+ModelIdAdapter: TypeAdapter[ModelId] = TypeAdapter(_ModelId)
diff --git a/shared/types/models/metadata.py b/shared/types/models/metadata.py
new file mode 100644
index 00000000..6a1b8481
--- /dev/null
+++ b/shared/types/models/metadata.py
@@ -0,0 +1,8 @@
+from typing import Annotated
+
+from pydantic import BaseModel, PositiveInt
+
+
+class ModelMetadata(BaseModel):
+    storage_size_kilobytes: Annotated[int, PositiveInt]
+    n_layers: Annotated[int, PositiveInt]
diff --git a/shared/types/model.py b/shared/types/models/sources.py
similarity index 59%
rename from shared/types/model.py
rename to shared/types/models/sources.py
index 907366c7..e00e8650 100644
--- a/shared/types/model.py
+++ b/shared/types/models/sources.py
@@ -1,17 +1,13 @@
-from typing import Annotated, Any, Generic, Literal, Sequence, TypeVar, Union, final
-from uuid import UUID
+from typing import Annotated, Any, Generic, Literal, TypeVar, Union, final
 
-from pydantic import AnyHttpUrl, BaseModel, Field, PositiveInt, TypeAdapter
-from pydantic.types import UuidVersion
+from pydantic import AnyHttpUrl, BaseModel, Field, TypeAdapter
+
+from shared.types.models.common import ModelId
 
 SourceType = Literal["HuggingFace", "GitHub"]
 
 T = TypeVar("T", bound=SourceType)
 
-_ModelId = Annotated[UUID, UuidVersion(4)]
-ModelId = type("ModelId", (UUID,), {})
-ModelIdParser: TypeAdapter[ModelId] = TypeAdapter(_ModelId)
-
 RepoPath = Annotated[str, Field(pattern=r"^[^/]+/[^/]+$")]
 
 
@@ -43,11 +39,6 @@ class GitHubModelSource(BaseModelSource[Literal["GitHub"]]):
     source_data: GitHubModelSourceData
 
 
-class ModelMetadata(BaseModel):
-    storage_size_kilobytes: Annotated[int, PositiveInt]
-    n_layers: Annotated[int, PositiveInt]
-
-
 _ModelSource = Annotated[
     Union[
         HuggingFaceModelSource,
@@ -56,14 +47,4 @@ _ModelSource = Annotated[
     Field(discriminator="source_type"),
 ]
 ModelSource = BaseModelSource[SourceType]
-
-
-@final
-class Model(BaseModel):
-    model_id: ModelId
-    model_sources: Sequence[ModelSource]
-    model_metadata: ModelMetadata
-
-
-ModelIdAdapter: TypeAdapter[ModelId] = TypeAdapter(_ModelId)
 ModelSourceAdapter: TypeAdapter[ModelSource] = TypeAdapter(_ModelSource)
diff --git a/shared/types/networking/edges.py b/shared/types/networking/edges.py
new file mode 100644
index 00000000..3866fc2e
--- /dev/null
+++ b/shared/types/networking/edges.py
@@ -0,0 +1,85 @@
+from dataclasses import dataclass
+from enum import Enum
+from typing import Annotated, Generic, NamedTuple, TypeVar, final
+from uuid import UUID
+
+from pydantic import BaseModel, IPvAnyAddress, TypeAdapter
+from pydantic.types import UuidVersion
+
+from shared.types.common import NodeId
+
+_EdgeId = Annotated[UUID, UuidVersion(4)]
+EdgeId = type("EdgeId", (UUID,), {})
+EdgeIdParser: TypeAdapter[EdgeId] = TypeAdapter(_EdgeId)
+
+
+@final
+class EdgeDataTransferRate(BaseModel):
+    throughput: float
+    latency: float
+    jitter: float
+
+
+class AddressingProtocol(str, Enum):
+    IPvAny = "IPvAny"
+
+
+class ApplicationProtocol(str, Enum):
+    MLX = "MLX"
+
+
+TE = TypeVar("TE", bound=AddressingProtocol)
+TF = TypeVar("TF", bound=ApplicationProtocol)
+
+
+@final
+class EdgeType(BaseModel, Generic[TE, TF]):
+    addressing_protocol: TE
+    application_protocol: TF
+
+
+@final
+class EdgeDirection(NamedTuple):
+    source: NodeId
+    sink: NodeId
+
+
+@dataclass
+class EdgeMetadata(BaseModel, Generic[TE, TF]): ...
+
+
+@final
+@dataclass
+class MLXEdgeContext(EdgeMetadata[AddressingProtocol.IPvAny, ApplicationProtocol.MLX]):
+    source_ip: IPvAnyAddress
+    sink_ip: IPvAnyAddress
+
+
+@final
+class EdgeInfo(BaseModel, Generic[TE, TF]):
+    edge_type: EdgeType[TE, TF]
+    edge_data_transfer_rate: EdgeDataTransferRate
+    edge_metadata: EdgeMetadata[TE, TF]
+
+
+@final
+class DirectedEdge(BaseModel, Generic[TE, TF]):
+    edge_direction: EdgeDirection
+    edge_identifier: EdgeId
+    edge_info: EdgeInfo[TE, TF]
+
+
+"""
+an_edge: DirectedEdge[Literal[AddressingProtocol.IPvAny], Literal[ApplicationProtocol.MLX]] = DirectedEdge(
+    edge_identifier=UUID(),
+    edge_direction=EdgeDirection(source=NodeId("1"), sink=NodeId("2")),
+    edge_info=EdgeInfo(
+        edge_type=EdgeType(
+            addressing_protocol=AddressingProtocol.ipv4,
+            application_protocol=ApplicationProtocol.mlx
+        ),
+        edge_data_transfer_rate=EdgeDataTransferRate(throughput=1000, latency=0.1, jitter=0.01),
+        edge_metadata=MLXEdgeContext(source_ip=IpV4Addr("192.168.1.1"), sink_ip=IpV4Addr("192.168.1.2"))
+    )
+)
+"""
diff --git a/shared/types/networking/services.py b/shared/types/networking/services.py
new file mode 100644
index 00000000..bce1d3e6
--- /dev/null
+++ b/shared/types/networking/services.py
@@ -0,0 +1,42 @@
+from typing import Annotated, Callable, NewType, Protocol
+
+from pydantic import BaseModel, Field
+
+from shared.types.common import NodeId
+from shared.types.networking.edges import (
+    AddressingProtocol,
+    ApplicationProtocol,
+    EdgeDirection,
+    EdgeId,
+    EdgeInfo,
+)
+
+TopicName = NewType("TopicName", str)
+
+
+class WrappedMessage(BaseModel):
+    node_id: NodeId
+    unix_timestamp: Annotated[int, Field(gt=0)]
+
+
+PubSubMessageHandler = Callable[[TopicName, WrappedMessage], None]
+NodeConnectedHandler = Callable[
+    [EdgeId, EdgeDirection, EdgeInfo[AddressingProtocol, ApplicationProtocol]], None
+]
+NodeDisconnectedHandler = Callable[[EdgeId], None]
+
+
+class DiscoveryService(Protocol):
+    def register_node_connected_handler(
+        self, handler: NodeConnectedHandler
+    ) -> None: ...
+    def register_node_disconnected_handler(
+        self, handler: NodeDisconnectedHandler
+    ) -> None: ...
+
+
+class PubSubService(Protocol):
+    def register_handler(
+        self, key: str, topic_name: TopicName, handler: PubSubMessageHandler
+    ) -> None: ...
+    def deregister_handler(self, key: str) -> None: ...
diff --git a/shared/types/networking/topology.py b/shared/types/networking/topology.py
new file mode 100644
index 00000000..33b1e191
--- /dev/null
+++ b/shared/types/networking/topology.py
@@ -0,0 +1,22 @@
+from collections.abc import Sequence
+
+from pydantic import BaseModel
+
+from shared.types.networking.edges import (
+    AddressingProtocol,
+    ApplicationProtocol,
+    EdgeDirection,
+    EdgeId,
+    EdgeInfo,
+)
+
+
+class Topology(BaseModel):
+    edges: dict[
+        EdgeId, tuple[EdgeDirection, EdgeInfo[AddressingProtocol, ApplicationProtocol]]
+    ]
+
+
+class NetworkState(BaseModel):
+    topology: Topology
+    history: Sequence[Topology]
diff --git a/shared/types/profiling/common.py b/shared/types/profiling/common.py
new file mode 100644
index 00000000..5faffb43
--- /dev/null
+++ b/shared/types/profiling/common.py
@@ -0,0 +1,4 @@
+from pydantic import BaseModel
+
+
+class NodeProfile(BaseModel): ...
diff --git a/shared/types/states/master.py b/shared/types/states/master.py
new file mode 100644
index 00000000..d9dff550
--- /dev/null
+++ b/shared/types/states/master.py
@@ -0,0 +1,17 @@
+from queue import Queue
+
+from pydantic import BaseModel
+
+from shared.types.common import NodeId
+from shared.types.networking.topology import NetworkState
+from shared.types.profiling.common import NodeProfile
+
+
+class ExternalCommand(BaseModel): ...
+
+
+class MasterState(BaseModel):
+    network_state: NetworkState
+    node_profiles: dict[NodeId, NodeProfile]
+    job_inbox: Queue[ExternalCommand]
+    job_outbox: Queue[ExternalCommand]
diff --git a/shared/types/states/shared.py b/shared/types/states/shared.py
new file mode 100644
index 00000000..f5c55c09
--- /dev/null
+++ b/shared/types/states/shared.py
@@ -0,0 +1,10 @@
+from pydantic import BaseModel
+
+from shared.types.common import NodeId
+from shared.types.worker.common import InstanceId
+from shared.types.worker.shards import ShardPlacement
+
+
+class SharedState(BaseModel):
+    node_id: NodeId
+    compute_instances: dict[InstanceId, ShardPlacement]
diff --git a/shared/types/states/worker.py b/shared/types/states/worker.py
new file mode 100644
index 00000000..041e4cf5
--- /dev/null
+++ b/shared/types/states/worker.py
@@ -0,0 +1,14 @@
+from typing import Tuple
+
+from shared.types.models.common import ModelId
+from shared.types.states.shared import SharedState
+from shared.types.tasks.common import Task, TaskId, TaskType
+from shared.types.worker.downloads import BaseDownloadProgress, DownloadStatus
+from shared.types.worker.shards import ShardData, ShardType
+
+
+class WorkerState(SharedState):
+    download_state: dict[
+        Tuple[ModelId, ShardData[ShardType]], BaseDownloadProgress[DownloadStatus]
+    ]
+    compute_tasks: dict[TaskId, Task[TaskType]]
diff --git a/shared/types/tasks/common.py b/shared/types/tasks/common.py
new file mode 100644
index 00000000..e680caf1
--- /dev/null
+++ b/shared/types/tasks/common.py
@@ -0,0 +1,39 @@
+from enum import Enum
+from typing import Annotated, Any, Generic, Literal, TypeVar
+from uuid import UUID
+
+import openai.types.chat as openai
+from pydantic import BaseModel, TypeAdapter
+from pydantic.types import UuidVersion
+
+_TaskId = Annotated[UUID, UuidVersion(4)]
+TaskId = type("TaskId", (UUID,), {})
+TaskIdParser: TypeAdapter[TaskId] = TypeAdapter(_TaskId)
+
+
+class TaskType(str, Enum):
+    ChatCompletionNonStreaming = "ChatCompletionNonStreaming"
+    ChatCompletionStreaming = "ChatCompletionStreaming"
+
+
+TaskTypeT = TypeVar("TaskTypeT", bound=TaskType)
+
+
+class Task(BaseModel, Generic[TaskTypeT]):
+    task_id: TaskId
+    task_type: TaskTypeT
+    task_data: Any
+
+
+class ChatCompletionNonStreamingTask(Task[TaskType.ChatCompletionNonStreaming]):
+    task_type: Literal[TaskType.ChatCompletionNonStreaming] = (
+        TaskType.ChatCompletionNonStreaming
+    )
+    task_data: openai.completion_create_params.CompletionCreateParams
+
+
+class ChatCompletionStreamingTask(Task[TaskType.ChatCompletionStreaming]):
+    task_type: Literal[TaskType.ChatCompletionStreaming] = (
+        TaskType.ChatCompletionStreaming
+    )
+    task_data: openai.completion_create_params.CompletionCreateParams
diff --git a/shared/types/worker/common.py b/shared/types/worker/common.py
new file mode 100644
index 00000000..0ec0b74b
--- /dev/null
+++ b/shared/types/worker/common.py
@@ -0,0 +1,13 @@
+from typing import Annotated
+from uuid import UUID
+
+from pydantic import TypeAdapter
+from pydantic.types import UuidVersion
+
+_InstanceId = Annotated[UUID, UuidVersion(4)]
+InstanceId = type("InstanceId", (UUID,), {})
+InstanceIdParser: TypeAdapter[InstanceId] = TypeAdapter(_InstanceId)
+
+_RunnerId = Annotated[UUID, UuidVersion(4)]
+RunnerId = type("RunnerId", (UUID,), {})
+RunnerIdParser: TypeAdapter[RunnerId] = TypeAdapter(_RunnerId)
diff --git a/shared/types/instance.py b/shared/types/worker/downloads.py
similarity index 61%
rename from shared/types/instance.py
rename to shared/types/worker/downloads.py
index 67f48285..c46da775 100644
--- a/shared/types/instance.py
+++ b/shared/types/worker/downloads.py
@@ -1,33 +1,21 @@
-from typing import Annotated, Literal, Generic, TypeVar, Union
 from enum import Enum
-from pydantic import BaseModel, UUID4, PositiveInt, Field
+from typing import (
+    Annotated,
+    Callable,
+    Generic,
+    Literal,
+    NewType,
+    Sequence,
+    TypeVar,
+    Union,
+)
 
-from shared.types.model import ModelId
+from pydantic import BaseModel, Field, PositiveInt
 
-InstanceId = Annotated[str, UUID4]
-NodeId = Annotated[str, UUID4]
-RunnerId = Annotated[str, UUID4]
-
-
-class ShardType(str, Enum):
-    PipelineParallel = "PipelineParallel"
-
-
-ShardTypeT = TypeVar("ShardTypeT", bound=ShardType)
-
-
-class ShardData(BaseModel, Generic[ShardTypeT]):
-    shard_type: ShardTypeT
-
-
-class Shard(BaseModel, Generic[ShardTypeT]):
-    shard_data: ShardData[ShardTypeT]
-    runner_id: RunnerId
-
-
-class ShardPlacement(BaseModel):
-    model_id: ModelId
-    shard_assignments: dict[NodeId, Shard[ShardType]]
+from shared.types.common import NodeId
+from shared.types.models.common import ModelId
+from shared.types.models.sources import ModelSource
+from shared.types.worker.shards import ShardData, ShardType
 
 
 class DownloadProgressData(BaseModel):
@@ -81,10 +69,17 @@ DownloadProgress = Annotated[
 ]
 
 
-class Instance(ShardPlacement):
-    instance_id: InstanceId
+BytesToDownload = NewType("BytesToDownload", int)
+BytesDownloaded = NewType("BytesDownloaded", int)
+
+DownloadEffectHandler = Callable[
+    [ModelId, DownloadStatus, BytesToDownload, BytesDownloaded], None
+]
 
 
-class InstanceDownloadProgress(BaseModel, Generic[DownloadStatusT]):
-    instance_id: InstanceId
-    download_progress: BaseDownloadProgress[DownloadStatusT]
+def download_shard(
+    model_id: ModelId,
+    model_source: ModelSource,
+    shard_data: ShardData[ShardType],
+    effect_handlers: Sequence[DownloadEffectHandler],
+) -> None: ...
diff --git a/shared/types/worker/instances.py b/shared/types/worker/instances.py
new file mode 100644
index 00000000..447552d7
--- /dev/null
+++ b/shared/types/worker/instances.py
@@ -0,0 +1,18 @@
+from typing import Generic, TypeVar
+
+from pydantic import BaseModel
+
+from shared.types.worker.common import InstanceId
+from shared.types.worker.downloads import BaseDownloadProgress, DownloadStatus
+from shared.types.worker.shards import ShardPlacement
+
+DownloadStatusT = TypeVar("DownloadStatusT", bound=DownloadStatus)
+
+
+class Instance(ShardPlacement):
+    instance_id: InstanceId
+
+
+class InstanceDownloadProgress(BaseModel, Generic[DownloadStatusT]):
+    instance_id: InstanceId
+    download_progress: BaseDownloadProgress[DownloadStatusT]
diff --git a/shared/types/worker/shards.py b/shared/types/worker/shards.py
new file mode 100644
index 00000000..aa6df9ad
--- /dev/null
+++ b/shared/types/worker/shards.py
@@ -0,0 +1,29 @@
+from enum import Enum
+from typing import Generic, TypeVar
+
+from pydantic import BaseModel
+
+from shared.types.common import NodeId
+from shared.types.models.common import ModelId
+from shared.types.worker.common import RunnerId
+
+
+class ShardType(str, Enum):
+    PipelineParallel = "PipelineParallel"
+
+
+ShardTypeT = TypeVar("ShardTypeT", bound=ShardType)
+
+
+class ShardData(BaseModel, Generic[ShardTypeT]):
+    shard_type: ShardTypeT
+
+
+class Shard(BaseModel, Generic[ShardTypeT]):
+    shard_data: ShardData[ShardTypeT]
+    runner_id: RunnerId
+
+
+class ShardPlacement(BaseModel):
+    model_id: ModelId
+    shard_assignments: dict[NodeId, Shard[ShardType]]

← 899d8820 Merge Seth's Control Plane API Work into Alex's Events Branc  ·  back to Exo  ·  refactor: Replace Literal with Enum in sources.py d5033e65 →