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.
- by_framework/common/config.py +15 -1
- by_framework/common/constants.py +353 -107
- by_framework/common/emitter.py +10 -0
- by_framework/common/redis_client.py +49 -9
- by_framework/core/discovery.py +1 -1
- by_framework/core/protocol/content_type.py +1 -0
- by_framework/core/registry.py +27 -10
- by_framework/metrics/snapshot.py +76 -80
- by_framework/trace/span_recorder.py +40 -25
- by_framework/trace/trace_writer.py +34 -20
- by_framework/worker/app.py +56 -14
- by_framework/worker/context.py +4 -0
- by_framework/worker/heartbeat.py +26 -0
- by_framework/worker/runner.py +42 -3
- {by_framework-0.2.2.dev6.dist-info → by_framework-0.2.2.dev7.dist-info}/METADATA +1 -1
- {by_framework-0.2.2.dev6.dist-info → by_framework-0.2.2.dev7.dist-info}/RECORD +19 -19
- {by_framework-0.2.2.dev6.dist-info → by_framework-0.2.2.dev7.dist-info}/WHEEL +0 -0
- {by_framework-0.2.2.dev6.dist-info → by_framework-0.2.2.dev7.dist-info}/entry_points.txt +0 -0
- {by_framework-0.2.2.dev6.dist-info → by_framework-0.2.2.dev7.dist-info}/licenses/LICENSE +0 -0
by_framework/common/config.py
CHANGED
|
@@ -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
|
|
by_framework/common/constants.py
CHANGED
|
@@ -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
|
-
@
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
101
|
-
def trace_meta(trace_id: str) -> str:
|
|
102
|
-
"""Hash storing trace-level metadata for observability.
|
|
103
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
"""
|
|
118
|
-
return
|
|
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
|
-
@
|
|
121
|
-
def
|
|
122
|
-
"""Sorted Set index from
|
|
123
|
-
return
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
173
|
-
def
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
|
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)
|