by-framework 0.2.2.dev6__py3-none-any.whl → 0.2.2.dev7__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.
@@ -6,7 +6,7 @@ Provides typed configuration loaded from environment variables.
6
6
 
7
7
  import os
8
8
  from dataclasses import dataclass, field
9
- from typing import Optional
9
+ from typing import Literal, Optional
10
10
 
11
11
  from by_framework.common.constants import RedisKeys
12
12
 
@@ -22,6 +22,10 @@ class RedisConfig:
22
22
  username: Optional[str] = None
23
23
  decode_responses: bool = True
24
24
  max_connections: Optional[int] = None
25
+ mode: Literal["standalone", "cluster"] = "standalone"
26
+ cluster_nodes: Optional[list[tuple[str, int]]] = None
27
+ socket_connect_timeout: int = 5
28
+ socket_timeout: int = 10
25
29
 
26
30
  @classmethod
27
31
  def from_env(cls) -> "RedisConfig":
@@ -29,6 +33,14 @@ class RedisConfig:
29
33
  password = os.environ.get("REDIS_PASSWORD", "")
30
34
  username = os.environ.get("REDIS_USERNAME") or None
31
35
  max_connections = os.environ.get("REDIS_MAX_CONNECTIONS")
36
+ mode = os.environ.get("REDIS_MODE", "standalone")
37
+ cluster_nodes_str = os.environ.get("REDIS_CLUSTER_NODES")
38
+ cluster_nodes = None
39
+ if cluster_nodes_str:
40
+ cluster_nodes = []
41
+ for node in cluster_nodes_str.split(","):
42
+ node_host, node_port = node.rsplit(":", 1)
43
+ cluster_nodes.append((node_host, int(node_port)))
32
44
  return cls(
33
45
  host=os.environ.get("REDIS_HOST", "localhost"),
34
46
  port=int(os.environ.get("REDIS_PORT", "6379")),
@@ -36,6 +48,8 @@ class RedisConfig:
36
48
  password=password,
37
49
  username=username,
38
50
  max_connections=int(max_connections) if max_connections else None,
51
+ mode=mode,
52
+ cluster_nodes=cluster_nodes,
39
53
  )
40
54
 
41
55
 
@@ -5,67 +5,172 @@ All Redis Stream names, Hash Keys, Set Keys and other configuration items
5
5
  are centrally managed in this file. Do not hardcode literal strings in business code.
6
6
  """
7
7
 
8
+ import os
9
+ from typing import Optional
10
+
11
+
12
+ def get_key_schema_version() -> str:
13
+ """Return the configured Redis key schema version ("v1" or "v2").
14
+
15
+ Controlled by REDIS_KEY_SCHEMA_VERSION, defaulting to "v1" (the current
16
+ unprefixed key format). Cluster mode requires "v2" (see redis_client.init_redis).
17
+ """
18
+ version = os.environ.get("REDIS_KEY_SCHEMA_VERSION", "v1")
19
+ if version not in ("v1", "v2"):
20
+ raise ValueError(
21
+ f"Invalid REDIS_KEY_SCHEMA_VERSION: {version!r} (must be 'v1' or 'v2')"
22
+ )
23
+ return version
24
+
8
25
 
9
26
  class RedisKeys:
10
27
  """Gateway SDK global Redis Key naming conventions and constants."""
11
28
 
12
29
  CONTROL_PLANE_PREFIX = "byai_gateway:control_plane"
30
+ CONTROL_PLANE_SUFFIX = "control_plane"
31
+ V2_PREFIX = "byai_gateway:v2:"
32
+
33
+ @classmethod
34
+ def _versioned(cls, v1_key: str, v2_suffix: str) -> str:
35
+ """Resolve a key according to REDIS_KEY_SCHEMA_VERSION.
36
+
37
+ v1 (default): returns v1_key unchanged, byte-for-byte.
38
+ v2: returns V2_PREFIX + v2_suffix, where v2_suffix already encodes
39
+ any Cluster hash tag needed for same-entity key groups.
40
+
41
+ Every RedisKeys factory method routes through this one function so
42
+ the v1/v2 decision lives in exactly one place.
43
+ """
44
+ if get_key_schema_version() == "v2":
45
+ return f"{cls.V2_PREFIX}{v2_suffix}"
46
+ return v1_key
47
+
48
+ @classmethod
49
+ def _worker_scan_pattern(cls, v1_prefix: str, v2_field: str) -> str:
50
+ """SCAN MATCH glob pattern matching every worker key in this family.
51
+
52
+ Under v1 the worker_id is the last path segment (prefix + id, no
53
+ suffix). Under v2 it's wrapped in a Cluster hash tag in the middle
54
+ of the key (prefix + "{" + id + "}" + suffix) — a bare "{prefix}*"
55
+ pattern (or a str.startswith("{prefix}") check) would never match a
56
+ real v2 key, since "{"/"}" are literal characters in Redis's glob
57
+ matching (only *, ?, [seq] are special), not wildcards.
58
+ """
59
+ if get_key_schema_version() == "v2":
60
+ return f"{cls.V2_PREFIX}registry:worker:{{*}}:{v2_field}"
61
+ return f"{v1_prefix}*"
62
+
63
+ @classmethod
64
+ def _worker_id_from_scanned_key(
65
+ cls, key: str, v1_prefix: str, v2_field: str
66
+ ) -> Optional[str]:
67
+ """Extract worker_id from a key returned by scanning with the
68
+ pattern from _worker_scan_pattern(v1_prefix, v2_field)."""
69
+ if get_key_schema_version() == "v2":
70
+ prefix = f"{cls.V2_PREFIX}registry:worker:{{"
71
+ suffix = f"}}:{v2_field}"
72
+ if key.startswith(prefix) and key.endswith(suffix):
73
+ return key[len(prefix) : -len(suffix)]
74
+ return None
75
+ if key.startswith(v1_prefix):
76
+ return key[len(v1_prefix) :]
77
+ return None
13
78
 
14
79
  # --- Queues and Streams ---
15
- @staticmethod
16
- def ctrl_stream(agent_type: str) -> str:
17
- """Control stream queue for dispatching tasks to Workers."""
18
- return f"byai_gateway:ctrl:agent_type:{agent_type}"
80
+ @classmethod
81
+ def ctrl_stream(cls, agent_type: str) -> str:
82
+ """Control stream queue for dispatching tasks to Workers.
19
83
 
20
- @staticmethod
21
- def worker_ctrl_stream(worker_id: str) -> str:
84
+ Single-key: agent_type is the only variable dimension. Multiple
85
+ ctrl_stream calls are fanned out at the application layer (see
86
+ Phase 4's two-phase XREADGROUP split), not combined into one slot.
87
+ """
88
+ return cls._versioned(
89
+ v1_key=f"byai_gateway:ctrl:agent_type:{agent_type}",
90
+ v2_suffix=f"ctrl:agent_type:{agent_type}",
91
+ )
92
+
93
+ @classmethod
94
+ def worker_ctrl_stream(cls, worker_id: str) -> str:
22
95
  """Worker-specific control queue for directing control commands to worker."""
23
- return f"byai_gateway:ctrl:worker:{worker_id}"
96
+ return cls._versioned(
97
+ v1_key=f"byai_gateway:ctrl:worker:{worker_id}",
98
+ v2_suffix=f"ctrl:worker:{{{worker_id}}}",
99
+ )
24
100
 
25
- @staticmethod
26
- def plugin_reload_ack_stream(reload_id: str) -> str:
101
+ @classmethod
102
+ def plugin_reload_ack_stream(cls, reload_id: str) -> str:
27
103
  """Stream for worker ACKs emitted after handling a plugin reload command."""
28
- return f"byai_gateway:plugin_reload:{reload_id}:ack"
104
+ return cls._versioned(
105
+ v1_key=f"byai_gateway:plugin_reload:{reload_id}:ack",
106
+ v2_suffix=f"plugin_reload:{reload_id}:ack",
107
+ )
29
108
 
30
109
  @classmethod
31
110
  def control_plane_wakeup_stream(cls) -> str:
32
111
  """Management stream for agent availability wakeup requests."""
33
- return f"{cls.CONTROL_PLANE_PREFIX}:mgmt:wakeup"
112
+ return cls._versioned(
113
+ v1_key=f"{cls.CONTROL_PLANE_PREFIX}:mgmt:wakeup",
114
+ v2_suffix=f"{cls.CONTROL_PLANE_SUFFIX}:mgmt:wakeup",
115
+ )
34
116
 
35
117
  @classmethod
36
118
  def control_plane_wakeup_result_stream(cls, execution_id: str) -> str:
37
119
  """Management stream for wakeup controller decisions."""
38
- return f"{cls.CONTROL_PLANE_PREFIX}:mgmt:wakeup:result:{execution_id}"
120
+ return cls._versioned(
121
+ v1_key=f"{cls.CONTROL_PLANE_PREFIX}:mgmt:wakeup:result:{execution_id}",
122
+ v2_suffix=f"{cls.CONTROL_PLANE_SUFFIX}:mgmt:wakeup:result:{execution_id}",
123
+ )
39
124
 
40
125
  @classmethod
41
126
  def control_plane_delivery_pending_stream(cls) -> str:
42
127
  """Management stream for pending control-message delivery."""
43
- return f"{cls.CONTROL_PLANE_PREFIX}:mgmt:delivery:pending"
128
+ return cls._versioned(
129
+ v1_key=f"{cls.CONTROL_PLANE_PREFIX}:mgmt:delivery:pending",
130
+ v2_suffix=f"{cls.CONTROL_PLANE_SUFFIX}:mgmt:delivery:pending",
131
+ )
44
132
 
45
133
  @classmethod
46
134
  def control_plane_deadletter_stream(cls) -> str:
47
135
  """Management stream for failed control-plane work."""
48
- return f"{cls.CONTROL_PLANE_PREFIX}:mgmt:deadletter"
136
+ return cls._versioned(
137
+ v1_key=f"{cls.CONTROL_PLANE_PREFIX}:mgmt:deadletter",
138
+ v2_suffix=f"{cls.CONTROL_PLANE_SUFFIX}:mgmt:deadletter",
139
+ )
49
140
 
50
141
  @classmethod
51
142
  def control_plane_agent_availability(cls, agent_type: str) -> str:
52
143
  """Availability state key for an agent type."""
53
- return f"{cls.CONTROL_PLANE_PREFIX}:availability:agent_type:{agent_type}"
144
+ return cls._versioned(
145
+ v1_key=f"{cls.CONTROL_PLANE_PREFIX}:availability:agent_type:{agent_type}",
146
+ v2_suffix=(
147
+ f"{cls.CONTROL_PLANE_SUFFIX}:availability:agent_type:{agent_type}"
148
+ ),
149
+ )
54
150
 
55
151
  @classmethod
56
152
  def control_plane_agent_circuit(cls, agent_type: str) -> str:
57
153
  """Circuit-breaker state key for an agent type."""
58
- return f"{cls.CONTROL_PLANE_PREFIX}:circuit:agent_type:{agent_type}"
154
+ return cls._versioned(
155
+ v1_key=f"{cls.CONTROL_PLANE_PREFIX}:circuit:agent_type:{agent_type}",
156
+ v2_suffix=f"{cls.CONTROL_PLANE_SUFFIX}:circuit:agent_type:{agent_type}",
157
+ )
59
158
 
60
159
  @classmethod
61
160
  def control_plane_agent_fallback(cls, agent_type: str) -> str:
62
161
  """Fallback routing state key for an agent type."""
63
- return f"{cls.CONTROL_PLANE_PREFIX}:fallback:agent_type:{agent_type}"
162
+ return cls._versioned(
163
+ v1_key=f"{cls.CONTROL_PLANE_PREFIX}:fallback:agent_type:{agent_type}",
164
+ v2_suffix=f"{cls.CONTROL_PLANE_SUFFIX}:fallback:agent_type:{agent_type}",
165
+ )
64
166
 
65
167
  @classmethod
66
168
  def control_plane_user_quota(cls, user_code: str) -> str:
67
169
  """User quota state key for control-plane scheduling."""
68
- return f"{cls.CONTROL_PLANE_PREFIX}:quota:user:{user_code}"
170
+ return cls._versioned(
171
+ v1_key=f"{cls.CONTROL_PLANE_PREFIX}:quota:user:{user_code}",
172
+ v2_suffix=f"{cls.CONTROL_PLANE_SUFFIX}:quota:user:{user_code}",
173
+ )
69
174
 
70
175
  @classmethod
71
176
  def control_plane_tenant_quota(cls, tenant_id: str) -> str:
@@ -77,67 +182,131 @@ class RedisKeys:
77
182
  cls, agent_type: str, user_code: str, region: str
78
183
  ) -> str:
79
184
  """Dedupe key for concurrent wakeup requests."""
80
- return (
81
- f"{cls.CONTROL_PLANE_PREFIX}:wakeup:dedupe:"
82
- f"{agent_type}:{user_code}:{region}"
185
+ return cls._versioned(
186
+ v1_key=(
187
+ f"{cls.CONTROL_PLANE_PREFIX}:wakeup:dedupe:"
188
+ f"{agent_type}:{user_code}:{region}"
189
+ ),
190
+ v2_suffix=(
191
+ f"{cls.CONTROL_PLANE_SUFFIX}:wakeup:dedupe:"
192
+ f"{agent_type}:{user_code}:{region}"
193
+ ),
83
194
  )
84
195
 
85
- @staticmethod
86
- def agent_configs_snapshot(snapshot_key: str) -> str:
196
+ @classmethod
197
+ def agent_configs_snapshot(cls, snapshot_key: str) -> str:
87
198
  """Blob key for a persisted AgentConfigsSnapshot payload."""
88
- return f"byai_gateway:agent_configs_snapshot:{snapshot_key}"
199
+ return cls._versioned(
200
+ v1_key=f"byai_gateway:agent_configs_snapshot:{snapshot_key}",
201
+ v2_suffix=f"agent_configs_snapshot:{snapshot_key}",
202
+ )
89
203
 
90
- @staticmethod
91
- def session_data_stream(session_id: str) -> str:
204
+ @classmethod
205
+ def session_data_stream(cls, session_id: str) -> str:
92
206
  """Session-level data stream. Workers push streaming content here."""
93
- return f"byai_gateway:session:{session_id}:data_stream"
207
+ return cls._versioned(
208
+ v1_key=f"byai_gateway:session:{session_id}:data_stream",
209
+ v2_suffix=f"session:{{{session_id}}}:data_stream",
210
+ )
94
211
 
95
- @staticmethod
96
- def session_data_checkpoint(session_id: str, consumer_name: str) -> str:
212
+ @classmethod
213
+ def session_data_checkpoint(cls, session_id: str, consumer_name: str) -> str:
97
214
  """Checkpoint key storing a consumer's last processed data stream ID."""
98
- return f"byai_gateway:session:{session_id}:consumer:{consumer_name}:checkpoint"
215
+ return cls._versioned(
216
+ v1_key=(
217
+ f"byai_gateway:session:{session_id}:consumer:"
218
+ f"{consumer_name}:checkpoint"
219
+ ),
220
+ v2_suffix=(f"session:{{{session_id}}}:consumer:{consumer_name}:checkpoint"),
221
+ )
99
222
 
100
- @staticmethod
101
- def trace_meta(trace_id: str) -> str:
102
- """Hash storing trace-level metadata for observability."""
103
- return f"by_framework:trace:{trace_id}"
223
+ @classmethod
224
+ def trace_meta(cls, trace_id: str) -> str:
225
+ """Hash storing trace-level metadata for observability.
226
+
227
+ v1 keeps Python's historical by_framework:trace:* namespace. v2
228
+ unifies onto the shared byai_gateway:v2:trace:{id} format used by
229
+ all three language SDKs (Python/Java previously shared
230
+ by_framework:trace:*, TS used a different byai_gateway:trace:*
231
+ layout — v2 replaces both with one namespace).
232
+ """
233
+ return cls._versioned(
234
+ v1_key=f"by_framework:trace:{trace_id}",
235
+ v2_suffix=f"trace:{{{trace_id}}}",
236
+ )
104
237
 
105
- @staticmethod
106
- def trace_spans(trace_id: str) -> str:
238
+ @classmethod
239
+ def trace_spans(cls, trace_id: str) -> str:
107
240
  """List storing trace span JSON payloads ordered by write time."""
108
- return f"by_framework:trace:spans:{trace_id}"
241
+ return cls._versioned(
242
+ v1_key=f"by_framework:trace:spans:{trace_id}",
243
+ v2_suffix=f"trace:spans:{{{trace_id}}}",
244
+ )
109
245
 
110
- @staticmethod
111
- def trace_index_session(session_id: str) -> str:
112
- """Sorted Set index from session_id to trace IDs."""
113
- return f"by_framework:trace:idx:session:{session_id}"
246
+ @classmethod
247
+ def trace_index_session(cls, session_id: str) -> str:
248
+ """Sorted Set index from session_id to trace IDs.
114
249
 
115
- @staticmethod
116
- def trace_index_worker(worker_id: str) -> str:
117
- """Sorted Set index from worker_id to trace IDs."""
118
- return f"by_framework:trace:idx:worker:{worker_id}"
250
+ Cross-entity relative to the trace group (meta/spans) — deliberately
251
+ untagged, see _versioned/module docs on cross-entity splitting.
252
+ """
253
+ return cls._versioned(
254
+ v1_key=f"by_framework:trace:idx:session:{session_id}",
255
+ v2_suffix=f"trace:idx:session:{session_id}",
256
+ )
119
257
 
120
- @staticmethod
121
- def trace_index_agent(agent_type: str) -> str:
122
- """Sorted Set index from agent type to trace IDs."""
123
- return f"by_framework:trace:idx:agent:{agent_type}"
258
+ @classmethod
259
+ def trace_index_worker(cls, worker_id: str) -> str:
260
+ """Sorted Set index from worker_id to trace IDs. Cross-entity, untagged."""
261
+ return cls._versioned(
262
+ v1_key=f"by_framework:trace:idx:worker:{worker_id}",
263
+ v2_suffix=f"trace:idx:worker:{worker_id}",
264
+ )
265
+
266
+ @classmethod
267
+ def trace_index_agent(cls, agent_type: str) -> str:
268
+ """Sorted Set index from agent type to trace IDs. Cross-entity, untagged."""
269
+ return cls._versioned(
270
+ v1_key=f"by_framework:trace:idx:agent:{agent_type}",
271
+ v2_suffix=f"trace:idx:agent:{agent_type}",
272
+ )
124
273
 
125
- @staticmethod
126
- def task_group(group_id: str) -> str:
274
+ @classmethod
275
+ def task_group(cls, group_id: str) -> str:
127
276
  """Task group progress tracking Hash Key."""
128
- return f"byai_gateway:task_group:{group_id}"
277
+ return cls._versioned(
278
+ v1_key=f"byai_gateway:task_group:{group_id}",
279
+ v2_suffix=f"task_group:{{{group_id}}}",
280
+ )
129
281
 
130
- @staticmethod
131
- def task_group_results(group_id: str) -> str:
282
+ @classmethod
283
+ def task_group_results(cls, group_id: str) -> str:
132
284
  """All subtask results Hash Key for a task group."""
133
- return f"byai_gateway:task_group:{group_id}:results"
285
+ return cls._versioned(
286
+ v1_key=f"byai_gateway:task_group:{group_id}:results",
287
+ v2_suffix=f"task_group:{{{group_id}}}:results",
288
+ )
134
289
 
135
290
  # --- Registry ---
136
- # Set of known workers used for registry enumeration.
137
- KNOWN_WORKERS = "byai_gateway:registry:workers"
138
- # Set of workers with explicit admin lifecycle state. Used by dashboard
139
- # management views to include offline suspended/evicted workers.
140
- ADMIN_WORKERS = "byai_gateway:registry:worker:admin_workers"
291
+ @classmethod
292
+ def known_workers(cls) -> str:
293
+ """Set of known workers used for registry enumeration. Global index,
294
+ untagged (spans every worker entity), still version-prefixed."""
295
+ return cls._versioned(
296
+ v1_key="byai_gateway:registry:workers",
297
+ v2_suffix="registry:workers",
298
+ )
299
+
300
+ @classmethod
301
+ def admin_workers(cls) -> str:
302
+ """Set of workers with explicit admin lifecycle state. Used by
303
+ dashboard management views to include offline suspended/evicted
304
+ workers. Global index, untagged, still version-prefixed."""
305
+ return cls._versioned(
306
+ v1_key="byai_gateway:registry:worker:admin_workers",
307
+ v2_suffix="registry:worker:admin_workers",
308
+ )
309
+
141
310
  WORKER_DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 5
142
311
  # 6× the heartbeat interval gives enough margin even when the main event
143
312
  # loop is briefly occupied by an LLM call. The dedicated heartbeat thread
@@ -149,99 +318,176 @@ class RedisKeys:
149
318
  DEFAULT_SESSION_TTL = 7 * 24 * 3600
150
319
  AGENT_CONFIGS_SNAPSHOT_TTL_SECONDS = DEFAULT_SESSION_TTL
151
320
 
152
- @staticmethod
153
- def worker_declared_agent_types(worker_id: str) -> str:
321
+ @classmethod
322
+ def worker_declared_agent_types(cls, worker_id: str) -> str:
154
323
  """Set Key storing all agent type identifiers supported by a Worker."""
155
- return f"byai_gateway:registry:worker:agent_types:{worker_id}"
324
+ return cls._versioned(
325
+ v1_key=f"byai_gateway:registry:worker:agent_types:{worker_id}",
326
+ v2_suffix=f"registry:worker:{{{worker_id}}}:agent_types",
327
+ )
156
328
 
157
- @staticmethod
158
- def agent_type_members(agent_type: str) -> str:
329
+ @classmethod
330
+ def agent_type_members(cls, agent_type: str) -> str:
159
331
  """Set Key storing all Worker IDs with a specific agent type."""
160
- return f"byai_gateway:registry:agent_type:workers:{agent_type}"
332
+ return cls._versioned(
333
+ v1_key=f"byai_gateway:registry:agent_type:workers:{agent_type}",
334
+ v2_suffix=f"registry:agent_type:{{{agent_type}}}:workers",
335
+ )
161
336
 
162
- @staticmethod
163
- def worker_lock(worker_id: str) -> str:
337
+ @classmethod
338
+ def worker_lock(cls, worker_id: str) -> str:
164
339
  """Worker startup mutex lock to prevent duplicate worker_id startup."""
165
- return f"byai_gateway:registry:worker:lock:{worker_id}"
340
+ return cls._versioned(
341
+ v1_key=f"byai_gateway:registry:worker:lock:{worker_id}",
342
+ v2_suffix=f"registry:worker:{{{worker_id}}}:lock",
343
+ )
166
344
 
167
- @staticmethod
168
- def worker_online_lease(worker_id: str) -> str:
345
+ @classmethod
346
+ def worker_online_lease(cls, worker_id: str) -> str:
169
347
  """Worker online lease Key. Presence means the worker is considered online."""
170
- return f"byai_gateway:registry:worker:online:{worker_id}"
348
+ return cls._versioned(
349
+ v1_key=f"byai_gateway:registry:worker:online:{worker_id}",
350
+ v2_suffix=f"registry:worker:{{{worker_id}}}:online",
351
+ )
171
352
 
172
- @staticmethod
173
- def worker_status(worker_id: str) -> str:
353
+ @classmethod
354
+ def worker_online_lease_scan_pattern(cls) -> str:
355
+ """SCAN MATCH glob pattern matching every worker_online_lease key."""
356
+ return cls._worker_scan_pattern(
357
+ "byai_gateway:registry:worker:online:", "online"
358
+ )
359
+
360
+ @classmethod
361
+ def worker_id_from_online_lease_key(cls, key: str) -> Optional[str]:
362
+ """Extract worker_id from a key found via worker_online_lease_scan_pattern()."""
363
+ return cls._worker_id_from_scanned_key(
364
+ key, "byai_gateway:registry:worker:online:", "online"
365
+ )
366
+
367
+ @classmethod
368
+ def worker_status(cls, worker_id: str) -> str:
174
369
  """HASH storing aggregate execution counters for a Worker."""
175
- return f"byai_gateway:registry:worker:status:{worker_id}"
370
+ return cls._versioned(
371
+ v1_key=f"byai_gateway:registry:worker:status:{worker_id}",
372
+ v2_suffix=f"registry:worker:{{{worker_id}}}:status",
373
+ )
176
374
 
177
- @staticmethod
178
- def worker_executions(worker_id: str) -> str:
375
+ @classmethod
376
+ def worker_executions(cls, worker_id: str) -> str:
179
377
  """ZSET of execution IDs handled by a Worker, scored by update time."""
180
- return f"byai_gateway:registry:worker:executions:{worker_id}"
378
+ return cls._versioned(
379
+ v1_key=f"byai_gateway:registry:worker:executions:{worker_id}",
380
+ v2_suffix=f"registry:worker:{{{worker_id}}}:executions",
381
+ )
181
382
 
182
- @staticmethod
183
- def worker_active_executions(worker_id: str) -> str:
383
+ @classmethod
384
+ def worker_active_executions(cls, worker_id: str) -> str:
184
385
  """Legacy SET of non-terminal execution IDs assigned to a Worker."""
185
- return f"byai_gateway:registry:worker:active_executions:{worker_id}"
386
+ return cls._versioned(
387
+ v1_key=f"byai_gateway:registry:worker:active_executions:{worker_id}",
388
+ v2_suffix=f"registry:worker:{{{worker_id}}}:active_executions",
389
+ )
186
390
 
187
- @staticmethod
188
- def worker_active_execution_index(worker_id: str) -> str:
391
+ @classmethod
392
+ def worker_active_execution_index(cls, worker_id: str) -> str:
189
393
  """ZSET of active execution IDs assigned to a Worker, scored by update time."""
190
- return f"byai_gateway:registry:worker:active_execution_index:{worker_id}"
394
+ return cls._versioned(
395
+ v1_key=f"byai_gateway:registry:worker:active_execution_index:{worker_id}",
396
+ v2_suffix=f"registry:worker:{{{worker_id}}}:active_execution_index",
397
+ )
191
398
 
192
- @staticmethod
193
- def worker_active_snapshots(worker_id: str) -> str:
399
+ @classmethod
400
+ def worker_active_snapshots(cls, worker_id: str) -> str:
194
401
  """HASH mapping active execution IDs to lightweight snapshots."""
195
- return f"byai_gateway:registry:worker:active_snapshots:{worker_id}"
402
+ return cls._versioned(
403
+ v1_key=f"byai_gateway:registry:worker:active_snapshots:{worker_id}",
404
+ v2_suffix=f"registry:worker:{{{worker_id}}}:active_snapshots",
405
+ )
196
406
 
197
- @staticmethod
198
- def worker_history_snapshots(worker_id: str) -> str:
407
+ @classmethod
408
+ def worker_history_snapshots(cls, worker_id: str) -> str:
199
409
  """HASH mapping worker execution IDs to lightweight history snapshots."""
200
- return f"byai_gateway:registry:worker:history_snapshots:{worker_id}"
410
+ return cls._versioned(
411
+ v1_key=f"byai_gateway:registry:worker:history_snapshots:{worker_id}",
412
+ v2_suffix=f"registry:worker:{{{worker_id}}}:history_snapshots",
413
+ )
201
414
 
202
- @staticmethod
203
- def worker_admin(worker_id: str) -> str:
415
+ @classmethod
416
+ def worker_admin(cls, worker_id: str) -> str:
204
417
  """HASH storing admin-controlled state for a Worker.
205
418
 
206
419
  Fields: lifecycle (active|suspended|evicted), reason, updated_at.
207
420
  Written by WorkerManager; read by the worker on heartbeat and startup.
208
421
  No TTL — persists until explicitly cleared by an admin action.
209
422
  """
210
- return f"byai_gateway:registry:worker:admin:{worker_id}"
423
+ return cls._versioned(
424
+ v1_key=f"byai_gateway:registry:worker:admin:{worker_id}",
425
+ v2_suffix=f"registry:worker:{{{worker_id}}}:admin",
426
+ )
427
+
428
+ @classmethod
429
+ def worker_admin_scan_pattern(cls) -> str:
430
+ """SCAN MATCH glob pattern matching every worker_admin key."""
431
+ return cls._worker_scan_pattern("byai_gateway:registry:worker:admin:", "admin")
432
+
433
+ @classmethod
434
+ def worker_id_from_admin_key(cls, key: str) -> Optional[str]:
435
+ """Extract worker_id from a key found via worker_admin_scan_pattern()."""
436
+ return cls._worker_id_from_scanned_key(
437
+ key, "byai_gateway:registry:worker:admin:", "admin"
438
+ )
211
439
 
212
- @staticmethod
213
- def agent_type_denied(agent_type: str) -> str:
440
+ @classmethod
441
+ def agent_type_denied(cls, agent_type: str) -> str:
214
442
  """SET of worker_ids explicitly denied from consuming an agent_type stream.
215
443
 
216
444
  Key absent or empty SET means the agent_type is open to all workers.
217
445
  Written by WorkerManager; checked by workers before XREADGROUP and
218
446
  inside register_worker_membership().
219
447
  """
220
- return f"byai_gateway:registry:agent_type:denied:{agent_type}"
448
+ return cls._versioned(
449
+ v1_key=f"byai_gateway:registry:agent_type:denied:{agent_type}",
450
+ v2_suffix=f"registry:agent_type:{{{agent_type}}}:denied",
451
+ )
221
452
 
222
- @staticmethod
223
- def session_registry(session_id: str) -> str:
453
+ @classmethod
454
+ def session_registry(cls, session_id: str) -> str:
224
455
  """Session-level aggregate registry (Hash).
225
456
 
226
457
  Internally divided into the following Field categories:
227
458
  - exec:{execution_id} -> Stores specific execution details JSON
228
459
  - msg_map:{message_id} -> Stores message ID to execution ID mapping
229
460
  """
230
- return f"byai_gateway:session:{session_id}:registry"
461
+ return cls._versioned(
462
+ v1_key=f"byai_gateway:session:{session_id}:registry",
463
+ v2_suffix=f"session:{{{session_id}}}:registry",
464
+ )
231
465
 
232
466
  # --- Service Discovery ---
233
- @staticmethod
234
- def sd_active_instances(service_name: str) -> str:
467
+ @classmethod
468
+ def sd_active_instances(cls, service_name: str) -> str:
235
469
  """ZSET Key for active service instances (sorted by heartbeat timestamp)."""
236
- return f"byai_gateway:sd:active:{service_name}"
470
+ return cls._versioned(
471
+ v1_key=f"byai_gateway:sd:active:{service_name}",
472
+ v2_suffix=f"sd:{{{service_name}}}:active",
473
+ )
237
474
 
238
- @staticmethod
239
- def sd_instance_details(service_name: str) -> str:
475
+ @classmethod
476
+ def sd_instance_details(cls, service_name: str) -> str:
240
477
  """HASH Key for service instance metadata."""
241
- return f"byai_gateway:sd:instances:{service_name}"
478
+ return cls._versioned(
479
+ v1_key=f"byai_gateway:sd:instances:{service_name}",
480
+ v2_suffix=f"sd:{{{service_name}}}:instances",
481
+ )
482
+
483
+ @classmethod
484
+ def sd_services(cls) -> str:
485
+ """Set of all known service names. Global index, untagged, still
486
+ version-prefixed."""
487
+ return cls._versioned(
488
+ v1_key="byai_gateway:sd:services", v2_suffix="sd:services"
489
+ )
242
490
 
243
- # Set of all known service names (SET)
244
- SD_SERVICES = "byai_gateway:sd:services"
245
491
  # Default heartbeat send interval (10 seconds)
246
492
  SD_DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 10
247
493
  # Default service heartbeat threshold (30 seconds)