""" todo_registry.py — Registry for TodoUntil watchers. Tracks pending "todos" that agents have issued. When a matching message appears on the thread, the watcher's eyebrow is raised. Subsequent messages to the issuing agent include a nudge until the agent explicitly closes the todo with . Design: - Observer pattern, not interceptor — messages flow normally - Thread-scoped — watchers only see messages on their thread - Persistent nudge — keeps nagging until explicit close - Cheap matching — payload type + optional source filter Usage: registry = get_todo_registry() # Agent issues TodoUntil watcher_id = registry.register( thread_id="...", issuer="greeter", wait_for="ShoutedResponse", from_listener="shouter", # optional ) # On each message, check for matches registry.check(message_state) # When dispatching to an agent, get raised eyebrows raised = registry.get_raised_for(thread_id, agent_name) # Agent closes todo registry.close(watcher_id) """ from dataclasses import dataclass, field from typing import Dict, List, Any, Optional import uuid import threading @dataclass class TodoWatcher: """A pending todo that watches for a condition on a thread.""" id: str # Unique ID for explicit close thread_id: str # Thread being watched issuer: str # Agent that issued the todo (who to nag) wait_for: str # Payload type to match (e.g., "ShoutedResponse") from_listener: Optional[str] = None # Optional: must be from specific source description: str = "" # Human-readable description of what we're waiting for eyebrow_raised: bool = False # True when condition appears satisfied triggered_by: Any = None # The payload that raised the eyebrow triggered_from: str = "" # Who sent the triggering message class TodoRegistry: """ Registry for TodoUntil watchers. Thread-safe. Singleton pattern via get_todo_registry(). """ def __init__(self): self._lock = threading.Lock() # thread_id -> list of watchers self._watchers: Dict[str, List[TodoWatcher]] = {} # watcher_id -> watcher (for fast lookup on close) self._by_id: Dict[str, TodoWatcher] = {} def register( self, thread_id: str, issuer: str, wait_for: str, from_listener: Optional[str] = None, description: str = "", ) -> str: """ Register a new todo watcher. Returns the watcher ID for explicit close. """ watcher_id = str(uuid.uuid4()) watcher = TodoWatcher( id=watcher_id, thread_id=thread_id, issuer=issuer, wait_for=wait_for.lower(), # Normalize for matching from_listener=from_listener.lower() if from_listener else None, description=description, ) with self._lock: if thread_id not in self._watchers: self._watchers[thread_id] = [] self._watchers[thread_id].append(watcher) self._by_id[watcher_id] = watcher return watcher_id def check(self, thread_id: str, payload_type: str, from_id: str, payload: Any = None) -> List[TodoWatcher]: """ Check if any watchers on this thread match the incoming message. Raises eyebrows on matching watchers. Returns list of newly raised. """ newly_raised = [] payload_type_lower = payload_type.lower() from_id_lower = from_id.lower() if from_id else "" with self._lock: watchers = self._watchers.get(thread_id, []) for watcher in watchers: if watcher.eyebrow_raised: continue # Already raised # Check payload type match if watcher.wait_for not in payload_type_lower: continue # Check optional from_listener filter if watcher.from_listener and watcher.from_listener != from_id_lower: continue # Match! Raise the eyebrow watcher.eyebrow_raised = True watcher.triggered_by = payload watcher.triggered_from = from_id newly_raised.append(watcher) return newly_raised def get_raised_for(self, thread_id: str, agent: str) -> List[TodoWatcher]: """ Get all raised eyebrows for this agent on this thread. These are the todos that appear satisfied and should be nagged about. """ agent_lower = agent.lower() with self._lock: watchers = self._watchers.get(thread_id, []) return [w for w in watchers if w.issuer.lower() == agent_lower and w.eyebrow_raised] def get_pending_for(self, thread_id: str, agent: str) -> List[TodoWatcher]: """ Get all pending (not yet raised) todos for this agent on this thread. Useful for showing the agent what it's still waiting for. """ agent_lower = agent.lower() with self._lock: watchers = self._watchers.get(thread_id, []) return [w for w in watchers if w.issuer.lower() == agent_lower and not w.eyebrow_raised] def close(self, watcher_id: str) -> bool: """ Close a todo by ID. Returns True if found and removed, False if not found. """ with self._lock: watcher = self._by_id.pop(watcher_id, None) if watcher is None: return False thread_watchers = self._watchers.get(watcher.thread_id, []) try: thread_watchers.remove(watcher) except ValueError: pass # Clean up empty thread entries if not thread_watchers: self._watchers.pop(watcher.thread_id, None) return True def close_all_for_thread(self, thread_id: str) -> int: """ Close all watchers for a thread (e.g., when thread ends). Returns count of watchers removed. """ with self._lock: watchers = self._watchers.pop(thread_id, []) for w in watchers: self._by_id.pop(w.id, None) return len(watchers) def format_nudge(self, watchers: List[TodoWatcher]) -> str: """ Format raised eyebrows as a nudge string for the LLM. Returns empty string if no raised eyebrows. """ if not watchers: return "" lines = ["[SYSTEM NOTE: The following todos appear complete:]"] for w in watchers: desc = f" ({w.description})" if w.description else "" lines.append(f" - Waiting for {w.wait_for}{desc}: received from {w.triggered_from}") lines.append(f" Close with: {w.id}") return "\n".join(lines) def clear(self): """Clear all watchers. Useful for testing.""" with self._lock: self._watchers.clear() self._by_id.clear() # ============================================================================ # Singleton # ============================================================================ _registry: Optional[TodoRegistry] = None _registry_lock = threading.Lock() def get_todo_registry() -> TodoRegistry: """Get the global TodoRegistry singleton.""" global _registry if _registry is None: with _registry_lock: if _registry is None: _registry = TodoRegistry() return _registry