@@ -72,6 +72,8 @@ def _route_for_metrics(path: str) -> str:
7272 parts [1 ] = "{schedule_id}"
7373 elif parts [0 ] == "search-attributes" and len (parts ) >= 2 :
7474 parts [1 ] = "{name}"
75+ elif parts [0 ] == "workers" and len (parts ) >= 2 :
76+ parts [1 ] = "{worker_id}"
7577 elif parts [:2 ] == ["bridge-adapters" , "webhook" ] and len (parts ) >= 3 :
7678 parts [2 ] = "{adapter}"
7779 elif (
@@ -383,6 +385,58 @@ class TaskQueueList:
383385 task_queues : list [TaskQueueDescription ]
384386
385387
388+ @dataclass
389+ class WorkerDescription :
390+ """Current server view of one registered worker."""
391+
392+ worker_id : str
393+ task_queue : str | None = None
394+ runtime : str | None = None
395+ namespace : str | None = None
396+ sdk_version : str | None = None
397+ build_id : str | None = None
398+ status : str | None = None
399+ max_concurrent_workflow_tasks : int | None = None
400+ max_concurrent_activity_tasks : int | None = None
401+ supported_workflow_types : list [str ] | None = None
402+ supported_activity_types : list [str ] | None = None
403+ last_heartbeat_at : str | None = None
404+ registered_at : str | None = None
405+ updated_at : str | None = None
406+ raw : dict [str , Any ] | None = None
407+
408+ @classmethod
409+ def from_dict (cls , data : dict [str , Any ], * , worker_id : str | None = None ) -> WorkerDescription :
410+ workflow_types = data .get ("supported_workflow_types" )
411+ activity_types = data .get ("supported_activity_types" )
412+
413+ return cls (
414+ worker_id = data .get ("worker_id" , worker_id or "" ),
415+ task_queue = data .get ("task_queue" ),
416+ runtime = data .get ("runtime" ),
417+ namespace = data .get ("namespace" ),
418+ sdk_version = data .get ("sdk_version" ),
419+ build_id = data .get ("build_id" ),
420+ status = data .get ("status" ),
421+ max_concurrent_workflow_tasks = data .get ("max_concurrent_workflow_tasks" ),
422+ max_concurrent_activity_tasks = data .get ("max_concurrent_activity_tasks" ),
423+ supported_workflow_types = workflow_types if isinstance (workflow_types , list ) else None ,
424+ supported_activity_types = activity_types if isinstance (activity_types , list ) else None ,
425+ last_heartbeat_at = data .get ("last_heartbeat_at" ),
426+ registered_at = data .get ("registered_at" ),
427+ updated_at = data .get ("updated_at" ),
428+ raw = data ,
429+ )
430+
431+
432+ @dataclass
433+ class WorkerList :
434+ """Registered worker roster for one namespace."""
435+
436+ namespace : str | None
437+ workers : list [WorkerDescription ]
438+
439+
386440@dataclass
387441class ScheduleSpec :
388442 """Calendar or interval rules for a scheduled workflow."""
@@ -1063,6 +1117,70 @@ async def delete_search_attribute(self, name: str) -> dict[str, Any]:
10631117 )
10641118 return data
10651119
1120+ # ── Workers ───────────────────────────────────────────────────────
1121+ async def list_workers (
1122+ self ,
1123+ * ,
1124+ task_queue : str | None = None ,
1125+ status : str | None = None ,
1126+ ) -> WorkerList :
1127+ """List registered workers in the current namespace."""
1128+ params : dict [str , str ] = {}
1129+ if task_queue is not None :
1130+ params ["task_queue" ] = task_queue
1131+ if status is not None :
1132+ params ["status" ] = status
1133+
1134+ path = "/workers"
1135+ if params :
1136+ path = f"{ path } ?{ urlencode (params )} "
1137+
1138+ data = await self ._request ("GET" , path )
1139+ if not isinstance (data , dict ):
1140+ raise ServerError (
1141+ 200 ,
1142+ {
1143+ "reason" : "invalid_worker_response" ,
1144+ "message" : f"expected JSON object, got { type (data ).__name__ } " ,
1145+ },
1146+ )
1147+ items = data .get ("workers" , [])
1148+
1149+ return WorkerList (
1150+ namespace = data .get ("namespace" ),
1151+ workers = [
1152+ WorkerDescription .from_dict (item )
1153+ for item in items
1154+ if isinstance (item , dict )
1155+ ],
1156+ )
1157+
1158+ async def describe_worker (self , worker_id : str ) -> WorkerDescription :
1159+ """Return runtime, capacity, heartbeat, and type support for one worker."""
1160+ data = await self ._request ("GET" , f"/workers/{ quote (worker_id , safe = '' )} " , context = worker_id )
1161+ if not isinstance (data , dict ):
1162+ raise ServerError (
1163+ 200 ,
1164+ {
1165+ "reason" : "invalid_worker_response" ,
1166+ "message" : f"expected JSON object, got { type (data ).__name__ } " ,
1167+ },
1168+ )
1169+ return WorkerDescription .from_dict (data , worker_id = worker_id )
1170+
1171+ async def deregister_worker (self , worker_id : str ) -> dict [str , Any ]:
1172+ """Remove a stale or retired worker from the server roster."""
1173+ data = await self ._request ("DELETE" , f"/workers/{ quote (worker_id , safe = '' )} " , context = worker_id )
1174+ if not isinstance (data , dict ):
1175+ raise ServerError (
1176+ 200 ,
1177+ {
1178+ "reason" : "invalid_worker_response" ,
1179+ "message" : f"expected JSON object, got { type (data ).__name__ } " ,
1180+ },
1181+ )
1182+ return data
1183+
10661184 # ── Workflows ──────────────────────────────────────────────────────
10671185 async def start_workflow (
10681186 self ,
@@ -1701,6 +1819,7 @@ async def register_worker(
17011819 max_concurrent_activity_tasks : int | None = None ,
17021820 runtime : str = "python" ,
17031821 sdk_version : str | None = None ,
1822+ build_id : str | None = None ,
17041823 ) -> Any :
17051824 """Register this process with the server as a worker for ``task_queue``.
17061825
@@ -1725,6 +1844,8 @@ async def register_worker(
17251844 }
17261845 if workflow_definition_fingerprints is not None :
17271846 body ["workflow_definition_fingerprints" ] = workflow_definition_fingerprints
1847+ if build_id is not None :
1848+ body ["build_id" ] = build_id
17281849 if max_concurrent_workflow_tasks is not None :
17291850 body ["max_concurrent_workflow_tasks" ] = max_concurrent_workflow_tasks
17301851 if max_concurrent_activity_tasks is not None :
0 commit comments