Add task registry package
Introduces `TaskRegistry` and the supporting `TaskReadyData`, `TaskErrorData`, and `TaskRegistryEntry` dataclasses used to track local and remote tasks discovered through the bus.
This commit is contained in:
13
src/pipecat/registry/__init__.py
Normal file
13
src/pipecat/registry/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
#
|
||||
# Copyright (c) 2026, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
"""Agent registry for tracking discovered agents across runners."""
|
||||
|
||||
from pipecat.registry.registry import TaskRegistry
|
||||
|
||||
__all__ = [
|
||||
"TaskRegistry",
|
||||
]
|
||||
129
src/pipecat/registry/registry.py
Normal file
129
src/pipecat/registry/registry.py
Normal file
@@ -0,0 +1,129 @@
|
||||
#
|
||||
# Copyright (c) 2026, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
"""Shared registry for tracking known tasks across runners."""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Coroutine
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from pipecat.registry.types import TaskReadyData
|
||||
|
||||
WatchHandler = Callable[[TaskReadyData], Coroutine]
|
||||
|
||||
|
||||
class TaskRegistry:
|
||||
"""Tracks all known tasks across local and remote runners.
|
||||
|
||||
Owned by a runner and shared with its tasks. Organizes tasks into
|
||||
local (this runner) and remote (other runners) so they are easy to
|
||||
distinguish. Deduplication is built in: each task name is
|
||||
registered at most once.
|
||||
|
||||
Notifications use a targeted watch mechanism: call
|
||||
``watch(task_name, handler)`` to be notified when a specific task
|
||||
registers.
|
||||
"""
|
||||
|
||||
def __init__(self, runner_name: str):
|
||||
"""Initialize the TaskRegistry.
|
||||
|
||||
Args:
|
||||
runner_name: Name of the runner that owns this registry.
|
||||
"""
|
||||
self._runner_name = runner_name
|
||||
self._local_tasks: dict[str, TaskReadyData] = {}
|
||||
self._remote_tasks: dict[str, dict[str, TaskReadyData]] = defaultdict(dict)
|
||||
self._watches: dict[str, list[WatchHandler]] = defaultdict(list)
|
||||
|
||||
@property
|
||||
def runner_name(self) -> str:
|
||||
"""The name of the runner that owns this registry."""
|
||||
return self._runner_name
|
||||
|
||||
@property
|
||||
def local_tasks(self) -> list[str]:
|
||||
"""Names of tasks registered under this runner."""
|
||||
return list(self._local_tasks.keys())
|
||||
|
||||
@property
|
||||
def remote_tasks(self) -> list[str]:
|
||||
"""Names of tasks registered under remote runners."""
|
||||
result: list[str] = []
|
||||
for tasks in self._remote_tasks.values():
|
||||
result.extend(tasks.keys())
|
||||
return result
|
||||
|
||||
def get(self, task_name: str) -> TaskReadyData | None:
|
||||
"""Look up a registered task by name.
|
||||
|
||||
Args:
|
||||
task_name: The task name to look up.
|
||||
|
||||
Returns:
|
||||
The task's ``TaskReadyData``, or None if not found.
|
||||
"""
|
||||
if task_name in self._local_tasks:
|
||||
return self._local_tasks[task_name]
|
||||
for tasks in self._remote_tasks.values():
|
||||
if task_name in tasks:
|
||||
return tasks[task_name]
|
||||
return None
|
||||
|
||||
def __contains__(self, task_name: str) -> bool:
|
||||
return self.get(task_name) is not None
|
||||
|
||||
async def watch(self, task_name: str, handler: WatchHandler) -> None:
|
||||
"""Watch for a specific task's registration.
|
||||
|
||||
If the task is already registered, the handler fires immediately.
|
||||
|
||||
Args:
|
||||
task_name: The task name to watch for.
|
||||
handler: Async callable invoked with the task's data.
|
||||
"""
|
||||
self._watches[task_name].append(handler)
|
||||
existing = self.get(task_name)
|
||||
if existing:
|
||||
await handler(existing)
|
||||
|
||||
async def register(self, task_data: TaskReadyData) -> bool:
|
||||
"""Register a task. Returns True if the task was new.
|
||||
|
||||
If the task is already registered, this is a no-op and returns
|
||||
False. Otherwise the task is added and watchers are notified.
|
||||
|
||||
Args:
|
||||
task_data: Information about the task to register.
|
||||
|
||||
Returns:
|
||||
True if the task was newly registered, False if already known.
|
||||
"""
|
||||
is_local = task_data.runner == self._runner_name
|
||||
target = self._local_tasks if is_local else self._remote_tasks[task_data.runner]
|
||||
|
||||
if task_data.task_name in target:
|
||||
return False
|
||||
|
||||
# Warn if the same name exists on a different runner
|
||||
existing = self.get(task_data.task_name)
|
||||
if existing and existing.runner != task_data.runner:
|
||||
logger.warning(
|
||||
f"Task '{task_data.task_name}' registered on both "
|
||||
f"'{existing.runner}' and '{task_data.runner}'"
|
||||
)
|
||||
|
||||
target[task_data.task_name] = task_data
|
||||
locality = "local" if is_local else task_data.runner
|
||||
logger.debug(f"Task '{task_data.task_name}' ready ({locality})")
|
||||
await self._notify(task_data)
|
||||
return True
|
||||
|
||||
async def _notify(self, task_data: TaskReadyData) -> None:
|
||||
"""Notify watchers of a new registration."""
|
||||
for handler in self._watches.get(task_data.task_name, []):
|
||||
await handler(task_data)
|
||||
54
src/pipecat/registry/types.py
Normal file
54
src/pipecat/registry/types.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#
|
||||
# Copyright (c) 2026, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
"""Shared types for the task registry."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskRegistryEntry:
|
||||
"""Information about a task in a registry snapshot.
|
||||
|
||||
Parameters:
|
||||
name: The task's name.
|
||||
parent: Name of the parent task, or None for root tasks.
|
||||
active: Whether the task is currently active.
|
||||
bridged: Whether the task is bridged.
|
||||
started_at: Unix timestamp when the task became ready.
|
||||
"""
|
||||
|
||||
name: str
|
||||
parent: str | None = None
|
||||
active: bool = False
|
||||
bridged: bool = False
|
||||
started_at: float | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskReadyData:
|
||||
"""Information about a registered task.
|
||||
|
||||
Parameters:
|
||||
task_name: The name of the task.
|
||||
runner: The name of the runner managing this task.
|
||||
"""
|
||||
|
||||
task_name: str
|
||||
runner: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskErrorData:
|
||||
"""Information about a task error.
|
||||
|
||||
Parameters:
|
||||
task_name: The name of the task that errored.
|
||||
error: Description of the error.
|
||||
"""
|
||||
|
||||
task_name: str
|
||||
error: str
|
||||
Reference in New Issue
Block a user