- harness/es.py: append-only event store + проекции (образец — кооп-портал) - harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу - harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы - harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе (глобальный asyncio-рантайм Strands не переживает 2+ create_harness) - cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом) - 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
136 lines
5.5 KiB
Python
136 lines
5.5 KiB
Python
"""Tool watcher for hot reloading tools during development.
|
|
|
|
This module provides functionality to watch tool directories for changes and automatically reload tools when they are
|
|
modified.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from watchdog.events import FileSystemEventHandler
|
|
from watchdog.observers import Observer
|
|
|
|
from .registry import ToolRegistry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ToolWatcher:
|
|
"""Watches tool directories for changes and reloads tools when they are modified."""
|
|
|
|
# This class uses class variables for the observer and handlers because watchdog allows only one Observer instance
|
|
# per directory. Using class variables ensures that all ToolWatcher instances share a single Observer, with the
|
|
# MasterChangeHandler routing file system events to the appropriate individual handlers for each registry. This
|
|
# design pattern avoids conflicts when multiple tool registries are watching the same directories.
|
|
|
|
_shared_observer = None
|
|
_watched_dirs: set[str] = set()
|
|
_observer_started = False
|
|
_registry_handlers: dict[str, dict[int, "ToolWatcher.ToolChangeHandler"]] = {}
|
|
|
|
def __init__(self, tool_registry: ToolRegistry) -> None:
|
|
"""Initialize a tool watcher for the given tool registry.
|
|
|
|
Args:
|
|
tool_registry: The tool registry to report changes.
|
|
"""
|
|
self.tool_registry = tool_registry
|
|
self.start()
|
|
|
|
class ToolChangeHandler(FileSystemEventHandler):
|
|
"""Handler for tool file changes."""
|
|
|
|
def __init__(self, tool_registry: ToolRegistry) -> None:
|
|
"""Initialize a tool change handler.
|
|
|
|
Args:
|
|
tool_registry: The tool registry to update when tools change.
|
|
"""
|
|
self.tool_registry = tool_registry
|
|
|
|
def on_modified(self, event: Any) -> None:
|
|
"""Reload tool if file modification detected.
|
|
|
|
Args:
|
|
event: The file system event that triggered this handler.
|
|
"""
|
|
if event.src_path.endswith(".py"):
|
|
tool_path = Path(event.src_path)
|
|
tool_name = tool_path.stem
|
|
|
|
if tool_name not in ["__init__"]:
|
|
logger.debug("tool_name=<%s> | tool change detected", tool_name)
|
|
try:
|
|
self.tool_registry.reload_tool(tool_name)
|
|
except Exception as e:
|
|
logger.error("tool_name=<%s>, exception=<%s> | failed to reload tool", tool_name, str(e))
|
|
|
|
class MasterChangeHandler(FileSystemEventHandler):
|
|
"""Master handler that delegates to all registered handlers."""
|
|
|
|
def __init__(self, dir_path: str) -> None:
|
|
"""Initialize a master change handler for a specific directory.
|
|
|
|
Args:
|
|
dir_path: The directory path to watch.
|
|
"""
|
|
self.dir_path = dir_path
|
|
|
|
def on_modified(self, event: Any) -> None:
|
|
"""Delegate file modification events to all registered handlers.
|
|
|
|
Args:
|
|
event: The file system event that triggered this handler.
|
|
"""
|
|
if event.src_path.endswith(".py"):
|
|
tool_path = Path(event.src_path)
|
|
tool_name = tool_path.stem
|
|
|
|
if tool_name not in ["__init__"]:
|
|
# Delegate to all registered handlers for this directory
|
|
for handler in ToolWatcher._registry_handlers.get(self.dir_path, {}).values():
|
|
try:
|
|
handler.on_modified(event)
|
|
except Exception as e:
|
|
logger.error("exception=<%s> | handler error", str(e))
|
|
|
|
def start(self) -> None:
|
|
"""Start watching all tools directories for changes."""
|
|
# Initialize shared observer if not already done
|
|
if ToolWatcher._shared_observer is None:
|
|
ToolWatcher._shared_observer = Observer()
|
|
|
|
# Create handler for this instance
|
|
self.tool_change_handler = self.ToolChangeHandler(self.tool_registry)
|
|
registry_id = id(self.tool_registry)
|
|
|
|
# Get tools directories to watch
|
|
tools_dirs = self.tool_registry.get_tools_dirs()
|
|
|
|
for tools_dir in tools_dirs:
|
|
dir_str = str(tools_dir)
|
|
|
|
# Initialize the registry handlers dict for this directory if needed
|
|
if dir_str not in ToolWatcher._registry_handlers:
|
|
ToolWatcher._registry_handlers[dir_str] = {}
|
|
|
|
# Store this handler with its registry id
|
|
ToolWatcher._registry_handlers[dir_str][registry_id] = self.tool_change_handler
|
|
|
|
# Schedule or update the master handler for this directory
|
|
if dir_str not in ToolWatcher._watched_dirs:
|
|
# First time seeing this directory, create a master handler
|
|
master_handler = self.MasterChangeHandler(dir_str)
|
|
ToolWatcher._shared_observer.schedule(master_handler, dir_str, recursive=False)
|
|
ToolWatcher._watched_dirs.add(dir_str)
|
|
logger.debug("tools_dir=<%s> | started watching tools directory", tools_dir)
|
|
else:
|
|
# Directory already being watched, just log it
|
|
logger.debug("tools_dir=<%s> | directory already being watched", tools_dir)
|
|
|
|
# Start the observer if not already started
|
|
if not ToolWatcher._observer_started:
|
|
ToolWatcher._shared_observer.start()
|
|
ToolWatcher._observer_started = True
|
|
logger.debug("tool directory watching initialized")
|