Download src/sr_harness/runtime/interaction_manager.py from YuMeow/SRHarness: direct link, hf CLI and curl.
- Browser
- Download file 23.5 kB
-
https://huggingface.co/spaces/YuMeow/SRHarness/resolve/main/src/sr_harness/runtime/interaction_manager.py
- Command line
-
hf download hf://spaces/YuMeow/SRHarness/src/sr_harness/runtime/interaction_manager.py
-
curl -L -o interaction_manager.py https://huggingface.co/spaces/YuMeow/SRHarness/resolve/main/src/sr_harness/runtime/interaction_manager.py
23.5 kB
| """Thread-safe interaction state and event streams for one interactive agent.""" | |
| from __future__ import annotations | |
| from itertools import islice | |
| import threading | |
| import time | |
| import uuid | |
| from collections import deque | |
| from contextlib import contextmanager | |
| from dataclasses import dataclass | |
| from typing import Any, Callable, Iterator, Literal, Mapping, TypeAlias, cast | |
| from ..core import json_value | |
| InteractionState: TypeAlias = Literal[ | |
| "idle", | |
| "running", | |
| "pausing", | |
| "interrupting", | |
| "paused", | |
| ] | |
| InteractionAction: TypeAlias = Literal["message", "pause", "force_pause"] | |
| SRInteractionAction: TypeAlias = Literal[ | |
| "message", | |
| "pause", | |
| "force_pause", | |
| "next_c", | |
| "next_r", | |
| ] | |
| InteractionEventKind: TypeAlias = Literal[ | |
| "prompt_added", | |
| "context", | |
| "assistant_started", | |
| "assistant_delta", | |
| "assistant_completed", | |
| "assistant_failed", | |
| "tool_started", | |
| "tool_completed", | |
| "settings_applied", | |
| "settings_failed", | |
| "workspace_changed", | |
| "execution_completed", | |
| "execution_failed", | |
| "command_received", | |
| "state_changed", | |
| "search_position_changed", | |
| "topk_updated", | |
| ] | |
| class PendingMessage: | |
| """A user message waiting to be inserted at an agent boundary.""" | |
| content: str | |
| created_at: float | |
| class _CancellationSignal: | |
| """Read-only Event-like view of the manager's interruption state.""" | |
| def __init__(self, manager: "InteractionManager") -> None: | |
| self._manager = manager | |
| def is_set(self) -> bool: | |
| return self._manager.is_interrupting | |
| class InteractionManager: | |
| """Own the controls and observable event stream of exactly one agent. | |
| Args: | |
| event_capacity: Maximum number of retained timeline events. | |
| """ | |
| _DELTA_KIND = "assistant_delta" | |
| _TERMINAL_KINDS = {"assistant_completed", "assistant_failed"} | |
| _EVENT_KINDS = { | |
| "prompt_added", | |
| "context", | |
| "assistant_started", | |
| "assistant_delta", | |
| "assistant_completed", | |
| "assistant_failed", | |
| "tool_started", | |
| "tool_completed", | |
| "settings_applied", | |
| "settings_failed", | |
| "workspace_changed", | |
| "execution_completed", | |
| "execution_failed", | |
| "command_received", | |
| "state_changed", | |
| "search_position_changed", | |
| "topk_updated", | |
| } | |
| _STATE_FIELDS = { | |
| "version", | |
| "state", | |
| "messages", | |
| "events", | |
| "next_sequence", | |
| "discarded_through", | |
| "event_capacity", | |
| } | |
| def __init__(self, *, event_capacity: int = 1000) -> None: | |
| if event_capacity < 1: | |
| raise ValueError("event_capacity must be positive") | |
| self._condition = threading.Condition(threading.RLock()) | |
| self._state: InteractionState = "idle" | |
| self._messages: deque[PendingMessage] = deque() | |
| self._events: deque[dict[str, Any]] = deque(maxlen=event_capacity) | |
| self._next_sequence = 1 | |
| self._discarded_through = 0 | |
| self._cancel_current: Callable[[], str | None] | None = None | |
| self._cancellation_signal = _CancellationSignal(self) | |
| def state(self) -> InteractionState: | |
| """Return the authoritative execution state. | |
| Returns: | |
| Current interaction state. | |
| """ | |
| with self._condition: | |
| return self._state | |
| def is_interrupting(self) -> bool: | |
| """Return whether the active operation should abort promptly. | |
| Returns: | |
| Whether a force-pause request is interrupting active work. | |
| """ | |
| return self.state == "interrupting" | |
| def cancellation_signal(self) -> _CancellationSignal: | |
| """Expose a read-only Event-like cancellation adapter to tools. | |
| Returns: | |
| Object exposing ``is_set()`` for cancellation-aware tools. | |
| """ | |
| return self._cancellation_signal | |
| def status(self) -> dict[str, Any]: | |
| """Return a serializable control snapshot. | |
| Returns: | |
| Current state, pending-input counts, event cursor, and server time. | |
| """ | |
| with self._condition: | |
| return { | |
| "interaction_state": self._state, | |
| "paused": self._state in {"pausing", "interrupting", "paused"}, | |
| "waiting_at_boundary": self._state == "paused", | |
| "force_pause_requested": self._state == "interrupting", | |
| "pending_messages": len(self._messages), | |
| "last_event_seq": self._next_sequence - 1, | |
| "server_time": time.time(), | |
| } | |
| def command(self, action: InteractionAction, message: str | None = None) -> dict[str, Any]: | |
| """Apply a user control command to this agent. | |
| Args: | |
| action: Message, pause, or force-pause command. | |
| message: User guidance required by the ``message`` action. | |
| Returns: | |
| Control snapshot after applying the command. | |
| Raises: | |
| ValueError: If the action is invalid or a message is empty. | |
| """ | |
| if action not in {"message", "pause", "force_pause"}: | |
| raise ValueError("action must be message, pause, or force_pause") | |
| cancel: Callable[[], str | None] | None = None | |
| with self._condition: | |
| if action == "message": | |
| content = (message or "").strip() | |
| if not content: | |
| raise ValueError("message command requires non-empty message") | |
| self._messages.append(PendingMessage(content, time.time())) | |
| elif action == "pause": | |
| if self._state == "running": | |
| self._set_state_locked("pausing") | |
| elif self._state in {"running", "pausing"}: | |
| self._set_state_locked("interrupting") | |
| cancel = self._cancel_current | |
| self._condition.notify_all() | |
| cancellation_error = cancel() if cancel is not None else None | |
| with self._condition: | |
| if cancellation_error and self._state == "interrupting": | |
| self._set_state_locked("pausing") | |
| if action != "message": | |
| payload = {"action": action} | |
| if cancellation_error: | |
| payload["error"] = cancellation_error | |
| self._publish_event_locked("command_received", payload) | |
| result = self.status() | |
| if cancellation_error: | |
| result["cancellation_error"] = cancellation_error | |
| return result | |
| def request_pause(self) -> None: | |
| """Request a quiet pause, such as after a tool-free response.""" | |
| with self._condition: | |
| if self._state == "running": | |
| self._set_state_locked("pausing") | |
| self._condition.notify_all() | |
| def start_agent_execution(self) -> list[PendingMessage]: | |
| """Enter running state and consume messages that start this execution. | |
| Returns: | |
| Messages queued before execution started. | |
| Raises: | |
| RuntimeError: If the manager is not idle. | |
| """ | |
| with self._condition: | |
| if self._state != "idle": | |
| raise RuntimeError(f"cannot start agent execution while {self._state}") | |
| messages = self._consume_messages_locked() | |
| self._set_state_locked("running") | |
| return messages | |
| def finish_agent_execution(self) -> None: | |
| """Mark a naturally completed agent execution as idle. | |
| Raises: | |
| RuntimeError: If execution is paused at a boundary. | |
| """ | |
| with self._condition: | |
| if self._state == "paused": | |
| raise RuntimeError("cannot finish an execution while paused") | |
| self._cancel_current = None | |
| self._set_state_locked("idle") | |
| def wait(self) -> Iterator[list[PendingMessage]]: | |
| """Wait at a safe boundary and yield queued messages. | |
| Yields: | |
| Messages to insert before the next model request. | |
| Raises: | |
| RuntimeError: If no agent execution is active. | |
| """ | |
| with self._condition: | |
| if self._state == "running": | |
| messages = self._consume_messages_locked() | |
| paused = False | |
| elif self._state in {"pausing", "interrupting", "paused"}: | |
| if self._state != "paused": | |
| self._set_state_locked("paused") | |
| self._cancel_current = None | |
| while not self._has_boundary_input_locked(): | |
| self._condition.wait() | |
| messages = self._consume_messages_locked() | |
| paused = True | |
| else: | |
| raise RuntimeError(f"cannot wait at an agent boundary while {self._state}") | |
| try: | |
| yield messages | |
| finally: | |
| if paused: | |
| with self._condition: | |
| if self._state == "paused": | |
| self._set_state_locked("running") | |
| def cancellable(self, cancel: Callable[[], str | None]) -> Iterator[None]: | |
| """Register cancellation for the current blocking operation. | |
| Args: | |
| cancel: Callback that promptly interrupts the active operation. | |
| Yields: | |
| Control while the callback is registered. | |
| Raises: | |
| RuntimeError: If another cancellable operation is already active. | |
| """ | |
| with self._condition: | |
| if self._cancel_current is not None: | |
| raise RuntimeError("another cancellable operation is already active") | |
| self._cancel_current = cancel | |
| cancel_immediately = self._state == "interrupting" | |
| if cancel_immediately: | |
| cancellation_error = cancel() | |
| if cancellation_error: | |
| with self._condition: | |
| if self._state == "interrupting": | |
| self._set_state_locked("pausing") | |
| self._publish_event_locked( | |
| "command_received", | |
| {"action": "force_pause", "error": cancellation_error}, | |
| ) | |
| self._condition.notify_all() | |
| try: | |
| yield | |
| finally: | |
| with self._condition: | |
| if self._cancel_current is cancel: | |
| self._cancel_current = None | |
| def publish_event( | |
| self, kind: InteractionEventKind, payload: Mapping[str, Any] | |
| ) -> dict[str, Any]: | |
| """Append one observable event to this agent's timeline. | |
| Args: | |
| kind: Supported event kind. | |
| payload: JSON-serializable event data. | |
| Returns: | |
| Stored event with sequence, identifier, and timestamp. | |
| Raises: | |
| ValueError: If ``kind`` is unsupported. | |
| """ | |
| if kind not in self._EVENT_KINDS: | |
| raise ValueError(f"unsupported interaction event kind: {kind}") | |
| with self._condition: | |
| return self._publish_event_locked(kind, payload) | |
| def get_recent_events(self, after_sequence: int = 0, *, limit: int | None = None) -> dict[str, Any]: | |
| """Return retained events newer than a consumer-owned sequence cursor. | |
| Args: | |
| after_sequence: Last sequence already consumed by the caller. | |
| limit: Maximum events per batch; omitted for an unlimited internal snapshot. | |
| Returns: | |
| Event batch and cursor-reset or truncation metadata. | |
| """ | |
| with self._condition: | |
| latest_sequence = self._next_sequence - 1 | |
| cursor_reset = after_sequence > latest_sequence | |
| effective_sequence = 0 if cursor_reset else after_sequence | |
| return { | |
| "events": [ | |
| dict(event) for event in islice( | |
| (item for item in self._events if item["seq"] > effective_sequence), | |
| limit, | |
| ) | |
| ], | |
| "truncated": effective_sequence < self._discarded_through, | |
| "discarded_through": self._discarded_through, | |
| "cursor_reset": cursor_reset, | |
| "server_time": time.time(), | |
| } | |
| def export_state(self) -> dict[str, Any]: | |
| """Return the durable portion of this manager's state. | |
| Returns: | |
| Versioned interaction snapshot without runtime locks or callbacks. | |
| """ | |
| with self._condition: | |
| return { | |
| "version": 1, | |
| "state": self._state, | |
| "messages": [ | |
| {"content": message.content, "created_at": message.created_at} | |
| for message in self._messages | |
| ], | |
| "events": [dict(event) for event in self._events], | |
| "next_sequence": self._next_sequence, | |
| "discarded_through": self._discarded_through, | |
| "event_capacity": self._events.maxlen, | |
| } | |
| def restore_state(self, snapshot: Mapping[str, Any]) -> bool: | |
| """Restore durable state and interrupt work that died with the process. | |
| Runtime callbacks and locks are deliberately never restored. | |
| Args: | |
| snapshot: State returned by :meth:`export_state`. | |
| Returns: | |
| Whether in-flight work was converted into an interruption event. | |
| """ | |
| with self._condition: | |
| if set(snapshot) != self._STATE_FIELDS: | |
| raise ValueError("Persisted interaction state does not match the current schema") | |
| if snapshot["version"] != 1: | |
| raise ValueError("Unsupported InteractionManager snapshot version") | |
| previous = snapshot["state"] | |
| if previous not in {"idle", "running", "pausing", "interrupting", "paused"}: | |
| raise ValueError(f"Unknown persisted interaction state: {previous!r}") | |
| capacity = snapshot["event_capacity"] | |
| if not isinstance(capacity, int) or isinstance(capacity, bool) or capacity < 1: | |
| raise ValueError("Persisted event_capacity must be positive") | |
| events = snapshot["events"] | |
| if not isinstance(events, list) or any( | |
| not isinstance(event, Mapping) for event in events | |
| ): | |
| raise TypeError("Persisted events must be a list of mappings") | |
| required_event_fields = {"seq", "id", "kind", "timestamp", "payload"} | |
| if any(set(event) != required_event_fields for event in events): | |
| raise ValueError("Persisted events must use the current event schema") | |
| if any(event["kind"] not in self._EVENT_KINDS for event in events): | |
| raise ValueError("Persisted events contain an unsupported event kind") | |
| if any( | |
| not isinstance(event["seq"], int) | |
| or isinstance(event["seq"], bool) | |
| or event["seq"] < 1 | |
| or not isinstance(event["id"], str) | |
| or not event["id"] | |
| or not isinstance(event["timestamp"], (int, float)) | |
| or isinstance(event["timestamp"], bool) | |
| or not isinstance(event["payload"], Mapping) | |
| for event in events | |
| ): | |
| raise TypeError("Persisted events contain invalid field values") | |
| if [event["seq"] for event in events] != sorted({event["seq"] for event in events}): | |
| raise ValueError("Persisted event sequences must be unique and increasing") | |
| self._events = deque( | |
| (dict(event) for event in events), | |
| maxlen=capacity, | |
| ) | |
| messages = snapshot["messages"] | |
| if not isinstance(messages, list) or any( | |
| not isinstance(item, Mapping) for item in messages | |
| ): | |
| raise TypeError("Persisted messages must be a list of mappings") | |
| if any(set(item) != {"content", "created_at"} for item in messages): | |
| raise ValueError("Persisted messages must use the current message schema") | |
| if any( | |
| not isinstance(item["content"], str) | |
| or not item["content"] | |
| or not isinstance(item["created_at"], (int, float)) | |
| or isinstance(item["created_at"], bool) | |
| for item in messages | |
| ): | |
| raise TypeError("Persisted messages contain invalid field values") | |
| self._messages = deque( | |
| PendingMessage(item["content"], item["created_at"]) for item in messages | |
| ) | |
| discarded_through = snapshot["discarded_through"] | |
| if ( | |
| not isinstance(discarded_through, int) | |
| or isinstance(discarded_through, bool) | |
| or discarded_through < 0 | |
| ): | |
| raise ValueError("Persisted discarded_through must be a non-negative integer") | |
| self._discarded_through = discarded_through | |
| highest_sequence = max( | |
| (event["seq"] for event in self._events), | |
| default=0, | |
| ) | |
| next_sequence = snapshot["next_sequence"] | |
| if ( | |
| not isinstance(next_sequence, int) | |
| or isinstance(next_sequence, bool) | |
| or next_sequence <= highest_sequence | |
| or next_sequence <= discarded_through | |
| ): | |
| raise ValueError("Persisted next_sequence is inconsistent with retained events") | |
| self._next_sequence = next_sequence | |
| interrupted = previous in {"running", "pausing", "interrupting", "paused"} | |
| self._state = "idle" | |
| self._cancel_current = None | |
| if interrupted: | |
| self._publish_event_locked( | |
| "command_received", | |
| {"action": "force_pause", "restored_after_restart": True}, | |
| ) | |
| self._publish_event_locked( | |
| "state_changed", | |
| {"previous": previous, "current": "idle", "interrupted": True}, | |
| ) | |
| return interrupted | |
| def _consume_messages_locked(self) -> list[PendingMessage]: | |
| messages = list(self._messages) | |
| self._messages.clear() | |
| return messages | |
| def _has_boundary_input_locked(self) -> bool: | |
| return bool(self._messages) | |
| def _set_state_locked(self, state: InteractionState) -> None: | |
| previous = self._state | |
| if previous == state: | |
| return | |
| self._state = state | |
| self._publish_event_locked("state_changed", {"previous": previous, "current": state}) | |
| def _publish_event_locked( | |
| self, kind: InteractionEventKind, payload: Mapping[str, Any] | |
| ) -> dict[str, Any]: | |
| payload_dict = cast(dict[str, Any], json_value(dict(payload))) | |
| sequence = self._next_sequence | |
| self._next_sequence += 1 | |
| event = { | |
| "seq": sequence, | |
| "id": uuid.uuid4().hex, | |
| "kind": kind, | |
| "timestamp": time.time(), | |
| "payload": payload_dict, | |
| } | |
| response_id = payload_dict.get("response_id") | |
| if response_id and kind == self._DELTA_KIND: | |
| self._remove_stream_events_locked(response_id, {kind}) | |
| elif response_id and kind in self._TERMINAL_KINDS: | |
| self._remove_stream_events_locked(response_id, {self._DELTA_KIND}) | |
| if len(self._events) == self._events.maxlen and self._events: | |
| self._discarded_through = max(self._discarded_through, self._events[0]["seq"]) | |
| self._events.append(event) | |
| return dict(event) | |
| def _remove_stream_events_locked(self, response_id: str, kinds: set[str]) -> None: | |
| self._events = deque( | |
| ( | |
| event | |
| for event in self._events | |
| if not ( | |
| event["kind"] in kinds | |
| and event.get("payload", {}).get("response_id") == response_id | |
| ) | |
| ), | |
| maxlen=self._events.maxlen, | |
| ) | |
| class SRInteractionManager(InteractionManager): | |
| """Interaction manager with symbolic-regression branch commands.""" | |
| _STATE_FIELDS = InteractionManager._STATE_FIELDS | {"search_transitions"} | |
| def __init__(self, *, event_capacity: int = 1000) -> None: | |
| super().__init__(event_capacity=event_capacity) | |
| self._search_transitions: deque[Literal["next_c", "next_r"]] = deque() | |
| def command(self, action: SRInteractionAction, message: str | None = None) -> dict[str, Any]: | |
| """Apply a common command or queue an SR branch transition. | |
| Args: | |
| action: Common interaction action or ``next_c``/``next_r``. | |
| message: User guidance for a ``message`` action. | |
| Returns: | |
| Control snapshot after applying the command. | |
| """ | |
| if action not in {"next_c", "next_r"}: | |
| return super().command(cast(InteractionAction, action), message) | |
| with self._condition: | |
| self._search_transitions.clear() | |
| self._search_transitions.append(action) | |
| self._publish_event_locked("command_received", {"action": action}) | |
| self._condition.notify_all() | |
| return self.status() | |
| def consume_search_transition(self) -> Literal["next_c", "next_r"] | None: | |
| """Consume the newest queued branch transition. | |
| Returns: | |
| Queued transition, or ``None`` when no transition is pending. | |
| """ | |
| with self._condition: | |
| return self._search_transitions.popleft() if self._search_transitions else None | |
| def _has_boundary_input_locked(self) -> bool: | |
| return bool(self._messages or self._search_transitions) | |
| def status(self) -> dict[str, Any]: | |
| """Include the pending SR branch transition in the control snapshot. | |
| Returns: | |
| Common control snapshot with ``pending_transition``. | |
| """ | |
| with self._condition: | |
| status = super().status() | |
| status["pending_transition"] = ( | |
| self._search_transitions[-1] if self._search_transitions else None | |
| ) | |
| return status | |
| def export_state(self) -> dict[str, Any]: | |
| """Include pending search transitions in the durable snapshot. | |
| Returns: | |
| Versioned interaction snapshot with symbolic-search transitions. | |
| """ | |
| with self._condition: | |
| state = super().export_state() | |
| state["search_transitions"] = list(self._search_transitions) | |
| return state | |
| def restore_state(self, snapshot: Mapping[str, Any]) -> bool: | |
| """Restore common state plus queued symbolic-search transitions. | |
| Args: | |
| snapshot: State returned by :meth:`export_state`. | |
| Returns: | |
| Whether in-flight work was converted into an interruption event. | |
| """ | |
| interrupted = super().restore_state(snapshot) | |
| with self._condition: | |
| transitions = snapshot["search_transitions"] | |
| if not isinstance(transitions, list) or any( | |
| action not in {"next_c", "next_r"} for action in transitions | |
| ): | |
| raise ValueError("Persisted search_transitions contains an invalid action") | |
| self._search_transitions = deque(transitions) | |
| return interrupted | |